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
2863pub(crate) fn apply_op_motion_key<H: crate::types::Host>(
2877 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2878 op: Operator,
2879 motion_key: char,
2880 total_count: usize,
2881) {
2882 let input = Input {
2883 key: Key::Char(motion_key),
2884 ctrl: false,
2885 alt: false,
2886 shift: false,
2887 };
2888 let Some(motion) = parse_motion(&input) else {
2889 return;
2890 };
2891 let motion = match motion {
2892 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2893 Some((ch, forward, till)) => Motion::Find {
2894 ch,
2895 forward: if reverse { !forward } else { forward },
2896 till,
2897 },
2898 None => return,
2899 },
2900 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
2902 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
2903 m => m,
2904 };
2905 apply_op_with_motion(ed, op, &motion, total_count);
2906 if let Motion::Find { ch, forward, till } = &motion {
2907 ed.vim.last_find = Some((*ch, *forward, *till));
2908 }
2909 if !ed.vim.replaying && op_is_change(op) {
2910 ed.vim.last_change = Some(LastChange::OpMotion {
2911 op,
2912 motion,
2913 count: total_count,
2914 inserted: None,
2915 });
2916 }
2917}
2918
2919pub(crate) fn apply_op_double<H: crate::types::Host>(
2922 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2923 op: Operator,
2924 total_count: usize,
2925) {
2926 execute_line_op(ed, op, total_count);
2927 if !ed.vim.replaying {
2928 ed.vim.last_change = Some(LastChange::LineOp {
2929 op,
2930 count: total_count,
2931 inserted: None,
2932 });
2933 }
2934}
2935
2936pub(crate) fn enter_op_text_obj<H: crate::types::Host>(
2939 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2940 op: Operator,
2941 count1: usize,
2942 inner: bool,
2943) {
2944 ed.vim.pending = Pending::OpTextObj { op, count1, inner };
2945}
2946
2947pub(crate) fn enter_op_g<H: crate::types::Host>(
2950 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2951 op: Operator,
2952 count1: usize,
2953) {
2954 ed.vim.pending = Pending::OpG { op, count1 };
2955}
2956
2957pub(crate) fn enter_op_find<H: crate::types::Host>(
2960 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2961 op: Operator,
2962 count1: usize,
2963 forward: bool,
2964 till: bool,
2965) {
2966 ed.vim.pending = Pending::OpFind {
2967 op,
2968 count1,
2969 forward,
2970 till,
2971 };
2972}
2973
2974fn handle_after_op<H: crate::types::Host>(
2975 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2976 input: Input,
2977 op: Operator,
2978 count1: usize,
2979) -> bool {
2980 if let Key::Char(d @ '0'..='9') = input.key
2982 && !input.ctrl
2983 && (d != '0' || ed.vim.count > 0)
2984 {
2985 ed.vim.count = ed.vim.count.saturating_mul(10) + (d as usize - '0' as usize);
2986 ed.vim.pending = Pending::Op { op, count1 };
2987 return true;
2988 }
2989
2990 if input.key == Key::Esc {
2992 ed.vim.count = 0;
2993 return true;
2994 }
2995
2996 let double_ch = match op {
3000 Operator::Delete => Some('d'),
3001 Operator::Change => Some('c'),
3002 Operator::Yank => Some('y'),
3003 Operator::Indent => Some('>'),
3004 Operator::Outdent => Some('<'),
3005 Operator::Uppercase => Some('U'),
3006 Operator::Lowercase => Some('u'),
3007 Operator::ToggleCase => Some('~'),
3008 Operator::Fold => None,
3009 Operator::Reflow => Some('q'),
3012 };
3013 if let Key::Char(c) = input.key
3014 && !input.ctrl
3015 && Some(c) == double_ch
3016 {
3017 let count2 = take_count(&mut ed.vim);
3018 let total = count1.max(1) * count2.max(1);
3019 execute_line_op(ed, op, total);
3020 if !ed.vim.replaying {
3021 ed.vim.last_change = Some(LastChange::LineOp {
3022 op,
3023 count: total,
3024 inserted: None,
3025 });
3026 }
3027 return true;
3028 }
3029
3030 if let Key::Char('i') | Key::Char('a') = input.key
3032 && !input.ctrl
3033 {
3034 let inner = matches!(input.key, Key::Char('i'));
3035 ed.vim.pending = Pending::OpTextObj { op, count1, inner };
3036 return true;
3037 }
3038
3039 if input.key == Key::Char('g') && !input.ctrl {
3041 ed.vim.pending = Pending::OpG { op, count1 };
3042 return true;
3043 }
3044
3045 if let Some((forward, till)) = find_entry(&input) {
3047 ed.vim.pending = Pending::OpFind {
3048 op,
3049 count1,
3050 forward,
3051 till,
3052 };
3053 return true;
3054 }
3055
3056 let count2 = take_count(&mut ed.vim);
3058 let total = count1.max(1) * count2.max(1);
3059 if let Some(motion) = parse_motion(&input) {
3060 let motion = match motion {
3061 Motion::FindRepeat { reverse } => match ed.vim.last_find {
3062 Some((ch, forward, till)) => Motion::Find {
3063 ch,
3064 forward: if reverse { !forward } else { forward },
3065 till,
3066 },
3067 None => return true,
3068 },
3069 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
3073 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
3074 m => m,
3075 };
3076 apply_op_with_motion(ed, op, &motion, total);
3077 if let Motion::Find { ch, forward, till } = &motion {
3078 ed.vim.last_find = Some((*ch, *forward, *till));
3079 }
3080 if !ed.vim.replaying && op_is_change(op) {
3081 ed.vim.last_change = Some(LastChange::OpMotion {
3082 op,
3083 motion,
3084 count: total,
3085 inserted: None,
3086 });
3087 }
3088 return true;
3089 }
3090
3091 true
3093}
3094
3095fn handle_op_after_g<H: crate::types::Host>(
3096 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3097 input: Input,
3098 op: Operator,
3099 count1: usize,
3100) -> bool {
3101 if input.ctrl {
3102 return true;
3103 }
3104 let count2 = take_count(&mut ed.vim);
3105 let total = count1.max(1) * count2.max(1);
3106 if matches!(
3110 op,
3111 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase
3112 ) {
3113 let op_char = match op {
3114 Operator::Uppercase => 'U',
3115 Operator::Lowercase => 'u',
3116 Operator::ToggleCase => '~',
3117 _ => unreachable!(),
3118 };
3119 if input.key == Key::Char(op_char) {
3120 execute_line_op(ed, op, total);
3121 if !ed.vim.replaying {
3122 ed.vim.last_change = Some(LastChange::LineOp {
3123 op,
3124 count: total,
3125 inserted: None,
3126 });
3127 }
3128 return true;
3129 }
3130 }
3131 let motion = match input.key {
3132 Key::Char('g') => Motion::FileTop,
3133 Key::Char('e') => Motion::WordEndBack,
3134 Key::Char('E') => Motion::BigWordEndBack,
3135 Key::Char('j') => Motion::ScreenDown,
3136 Key::Char('k') => Motion::ScreenUp,
3137 _ => return true,
3138 };
3139 apply_op_with_motion(ed, op, &motion, total);
3140 if !ed.vim.replaying && op_is_change(op) {
3141 ed.vim.last_change = Some(LastChange::OpMotion {
3142 op,
3143 motion,
3144 count: total,
3145 inserted: None,
3146 });
3147 }
3148 true
3149}
3150
3151fn handle_after_g<H: crate::types::Host>(
3152 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3153 input: Input,
3154) -> bool {
3155 let count = take_count(&mut ed.vim);
3156 if let Key::Char(ch) = input.key {
3159 apply_after_g(ed, ch, count);
3160 }
3161 true
3162}
3163
3164pub(crate) fn apply_after_g<H: crate::types::Host>(
3169 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3170 ch: char,
3171 count: usize,
3172) {
3173 match ch {
3174 'g' => {
3175 let pre = ed.cursor();
3177 if count > 1 {
3178 ed.jump_cursor(count - 1, 0);
3179 } else {
3180 ed.jump_cursor(0, 0);
3181 }
3182 move_first_non_whitespace(ed);
3183 if ed.cursor() != pre {
3184 push_jump(ed, pre);
3185 }
3186 }
3187 'e' => execute_motion(ed, Motion::WordEndBack, count),
3188 'E' => execute_motion(ed, Motion::BigWordEndBack, count),
3189 '_' => execute_motion(ed, Motion::LastNonBlank, count),
3191 'M' => execute_motion(ed, Motion::LineMiddle, count),
3193 'v' => {
3195 if let Some(snap) = ed.vim.last_visual {
3196 match snap.mode {
3197 Mode::Visual => {
3198 ed.vim.visual_anchor = snap.anchor;
3199 ed.vim.mode = Mode::Visual;
3200 }
3201 Mode::VisualLine => {
3202 ed.vim.visual_line_anchor = snap.anchor.0;
3203 ed.vim.mode = Mode::VisualLine;
3204 }
3205 Mode::VisualBlock => {
3206 ed.vim.block_anchor = snap.anchor;
3207 ed.vim.block_vcol = snap.block_vcol;
3208 ed.vim.mode = Mode::VisualBlock;
3209 }
3210 _ => {}
3211 }
3212 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
3213 }
3214 }
3215 'j' => execute_motion(ed, Motion::ScreenDown, count),
3219 'k' => execute_motion(ed, Motion::ScreenUp, count),
3220 'U' => {
3224 ed.vim.pending = Pending::Op {
3225 op: Operator::Uppercase,
3226 count1: count,
3227 };
3228 }
3229 'u' => {
3230 ed.vim.pending = Pending::Op {
3231 op: Operator::Lowercase,
3232 count1: count,
3233 };
3234 }
3235 '~' => {
3236 ed.vim.pending = Pending::Op {
3237 op: Operator::ToggleCase,
3238 count1: count,
3239 };
3240 }
3241 'q' => {
3242 ed.vim.pending = Pending::Op {
3245 op: Operator::Reflow,
3246 count1: count,
3247 };
3248 }
3249 'J' => {
3250 for _ in 0..count.max(1) {
3252 ed.push_undo();
3253 join_line_raw(ed);
3254 }
3255 if !ed.vim.replaying {
3256 ed.vim.last_change = Some(LastChange::JoinLine {
3257 count: count.max(1),
3258 });
3259 }
3260 }
3261 'd' => {
3262 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
3267 }
3268 'i' => {
3273 if let Some((row, col)) = ed.vim.last_insert_pos {
3274 ed.jump_cursor(row, col);
3275 }
3276 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3277 }
3278 ';' => walk_change_list(ed, -1, count.max(1)),
3281 ',' => walk_change_list(ed, 1, count.max(1)),
3282 '*' => execute_motion(
3286 ed,
3287 Motion::WordAtCursor {
3288 forward: true,
3289 whole_word: false,
3290 },
3291 count,
3292 ),
3293 '#' => execute_motion(
3294 ed,
3295 Motion::WordAtCursor {
3296 forward: false,
3297 whole_word: false,
3298 },
3299 count,
3300 ),
3301 _ => {}
3302 }
3303}
3304
3305fn handle_after_z<H: crate::types::Host>(
3306 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3307 input: Input,
3308) -> bool {
3309 let count = take_count(&mut ed.vim);
3310 if let Key::Char(ch) = input.key {
3313 apply_after_z(ed, ch, count);
3314 }
3315 true
3316}
3317
3318pub(crate) fn apply_after_z<H: crate::types::Host>(
3323 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3324 ch: char,
3325 count: usize,
3326) {
3327 use crate::editor::CursorScrollTarget;
3328 let row = ed.cursor().0;
3329 match ch {
3330 'z' => {
3331 ed.scroll_cursor_to(CursorScrollTarget::Center);
3332 ed.vim.viewport_pinned = true;
3333 }
3334 't' => {
3335 ed.scroll_cursor_to(CursorScrollTarget::Top);
3336 ed.vim.viewport_pinned = true;
3337 }
3338 'b' => {
3339 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
3340 ed.vim.viewport_pinned = true;
3341 }
3342 'o' => {
3347 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
3348 }
3349 'c' => {
3350 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
3351 }
3352 'a' => {
3353 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
3354 }
3355 'R' => {
3356 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
3357 }
3358 'M' => {
3359 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
3360 }
3361 'E' => {
3362 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
3363 }
3364 'd' => {
3365 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
3366 }
3367 'f' => {
3368 if matches!(
3369 ed.vim.mode,
3370 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
3371 ) {
3372 let anchor_row = match ed.vim.mode {
3375 Mode::VisualLine => ed.vim.visual_line_anchor,
3376 Mode::VisualBlock => ed.vim.block_anchor.0,
3377 _ => ed.vim.visual_anchor.0,
3378 };
3379 let cur = ed.cursor().0;
3380 let top = anchor_row.min(cur);
3381 let bot = anchor_row.max(cur);
3382 ed.apply_fold_op(crate::types::FoldOp::Add {
3383 start_row: top,
3384 end_row: bot,
3385 closed: true,
3386 });
3387 ed.vim.mode = Mode::Normal;
3388 } else {
3389 ed.vim.pending = Pending::Op {
3394 op: Operator::Fold,
3395 count1: count,
3396 };
3397 }
3398 }
3399 _ => {}
3400 }
3401}
3402
3403fn handle_replace<H: crate::types::Host>(
3404 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3405 input: Input,
3406) -> bool {
3407 if let Key::Char(ch) = input.key {
3408 if ed.vim.mode == Mode::VisualBlock {
3409 block_replace(ed, ch);
3410 return true;
3411 }
3412 let count = take_count(&mut ed.vim);
3413 replace_char(ed, ch, count.max(1));
3414 if !ed.vim.replaying {
3415 ed.vim.last_change = Some(LastChange::ReplaceChar {
3416 ch,
3417 count: count.max(1),
3418 });
3419 }
3420 }
3421 true
3422}
3423
3424fn handle_find_target<H: crate::types::Host>(
3425 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3426 input: Input,
3427 forward: bool,
3428 till: bool,
3429) -> bool {
3430 let Key::Char(ch) = input.key else {
3431 return true;
3432 };
3433 let count = take_count(&mut ed.vim);
3434 apply_find_char(ed, ch, forward, till, count.max(1));
3435 true
3436}
3437
3438pub(crate) fn apply_find_char<H: crate::types::Host>(
3444 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3445 ch: char,
3446 forward: bool,
3447 till: bool,
3448 count: usize,
3449) {
3450 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
3451 ed.vim.last_find = Some((ch, forward, till));
3452}
3453
3454pub(crate) fn apply_op_find_motion<H: crate::types::Host>(
3460 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3461 op: Operator,
3462 ch: char,
3463 forward: bool,
3464 till: bool,
3465 total_count: usize,
3466) {
3467 let motion = Motion::Find { ch, forward, till };
3468 apply_op_with_motion(ed, op, &motion, total_count);
3469 ed.vim.last_find = Some((ch, forward, till));
3470 if !ed.vim.replaying && op_is_change(op) {
3471 ed.vim.last_change = Some(LastChange::OpMotion {
3472 op,
3473 motion,
3474 count: total_count,
3475 inserted: None,
3476 });
3477 }
3478}
3479
3480fn handle_op_find_target<H: crate::types::Host>(
3481 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3482 input: Input,
3483 op: Operator,
3484 count1: usize,
3485 forward: bool,
3486 till: bool,
3487) -> bool {
3488 let Key::Char(ch) = input.key else {
3489 return true;
3490 };
3491 let count2 = take_count(&mut ed.vim);
3492 let total = count1.max(1) * count2.max(1);
3493 apply_op_find_motion(ed, op, ch, forward, till, total);
3494 true
3495}
3496
3497fn handle_text_object<H: crate::types::Host>(
3498 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3499 input: Input,
3500 op: Operator,
3501 _count1: usize,
3502 inner: bool,
3503) -> bool {
3504 let Key::Char(ch) = input.key else {
3505 return true;
3506 };
3507 let obj = match ch {
3508 'w' => TextObject::Word { big: false },
3509 'W' => TextObject::Word { big: true },
3510 '"' | '\'' | '`' => TextObject::Quote(ch),
3511 '(' | ')' | 'b' => TextObject::Bracket('('),
3512 '[' | ']' => TextObject::Bracket('['),
3513 '{' | '}' | 'B' => TextObject::Bracket('{'),
3514 '<' | '>' => TextObject::Bracket('<'),
3515 'p' => TextObject::Paragraph,
3516 't' => TextObject::XmlTag,
3517 's' => TextObject::Sentence,
3518 _ => return true,
3519 };
3520 apply_op_with_text_object(ed, op, obj, inner);
3521 if !ed.vim.replaying && op_is_change(op) {
3522 ed.vim.last_change = Some(LastChange::OpTextObj {
3523 op,
3524 obj,
3525 inner,
3526 inserted: None,
3527 });
3528 }
3529 true
3530}
3531
3532fn handle_visual_text_obj<H: crate::types::Host>(
3533 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3534 input: Input,
3535 inner: bool,
3536) -> bool {
3537 let Key::Char(ch) = input.key else {
3538 return true;
3539 };
3540 let obj = match ch {
3541 'w' => TextObject::Word { big: false },
3542 'W' => TextObject::Word { big: true },
3543 '"' | '\'' | '`' => TextObject::Quote(ch),
3544 '(' | ')' | 'b' => TextObject::Bracket('('),
3545 '[' | ']' => TextObject::Bracket('['),
3546 '{' | '}' | 'B' => TextObject::Bracket('{'),
3547 '<' | '>' => TextObject::Bracket('<'),
3548 'p' => TextObject::Paragraph,
3549 't' => TextObject::XmlTag,
3550 's' => TextObject::Sentence,
3551 _ => return true,
3552 };
3553 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3554 return true;
3555 };
3556 match kind {
3560 MotionKind::Linewise => {
3561 ed.vim.visual_line_anchor = start.0;
3562 ed.vim.mode = Mode::VisualLine;
3563 ed.jump_cursor(end.0, 0);
3564 }
3565 _ => {
3566 ed.vim.mode = Mode::Visual;
3567 ed.vim.visual_anchor = (start.0, start.1);
3568 let (er, ec) = retreat_one(ed, end);
3569 ed.jump_cursor(er, ec);
3570 }
3571 }
3572 true
3573}
3574
3575fn retreat_one<H: crate::types::Host>(
3577 ed: &Editor<hjkl_buffer::Buffer, H>,
3578 pos: (usize, usize),
3579) -> (usize, usize) {
3580 let (r, c) = pos;
3581 if c > 0 {
3582 (r, c - 1)
3583 } else if r > 0 {
3584 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
3585 (r - 1, prev_len)
3586 } else {
3587 (0, 0)
3588 }
3589}
3590
3591fn op_is_change(op: Operator) -> bool {
3592 matches!(op, Operator::Delete | Operator::Change)
3593}
3594
3595fn handle_normal_only<H: crate::types::Host>(
3598 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3599 input: &Input,
3600 count: usize,
3601) -> bool {
3602 if input.ctrl {
3603 return false;
3604 }
3605 match input.key {
3606 Key::Char('i') => {
3607 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3608 true
3609 }
3610 Key::Char('I') => {
3611 move_first_non_whitespace(ed);
3612 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
3613 true
3614 }
3615 Key::Char('a') => {
3616 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3617 ed.push_buffer_cursor_to_textarea();
3618 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
3619 true
3620 }
3621 Key::Char('A') => {
3622 crate::motions::move_line_end(&mut ed.buffer);
3623 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3624 ed.push_buffer_cursor_to_textarea();
3625 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
3626 true
3627 }
3628 Key::Char('R') => {
3629 begin_insert(ed, count.max(1), InsertReason::Replace);
3632 true
3633 }
3634 Key::Char('o') => {
3635 use hjkl_buffer::{Edit, Position};
3636 ed.push_undo();
3637 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
3640 ed.sync_buffer_content_from_textarea();
3641 let row = buf_cursor_pos(&ed.buffer).row;
3642 let line_chars = buf_line_chars(&ed.buffer, row);
3643 let prev_line = buf_line(&ed.buffer, row).unwrap_or_default();
3646 let indent = compute_enter_indent(&ed.settings, prev_line);
3647 ed.mutate_edit(Edit::InsertStr {
3648 at: Position::new(row, line_chars),
3649 text: format!("\n{indent}"),
3650 });
3651 ed.push_buffer_cursor_to_textarea();
3652 true
3653 }
3654 Key::Char('O') => {
3655 use hjkl_buffer::{Edit, Position};
3656 ed.push_undo();
3657 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
3658 ed.sync_buffer_content_from_textarea();
3659 let row = buf_cursor_pos(&ed.buffer).row;
3660 let indent = if row > 0 {
3664 let above = buf_line(&ed.buffer, row - 1).unwrap_or_default();
3665 compute_enter_indent(&ed.settings, above)
3666 } else {
3667 let cur = buf_line(&ed.buffer, row).unwrap_or_default();
3668 cur.chars()
3669 .take_while(|c| *c == ' ' || *c == '\t')
3670 .collect::<String>()
3671 };
3672 ed.mutate_edit(Edit::InsertStr {
3673 at: Position::new(row, 0),
3674 text: format!("{indent}\n"),
3675 });
3676 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3681 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
3682 let new_row = buf_cursor_pos(&ed.buffer).row;
3683 buf_set_cursor_rc(&mut ed.buffer, new_row, indent.chars().count());
3684 ed.push_buffer_cursor_to_textarea();
3685 true
3686 }
3687 Key::Char('x') => {
3688 do_char_delete(ed, true, count.max(1));
3689 if !ed.vim.replaying {
3690 ed.vim.last_change = Some(LastChange::CharDel {
3691 forward: true,
3692 count: count.max(1),
3693 });
3694 }
3695 true
3696 }
3697 Key::Char('X') => {
3698 do_char_delete(ed, false, count.max(1));
3699 if !ed.vim.replaying {
3700 ed.vim.last_change = Some(LastChange::CharDel {
3701 forward: false,
3702 count: count.max(1),
3703 });
3704 }
3705 true
3706 }
3707 Key::Char('~') => {
3708 for _ in 0..count.max(1) {
3709 ed.push_undo();
3710 toggle_case_at_cursor(ed);
3711 }
3712 if !ed.vim.replaying {
3713 ed.vim.last_change = Some(LastChange::ToggleCase {
3714 count: count.max(1),
3715 });
3716 }
3717 true
3718 }
3719 Key::Char('J') => {
3720 for _ in 0..count.max(1) {
3721 ed.push_undo();
3722 join_line(ed);
3723 }
3724 if !ed.vim.replaying {
3725 ed.vim.last_change = Some(LastChange::JoinLine {
3726 count: count.max(1),
3727 });
3728 }
3729 true
3730 }
3731 Key::Char('D') => {
3732 ed.push_undo();
3733 delete_to_eol(ed);
3734 crate::motions::move_left(&mut ed.buffer, 1);
3736 ed.push_buffer_cursor_to_textarea();
3737 if !ed.vim.replaying {
3738 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
3739 }
3740 true
3741 }
3742 Key::Char('Y') => {
3743 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
3745 true
3746 }
3747 Key::Char('C') => {
3748 ed.push_undo();
3749 delete_to_eol(ed);
3750 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
3751 true
3752 }
3753 Key::Char('s') => {
3754 use hjkl_buffer::{Edit, MotionKind, Position};
3755 ed.push_undo();
3756 ed.sync_buffer_content_from_textarea();
3757 for _ in 0..count.max(1) {
3758 let cursor = buf_cursor_pos(&ed.buffer);
3759 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
3760 if cursor.col >= line_chars {
3761 break;
3762 }
3763 ed.mutate_edit(Edit::DeleteRange {
3764 start: cursor,
3765 end: Position::new(cursor.row, cursor.col + 1),
3766 kind: MotionKind::Char,
3767 });
3768 }
3769 ed.push_buffer_cursor_to_textarea();
3770 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3771 if !ed.vim.replaying {
3773 ed.vim.last_change = Some(LastChange::OpMotion {
3774 op: Operator::Change,
3775 motion: Motion::Right,
3776 count: count.max(1),
3777 inserted: None,
3778 });
3779 }
3780 true
3781 }
3782 Key::Char('p') => {
3783 do_paste(ed, false, count.max(1));
3784 if !ed.vim.replaying {
3785 ed.vim.last_change = Some(LastChange::Paste {
3786 before: false,
3787 count: count.max(1),
3788 });
3789 }
3790 true
3791 }
3792 Key::Char('P') => {
3793 do_paste(ed, true, count.max(1));
3794 if !ed.vim.replaying {
3795 ed.vim.last_change = Some(LastChange::Paste {
3796 before: true,
3797 count: count.max(1),
3798 });
3799 }
3800 true
3801 }
3802 Key::Char('u') => {
3803 do_undo(ed);
3804 true
3805 }
3806 Key::Char('r') => {
3807 ed.vim.count = count;
3808 ed.vim.pending = Pending::Replace;
3809 true
3810 }
3811 Key::Char('/') => {
3812 enter_search(ed, true);
3813 true
3814 }
3815 Key::Char('?') => {
3816 enter_search(ed, false);
3817 true
3818 }
3819 Key::Char('.') => {
3820 replay_last_change(ed, count);
3821 true
3822 }
3823 _ => false,
3824 }
3825}
3826
3827fn begin_insert_noundo<H: crate::types::Host>(
3829 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3830 count: usize,
3831 reason: InsertReason,
3832) {
3833 let reason = if ed.vim.replaying {
3834 InsertReason::ReplayOnly
3835 } else {
3836 reason
3837 };
3838 let (row, _) = ed.cursor();
3839 ed.vim.insert_session = Some(InsertSession {
3840 count,
3841 row_min: row,
3842 row_max: row,
3843 before_lines: buf_lines_to_vec(&ed.buffer),
3844 reason,
3845 });
3846 ed.vim.mode = Mode::Insert;
3847}
3848
3849fn apply_op_with_motion<H: crate::types::Host>(
3852 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3853 op: Operator,
3854 motion: &Motion,
3855 count: usize,
3856) {
3857 let start = ed.cursor();
3858 apply_motion_cursor_ctx(ed, motion, count, true);
3863 let end = ed.cursor();
3864 let kind = motion_kind(motion);
3865 ed.jump_cursor(start.0, start.1);
3867 run_operator_over_range(ed, op, start, end, kind);
3868}
3869
3870fn apply_op_with_text_object<H: crate::types::Host>(
3871 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3872 op: Operator,
3873 obj: TextObject,
3874 inner: bool,
3875) {
3876 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3877 return;
3878 };
3879 ed.jump_cursor(start.0, start.1);
3880 run_operator_over_range(ed, op, start, end, kind);
3881}
3882
3883fn motion_kind(motion: &Motion) -> MotionKind {
3884 match motion {
3885 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => MotionKind::Linewise,
3886 Motion::FileTop | Motion::FileBottom => MotionKind::Linewise,
3887 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
3888 MotionKind::Linewise
3889 }
3890 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
3891 MotionKind::Inclusive
3892 }
3893 Motion::Find { .. } => MotionKind::Inclusive,
3894 Motion::MatchBracket => MotionKind::Inclusive,
3895 Motion::LineEnd => MotionKind::Inclusive,
3897 _ => MotionKind::Exclusive,
3898 }
3899}
3900
3901fn run_operator_over_range<H: crate::types::Host>(
3902 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3903 op: Operator,
3904 start: (usize, usize),
3905 end: (usize, usize),
3906 kind: MotionKind,
3907) {
3908 let (top, bot) = order(start, end);
3909 if top == bot {
3910 return;
3911 }
3912
3913 match op {
3914 Operator::Yank => {
3915 let text = read_vim_range(ed, top, bot, kind);
3916 if !text.is_empty() {
3917 ed.record_yank_to_host(text.clone());
3918 ed.record_yank(text, matches!(kind, MotionKind::Linewise));
3919 }
3920 let rbr = match kind {
3924 MotionKind::Linewise => {
3925 let last_col = buf_line_chars(&ed.buffer, bot.0).saturating_sub(1);
3926 (bot.0, last_col)
3927 }
3928 MotionKind::Inclusive => (bot.0, bot.1),
3929 MotionKind::Exclusive => (bot.0, bot.1.saturating_sub(1)),
3930 };
3931 ed.set_mark('[', top);
3932 ed.set_mark(']', rbr);
3933 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3934 ed.push_buffer_cursor_to_textarea();
3935 }
3936 Operator::Delete => {
3937 ed.push_undo();
3938 cut_vim_range(ed, top, bot, kind);
3939 if !matches!(kind, MotionKind::Linewise) {
3944 clamp_cursor_to_normal_mode(ed);
3945 }
3946 ed.vim.mode = Mode::Normal;
3947 let pos = ed.cursor();
3951 ed.set_mark('[', pos);
3952 ed.set_mark(']', pos);
3953 }
3954 Operator::Change => {
3955 ed.vim.change_mark_start = Some(top);
3960 ed.push_undo();
3961 cut_vim_range(ed, top, bot, kind);
3962 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3963 }
3964 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3965 apply_case_op_to_selection(ed, op, top, bot, kind);
3966 }
3967 Operator::Indent | Operator::Outdent => {
3968 ed.push_undo();
3971 if op == Operator::Indent {
3972 indent_rows(ed, top.0, bot.0, 1);
3973 } else {
3974 outdent_rows(ed, top.0, bot.0, 1);
3975 }
3976 ed.vim.mode = Mode::Normal;
3977 }
3978 Operator::Fold => {
3979 if bot.0 >= top.0 {
3983 ed.apply_fold_op(crate::types::FoldOp::Add {
3984 start_row: top.0,
3985 end_row: bot.0,
3986 closed: true,
3987 });
3988 }
3989 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3990 ed.push_buffer_cursor_to_textarea();
3991 ed.vim.mode = Mode::Normal;
3992 }
3993 Operator::Reflow => {
3994 ed.push_undo();
3995 reflow_rows(ed, top.0, bot.0);
3996 ed.vim.mode = Mode::Normal;
3997 }
3998 }
3999}
4000
4001fn reflow_rows<H: crate::types::Host>(
4006 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4007 top: usize,
4008 bot: usize,
4009) {
4010 let width = ed.settings().textwidth.max(1);
4011 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4012 let bot = bot.min(lines.len().saturating_sub(1));
4013 if top > bot {
4014 return;
4015 }
4016 let original = lines[top..=bot].to_vec();
4017 let mut wrapped: Vec<String> = Vec::new();
4018 let mut paragraph: Vec<String> = Vec::new();
4019 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
4020 if para.is_empty() {
4021 return;
4022 }
4023 let words = para.join(" ");
4024 let mut current = String::new();
4025 for word in words.split_whitespace() {
4026 let extra = if current.is_empty() {
4027 word.chars().count()
4028 } else {
4029 current.chars().count() + 1 + word.chars().count()
4030 };
4031 if extra > width && !current.is_empty() {
4032 out.push(std::mem::take(&mut current));
4033 current.push_str(word);
4034 } else if current.is_empty() {
4035 current.push_str(word);
4036 } else {
4037 current.push(' ');
4038 current.push_str(word);
4039 }
4040 }
4041 if !current.is_empty() {
4042 out.push(current);
4043 }
4044 para.clear();
4045 };
4046 for line in &original {
4047 if line.trim().is_empty() {
4048 flush(&mut paragraph, &mut wrapped, width);
4049 wrapped.push(String::new());
4050 } else {
4051 paragraph.push(line.clone());
4052 }
4053 }
4054 flush(&mut paragraph, &mut wrapped, width);
4055
4056 let after: Vec<String> = lines.split_off(bot + 1);
4058 lines.truncate(top);
4059 lines.extend(wrapped);
4060 lines.extend(after);
4061 ed.restore(lines, (top, 0));
4062 ed.mark_content_dirty();
4063}
4064
4065fn apply_case_op_to_selection<H: crate::types::Host>(
4071 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4072 op: Operator,
4073 top: (usize, usize),
4074 bot: (usize, usize),
4075 kind: MotionKind,
4076) {
4077 use hjkl_buffer::Edit;
4078 ed.push_undo();
4079 let saved_yank = ed.yank().to_string();
4080 let saved_yank_linewise = ed.vim.yank_linewise;
4081 let selection = cut_vim_range(ed, top, bot, kind);
4082 let transformed = match op {
4083 Operator::Uppercase => selection.to_uppercase(),
4084 Operator::Lowercase => selection.to_lowercase(),
4085 Operator::ToggleCase => toggle_case_str(&selection),
4086 _ => unreachable!(),
4087 };
4088 if !transformed.is_empty() {
4089 let cursor = buf_cursor_pos(&ed.buffer);
4090 ed.mutate_edit(Edit::InsertStr {
4091 at: cursor,
4092 text: transformed,
4093 });
4094 }
4095 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4096 ed.push_buffer_cursor_to_textarea();
4097 ed.set_yank(saved_yank);
4098 ed.vim.yank_linewise = saved_yank_linewise;
4099 ed.vim.mode = Mode::Normal;
4100}
4101
4102fn indent_rows<H: crate::types::Host>(
4107 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4108 top: usize,
4109 bot: usize,
4110 count: usize,
4111) {
4112 ed.sync_buffer_content_from_textarea();
4113 let width = ed.settings().shiftwidth * count.max(1);
4114 let pad: String = " ".repeat(width);
4115 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4116 let bot = bot.min(lines.len().saturating_sub(1));
4117 for line in lines.iter_mut().take(bot + 1).skip(top) {
4118 if !line.is_empty() {
4119 line.insert_str(0, &pad);
4120 }
4121 }
4122 ed.restore(lines, (top, 0));
4125 move_first_non_whitespace(ed);
4126}
4127
4128fn outdent_rows<H: crate::types::Host>(
4132 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4133 top: usize,
4134 bot: usize,
4135 count: usize,
4136) {
4137 ed.sync_buffer_content_from_textarea();
4138 let width = ed.settings().shiftwidth * count.max(1);
4139 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4140 let bot = bot.min(lines.len().saturating_sub(1));
4141 for line in lines.iter_mut().take(bot + 1).skip(top) {
4142 let strip: usize = line
4143 .chars()
4144 .take(width)
4145 .take_while(|c| *c == ' ' || *c == '\t')
4146 .count();
4147 if strip > 0 {
4148 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
4149 line.drain(..byte_len);
4150 }
4151 }
4152 ed.restore(lines, (top, 0));
4153 move_first_non_whitespace(ed);
4154}
4155
4156fn toggle_case_str(s: &str) -> String {
4157 s.chars()
4158 .map(|c| {
4159 if c.is_lowercase() {
4160 c.to_uppercase().next().unwrap_or(c)
4161 } else if c.is_uppercase() {
4162 c.to_lowercase().next().unwrap_or(c)
4163 } else {
4164 c
4165 }
4166 })
4167 .collect()
4168}
4169
4170fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
4171 if a <= b { (a, b) } else { (b, a) }
4172}
4173
4174fn clamp_cursor_to_normal_mode<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
4179 let (row, col) = ed.cursor();
4180 let line_chars = buf_line_chars(&ed.buffer, row);
4181 let max_col = line_chars.saturating_sub(1);
4182 if col > max_col {
4183 buf_set_cursor_rc(&mut ed.buffer, row, max_col);
4184 ed.push_buffer_cursor_to_textarea();
4185 }
4186}
4187
4188fn execute_line_op<H: crate::types::Host>(
4191 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4192 op: Operator,
4193 count: usize,
4194) {
4195 let (row, col) = ed.cursor();
4196 let total = buf_row_count(&ed.buffer);
4197 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
4198
4199 match op {
4200 Operator::Yank => {
4201 let text = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4203 if !text.is_empty() {
4204 ed.record_yank_to_host(text.clone());
4205 ed.record_yank(text, true);
4206 }
4207 let last_col = buf_line_chars(&ed.buffer, end_row).saturating_sub(1);
4210 ed.set_mark('[', (row, 0));
4211 ed.set_mark(']', (end_row, last_col));
4212 buf_set_cursor_rc(&mut ed.buffer, row, col);
4213 ed.push_buffer_cursor_to_textarea();
4214 ed.vim.mode = Mode::Normal;
4215 }
4216 Operator::Delete => {
4217 ed.push_undo();
4218 let deleted_through_last = end_row + 1 >= total;
4219 cut_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4220 let total_after = buf_row_count(&ed.buffer);
4224 let raw_target = if deleted_through_last {
4225 row.saturating_sub(1).min(total_after.saturating_sub(1))
4226 } else {
4227 row.min(total_after.saturating_sub(1))
4228 };
4229 let target_row = if raw_target > 0
4235 && raw_target + 1 == total_after
4236 && buf_line(&ed.buffer, raw_target)
4237 .map(str::is_empty)
4238 .unwrap_or(false)
4239 {
4240 raw_target - 1
4241 } else {
4242 raw_target
4243 };
4244 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
4245 ed.push_buffer_cursor_to_textarea();
4246 move_first_non_whitespace(ed);
4247 ed.sticky_col = Some(ed.cursor().1);
4248 ed.vim.mode = Mode::Normal;
4249 let pos = ed.cursor();
4252 ed.set_mark('[', pos);
4253 ed.set_mark(']', pos);
4254 }
4255 Operator::Change => {
4256 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4260 ed.vim.change_mark_start = Some((row, 0));
4262 ed.push_undo();
4263 ed.sync_buffer_content_from_textarea();
4264 let payload = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4266 if end_row > row {
4267 ed.mutate_edit(Edit::DeleteRange {
4268 start: Position::new(row + 1, 0),
4269 end: Position::new(end_row, 0),
4270 kind: BufKind::Line,
4271 });
4272 }
4273 let line_chars = buf_line_chars(&ed.buffer, row);
4274 if line_chars > 0 {
4275 ed.mutate_edit(Edit::DeleteRange {
4276 start: Position::new(row, 0),
4277 end: Position::new(row, line_chars),
4278 kind: BufKind::Char,
4279 });
4280 }
4281 if !payload.is_empty() {
4282 ed.record_yank_to_host(payload.clone());
4283 ed.record_delete(payload, true);
4284 }
4285 buf_set_cursor_rc(&mut ed.buffer, row, 0);
4286 ed.push_buffer_cursor_to_textarea();
4287 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4288 }
4289 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4290 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), MotionKind::Linewise);
4294 move_first_non_whitespace(ed);
4297 }
4298 Operator::Indent | Operator::Outdent => {
4299 ed.push_undo();
4301 if op == Operator::Indent {
4302 indent_rows(ed, row, end_row, 1);
4303 } else {
4304 outdent_rows(ed, row, end_row, 1);
4305 }
4306 ed.sticky_col = Some(ed.cursor().1);
4307 ed.vim.mode = Mode::Normal;
4308 }
4309 Operator::Fold => unreachable!("Fold has no line-op double"),
4311 Operator::Reflow => {
4312 ed.push_undo();
4314 reflow_rows(ed, row, end_row);
4315 move_first_non_whitespace(ed);
4316 ed.sticky_col = Some(ed.cursor().1);
4317 ed.vim.mode = Mode::Normal;
4318 }
4319 }
4320}
4321
4322fn apply_visual_operator<H: crate::types::Host>(
4325 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4326 op: Operator,
4327) {
4328 match ed.vim.mode {
4329 Mode::VisualLine => {
4330 let cursor_row = buf_cursor_pos(&ed.buffer).row;
4331 let top = cursor_row.min(ed.vim.visual_line_anchor);
4332 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4333 ed.vim.yank_linewise = true;
4334 match op {
4335 Operator::Yank => {
4336 let text = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4337 if !text.is_empty() {
4338 ed.record_yank_to_host(text.clone());
4339 ed.record_yank(text, true);
4340 }
4341 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4342 ed.push_buffer_cursor_to_textarea();
4343 ed.vim.mode = Mode::Normal;
4344 }
4345 Operator::Delete => {
4346 ed.push_undo();
4347 cut_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4348 ed.vim.mode = Mode::Normal;
4349 }
4350 Operator::Change => {
4351 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4354 ed.push_undo();
4355 ed.sync_buffer_content_from_textarea();
4356 let payload = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4357 if bot > top {
4358 ed.mutate_edit(Edit::DeleteRange {
4359 start: Position::new(top + 1, 0),
4360 end: Position::new(bot, 0),
4361 kind: BufKind::Line,
4362 });
4363 }
4364 let line_chars = buf_line_chars(&ed.buffer, top);
4365 if line_chars > 0 {
4366 ed.mutate_edit(Edit::DeleteRange {
4367 start: Position::new(top, 0),
4368 end: Position::new(top, line_chars),
4369 kind: BufKind::Char,
4370 });
4371 }
4372 if !payload.is_empty() {
4373 ed.record_yank_to_host(payload.clone());
4374 ed.record_delete(payload, true);
4375 }
4376 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4377 ed.push_buffer_cursor_to_textarea();
4378 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4379 }
4380 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4381 let bot = buf_cursor_pos(&ed.buffer)
4382 .row
4383 .max(ed.vim.visual_line_anchor);
4384 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), MotionKind::Linewise);
4385 move_first_non_whitespace(ed);
4386 }
4387 Operator::Indent | Operator::Outdent => {
4388 ed.push_undo();
4389 let (cursor_row, _) = ed.cursor();
4390 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4391 if op == Operator::Indent {
4392 indent_rows(ed, top, bot, 1);
4393 } else {
4394 outdent_rows(ed, top, bot, 1);
4395 }
4396 ed.vim.mode = Mode::Normal;
4397 }
4398 Operator::Reflow => {
4399 ed.push_undo();
4400 let (cursor_row, _) = ed.cursor();
4401 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4402 reflow_rows(ed, top, bot);
4403 ed.vim.mode = Mode::Normal;
4404 }
4405 Operator::Fold => unreachable!("Visual zf takes its own path"),
4408 }
4409 }
4410 Mode::Visual => {
4411 ed.vim.yank_linewise = false;
4412 let anchor = ed.vim.visual_anchor;
4413 let cursor = ed.cursor();
4414 let (top, bot) = order(anchor, cursor);
4415 match op {
4416 Operator::Yank => {
4417 let text = read_vim_range(ed, top, bot, MotionKind::Inclusive);
4418 if !text.is_empty() {
4419 ed.record_yank_to_host(text.clone());
4420 ed.record_yank(text, false);
4421 }
4422 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4423 ed.push_buffer_cursor_to_textarea();
4424 ed.vim.mode = Mode::Normal;
4425 }
4426 Operator::Delete => {
4427 ed.push_undo();
4428 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4429 ed.vim.mode = Mode::Normal;
4430 }
4431 Operator::Change => {
4432 ed.push_undo();
4433 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4434 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4435 }
4436 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4437 let anchor = ed.vim.visual_anchor;
4439 let cursor = ed.cursor();
4440 let (top, bot) = order(anchor, cursor);
4441 apply_case_op_to_selection(ed, op, top, bot, MotionKind::Inclusive);
4442 }
4443 Operator::Indent | Operator::Outdent => {
4444 ed.push_undo();
4445 let anchor = ed.vim.visual_anchor;
4446 let cursor = ed.cursor();
4447 let (top, bot) = order(anchor, cursor);
4448 if op == Operator::Indent {
4449 indent_rows(ed, top.0, bot.0, 1);
4450 } else {
4451 outdent_rows(ed, top.0, bot.0, 1);
4452 }
4453 ed.vim.mode = Mode::Normal;
4454 }
4455 Operator::Reflow => {
4456 ed.push_undo();
4457 let anchor = ed.vim.visual_anchor;
4458 let cursor = ed.cursor();
4459 let (top, bot) = order(anchor, cursor);
4460 reflow_rows(ed, top.0, bot.0);
4461 ed.vim.mode = Mode::Normal;
4462 }
4463 Operator::Fold => unreachable!("Visual zf takes its own path"),
4464 }
4465 }
4466 Mode::VisualBlock => apply_block_operator(ed, op),
4467 _ => {}
4468 }
4469}
4470
4471fn block_bounds<H: crate::types::Host>(
4476 ed: &Editor<hjkl_buffer::Buffer, H>,
4477) -> (usize, usize, usize, usize) {
4478 let (ar, ac) = ed.vim.block_anchor;
4479 let (cr, _) = ed.cursor();
4480 let cc = ed.vim.block_vcol;
4481 let top = ar.min(cr);
4482 let bot = ar.max(cr);
4483 let left = ac.min(cc);
4484 let right = ac.max(cc);
4485 (top, bot, left, right)
4486}
4487
4488fn update_block_vcol<H: crate::types::Host>(
4493 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4494 motion: &Motion,
4495) {
4496 match motion {
4497 Motion::Left
4498 | Motion::Right
4499 | Motion::WordFwd
4500 | Motion::BigWordFwd
4501 | Motion::WordBack
4502 | Motion::BigWordBack
4503 | Motion::WordEnd
4504 | Motion::BigWordEnd
4505 | Motion::WordEndBack
4506 | Motion::BigWordEndBack
4507 | Motion::LineStart
4508 | Motion::FirstNonBlank
4509 | Motion::LineEnd
4510 | Motion::Find { .. }
4511 | Motion::FindRepeat { .. }
4512 | Motion::MatchBracket => {
4513 ed.vim.block_vcol = ed.cursor().1;
4514 }
4515 _ => {}
4517 }
4518}
4519
4520fn apply_block_operator<H: crate::types::Host>(
4525 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4526 op: Operator,
4527) {
4528 let (top, bot, left, right) = block_bounds(ed);
4529 let yank = block_yank(ed, top, bot, left, right);
4531
4532 match op {
4533 Operator::Yank => {
4534 if !yank.is_empty() {
4535 ed.record_yank_to_host(yank.clone());
4536 ed.record_yank(yank, false);
4537 }
4538 ed.vim.mode = Mode::Normal;
4539 ed.jump_cursor(top, left);
4540 }
4541 Operator::Delete => {
4542 ed.push_undo();
4543 delete_block_contents(ed, top, bot, left, right);
4544 if !yank.is_empty() {
4545 ed.record_yank_to_host(yank.clone());
4546 ed.record_delete(yank, false);
4547 }
4548 ed.vim.mode = Mode::Normal;
4549 ed.jump_cursor(top, left);
4550 }
4551 Operator::Change => {
4552 ed.push_undo();
4553 delete_block_contents(ed, top, bot, left, right);
4554 if !yank.is_empty() {
4555 ed.record_yank_to_host(yank.clone());
4556 ed.record_delete(yank, false);
4557 }
4558 ed.jump_cursor(top, left);
4559 begin_insert_noundo(
4560 ed,
4561 1,
4562 InsertReason::BlockChange {
4563 top,
4564 bot,
4565 col: left,
4566 },
4567 );
4568 }
4569 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4570 ed.push_undo();
4571 transform_block_case(ed, op, top, bot, left, right);
4572 ed.vim.mode = Mode::Normal;
4573 ed.jump_cursor(top, left);
4574 }
4575 Operator::Indent | Operator::Outdent => {
4576 ed.push_undo();
4580 if op == Operator::Indent {
4581 indent_rows(ed, top, bot, 1);
4582 } else {
4583 outdent_rows(ed, top, bot, 1);
4584 }
4585 ed.vim.mode = Mode::Normal;
4586 }
4587 Operator::Fold => unreachable!("Visual zf takes its own path"),
4588 Operator::Reflow => {
4589 ed.push_undo();
4593 reflow_rows(ed, top, bot);
4594 ed.vim.mode = Mode::Normal;
4595 }
4596 }
4597}
4598
4599fn transform_block_case<H: crate::types::Host>(
4603 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4604 op: Operator,
4605 top: usize,
4606 bot: usize,
4607 left: usize,
4608 right: usize,
4609) {
4610 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4611 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4612 let chars: Vec<char> = lines[r].chars().collect();
4613 if left >= chars.len() {
4614 continue;
4615 }
4616 let end = (right + 1).min(chars.len());
4617 let head: String = chars[..left].iter().collect();
4618 let mid: String = chars[left..end].iter().collect();
4619 let tail: String = chars[end..].iter().collect();
4620 let transformed = match op {
4621 Operator::Uppercase => mid.to_uppercase(),
4622 Operator::Lowercase => mid.to_lowercase(),
4623 Operator::ToggleCase => toggle_case_str(&mid),
4624 _ => mid,
4625 };
4626 lines[r] = format!("{head}{transformed}{tail}");
4627 }
4628 let saved_yank = ed.yank().to_string();
4629 let saved_linewise = ed.vim.yank_linewise;
4630 ed.restore(lines, (top, left));
4631 ed.set_yank(saved_yank);
4632 ed.vim.yank_linewise = saved_linewise;
4633}
4634
4635fn block_yank<H: crate::types::Host>(
4636 ed: &Editor<hjkl_buffer::Buffer, H>,
4637 top: usize,
4638 bot: usize,
4639 left: usize,
4640 right: usize,
4641) -> String {
4642 let lines = buf_lines_to_vec(&ed.buffer);
4643 let mut rows: Vec<String> = Vec::new();
4644 for r in top..=bot {
4645 let line = match lines.get(r) {
4646 Some(l) => l,
4647 None => break,
4648 };
4649 let chars: Vec<char> = line.chars().collect();
4650 let end = (right + 1).min(chars.len());
4651 if left >= chars.len() {
4652 rows.push(String::new());
4653 } else {
4654 rows.push(chars[left..end].iter().collect());
4655 }
4656 }
4657 rows.join("\n")
4658}
4659
4660fn delete_block_contents<H: crate::types::Host>(
4661 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4662 top: usize,
4663 bot: usize,
4664 left: usize,
4665 right: usize,
4666) {
4667 use hjkl_buffer::{Edit, MotionKind, Position};
4668 ed.sync_buffer_content_from_textarea();
4669 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
4670 if last_row < top {
4671 return;
4672 }
4673 ed.mutate_edit(Edit::DeleteRange {
4674 start: Position::new(top, left),
4675 end: Position::new(last_row, right),
4676 kind: MotionKind::Block,
4677 });
4678 ed.push_buffer_cursor_to_textarea();
4679}
4680
4681fn block_replace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, ch: char) {
4683 let (top, bot, left, right) = block_bounds(ed);
4684 ed.push_undo();
4685 ed.sync_buffer_content_from_textarea();
4686 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4687 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4688 let chars: Vec<char> = lines[r].chars().collect();
4689 if left >= chars.len() {
4690 continue;
4691 }
4692 let end = (right + 1).min(chars.len());
4693 let before: String = chars[..left].iter().collect();
4694 let middle: String = std::iter::repeat_n(ch, end - left).collect();
4695 let after: String = chars[end..].iter().collect();
4696 lines[r] = format!("{before}{middle}{after}");
4697 }
4698 reset_textarea_lines(ed, lines);
4699 ed.vim.mode = Mode::Normal;
4700 ed.jump_cursor(top, left);
4701}
4702
4703fn reset_textarea_lines<H: crate::types::Host>(
4707 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4708 lines: Vec<String>,
4709) {
4710 let cursor = ed.cursor();
4711 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
4712 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
4713 ed.mark_content_dirty();
4714}
4715
4716type Pos = (usize, usize);
4722
4723fn text_object_range<H: crate::types::Host>(
4727 ed: &Editor<hjkl_buffer::Buffer, H>,
4728 obj: TextObject,
4729 inner: bool,
4730) -> Option<(Pos, Pos, MotionKind)> {
4731 match obj {
4732 TextObject::Word { big } => {
4733 word_text_object(ed, inner, big).map(|(s, e)| (s, e, MotionKind::Exclusive))
4734 }
4735 TextObject::Quote(q) => {
4736 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4737 }
4738 TextObject::Bracket(open) => bracket_text_object(ed, open, inner),
4739 TextObject::Paragraph => {
4740 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Linewise))
4741 }
4742 TextObject::XmlTag => {
4743 tag_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4744 }
4745 TextObject::Sentence => {
4746 sentence_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4747 }
4748 }
4749}
4750
4751fn sentence_boundary<H: crate::types::Host>(
4755 ed: &Editor<hjkl_buffer::Buffer, H>,
4756 forward: bool,
4757) -> Option<(usize, usize)> {
4758 let lines = buf_lines_to_vec(&ed.buffer);
4759 if lines.is_empty() {
4760 return None;
4761 }
4762 let pos_to_idx = |pos: (usize, usize)| -> usize {
4763 let mut idx = 0;
4764 for line in lines.iter().take(pos.0) {
4765 idx += line.chars().count() + 1;
4766 }
4767 idx + pos.1
4768 };
4769 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4770 for (r, line) in lines.iter().enumerate() {
4771 let len = line.chars().count();
4772 if idx <= len {
4773 return (r, idx);
4774 }
4775 idx -= len + 1;
4776 }
4777 let last = lines.len().saturating_sub(1);
4778 (last, lines[last].chars().count())
4779 };
4780 let mut chars: Vec<char> = Vec::new();
4781 for (r, line) in lines.iter().enumerate() {
4782 chars.extend(line.chars());
4783 if r + 1 < lines.len() {
4784 chars.push('\n');
4785 }
4786 }
4787 if chars.is_empty() {
4788 return None;
4789 }
4790 let total = chars.len();
4791 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
4792 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4793
4794 if forward {
4795 let mut i = cursor_idx + 1;
4798 while i < total {
4799 if is_terminator(chars[i]) {
4800 while i + 1 < total && is_terminator(chars[i + 1]) {
4801 i += 1;
4802 }
4803 if i + 1 >= total {
4804 return None;
4805 }
4806 if chars[i + 1].is_whitespace() {
4807 let mut j = i + 1;
4808 while j < total && chars[j].is_whitespace() {
4809 j += 1;
4810 }
4811 if j >= total {
4812 return None;
4813 }
4814 return Some(idx_to_pos(j));
4815 }
4816 }
4817 i += 1;
4818 }
4819 None
4820 } else {
4821 let find_start = |from: usize| -> Option<usize> {
4825 let mut start = from;
4826 while start > 0 {
4827 let prev = chars[start - 1];
4828 if prev.is_whitespace() {
4829 let mut k = start - 1;
4830 while k > 0 && chars[k - 1].is_whitespace() {
4831 k -= 1;
4832 }
4833 if k > 0 && is_terminator(chars[k - 1]) {
4834 break;
4835 }
4836 }
4837 start -= 1;
4838 }
4839 while start < total && chars[start].is_whitespace() {
4840 start += 1;
4841 }
4842 (start < total).then_some(start)
4843 };
4844 let current_start = find_start(cursor_idx)?;
4845 if current_start < cursor_idx {
4846 return Some(idx_to_pos(current_start));
4847 }
4848 let mut k = current_start;
4851 while k > 0 && chars[k - 1].is_whitespace() {
4852 k -= 1;
4853 }
4854 if k == 0 {
4855 return None;
4856 }
4857 let prev_start = find_start(k - 1)?;
4858 Some(idx_to_pos(prev_start))
4859 }
4860}
4861
4862fn sentence_text_object<H: crate::types::Host>(
4868 ed: &Editor<hjkl_buffer::Buffer, H>,
4869 inner: bool,
4870) -> Option<((usize, usize), (usize, usize))> {
4871 let lines = buf_lines_to_vec(&ed.buffer);
4872 if lines.is_empty() {
4873 return None;
4874 }
4875 let pos_to_idx = |pos: (usize, usize)| -> usize {
4878 let mut idx = 0;
4879 for line in lines.iter().take(pos.0) {
4880 idx += line.chars().count() + 1;
4881 }
4882 idx + pos.1
4883 };
4884 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4885 for (r, line) in lines.iter().enumerate() {
4886 let len = line.chars().count();
4887 if idx <= len {
4888 return (r, idx);
4889 }
4890 idx -= len + 1;
4891 }
4892 let last = lines.len().saturating_sub(1);
4893 (last, lines[last].chars().count())
4894 };
4895 let mut chars: Vec<char> = Vec::new();
4896 for (r, line) in lines.iter().enumerate() {
4897 chars.extend(line.chars());
4898 if r + 1 < lines.len() {
4899 chars.push('\n');
4900 }
4901 }
4902 if chars.is_empty() {
4903 return None;
4904 }
4905
4906 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
4907 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4908
4909 let mut start = cursor_idx;
4913 while start > 0 {
4914 let prev = chars[start - 1];
4915 if prev.is_whitespace() {
4916 let mut k = start - 1;
4920 while k > 0 && chars[k - 1].is_whitespace() {
4921 k -= 1;
4922 }
4923 if k > 0 && is_terminator(chars[k - 1]) {
4924 break;
4925 }
4926 }
4927 start -= 1;
4928 }
4929 while start < chars.len() && chars[start].is_whitespace() {
4932 start += 1;
4933 }
4934 if start >= chars.len() {
4935 return None;
4936 }
4937
4938 let mut end = start;
4941 while end < chars.len() {
4942 if is_terminator(chars[end]) {
4943 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
4945 end += 1;
4946 }
4947 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
4950 break;
4951 }
4952 }
4953 end += 1;
4954 }
4955 let end_idx = (end + 1).min(chars.len());
4957
4958 let final_end = if inner {
4959 end_idx
4960 } else {
4961 let mut e = end_idx;
4965 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
4966 e += 1;
4967 }
4968 e
4969 };
4970
4971 Some((idx_to_pos(start), idx_to_pos(final_end)))
4972}
4973
4974fn tag_text_object<H: crate::types::Host>(
4978 ed: &Editor<hjkl_buffer::Buffer, H>,
4979 inner: bool,
4980) -> Option<((usize, usize), (usize, usize))> {
4981 let lines = buf_lines_to_vec(&ed.buffer);
4982 if lines.is_empty() {
4983 return None;
4984 }
4985 let pos_to_idx = |pos: (usize, usize)| -> usize {
4989 let mut idx = 0;
4990 for line in lines.iter().take(pos.0) {
4991 idx += line.chars().count() + 1;
4992 }
4993 idx + pos.1
4994 };
4995 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4996 for (r, line) in lines.iter().enumerate() {
4997 let len = line.chars().count();
4998 if idx <= len {
4999 return (r, idx);
5000 }
5001 idx -= len + 1;
5002 }
5003 let last = lines.len().saturating_sub(1);
5004 (last, lines[last].chars().count())
5005 };
5006 let mut chars: Vec<char> = Vec::new();
5007 for (r, line) in lines.iter().enumerate() {
5008 chars.extend(line.chars());
5009 if r + 1 < lines.len() {
5010 chars.push('\n');
5011 }
5012 }
5013 let cursor_idx = pos_to_idx(ed.cursor());
5014
5015 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
5023 let mut next_after: Option<(usize, usize, usize, usize)> = None;
5024 let mut i = 0;
5025 while i < chars.len() {
5026 if chars[i] != '<' {
5027 i += 1;
5028 continue;
5029 }
5030 let mut j = i + 1;
5031 while j < chars.len() && chars[j] != '>' {
5032 j += 1;
5033 }
5034 if j >= chars.len() {
5035 break;
5036 }
5037 let inside: String = chars[i + 1..j].iter().collect();
5038 let close_end = j + 1;
5039 let trimmed = inside.trim();
5040 if trimmed.starts_with('!') || trimmed.starts_with('?') {
5041 i = close_end;
5042 continue;
5043 }
5044 if let Some(rest) = trimmed.strip_prefix('/') {
5045 let name = rest.split_whitespace().next().unwrap_or("").to_string();
5046 if !name.is_empty()
5047 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
5048 {
5049 let (open_start, content_start, _) = stack[stack_idx].clone();
5050 stack.truncate(stack_idx);
5051 let content_end = i;
5052 let candidate = (open_start, content_start, content_end, close_end);
5053 if cursor_idx >= content_start && cursor_idx <= content_end {
5054 innermost = match innermost {
5055 Some((_, cs, ce, _)) if cs <= content_start && content_end <= ce => {
5056 Some(candidate)
5057 }
5058 None => Some(candidate),
5059 existing => existing,
5060 };
5061 } else if open_start >= cursor_idx && next_after.is_none() {
5062 next_after = Some(candidate);
5063 }
5064 }
5065 } else if !trimmed.ends_with('/') {
5066 let name: String = trimmed
5067 .split(|c: char| c.is_whitespace() || c == '/')
5068 .next()
5069 .unwrap_or("")
5070 .to_string();
5071 if !name.is_empty() {
5072 stack.push((i, close_end, name));
5073 }
5074 }
5075 i = close_end;
5076 }
5077
5078 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
5079 if inner {
5080 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
5081 } else {
5082 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
5083 }
5084}
5085
5086fn is_wordchar(c: char) -> bool {
5087 c.is_alphanumeric() || c == '_'
5088}
5089
5090pub(crate) use hjkl_buffer::is_keyword_char;
5094
5095fn word_text_object<H: crate::types::Host>(
5096 ed: &Editor<hjkl_buffer::Buffer, H>,
5097 inner: bool,
5098 big: bool,
5099) -> Option<((usize, usize), (usize, usize))> {
5100 let (row, col) = ed.cursor();
5101 let line = buf_line(&ed.buffer, row)?;
5102 let chars: Vec<char> = line.chars().collect();
5103 if chars.is_empty() {
5104 return None;
5105 }
5106 let at = col.min(chars.len().saturating_sub(1));
5107 let classify = |c: char| -> u8 {
5108 if c.is_whitespace() {
5109 0
5110 } else if big || is_wordchar(c) {
5111 1
5112 } else {
5113 2
5114 }
5115 };
5116 let cls = classify(chars[at]);
5117 let mut start = at;
5118 while start > 0 && classify(chars[start - 1]) == cls {
5119 start -= 1;
5120 }
5121 let mut end = at;
5122 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
5123 end += 1;
5124 }
5125 let char_byte = |i: usize| {
5127 if i >= chars.len() {
5128 line.len()
5129 } else {
5130 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
5131 }
5132 };
5133 let mut start_col = char_byte(start);
5134 let mut end_col = char_byte(end + 1);
5136 if !inner {
5137 let mut t = end + 1;
5139 let mut included_trailing = false;
5140 while t < chars.len() && chars[t].is_whitespace() {
5141 included_trailing = true;
5142 t += 1;
5143 }
5144 if included_trailing {
5145 end_col = char_byte(t);
5146 } else {
5147 let mut s = start;
5148 while s > 0 && chars[s - 1].is_whitespace() {
5149 s -= 1;
5150 }
5151 start_col = char_byte(s);
5152 }
5153 }
5154 Some(((row, start_col), (row, end_col)))
5155}
5156
5157fn quote_text_object<H: crate::types::Host>(
5158 ed: &Editor<hjkl_buffer::Buffer, H>,
5159 q: char,
5160 inner: bool,
5161) -> Option<((usize, usize), (usize, usize))> {
5162 let (row, col) = ed.cursor();
5163 let line = buf_line(&ed.buffer, row)?;
5164 let bytes = line.as_bytes();
5165 let q_byte = q as u8;
5166 let mut positions: Vec<usize> = Vec::new();
5168 for (i, &b) in bytes.iter().enumerate() {
5169 if b == q_byte {
5170 positions.push(i);
5171 }
5172 }
5173 if positions.len() < 2 {
5174 return None;
5175 }
5176 let mut open_idx: Option<usize> = None;
5177 let mut close_idx: Option<usize> = None;
5178 for pair in positions.chunks(2) {
5179 if pair.len() < 2 {
5180 break;
5181 }
5182 if col >= pair[0] && col <= pair[1] {
5183 open_idx = Some(pair[0]);
5184 close_idx = Some(pair[1]);
5185 break;
5186 }
5187 if col < pair[0] {
5188 open_idx = Some(pair[0]);
5189 close_idx = Some(pair[1]);
5190 break;
5191 }
5192 }
5193 let open = open_idx?;
5194 let close = close_idx?;
5195 if inner {
5197 if close <= open + 1 {
5198 return None;
5199 }
5200 Some(((row, open + 1), (row, close)))
5201 } else {
5202 let after_close = close + 1; if after_close < bytes.len() && bytes[after_close].is_ascii_whitespace() {
5209 let mut end = after_close;
5211 while end < bytes.len() && bytes[end].is_ascii_whitespace() {
5212 end += 1;
5213 }
5214 Some(((row, open), (row, end)))
5215 } else if open > 0 && bytes[open - 1].is_ascii_whitespace() {
5216 let mut start = open;
5218 while start > 0 && bytes[start - 1].is_ascii_whitespace() {
5219 start -= 1;
5220 }
5221 Some(((row, start), (row, close + 1)))
5222 } else {
5223 Some(((row, open), (row, close + 1)))
5224 }
5225 }
5226}
5227
5228fn bracket_text_object<H: crate::types::Host>(
5229 ed: &Editor<hjkl_buffer::Buffer, H>,
5230 open: char,
5231 inner: bool,
5232) -> Option<(Pos, Pos, MotionKind)> {
5233 let close = match open {
5234 '(' => ')',
5235 '[' => ']',
5236 '{' => '}',
5237 '<' => '>',
5238 _ => return None,
5239 };
5240 let (row, col) = ed.cursor();
5241 let lines = buf_lines_to_vec(&ed.buffer);
5242 let lines = lines.as_slice();
5243 let open_pos = find_open_bracket(lines, row, col, open, close)
5248 .or_else(|| find_next_open(lines, row, col, open))?;
5249 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
5250 if inner {
5252 if close_pos.0 > open_pos.0 + 1 {
5258 let inner_row_start = open_pos.0 + 1;
5260 let inner_row_end = close_pos.0 - 1;
5261 let end_col = lines
5262 .get(inner_row_end)
5263 .map(|l| l.chars().count())
5264 .unwrap_or(0);
5265 return Some((
5266 (inner_row_start, 0),
5267 (inner_row_end, end_col),
5268 MotionKind::Linewise,
5269 ));
5270 }
5271 let inner_start = advance_pos(lines, open_pos);
5272 if inner_start.0 > close_pos.0
5273 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
5274 {
5275 return None;
5276 }
5277 Some((inner_start, close_pos, MotionKind::Exclusive))
5278 } else {
5279 Some((
5280 open_pos,
5281 advance_pos(lines, close_pos),
5282 MotionKind::Exclusive,
5283 ))
5284 }
5285}
5286
5287fn find_open_bracket(
5288 lines: &[String],
5289 row: usize,
5290 col: usize,
5291 open: char,
5292 close: char,
5293) -> Option<(usize, usize)> {
5294 let mut depth: i32 = 0;
5295 let mut r = row;
5296 let mut c = col as isize;
5297 loop {
5298 let cur = &lines[r];
5299 let chars: Vec<char> = cur.chars().collect();
5300 if (c as usize) >= chars.len() {
5304 c = chars.len() as isize - 1;
5305 }
5306 while c >= 0 {
5307 let ch = chars[c as usize];
5308 if ch == close {
5309 depth += 1;
5310 } else if ch == open {
5311 if depth == 0 {
5312 return Some((r, c as usize));
5313 }
5314 depth -= 1;
5315 }
5316 c -= 1;
5317 }
5318 if r == 0 {
5319 return None;
5320 }
5321 r -= 1;
5322 c = lines[r].chars().count() as isize - 1;
5323 }
5324}
5325
5326fn find_close_bracket(
5327 lines: &[String],
5328 row: usize,
5329 start_col: usize,
5330 open: char,
5331 close: char,
5332) -> Option<(usize, usize)> {
5333 let mut depth: i32 = 0;
5334 let mut r = row;
5335 let mut c = start_col;
5336 loop {
5337 let cur = &lines[r];
5338 let chars: Vec<char> = cur.chars().collect();
5339 while c < chars.len() {
5340 let ch = chars[c];
5341 if ch == open {
5342 depth += 1;
5343 } else if ch == close {
5344 if depth == 0 {
5345 return Some((r, c));
5346 }
5347 depth -= 1;
5348 }
5349 c += 1;
5350 }
5351 if r + 1 >= lines.len() {
5352 return None;
5353 }
5354 r += 1;
5355 c = 0;
5356 }
5357}
5358
5359fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
5363 let mut r = row;
5364 let mut c = col;
5365 while r < lines.len() {
5366 let chars: Vec<char> = lines[r].chars().collect();
5367 while c < chars.len() {
5368 if chars[c] == open {
5369 return Some((r, c));
5370 }
5371 c += 1;
5372 }
5373 r += 1;
5374 c = 0;
5375 }
5376 None
5377}
5378
5379fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
5380 let (r, c) = pos;
5381 let line_len = lines[r].chars().count();
5382 if c < line_len {
5383 (r, c + 1)
5384 } else if r + 1 < lines.len() {
5385 (r + 1, 0)
5386 } else {
5387 pos
5388 }
5389}
5390
5391fn paragraph_text_object<H: crate::types::Host>(
5392 ed: &Editor<hjkl_buffer::Buffer, H>,
5393 inner: bool,
5394) -> Option<((usize, usize), (usize, usize))> {
5395 let (row, _) = ed.cursor();
5396 let lines = buf_lines_to_vec(&ed.buffer);
5397 if lines.is_empty() {
5398 return None;
5399 }
5400 let is_blank = |r: usize| lines.get(r).map(|s| s.trim().is_empty()).unwrap_or(true);
5402 if is_blank(row) {
5403 return None;
5404 }
5405 let mut top = row;
5406 while top > 0 && !is_blank(top - 1) {
5407 top -= 1;
5408 }
5409 let mut bot = row;
5410 while bot + 1 < lines.len() && !is_blank(bot + 1) {
5411 bot += 1;
5412 }
5413 if !inner && bot + 1 < lines.len() && is_blank(bot + 1) {
5415 bot += 1;
5416 }
5417 let end_col = lines[bot].chars().count();
5418 Some(((top, 0), (bot, end_col)))
5419}
5420
5421fn read_vim_range<H: crate::types::Host>(
5427 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5428 start: (usize, usize),
5429 end: (usize, usize),
5430 kind: MotionKind,
5431) -> String {
5432 let (top, bot) = order(start, end);
5433 ed.sync_buffer_content_from_textarea();
5434 let lines = buf_lines_to_vec(&ed.buffer);
5435 match kind {
5436 MotionKind::Linewise => {
5437 let lo = top.0;
5438 let hi = bot.0.min(lines.len().saturating_sub(1));
5439 let mut text = lines[lo..=hi].join("\n");
5440 text.push('\n');
5441 text
5442 }
5443 MotionKind::Inclusive | MotionKind::Exclusive => {
5444 let inclusive = matches!(kind, MotionKind::Inclusive);
5445 let mut out = String::new();
5447 for row in top.0..=bot.0 {
5448 let line = lines.get(row).map(String::as_str).unwrap_or("");
5449 let lo = if row == top.0 { top.1 } else { 0 };
5450 let hi_unclamped = if row == bot.0 {
5451 if inclusive { bot.1 + 1 } else { bot.1 }
5452 } else {
5453 line.chars().count() + 1
5454 };
5455 let row_chars: Vec<char> = line.chars().collect();
5456 let hi = hi_unclamped.min(row_chars.len());
5457 if lo < hi {
5458 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
5459 }
5460 if row < bot.0 {
5461 out.push('\n');
5462 }
5463 }
5464 out
5465 }
5466 }
5467}
5468
5469fn cut_vim_range<H: crate::types::Host>(
5478 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5479 start: (usize, usize),
5480 end: (usize, usize),
5481 kind: MotionKind,
5482) -> String {
5483 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
5484 let (top, bot) = order(start, end);
5485 ed.sync_buffer_content_from_textarea();
5486 let (buf_start, buf_end, buf_kind) = match kind {
5487 MotionKind::Linewise => (
5488 Position::new(top.0, 0),
5489 Position::new(bot.0, 0),
5490 BufKind::Line,
5491 ),
5492 MotionKind::Inclusive => {
5493 let line_chars = buf_line_chars(&ed.buffer, bot.0);
5494 let next = if bot.1 < line_chars {
5498 Position::new(bot.0, bot.1 + 1)
5499 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
5500 Position::new(bot.0 + 1, 0)
5501 } else {
5502 Position::new(bot.0, line_chars)
5503 };
5504 (Position::new(top.0, top.1), next, BufKind::Char)
5505 }
5506 MotionKind::Exclusive => (
5507 Position::new(top.0, top.1),
5508 Position::new(bot.0, bot.1),
5509 BufKind::Char,
5510 ),
5511 };
5512 let inverse = ed.mutate_edit(Edit::DeleteRange {
5513 start: buf_start,
5514 end: buf_end,
5515 kind: buf_kind,
5516 });
5517 let text = match inverse {
5518 Edit::InsertStr { text, .. } => text,
5519 _ => String::new(),
5520 };
5521 if !text.is_empty() {
5522 ed.record_yank_to_host(text.clone());
5523 ed.record_delete(text.clone(), matches!(kind, MotionKind::Linewise));
5524 }
5525 ed.push_buffer_cursor_to_textarea();
5526 text
5527}
5528
5529fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5535 use hjkl_buffer::{Edit, MotionKind, Position};
5536 ed.sync_buffer_content_from_textarea();
5537 let cursor = buf_cursor_pos(&ed.buffer);
5538 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5539 if cursor.col >= line_chars {
5540 return;
5541 }
5542 let inverse = ed.mutate_edit(Edit::DeleteRange {
5543 start: cursor,
5544 end: Position::new(cursor.row, line_chars),
5545 kind: MotionKind::Char,
5546 });
5547 if let Edit::InsertStr { text, .. } = inverse
5548 && !text.is_empty()
5549 {
5550 ed.record_yank_to_host(text.clone());
5551 ed.vim.yank_linewise = false;
5552 ed.set_yank(text);
5553 }
5554 buf_set_cursor_pos(&mut ed.buffer, cursor);
5555 ed.push_buffer_cursor_to_textarea();
5556}
5557
5558fn do_char_delete<H: crate::types::Host>(
5559 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5560 forward: bool,
5561 count: usize,
5562) {
5563 use hjkl_buffer::{Edit, MotionKind, Position};
5564 ed.push_undo();
5565 ed.sync_buffer_content_from_textarea();
5566 let mut deleted = String::new();
5569 for _ in 0..count {
5570 let cursor = buf_cursor_pos(&ed.buffer);
5571 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5572 if forward {
5573 if cursor.col >= line_chars {
5576 continue;
5577 }
5578 let inverse = ed.mutate_edit(Edit::DeleteRange {
5579 start: cursor,
5580 end: Position::new(cursor.row, cursor.col + 1),
5581 kind: MotionKind::Char,
5582 });
5583 if let Edit::InsertStr { text, .. } = inverse {
5584 deleted.push_str(&text);
5585 }
5586 } else {
5587 if cursor.col == 0 {
5589 continue;
5590 }
5591 let inverse = ed.mutate_edit(Edit::DeleteRange {
5592 start: Position::new(cursor.row, cursor.col - 1),
5593 end: cursor,
5594 kind: MotionKind::Char,
5595 });
5596 if let Edit::InsertStr { text, .. } = inverse {
5597 deleted = text + &deleted;
5600 }
5601 }
5602 }
5603 if !deleted.is_empty() {
5604 ed.record_yank_to_host(deleted.clone());
5605 ed.record_delete(deleted, false);
5606 }
5607 ed.push_buffer_cursor_to_textarea();
5608}
5609
5610fn adjust_number<H: crate::types::Host>(
5614 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5615 delta: i64,
5616) -> bool {
5617 use hjkl_buffer::{Edit, MotionKind, Position};
5618 ed.sync_buffer_content_from_textarea();
5619 let cursor = buf_cursor_pos(&ed.buffer);
5620 let row = cursor.row;
5621 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
5622 Some(l) => l.chars().collect(),
5623 None => return false,
5624 };
5625 let Some(digit_start) = (cursor.col..chars.len()).find(|&i| chars[i].is_ascii_digit()) else {
5626 return false;
5627 };
5628 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
5629 digit_start - 1
5630 } else {
5631 digit_start
5632 };
5633 let mut span_end = digit_start;
5634 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
5635 span_end += 1;
5636 }
5637 let s: String = chars[span_start..span_end].iter().collect();
5638 let Ok(n) = s.parse::<i64>() else {
5639 return false;
5640 };
5641 let new_s = n.saturating_add(delta).to_string();
5642
5643 ed.push_undo();
5644 let span_start_pos = Position::new(row, span_start);
5645 let span_end_pos = Position::new(row, span_end);
5646 ed.mutate_edit(Edit::DeleteRange {
5647 start: span_start_pos,
5648 end: span_end_pos,
5649 kind: MotionKind::Char,
5650 });
5651 ed.mutate_edit(Edit::InsertStr {
5652 at: span_start_pos,
5653 text: new_s.clone(),
5654 });
5655 let new_len = new_s.chars().count();
5656 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
5657 ed.push_buffer_cursor_to_textarea();
5658 true
5659}
5660
5661pub(crate) fn replace_char<H: crate::types::Host>(
5662 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5663 ch: char,
5664 count: usize,
5665) {
5666 use hjkl_buffer::{Edit, MotionKind, Position};
5667 ed.push_undo();
5668 ed.sync_buffer_content_from_textarea();
5669 for _ in 0..count {
5670 let cursor = buf_cursor_pos(&ed.buffer);
5671 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5672 if cursor.col >= line_chars {
5673 break;
5674 }
5675 ed.mutate_edit(Edit::DeleteRange {
5676 start: cursor,
5677 end: Position::new(cursor.row, cursor.col + 1),
5678 kind: MotionKind::Char,
5679 });
5680 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
5681 }
5682 crate::motions::move_left(&mut ed.buffer, 1);
5684 ed.push_buffer_cursor_to_textarea();
5685}
5686
5687fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5688 use hjkl_buffer::{Edit, MotionKind, Position};
5689 ed.sync_buffer_content_from_textarea();
5690 let cursor = buf_cursor_pos(&ed.buffer);
5691 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
5692 return;
5693 };
5694 let toggled = if c.is_uppercase() {
5695 c.to_lowercase().next().unwrap_or(c)
5696 } else {
5697 c.to_uppercase().next().unwrap_or(c)
5698 };
5699 ed.mutate_edit(Edit::DeleteRange {
5700 start: cursor,
5701 end: Position::new(cursor.row, cursor.col + 1),
5702 kind: MotionKind::Char,
5703 });
5704 ed.mutate_edit(Edit::InsertChar {
5705 at: cursor,
5706 ch: toggled,
5707 });
5708}
5709
5710fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5711 use hjkl_buffer::{Edit, Position};
5712 ed.sync_buffer_content_from_textarea();
5713 let row = buf_cursor_pos(&ed.buffer).row;
5714 if row + 1 >= buf_row_count(&ed.buffer) {
5715 return;
5716 }
5717 let cur_line = buf_line(&ed.buffer, row).unwrap_or("").to_string();
5718 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or("").to_string();
5719 let next_trimmed = next_raw.trim_start();
5720 let cur_chars = cur_line.chars().count();
5721 let next_chars = next_raw.chars().count();
5722 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
5725 " "
5726 } else {
5727 ""
5728 };
5729 let joined = format!("{cur_line}{separator}{next_trimmed}");
5730 ed.mutate_edit(Edit::Replace {
5731 start: Position::new(row, 0),
5732 end: Position::new(row + 1, next_chars),
5733 with: joined,
5734 });
5735 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
5739 ed.push_buffer_cursor_to_textarea();
5740}
5741
5742fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5745 use hjkl_buffer::Edit;
5746 ed.sync_buffer_content_from_textarea();
5747 let row = buf_cursor_pos(&ed.buffer).row;
5748 if row + 1 >= buf_row_count(&ed.buffer) {
5749 return;
5750 }
5751 let join_col = buf_line_chars(&ed.buffer, row);
5752 ed.mutate_edit(Edit::JoinLines {
5753 row,
5754 count: 1,
5755 with_space: false,
5756 });
5757 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
5759 ed.push_buffer_cursor_to_textarea();
5760}
5761
5762fn do_paste<H: crate::types::Host>(
5763 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5764 before: bool,
5765 count: usize,
5766) {
5767 use hjkl_buffer::{Edit, Position};
5768 ed.push_undo();
5769 let selector = ed.vim.pending_register.take();
5774 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
5775 Some(slot) => (slot.text.clone(), slot.linewise),
5776 None => {
5782 let s = &ed.registers().unnamed;
5783 (s.text.clone(), s.linewise)
5784 }
5785 };
5786 let mut paste_mark: Option<((usize, usize), (usize, usize))> = None;
5790 for _ in 0..count {
5791 ed.sync_buffer_content_from_textarea();
5792 let yank = yank.clone();
5793 if yank.is_empty() {
5794 continue;
5795 }
5796 if linewise {
5797 let text = yank.trim_matches('\n').to_string();
5801 let row = buf_cursor_pos(&ed.buffer).row;
5802 let target_row = if before {
5803 ed.mutate_edit(Edit::InsertStr {
5804 at: Position::new(row, 0),
5805 text: format!("{text}\n"),
5806 });
5807 row
5808 } else {
5809 let line_chars = buf_line_chars(&ed.buffer, row);
5810 ed.mutate_edit(Edit::InsertStr {
5811 at: Position::new(row, line_chars),
5812 text: format!("\n{text}"),
5813 });
5814 row + 1
5815 };
5816 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
5817 crate::motions::move_first_non_blank(&mut ed.buffer);
5818 ed.push_buffer_cursor_to_textarea();
5819 let payload_lines = text.lines().count().max(1);
5821 let bot_row = target_row + payload_lines - 1;
5822 let bot_last_col = buf_line_chars(&ed.buffer, bot_row).saturating_sub(1);
5823 paste_mark = Some(((target_row, 0), (bot_row, bot_last_col)));
5824 } else {
5825 let cursor = buf_cursor_pos(&ed.buffer);
5829 let at = if before {
5830 cursor
5831 } else {
5832 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5833 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
5834 };
5835 ed.mutate_edit(Edit::InsertStr {
5836 at,
5837 text: yank.clone(),
5838 });
5839 crate::motions::move_left(&mut ed.buffer, 1);
5842 ed.push_buffer_cursor_to_textarea();
5843 let lo = (at.row, at.col);
5845 let hi = ed.cursor();
5846 paste_mark = Some((lo, hi));
5847 }
5848 }
5849 if let Some((lo, hi)) = paste_mark {
5850 ed.set_mark('[', lo);
5851 ed.set_mark(']', hi);
5852 }
5853 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
5855}
5856
5857pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5858 if let Some((lines, cursor)) = ed.undo_stack.pop() {
5859 let current = ed.snapshot();
5860 ed.redo_stack.push(current);
5861 ed.restore(lines, cursor);
5862 }
5863 ed.vim.mode = Mode::Normal;
5864 clamp_cursor_to_normal_mode(ed);
5868}
5869
5870pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5871 if let Some((lines, cursor)) = ed.redo_stack.pop() {
5872 let current = ed.snapshot();
5873 ed.undo_stack.push(current);
5874 ed.cap_undo();
5875 ed.restore(lines, cursor);
5876 }
5877 ed.vim.mode = Mode::Normal;
5878}
5879
5880fn replay_insert_and_finish<H: crate::types::Host>(
5887 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5888 text: &str,
5889) {
5890 use hjkl_buffer::{Edit, Position};
5891 let cursor = ed.cursor();
5892 ed.mutate_edit(Edit::InsertStr {
5893 at: Position::new(cursor.0, cursor.1),
5894 text: text.to_string(),
5895 });
5896 if ed.vim.insert_session.take().is_some() {
5897 if ed.cursor().1 > 0 {
5898 crate::motions::move_left(&mut ed.buffer, 1);
5899 ed.push_buffer_cursor_to_textarea();
5900 }
5901 ed.vim.mode = Mode::Normal;
5902 }
5903}
5904
5905fn replay_last_change<H: crate::types::Host>(
5906 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5907 outer_count: usize,
5908) {
5909 let Some(change) = ed.vim.last_change.clone() else {
5910 return;
5911 };
5912 ed.vim.replaying = true;
5913 let scale = if outer_count > 0 { outer_count } else { 1 };
5914 match change {
5915 LastChange::OpMotion {
5916 op,
5917 motion,
5918 count,
5919 inserted,
5920 } => {
5921 let total = count.max(1) * scale;
5922 apply_op_with_motion(ed, op, &motion, total);
5923 if let Some(text) = inserted {
5924 replay_insert_and_finish(ed, &text);
5925 }
5926 }
5927 LastChange::OpTextObj {
5928 op,
5929 obj,
5930 inner,
5931 inserted,
5932 } => {
5933 apply_op_with_text_object(ed, op, obj, inner);
5934 if let Some(text) = inserted {
5935 replay_insert_and_finish(ed, &text);
5936 }
5937 }
5938 LastChange::LineOp {
5939 op,
5940 count,
5941 inserted,
5942 } => {
5943 let total = count.max(1) * scale;
5944 execute_line_op(ed, op, total);
5945 if let Some(text) = inserted {
5946 replay_insert_and_finish(ed, &text);
5947 }
5948 }
5949 LastChange::CharDel { forward, count } => {
5950 do_char_delete(ed, forward, count * scale);
5951 }
5952 LastChange::ReplaceChar { ch, count } => {
5953 replace_char(ed, ch, count * scale);
5954 }
5955 LastChange::ToggleCase { count } => {
5956 for _ in 0..count * scale {
5957 ed.push_undo();
5958 toggle_case_at_cursor(ed);
5959 }
5960 }
5961 LastChange::JoinLine { count } => {
5962 for _ in 0..count * scale {
5963 ed.push_undo();
5964 join_line(ed);
5965 }
5966 }
5967 LastChange::Paste { before, count } => {
5968 do_paste(ed, before, count * scale);
5969 }
5970 LastChange::DeleteToEol { inserted } => {
5971 use hjkl_buffer::{Edit, Position};
5972 ed.push_undo();
5973 delete_to_eol(ed);
5974 if let Some(text) = inserted {
5975 let cursor = ed.cursor();
5976 ed.mutate_edit(Edit::InsertStr {
5977 at: Position::new(cursor.0, cursor.1),
5978 text,
5979 });
5980 }
5981 }
5982 LastChange::OpenLine { above, inserted } => {
5983 use hjkl_buffer::{Edit, Position};
5984 ed.push_undo();
5985 ed.sync_buffer_content_from_textarea();
5986 let row = buf_cursor_pos(&ed.buffer).row;
5987 if above {
5988 ed.mutate_edit(Edit::InsertStr {
5989 at: Position::new(row, 0),
5990 text: "\n".to_string(),
5991 });
5992 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
5993 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
5994 } else {
5995 let line_chars = buf_line_chars(&ed.buffer, row);
5996 ed.mutate_edit(Edit::InsertStr {
5997 at: Position::new(row, line_chars),
5998 text: "\n".to_string(),
5999 });
6000 }
6001 ed.push_buffer_cursor_to_textarea();
6002 let cursor = ed.cursor();
6003 ed.mutate_edit(Edit::InsertStr {
6004 at: Position::new(cursor.0, cursor.1),
6005 text: inserted,
6006 });
6007 }
6008 LastChange::InsertAt {
6009 entry,
6010 inserted,
6011 count,
6012 } => {
6013 use hjkl_buffer::{Edit, Position};
6014 ed.push_undo();
6015 match entry {
6016 InsertEntry::I => {}
6017 InsertEntry::ShiftI => move_first_non_whitespace(ed),
6018 InsertEntry::A => {
6019 crate::motions::move_right_to_end(&mut ed.buffer, 1);
6020 ed.push_buffer_cursor_to_textarea();
6021 }
6022 InsertEntry::ShiftA => {
6023 crate::motions::move_line_end(&mut ed.buffer);
6024 crate::motions::move_right_to_end(&mut ed.buffer, 1);
6025 ed.push_buffer_cursor_to_textarea();
6026 }
6027 }
6028 for _ in 0..count.max(1) {
6029 let cursor = ed.cursor();
6030 ed.mutate_edit(Edit::InsertStr {
6031 at: Position::new(cursor.0, cursor.1),
6032 text: inserted.clone(),
6033 });
6034 }
6035 }
6036 }
6037 ed.vim.replaying = false;
6038}
6039
6040fn extract_inserted(before: &str, after: &str) -> String {
6043 let before_chars: Vec<char> = before.chars().collect();
6044 let after_chars: Vec<char> = after.chars().collect();
6045 if after_chars.len() <= before_chars.len() {
6046 return String::new();
6047 }
6048 let prefix = before_chars
6049 .iter()
6050 .zip(after_chars.iter())
6051 .take_while(|(a, b)| a == b)
6052 .count();
6053 let max_suffix = before_chars.len() - prefix;
6054 let suffix = before_chars
6055 .iter()
6056 .rev()
6057 .zip(after_chars.iter().rev())
6058 .take(max_suffix)
6059 .take_while(|(a, b)| a == b)
6060 .count();
6061 after_chars[prefix..after_chars.len() - suffix]
6062 .iter()
6063 .collect()
6064}
6065
6066#[cfg(all(test, feature = "crossterm"))]
6069mod tests {
6070 use crate::VimMode;
6071 use crate::editor::Editor;
6072 use crate::types::Host;
6073 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
6074
6075 fn run_keys<H: crate::types::Host>(e: &mut Editor<hjkl_buffer::Buffer, H>, keys: &str) {
6076 let mut iter = keys.chars().peekable();
6080 while let Some(c) = iter.next() {
6081 if c == '<' {
6082 let mut tag = String::new();
6083 for ch in iter.by_ref() {
6084 if ch == '>' {
6085 break;
6086 }
6087 tag.push(ch);
6088 }
6089 let ev = match tag.as_str() {
6090 "Esc" => KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
6091 "CR" => KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
6092 "BS" => KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE),
6093 "Space" => KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE),
6094 "Up" => KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
6095 "Down" => KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
6096 "Left" => KeyEvent::new(KeyCode::Left, KeyModifiers::NONE),
6097 "Right" => KeyEvent::new(KeyCode::Right, KeyModifiers::NONE),
6098 "lt" => KeyEvent::new(KeyCode::Char('<'), KeyModifiers::NONE),
6102 s if s.starts_with("C-") => {
6103 let ch = s.chars().nth(2).unwrap();
6104 KeyEvent::new(KeyCode::Char(ch), KeyModifiers::CONTROL)
6105 }
6106 _ => continue,
6107 };
6108 e.handle_key(ev);
6109 } else {
6110 let mods = if c.is_uppercase() {
6111 KeyModifiers::SHIFT
6112 } else {
6113 KeyModifiers::NONE
6114 };
6115 e.handle_key(KeyEvent::new(KeyCode::Char(c), mods));
6116 }
6117 }
6118 }
6119
6120 fn editor_with(content: &str) -> Editor {
6121 let opts = crate::types::Options {
6126 shiftwidth: 2,
6127 ..crate::types::Options::default()
6128 };
6129 let mut e = Editor::new(
6130 hjkl_buffer::Buffer::new(),
6131 crate::types::DefaultHost::new(),
6132 opts,
6133 );
6134 e.set_content(content);
6135 e
6136 }
6137
6138 #[test]
6139 fn f_char_jumps_on_line() {
6140 let mut e = editor_with("hello world");
6141 run_keys(&mut e, "fw");
6142 assert_eq!(e.cursor(), (0, 6));
6143 }
6144
6145 #[test]
6146 fn cap_f_jumps_backward() {
6147 let mut e = editor_with("hello world");
6148 e.jump_cursor(0, 10);
6149 run_keys(&mut e, "Fo");
6150 assert_eq!(e.cursor().1, 7);
6151 }
6152
6153 #[test]
6154 fn t_stops_before_char() {
6155 let mut e = editor_with("hello");
6156 run_keys(&mut e, "tl");
6157 assert_eq!(e.cursor(), (0, 1));
6158 }
6159
6160 #[test]
6161 fn semicolon_repeats_find() {
6162 let mut e = editor_with("aa.bb.cc");
6163 run_keys(&mut e, "f.");
6164 assert_eq!(e.cursor().1, 2);
6165 run_keys(&mut e, ";");
6166 assert_eq!(e.cursor().1, 5);
6167 }
6168
6169 #[test]
6170 fn comma_repeats_find_reverse() {
6171 let mut e = editor_with("aa.bb.cc");
6172 run_keys(&mut e, "f.");
6173 run_keys(&mut e, ";");
6174 run_keys(&mut e, ",");
6175 assert_eq!(e.cursor().1, 2);
6176 }
6177
6178 #[test]
6179 fn di_quote_deletes_content() {
6180 let mut e = editor_with("foo \"bar\" baz");
6181 e.jump_cursor(0, 6); run_keys(&mut e, "di\"");
6183 assert_eq!(e.buffer().lines()[0], "foo \"\" baz");
6184 }
6185
6186 #[test]
6187 fn da_quote_deletes_with_quotes() {
6188 let mut e = editor_with("foo \"bar\" baz");
6191 e.jump_cursor(0, 6);
6192 run_keys(&mut e, "da\"");
6193 assert_eq!(e.buffer().lines()[0], "foo baz");
6194 }
6195
6196 #[test]
6197 fn ci_paren_deletes_and_inserts() {
6198 let mut e = editor_with("fn(a, b, c)");
6199 e.jump_cursor(0, 5);
6200 run_keys(&mut e, "ci(");
6201 assert_eq!(e.vim_mode(), VimMode::Insert);
6202 assert_eq!(e.buffer().lines()[0], "fn()");
6203 }
6204
6205 #[test]
6206 fn diw_deletes_inner_word() {
6207 let mut e = editor_with("hello world");
6208 e.jump_cursor(0, 2);
6209 run_keys(&mut e, "diw");
6210 assert_eq!(e.buffer().lines()[0], " world");
6211 }
6212
6213 #[test]
6214 fn daw_deletes_word_with_trailing_space() {
6215 let mut e = editor_with("hello world");
6216 run_keys(&mut e, "daw");
6217 assert_eq!(e.buffer().lines()[0], "world");
6218 }
6219
6220 #[test]
6221 fn percent_jumps_to_matching_bracket() {
6222 let mut e = editor_with("foo(bar)");
6223 e.jump_cursor(0, 3);
6224 run_keys(&mut e, "%");
6225 assert_eq!(e.cursor().1, 7);
6226 run_keys(&mut e, "%");
6227 assert_eq!(e.cursor().1, 3);
6228 }
6229
6230 #[test]
6231 fn dot_repeats_last_change() {
6232 let mut e = editor_with("aaa bbb ccc");
6233 run_keys(&mut e, "dw");
6234 assert_eq!(e.buffer().lines()[0], "bbb ccc");
6235 run_keys(&mut e, ".");
6236 assert_eq!(e.buffer().lines()[0], "ccc");
6237 }
6238
6239 #[test]
6240 fn dot_repeats_change_operator_with_text() {
6241 let mut e = editor_with("foo foo foo");
6242 run_keys(&mut e, "cwbar<Esc>");
6243 assert_eq!(e.buffer().lines()[0], "bar foo foo");
6244 run_keys(&mut e, "w");
6246 run_keys(&mut e, ".");
6247 assert_eq!(e.buffer().lines()[0], "bar bar foo");
6248 }
6249
6250 #[test]
6251 fn dot_repeats_x() {
6252 let mut e = editor_with("abcdef");
6253 run_keys(&mut e, "x");
6254 run_keys(&mut e, "..");
6255 assert_eq!(e.buffer().lines()[0], "def");
6256 }
6257
6258 #[test]
6259 fn count_operator_motion_compose() {
6260 let mut e = editor_with("one two three four five");
6261 run_keys(&mut e, "d3w");
6262 assert_eq!(e.buffer().lines()[0], "four five");
6263 }
6264
6265 #[test]
6266 fn two_dd_deletes_two_lines() {
6267 let mut e = editor_with("a\nb\nc");
6268 run_keys(&mut e, "2dd");
6269 assert_eq!(e.buffer().lines().len(), 1);
6270 assert_eq!(e.buffer().lines()[0], "c");
6271 }
6272
6273 #[test]
6278 fn dd_in_middle_puts_cursor_on_first_non_blank_of_next() {
6279 let mut e = editor_with("one\ntwo\n three\nfour");
6280 e.jump_cursor(1, 2);
6281 run_keys(&mut e, "dd");
6282 assert_eq!(e.buffer().lines()[1], " three");
6284 assert_eq!(e.cursor(), (1, 4));
6285 }
6286
6287 #[test]
6288 fn dd_on_last_line_puts_cursor_on_first_non_blank_of_prev() {
6289 let mut e = editor_with("one\n two\nthree");
6290 e.jump_cursor(2, 0);
6291 run_keys(&mut e, "dd");
6292 assert_eq!(e.buffer().lines().len(), 2);
6294 assert_eq!(e.cursor(), (1, 2));
6295 }
6296
6297 #[test]
6298 fn dd_on_only_line_leaves_empty_buffer_and_cursor_at_zero() {
6299 let mut e = editor_with("lonely");
6300 run_keys(&mut e, "dd");
6301 assert_eq!(e.buffer().lines().len(), 1);
6302 assert_eq!(e.buffer().lines()[0], "");
6303 assert_eq!(e.cursor(), (0, 0));
6304 }
6305
6306 #[test]
6307 fn count_dd_puts_cursor_on_first_non_blank_of_remaining() {
6308 let mut e = editor_with("a\nb\nc\n d\ne");
6309 e.jump_cursor(1, 0);
6311 run_keys(&mut e, "3dd");
6312 assert_eq!(e.buffer().lines(), &["a".to_string(), "e".to_string()]);
6313 assert_eq!(e.cursor(), (1, 0));
6314 }
6315
6316 #[test]
6317 fn dd_then_j_uses_first_non_blank_not_sticky_col() {
6318 let mut e = editor_with(" line one\n line two\n xyz!");
6337 e.jump_cursor(0, 8);
6339 assert_eq!(e.cursor(), (0, 8));
6340 run_keys(&mut e, "dd");
6343 assert_eq!(
6344 e.cursor(),
6345 (0, 4),
6346 "dd must place cursor on first-non-blank"
6347 );
6348 run_keys(&mut e, "j");
6352 let (row, col) = e.cursor();
6353 assert_eq!(row, 1);
6354 assert_eq!(
6355 col, 4,
6356 "after dd, j should use the column dd established (4), not pre-dd sticky_col (8)"
6357 );
6358 }
6359
6360 #[test]
6361 fn gu_lowercases_motion_range() {
6362 let mut e = editor_with("HELLO WORLD");
6363 run_keys(&mut e, "guw");
6364 assert_eq!(e.buffer().lines()[0], "hello WORLD");
6365 assert_eq!(e.cursor(), (0, 0));
6366 }
6367
6368 #[test]
6369 fn g_u_uppercases_text_object() {
6370 let mut e = editor_with("hello world");
6371 run_keys(&mut e, "gUiw");
6373 assert_eq!(e.buffer().lines()[0], "HELLO world");
6374 assert_eq!(e.cursor(), (0, 0));
6375 }
6376
6377 #[test]
6378 fn g_tilde_toggles_case_of_range() {
6379 let mut e = editor_with("Hello World");
6380 run_keys(&mut e, "g~iw");
6381 assert_eq!(e.buffer().lines()[0], "hELLO World");
6382 }
6383
6384 #[test]
6385 fn g_uu_uppercases_current_line() {
6386 let mut e = editor_with("select 1\nselect 2");
6387 run_keys(&mut e, "gUU");
6388 assert_eq!(e.buffer().lines()[0], "SELECT 1");
6389 assert_eq!(e.buffer().lines()[1], "select 2");
6390 }
6391
6392 #[test]
6393 fn gugu_lowercases_current_line() {
6394 let mut e = editor_with("FOO BAR\nBAZ");
6395 run_keys(&mut e, "gugu");
6396 assert_eq!(e.buffer().lines()[0], "foo bar");
6397 }
6398
6399 #[test]
6400 fn visual_u_uppercases_selection() {
6401 let mut e = editor_with("hello world");
6402 run_keys(&mut e, "veU");
6404 assert_eq!(e.buffer().lines()[0], "HELLO world");
6405 }
6406
6407 #[test]
6408 fn visual_line_u_lowercases_line() {
6409 let mut e = editor_with("HELLO WORLD\nOTHER");
6410 run_keys(&mut e, "Vu");
6411 assert_eq!(e.buffer().lines()[0], "hello world");
6412 assert_eq!(e.buffer().lines()[1], "OTHER");
6413 }
6414
6415 #[test]
6416 fn g_uu_with_count_uppercases_multiple_lines() {
6417 let mut e = editor_with("one\ntwo\nthree\nfour");
6418 run_keys(&mut e, "3gUU");
6420 assert_eq!(e.buffer().lines()[0], "ONE");
6421 assert_eq!(e.buffer().lines()[1], "TWO");
6422 assert_eq!(e.buffer().lines()[2], "THREE");
6423 assert_eq!(e.buffer().lines()[3], "four");
6424 }
6425
6426 #[test]
6427 fn double_gt_indents_current_line() {
6428 let mut e = editor_with("hello");
6429 run_keys(&mut e, ">>");
6430 assert_eq!(e.buffer().lines()[0], " hello");
6431 assert_eq!(e.cursor(), (0, 2));
6433 }
6434
6435 #[test]
6436 fn double_lt_outdents_current_line() {
6437 let mut e = editor_with(" hello");
6438 run_keys(&mut e, "<lt><lt>");
6439 assert_eq!(e.buffer().lines()[0], " hello");
6440 assert_eq!(e.cursor(), (0, 2));
6441 }
6442
6443 #[test]
6444 fn count_double_gt_indents_multiple_lines() {
6445 let mut e = editor_with("a\nb\nc\nd");
6446 run_keys(&mut e, "3>>");
6448 assert_eq!(e.buffer().lines()[0], " a");
6449 assert_eq!(e.buffer().lines()[1], " b");
6450 assert_eq!(e.buffer().lines()[2], " c");
6451 assert_eq!(e.buffer().lines()[3], "d");
6452 }
6453
6454 #[test]
6455 fn outdent_clips_ragged_leading_whitespace() {
6456 let mut e = editor_with(" x");
6459 run_keys(&mut e, "<lt><lt>");
6460 assert_eq!(e.buffer().lines()[0], "x");
6461 }
6462
6463 #[test]
6464 fn indent_motion_is_always_linewise() {
6465 let mut e = editor_with("foo bar");
6468 run_keys(&mut e, ">w");
6469 assert_eq!(e.buffer().lines()[0], " foo bar");
6470 }
6471
6472 #[test]
6473 fn indent_text_object_extends_over_paragraph() {
6474 let mut e = editor_with("a\nb\n\nc\nd");
6475 run_keys(&mut e, ">ap");
6477 assert_eq!(e.buffer().lines()[0], " a");
6478 assert_eq!(e.buffer().lines()[1], " b");
6479 assert_eq!(e.buffer().lines()[2], "");
6480 assert_eq!(e.buffer().lines()[3], "c");
6481 }
6482
6483 #[test]
6484 fn visual_line_indent_shifts_selected_rows() {
6485 let mut e = editor_with("x\ny\nz");
6486 run_keys(&mut e, "Vj>");
6488 assert_eq!(e.buffer().lines()[0], " x");
6489 assert_eq!(e.buffer().lines()[1], " y");
6490 assert_eq!(e.buffer().lines()[2], "z");
6491 }
6492
6493 #[test]
6494 fn outdent_empty_line_is_noop() {
6495 let mut e = editor_with("\nfoo");
6496 run_keys(&mut e, "<lt><lt>");
6497 assert_eq!(e.buffer().lines()[0], "");
6498 }
6499
6500 #[test]
6501 fn indent_skips_empty_lines() {
6502 let mut e = editor_with("");
6505 run_keys(&mut e, ">>");
6506 assert_eq!(e.buffer().lines()[0], "");
6507 }
6508
6509 #[test]
6510 fn insert_ctrl_t_indents_current_line() {
6511 let mut e = editor_with("x");
6512 run_keys(&mut e, "i<C-t>");
6514 assert_eq!(e.buffer().lines()[0], " x");
6515 assert_eq!(e.cursor(), (0, 2));
6518 }
6519
6520 #[test]
6521 fn insert_ctrl_d_outdents_current_line() {
6522 let mut e = editor_with(" x");
6523 run_keys(&mut e, "A<C-d>");
6525 assert_eq!(e.buffer().lines()[0], " x");
6526 }
6527
6528 #[test]
6529 fn h_at_col_zero_does_not_wrap_to_prev_line() {
6530 let mut e = editor_with("first\nsecond");
6531 e.jump_cursor(1, 0);
6532 run_keys(&mut e, "h");
6533 assert_eq!(e.cursor(), (1, 0));
6535 }
6536
6537 #[test]
6538 fn l_at_last_char_does_not_wrap_to_next_line() {
6539 let mut e = editor_with("ab\ncd");
6540 e.jump_cursor(0, 1);
6542 run_keys(&mut e, "l");
6543 assert_eq!(e.cursor(), (0, 1));
6545 }
6546
6547 #[test]
6548 fn count_l_clamps_at_line_end() {
6549 let mut e = editor_with("abcde");
6550 run_keys(&mut e, "20l");
6553 assert_eq!(e.cursor(), (0, 4));
6554 }
6555
6556 #[test]
6557 fn count_h_clamps_at_col_zero() {
6558 let mut e = editor_with("abcde");
6559 e.jump_cursor(0, 3);
6560 run_keys(&mut e, "20h");
6561 assert_eq!(e.cursor(), (0, 0));
6562 }
6563
6564 #[test]
6565 fn dl_on_last_char_still_deletes_it() {
6566 let mut e = editor_with("ab");
6570 e.jump_cursor(0, 1);
6571 run_keys(&mut e, "dl");
6572 assert_eq!(e.buffer().lines()[0], "a");
6573 }
6574
6575 #[test]
6576 fn case_op_preserves_yank_register() {
6577 let mut e = editor_with("target");
6578 run_keys(&mut e, "yy");
6579 let yank_before = e.yank().to_string();
6580 run_keys(&mut e, "gUU");
6582 assert_eq!(e.buffer().lines()[0], "TARGET");
6583 assert_eq!(
6584 e.yank(),
6585 yank_before,
6586 "case ops must preserve the yank buffer"
6587 );
6588 }
6589
6590 #[test]
6591 fn dap_deletes_paragraph() {
6592 let mut e = editor_with("a\nb\n\nc\nd");
6593 run_keys(&mut e, "dap");
6594 assert_eq!(e.buffer().lines().first().map(String::as_str), Some("c"));
6595 }
6596
6597 #[test]
6598 fn dit_deletes_inner_tag_content() {
6599 let mut e = editor_with("<b>hello</b>");
6600 e.jump_cursor(0, 4);
6602 run_keys(&mut e, "dit");
6603 assert_eq!(e.buffer().lines()[0], "<b></b>");
6604 }
6605
6606 #[test]
6607 fn dat_deletes_around_tag() {
6608 let mut e = editor_with("hi <b>foo</b> bye");
6609 e.jump_cursor(0, 6);
6610 run_keys(&mut e, "dat");
6611 assert_eq!(e.buffer().lines()[0], "hi bye");
6612 }
6613
6614 #[test]
6615 fn dit_picks_innermost_tag() {
6616 let mut e = editor_with("<a><b>x</b></a>");
6617 e.jump_cursor(0, 6);
6619 run_keys(&mut e, "dit");
6620 assert_eq!(e.buffer().lines()[0], "<a><b></b></a>");
6622 }
6623
6624 #[test]
6625 fn dat_innermost_tag_pair() {
6626 let mut e = editor_with("<a><b>x</b></a>");
6627 e.jump_cursor(0, 6);
6628 run_keys(&mut e, "dat");
6629 assert_eq!(e.buffer().lines()[0], "<a></a>");
6630 }
6631
6632 #[test]
6633 fn dit_outside_any_tag_no_op() {
6634 let mut e = editor_with("plain text");
6635 e.jump_cursor(0, 3);
6636 run_keys(&mut e, "dit");
6637 assert_eq!(e.buffer().lines()[0], "plain text");
6639 }
6640
6641 #[test]
6642 fn cit_changes_inner_tag_content() {
6643 let mut e = editor_with("<b>hello</b>");
6644 e.jump_cursor(0, 4);
6645 run_keys(&mut e, "citNEW<Esc>");
6646 assert_eq!(e.buffer().lines()[0], "<b>NEW</b>");
6647 }
6648
6649 #[test]
6650 fn cat_changes_around_tag() {
6651 let mut e = editor_with("hi <b>foo</b> bye");
6652 e.jump_cursor(0, 6);
6653 run_keys(&mut e, "catBAR<Esc>");
6654 assert_eq!(e.buffer().lines()[0], "hi BAR bye");
6655 }
6656
6657 #[test]
6658 fn yit_yanks_inner_tag_content() {
6659 let mut e = editor_with("<b>hello</b>");
6660 e.jump_cursor(0, 4);
6661 run_keys(&mut e, "yit");
6662 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6663 }
6664
6665 #[test]
6666 fn yat_yanks_full_tag_pair() {
6667 let mut e = editor_with("hi <b>foo</b> bye");
6668 e.jump_cursor(0, 6);
6669 run_keys(&mut e, "yat");
6670 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6671 }
6672
6673 #[test]
6674 fn vit_visually_selects_inner_tag() {
6675 let mut e = editor_with("<b>hello</b>");
6676 e.jump_cursor(0, 4);
6677 run_keys(&mut e, "vit");
6678 assert_eq!(e.vim_mode(), VimMode::Visual);
6679 run_keys(&mut e, "y");
6680 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6681 }
6682
6683 #[test]
6684 fn vat_visually_selects_around_tag() {
6685 let mut e = editor_with("x<b>foo</b>y");
6686 e.jump_cursor(0, 5);
6687 run_keys(&mut e, "vat");
6688 assert_eq!(e.vim_mode(), VimMode::Visual);
6689 run_keys(&mut e, "y");
6690 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6691 }
6692
6693 #[test]
6696 #[allow(non_snake_case)]
6697 fn diW_deletes_inner_big_word() {
6698 let mut e = editor_with("foo.bar baz");
6699 e.jump_cursor(0, 2);
6700 run_keys(&mut e, "diW");
6701 assert_eq!(e.buffer().lines()[0], " baz");
6703 }
6704
6705 #[test]
6706 #[allow(non_snake_case)]
6707 fn daW_deletes_around_big_word() {
6708 let mut e = editor_with("foo.bar baz");
6709 e.jump_cursor(0, 2);
6710 run_keys(&mut e, "daW");
6711 assert_eq!(e.buffer().lines()[0], "baz");
6712 }
6713
6714 #[test]
6715 fn di_double_quote_deletes_inside() {
6716 let mut e = editor_with("a \"hello\" b");
6717 e.jump_cursor(0, 4);
6718 run_keys(&mut e, "di\"");
6719 assert_eq!(e.buffer().lines()[0], "a \"\" b");
6720 }
6721
6722 #[test]
6723 fn da_double_quote_deletes_around() {
6724 let mut e = editor_with("a \"hello\" b");
6726 e.jump_cursor(0, 4);
6727 run_keys(&mut e, "da\"");
6728 assert_eq!(e.buffer().lines()[0], "a b");
6729 }
6730
6731 #[test]
6732 fn di_single_quote_deletes_inside() {
6733 let mut e = editor_with("x 'foo' y");
6734 e.jump_cursor(0, 4);
6735 run_keys(&mut e, "di'");
6736 assert_eq!(e.buffer().lines()[0], "x '' y");
6737 }
6738
6739 #[test]
6740 fn da_single_quote_deletes_around() {
6741 let mut e = editor_with("x 'foo' y");
6743 e.jump_cursor(0, 4);
6744 run_keys(&mut e, "da'");
6745 assert_eq!(e.buffer().lines()[0], "x y");
6746 }
6747
6748 #[test]
6749 fn di_backtick_deletes_inside() {
6750 let mut e = editor_with("p `q` r");
6751 e.jump_cursor(0, 3);
6752 run_keys(&mut e, "di`");
6753 assert_eq!(e.buffer().lines()[0], "p `` r");
6754 }
6755
6756 #[test]
6757 fn da_backtick_deletes_around() {
6758 let mut e = editor_with("p `q` r");
6760 e.jump_cursor(0, 3);
6761 run_keys(&mut e, "da`");
6762 assert_eq!(e.buffer().lines()[0], "p r");
6763 }
6764
6765 #[test]
6766 fn di_paren_deletes_inside() {
6767 let mut e = editor_with("f(arg)");
6768 e.jump_cursor(0, 3);
6769 run_keys(&mut e, "di(");
6770 assert_eq!(e.buffer().lines()[0], "f()");
6771 }
6772
6773 #[test]
6774 fn di_paren_alias_b_works() {
6775 let mut e = editor_with("f(arg)");
6776 e.jump_cursor(0, 3);
6777 run_keys(&mut e, "dib");
6778 assert_eq!(e.buffer().lines()[0], "f()");
6779 }
6780
6781 #[test]
6782 fn di_bracket_deletes_inside() {
6783 let mut e = editor_with("a[b,c]d");
6784 e.jump_cursor(0, 3);
6785 run_keys(&mut e, "di[");
6786 assert_eq!(e.buffer().lines()[0], "a[]d");
6787 }
6788
6789 #[test]
6790 fn da_bracket_deletes_around() {
6791 let mut e = editor_with("a[b,c]d");
6792 e.jump_cursor(0, 3);
6793 run_keys(&mut e, "da[");
6794 assert_eq!(e.buffer().lines()[0], "ad");
6795 }
6796
6797 #[test]
6798 fn di_brace_deletes_inside() {
6799 let mut e = editor_with("x{y}z");
6800 e.jump_cursor(0, 2);
6801 run_keys(&mut e, "di{");
6802 assert_eq!(e.buffer().lines()[0], "x{}z");
6803 }
6804
6805 #[test]
6806 fn da_brace_deletes_around() {
6807 let mut e = editor_with("x{y}z");
6808 e.jump_cursor(0, 2);
6809 run_keys(&mut e, "da{");
6810 assert_eq!(e.buffer().lines()[0], "xz");
6811 }
6812
6813 #[test]
6814 fn di_brace_alias_capital_b_works() {
6815 let mut e = editor_with("x{y}z");
6816 e.jump_cursor(0, 2);
6817 run_keys(&mut e, "diB");
6818 assert_eq!(e.buffer().lines()[0], "x{}z");
6819 }
6820
6821 #[test]
6822 fn di_angle_deletes_inside() {
6823 let mut e = editor_with("p<q>r");
6824 e.jump_cursor(0, 2);
6825 run_keys(&mut e, "di<lt>");
6827 assert_eq!(e.buffer().lines()[0], "p<>r");
6828 }
6829
6830 #[test]
6831 fn da_angle_deletes_around() {
6832 let mut e = editor_with("p<q>r");
6833 e.jump_cursor(0, 2);
6834 run_keys(&mut e, "da<lt>");
6835 assert_eq!(e.buffer().lines()[0], "pr");
6836 }
6837
6838 #[test]
6839 fn dip_deletes_inner_paragraph() {
6840 let mut e = editor_with("a\nb\nc\n\nd");
6841 e.jump_cursor(1, 0);
6842 run_keys(&mut e, "dip");
6843 assert_eq!(e.buffer().lines(), vec!["".to_string(), "d".into()]);
6846 }
6847
6848 #[test]
6851 fn sentence_motion_close_paren_jumps_forward() {
6852 let mut e = editor_with("Alpha. Beta. Gamma.");
6853 e.jump_cursor(0, 0);
6854 run_keys(&mut e, ")");
6855 assert_eq!(e.cursor(), (0, 7));
6857 run_keys(&mut e, ")");
6858 assert_eq!(e.cursor(), (0, 13));
6859 }
6860
6861 #[test]
6862 fn sentence_motion_open_paren_jumps_backward() {
6863 let mut e = editor_with("Alpha. Beta. Gamma.");
6864 e.jump_cursor(0, 13);
6865 run_keys(&mut e, "(");
6866 assert_eq!(e.cursor(), (0, 7));
6869 run_keys(&mut e, "(");
6870 assert_eq!(e.cursor(), (0, 0));
6871 }
6872
6873 #[test]
6874 fn sentence_motion_count() {
6875 let mut e = editor_with("A. B. C. D.");
6876 e.jump_cursor(0, 0);
6877 run_keys(&mut e, "3)");
6878 assert_eq!(e.cursor(), (0, 9));
6880 }
6881
6882 #[test]
6883 fn dis_deletes_inner_sentence() {
6884 let mut e = editor_with("First one. Second one. Third one.");
6885 e.jump_cursor(0, 13);
6886 run_keys(&mut e, "dis");
6887 assert_eq!(e.buffer().lines()[0], "First one. Third one.");
6889 }
6890
6891 #[test]
6892 fn das_deletes_around_sentence_with_trailing_space() {
6893 let mut e = editor_with("Alpha. Beta. Gamma.");
6894 e.jump_cursor(0, 8);
6895 run_keys(&mut e, "das");
6896 assert_eq!(e.buffer().lines()[0], "Alpha. Gamma.");
6899 }
6900
6901 #[test]
6902 fn dis_handles_double_terminator() {
6903 let mut e = editor_with("Wow!? Next.");
6904 e.jump_cursor(0, 1);
6905 run_keys(&mut e, "dis");
6906 assert_eq!(e.buffer().lines()[0], " Next.");
6909 }
6910
6911 #[test]
6912 fn dis_first_sentence_from_cursor_at_zero() {
6913 let mut e = editor_with("Alpha. Beta.");
6914 e.jump_cursor(0, 0);
6915 run_keys(&mut e, "dis");
6916 assert_eq!(e.buffer().lines()[0], " Beta.");
6917 }
6918
6919 #[test]
6920 fn yis_yanks_inner_sentence() {
6921 let mut e = editor_with("Hello world. Bye.");
6922 e.jump_cursor(0, 5);
6923 run_keys(&mut e, "yis");
6924 assert_eq!(e.registers().read('"').unwrap().text, "Hello world.");
6925 }
6926
6927 #[test]
6928 fn vis_visually_selects_inner_sentence() {
6929 let mut e = editor_with("First. Second.");
6930 e.jump_cursor(0, 1);
6931 run_keys(&mut e, "vis");
6932 assert_eq!(e.vim_mode(), VimMode::Visual);
6933 run_keys(&mut e, "y");
6934 assert_eq!(e.registers().read('"').unwrap().text, "First.");
6935 }
6936
6937 #[test]
6938 fn ciw_changes_inner_word() {
6939 let mut e = editor_with("hello world");
6940 e.jump_cursor(0, 1);
6941 run_keys(&mut e, "ciwHEY<Esc>");
6942 assert_eq!(e.buffer().lines()[0], "HEY world");
6943 }
6944
6945 #[test]
6946 fn yiw_yanks_inner_word() {
6947 let mut e = editor_with("hello world");
6948 e.jump_cursor(0, 1);
6949 run_keys(&mut e, "yiw");
6950 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6951 }
6952
6953 #[test]
6954 fn viw_selects_inner_word() {
6955 let mut e = editor_with("hello world");
6956 e.jump_cursor(0, 2);
6957 run_keys(&mut e, "viw");
6958 assert_eq!(e.vim_mode(), VimMode::Visual);
6959 run_keys(&mut e, "y");
6960 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6961 }
6962
6963 #[test]
6964 fn ci_paren_changes_inside() {
6965 let mut e = editor_with("f(old)");
6966 e.jump_cursor(0, 3);
6967 run_keys(&mut e, "ci(NEW<Esc>");
6968 assert_eq!(e.buffer().lines()[0], "f(NEW)");
6969 }
6970
6971 #[test]
6972 fn yi_double_quote_yanks_inside() {
6973 let mut e = editor_with("say \"hi there\" then");
6974 e.jump_cursor(0, 6);
6975 run_keys(&mut e, "yi\"");
6976 assert_eq!(e.registers().read('"').unwrap().text, "hi there");
6977 }
6978
6979 #[test]
6980 fn vap_visual_selects_around_paragraph() {
6981 let mut e = editor_with("a\nb\n\nc");
6982 e.jump_cursor(0, 0);
6983 run_keys(&mut e, "vap");
6984 assert_eq!(e.vim_mode(), VimMode::VisualLine);
6985 run_keys(&mut e, "y");
6986 let text = e.registers().read('"').unwrap().text.clone();
6988 assert!(text.starts_with("a\nb"));
6989 }
6990
6991 #[test]
6992 fn star_finds_next_occurrence() {
6993 let mut e = editor_with("foo bar foo baz");
6994 run_keys(&mut e, "*");
6995 assert_eq!(e.cursor().1, 8);
6996 }
6997
6998 #[test]
6999 fn star_skips_substring_match() {
7000 let mut e = editor_with("foo foobar baz");
7003 run_keys(&mut e, "*");
7004 assert_eq!(e.cursor().1, 0);
7005 }
7006
7007 #[test]
7008 fn g_star_matches_substring() {
7009 let mut e = editor_with("foo foobar baz");
7012 run_keys(&mut e, "g*");
7013 assert_eq!(e.cursor().1, 4);
7014 }
7015
7016 #[test]
7017 fn g_pound_matches_substring_backward() {
7018 let mut e = editor_with("foo foobar baz foo");
7021 run_keys(&mut e, "$b");
7022 assert_eq!(e.cursor().1, 15);
7023 run_keys(&mut e, "g#");
7024 assert_eq!(e.cursor().1, 4);
7025 }
7026
7027 #[test]
7028 fn n_repeats_last_search_forward() {
7029 let mut e = editor_with("foo bar foo baz foo");
7030 run_keys(&mut e, "/foo<CR>");
7033 assert_eq!(e.cursor().1, 8);
7034 run_keys(&mut e, "n");
7035 assert_eq!(e.cursor().1, 16);
7036 }
7037
7038 #[test]
7039 fn shift_n_reverses_search() {
7040 let mut e = editor_with("foo bar foo baz foo");
7041 run_keys(&mut e, "/foo<CR>");
7042 run_keys(&mut e, "n");
7043 assert_eq!(e.cursor().1, 16);
7044 run_keys(&mut e, "N");
7045 assert_eq!(e.cursor().1, 8);
7046 }
7047
7048 #[test]
7049 fn n_noop_without_pattern() {
7050 let mut e = editor_with("foo bar");
7051 run_keys(&mut e, "n");
7052 assert_eq!(e.cursor(), (0, 0));
7053 }
7054
7055 #[test]
7056 fn visual_line_preserves_cursor_column() {
7057 let mut e = editor_with("hello world\nanother one\nbye");
7060 run_keys(&mut e, "lllll"); run_keys(&mut e, "V");
7062 assert_eq!(e.vim_mode(), VimMode::VisualLine);
7063 assert_eq!(e.cursor(), (0, 5));
7064 run_keys(&mut e, "j");
7065 assert_eq!(e.cursor(), (1, 5));
7066 }
7067
7068 #[test]
7069 fn visual_line_yank_includes_trailing_newline() {
7070 let mut e = editor_with("aaa\nbbb\nccc");
7071 run_keys(&mut e, "Vjy");
7072 assert_eq!(e.last_yank.as_deref(), Some("aaa\nbbb\n"));
7074 }
7075
7076 #[test]
7077 fn visual_line_yank_last_line_trailing_newline() {
7078 let mut e = editor_with("aaa\nbbb\nccc");
7079 run_keys(&mut e, "jj");
7081 run_keys(&mut e, "Vy");
7082 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
7083 }
7084
7085 #[test]
7086 fn yy_on_last_line_has_trailing_newline() {
7087 let mut e = editor_with("aaa\nbbb\nccc");
7088 run_keys(&mut e, "jj");
7089 run_keys(&mut e, "yy");
7090 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
7091 }
7092
7093 #[test]
7094 fn yy_in_middle_has_trailing_newline() {
7095 let mut e = editor_with("aaa\nbbb\nccc");
7096 run_keys(&mut e, "j");
7097 run_keys(&mut e, "yy");
7098 assert_eq!(e.last_yank.as_deref(), Some("bbb\n"));
7099 }
7100
7101 #[test]
7102 fn di_single_quote() {
7103 let mut e = editor_with("say 'hello world' now");
7104 e.jump_cursor(0, 7);
7105 run_keys(&mut e, "di'");
7106 assert_eq!(e.buffer().lines()[0], "say '' now");
7107 }
7108
7109 #[test]
7110 fn da_single_quote() {
7111 let mut e = editor_with("say 'hello' now");
7113 e.jump_cursor(0, 7);
7114 run_keys(&mut e, "da'");
7115 assert_eq!(e.buffer().lines()[0], "say now");
7116 }
7117
7118 #[test]
7119 fn di_backtick() {
7120 let mut e = editor_with("say `hi` now");
7121 e.jump_cursor(0, 5);
7122 run_keys(&mut e, "di`");
7123 assert_eq!(e.buffer().lines()[0], "say `` now");
7124 }
7125
7126 #[test]
7127 fn di_brace() {
7128 let mut e = editor_with("fn { a; b; c }");
7129 e.jump_cursor(0, 7);
7130 run_keys(&mut e, "di{");
7131 assert_eq!(e.buffer().lines()[0], "fn {}");
7132 }
7133
7134 #[test]
7135 fn di_bracket() {
7136 let mut e = editor_with("arr[1, 2, 3]");
7137 e.jump_cursor(0, 5);
7138 run_keys(&mut e, "di[");
7139 assert_eq!(e.buffer().lines()[0], "arr[]");
7140 }
7141
7142 #[test]
7143 fn dab_deletes_around_paren() {
7144 let mut e = editor_with("fn(a, b) + 1");
7145 e.jump_cursor(0, 4);
7146 run_keys(&mut e, "dab");
7147 assert_eq!(e.buffer().lines()[0], "fn + 1");
7148 }
7149
7150 #[test]
7151 fn da_big_b_deletes_around_brace() {
7152 let mut e = editor_with("x = {a: 1}");
7153 e.jump_cursor(0, 6);
7154 run_keys(&mut e, "daB");
7155 assert_eq!(e.buffer().lines()[0], "x = ");
7156 }
7157
7158 #[test]
7159 fn di_big_w_deletes_bigword() {
7160 let mut e = editor_with("foo-bar baz");
7161 e.jump_cursor(0, 2);
7162 run_keys(&mut e, "diW");
7163 assert_eq!(e.buffer().lines()[0], " baz");
7164 }
7165
7166 #[test]
7167 fn visual_select_inner_word() {
7168 let mut e = editor_with("hello world");
7169 e.jump_cursor(0, 2);
7170 run_keys(&mut e, "viw");
7171 assert_eq!(e.vim_mode(), VimMode::Visual);
7172 run_keys(&mut e, "y");
7173 assert_eq!(e.last_yank.as_deref(), Some("hello"));
7174 }
7175
7176 #[test]
7177 fn visual_select_inner_quote() {
7178 let mut e = editor_with("foo \"bar\" baz");
7179 e.jump_cursor(0, 6);
7180 run_keys(&mut e, "vi\"");
7181 run_keys(&mut e, "y");
7182 assert_eq!(e.last_yank.as_deref(), Some("bar"));
7183 }
7184
7185 #[test]
7186 fn visual_select_inner_paren() {
7187 let mut e = editor_with("fn(a, b)");
7188 e.jump_cursor(0, 4);
7189 run_keys(&mut e, "vi(");
7190 run_keys(&mut e, "y");
7191 assert_eq!(e.last_yank.as_deref(), Some("a, b"));
7192 }
7193
7194 #[test]
7195 fn visual_select_outer_brace() {
7196 let mut e = editor_with("{x}");
7197 e.jump_cursor(0, 1);
7198 run_keys(&mut e, "va{");
7199 run_keys(&mut e, "y");
7200 assert_eq!(e.last_yank.as_deref(), Some("{x}"));
7201 }
7202
7203 #[test]
7204 fn ci_paren_forward_scans_when_cursor_before_pair() {
7205 let mut e = editor_with("foo(bar)");
7208 e.jump_cursor(0, 0);
7209 run_keys(&mut e, "ci(NEW<Esc>");
7210 assert_eq!(e.buffer().lines()[0], "foo(NEW)");
7211 }
7212
7213 #[test]
7214 fn ci_paren_forward_scans_across_lines() {
7215 let mut e = editor_with("first\nfoo(bar)\nlast");
7216 e.jump_cursor(0, 0);
7217 run_keys(&mut e, "ci(NEW<Esc>");
7218 assert_eq!(e.buffer().lines()[1], "foo(NEW)");
7219 }
7220
7221 #[test]
7222 fn ci_brace_forward_scans_when_cursor_before_pair() {
7223 let mut e = editor_with("let x = {y};");
7224 e.jump_cursor(0, 0);
7225 run_keys(&mut e, "ci{NEW<Esc>");
7226 assert_eq!(e.buffer().lines()[0], "let x = {NEW};");
7227 }
7228
7229 #[test]
7230 fn cit_forward_scans_when_cursor_before_tag() {
7231 let mut e = editor_with("text <b>hello</b> rest");
7234 e.jump_cursor(0, 0);
7235 run_keys(&mut e, "citNEW<Esc>");
7236 assert_eq!(e.buffer().lines()[0], "text <b>NEW</b> rest");
7237 }
7238
7239 #[test]
7240 fn dat_forward_scans_when_cursor_before_tag() {
7241 let mut e = editor_with("text <b>hello</b> rest");
7243 e.jump_cursor(0, 0);
7244 run_keys(&mut e, "dat");
7245 assert_eq!(e.buffer().lines()[0], "text rest");
7246 }
7247
7248 #[test]
7249 fn ci_paren_still_works_when_cursor_inside() {
7250 let mut e = editor_with("fn(a, b)");
7253 e.jump_cursor(0, 4);
7254 run_keys(&mut e, "ci(NEW<Esc>");
7255 assert_eq!(e.buffer().lines()[0], "fn(NEW)");
7256 }
7257
7258 #[test]
7259 fn caw_changes_word_with_trailing_space() {
7260 let mut e = editor_with("hello world");
7261 run_keys(&mut e, "cawfoo<Esc>");
7262 assert_eq!(e.buffer().lines()[0], "fooworld");
7263 }
7264
7265 #[test]
7266 fn visual_char_yank_preserves_raw_text() {
7267 let mut e = editor_with("hello world");
7268 run_keys(&mut e, "vllly");
7269 assert_eq!(e.last_yank.as_deref(), Some("hell"));
7270 }
7271
7272 #[test]
7273 fn single_line_visual_line_selects_full_line_on_yank() {
7274 let mut e = editor_with("hello world\nbye");
7275 run_keys(&mut e, "V");
7276 run_keys(&mut e, "y");
7279 assert_eq!(e.last_yank.as_deref(), Some("hello world\n"));
7280 }
7281
7282 #[test]
7283 fn visual_line_extends_both_directions() {
7284 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7285 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
7287 assert_eq!(e.cursor(), (3, 0));
7288 run_keys(&mut e, "k");
7289 assert_eq!(e.cursor(), (2, 0));
7291 run_keys(&mut e, "k");
7292 assert_eq!(e.cursor(), (1, 0));
7293 }
7294
7295 #[test]
7296 fn visual_char_preserves_cursor_column() {
7297 let mut e = editor_with("hello world");
7298 run_keys(&mut e, "lllll"); run_keys(&mut e, "v");
7300 assert_eq!(e.cursor(), (0, 5));
7301 run_keys(&mut e, "ll");
7302 assert_eq!(e.cursor(), (0, 7));
7303 }
7304
7305 #[test]
7306 fn visual_char_highlight_bounds_order() {
7307 let mut e = editor_with("abcdef");
7308 run_keys(&mut e, "lll"); run_keys(&mut e, "v");
7310 run_keys(&mut e, "hh"); assert_eq!(e.char_highlight(), Some(((0, 1), (0, 3))));
7313 }
7314
7315 #[test]
7316 fn visual_line_highlight_bounds() {
7317 let mut e = editor_with("a\nb\nc");
7318 run_keys(&mut e, "V");
7319 assert_eq!(e.line_highlight(), Some((0, 0)));
7320 run_keys(&mut e, "j");
7321 assert_eq!(e.line_highlight(), Some((0, 1)));
7322 run_keys(&mut e, "j");
7323 assert_eq!(e.line_highlight(), Some((0, 2)));
7324 }
7325
7326 #[test]
7329 fn h_moves_left() {
7330 let mut e = editor_with("hello");
7331 e.jump_cursor(0, 3);
7332 run_keys(&mut e, "h");
7333 assert_eq!(e.cursor(), (0, 2));
7334 }
7335
7336 #[test]
7337 fn l_moves_right() {
7338 let mut e = editor_with("hello");
7339 run_keys(&mut e, "l");
7340 assert_eq!(e.cursor(), (0, 1));
7341 }
7342
7343 #[test]
7344 fn k_moves_up() {
7345 let mut e = editor_with("a\nb\nc");
7346 e.jump_cursor(2, 0);
7347 run_keys(&mut e, "k");
7348 assert_eq!(e.cursor(), (1, 0));
7349 }
7350
7351 #[test]
7352 fn zero_moves_to_line_start() {
7353 let mut e = editor_with(" hello");
7354 run_keys(&mut e, "$");
7355 run_keys(&mut e, "0");
7356 assert_eq!(e.cursor().1, 0);
7357 }
7358
7359 #[test]
7360 fn caret_moves_to_first_non_blank() {
7361 let mut e = editor_with(" hello");
7362 run_keys(&mut e, "0");
7363 run_keys(&mut e, "^");
7364 assert_eq!(e.cursor().1, 4);
7365 }
7366
7367 #[test]
7368 fn dollar_moves_to_last_char() {
7369 let mut e = editor_with("hello");
7370 run_keys(&mut e, "$");
7371 assert_eq!(e.cursor().1, 4);
7372 }
7373
7374 #[test]
7375 fn dollar_on_empty_line_stays_at_col_zero() {
7376 let mut e = editor_with("");
7377 run_keys(&mut e, "$");
7378 assert_eq!(e.cursor().1, 0);
7379 }
7380
7381 #[test]
7382 fn w_jumps_to_next_word() {
7383 let mut e = editor_with("foo bar baz");
7384 run_keys(&mut e, "w");
7385 assert_eq!(e.cursor().1, 4);
7386 }
7387
7388 #[test]
7389 fn b_jumps_back_a_word() {
7390 let mut e = editor_with("foo bar");
7391 e.jump_cursor(0, 6);
7392 run_keys(&mut e, "b");
7393 assert_eq!(e.cursor().1, 4);
7394 }
7395
7396 #[test]
7397 fn e_jumps_to_word_end() {
7398 let mut e = editor_with("foo bar");
7399 run_keys(&mut e, "e");
7400 assert_eq!(e.cursor().1, 2);
7401 }
7402
7403 #[test]
7406 fn d_dollar_deletes_to_eol() {
7407 let mut e = editor_with("hello world");
7408 e.jump_cursor(0, 5);
7409 run_keys(&mut e, "d$");
7410 assert_eq!(e.buffer().lines()[0], "hello");
7411 }
7412
7413 #[test]
7414 fn d_zero_deletes_to_line_start() {
7415 let mut e = editor_with("hello world");
7416 e.jump_cursor(0, 6);
7417 run_keys(&mut e, "d0");
7418 assert_eq!(e.buffer().lines()[0], "world");
7419 }
7420
7421 #[test]
7422 fn d_caret_deletes_to_first_non_blank() {
7423 let mut e = editor_with(" hello");
7424 e.jump_cursor(0, 6);
7425 run_keys(&mut e, "d^");
7426 assert_eq!(e.buffer().lines()[0], " llo");
7427 }
7428
7429 #[test]
7430 fn d_capital_g_deletes_to_end_of_file() {
7431 let mut e = editor_with("a\nb\nc\nd");
7432 e.jump_cursor(1, 0);
7433 run_keys(&mut e, "dG");
7434 assert_eq!(e.buffer().lines(), &["a".to_string()]);
7435 }
7436
7437 #[test]
7438 fn d_gg_deletes_to_start_of_file() {
7439 let mut e = editor_with("a\nb\nc\nd");
7440 e.jump_cursor(2, 0);
7441 run_keys(&mut e, "dgg");
7442 assert_eq!(e.buffer().lines(), &["d".to_string()]);
7443 }
7444
7445 #[test]
7446 fn cw_is_ce_quirk() {
7447 let mut e = editor_with("foo bar");
7450 run_keys(&mut e, "cwxyz<Esc>");
7451 assert_eq!(e.buffer().lines()[0], "xyz bar");
7452 }
7453
7454 #[test]
7457 fn big_d_deletes_to_eol() {
7458 let mut e = editor_with("hello world");
7459 e.jump_cursor(0, 5);
7460 run_keys(&mut e, "D");
7461 assert_eq!(e.buffer().lines()[0], "hello");
7462 }
7463
7464 #[test]
7465 fn big_c_deletes_to_eol_and_inserts() {
7466 let mut e = editor_with("hello world");
7467 e.jump_cursor(0, 5);
7468 run_keys(&mut e, "C!<Esc>");
7469 assert_eq!(e.buffer().lines()[0], "hello!");
7470 }
7471
7472 #[test]
7473 fn j_joins_next_line_with_space() {
7474 let mut e = editor_with("hello\nworld");
7475 run_keys(&mut e, "J");
7476 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7477 }
7478
7479 #[test]
7480 fn j_strips_leading_whitespace_on_join() {
7481 let mut e = editor_with("hello\n world");
7482 run_keys(&mut e, "J");
7483 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7484 }
7485
7486 #[test]
7487 fn big_x_deletes_char_before_cursor() {
7488 let mut e = editor_with("hello");
7489 e.jump_cursor(0, 3);
7490 run_keys(&mut e, "X");
7491 assert_eq!(e.buffer().lines()[0], "helo");
7492 }
7493
7494 #[test]
7495 fn s_substitutes_char_and_enters_insert() {
7496 let mut e = editor_with("hello");
7497 run_keys(&mut e, "sX<Esc>");
7498 assert_eq!(e.buffer().lines()[0], "Xello");
7499 }
7500
7501 #[test]
7502 fn count_x_deletes_many() {
7503 let mut e = editor_with("abcdef");
7504 run_keys(&mut e, "3x");
7505 assert_eq!(e.buffer().lines()[0], "def");
7506 }
7507
7508 #[test]
7511 fn p_pastes_charwise_after_cursor() {
7512 let mut e = editor_with("hello");
7513 run_keys(&mut e, "yw");
7514 run_keys(&mut e, "$p");
7515 assert_eq!(e.buffer().lines()[0], "hellohello");
7516 }
7517
7518 #[test]
7519 fn capital_p_pastes_charwise_before_cursor() {
7520 let mut e = editor_with("hello");
7521 run_keys(&mut e, "v");
7523 run_keys(&mut e, "l");
7524 run_keys(&mut e, "y");
7525 run_keys(&mut e, "$P");
7526 assert_eq!(e.buffer().lines()[0], "hellheo");
7529 }
7530
7531 #[test]
7532 fn p_pastes_linewise_below() {
7533 let mut e = editor_with("one\ntwo\nthree");
7534 run_keys(&mut e, "yy");
7535 run_keys(&mut e, "p");
7536 assert_eq!(
7537 e.buffer().lines(),
7538 &[
7539 "one".to_string(),
7540 "one".to_string(),
7541 "two".to_string(),
7542 "three".to_string()
7543 ]
7544 );
7545 }
7546
7547 #[test]
7548 fn capital_p_pastes_linewise_above() {
7549 let mut e = editor_with("one\ntwo");
7550 e.jump_cursor(1, 0);
7551 run_keys(&mut e, "yy");
7552 run_keys(&mut e, "P");
7553 assert_eq!(
7554 e.buffer().lines(),
7555 &["one".to_string(), "two".to_string(), "two".to_string()]
7556 );
7557 }
7558
7559 #[test]
7562 fn hash_finds_previous_occurrence() {
7563 let mut e = editor_with("foo bar foo baz foo");
7564 e.jump_cursor(0, 16);
7566 run_keys(&mut e, "#");
7567 assert_eq!(e.cursor().1, 8);
7568 }
7569
7570 #[test]
7573 fn visual_line_delete_removes_full_lines() {
7574 let mut e = editor_with("a\nb\nc\nd");
7575 run_keys(&mut e, "Vjd");
7576 assert_eq!(e.buffer().lines(), &["c".to_string(), "d".to_string()]);
7577 }
7578
7579 #[test]
7580 fn visual_line_change_leaves_blank_line() {
7581 let mut e = editor_with("a\nb\nc");
7582 run_keys(&mut e, "Vjc");
7583 assert_eq!(e.vim_mode(), VimMode::Insert);
7584 run_keys(&mut e, "X<Esc>");
7585 assert_eq!(e.buffer().lines(), &["X".to_string(), "c".to_string()]);
7589 }
7590
7591 #[test]
7592 fn cc_leaves_blank_line() {
7593 let mut e = editor_with("a\nb\nc");
7594 e.jump_cursor(1, 0);
7595 run_keys(&mut e, "ccX<Esc>");
7596 assert_eq!(
7597 e.buffer().lines(),
7598 &["a".to_string(), "X".to_string(), "c".to_string()]
7599 );
7600 }
7601
7602 #[test]
7607 fn big_w_skips_hyphens() {
7608 let mut e = editor_with("foo-bar baz");
7610 run_keys(&mut e, "W");
7611 assert_eq!(e.cursor().1, 8);
7612 }
7613
7614 #[test]
7615 fn big_w_crosses_lines() {
7616 let mut e = editor_with("foo-bar\nbaz-qux");
7617 run_keys(&mut e, "W");
7618 assert_eq!(e.cursor(), (1, 0));
7619 }
7620
7621 #[test]
7622 fn big_b_skips_hyphens() {
7623 let mut e = editor_with("foo-bar baz");
7624 e.jump_cursor(0, 9);
7625 run_keys(&mut e, "B");
7626 assert_eq!(e.cursor().1, 8);
7627 run_keys(&mut e, "B");
7628 assert_eq!(e.cursor().1, 0);
7629 }
7630
7631 #[test]
7632 fn big_e_jumps_to_big_word_end() {
7633 let mut e = editor_with("foo-bar baz");
7634 run_keys(&mut e, "E");
7635 assert_eq!(e.cursor().1, 6);
7636 run_keys(&mut e, "E");
7637 assert_eq!(e.cursor().1, 10);
7638 }
7639
7640 #[test]
7641 fn dw_with_big_word_variant() {
7642 let mut e = editor_with("foo-bar baz");
7644 run_keys(&mut e, "dW");
7645 assert_eq!(e.buffer().lines()[0], "baz");
7646 }
7647
7648 #[test]
7651 fn insert_ctrl_w_deletes_word_back() {
7652 let mut e = editor_with("");
7653 run_keys(&mut e, "i");
7654 for c in "hello world".chars() {
7655 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7656 }
7657 run_keys(&mut e, "<C-w>");
7658 assert_eq!(e.buffer().lines()[0], "hello ");
7659 }
7660
7661 #[test]
7662 fn insert_ctrl_w_at_col0_joins_with_prev_word() {
7663 let mut e = editor_with("hello\nworld");
7667 e.jump_cursor(1, 0);
7668 run_keys(&mut e, "i");
7669 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7670 assert_eq!(e.buffer().lines(), vec!["world".to_string()]);
7673 assert_eq!(e.cursor(), (0, 0));
7674 }
7675
7676 #[test]
7677 fn insert_ctrl_w_at_col0_keeps_prefix_words() {
7678 let mut e = editor_with("foo bar\nbaz");
7679 e.jump_cursor(1, 0);
7680 run_keys(&mut e, "i");
7681 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7682 assert_eq!(e.buffer().lines(), vec!["foo baz".to_string()]);
7684 assert_eq!(e.cursor(), (0, 4));
7685 }
7686
7687 #[test]
7688 fn insert_ctrl_u_deletes_to_line_start() {
7689 let mut e = editor_with("");
7690 run_keys(&mut e, "i");
7691 for c in "hello world".chars() {
7692 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7693 }
7694 run_keys(&mut e, "<C-u>");
7695 assert_eq!(e.buffer().lines()[0], "");
7696 }
7697
7698 #[test]
7699 fn insert_ctrl_o_runs_one_normal_command() {
7700 let mut e = editor_with("hello world");
7701 run_keys(&mut e, "A");
7703 assert_eq!(e.vim_mode(), VimMode::Insert);
7704 e.jump_cursor(0, 0);
7706 run_keys(&mut e, "<C-o>");
7707 assert_eq!(e.vim_mode(), VimMode::Normal);
7708 run_keys(&mut e, "dw");
7709 assert_eq!(e.vim_mode(), VimMode::Insert);
7711 assert_eq!(e.buffer().lines()[0], "world");
7712 }
7713
7714 #[test]
7717 fn j_through_empty_line_preserves_column() {
7718 let mut e = editor_with("hello world\n\nanother line");
7719 run_keys(&mut e, "llllll");
7721 assert_eq!(e.cursor(), (0, 6));
7722 run_keys(&mut e, "j");
7725 assert_eq!(e.cursor(), (1, 0));
7726 run_keys(&mut e, "j");
7728 assert_eq!(e.cursor(), (2, 6));
7729 }
7730
7731 #[test]
7732 fn j_through_shorter_line_preserves_column() {
7733 let mut e = editor_with("hello world\nhi\nanother line");
7734 run_keys(&mut e, "lllllll"); run_keys(&mut e, "j"); assert_eq!(e.cursor(), (1, 1));
7737 run_keys(&mut e, "j");
7738 assert_eq!(e.cursor(), (2, 7));
7739 }
7740
7741 #[test]
7742 fn esc_from_insert_sticky_matches_visible_cursor() {
7743 let mut e = editor_with(" this is a line\n another one of a similar size");
7747 e.jump_cursor(0, 12);
7748 run_keys(&mut e, "I");
7749 assert_eq!(e.cursor(), (0, 4));
7750 run_keys(&mut e, "X<Esc>");
7751 assert_eq!(e.cursor(), (0, 4));
7752 run_keys(&mut e, "j");
7753 assert_eq!(e.cursor(), (1, 4));
7754 }
7755
7756 #[test]
7757 fn esc_from_insert_sticky_tracks_inserted_chars() {
7758 let mut e = editor_with("xxxxxxx\nyyyyyyy");
7759 run_keys(&mut e, "i");
7760 run_keys(&mut e, "abc<Esc>");
7761 assert_eq!(e.cursor(), (0, 2));
7762 run_keys(&mut e, "j");
7763 assert_eq!(e.cursor(), (1, 2));
7764 }
7765
7766 #[test]
7767 fn esc_from_insert_sticky_tracks_arrow_nav() {
7768 let mut e = editor_with("xxxxxx\nyyyyyy");
7769 run_keys(&mut e, "i");
7770 run_keys(&mut e, "abc");
7771 for _ in 0..2 {
7772 e.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
7773 }
7774 run_keys(&mut e, "<Esc>");
7775 assert_eq!(e.cursor(), (0, 0));
7776 run_keys(&mut e, "j");
7777 assert_eq!(e.cursor(), (1, 0));
7778 }
7779
7780 #[test]
7781 fn esc_from_insert_at_col_14_followed_by_j() {
7782 let line = "x".repeat(30);
7785 let buf = format!("{line}\n{line}");
7786 let mut e = editor_with(&buf);
7787 e.jump_cursor(0, 14);
7788 run_keys(&mut e, "i");
7789 for c in "test ".chars() {
7790 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7791 }
7792 run_keys(&mut e, "<Esc>");
7793 assert_eq!(e.cursor(), (0, 18));
7794 run_keys(&mut e, "j");
7795 assert_eq!(e.cursor(), (1, 18));
7796 }
7797
7798 #[test]
7799 fn linewise_paste_resets_sticky_column() {
7800 let mut e = editor_with(" hello\naaaaaaaa\nbye");
7804 run_keys(&mut e, "llllll"); run_keys(&mut e, "yy");
7806 run_keys(&mut e, "j"); run_keys(&mut e, "p"); assert_eq!(e.cursor(), (2, 4));
7810 run_keys(&mut e, "j");
7812 assert_eq!(e.cursor(), (3, 2));
7813 }
7814
7815 #[test]
7816 fn horizontal_motion_resyncs_sticky_column() {
7817 let mut e = editor_with("hello world\n\nanother line");
7821 run_keys(&mut e, "llllll"); run_keys(&mut e, "hhh"); run_keys(&mut e, "jj");
7824 assert_eq!(e.cursor(), (2, 3));
7825 }
7826
7827 #[test]
7830 fn ctrl_v_enters_visual_block() {
7831 let mut e = editor_with("aaa\nbbb\nccc");
7832 run_keys(&mut e, "<C-v>");
7833 assert_eq!(e.vim_mode(), VimMode::VisualBlock);
7834 }
7835
7836 #[test]
7837 fn visual_block_esc_returns_to_normal() {
7838 let mut e = editor_with("aaa\nbbb\nccc");
7839 run_keys(&mut e, "<C-v>");
7840 run_keys(&mut e, "<Esc>");
7841 assert_eq!(e.vim_mode(), VimMode::Normal);
7842 }
7843
7844 #[test]
7845 fn backtick_lt_jumps_to_visual_start_mark() {
7846 let mut e = editor_with("foo bar baz\n");
7850 run_keys(&mut e, "v");
7851 run_keys(&mut e, "w"); run_keys(&mut e, "<Esc>"); assert_eq!(e.cursor(), (0, 4));
7854 run_keys(&mut e, "`<lt>");
7856 assert_eq!(e.cursor(), (0, 0));
7857 }
7858
7859 #[test]
7860 fn backtick_gt_jumps_to_visual_end_mark() {
7861 let mut e = editor_with("foo bar baz\n");
7862 run_keys(&mut e, "v");
7863 run_keys(&mut e, "w"); run_keys(&mut e, "<Esc>");
7865 run_keys(&mut e, "0"); run_keys(&mut e, "`>");
7867 assert_eq!(e.cursor(), (0, 4));
7868 }
7869
7870 #[test]
7871 fn visual_exit_sets_lt_gt_marks() {
7872 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7875 run_keys(&mut e, "V");
7877 run_keys(&mut e, "j");
7878 run_keys(&mut e, "<Esc>");
7879 let lt = e.mark('<').expect("'<' mark must be set on visual exit");
7880 let gt = e.mark('>').expect("'>' mark must be set on visual exit");
7881 assert_eq!(lt.0, 0, "'< row should be the lower bound");
7882 assert_eq!(gt.0, 1, "'> row should be the upper bound");
7883 }
7884
7885 #[test]
7886 fn visual_exit_marks_use_lower_higher_order() {
7887 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7891 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
7893 run_keys(&mut e, "k"); run_keys(&mut e, "<Esc>");
7895 let lt = e.mark('<').unwrap();
7896 let gt = e.mark('>').unwrap();
7897 assert_eq!(lt.0, 2);
7898 assert_eq!(gt.0, 3);
7899 }
7900
7901 #[test]
7902 fn visualline_exit_marks_snap_to_line_edges() {
7903 let mut e = editor_with("aaaaa\nbbbbb\ncc");
7905 run_keys(&mut e, "lll"); run_keys(&mut e, "V");
7907 run_keys(&mut e, "j"); run_keys(&mut e, "<Esc>");
7909 let lt = e.mark('<').unwrap();
7910 let gt = e.mark('>').unwrap();
7911 assert_eq!(lt, (0, 0), "'< should snap to (top_row, 0)");
7912 assert_eq!(gt, (1, 4), "'> should snap to (bot_row, last_col)");
7914 }
7915
7916 #[test]
7917 fn visualblock_exit_marks_use_block_corners() {
7918 let mut e = editor_with("aaaaa\nbbbbb\nccccc");
7922 run_keys(&mut e, "llll"); run_keys(&mut e, "<C-v>");
7924 run_keys(&mut e, "j"); run_keys(&mut e, "hh"); run_keys(&mut e, "<Esc>");
7927 let lt = e.mark('<').unwrap();
7928 let gt = e.mark('>').unwrap();
7929 assert_eq!(lt, (0, 2), "'< should be top-left corner");
7931 assert_eq!(gt, (1, 4), "'> should be bottom-right corner");
7932 }
7933
7934 #[test]
7935 fn visual_block_delete_removes_column_range() {
7936 let mut e = editor_with("hello\nworld\nhappy");
7937 run_keys(&mut e, "l");
7939 run_keys(&mut e, "<C-v>");
7940 run_keys(&mut e, "jj");
7941 run_keys(&mut e, "ll");
7942 run_keys(&mut e, "d");
7943 assert_eq!(
7945 e.buffer().lines(),
7946 &["ho".to_string(), "wd".to_string(), "hy".to_string()]
7947 );
7948 }
7949
7950 #[test]
7951 fn visual_block_yank_joins_with_newlines() {
7952 let mut e = editor_with("hello\nworld\nhappy");
7953 run_keys(&mut e, "<C-v>");
7954 run_keys(&mut e, "jj");
7955 run_keys(&mut e, "ll");
7956 run_keys(&mut e, "y");
7957 assert_eq!(e.last_yank.as_deref(), Some("hel\nwor\nhap"));
7958 }
7959
7960 #[test]
7961 fn visual_block_replace_fills_block() {
7962 let mut e = editor_with("hello\nworld\nhappy");
7963 run_keys(&mut e, "<C-v>");
7964 run_keys(&mut e, "jj");
7965 run_keys(&mut e, "ll");
7966 run_keys(&mut e, "rx");
7967 assert_eq!(
7968 e.buffer().lines(),
7969 &[
7970 "xxxlo".to_string(),
7971 "xxxld".to_string(),
7972 "xxxpy".to_string()
7973 ]
7974 );
7975 }
7976
7977 #[test]
7978 fn visual_block_insert_repeats_across_rows() {
7979 let mut e = editor_with("hello\nworld\nhappy");
7980 run_keys(&mut e, "<C-v>");
7981 run_keys(&mut e, "jj");
7982 run_keys(&mut e, "I");
7983 run_keys(&mut e, "# <Esc>");
7984 assert_eq!(
7985 e.buffer().lines(),
7986 &[
7987 "# hello".to_string(),
7988 "# world".to_string(),
7989 "# happy".to_string()
7990 ]
7991 );
7992 }
7993
7994 #[test]
7995 fn block_highlight_returns_none_outside_block_mode() {
7996 let mut e = editor_with("abc");
7997 assert!(e.block_highlight().is_none());
7998 run_keys(&mut e, "v");
7999 assert!(e.block_highlight().is_none());
8000 run_keys(&mut e, "<Esc>V");
8001 assert!(e.block_highlight().is_none());
8002 }
8003
8004 #[test]
8005 fn block_highlight_bounds_track_anchor_and_cursor() {
8006 let mut e = editor_with("aaaa\nbbbb\ncccc");
8007 run_keys(&mut e, "ll"); run_keys(&mut e, "<C-v>");
8009 run_keys(&mut e, "jh"); assert_eq!(e.block_highlight(), Some((0, 1, 1, 2)));
8012 }
8013
8014 #[test]
8015 fn visual_block_delete_handles_short_lines() {
8016 let mut e = editor_with("hello\nhi\nworld");
8018 run_keys(&mut e, "l"); run_keys(&mut e, "<C-v>");
8020 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
8022 assert_eq!(
8027 e.buffer().lines(),
8028 &["ho".to_string(), "h".to_string(), "wd".to_string()]
8029 );
8030 }
8031
8032 #[test]
8033 fn visual_block_yank_pads_short_lines_with_empties() {
8034 let mut e = editor_with("hello\nhi\nworld");
8035 run_keys(&mut e, "l");
8036 run_keys(&mut e, "<C-v>");
8037 run_keys(&mut e, "jjll");
8038 run_keys(&mut e, "y");
8039 assert_eq!(e.last_yank.as_deref(), Some("ell\ni\norl"));
8041 }
8042
8043 #[test]
8044 fn visual_block_replace_skips_past_eol() {
8045 let mut e = editor_with("ab\ncd\nef");
8048 run_keys(&mut e, "l");
8050 run_keys(&mut e, "<C-v>");
8051 run_keys(&mut e, "jjllllll");
8052 run_keys(&mut e, "rX");
8053 assert_eq!(
8056 e.buffer().lines(),
8057 &["aX".to_string(), "cX".to_string(), "eX".to_string()]
8058 );
8059 }
8060
8061 #[test]
8062 fn visual_block_with_empty_line_in_middle() {
8063 let mut e = editor_with("abcd\n\nefgh");
8064 run_keys(&mut e, "<C-v>");
8065 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
8067 assert_eq!(
8070 e.buffer().lines(),
8071 &["d".to_string(), "".to_string(), "h".to_string()]
8072 );
8073 }
8074
8075 #[test]
8076 fn block_insert_pads_empty_lines_to_block_column() {
8077 let mut e = editor_with("this is a line\n\nthis is a line");
8080 e.jump_cursor(0, 3);
8081 run_keys(&mut e, "<C-v>");
8082 run_keys(&mut e, "jj");
8083 run_keys(&mut e, "I");
8084 run_keys(&mut e, "XX<Esc>");
8085 assert_eq!(
8086 e.buffer().lines(),
8087 &[
8088 "thiXXs is a line".to_string(),
8089 " XX".to_string(),
8090 "thiXXs is a line".to_string()
8091 ]
8092 );
8093 }
8094
8095 #[test]
8096 fn block_insert_pads_short_lines_to_block_column() {
8097 let mut e = editor_with("aaaaa\nbb\naaaaa");
8098 e.jump_cursor(0, 3);
8099 run_keys(&mut e, "<C-v>");
8100 run_keys(&mut e, "jj");
8101 run_keys(&mut e, "I");
8102 run_keys(&mut e, "Y<Esc>");
8103 assert_eq!(
8105 e.buffer().lines(),
8106 &[
8107 "aaaYaa".to_string(),
8108 "bb Y".to_string(),
8109 "aaaYaa".to_string()
8110 ]
8111 );
8112 }
8113
8114 #[test]
8115 fn visual_block_append_repeats_across_rows() {
8116 let mut e = editor_with("foo\nbar\nbaz");
8117 run_keys(&mut e, "<C-v>");
8118 run_keys(&mut e, "jj");
8119 run_keys(&mut e, "A");
8122 run_keys(&mut e, "!<Esc>");
8123 assert_eq!(
8124 e.buffer().lines(),
8125 &["f!oo".to_string(), "b!ar".to_string(), "b!az".to_string()]
8126 );
8127 }
8128
8129 #[test]
8132 fn slash_opens_forward_search_prompt() {
8133 let mut e = editor_with("hello world");
8134 run_keys(&mut e, "/");
8135 let p = e.search_prompt().expect("prompt should be active");
8136 assert!(p.text.is_empty());
8137 assert!(p.forward);
8138 }
8139
8140 #[test]
8141 fn question_opens_backward_search_prompt() {
8142 let mut e = editor_with("hello world");
8143 run_keys(&mut e, "?");
8144 let p = e.search_prompt().expect("prompt should be active");
8145 assert!(!p.forward);
8146 }
8147
8148 #[test]
8149 fn search_prompt_typing_updates_pattern_live() {
8150 let mut e = editor_with("foo bar\nbaz");
8151 run_keys(&mut e, "/bar");
8152 assert_eq!(e.search_prompt().unwrap().text, "bar");
8153 assert!(e.search_state().pattern.is_some());
8155 }
8156
8157 #[test]
8158 fn search_prompt_backspace_and_enter() {
8159 let mut e = editor_with("hello world\nagain");
8160 run_keys(&mut e, "/worlx");
8161 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
8162 assert_eq!(e.search_prompt().unwrap().text, "worl");
8163 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8164 assert!(e.search_prompt().is_none());
8166 assert_eq!(e.last_search(), Some("worl"));
8167 assert_eq!(e.cursor(), (0, 6));
8168 }
8169
8170 #[test]
8171 fn empty_search_prompt_enter_repeats_last_search() {
8172 let mut e = editor_with("foo bar foo baz foo");
8173 run_keys(&mut e, "/foo");
8174 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8175 assert_eq!(e.cursor().1, 8);
8176 run_keys(&mut e, "/");
8178 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8179 assert_eq!(e.cursor().1, 16);
8180 assert_eq!(e.last_search(), Some("foo"));
8181 }
8182
8183 #[test]
8184 fn search_history_records_committed_patterns() {
8185 let mut e = editor_with("alpha beta gamma");
8186 run_keys(&mut e, "/alpha<CR>");
8187 run_keys(&mut e, "/beta<CR>");
8188 let history = e.vim.search_history.clone();
8190 assert_eq!(history, vec!["alpha", "beta"]);
8191 }
8192
8193 #[test]
8194 fn search_history_dedupes_consecutive_repeats() {
8195 let mut e = editor_with("foo bar foo");
8196 run_keys(&mut e, "/foo<CR>");
8197 run_keys(&mut e, "/foo<CR>");
8198 run_keys(&mut e, "/bar<CR>");
8199 run_keys(&mut e, "/bar<CR>");
8200 assert_eq!(e.vim.search_history.clone(), vec!["foo", "bar"]);
8202 }
8203
8204 #[test]
8205 fn ctrl_p_walks_history_backward() {
8206 let mut e = editor_with("alpha beta gamma");
8207 run_keys(&mut e, "/alpha<CR>");
8208 run_keys(&mut e, "/beta<CR>");
8209 run_keys(&mut e, "/");
8211 assert_eq!(e.search_prompt().unwrap().text, "");
8212 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8213 assert_eq!(e.search_prompt().unwrap().text, "beta");
8214 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8215 assert_eq!(e.search_prompt().unwrap().text, "alpha");
8216 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8218 assert_eq!(e.search_prompt().unwrap().text, "alpha");
8219 }
8220
8221 #[test]
8222 fn ctrl_n_walks_history_forward_after_ctrl_p() {
8223 let mut e = editor_with("a b c");
8224 run_keys(&mut e, "/a<CR>");
8225 run_keys(&mut e, "/b<CR>");
8226 run_keys(&mut e, "/c<CR>");
8227 run_keys(&mut e, "/");
8228 for _ in 0..3 {
8230 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8231 }
8232 assert_eq!(e.search_prompt().unwrap().text, "a");
8233 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8234 assert_eq!(e.search_prompt().unwrap().text, "b");
8235 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8236 assert_eq!(e.search_prompt().unwrap().text, "c");
8237 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8239 assert_eq!(e.search_prompt().unwrap().text, "c");
8240 }
8241
8242 #[test]
8243 fn typing_after_history_walk_resets_cursor() {
8244 let mut e = editor_with("foo");
8245 run_keys(&mut e, "/foo<CR>");
8246 run_keys(&mut e, "/");
8247 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8248 assert_eq!(e.search_prompt().unwrap().text, "foo");
8249 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
8252 assert_eq!(e.search_prompt().unwrap().text, "foox");
8253 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8254 assert_eq!(e.search_prompt().unwrap().text, "foo");
8255 }
8256
8257 #[test]
8258 fn empty_backward_search_prompt_enter_repeats_last_search() {
8259 let mut e = editor_with("foo bar foo baz foo");
8260 run_keys(&mut e, "/foo");
8262 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8263 assert_eq!(e.cursor().1, 8);
8264 run_keys(&mut e, "?");
8265 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8266 assert_eq!(e.cursor().1, 0);
8267 assert_eq!(e.last_search(), Some("foo"));
8268 }
8269
8270 #[test]
8271 fn search_prompt_esc_cancels_but_keeps_last_search() {
8272 let mut e = editor_with("foo bar\nbaz");
8273 run_keys(&mut e, "/bar");
8274 e.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
8275 assert!(e.search_prompt().is_none());
8276 assert_eq!(e.last_search(), Some("bar"));
8277 }
8278
8279 #[test]
8280 fn search_then_n_and_shift_n_navigate() {
8281 let mut e = editor_with("foo bar foo baz foo");
8282 run_keys(&mut e, "/foo");
8283 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8284 assert_eq!(e.cursor().1, 8);
8286 run_keys(&mut e, "n");
8287 assert_eq!(e.cursor().1, 16);
8288 run_keys(&mut e, "N");
8289 assert_eq!(e.cursor().1, 8);
8290 }
8291
8292 #[test]
8293 fn question_mark_searches_backward_on_enter() {
8294 let mut e = editor_with("foo bar foo baz");
8295 e.jump_cursor(0, 10);
8296 run_keys(&mut e, "?foo");
8297 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8298 assert_eq!(e.cursor(), (0, 8));
8300 }
8301
8302 #[test]
8305 fn big_y_yanks_to_end_of_line() {
8306 let mut e = editor_with("hello world");
8307 e.jump_cursor(0, 6);
8308 run_keys(&mut e, "Y");
8309 assert_eq!(e.last_yank.as_deref(), Some("world"));
8310 }
8311
8312 #[test]
8313 fn big_y_from_line_start_yanks_full_line() {
8314 let mut e = editor_with("hello world");
8315 run_keys(&mut e, "Y");
8316 assert_eq!(e.last_yank.as_deref(), Some("hello world"));
8317 }
8318
8319 #[test]
8320 fn gj_joins_without_inserting_space() {
8321 let mut e = editor_with("hello\n world");
8322 run_keys(&mut e, "gJ");
8323 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
8325 }
8326
8327 #[test]
8328 fn gj_noop_on_last_line() {
8329 let mut e = editor_with("only");
8330 run_keys(&mut e, "gJ");
8331 assert_eq!(e.buffer().lines(), &["only".to_string()]);
8332 }
8333
8334 #[test]
8335 fn ge_jumps_to_previous_word_end() {
8336 let mut e = editor_with("foo bar baz");
8337 e.jump_cursor(0, 5);
8338 run_keys(&mut e, "ge");
8339 assert_eq!(e.cursor(), (0, 2));
8340 }
8341
8342 #[test]
8343 fn ge_respects_word_class() {
8344 let mut e = editor_with("foo-bar baz");
8347 e.jump_cursor(0, 5);
8348 run_keys(&mut e, "ge");
8349 assert_eq!(e.cursor(), (0, 3));
8350 }
8351
8352 #[test]
8353 fn big_ge_treats_hyphens_as_part_of_word() {
8354 let mut e = editor_with("foo-bar baz");
8357 e.jump_cursor(0, 10);
8358 run_keys(&mut e, "gE");
8359 assert_eq!(e.cursor(), (0, 6));
8360 }
8361
8362 #[test]
8363 fn ge_crosses_line_boundary() {
8364 let mut e = editor_with("foo\nbar");
8365 e.jump_cursor(1, 0);
8366 run_keys(&mut e, "ge");
8367 assert_eq!(e.cursor(), (0, 2));
8368 }
8369
8370 #[test]
8371 fn dge_deletes_to_end_of_previous_word() {
8372 let mut e = editor_with("foo bar baz");
8373 e.jump_cursor(0, 8);
8374 run_keys(&mut e, "dge");
8377 assert_eq!(e.buffer().lines()[0], "foo baaz");
8378 }
8379
8380 #[test]
8381 fn ctrl_scroll_keys_do_not_panic() {
8382 let mut e = editor_with(
8385 (0..50)
8386 .map(|i| format!("line{i}"))
8387 .collect::<Vec<_>>()
8388 .join("\n")
8389 .as_str(),
8390 );
8391 run_keys(&mut e, "<C-f>");
8392 run_keys(&mut e, "<C-b>");
8393 assert!(!e.buffer().lines().is_empty());
8395 }
8396
8397 #[test]
8404 fn count_insert_with_arrow_nav_does_not_leak_rows() {
8405 let mut e = Editor::new(
8406 hjkl_buffer::Buffer::new(),
8407 crate::types::DefaultHost::new(),
8408 crate::types::Options::default(),
8409 );
8410 e.set_content("row0\nrow1\nrow2");
8411 run_keys(&mut e, "3iX<Down><Esc>");
8413 assert!(e.buffer().lines()[0].contains('X'));
8415 assert!(
8418 !e.buffer().lines()[1].contains("row0"),
8419 "row1 leaked row0 contents: {:?}",
8420 e.buffer().lines()[1]
8421 );
8422 assert_eq!(e.buffer().lines().len(), 3);
8425 }
8426
8427 fn editor_with_rows(n: usize, viewport: u16) -> Editor {
8430 let mut e = Editor::new(
8431 hjkl_buffer::Buffer::new(),
8432 crate::types::DefaultHost::new(),
8433 crate::types::Options::default(),
8434 );
8435 let body = (0..n)
8436 .map(|i| format!(" line{}", i))
8437 .collect::<Vec<_>>()
8438 .join("\n");
8439 e.set_content(&body);
8440 e.set_viewport_height(viewport);
8441 e
8442 }
8443
8444 #[test]
8445 fn ctrl_d_moves_cursor_half_page_down() {
8446 let mut e = editor_with_rows(100, 20);
8447 run_keys(&mut e, "<C-d>");
8448 assert_eq!(e.cursor().0, 10);
8449 }
8450
8451 fn editor_with_wrap_lines(lines: &[&str], viewport: u16, text_width: u16) -> Editor {
8452 let mut e = Editor::new(
8453 hjkl_buffer::Buffer::new(),
8454 crate::types::DefaultHost::new(),
8455 crate::types::Options::default(),
8456 );
8457 e.set_content(&lines.join("\n"));
8458 e.set_viewport_height(viewport);
8459 let v = e.host_mut().viewport_mut();
8460 v.height = viewport;
8461 v.width = text_width;
8462 v.text_width = text_width;
8463 v.wrap = hjkl_buffer::Wrap::Char;
8464 e.settings_mut().wrap = hjkl_buffer::Wrap::Char;
8465 e
8466 }
8467
8468 #[test]
8469 fn scrolloff_wrap_keeps_cursor_off_bottom_edge() {
8470 let lines = ["aaaabbbbcccc"; 10];
8474 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8475 e.jump_cursor(4, 0);
8476 e.ensure_cursor_in_scrolloff();
8477 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8478 assert!(csr <= 6, "csr={csr}");
8479 }
8480
8481 #[test]
8482 fn scrolloff_wrap_keeps_cursor_off_top_edge() {
8483 let lines = ["aaaabbbbcccc"; 10];
8484 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8485 e.jump_cursor(7, 0);
8488 e.ensure_cursor_in_scrolloff();
8489 e.jump_cursor(2, 0);
8490 e.ensure_cursor_in_scrolloff();
8491 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8492 assert!(csr >= 5, "csr={csr}");
8494 }
8495
8496 #[test]
8497 fn scrolloff_wrap_clamps_top_at_buffer_end() {
8498 let lines = ["aaaabbbbcccc"; 5];
8499 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8500 e.jump_cursor(4, 11);
8501 e.ensure_cursor_in_scrolloff();
8502 let top = e.host().viewport().top_row;
8507 assert_eq!(top, 1);
8508 }
8509
8510 #[test]
8511 fn ctrl_u_moves_cursor_half_page_up() {
8512 let mut e = editor_with_rows(100, 20);
8513 e.jump_cursor(50, 0);
8514 run_keys(&mut e, "<C-u>");
8515 assert_eq!(e.cursor().0, 40);
8516 }
8517
8518 #[test]
8519 fn ctrl_f_moves_cursor_full_page_down() {
8520 let mut e = editor_with_rows(100, 20);
8521 run_keys(&mut e, "<C-f>");
8522 assert_eq!(e.cursor().0, 18);
8524 }
8525
8526 #[test]
8527 fn ctrl_b_moves_cursor_full_page_up() {
8528 let mut e = editor_with_rows(100, 20);
8529 e.jump_cursor(50, 0);
8530 run_keys(&mut e, "<C-b>");
8531 assert_eq!(e.cursor().0, 32);
8532 }
8533
8534 #[test]
8535 fn ctrl_d_lands_on_first_non_blank() {
8536 let mut e = editor_with_rows(100, 20);
8537 run_keys(&mut e, "<C-d>");
8538 assert_eq!(e.cursor().1, 2);
8540 }
8541
8542 #[test]
8543 fn ctrl_d_clamps_at_end_of_buffer() {
8544 let mut e = editor_with_rows(5, 20);
8545 run_keys(&mut e, "<C-d>");
8546 assert_eq!(e.cursor().0, 4);
8547 }
8548
8549 #[test]
8550 fn capital_h_jumps_to_viewport_top() {
8551 let mut e = editor_with_rows(100, 10);
8552 e.jump_cursor(50, 0);
8553 e.set_viewport_top(45);
8554 let top = e.host().viewport().top_row;
8555 run_keys(&mut e, "H");
8556 assert_eq!(e.cursor().0, top);
8557 assert_eq!(e.cursor().1, 2);
8558 }
8559
8560 #[test]
8561 fn capital_l_jumps_to_viewport_bottom() {
8562 let mut e = editor_with_rows(100, 10);
8563 e.jump_cursor(50, 0);
8564 e.set_viewport_top(45);
8565 let top = e.host().viewport().top_row;
8566 run_keys(&mut e, "L");
8567 assert_eq!(e.cursor().0, top + 9);
8568 }
8569
8570 #[test]
8571 fn capital_m_jumps_to_viewport_middle() {
8572 let mut e = editor_with_rows(100, 10);
8573 e.jump_cursor(50, 0);
8574 e.set_viewport_top(45);
8575 let top = e.host().viewport().top_row;
8576 run_keys(&mut e, "M");
8577 assert_eq!(e.cursor().0, top + 4);
8579 }
8580
8581 #[test]
8582 fn g_capital_m_lands_at_line_midpoint() {
8583 let mut e = editor_with("hello world!"); run_keys(&mut e, "gM");
8585 assert_eq!(e.cursor(), (0, 6));
8587 }
8588
8589 #[test]
8590 fn g_capital_m_on_empty_line_stays_at_zero() {
8591 let mut e = editor_with("");
8592 run_keys(&mut e, "gM");
8593 assert_eq!(e.cursor(), (0, 0));
8594 }
8595
8596 #[test]
8597 fn g_capital_m_uses_current_line_only() {
8598 let mut e = editor_with("a\nlonglongline"); e.jump_cursor(1, 0);
8601 run_keys(&mut e, "gM");
8602 assert_eq!(e.cursor(), (1, 6));
8603 }
8604
8605 #[test]
8606 fn capital_h_count_offsets_from_top() {
8607 let mut e = editor_with_rows(100, 10);
8608 e.jump_cursor(50, 0);
8609 e.set_viewport_top(45);
8610 let top = e.host().viewport().top_row;
8611 run_keys(&mut e, "3H");
8612 assert_eq!(e.cursor().0, top + 2);
8613 }
8614
8615 #[test]
8618 fn ctrl_o_returns_to_pre_g_position() {
8619 let mut e = editor_with_rows(50, 20);
8620 e.jump_cursor(5, 2);
8621 run_keys(&mut e, "G");
8622 assert_eq!(e.cursor().0, 49);
8623 run_keys(&mut e, "<C-o>");
8624 assert_eq!(e.cursor(), (5, 2));
8625 }
8626
8627 #[test]
8628 fn ctrl_i_redoes_jump_after_ctrl_o() {
8629 let mut e = editor_with_rows(50, 20);
8630 e.jump_cursor(5, 2);
8631 run_keys(&mut e, "G");
8632 let post = e.cursor();
8633 run_keys(&mut e, "<C-o>");
8634 run_keys(&mut e, "<C-i>");
8635 assert_eq!(e.cursor(), post);
8636 }
8637
8638 #[test]
8639 fn new_jump_clears_forward_stack() {
8640 let mut e = editor_with_rows(50, 20);
8641 e.jump_cursor(5, 2);
8642 run_keys(&mut e, "G");
8643 run_keys(&mut e, "<C-o>");
8644 run_keys(&mut e, "gg");
8645 run_keys(&mut e, "<C-i>");
8646 assert_eq!(e.cursor().0, 0);
8647 }
8648
8649 #[test]
8650 fn ctrl_o_on_empty_stack_is_noop() {
8651 let mut e = editor_with_rows(10, 20);
8652 e.jump_cursor(3, 1);
8653 run_keys(&mut e, "<C-o>");
8654 assert_eq!(e.cursor(), (3, 1));
8655 }
8656
8657 #[test]
8658 fn asterisk_search_pushes_jump() {
8659 let mut e = editor_with("foo bar\nbaz foo end");
8660 e.jump_cursor(0, 0);
8661 run_keys(&mut e, "*");
8662 let after = e.cursor();
8663 assert_ne!(after, (0, 0));
8664 run_keys(&mut e, "<C-o>");
8665 assert_eq!(e.cursor(), (0, 0));
8666 }
8667
8668 #[test]
8669 fn h_viewport_jump_is_recorded() {
8670 let mut e = editor_with_rows(100, 10);
8671 e.jump_cursor(50, 0);
8672 e.set_viewport_top(45);
8673 let pre = e.cursor();
8674 run_keys(&mut e, "H");
8675 assert_ne!(e.cursor(), pre);
8676 run_keys(&mut e, "<C-o>");
8677 assert_eq!(e.cursor(), pre);
8678 }
8679
8680 #[test]
8681 fn j_k_motion_does_not_push_jump() {
8682 let mut e = editor_with_rows(50, 20);
8683 e.jump_cursor(5, 0);
8684 run_keys(&mut e, "jjj");
8685 run_keys(&mut e, "<C-o>");
8686 assert_eq!(e.cursor().0, 8);
8687 }
8688
8689 #[test]
8690 fn jumplist_caps_at_100() {
8691 let mut e = editor_with_rows(200, 20);
8692 for i in 0..101 {
8693 e.jump_cursor(i, 0);
8694 run_keys(&mut e, "G");
8695 }
8696 assert!(e.vim.jump_back.len() <= 100);
8697 }
8698
8699 #[test]
8700 fn tab_acts_as_ctrl_i() {
8701 let mut e = editor_with_rows(50, 20);
8702 e.jump_cursor(5, 2);
8703 run_keys(&mut e, "G");
8704 let post = e.cursor();
8705 run_keys(&mut e, "<C-o>");
8706 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8707 assert_eq!(e.cursor(), post);
8708 }
8709
8710 #[test]
8713 fn ma_then_backtick_a_jumps_exact() {
8714 let mut e = editor_with_rows(50, 20);
8715 e.jump_cursor(5, 3);
8716 run_keys(&mut e, "ma");
8717 e.jump_cursor(20, 0);
8718 run_keys(&mut e, "`a");
8719 assert_eq!(e.cursor(), (5, 3));
8720 }
8721
8722 #[test]
8723 fn ma_then_apostrophe_a_lands_on_first_non_blank() {
8724 let mut e = editor_with_rows(50, 20);
8725 e.jump_cursor(5, 6);
8727 run_keys(&mut e, "ma");
8728 e.jump_cursor(30, 4);
8729 run_keys(&mut e, "'a");
8730 assert_eq!(e.cursor(), (5, 2));
8731 }
8732
8733 #[test]
8734 fn goto_mark_pushes_jumplist() {
8735 let mut e = editor_with_rows(50, 20);
8736 e.jump_cursor(10, 2);
8737 run_keys(&mut e, "mz");
8738 e.jump_cursor(3, 0);
8739 run_keys(&mut e, "`z");
8740 assert_eq!(e.cursor(), (10, 2));
8741 run_keys(&mut e, "<C-o>");
8742 assert_eq!(e.cursor(), (3, 0));
8743 }
8744
8745 #[test]
8746 fn goto_missing_mark_is_noop() {
8747 let mut e = editor_with_rows(50, 20);
8748 e.jump_cursor(3, 1);
8749 run_keys(&mut e, "`q");
8750 assert_eq!(e.cursor(), (3, 1));
8751 }
8752
8753 #[test]
8754 fn uppercase_mark_stored_under_uppercase_key() {
8755 let mut e = editor_with_rows(50, 20);
8756 e.jump_cursor(5, 3);
8757 run_keys(&mut e, "mA");
8758 assert_eq!(e.mark('A'), Some((5, 3)));
8761 assert!(e.mark('a').is_none());
8762 }
8763
8764 #[test]
8765 fn mark_survives_document_shrink_via_clamp() {
8766 let mut e = editor_with_rows(50, 20);
8767 e.jump_cursor(40, 4);
8768 run_keys(&mut e, "mx");
8769 e.set_content("a\nb\nc\nd\ne");
8771 run_keys(&mut e, "`x");
8772 let (r, _) = e.cursor();
8774 assert!(r <= 4);
8775 }
8776
8777 #[test]
8778 fn g_semicolon_walks_back_through_edits() {
8779 let mut e = editor_with("alpha\nbeta\ngamma");
8780 e.jump_cursor(0, 0);
8783 run_keys(&mut e, "iX<Esc>");
8784 e.jump_cursor(2, 0);
8785 run_keys(&mut e, "iY<Esc>");
8786 run_keys(&mut e, "g;");
8788 assert_eq!(e.cursor(), (2, 1));
8789 run_keys(&mut e, "g;");
8791 assert_eq!(e.cursor(), (0, 1));
8792 run_keys(&mut e, "g;");
8794 assert_eq!(e.cursor(), (0, 1));
8795 }
8796
8797 #[test]
8798 fn g_comma_walks_forward_after_g_semicolon() {
8799 let mut e = editor_with("a\nb\nc");
8800 e.jump_cursor(0, 0);
8801 run_keys(&mut e, "iX<Esc>");
8802 e.jump_cursor(2, 0);
8803 run_keys(&mut e, "iY<Esc>");
8804 run_keys(&mut e, "g;");
8805 run_keys(&mut e, "g;");
8806 assert_eq!(e.cursor(), (0, 1));
8807 run_keys(&mut e, "g,");
8808 assert_eq!(e.cursor(), (2, 1));
8809 }
8810
8811 #[test]
8812 fn new_edit_during_walk_trims_forward_entries() {
8813 let mut e = editor_with("a\nb\nc\nd");
8814 e.jump_cursor(0, 0);
8815 run_keys(&mut e, "iX<Esc>"); e.jump_cursor(2, 0);
8817 run_keys(&mut e, "iY<Esc>"); run_keys(&mut e, "g;");
8820 run_keys(&mut e, "g;");
8821 assert_eq!(e.cursor(), (0, 1));
8822 run_keys(&mut e, "iZ<Esc>");
8824 run_keys(&mut e, "g,");
8826 assert_ne!(e.cursor(), (2, 1));
8828 }
8829
8830 #[test]
8836 fn capital_mark_set_and_jump() {
8837 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
8838 e.jump_cursor(2, 1);
8839 run_keys(&mut e, "mA");
8840 e.jump_cursor(0, 0);
8842 run_keys(&mut e, "'A");
8844 assert_eq!(e.cursor().0, 2);
8846 }
8847
8848 #[test]
8849 fn capital_mark_survives_set_content() {
8850 let mut e = editor_with("first buffer line\nsecond");
8851 e.jump_cursor(1, 3);
8852 run_keys(&mut e, "mA");
8853 e.set_content("totally different content\non many\nrows of text");
8855 e.jump_cursor(0, 0);
8857 run_keys(&mut e, "'A");
8858 assert_eq!(e.cursor().0, 1);
8859 }
8860
8861 #[test]
8866 fn capital_mark_shifts_with_edit() {
8867 let mut e = editor_with("a\nb\nc\nd");
8868 e.jump_cursor(3, 0);
8869 run_keys(&mut e, "mA");
8870 e.jump_cursor(0, 0);
8872 run_keys(&mut e, "dd");
8873 e.jump_cursor(0, 0);
8874 run_keys(&mut e, "'A");
8875 assert_eq!(e.cursor().0, 2);
8876 }
8877
8878 #[test]
8879 fn mark_below_delete_shifts_up() {
8880 let mut e = editor_with("a\nb\nc\nd\ne");
8881 e.jump_cursor(3, 0);
8883 run_keys(&mut e, "ma");
8884 e.jump_cursor(0, 0);
8886 run_keys(&mut e, "dd");
8887 e.jump_cursor(0, 0);
8889 run_keys(&mut e, "'a");
8890 assert_eq!(e.cursor().0, 2);
8891 assert_eq!(e.buffer().line(2).unwrap(), "d");
8892 }
8893
8894 #[test]
8895 fn mark_on_deleted_row_is_dropped() {
8896 let mut e = editor_with("a\nb\nc\nd");
8897 e.jump_cursor(1, 0);
8899 run_keys(&mut e, "ma");
8900 run_keys(&mut e, "dd");
8902 e.jump_cursor(2, 0);
8904 run_keys(&mut e, "'a");
8905 assert_eq!(e.cursor().0, 2);
8907 }
8908
8909 #[test]
8910 fn mark_above_edit_unchanged() {
8911 let mut e = editor_with("a\nb\nc\nd\ne");
8912 e.jump_cursor(0, 0);
8914 run_keys(&mut e, "ma");
8915 e.jump_cursor(3, 0);
8917 run_keys(&mut e, "dd");
8918 e.jump_cursor(2, 0);
8920 run_keys(&mut e, "'a");
8921 assert_eq!(e.cursor().0, 0);
8922 }
8923
8924 #[test]
8925 fn mark_shifts_down_after_insert() {
8926 let mut e = editor_with("a\nb\nc");
8927 e.jump_cursor(2, 0);
8929 run_keys(&mut e, "ma");
8930 e.jump_cursor(0, 0);
8932 run_keys(&mut e, "Onew<Esc>");
8933 e.jump_cursor(0, 0);
8936 run_keys(&mut e, "'a");
8937 assert_eq!(e.cursor().0, 3);
8938 assert_eq!(e.buffer().line(3).unwrap(), "c");
8939 }
8940
8941 #[test]
8944 fn forward_search_commit_pushes_jump() {
8945 let mut e = editor_with("alpha beta\nfoo target end\nmore");
8946 e.jump_cursor(0, 0);
8947 run_keys(&mut e, "/target<CR>");
8948 assert_ne!(e.cursor(), (0, 0));
8950 run_keys(&mut e, "<C-o>");
8952 assert_eq!(e.cursor(), (0, 0));
8953 }
8954
8955 #[test]
8956 fn search_commit_no_match_does_not_push_jump() {
8957 let mut e = editor_with("alpha beta\nfoo end");
8958 e.jump_cursor(0, 3);
8959 let pre_len = e.vim.jump_back.len();
8960 run_keys(&mut e, "/zzznotfound<CR>");
8961 assert_eq!(e.vim.jump_back.len(), pre_len);
8963 }
8964
8965 #[test]
8968 fn buffer_cursor_mirrors_textarea_after_horizontal_motion() {
8969 let mut e = editor_with("hello world");
8970 run_keys(&mut e, "lll");
8971 let (row, col) = e.cursor();
8972 assert_eq!(e.buffer.cursor().row, row);
8973 assert_eq!(e.buffer.cursor().col, col);
8974 }
8975
8976 #[test]
8977 fn buffer_cursor_mirrors_textarea_after_vertical_motion() {
8978 let mut e = editor_with("aaaa\nbbbb\ncccc");
8979 run_keys(&mut e, "jj");
8980 let (row, col) = e.cursor();
8981 assert_eq!(e.buffer.cursor().row, row);
8982 assert_eq!(e.buffer.cursor().col, col);
8983 }
8984
8985 #[test]
8986 fn buffer_cursor_mirrors_textarea_after_word_motion() {
8987 let mut e = editor_with("foo bar baz");
8988 run_keys(&mut e, "ww");
8989 let (row, col) = e.cursor();
8990 assert_eq!(e.buffer.cursor().row, row);
8991 assert_eq!(e.buffer.cursor().col, col);
8992 }
8993
8994 #[test]
8995 fn buffer_cursor_mirrors_textarea_after_jump_motion() {
8996 let mut e = editor_with("a\nb\nc\nd\ne");
8997 run_keys(&mut e, "G");
8998 let (row, col) = e.cursor();
8999 assert_eq!(e.buffer.cursor().row, row);
9000 assert_eq!(e.buffer.cursor().col, col);
9001 }
9002
9003 #[test]
9004 fn editor_sticky_col_tracks_horizontal_motion() {
9005 let mut e = editor_with("longline\nhi\nlongline");
9006 run_keys(&mut e, "fl");
9011 let landed = e.cursor().1;
9012 assert!(landed > 0, "fl should have moved");
9013 run_keys(&mut e, "j");
9014 assert_eq!(e.sticky_col(), Some(landed));
9017 }
9018
9019 #[test]
9020 fn buffer_content_mirrors_textarea_after_insert() {
9021 let mut e = editor_with("hello");
9022 run_keys(&mut e, "iXYZ<Esc>");
9023 let text = e.buffer().lines().join("\n");
9024 assert_eq!(e.buffer.as_string(), text);
9025 }
9026
9027 #[test]
9028 fn buffer_content_mirrors_textarea_after_delete() {
9029 let mut e = editor_with("alpha bravo charlie");
9030 run_keys(&mut e, "dw");
9031 let text = e.buffer().lines().join("\n");
9032 assert_eq!(e.buffer.as_string(), text);
9033 }
9034
9035 #[test]
9036 fn buffer_content_mirrors_textarea_after_dd() {
9037 let mut e = editor_with("a\nb\nc\nd");
9038 run_keys(&mut e, "jdd");
9039 let text = e.buffer().lines().join("\n");
9040 assert_eq!(e.buffer.as_string(), text);
9041 }
9042
9043 #[test]
9044 fn buffer_content_mirrors_textarea_after_open_line() {
9045 let mut e = editor_with("foo\nbar");
9046 run_keys(&mut e, "oNEW<Esc>");
9047 let text = e.buffer().lines().join("\n");
9048 assert_eq!(e.buffer.as_string(), text);
9049 }
9050
9051 #[test]
9052 fn buffer_content_mirrors_textarea_after_paste() {
9053 let mut e = editor_with("hello");
9054 run_keys(&mut e, "yy");
9055 run_keys(&mut e, "p");
9056 let text = e.buffer().lines().join("\n");
9057 assert_eq!(e.buffer.as_string(), text);
9058 }
9059
9060 #[test]
9061 fn buffer_selection_none_in_normal_mode() {
9062 let e = editor_with("foo bar");
9063 assert!(e.buffer_selection().is_none());
9064 }
9065
9066 #[test]
9067 fn buffer_selection_char_in_visual_mode() {
9068 use hjkl_buffer::{Position, Selection};
9069 let mut e = editor_with("hello world");
9070 run_keys(&mut e, "vlll");
9071 assert_eq!(
9072 e.buffer_selection(),
9073 Some(Selection::Char {
9074 anchor: Position::new(0, 0),
9075 head: Position::new(0, 3),
9076 })
9077 );
9078 }
9079
9080 #[test]
9081 fn buffer_selection_line_in_visual_line_mode() {
9082 use hjkl_buffer::Selection;
9083 let mut e = editor_with("a\nb\nc\nd");
9084 run_keys(&mut e, "Vj");
9085 assert_eq!(
9086 e.buffer_selection(),
9087 Some(Selection::Line {
9088 anchor_row: 0,
9089 head_row: 1,
9090 })
9091 );
9092 }
9093
9094 #[test]
9095 fn wrapscan_off_blocks_wrap_around() {
9096 let mut e = editor_with("first\nsecond\nthird\n");
9097 e.settings_mut().wrapscan = false;
9098 e.jump_cursor(2, 0);
9100 run_keys(&mut e, "/first<CR>");
9101 assert_eq!(e.cursor().0, 2, "wrapscan off should block wrap");
9103 e.settings_mut().wrapscan = true;
9105 run_keys(&mut e, "/first<CR>");
9106 assert_eq!(e.cursor().0, 0, "wrapscan on should wrap to row 0");
9107 }
9108
9109 #[test]
9110 fn smartcase_uppercase_pattern_stays_sensitive() {
9111 let mut e = editor_with("foo\nFoo\nBAR\n");
9112 e.settings_mut().ignore_case = true;
9113 e.settings_mut().smartcase = true;
9114 run_keys(&mut e, "/foo<CR>");
9117 let r1 = e
9118 .search_state()
9119 .pattern
9120 .as_ref()
9121 .unwrap()
9122 .as_str()
9123 .to_string();
9124 assert!(r1.starts_with("(?i)"), "lowercase under smartcase: {r1}");
9125 run_keys(&mut e, "/Foo<CR>");
9127 let r2 = e
9128 .search_state()
9129 .pattern
9130 .as_ref()
9131 .unwrap()
9132 .as_str()
9133 .to_string();
9134 assert!(!r2.starts_with("(?i)"), "mixed-case under smartcase: {r2}");
9135 }
9136
9137 #[test]
9138 fn enter_with_autoindent_copies_leading_whitespace() {
9139 let mut e = editor_with(" foo");
9140 e.jump_cursor(0, 7);
9141 run_keys(&mut e, "i<CR>");
9142 assert_eq!(e.buffer.line(1).unwrap(), " ");
9143 }
9144
9145 #[test]
9146 fn enter_without_autoindent_inserts_bare_newline() {
9147 let mut e = editor_with(" foo");
9148 e.settings_mut().autoindent = false;
9149 e.jump_cursor(0, 7);
9150 run_keys(&mut e, "i<CR>");
9151 assert_eq!(e.buffer.line(1).unwrap(), "");
9152 }
9153
9154 #[test]
9155 fn iskeyword_default_treats_alnum_underscore_as_word() {
9156 let mut e = editor_with("foo_bar baz");
9157 e.jump_cursor(0, 0);
9161 run_keys(&mut e, "*");
9162 let p = e
9163 .search_state()
9164 .pattern
9165 .as_ref()
9166 .unwrap()
9167 .as_str()
9168 .to_string();
9169 assert!(p.contains("foo_bar"), "default iskeyword: {p}");
9170 }
9171
9172 #[test]
9173 fn w_motion_respects_custom_iskeyword() {
9174 let mut e = editor_with("foo-bar baz");
9178 run_keys(&mut e, "w");
9179 assert_eq!(e.cursor().1, 3, "default iskeyword: {:?}", e.cursor());
9180 let mut e2 = editor_with("foo-bar baz");
9183 e2.set_iskeyword("@,_,45");
9184 run_keys(&mut e2, "w");
9185 assert_eq!(e2.cursor().1, 8, "dash-as-word: {:?}", e2.cursor());
9186 }
9187
9188 #[test]
9189 fn iskeyword_with_dash_treats_dash_as_word_char() {
9190 let mut e = editor_with("foo-bar baz");
9191 e.settings_mut().iskeyword = "@,_,45".to_string();
9192 e.jump_cursor(0, 0);
9193 run_keys(&mut e, "*");
9194 let p = e
9195 .search_state()
9196 .pattern
9197 .as_ref()
9198 .unwrap()
9199 .as_str()
9200 .to_string();
9201 assert!(p.contains("foo-bar"), "dash-as-word: {p}");
9202 }
9203
9204 #[test]
9205 fn timeoutlen_drops_pending_g_prefix() {
9206 use std::time::{Duration, Instant};
9207 let mut e = editor_with("a\nb\nc");
9208 e.jump_cursor(2, 0);
9209 run_keys(&mut e, "g");
9211 assert!(matches!(e.vim.pending, super::Pending::G));
9212 e.settings.timeout_len = Duration::from_nanos(0);
9220 e.vim.last_input_at = Some(Instant::now() - Duration::from_secs(60));
9221 e.vim.last_input_host_at = Some(Duration::ZERO);
9222 run_keys(&mut e, "g");
9226 assert_eq!(e.cursor().0, 2, "timeout must abandon g-prefix");
9228 }
9229
9230 #[test]
9231 fn undobreak_on_breaks_group_at_arrow_motion() {
9232 let mut e = editor_with("");
9233 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
9235 let line = e.buffer.line(0).unwrap_or("").to_string();
9238 assert!(line.contains("aaa"), "after undobreak: {line:?}");
9239 assert!(!line.contains("bbb"), "bbb should be undone: {line:?}");
9240 }
9241
9242 #[test]
9243 fn undobreak_off_keeps_full_run_in_one_group() {
9244 let mut e = editor_with("");
9245 e.settings_mut().undo_break_on_motion = false;
9246 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
9247 assert_eq!(e.buffer.line(0).unwrap_or(""), "");
9250 }
9251
9252 #[test]
9253 fn undobreak_round_trips_through_options() {
9254 let e = editor_with("");
9255 let opts = e.current_options();
9256 assert!(opts.undo_break_on_motion);
9257 let mut e2 = editor_with("");
9258 let mut new_opts = opts.clone();
9259 new_opts.undo_break_on_motion = false;
9260 e2.apply_options(&new_opts);
9261 assert!(!e2.current_options().undo_break_on_motion);
9262 }
9263
9264 #[test]
9265 fn undo_levels_cap_drops_oldest() {
9266 let mut e = editor_with("abcde");
9267 e.settings_mut().undo_levels = 3;
9268 run_keys(&mut e, "ra");
9269 run_keys(&mut e, "lrb");
9270 run_keys(&mut e, "lrc");
9271 run_keys(&mut e, "lrd");
9272 run_keys(&mut e, "lre");
9273 assert_eq!(e.undo_stack_len(), 3);
9274 }
9275
9276 #[test]
9277 fn tab_inserts_literal_tab_when_noexpandtab() {
9278 let mut e = editor_with("");
9279 e.settings_mut().expandtab = false;
9282 e.settings_mut().softtabstop = 0;
9283 run_keys(&mut e, "i");
9284 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9285 assert_eq!(e.buffer.line(0).unwrap(), "\t");
9286 }
9287
9288 #[test]
9289 fn tab_inserts_spaces_when_expandtab() {
9290 let mut e = editor_with("");
9291 e.settings_mut().expandtab = true;
9292 e.settings_mut().tabstop = 4;
9293 run_keys(&mut e, "i");
9294 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9295 assert_eq!(e.buffer.line(0).unwrap(), " ");
9296 }
9297
9298 #[test]
9299 fn tab_with_softtabstop_fills_to_next_boundary() {
9300 let mut e = editor_with("ab");
9302 e.settings_mut().expandtab = true;
9303 e.settings_mut().tabstop = 8;
9304 e.settings_mut().softtabstop = 4;
9305 run_keys(&mut e, "A"); e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9307 assert_eq!(e.buffer.line(0).unwrap(), "ab ");
9308 }
9309
9310 #[test]
9311 fn backspace_deletes_softtab_run() {
9312 let mut e = editor_with(" x");
9315 e.settings_mut().softtabstop = 4;
9316 run_keys(&mut e, "fxi");
9318 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
9319 assert_eq!(e.buffer.line(0).unwrap(), "x");
9320 }
9321
9322 #[test]
9323 fn backspace_falls_back_to_single_char_when_run_not_aligned() {
9324 let mut e = editor_with(" x");
9327 e.settings_mut().softtabstop = 4;
9328 run_keys(&mut e, "fxi");
9329 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
9330 assert_eq!(e.buffer.line(0).unwrap(), " x");
9331 }
9332
9333 #[test]
9334 fn readonly_blocks_insert_mutation() {
9335 let mut e = editor_with("hello");
9336 e.settings_mut().readonly = true;
9337 run_keys(&mut e, "iX<Esc>");
9338 assert_eq!(e.buffer.line(0).unwrap(), "hello");
9339 }
9340
9341 #[cfg(feature = "ratatui")]
9342 #[test]
9343 fn intern_ratatui_style_dedups_repeated_styles() {
9344 use ratatui::style::{Color, Style};
9345 let mut e = editor_with("");
9346 let red = Style::default().fg(Color::Red);
9347 let blue = Style::default().fg(Color::Blue);
9348 let id_r1 = e.intern_ratatui_style(red);
9349 let id_r2 = e.intern_ratatui_style(red);
9350 let id_b = e.intern_ratatui_style(blue);
9351 assert_eq!(id_r1, id_r2);
9352 assert_ne!(id_r1, id_b);
9353 assert_eq!(e.style_table().len(), 2);
9354 }
9355
9356 #[cfg(feature = "ratatui")]
9357 #[test]
9358 fn install_ratatui_syntax_spans_translates_styled_spans() {
9359 use ratatui::style::{Color, Style};
9360 let mut e = editor_with("SELECT foo");
9361 e.install_ratatui_syntax_spans(vec![vec![(0, 6, Style::default().fg(Color::Red))]]);
9362 let by_row = e.buffer_spans();
9363 assert_eq!(by_row.len(), 1);
9364 assert_eq!(by_row[0].len(), 1);
9365 assert_eq!(by_row[0][0].start_byte, 0);
9366 assert_eq!(by_row[0][0].end_byte, 6);
9367 let id = by_row[0][0].style;
9368 assert_eq!(e.style_table()[id as usize].fg, Some(Color::Red));
9369 }
9370
9371 #[cfg(feature = "ratatui")]
9372 #[test]
9373 fn install_ratatui_syntax_spans_clamps_sentinel_end() {
9374 use ratatui::style::{Color, Style};
9375 let mut e = editor_with("hello");
9376 e.install_ratatui_syntax_spans(vec![vec![(
9377 0,
9378 usize::MAX,
9379 Style::default().fg(Color::Blue),
9380 )]]);
9381 let by_row = e.buffer_spans();
9382 assert_eq!(by_row[0][0].end_byte, 5);
9383 }
9384
9385 #[cfg(feature = "ratatui")]
9386 #[test]
9387 fn install_ratatui_syntax_spans_drops_zero_width() {
9388 use ratatui::style::{Color, Style};
9389 let mut e = editor_with("abc");
9390 e.install_ratatui_syntax_spans(vec![vec![(2, 2, Style::default().fg(Color::Red))]]);
9391 assert!(e.buffer_spans()[0].is_empty());
9392 }
9393
9394 #[test]
9395 fn named_register_yank_into_a_then_paste_from_a() {
9396 let mut e = editor_with("hello world\nsecond");
9397 run_keys(&mut e, "\"ayw");
9398 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
9400 run_keys(&mut e, "j0\"aP");
9402 assert_eq!(e.buffer().lines()[1], "hello second");
9403 }
9404
9405 #[test]
9406 fn capital_r_overstrikes_chars() {
9407 let mut e = editor_with("hello");
9408 e.jump_cursor(0, 0);
9409 run_keys(&mut e, "RXY<Esc>");
9410 assert_eq!(e.buffer().lines()[0], "XYllo");
9412 }
9413
9414 #[test]
9415 fn capital_r_at_eol_appends() {
9416 let mut e = editor_with("hi");
9417 e.jump_cursor(0, 1);
9418 run_keys(&mut e, "RXYZ<Esc>");
9420 assert_eq!(e.buffer().lines()[0], "hXYZ");
9421 }
9422
9423 #[test]
9424 fn capital_r_count_does_not_repeat_overstrike_char_by_char() {
9425 let mut e = editor_with("abc");
9429 e.jump_cursor(0, 0);
9430 run_keys(&mut e, "RX<Esc>");
9431 assert_eq!(e.buffer().lines()[0], "Xbc");
9432 }
9433
9434 #[test]
9435 fn ctrl_r_in_insert_pastes_named_register() {
9436 let mut e = editor_with("hello world");
9437 run_keys(&mut e, "\"ayw");
9439 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
9440 run_keys(&mut e, "o");
9442 assert_eq!(e.vim_mode(), VimMode::Insert);
9443 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9444 e.handle_key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
9445 assert_eq!(e.buffer().lines()[1], "hello ");
9446 assert_eq!(e.cursor(), (1, 6));
9448 assert_eq!(e.vim_mode(), VimMode::Insert);
9450 e.handle_key(KeyEvent::new(KeyCode::Char('X'), KeyModifiers::NONE));
9451 assert_eq!(e.buffer().lines()[1], "hello X");
9452 }
9453
9454 #[test]
9455 fn ctrl_r_with_unnamed_register() {
9456 let mut e = editor_with("foo");
9457 run_keys(&mut e, "yiw");
9458 run_keys(&mut e, "A ");
9459 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9461 e.handle_key(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE));
9462 assert_eq!(e.buffer().lines()[0], "foo foo");
9463 }
9464
9465 #[test]
9466 fn ctrl_r_unknown_selector_is_no_op() {
9467 let mut e = editor_with("abc");
9468 run_keys(&mut e, "A");
9469 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9470 e.handle_key(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE));
9473 e.handle_key(KeyEvent::new(KeyCode::Char('Z'), KeyModifiers::NONE));
9474 assert_eq!(e.buffer().lines()[0], "abcZ");
9475 }
9476
9477 #[test]
9478 fn ctrl_r_multiline_register_pastes_with_newlines() {
9479 let mut e = editor_with("alpha\nbeta\ngamma");
9480 run_keys(&mut e, "\"byy");
9482 run_keys(&mut e, "j\"byy");
9483 run_keys(&mut e, "ggVj\"by");
9487 let payload = e.registers().read('b').unwrap().text.clone();
9488 assert!(payload.contains('\n'));
9489 run_keys(&mut e, "Go");
9490 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9491 e.handle_key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));
9492 let total_lines = e.buffer().lines().len();
9495 assert!(total_lines >= 5);
9496 }
9497
9498 #[test]
9499 fn yank_zero_holds_last_yank_after_delete() {
9500 let mut e = editor_with("hello world");
9501 run_keys(&mut e, "yw");
9502 let yanked = e.registers().read('0').unwrap().text.clone();
9503 assert!(!yanked.is_empty());
9504 run_keys(&mut e, "dw");
9506 assert_eq!(e.registers().read('0').unwrap().text, yanked);
9507 assert!(!e.registers().read('1').unwrap().text.is_empty());
9509 }
9510
9511 #[test]
9512 fn delete_ring_rotates_through_one_through_nine() {
9513 let mut e = editor_with("a b c d e f g h i j");
9514 for _ in 0..3 {
9516 run_keys(&mut e, "dw");
9517 }
9518 let r1 = e.registers().read('1').unwrap().text.clone();
9520 let r2 = e.registers().read('2').unwrap().text.clone();
9521 let r3 = e.registers().read('3').unwrap().text.clone();
9522 assert!(!r1.is_empty() && !r2.is_empty() && !r3.is_empty());
9523 assert_ne!(r1, r2);
9524 assert_ne!(r2, r3);
9525 }
9526
9527 #[test]
9528 fn capital_register_appends_to_lowercase() {
9529 let mut e = editor_with("foo bar");
9530 run_keys(&mut e, "\"ayw");
9531 let first = e.registers().read('a').unwrap().text.clone();
9532 assert!(first.contains("foo"));
9533 run_keys(&mut e, "w\"Ayw");
9535 let combined = e.registers().read('a').unwrap().text.clone();
9536 assert!(combined.starts_with(&first));
9537 assert!(combined.contains("bar"));
9538 }
9539
9540 #[test]
9541 fn zf_in_visual_line_creates_closed_fold() {
9542 let mut e = editor_with("a\nb\nc\nd\ne");
9543 e.jump_cursor(1, 0);
9545 run_keys(&mut e, "Vjjzf");
9546 assert_eq!(e.buffer().folds().len(), 1);
9547 let f = e.buffer().folds()[0];
9548 assert_eq!(f.start_row, 1);
9549 assert_eq!(f.end_row, 3);
9550 assert!(f.closed);
9551 }
9552
9553 #[test]
9554 fn zfj_in_normal_creates_two_row_fold() {
9555 let mut e = editor_with("a\nb\nc\nd\ne");
9556 e.jump_cursor(1, 0);
9557 run_keys(&mut e, "zfj");
9558 assert_eq!(e.buffer().folds().len(), 1);
9559 let f = e.buffer().folds()[0];
9560 assert_eq!(f.start_row, 1);
9561 assert_eq!(f.end_row, 2);
9562 assert!(f.closed);
9563 assert_eq!(e.cursor().0, 1);
9565 }
9566
9567 #[test]
9568 fn zf_with_count_folds_count_rows() {
9569 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9570 e.jump_cursor(0, 0);
9571 run_keys(&mut e, "zf3j");
9573 assert_eq!(e.buffer().folds().len(), 1);
9574 let f = e.buffer().folds()[0];
9575 assert_eq!(f.start_row, 0);
9576 assert_eq!(f.end_row, 3);
9577 }
9578
9579 #[test]
9580 fn zfk_folds_upward_range() {
9581 let mut e = editor_with("a\nb\nc\nd\ne");
9582 e.jump_cursor(3, 0);
9583 run_keys(&mut e, "zfk");
9584 let f = e.buffer().folds()[0];
9585 assert_eq!(f.start_row, 2);
9587 assert_eq!(f.end_row, 3);
9588 }
9589
9590 #[test]
9591 fn zf_capital_g_folds_to_bottom() {
9592 let mut e = editor_with("a\nb\nc\nd\ne");
9593 e.jump_cursor(1, 0);
9594 run_keys(&mut e, "zfG");
9596 let f = e.buffer().folds()[0];
9597 assert_eq!(f.start_row, 1);
9598 assert_eq!(f.end_row, 4);
9599 }
9600
9601 #[test]
9602 fn zfgg_folds_to_top_via_operator_pipeline() {
9603 let mut e = editor_with("a\nb\nc\nd\ne");
9604 e.jump_cursor(3, 0);
9605 run_keys(&mut e, "zfgg");
9609 let f = e.buffer().folds()[0];
9610 assert_eq!(f.start_row, 0);
9611 assert_eq!(f.end_row, 3);
9612 }
9613
9614 #[test]
9615 fn zfip_folds_paragraph_via_text_object() {
9616 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta\nepsilon");
9617 e.jump_cursor(1, 0);
9618 run_keys(&mut e, "zfip");
9620 assert_eq!(e.buffer().folds().len(), 1);
9621 let f = e.buffer().folds()[0];
9622 assert_eq!(f.start_row, 0);
9623 assert_eq!(f.end_row, 2);
9624 }
9625
9626 #[test]
9627 fn zfap_folds_paragraph_with_trailing_blank() {
9628 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta");
9629 e.jump_cursor(0, 0);
9630 run_keys(&mut e, "zfap");
9632 let f = e.buffer().folds()[0];
9633 assert_eq!(f.start_row, 0);
9634 assert_eq!(f.end_row, 3);
9635 }
9636
9637 #[test]
9638 fn zf_paragraph_motion_folds_to_blank() {
9639 let mut e = editor_with("alpha\nbeta\n\ngamma");
9640 e.jump_cursor(0, 0);
9641 run_keys(&mut e, "zf}");
9643 let f = e.buffer().folds()[0];
9644 assert_eq!(f.start_row, 0);
9645 assert_eq!(f.end_row, 2);
9646 }
9647
9648 #[test]
9649 fn za_toggles_fold_under_cursor() {
9650 let mut e = editor_with("a\nb\nc\nd");
9651 e.buffer_mut().add_fold(1, 2, true);
9652 e.jump_cursor(1, 0);
9653 run_keys(&mut e, "za");
9654 assert!(!e.buffer().folds()[0].closed);
9655 run_keys(&mut e, "za");
9656 assert!(e.buffer().folds()[0].closed);
9657 }
9658
9659 #[test]
9660 fn zr_opens_all_folds_zm_closes_all() {
9661 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9662 e.buffer_mut().add_fold(0, 1, true);
9663 e.buffer_mut().add_fold(2, 3, true);
9664 e.buffer_mut().add_fold(4, 5, true);
9665 run_keys(&mut e, "zR");
9666 assert!(e.buffer().folds().iter().all(|f| !f.closed));
9667 run_keys(&mut e, "zM");
9668 assert!(e.buffer().folds().iter().all(|f| f.closed));
9669 }
9670
9671 #[test]
9672 fn ze_clears_all_folds() {
9673 let mut e = editor_with("a\nb\nc\nd");
9674 e.buffer_mut().add_fold(0, 1, true);
9675 e.buffer_mut().add_fold(2, 3, false);
9676 run_keys(&mut e, "zE");
9677 assert!(e.buffer().folds().is_empty());
9678 }
9679
9680 #[test]
9681 fn g_underscore_jumps_to_last_non_blank() {
9682 let mut e = editor_with("hello world ");
9683 run_keys(&mut e, "g_");
9684 assert_eq!(e.cursor().1, 10);
9686 }
9687
9688 #[test]
9689 fn gj_and_gk_alias_j_and_k() {
9690 let mut e = editor_with("a\nb\nc");
9691 run_keys(&mut e, "gj");
9692 assert_eq!(e.cursor().0, 1);
9693 run_keys(&mut e, "gk");
9694 assert_eq!(e.cursor().0, 0);
9695 }
9696
9697 #[test]
9698 fn paragraph_motions_walk_blank_lines() {
9699 let mut e = editor_with("first\nblock\n\nsecond\nblock\n\nthird");
9700 run_keys(&mut e, "}");
9701 assert_eq!(e.cursor().0, 2);
9702 run_keys(&mut e, "}");
9703 assert_eq!(e.cursor().0, 5);
9704 run_keys(&mut e, "{");
9705 assert_eq!(e.cursor().0, 2);
9706 }
9707
9708 #[test]
9709 fn gv_reenters_last_visual_selection() {
9710 let mut e = editor_with("alpha\nbeta\ngamma");
9711 run_keys(&mut e, "Vj");
9712 run_keys(&mut e, "<Esc>");
9714 assert_eq!(e.vim_mode(), VimMode::Normal);
9715 run_keys(&mut e, "gv");
9717 assert_eq!(e.vim_mode(), VimMode::VisualLine);
9718 }
9719
9720 #[test]
9721 fn o_in_visual_swaps_anchor_and_cursor() {
9722 let mut e = editor_with("hello world");
9723 run_keys(&mut e, "vllll");
9725 assert_eq!(e.cursor().1, 4);
9726 run_keys(&mut e, "o");
9728 assert_eq!(e.cursor().1, 0);
9729 assert_eq!(e.vim.visual_anchor, (0, 4));
9731 }
9732
9733 #[test]
9734 fn editing_inside_fold_invalidates_it() {
9735 let mut e = editor_with("a\nb\nc\nd");
9736 e.buffer_mut().add_fold(1, 2, true);
9737 e.jump_cursor(1, 0);
9738 run_keys(&mut e, "iX<Esc>");
9740 assert!(e.buffer().folds().is_empty());
9742 }
9743
9744 #[test]
9745 fn zd_removes_fold_under_cursor() {
9746 let mut e = editor_with("a\nb\nc\nd");
9747 e.buffer_mut().add_fold(1, 2, true);
9748 e.jump_cursor(2, 0);
9749 run_keys(&mut e, "zd");
9750 assert!(e.buffer().folds().is_empty());
9751 }
9752
9753 #[test]
9754 fn take_fold_ops_observes_z_keystroke_dispatch() {
9755 use crate::types::FoldOp;
9760 let mut e = editor_with("a\nb\nc\nd");
9761 e.buffer_mut().add_fold(1, 2, true);
9762 e.jump_cursor(1, 0);
9763 let _ = e.take_fold_ops();
9766 run_keys(&mut e, "zo");
9767 run_keys(&mut e, "zM");
9768 let ops = e.take_fold_ops();
9769 assert_eq!(ops.len(), 2);
9770 assert!(matches!(ops[0], FoldOp::OpenAt(1)));
9771 assert!(matches!(ops[1], FoldOp::CloseAll));
9772 assert!(e.take_fold_ops().is_empty());
9774 }
9775
9776 #[test]
9777 fn edit_pipeline_emits_invalidate_fold_op() {
9778 use crate::types::FoldOp;
9781 let mut e = editor_with("a\nb\nc\nd");
9782 e.buffer_mut().add_fold(1, 2, true);
9783 e.jump_cursor(1, 0);
9784 let _ = e.take_fold_ops();
9785 run_keys(&mut e, "iX<Esc>");
9786 let ops = e.take_fold_ops();
9787 assert!(
9788 ops.iter().any(|op| matches!(op, FoldOp::Invalidate { .. })),
9789 "expected at least one Invalidate op, got {ops:?}"
9790 );
9791 }
9792
9793 #[test]
9794 fn dot_mark_jumps_to_last_edit_position() {
9795 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
9796 e.jump_cursor(2, 0);
9797 run_keys(&mut e, "iX<Esc>");
9799 let after_edit = e.cursor();
9800 run_keys(&mut e, "gg");
9802 assert_eq!(e.cursor().0, 0);
9803 run_keys(&mut e, "'.");
9805 assert_eq!(e.cursor().0, after_edit.0);
9806 }
9807
9808 #[test]
9809 fn quote_quote_returns_to_pre_jump_position() {
9810 let mut e = editor_with_rows(50, 20);
9811 e.jump_cursor(10, 2);
9812 let before = e.cursor();
9813 run_keys(&mut e, "G");
9815 assert_ne!(e.cursor(), before);
9816 run_keys(&mut e, "''");
9818 assert_eq!(e.cursor().0, before.0);
9819 }
9820
9821 #[test]
9822 fn backtick_backtick_restores_exact_pre_jump_pos() {
9823 let mut e = editor_with_rows(50, 20);
9824 e.jump_cursor(7, 3);
9825 let before = e.cursor();
9826 run_keys(&mut e, "G");
9827 run_keys(&mut e, "``");
9828 assert_eq!(e.cursor(), before);
9829 }
9830
9831 #[test]
9832 fn macro_record_and_replay_basic() {
9833 let mut e = editor_with("foo\nbar\nbaz");
9834 run_keys(&mut e, "qaIX<Esc>jq");
9836 assert_eq!(e.buffer().lines()[0], "Xfoo");
9837 run_keys(&mut e, "@a");
9839 assert_eq!(e.buffer().lines()[1], "Xbar");
9840 run_keys(&mut e, "j@@");
9842 assert_eq!(e.buffer().lines()[2], "Xbaz");
9843 }
9844
9845 #[test]
9846 fn macro_count_replays_n_times() {
9847 let mut e = editor_with("a\nb\nc\nd\ne");
9848 run_keys(&mut e, "qajq");
9850 assert_eq!(e.cursor().0, 1);
9851 run_keys(&mut e, "3@a");
9853 assert_eq!(e.cursor().0, 4);
9854 }
9855
9856 #[test]
9857 fn macro_capital_q_appends_to_lowercase_register() {
9858 let mut e = editor_with("hello");
9859 run_keys(&mut e, "qall<Esc>q");
9860 run_keys(&mut e, "qAhh<Esc>q");
9861 let text = e.registers().read('a').unwrap().text.clone();
9864 assert!(text.contains("ll<Esc>"));
9865 assert!(text.contains("hh<Esc>"));
9866 }
9867
9868 #[test]
9869 fn buffer_selection_block_in_visual_block_mode() {
9870 use hjkl_buffer::{Position, Selection};
9871 let mut e = editor_with("aaaa\nbbbb\ncccc");
9872 run_keys(&mut e, "<C-v>jl");
9873 assert_eq!(
9874 e.buffer_selection(),
9875 Some(Selection::Block {
9876 anchor: Position::new(0, 0),
9877 head: Position::new(1, 1),
9878 })
9879 );
9880 }
9881
9882 #[test]
9885 fn n_after_question_mark_keeps_walking_backward() {
9886 let mut e = editor_with("foo bar foo baz foo end");
9889 e.jump_cursor(0, 22);
9890 run_keys(&mut e, "?foo<CR>");
9891 assert_eq!(e.cursor().1, 16);
9892 run_keys(&mut e, "n");
9893 assert_eq!(e.cursor().1, 8);
9894 run_keys(&mut e, "N");
9895 assert_eq!(e.cursor().1, 16);
9896 }
9897
9898 #[test]
9899 fn nested_macro_chord_records_literal_keys() {
9900 let mut e = editor_with("alpha\nbeta\ngamma");
9903 run_keys(&mut e, "qblq");
9905 run_keys(&mut e, "qaIX<Esc>q");
9908 e.jump_cursor(1, 0);
9910 run_keys(&mut e, "@a");
9911 assert_eq!(e.buffer().lines()[1], "Xbeta");
9912 }
9913
9914 #[test]
9915 fn shift_gt_motion_indents_one_line() {
9916 let mut e = editor_with("hello world");
9920 run_keys(&mut e, ">w");
9921 assert_eq!(e.buffer().lines()[0], " hello world");
9922 }
9923
9924 #[test]
9925 fn shift_lt_motion_outdents_one_line() {
9926 let mut e = editor_with(" hello world");
9927 run_keys(&mut e, "<lt>w");
9928 assert_eq!(e.buffer().lines()[0], " hello world");
9930 }
9931
9932 #[test]
9933 fn shift_gt_text_object_indents_paragraph() {
9934 let mut e = editor_with("alpha\nbeta\ngamma\n\nrest");
9935 e.jump_cursor(0, 0);
9936 run_keys(&mut e, ">ip");
9937 assert_eq!(e.buffer().lines()[0], " alpha");
9938 assert_eq!(e.buffer().lines()[1], " beta");
9939 assert_eq!(e.buffer().lines()[2], " gamma");
9940 assert_eq!(e.buffer().lines()[4], "rest");
9942 }
9943
9944 #[test]
9945 fn ctrl_o_runs_exactly_one_normal_command() {
9946 let mut e = editor_with("alpha beta gamma");
9949 e.jump_cursor(0, 0);
9950 run_keys(&mut e, "i");
9951 e.handle_key(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL));
9952 run_keys(&mut e, "dw");
9953 assert_eq!(e.vim_mode(), VimMode::Insert);
9955 run_keys(&mut e, "X");
9957 assert_eq!(e.buffer().lines()[0], "Xbeta gamma");
9958 }
9959
9960 #[test]
9961 fn macro_replay_respects_mode_switching() {
9962 let mut e = editor_with("hi");
9966 run_keys(&mut e, "qaiX<Esc>0q");
9967 assert_eq!(e.vim_mode(), VimMode::Normal);
9968 e.set_content("yo");
9970 run_keys(&mut e, "@a");
9971 assert_eq!(e.vim_mode(), VimMode::Normal);
9972 assert_eq!(e.cursor().1, 0);
9973 assert_eq!(e.buffer().lines()[0], "Xyo");
9974 }
9975
9976 #[test]
9977 fn macro_recorded_text_round_trips_through_register() {
9978 let mut e = editor_with("");
9982 run_keys(&mut e, "qaiX<Esc>q");
9983 let text = e.registers().read('a').unwrap().text.clone();
9984 assert!(text.starts_with("iX"));
9985 run_keys(&mut e, "@a");
9987 assert_eq!(e.buffer().lines()[0], "XX");
9988 }
9989
9990 #[test]
9991 fn dot_after_macro_replays_macros_last_change() {
9992 let mut e = editor_with("ab\ncd\nef");
9995 run_keys(&mut e, "qaIX<Esc>jq");
9998 assert_eq!(e.buffer().lines()[0], "Xab");
9999 run_keys(&mut e, "@a");
10000 assert_eq!(e.buffer().lines()[1], "Xcd");
10001 let row_before_dot = e.cursor().0;
10004 run_keys(&mut e, ".");
10005 assert!(e.buffer().lines()[row_before_dot].starts_with('X'));
10006 }
10007
10008 fn si_editor(content: &str) -> Editor {
10014 let opts = crate::types::Options {
10015 shiftwidth: 4,
10016 softtabstop: 4,
10017 expandtab: true,
10018 smartindent: true,
10019 autoindent: true,
10020 ..crate::types::Options::default()
10021 };
10022 let mut e = Editor::new(
10023 hjkl_buffer::Buffer::new(),
10024 crate::types::DefaultHost::new(),
10025 opts,
10026 );
10027 e.set_content(content);
10028 e
10029 }
10030
10031 #[test]
10032 fn smartindent_bumps_indent_after_open_brace() {
10033 let mut e = si_editor("fn foo() {");
10035 e.jump_cursor(0, 10); run_keys(&mut e, "i<CR>");
10037 assert_eq!(
10038 e.buffer().lines()[1],
10039 " ",
10040 "smartindent should bump one shiftwidth after {{"
10041 );
10042 }
10043
10044 #[test]
10045 fn smartindent_no_bump_when_off() {
10046 let mut e = si_editor("fn foo() {");
10049 e.settings_mut().smartindent = false;
10050 e.jump_cursor(0, 10);
10051 run_keys(&mut e, "i<CR>");
10052 assert_eq!(
10053 e.buffer().lines()[1],
10054 "",
10055 "without smartindent, no bump: new line copies empty leading ws"
10056 );
10057 }
10058
10059 #[test]
10060 fn smartindent_uses_tab_when_noexpandtab() {
10061 let opts = crate::types::Options {
10063 shiftwidth: 4,
10064 softtabstop: 0,
10065 expandtab: false,
10066 smartindent: true,
10067 autoindent: true,
10068 ..crate::types::Options::default()
10069 };
10070 let mut e = Editor::new(
10071 hjkl_buffer::Buffer::new(),
10072 crate::types::DefaultHost::new(),
10073 opts,
10074 );
10075 e.set_content("fn foo() {");
10076 e.jump_cursor(0, 10);
10077 run_keys(&mut e, "i<CR>");
10078 assert_eq!(
10079 e.buffer().lines()[1],
10080 "\t",
10081 "noexpandtab: smartindent bump inserts a literal tab"
10082 );
10083 }
10084
10085 #[test]
10086 fn smartindent_dedent_on_close_brace() {
10087 let mut e = si_editor("fn foo() {");
10090 e.set_content("fn foo() {\n ");
10092 e.jump_cursor(1, 4); run_keys(&mut e, "i}");
10094 assert_eq!(
10095 e.buffer().lines()[1],
10096 "}",
10097 "close brace on whitespace-only line should dedent"
10098 );
10099 assert_eq!(e.cursor(), (1, 1), "cursor should be after the `}}`");
10100 }
10101
10102 #[test]
10103 fn smartindent_no_dedent_when_off() {
10104 let mut e = si_editor("fn foo() {\n ");
10106 e.settings_mut().smartindent = false;
10107 e.jump_cursor(1, 4);
10108 run_keys(&mut e, "i}");
10109 assert_eq!(
10110 e.buffer().lines()[1],
10111 " }",
10112 "without smartindent, `}}` just appends at cursor"
10113 );
10114 }
10115
10116 #[test]
10117 fn smartindent_no_dedent_mid_line() {
10118 let mut e = si_editor(" let x = 1");
10121 e.jump_cursor(0, 13); run_keys(&mut e, "i}");
10123 assert_eq!(
10124 e.buffer().lines()[0],
10125 " let x = 1}",
10126 "mid-line `}}` should not dedent"
10127 );
10128 }
10129
10130 #[test]
10134 fn count_5x_fills_unnamed_register() {
10135 let mut e = editor_with("hello world\n");
10136 e.jump_cursor(0, 0);
10137 run_keys(&mut e, "5x");
10138 assert_eq!(e.buffer().lines()[0], " world");
10139 assert_eq!(e.cursor(), (0, 0));
10140 assert_eq!(e.yank(), "hello");
10141 }
10142
10143 #[test]
10144 fn x_fills_unnamed_register_single_char() {
10145 let mut e = editor_with("abc\n");
10146 e.jump_cursor(0, 0);
10147 run_keys(&mut e, "x");
10148 assert_eq!(e.buffer().lines()[0], "bc");
10149 assert_eq!(e.yank(), "a");
10150 }
10151
10152 #[test]
10153 fn big_x_fills_unnamed_register() {
10154 let mut e = editor_with("hello\n");
10155 e.jump_cursor(0, 3);
10156 run_keys(&mut e, "X");
10157 assert_eq!(e.buffer().lines()[0], "helo");
10158 assert_eq!(e.yank(), "l");
10159 }
10160
10161 #[test]
10163 fn g_motion_trailing_newline_lands_on_last_content_row() {
10164 let mut e = editor_with("foo\nbar\nbaz\n");
10165 e.jump_cursor(0, 0);
10166 run_keys(&mut e, "G");
10167 assert_eq!(
10169 e.cursor().0,
10170 2,
10171 "G should land on row 2 (baz), not row 3 (phantom empty)"
10172 );
10173 }
10174
10175 #[test]
10177 fn dd_last_line_clamps_cursor_to_new_last_row() {
10178 let mut e = editor_with("foo\nbar\n");
10179 e.jump_cursor(1, 0);
10180 run_keys(&mut e, "dd");
10181 assert_eq!(e.buffer().lines()[0], "foo");
10182 assert_eq!(
10183 e.cursor(),
10184 (0, 0),
10185 "cursor should clamp to row 0 after dd on last content line"
10186 );
10187 }
10188
10189 #[test]
10191 fn d_dollar_cursor_on_last_char() {
10192 let mut e = editor_with("hello world\n");
10193 e.jump_cursor(0, 5);
10194 run_keys(&mut e, "d$");
10195 assert_eq!(e.buffer().lines()[0], "hello");
10196 assert_eq!(
10197 e.cursor(),
10198 (0, 4),
10199 "d$ should leave cursor on col 4, not col 5"
10200 );
10201 }
10202
10203 #[test]
10205 fn undo_insert_clamps_cursor_to_last_valid_col() {
10206 let mut e = editor_with("hello\n");
10207 e.jump_cursor(0, 5); run_keys(&mut e, "a world<Esc>u");
10209 assert_eq!(e.buffer().lines()[0], "hello");
10210 assert_eq!(
10211 e.cursor(),
10212 (0, 4),
10213 "undo should clamp cursor to col 4 on 'hello'"
10214 );
10215 }
10216
10217 #[test]
10219 fn da_doublequote_eats_trailing_whitespace() {
10220 let mut e = editor_with("say \"hello\" there\n");
10221 e.jump_cursor(0, 6);
10222 run_keys(&mut e, "da\"");
10223 assert_eq!(e.buffer().lines()[0], "say there");
10224 assert_eq!(e.cursor().1, 4, "cursor should be at col 4 after da\"");
10225 }
10226
10227 #[test]
10229 fn dab_cursor_col_clamped_after_delete() {
10230 let mut e = editor_with("fn x() {\n body\n}\n");
10231 e.jump_cursor(1, 4);
10232 run_keys(&mut e, "daB");
10233 assert_eq!(e.buffer().lines()[0], "fn x() ");
10234 assert_eq!(
10235 e.cursor(),
10236 (0, 6),
10237 "daB should leave cursor at col 6, not 7"
10238 );
10239 }
10240
10241 #[test]
10243 fn dib_preserves_surrounding_newlines() {
10244 let mut e = editor_with("{\n body\n}\n");
10245 e.jump_cursor(1, 4);
10246 run_keys(&mut e, "diB");
10247 assert_eq!(e.buffer().lines()[0], "{");
10248 assert_eq!(e.buffer().lines()[1], "}");
10249 assert_eq!(e.cursor().0, 1, "cursor should be on the '}}' line");
10250 }
10251
10252 #[test]
10253 fn is_chord_pending_tracks_replace_state() {
10254 let mut e = editor_with("abc\n");
10255 assert!(!e.is_chord_pending());
10256 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE));
10258 assert!(e.is_chord_pending(), "engine should be pending after r");
10259 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
10261 assert!(
10262 !e.is_chord_pending(),
10263 "engine pending should clear after replace"
10264 );
10265 }
10266
10267 #[test]
10270 fn yiw_sets_lbr_rbr_marks_around_word() {
10271 let mut e = editor_with("hello world");
10274 run_keys(&mut e, "yiw");
10275 let lo = e.mark('[').expect("'[' must be set after yiw");
10276 let hi = e.mark(']').expect("']' must be set after yiw");
10277 assert_eq!(lo, (0, 0), "'[ should be first char of yanked word");
10278 assert_eq!(hi, (0, 4), "'] should be last char of yanked word");
10279 }
10280
10281 #[test]
10282 fn yj_linewise_sets_marks_at_line_edges() {
10283 let mut e = editor_with("aaaaa\nbbbbb\nccc");
10286 run_keys(&mut e, "yj");
10287 let lo = e.mark('[').expect("'[' must be set after yj");
10288 let hi = e.mark(']').expect("']' must be set after yj");
10289 assert_eq!(lo, (0, 0), "'[ snaps to (top_row, 0) for linewise yank");
10290 assert_eq!(
10291 hi,
10292 (1, 4),
10293 "'] snaps to (bot_row, last_col) for linewise yank"
10294 );
10295 }
10296
10297 #[test]
10298 fn dd_sets_lbr_rbr_marks_to_cursor() {
10299 let mut e = editor_with("aaa\nbbb");
10302 run_keys(&mut e, "dd");
10303 let lo = e.mark('[').expect("'[' must be set after dd");
10304 let hi = e.mark(']').expect("']' must be set after dd");
10305 assert_eq!(lo, hi, "after delete both marks are at the same position");
10306 assert_eq!(lo.0, 0, "post-delete cursor row should be 0");
10307 }
10308
10309 #[test]
10310 fn dw_sets_lbr_rbr_marks_to_cursor() {
10311 let mut e = editor_with("hello world");
10314 run_keys(&mut e, "dw");
10315 let lo = e.mark('[').expect("'[' must be set after dw");
10316 let hi = e.mark(']').expect("']' must be set after dw");
10317 assert_eq!(lo, hi, "after delete both marks are at the same position");
10318 assert_eq!(lo, (0, 0), "post-dw cursor is at col 0");
10319 }
10320
10321 #[test]
10322 fn cw_then_esc_sets_lbr_at_start_rbr_at_inserted_text_end() {
10323 let mut e = editor_with("hello world");
10328 run_keys(&mut e, "cwfoo<Esc>");
10329 let lo = e.mark('[').expect("'[' must be set after cw");
10330 let hi = e.mark(']').expect("']' must be set after cw");
10331 assert_eq!(lo, (0, 0), "'[ should be start of change");
10332 assert_eq!(hi.0, 0, "'] should be on row 0");
10335 assert!(hi.1 >= 2, "'] should be at or past last char of 'foo'");
10336 }
10337
10338 #[test]
10339 fn cw_with_no_insertion_sets_marks_at_change_start() {
10340 let mut e = editor_with("hello world");
10343 run_keys(&mut e, "cw<Esc>");
10344 let lo = e.mark('[').expect("'[' must be set after cw<Esc>");
10345 let hi = e.mark(']').expect("']' must be set after cw<Esc>");
10346 assert_eq!(lo.0, 0, "'[ should be on row 0");
10347 assert_eq!(hi.0, 0, "'] should be on row 0");
10348 assert_eq!(lo, hi, "marks coincide when insert is empty");
10350 }
10351
10352 #[test]
10353 fn p_charwise_sets_marks_around_pasted_text() {
10354 let mut e = editor_with("abc xyz");
10357 run_keys(&mut e, "yiw"); run_keys(&mut e, "p"); let lo = e.mark('[').expect("'[' set after charwise paste");
10360 let hi = e.mark(']').expect("']' set after charwise paste");
10361 assert!(lo <= hi, "'[ must not exceed ']'");
10362 assert_eq!(
10364 hi.1.wrapping_sub(lo.1),
10365 2,
10366 "'] - '[ should span 2 cols for a 3-char paste"
10367 );
10368 }
10369
10370 #[test]
10371 fn p_linewise_sets_marks_at_line_edges() {
10372 let mut e = editor_with("aaa\nbbb\nccc");
10375 run_keys(&mut e, "yj"); run_keys(&mut e, "j"); run_keys(&mut e, "p"); let lo = e.mark('[').expect("'[' set after linewise paste");
10379 let hi = e.mark(']').expect("']' set after linewise paste");
10380 assert_eq!(lo.1, 0, "'[ col must be 0 for linewise paste");
10381 assert!(hi.0 > lo.0, "'] row must be below '[ row for 2-line paste");
10382 assert_eq!(hi.0 - lo.0, 1, "exactly 1 row gap for a 2-line payload");
10383 }
10384
10385 #[test]
10386 fn backtick_lbr_v_backtick_rbr_reselects_yanked_text() {
10387 let mut e = editor_with("hello world");
10391 run_keys(&mut e, "yiw"); run_keys(&mut e, "`[v`]");
10395 assert_eq!(
10397 e.cursor(),
10398 (0, 4),
10399 "visual `[v`] should land on last yanked char"
10400 );
10401 assert_eq!(
10403 e.vim_mode(),
10404 crate::VimMode::Visual,
10405 "should be in Visual mode"
10406 );
10407 }
10408
10409 #[test]
10415 fn mark_dot_jump_to_last_edit_pre_edit_cursor() {
10416 let mut e = editor_with("hello\nworld\n");
10419 e.jump_cursor(0, 0);
10420 run_keys(&mut e, "iX<Esc>j`.");
10421 assert_eq!(
10422 e.cursor(),
10423 (0, 0),
10424 "dot mark should jump to the change-start (col 0), not post-insert col"
10425 );
10426 }
10427
10428 #[test]
10431 fn count_100g_clamps_to_last_content_row() {
10432 let mut e = editor_with("foo\nbar\nbaz\n");
10435 e.jump_cursor(0, 0);
10436 run_keys(&mut e, "100G");
10437 assert_eq!(
10438 e.cursor(),
10439 (2, 0),
10440 "100G on trailing-newline buffer must clamp to row 2 (last content row)"
10441 );
10442 }
10443
10444 #[test]
10447 fn gi_resumes_last_insert_position() {
10448 let mut e = editor_with("world\nhello\n");
10454 e.jump_cursor(0, 0);
10455 run_keys(&mut e, "iHi<Esc>jgi<Esc>");
10456 assert_eq!(
10457 e.vim_mode(),
10458 crate::VimMode::Normal,
10459 "should be in Normal mode after gi<Esc>"
10460 );
10461 assert_eq!(
10462 e.cursor(),
10463 (0, 1),
10464 "gi<Esc> cursor should be at (0,1) — the insert row, step-back col"
10465 );
10466 }
10467
10468 #[test]
10472 fn visual_block_change_cursor_on_last_inserted_char() {
10473 let mut e = editor_with("foo\nbar\nbaz\n");
10477 e.jump_cursor(0, 0);
10478 run_keys(&mut e, "<C-v>jlcZZ<Esc>");
10479 let lines = e.buffer().lines().to_vec();
10480 assert_eq!(lines[0], "ZZo", "row 0 should be 'ZZo'");
10481 assert_eq!(lines[1], "ZZr", "row 1 should be 'ZZr'");
10482 assert_eq!(
10483 e.cursor(),
10484 (0, 1),
10485 "cursor should be on last char of inserted 'ZZ' (col 1)"
10486 );
10487 }
10488
10489 #[test]
10494 fn register_blackhole_delete_preserves_unnamed_register() {
10495 let mut e = editor_with("foo bar baz\n");
10502 e.jump_cursor(0, 0);
10503 run_keys(&mut e, "yiww\"_dwbp");
10504 let lines = e.buffer().lines().to_vec();
10505 assert_eq!(
10506 lines[0], "ffoooo baz",
10507 "black-hole delete must not corrupt unnamed register"
10508 );
10509 assert_eq!(
10510 e.cursor(),
10511 (0, 3),
10512 "cursor should be on last pasted char (col 3)"
10513 );
10514 }
10515
10516 #[test]
10519 fn after_z_zz_sets_viewport_pinned() {
10520 let mut e = editor_with("a\nb\nc\nd\ne");
10521 e.jump_cursor(2, 0);
10522 e.after_z('z', 1);
10523 assert!(e.vim.viewport_pinned, "zz must set viewport_pinned");
10524 }
10525
10526 #[test]
10527 fn after_z_zo_opens_fold_at_cursor() {
10528 let mut e = editor_with("a\nb\nc\nd");
10529 e.buffer_mut().add_fold(1, 2, true);
10530 e.jump_cursor(1, 0);
10531 e.after_z('o', 1);
10532 assert!(
10533 !e.buffer().folds()[0].closed,
10534 "zo must open the fold at the cursor row"
10535 );
10536 }
10537
10538 #[test]
10539 fn after_z_zm_closes_all_folds() {
10540 let mut e = editor_with("a\nb\nc\nd\ne\nf");
10541 e.buffer_mut().add_fold(0, 1, false);
10542 e.buffer_mut().add_fold(4, 5, false);
10543 e.after_z('M', 1);
10544 assert!(
10545 e.buffer().folds().iter().all(|f| f.closed),
10546 "zM must close all folds"
10547 );
10548 }
10549
10550 #[test]
10551 fn after_z_zd_removes_fold_at_cursor() {
10552 let mut e = editor_with("a\nb\nc\nd");
10553 e.buffer_mut().add_fold(1, 2, true);
10554 e.jump_cursor(1, 0);
10555 e.after_z('d', 1);
10556 assert!(
10557 e.buffer().folds().is_empty(),
10558 "zd must remove the fold at the cursor row"
10559 );
10560 }
10561
10562 #[test]
10563 fn after_z_zf_in_visual_creates_fold() {
10564 let mut e = editor_with("a\nb\nc\nd\ne");
10565 e.jump_cursor(1, 0);
10567 run_keys(&mut e, "V2j");
10568 e.after_z('f', 1);
10570 let folds = e.buffer().folds();
10571 assert_eq!(folds.len(), 1, "zf in visual must create exactly one fold");
10572 assert_eq!(folds[0].start_row, 1);
10573 assert_eq!(folds[0].end_row, 3);
10574 assert!(folds[0].closed);
10575 }
10576
10577 #[test]
10580 fn apply_op_motion_dw_deletes_word() {
10581 let mut e = editor_with("hello world");
10583 e.apply_op_motion(crate::vim::Operator::Delete, 'w', 1);
10584 assert_eq!(
10585 e.buffer().lines().first().cloned().unwrap_or_default(),
10586 "world"
10587 );
10588 }
10589
10590 #[test]
10591 fn apply_op_motion_cw_quirk_leaves_trailing_space() {
10592 let mut e = editor_with("hello world");
10594 e.apply_op_motion(crate::vim::Operator::Change, 'w', 1);
10595 let line = e.buffer().lines().first().cloned().unwrap_or_default();
10598 assert!(
10599 line.starts_with(' ') || line == " world",
10600 "cw quirk: got {line:?}"
10601 );
10602 assert_eq!(e.vim_mode(), VimMode::Insert);
10603 }
10604
10605 #[test]
10606 fn apply_op_double_dd_deletes_line() {
10607 let mut e = editor_with("line1\nline2\nline3");
10608 e.apply_op_double(crate::vim::Operator::Delete, 1);
10610 let lines: Vec<_> = e.buffer().lines().to_vec();
10611 assert_eq!(lines, vec!["line2", "line3"], "dd should delete line1");
10612 }
10613
10614 #[test]
10615 fn apply_op_double_yy_does_not_modify_buffer() {
10616 let mut e = editor_with("hello");
10617 e.apply_op_double(crate::vim::Operator::Yank, 1);
10618 assert_eq!(
10619 e.buffer().lines().first().cloned().unwrap_or_default(),
10620 "hello"
10621 );
10622 }
10623
10624 #[test]
10625 fn enter_op_text_obj_sets_pending() {
10626 let mut e = editor_with("hello world");
10627 e.enter_op_text_obj(crate::vim::Operator::Delete, 1, true);
10628 assert!(e.is_chord_pending(), "OpTextObj should set chord pending");
10629 }
10630
10631 #[test]
10632 fn enter_op_g_sets_pending() {
10633 let mut e = editor_with("hello world");
10634 e.enter_op_g(crate::vim::Operator::Delete, 1);
10635 assert!(e.is_chord_pending(), "OpG should set chord pending");
10636 }
10637
10638 #[test]
10639 fn enter_op_find_sets_pending() {
10640 let mut e = editor_with("hello world");
10641 e.enter_op_find(crate::vim::Operator::Delete, 1, true, false);
10642 assert!(e.is_chord_pending(), "OpFind should set chord pending");
10643 }
10644
10645 #[test]
10646 fn apply_op_double_dd_count2_deletes_two_lines() {
10647 let mut e = editor_with("line1\nline2\nline3");
10648 e.apply_op_double(crate::vim::Operator::Delete, 2);
10649 let lines: Vec<_> = e.buffer().lines().to_vec();
10650 assert_eq!(lines, vec!["line3"], "2dd should delete two lines");
10651 }
10652
10653 #[test]
10654 fn apply_op_motion_unknown_key_is_noop() {
10655 let mut e = editor_with("hello");
10657 let before = e.cursor();
10658 e.apply_op_motion(crate::vim::Operator::Delete, 'X', 1); assert_eq!(e.cursor(), before);
10660 assert_eq!(
10661 e.buffer().lines().first().cloned().unwrap_or_default(),
10662 "hello"
10663 );
10664 }
10665
10666 #[test]
10669 fn apply_op_find_dfx_deletes_to_x() {
10670 let mut e = editor_with("hello x world");
10672 e.apply_op_find(crate::vim::Operator::Delete, 'x', true, false, 1);
10673 assert_eq!(
10674 e.buffer().lines().first().cloned().unwrap_or_default(),
10675 " world",
10676 "dfx must delete 'hello x'"
10677 );
10678 }
10679
10680 #[test]
10681 fn apply_op_find_dtx_deletes_up_to_x() {
10682 let mut e = editor_with("hello x world");
10684 e.apply_op_find(crate::vim::Operator::Delete, 'x', true, true, 1);
10685 assert_eq!(
10686 e.buffer().lines().first().cloned().unwrap_or_default(),
10687 "x world",
10688 "dtx must delete 'hello ' leaving 'x world'"
10689 );
10690 }
10691
10692 #[test]
10693 fn apply_op_find_records_last_find() {
10694 let mut e = editor_with("hello x world");
10696 e.apply_op_find(crate::vim::Operator::Delete, 'x', true, false, 1);
10697 let _ = e.cursor(); }
10704}