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
3497pub(crate) fn apply_op_text_obj_inner<H: crate::types::Host>(
3507 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3508 op: Operator,
3509 ch: char,
3510 inner: bool,
3511 _total_count: usize,
3512) -> bool {
3513 let obj = match ch {
3516 'w' => TextObject::Word { big: false },
3517 'W' => TextObject::Word { big: true },
3518 '"' | '\'' | '`' => TextObject::Quote(ch),
3519 '(' | ')' | 'b' => TextObject::Bracket('('),
3520 '[' | ']' => TextObject::Bracket('['),
3521 '{' | '}' | 'B' => TextObject::Bracket('{'),
3522 '<' | '>' => TextObject::Bracket('<'),
3523 'p' => TextObject::Paragraph,
3524 't' => TextObject::XmlTag,
3525 's' => TextObject::Sentence,
3526 _ => return false,
3527 };
3528 apply_op_with_text_object(ed, op, obj, inner);
3529 if !ed.vim.replaying && op_is_change(op) {
3530 ed.vim.last_change = Some(LastChange::OpTextObj {
3531 op,
3532 obj,
3533 inner,
3534 inserted: None,
3535 });
3536 }
3537 true
3538}
3539
3540fn handle_text_object<H: crate::types::Host>(
3541 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3542 input: Input,
3543 op: Operator,
3544 _count1: usize,
3545 inner: bool,
3546) -> bool {
3547 let Key::Char(ch) = input.key else {
3548 return true;
3549 };
3550 apply_op_text_obj_inner(ed, op, ch, inner, 1);
3553 true
3554}
3555
3556fn handle_visual_text_obj<H: crate::types::Host>(
3557 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3558 input: Input,
3559 inner: bool,
3560) -> bool {
3561 let Key::Char(ch) = input.key else {
3562 return true;
3563 };
3564 let obj = match ch {
3565 'w' => TextObject::Word { big: false },
3566 'W' => TextObject::Word { big: true },
3567 '"' | '\'' | '`' => TextObject::Quote(ch),
3568 '(' | ')' | 'b' => TextObject::Bracket('('),
3569 '[' | ']' => TextObject::Bracket('['),
3570 '{' | '}' | 'B' => TextObject::Bracket('{'),
3571 '<' | '>' => TextObject::Bracket('<'),
3572 'p' => TextObject::Paragraph,
3573 't' => TextObject::XmlTag,
3574 's' => TextObject::Sentence,
3575 _ => return true,
3576 };
3577 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3578 return true;
3579 };
3580 match kind {
3584 MotionKind::Linewise => {
3585 ed.vim.visual_line_anchor = start.0;
3586 ed.vim.mode = Mode::VisualLine;
3587 ed.jump_cursor(end.0, 0);
3588 }
3589 _ => {
3590 ed.vim.mode = Mode::Visual;
3591 ed.vim.visual_anchor = (start.0, start.1);
3592 let (er, ec) = retreat_one(ed, end);
3593 ed.jump_cursor(er, ec);
3594 }
3595 }
3596 true
3597}
3598
3599fn retreat_one<H: crate::types::Host>(
3601 ed: &Editor<hjkl_buffer::Buffer, H>,
3602 pos: (usize, usize),
3603) -> (usize, usize) {
3604 let (r, c) = pos;
3605 if c > 0 {
3606 (r, c - 1)
3607 } else if r > 0 {
3608 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
3609 (r - 1, prev_len)
3610 } else {
3611 (0, 0)
3612 }
3613}
3614
3615fn op_is_change(op: Operator) -> bool {
3616 matches!(op, Operator::Delete | Operator::Change)
3617}
3618
3619fn handle_normal_only<H: crate::types::Host>(
3622 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3623 input: &Input,
3624 count: usize,
3625) -> bool {
3626 if input.ctrl {
3627 return false;
3628 }
3629 match input.key {
3630 Key::Char('i') => {
3631 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3632 true
3633 }
3634 Key::Char('I') => {
3635 move_first_non_whitespace(ed);
3636 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
3637 true
3638 }
3639 Key::Char('a') => {
3640 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3641 ed.push_buffer_cursor_to_textarea();
3642 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
3643 true
3644 }
3645 Key::Char('A') => {
3646 crate::motions::move_line_end(&mut ed.buffer);
3647 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3648 ed.push_buffer_cursor_to_textarea();
3649 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
3650 true
3651 }
3652 Key::Char('R') => {
3653 begin_insert(ed, count.max(1), InsertReason::Replace);
3656 true
3657 }
3658 Key::Char('o') => {
3659 use hjkl_buffer::{Edit, Position};
3660 ed.push_undo();
3661 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
3664 ed.sync_buffer_content_from_textarea();
3665 let row = buf_cursor_pos(&ed.buffer).row;
3666 let line_chars = buf_line_chars(&ed.buffer, row);
3667 let prev_line = buf_line(&ed.buffer, row).unwrap_or_default();
3670 let indent = compute_enter_indent(&ed.settings, prev_line);
3671 ed.mutate_edit(Edit::InsertStr {
3672 at: Position::new(row, line_chars),
3673 text: format!("\n{indent}"),
3674 });
3675 ed.push_buffer_cursor_to_textarea();
3676 true
3677 }
3678 Key::Char('O') => {
3679 use hjkl_buffer::{Edit, Position};
3680 ed.push_undo();
3681 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
3682 ed.sync_buffer_content_from_textarea();
3683 let row = buf_cursor_pos(&ed.buffer).row;
3684 let indent = if row > 0 {
3688 let above = buf_line(&ed.buffer, row - 1).unwrap_or_default();
3689 compute_enter_indent(&ed.settings, above)
3690 } else {
3691 let cur = buf_line(&ed.buffer, row).unwrap_or_default();
3692 cur.chars()
3693 .take_while(|c| *c == ' ' || *c == '\t')
3694 .collect::<String>()
3695 };
3696 ed.mutate_edit(Edit::InsertStr {
3697 at: Position::new(row, 0),
3698 text: format!("{indent}\n"),
3699 });
3700 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3705 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
3706 let new_row = buf_cursor_pos(&ed.buffer).row;
3707 buf_set_cursor_rc(&mut ed.buffer, new_row, indent.chars().count());
3708 ed.push_buffer_cursor_to_textarea();
3709 true
3710 }
3711 Key::Char('x') => {
3712 do_char_delete(ed, true, count.max(1));
3713 if !ed.vim.replaying {
3714 ed.vim.last_change = Some(LastChange::CharDel {
3715 forward: true,
3716 count: count.max(1),
3717 });
3718 }
3719 true
3720 }
3721 Key::Char('X') => {
3722 do_char_delete(ed, false, count.max(1));
3723 if !ed.vim.replaying {
3724 ed.vim.last_change = Some(LastChange::CharDel {
3725 forward: false,
3726 count: count.max(1),
3727 });
3728 }
3729 true
3730 }
3731 Key::Char('~') => {
3732 for _ in 0..count.max(1) {
3733 ed.push_undo();
3734 toggle_case_at_cursor(ed);
3735 }
3736 if !ed.vim.replaying {
3737 ed.vim.last_change = Some(LastChange::ToggleCase {
3738 count: count.max(1),
3739 });
3740 }
3741 true
3742 }
3743 Key::Char('J') => {
3744 for _ in 0..count.max(1) {
3745 ed.push_undo();
3746 join_line(ed);
3747 }
3748 if !ed.vim.replaying {
3749 ed.vim.last_change = Some(LastChange::JoinLine {
3750 count: count.max(1),
3751 });
3752 }
3753 true
3754 }
3755 Key::Char('D') => {
3756 ed.push_undo();
3757 delete_to_eol(ed);
3758 crate::motions::move_left(&mut ed.buffer, 1);
3760 ed.push_buffer_cursor_to_textarea();
3761 if !ed.vim.replaying {
3762 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
3763 }
3764 true
3765 }
3766 Key::Char('Y') => {
3767 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
3769 true
3770 }
3771 Key::Char('C') => {
3772 ed.push_undo();
3773 delete_to_eol(ed);
3774 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
3775 true
3776 }
3777 Key::Char('s') => {
3778 use hjkl_buffer::{Edit, MotionKind, Position};
3779 ed.push_undo();
3780 ed.sync_buffer_content_from_textarea();
3781 for _ in 0..count.max(1) {
3782 let cursor = buf_cursor_pos(&ed.buffer);
3783 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
3784 if cursor.col >= line_chars {
3785 break;
3786 }
3787 ed.mutate_edit(Edit::DeleteRange {
3788 start: cursor,
3789 end: Position::new(cursor.row, cursor.col + 1),
3790 kind: MotionKind::Char,
3791 });
3792 }
3793 ed.push_buffer_cursor_to_textarea();
3794 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3795 if !ed.vim.replaying {
3797 ed.vim.last_change = Some(LastChange::OpMotion {
3798 op: Operator::Change,
3799 motion: Motion::Right,
3800 count: count.max(1),
3801 inserted: None,
3802 });
3803 }
3804 true
3805 }
3806 Key::Char('p') => {
3807 do_paste(ed, false, count.max(1));
3808 if !ed.vim.replaying {
3809 ed.vim.last_change = Some(LastChange::Paste {
3810 before: false,
3811 count: count.max(1),
3812 });
3813 }
3814 true
3815 }
3816 Key::Char('P') => {
3817 do_paste(ed, true, count.max(1));
3818 if !ed.vim.replaying {
3819 ed.vim.last_change = Some(LastChange::Paste {
3820 before: true,
3821 count: count.max(1),
3822 });
3823 }
3824 true
3825 }
3826 Key::Char('u') => {
3827 do_undo(ed);
3828 true
3829 }
3830 Key::Char('r') => {
3831 ed.vim.count = count;
3832 ed.vim.pending = Pending::Replace;
3833 true
3834 }
3835 Key::Char('/') => {
3836 enter_search(ed, true);
3837 true
3838 }
3839 Key::Char('?') => {
3840 enter_search(ed, false);
3841 true
3842 }
3843 Key::Char('.') => {
3844 replay_last_change(ed, count);
3845 true
3846 }
3847 _ => false,
3848 }
3849}
3850
3851fn begin_insert_noundo<H: crate::types::Host>(
3853 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3854 count: usize,
3855 reason: InsertReason,
3856) {
3857 let reason = if ed.vim.replaying {
3858 InsertReason::ReplayOnly
3859 } else {
3860 reason
3861 };
3862 let (row, _) = ed.cursor();
3863 ed.vim.insert_session = Some(InsertSession {
3864 count,
3865 row_min: row,
3866 row_max: row,
3867 before_lines: buf_lines_to_vec(&ed.buffer),
3868 reason,
3869 });
3870 ed.vim.mode = Mode::Insert;
3871}
3872
3873fn apply_op_with_motion<H: crate::types::Host>(
3876 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3877 op: Operator,
3878 motion: &Motion,
3879 count: usize,
3880) {
3881 let start = ed.cursor();
3882 apply_motion_cursor_ctx(ed, motion, count, true);
3887 let end = ed.cursor();
3888 let kind = motion_kind(motion);
3889 ed.jump_cursor(start.0, start.1);
3891 run_operator_over_range(ed, op, start, end, kind);
3892}
3893
3894fn apply_op_with_text_object<H: crate::types::Host>(
3895 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3896 op: Operator,
3897 obj: TextObject,
3898 inner: bool,
3899) {
3900 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3901 return;
3902 };
3903 ed.jump_cursor(start.0, start.1);
3904 run_operator_over_range(ed, op, start, end, kind);
3905}
3906
3907fn motion_kind(motion: &Motion) -> MotionKind {
3908 match motion {
3909 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => MotionKind::Linewise,
3910 Motion::FileTop | Motion::FileBottom => MotionKind::Linewise,
3911 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
3912 MotionKind::Linewise
3913 }
3914 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
3915 MotionKind::Inclusive
3916 }
3917 Motion::Find { .. } => MotionKind::Inclusive,
3918 Motion::MatchBracket => MotionKind::Inclusive,
3919 Motion::LineEnd => MotionKind::Inclusive,
3921 _ => MotionKind::Exclusive,
3922 }
3923}
3924
3925fn run_operator_over_range<H: crate::types::Host>(
3926 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3927 op: Operator,
3928 start: (usize, usize),
3929 end: (usize, usize),
3930 kind: MotionKind,
3931) {
3932 let (top, bot) = order(start, end);
3933 if top == bot {
3934 return;
3935 }
3936
3937 match op {
3938 Operator::Yank => {
3939 let text = read_vim_range(ed, top, bot, kind);
3940 if !text.is_empty() {
3941 ed.record_yank_to_host(text.clone());
3942 ed.record_yank(text, matches!(kind, MotionKind::Linewise));
3943 }
3944 let rbr = match kind {
3948 MotionKind::Linewise => {
3949 let last_col = buf_line_chars(&ed.buffer, bot.0).saturating_sub(1);
3950 (bot.0, last_col)
3951 }
3952 MotionKind::Inclusive => (bot.0, bot.1),
3953 MotionKind::Exclusive => (bot.0, bot.1.saturating_sub(1)),
3954 };
3955 ed.set_mark('[', top);
3956 ed.set_mark(']', rbr);
3957 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3958 ed.push_buffer_cursor_to_textarea();
3959 }
3960 Operator::Delete => {
3961 ed.push_undo();
3962 cut_vim_range(ed, top, bot, kind);
3963 if !matches!(kind, MotionKind::Linewise) {
3968 clamp_cursor_to_normal_mode(ed);
3969 }
3970 ed.vim.mode = Mode::Normal;
3971 let pos = ed.cursor();
3975 ed.set_mark('[', pos);
3976 ed.set_mark(']', pos);
3977 }
3978 Operator::Change => {
3979 ed.vim.change_mark_start = Some(top);
3984 ed.push_undo();
3985 cut_vim_range(ed, top, bot, kind);
3986 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3987 }
3988 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3989 apply_case_op_to_selection(ed, op, top, bot, kind);
3990 }
3991 Operator::Indent | Operator::Outdent => {
3992 ed.push_undo();
3995 if op == Operator::Indent {
3996 indent_rows(ed, top.0, bot.0, 1);
3997 } else {
3998 outdent_rows(ed, top.0, bot.0, 1);
3999 }
4000 ed.vim.mode = Mode::Normal;
4001 }
4002 Operator::Fold => {
4003 if bot.0 >= top.0 {
4007 ed.apply_fold_op(crate::types::FoldOp::Add {
4008 start_row: top.0,
4009 end_row: bot.0,
4010 closed: true,
4011 });
4012 }
4013 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4014 ed.push_buffer_cursor_to_textarea();
4015 ed.vim.mode = Mode::Normal;
4016 }
4017 Operator::Reflow => {
4018 ed.push_undo();
4019 reflow_rows(ed, top.0, bot.0);
4020 ed.vim.mode = Mode::Normal;
4021 }
4022 }
4023}
4024
4025fn reflow_rows<H: crate::types::Host>(
4030 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4031 top: usize,
4032 bot: usize,
4033) {
4034 let width = ed.settings().textwidth.max(1);
4035 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4036 let bot = bot.min(lines.len().saturating_sub(1));
4037 if top > bot {
4038 return;
4039 }
4040 let original = lines[top..=bot].to_vec();
4041 let mut wrapped: Vec<String> = Vec::new();
4042 let mut paragraph: Vec<String> = Vec::new();
4043 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
4044 if para.is_empty() {
4045 return;
4046 }
4047 let words = para.join(" ");
4048 let mut current = String::new();
4049 for word in words.split_whitespace() {
4050 let extra = if current.is_empty() {
4051 word.chars().count()
4052 } else {
4053 current.chars().count() + 1 + word.chars().count()
4054 };
4055 if extra > width && !current.is_empty() {
4056 out.push(std::mem::take(&mut current));
4057 current.push_str(word);
4058 } else if current.is_empty() {
4059 current.push_str(word);
4060 } else {
4061 current.push(' ');
4062 current.push_str(word);
4063 }
4064 }
4065 if !current.is_empty() {
4066 out.push(current);
4067 }
4068 para.clear();
4069 };
4070 for line in &original {
4071 if line.trim().is_empty() {
4072 flush(&mut paragraph, &mut wrapped, width);
4073 wrapped.push(String::new());
4074 } else {
4075 paragraph.push(line.clone());
4076 }
4077 }
4078 flush(&mut paragraph, &mut wrapped, width);
4079
4080 let after: Vec<String> = lines.split_off(bot + 1);
4082 lines.truncate(top);
4083 lines.extend(wrapped);
4084 lines.extend(after);
4085 ed.restore(lines, (top, 0));
4086 ed.mark_content_dirty();
4087}
4088
4089fn apply_case_op_to_selection<H: crate::types::Host>(
4095 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4096 op: Operator,
4097 top: (usize, usize),
4098 bot: (usize, usize),
4099 kind: MotionKind,
4100) {
4101 use hjkl_buffer::Edit;
4102 ed.push_undo();
4103 let saved_yank = ed.yank().to_string();
4104 let saved_yank_linewise = ed.vim.yank_linewise;
4105 let selection = cut_vim_range(ed, top, bot, kind);
4106 let transformed = match op {
4107 Operator::Uppercase => selection.to_uppercase(),
4108 Operator::Lowercase => selection.to_lowercase(),
4109 Operator::ToggleCase => toggle_case_str(&selection),
4110 _ => unreachable!(),
4111 };
4112 if !transformed.is_empty() {
4113 let cursor = buf_cursor_pos(&ed.buffer);
4114 ed.mutate_edit(Edit::InsertStr {
4115 at: cursor,
4116 text: transformed,
4117 });
4118 }
4119 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4120 ed.push_buffer_cursor_to_textarea();
4121 ed.set_yank(saved_yank);
4122 ed.vim.yank_linewise = saved_yank_linewise;
4123 ed.vim.mode = Mode::Normal;
4124}
4125
4126fn indent_rows<H: crate::types::Host>(
4131 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4132 top: usize,
4133 bot: usize,
4134 count: usize,
4135) {
4136 ed.sync_buffer_content_from_textarea();
4137 let width = ed.settings().shiftwidth * count.max(1);
4138 let pad: String = " ".repeat(width);
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 if !line.is_empty() {
4143 line.insert_str(0, &pad);
4144 }
4145 }
4146 ed.restore(lines, (top, 0));
4149 move_first_non_whitespace(ed);
4150}
4151
4152fn outdent_rows<H: crate::types::Host>(
4156 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4157 top: usize,
4158 bot: usize,
4159 count: usize,
4160) {
4161 ed.sync_buffer_content_from_textarea();
4162 let width = ed.settings().shiftwidth * count.max(1);
4163 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4164 let bot = bot.min(lines.len().saturating_sub(1));
4165 for line in lines.iter_mut().take(bot + 1).skip(top) {
4166 let strip: usize = line
4167 .chars()
4168 .take(width)
4169 .take_while(|c| *c == ' ' || *c == '\t')
4170 .count();
4171 if strip > 0 {
4172 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
4173 line.drain(..byte_len);
4174 }
4175 }
4176 ed.restore(lines, (top, 0));
4177 move_first_non_whitespace(ed);
4178}
4179
4180fn toggle_case_str(s: &str) -> String {
4181 s.chars()
4182 .map(|c| {
4183 if c.is_lowercase() {
4184 c.to_uppercase().next().unwrap_or(c)
4185 } else if c.is_uppercase() {
4186 c.to_lowercase().next().unwrap_or(c)
4187 } else {
4188 c
4189 }
4190 })
4191 .collect()
4192}
4193
4194fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
4195 if a <= b { (a, b) } else { (b, a) }
4196}
4197
4198fn clamp_cursor_to_normal_mode<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
4203 let (row, col) = ed.cursor();
4204 let line_chars = buf_line_chars(&ed.buffer, row);
4205 let max_col = line_chars.saturating_sub(1);
4206 if col > max_col {
4207 buf_set_cursor_rc(&mut ed.buffer, row, max_col);
4208 ed.push_buffer_cursor_to_textarea();
4209 }
4210}
4211
4212fn execute_line_op<H: crate::types::Host>(
4215 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4216 op: Operator,
4217 count: usize,
4218) {
4219 let (row, col) = ed.cursor();
4220 let total = buf_row_count(&ed.buffer);
4221 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
4222
4223 match op {
4224 Operator::Yank => {
4225 let text = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4227 if !text.is_empty() {
4228 ed.record_yank_to_host(text.clone());
4229 ed.record_yank(text, true);
4230 }
4231 let last_col = buf_line_chars(&ed.buffer, end_row).saturating_sub(1);
4234 ed.set_mark('[', (row, 0));
4235 ed.set_mark(']', (end_row, last_col));
4236 buf_set_cursor_rc(&mut ed.buffer, row, col);
4237 ed.push_buffer_cursor_to_textarea();
4238 ed.vim.mode = Mode::Normal;
4239 }
4240 Operator::Delete => {
4241 ed.push_undo();
4242 let deleted_through_last = end_row + 1 >= total;
4243 cut_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4244 let total_after = buf_row_count(&ed.buffer);
4248 let raw_target = if deleted_through_last {
4249 row.saturating_sub(1).min(total_after.saturating_sub(1))
4250 } else {
4251 row.min(total_after.saturating_sub(1))
4252 };
4253 let target_row = if raw_target > 0
4259 && raw_target + 1 == total_after
4260 && buf_line(&ed.buffer, raw_target)
4261 .map(str::is_empty)
4262 .unwrap_or(false)
4263 {
4264 raw_target - 1
4265 } else {
4266 raw_target
4267 };
4268 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
4269 ed.push_buffer_cursor_to_textarea();
4270 move_first_non_whitespace(ed);
4271 ed.sticky_col = Some(ed.cursor().1);
4272 ed.vim.mode = Mode::Normal;
4273 let pos = ed.cursor();
4276 ed.set_mark('[', pos);
4277 ed.set_mark(']', pos);
4278 }
4279 Operator::Change => {
4280 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4284 ed.vim.change_mark_start = Some((row, 0));
4286 ed.push_undo();
4287 ed.sync_buffer_content_from_textarea();
4288 let payload = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4290 if end_row > row {
4291 ed.mutate_edit(Edit::DeleteRange {
4292 start: Position::new(row + 1, 0),
4293 end: Position::new(end_row, 0),
4294 kind: BufKind::Line,
4295 });
4296 }
4297 let line_chars = buf_line_chars(&ed.buffer, row);
4298 if line_chars > 0 {
4299 ed.mutate_edit(Edit::DeleteRange {
4300 start: Position::new(row, 0),
4301 end: Position::new(row, line_chars),
4302 kind: BufKind::Char,
4303 });
4304 }
4305 if !payload.is_empty() {
4306 ed.record_yank_to_host(payload.clone());
4307 ed.record_delete(payload, true);
4308 }
4309 buf_set_cursor_rc(&mut ed.buffer, row, 0);
4310 ed.push_buffer_cursor_to_textarea();
4311 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4312 }
4313 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4314 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), MotionKind::Linewise);
4318 move_first_non_whitespace(ed);
4321 }
4322 Operator::Indent | Operator::Outdent => {
4323 ed.push_undo();
4325 if op == Operator::Indent {
4326 indent_rows(ed, row, end_row, 1);
4327 } else {
4328 outdent_rows(ed, row, end_row, 1);
4329 }
4330 ed.sticky_col = Some(ed.cursor().1);
4331 ed.vim.mode = Mode::Normal;
4332 }
4333 Operator::Fold => unreachable!("Fold has no line-op double"),
4335 Operator::Reflow => {
4336 ed.push_undo();
4338 reflow_rows(ed, row, end_row);
4339 move_first_non_whitespace(ed);
4340 ed.sticky_col = Some(ed.cursor().1);
4341 ed.vim.mode = Mode::Normal;
4342 }
4343 }
4344}
4345
4346fn apply_visual_operator<H: crate::types::Host>(
4349 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4350 op: Operator,
4351) {
4352 match ed.vim.mode {
4353 Mode::VisualLine => {
4354 let cursor_row = buf_cursor_pos(&ed.buffer).row;
4355 let top = cursor_row.min(ed.vim.visual_line_anchor);
4356 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4357 ed.vim.yank_linewise = true;
4358 match op {
4359 Operator::Yank => {
4360 let text = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4361 if !text.is_empty() {
4362 ed.record_yank_to_host(text.clone());
4363 ed.record_yank(text, true);
4364 }
4365 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4366 ed.push_buffer_cursor_to_textarea();
4367 ed.vim.mode = Mode::Normal;
4368 }
4369 Operator::Delete => {
4370 ed.push_undo();
4371 cut_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4372 ed.vim.mode = Mode::Normal;
4373 }
4374 Operator::Change => {
4375 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4378 ed.push_undo();
4379 ed.sync_buffer_content_from_textarea();
4380 let payload = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4381 if bot > top {
4382 ed.mutate_edit(Edit::DeleteRange {
4383 start: Position::new(top + 1, 0),
4384 end: Position::new(bot, 0),
4385 kind: BufKind::Line,
4386 });
4387 }
4388 let line_chars = buf_line_chars(&ed.buffer, top);
4389 if line_chars > 0 {
4390 ed.mutate_edit(Edit::DeleteRange {
4391 start: Position::new(top, 0),
4392 end: Position::new(top, line_chars),
4393 kind: BufKind::Char,
4394 });
4395 }
4396 if !payload.is_empty() {
4397 ed.record_yank_to_host(payload.clone());
4398 ed.record_delete(payload, true);
4399 }
4400 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4401 ed.push_buffer_cursor_to_textarea();
4402 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4403 }
4404 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4405 let bot = buf_cursor_pos(&ed.buffer)
4406 .row
4407 .max(ed.vim.visual_line_anchor);
4408 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), MotionKind::Linewise);
4409 move_first_non_whitespace(ed);
4410 }
4411 Operator::Indent | Operator::Outdent => {
4412 ed.push_undo();
4413 let (cursor_row, _) = ed.cursor();
4414 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4415 if op == Operator::Indent {
4416 indent_rows(ed, top, bot, 1);
4417 } else {
4418 outdent_rows(ed, top, bot, 1);
4419 }
4420 ed.vim.mode = Mode::Normal;
4421 }
4422 Operator::Reflow => {
4423 ed.push_undo();
4424 let (cursor_row, _) = ed.cursor();
4425 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4426 reflow_rows(ed, top, bot);
4427 ed.vim.mode = Mode::Normal;
4428 }
4429 Operator::Fold => unreachable!("Visual zf takes its own path"),
4432 }
4433 }
4434 Mode::Visual => {
4435 ed.vim.yank_linewise = false;
4436 let anchor = ed.vim.visual_anchor;
4437 let cursor = ed.cursor();
4438 let (top, bot) = order(anchor, cursor);
4439 match op {
4440 Operator::Yank => {
4441 let text = read_vim_range(ed, top, bot, MotionKind::Inclusive);
4442 if !text.is_empty() {
4443 ed.record_yank_to_host(text.clone());
4444 ed.record_yank(text, false);
4445 }
4446 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4447 ed.push_buffer_cursor_to_textarea();
4448 ed.vim.mode = Mode::Normal;
4449 }
4450 Operator::Delete => {
4451 ed.push_undo();
4452 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4453 ed.vim.mode = Mode::Normal;
4454 }
4455 Operator::Change => {
4456 ed.push_undo();
4457 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4458 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4459 }
4460 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4461 let anchor = ed.vim.visual_anchor;
4463 let cursor = ed.cursor();
4464 let (top, bot) = order(anchor, cursor);
4465 apply_case_op_to_selection(ed, op, top, bot, MotionKind::Inclusive);
4466 }
4467 Operator::Indent | Operator::Outdent => {
4468 ed.push_undo();
4469 let anchor = ed.vim.visual_anchor;
4470 let cursor = ed.cursor();
4471 let (top, bot) = order(anchor, cursor);
4472 if op == Operator::Indent {
4473 indent_rows(ed, top.0, bot.0, 1);
4474 } else {
4475 outdent_rows(ed, top.0, bot.0, 1);
4476 }
4477 ed.vim.mode = Mode::Normal;
4478 }
4479 Operator::Reflow => {
4480 ed.push_undo();
4481 let anchor = ed.vim.visual_anchor;
4482 let cursor = ed.cursor();
4483 let (top, bot) = order(anchor, cursor);
4484 reflow_rows(ed, top.0, bot.0);
4485 ed.vim.mode = Mode::Normal;
4486 }
4487 Operator::Fold => unreachable!("Visual zf takes its own path"),
4488 }
4489 }
4490 Mode::VisualBlock => apply_block_operator(ed, op),
4491 _ => {}
4492 }
4493}
4494
4495fn block_bounds<H: crate::types::Host>(
4500 ed: &Editor<hjkl_buffer::Buffer, H>,
4501) -> (usize, usize, usize, usize) {
4502 let (ar, ac) = ed.vim.block_anchor;
4503 let (cr, _) = ed.cursor();
4504 let cc = ed.vim.block_vcol;
4505 let top = ar.min(cr);
4506 let bot = ar.max(cr);
4507 let left = ac.min(cc);
4508 let right = ac.max(cc);
4509 (top, bot, left, right)
4510}
4511
4512fn update_block_vcol<H: crate::types::Host>(
4517 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4518 motion: &Motion,
4519) {
4520 match motion {
4521 Motion::Left
4522 | Motion::Right
4523 | Motion::WordFwd
4524 | Motion::BigWordFwd
4525 | Motion::WordBack
4526 | Motion::BigWordBack
4527 | Motion::WordEnd
4528 | Motion::BigWordEnd
4529 | Motion::WordEndBack
4530 | Motion::BigWordEndBack
4531 | Motion::LineStart
4532 | Motion::FirstNonBlank
4533 | Motion::LineEnd
4534 | Motion::Find { .. }
4535 | Motion::FindRepeat { .. }
4536 | Motion::MatchBracket => {
4537 ed.vim.block_vcol = ed.cursor().1;
4538 }
4539 _ => {}
4541 }
4542}
4543
4544fn apply_block_operator<H: crate::types::Host>(
4549 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4550 op: Operator,
4551) {
4552 let (top, bot, left, right) = block_bounds(ed);
4553 let yank = block_yank(ed, top, bot, left, right);
4555
4556 match op {
4557 Operator::Yank => {
4558 if !yank.is_empty() {
4559 ed.record_yank_to_host(yank.clone());
4560 ed.record_yank(yank, false);
4561 }
4562 ed.vim.mode = Mode::Normal;
4563 ed.jump_cursor(top, left);
4564 }
4565 Operator::Delete => {
4566 ed.push_undo();
4567 delete_block_contents(ed, top, bot, left, right);
4568 if !yank.is_empty() {
4569 ed.record_yank_to_host(yank.clone());
4570 ed.record_delete(yank, false);
4571 }
4572 ed.vim.mode = Mode::Normal;
4573 ed.jump_cursor(top, left);
4574 }
4575 Operator::Change => {
4576 ed.push_undo();
4577 delete_block_contents(ed, top, bot, left, right);
4578 if !yank.is_empty() {
4579 ed.record_yank_to_host(yank.clone());
4580 ed.record_delete(yank, false);
4581 }
4582 ed.jump_cursor(top, left);
4583 begin_insert_noundo(
4584 ed,
4585 1,
4586 InsertReason::BlockChange {
4587 top,
4588 bot,
4589 col: left,
4590 },
4591 );
4592 }
4593 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4594 ed.push_undo();
4595 transform_block_case(ed, op, top, bot, left, right);
4596 ed.vim.mode = Mode::Normal;
4597 ed.jump_cursor(top, left);
4598 }
4599 Operator::Indent | Operator::Outdent => {
4600 ed.push_undo();
4604 if op == Operator::Indent {
4605 indent_rows(ed, top, bot, 1);
4606 } else {
4607 outdent_rows(ed, top, bot, 1);
4608 }
4609 ed.vim.mode = Mode::Normal;
4610 }
4611 Operator::Fold => unreachable!("Visual zf takes its own path"),
4612 Operator::Reflow => {
4613 ed.push_undo();
4617 reflow_rows(ed, top, bot);
4618 ed.vim.mode = Mode::Normal;
4619 }
4620 }
4621}
4622
4623fn transform_block_case<H: crate::types::Host>(
4627 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4628 op: Operator,
4629 top: usize,
4630 bot: usize,
4631 left: usize,
4632 right: usize,
4633) {
4634 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4635 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4636 let chars: Vec<char> = lines[r].chars().collect();
4637 if left >= chars.len() {
4638 continue;
4639 }
4640 let end = (right + 1).min(chars.len());
4641 let head: String = chars[..left].iter().collect();
4642 let mid: String = chars[left..end].iter().collect();
4643 let tail: String = chars[end..].iter().collect();
4644 let transformed = match op {
4645 Operator::Uppercase => mid.to_uppercase(),
4646 Operator::Lowercase => mid.to_lowercase(),
4647 Operator::ToggleCase => toggle_case_str(&mid),
4648 _ => mid,
4649 };
4650 lines[r] = format!("{head}{transformed}{tail}");
4651 }
4652 let saved_yank = ed.yank().to_string();
4653 let saved_linewise = ed.vim.yank_linewise;
4654 ed.restore(lines, (top, left));
4655 ed.set_yank(saved_yank);
4656 ed.vim.yank_linewise = saved_linewise;
4657}
4658
4659fn block_yank<H: crate::types::Host>(
4660 ed: &Editor<hjkl_buffer::Buffer, H>,
4661 top: usize,
4662 bot: usize,
4663 left: usize,
4664 right: usize,
4665) -> String {
4666 let lines = buf_lines_to_vec(&ed.buffer);
4667 let mut rows: Vec<String> = Vec::new();
4668 for r in top..=bot {
4669 let line = match lines.get(r) {
4670 Some(l) => l,
4671 None => break,
4672 };
4673 let chars: Vec<char> = line.chars().collect();
4674 let end = (right + 1).min(chars.len());
4675 if left >= chars.len() {
4676 rows.push(String::new());
4677 } else {
4678 rows.push(chars[left..end].iter().collect());
4679 }
4680 }
4681 rows.join("\n")
4682}
4683
4684fn delete_block_contents<H: crate::types::Host>(
4685 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4686 top: usize,
4687 bot: usize,
4688 left: usize,
4689 right: usize,
4690) {
4691 use hjkl_buffer::{Edit, MotionKind, Position};
4692 ed.sync_buffer_content_from_textarea();
4693 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
4694 if last_row < top {
4695 return;
4696 }
4697 ed.mutate_edit(Edit::DeleteRange {
4698 start: Position::new(top, left),
4699 end: Position::new(last_row, right),
4700 kind: MotionKind::Block,
4701 });
4702 ed.push_buffer_cursor_to_textarea();
4703}
4704
4705fn block_replace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, ch: char) {
4707 let (top, bot, left, right) = block_bounds(ed);
4708 ed.push_undo();
4709 ed.sync_buffer_content_from_textarea();
4710 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4711 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4712 let chars: Vec<char> = lines[r].chars().collect();
4713 if left >= chars.len() {
4714 continue;
4715 }
4716 let end = (right + 1).min(chars.len());
4717 let before: String = chars[..left].iter().collect();
4718 let middle: String = std::iter::repeat_n(ch, end - left).collect();
4719 let after: String = chars[end..].iter().collect();
4720 lines[r] = format!("{before}{middle}{after}");
4721 }
4722 reset_textarea_lines(ed, lines);
4723 ed.vim.mode = Mode::Normal;
4724 ed.jump_cursor(top, left);
4725}
4726
4727fn reset_textarea_lines<H: crate::types::Host>(
4731 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4732 lines: Vec<String>,
4733) {
4734 let cursor = ed.cursor();
4735 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
4736 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
4737 ed.mark_content_dirty();
4738}
4739
4740type Pos = (usize, usize);
4746
4747fn text_object_range<H: crate::types::Host>(
4751 ed: &Editor<hjkl_buffer::Buffer, H>,
4752 obj: TextObject,
4753 inner: bool,
4754) -> Option<(Pos, Pos, MotionKind)> {
4755 match obj {
4756 TextObject::Word { big } => {
4757 word_text_object(ed, inner, big).map(|(s, e)| (s, e, MotionKind::Exclusive))
4758 }
4759 TextObject::Quote(q) => {
4760 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4761 }
4762 TextObject::Bracket(open) => bracket_text_object(ed, open, inner),
4763 TextObject::Paragraph => {
4764 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Linewise))
4765 }
4766 TextObject::XmlTag => {
4767 tag_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4768 }
4769 TextObject::Sentence => {
4770 sentence_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4771 }
4772 }
4773}
4774
4775fn sentence_boundary<H: crate::types::Host>(
4779 ed: &Editor<hjkl_buffer::Buffer, H>,
4780 forward: bool,
4781) -> Option<(usize, usize)> {
4782 let lines = buf_lines_to_vec(&ed.buffer);
4783 if lines.is_empty() {
4784 return None;
4785 }
4786 let pos_to_idx = |pos: (usize, usize)| -> usize {
4787 let mut idx = 0;
4788 for line in lines.iter().take(pos.0) {
4789 idx += line.chars().count() + 1;
4790 }
4791 idx + pos.1
4792 };
4793 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4794 for (r, line) in lines.iter().enumerate() {
4795 let len = line.chars().count();
4796 if idx <= len {
4797 return (r, idx);
4798 }
4799 idx -= len + 1;
4800 }
4801 let last = lines.len().saturating_sub(1);
4802 (last, lines[last].chars().count())
4803 };
4804 let mut chars: Vec<char> = Vec::new();
4805 for (r, line) in lines.iter().enumerate() {
4806 chars.extend(line.chars());
4807 if r + 1 < lines.len() {
4808 chars.push('\n');
4809 }
4810 }
4811 if chars.is_empty() {
4812 return None;
4813 }
4814 let total = chars.len();
4815 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
4816 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4817
4818 if forward {
4819 let mut i = cursor_idx + 1;
4822 while i < total {
4823 if is_terminator(chars[i]) {
4824 while i + 1 < total && is_terminator(chars[i + 1]) {
4825 i += 1;
4826 }
4827 if i + 1 >= total {
4828 return None;
4829 }
4830 if chars[i + 1].is_whitespace() {
4831 let mut j = i + 1;
4832 while j < total && chars[j].is_whitespace() {
4833 j += 1;
4834 }
4835 if j >= total {
4836 return None;
4837 }
4838 return Some(idx_to_pos(j));
4839 }
4840 }
4841 i += 1;
4842 }
4843 None
4844 } else {
4845 let find_start = |from: usize| -> Option<usize> {
4849 let mut start = from;
4850 while start > 0 {
4851 let prev = chars[start - 1];
4852 if prev.is_whitespace() {
4853 let mut k = start - 1;
4854 while k > 0 && chars[k - 1].is_whitespace() {
4855 k -= 1;
4856 }
4857 if k > 0 && is_terminator(chars[k - 1]) {
4858 break;
4859 }
4860 }
4861 start -= 1;
4862 }
4863 while start < total && chars[start].is_whitespace() {
4864 start += 1;
4865 }
4866 (start < total).then_some(start)
4867 };
4868 let current_start = find_start(cursor_idx)?;
4869 if current_start < cursor_idx {
4870 return Some(idx_to_pos(current_start));
4871 }
4872 let mut k = current_start;
4875 while k > 0 && chars[k - 1].is_whitespace() {
4876 k -= 1;
4877 }
4878 if k == 0 {
4879 return None;
4880 }
4881 let prev_start = find_start(k - 1)?;
4882 Some(idx_to_pos(prev_start))
4883 }
4884}
4885
4886fn sentence_text_object<H: crate::types::Host>(
4892 ed: &Editor<hjkl_buffer::Buffer, H>,
4893 inner: bool,
4894) -> Option<((usize, usize), (usize, usize))> {
4895 let lines = buf_lines_to_vec(&ed.buffer);
4896 if lines.is_empty() {
4897 return None;
4898 }
4899 let pos_to_idx = |pos: (usize, usize)| -> usize {
4902 let mut idx = 0;
4903 for line in lines.iter().take(pos.0) {
4904 idx += line.chars().count() + 1;
4905 }
4906 idx + pos.1
4907 };
4908 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4909 for (r, line) in lines.iter().enumerate() {
4910 let len = line.chars().count();
4911 if idx <= len {
4912 return (r, idx);
4913 }
4914 idx -= len + 1;
4915 }
4916 let last = lines.len().saturating_sub(1);
4917 (last, lines[last].chars().count())
4918 };
4919 let mut chars: Vec<char> = Vec::new();
4920 for (r, line) in lines.iter().enumerate() {
4921 chars.extend(line.chars());
4922 if r + 1 < lines.len() {
4923 chars.push('\n');
4924 }
4925 }
4926 if chars.is_empty() {
4927 return None;
4928 }
4929
4930 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
4931 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4932
4933 let mut start = cursor_idx;
4937 while start > 0 {
4938 let prev = chars[start - 1];
4939 if prev.is_whitespace() {
4940 let mut k = start - 1;
4944 while k > 0 && chars[k - 1].is_whitespace() {
4945 k -= 1;
4946 }
4947 if k > 0 && is_terminator(chars[k - 1]) {
4948 break;
4949 }
4950 }
4951 start -= 1;
4952 }
4953 while start < chars.len() && chars[start].is_whitespace() {
4956 start += 1;
4957 }
4958 if start >= chars.len() {
4959 return None;
4960 }
4961
4962 let mut end = start;
4965 while end < chars.len() {
4966 if is_terminator(chars[end]) {
4967 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
4969 end += 1;
4970 }
4971 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
4974 break;
4975 }
4976 }
4977 end += 1;
4978 }
4979 let end_idx = (end + 1).min(chars.len());
4981
4982 let final_end = if inner {
4983 end_idx
4984 } else {
4985 let mut e = end_idx;
4989 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
4990 e += 1;
4991 }
4992 e
4993 };
4994
4995 Some((idx_to_pos(start), idx_to_pos(final_end)))
4996}
4997
4998fn tag_text_object<H: crate::types::Host>(
5002 ed: &Editor<hjkl_buffer::Buffer, H>,
5003 inner: bool,
5004) -> Option<((usize, usize), (usize, usize))> {
5005 let lines = buf_lines_to_vec(&ed.buffer);
5006 if lines.is_empty() {
5007 return None;
5008 }
5009 let pos_to_idx = |pos: (usize, usize)| -> usize {
5013 let mut idx = 0;
5014 for line in lines.iter().take(pos.0) {
5015 idx += line.chars().count() + 1;
5016 }
5017 idx + pos.1
5018 };
5019 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
5020 for (r, line) in lines.iter().enumerate() {
5021 let len = line.chars().count();
5022 if idx <= len {
5023 return (r, idx);
5024 }
5025 idx -= len + 1;
5026 }
5027 let last = lines.len().saturating_sub(1);
5028 (last, lines[last].chars().count())
5029 };
5030 let mut chars: Vec<char> = Vec::new();
5031 for (r, line) in lines.iter().enumerate() {
5032 chars.extend(line.chars());
5033 if r + 1 < lines.len() {
5034 chars.push('\n');
5035 }
5036 }
5037 let cursor_idx = pos_to_idx(ed.cursor());
5038
5039 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
5047 let mut next_after: Option<(usize, usize, usize, usize)> = None;
5048 let mut i = 0;
5049 while i < chars.len() {
5050 if chars[i] != '<' {
5051 i += 1;
5052 continue;
5053 }
5054 let mut j = i + 1;
5055 while j < chars.len() && chars[j] != '>' {
5056 j += 1;
5057 }
5058 if j >= chars.len() {
5059 break;
5060 }
5061 let inside: String = chars[i + 1..j].iter().collect();
5062 let close_end = j + 1;
5063 let trimmed = inside.trim();
5064 if trimmed.starts_with('!') || trimmed.starts_with('?') {
5065 i = close_end;
5066 continue;
5067 }
5068 if let Some(rest) = trimmed.strip_prefix('/') {
5069 let name = rest.split_whitespace().next().unwrap_or("").to_string();
5070 if !name.is_empty()
5071 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
5072 {
5073 let (open_start, content_start, _) = stack[stack_idx].clone();
5074 stack.truncate(stack_idx);
5075 let content_end = i;
5076 let candidate = (open_start, content_start, content_end, close_end);
5077 if cursor_idx >= content_start && cursor_idx <= content_end {
5078 innermost = match innermost {
5079 Some((_, cs, ce, _)) if cs <= content_start && content_end <= ce => {
5080 Some(candidate)
5081 }
5082 None => Some(candidate),
5083 existing => existing,
5084 };
5085 } else if open_start >= cursor_idx && next_after.is_none() {
5086 next_after = Some(candidate);
5087 }
5088 }
5089 } else if !trimmed.ends_with('/') {
5090 let name: String = trimmed
5091 .split(|c: char| c.is_whitespace() || c == '/')
5092 .next()
5093 .unwrap_or("")
5094 .to_string();
5095 if !name.is_empty() {
5096 stack.push((i, close_end, name));
5097 }
5098 }
5099 i = close_end;
5100 }
5101
5102 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
5103 if inner {
5104 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
5105 } else {
5106 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
5107 }
5108}
5109
5110fn is_wordchar(c: char) -> bool {
5111 c.is_alphanumeric() || c == '_'
5112}
5113
5114pub(crate) use hjkl_buffer::is_keyword_char;
5118
5119fn word_text_object<H: crate::types::Host>(
5120 ed: &Editor<hjkl_buffer::Buffer, H>,
5121 inner: bool,
5122 big: bool,
5123) -> Option<((usize, usize), (usize, usize))> {
5124 let (row, col) = ed.cursor();
5125 let line = buf_line(&ed.buffer, row)?;
5126 let chars: Vec<char> = line.chars().collect();
5127 if chars.is_empty() {
5128 return None;
5129 }
5130 let at = col.min(chars.len().saturating_sub(1));
5131 let classify = |c: char| -> u8 {
5132 if c.is_whitespace() {
5133 0
5134 } else if big || is_wordchar(c) {
5135 1
5136 } else {
5137 2
5138 }
5139 };
5140 let cls = classify(chars[at]);
5141 let mut start = at;
5142 while start > 0 && classify(chars[start - 1]) == cls {
5143 start -= 1;
5144 }
5145 let mut end = at;
5146 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
5147 end += 1;
5148 }
5149 let char_byte = |i: usize| {
5151 if i >= chars.len() {
5152 line.len()
5153 } else {
5154 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
5155 }
5156 };
5157 let mut start_col = char_byte(start);
5158 let mut end_col = char_byte(end + 1);
5160 if !inner {
5161 let mut t = end + 1;
5163 let mut included_trailing = false;
5164 while t < chars.len() && chars[t].is_whitespace() {
5165 included_trailing = true;
5166 t += 1;
5167 }
5168 if included_trailing {
5169 end_col = char_byte(t);
5170 } else {
5171 let mut s = start;
5172 while s > 0 && chars[s - 1].is_whitespace() {
5173 s -= 1;
5174 }
5175 start_col = char_byte(s);
5176 }
5177 }
5178 Some(((row, start_col), (row, end_col)))
5179}
5180
5181fn quote_text_object<H: crate::types::Host>(
5182 ed: &Editor<hjkl_buffer::Buffer, H>,
5183 q: char,
5184 inner: bool,
5185) -> Option<((usize, usize), (usize, usize))> {
5186 let (row, col) = ed.cursor();
5187 let line = buf_line(&ed.buffer, row)?;
5188 let bytes = line.as_bytes();
5189 let q_byte = q as u8;
5190 let mut positions: Vec<usize> = Vec::new();
5192 for (i, &b) in bytes.iter().enumerate() {
5193 if b == q_byte {
5194 positions.push(i);
5195 }
5196 }
5197 if positions.len() < 2 {
5198 return None;
5199 }
5200 let mut open_idx: Option<usize> = None;
5201 let mut close_idx: Option<usize> = None;
5202 for pair in positions.chunks(2) {
5203 if pair.len() < 2 {
5204 break;
5205 }
5206 if col >= pair[0] && col <= pair[1] {
5207 open_idx = Some(pair[0]);
5208 close_idx = Some(pair[1]);
5209 break;
5210 }
5211 if col < pair[0] {
5212 open_idx = Some(pair[0]);
5213 close_idx = Some(pair[1]);
5214 break;
5215 }
5216 }
5217 let open = open_idx?;
5218 let close = close_idx?;
5219 if inner {
5221 if close <= open + 1 {
5222 return None;
5223 }
5224 Some(((row, open + 1), (row, close)))
5225 } else {
5226 let after_close = close + 1; if after_close < bytes.len() && bytes[after_close].is_ascii_whitespace() {
5233 let mut end = after_close;
5235 while end < bytes.len() && bytes[end].is_ascii_whitespace() {
5236 end += 1;
5237 }
5238 Some(((row, open), (row, end)))
5239 } else if open > 0 && bytes[open - 1].is_ascii_whitespace() {
5240 let mut start = open;
5242 while start > 0 && bytes[start - 1].is_ascii_whitespace() {
5243 start -= 1;
5244 }
5245 Some(((row, start), (row, close + 1)))
5246 } else {
5247 Some(((row, open), (row, close + 1)))
5248 }
5249 }
5250}
5251
5252fn bracket_text_object<H: crate::types::Host>(
5253 ed: &Editor<hjkl_buffer::Buffer, H>,
5254 open: char,
5255 inner: bool,
5256) -> Option<(Pos, Pos, MotionKind)> {
5257 let close = match open {
5258 '(' => ')',
5259 '[' => ']',
5260 '{' => '}',
5261 '<' => '>',
5262 _ => return None,
5263 };
5264 let (row, col) = ed.cursor();
5265 let lines = buf_lines_to_vec(&ed.buffer);
5266 let lines = lines.as_slice();
5267 let open_pos = find_open_bracket(lines, row, col, open, close)
5272 .or_else(|| find_next_open(lines, row, col, open))?;
5273 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
5274 if inner {
5276 if close_pos.0 > open_pos.0 + 1 {
5282 let inner_row_start = open_pos.0 + 1;
5284 let inner_row_end = close_pos.0 - 1;
5285 let end_col = lines
5286 .get(inner_row_end)
5287 .map(|l| l.chars().count())
5288 .unwrap_or(0);
5289 return Some((
5290 (inner_row_start, 0),
5291 (inner_row_end, end_col),
5292 MotionKind::Linewise,
5293 ));
5294 }
5295 let inner_start = advance_pos(lines, open_pos);
5296 if inner_start.0 > close_pos.0
5297 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
5298 {
5299 return None;
5300 }
5301 Some((inner_start, close_pos, MotionKind::Exclusive))
5302 } else {
5303 Some((
5304 open_pos,
5305 advance_pos(lines, close_pos),
5306 MotionKind::Exclusive,
5307 ))
5308 }
5309}
5310
5311fn find_open_bracket(
5312 lines: &[String],
5313 row: usize,
5314 col: usize,
5315 open: char,
5316 close: char,
5317) -> Option<(usize, usize)> {
5318 let mut depth: i32 = 0;
5319 let mut r = row;
5320 let mut c = col as isize;
5321 loop {
5322 let cur = &lines[r];
5323 let chars: Vec<char> = cur.chars().collect();
5324 if (c as usize) >= chars.len() {
5328 c = chars.len() as isize - 1;
5329 }
5330 while c >= 0 {
5331 let ch = chars[c as usize];
5332 if ch == close {
5333 depth += 1;
5334 } else if ch == open {
5335 if depth == 0 {
5336 return Some((r, c as usize));
5337 }
5338 depth -= 1;
5339 }
5340 c -= 1;
5341 }
5342 if r == 0 {
5343 return None;
5344 }
5345 r -= 1;
5346 c = lines[r].chars().count() as isize - 1;
5347 }
5348}
5349
5350fn find_close_bracket(
5351 lines: &[String],
5352 row: usize,
5353 start_col: usize,
5354 open: char,
5355 close: char,
5356) -> Option<(usize, usize)> {
5357 let mut depth: i32 = 0;
5358 let mut r = row;
5359 let mut c = start_col;
5360 loop {
5361 let cur = &lines[r];
5362 let chars: Vec<char> = cur.chars().collect();
5363 while c < chars.len() {
5364 let ch = chars[c];
5365 if ch == open {
5366 depth += 1;
5367 } else if ch == close {
5368 if depth == 0 {
5369 return Some((r, c));
5370 }
5371 depth -= 1;
5372 }
5373 c += 1;
5374 }
5375 if r + 1 >= lines.len() {
5376 return None;
5377 }
5378 r += 1;
5379 c = 0;
5380 }
5381}
5382
5383fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
5387 let mut r = row;
5388 let mut c = col;
5389 while r < lines.len() {
5390 let chars: Vec<char> = lines[r].chars().collect();
5391 while c < chars.len() {
5392 if chars[c] == open {
5393 return Some((r, c));
5394 }
5395 c += 1;
5396 }
5397 r += 1;
5398 c = 0;
5399 }
5400 None
5401}
5402
5403fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
5404 let (r, c) = pos;
5405 let line_len = lines[r].chars().count();
5406 if c < line_len {
5407 (r, c + 1)
5408 } else if r + 1 < lines.len() {
5409 (r + 1, 0)
5410 } else {
5411 pos
5412 }
5413}
5414
5415fn paragraph_text_object<H: crate::types::Host>(
5416 ed: &Editor<hjkl_buffer::Buffer, H>,
5417 inner: bool,
5418) -> Option<((usize, usize), (usize, usize))> {
5419 let (row, _) = ed.cursor();
5420 let lines = buf_lines_to_vec(&ed.buffer);
5421 if lines.is_empty() {
5422 return None;
5423 }
5424 let is_blank = |r: usize| lines.get(r).map(|s| s.trim().is_empty()).unwrap_or(true);
5426 if is_blank(row) {
5427 return None;
5428 }
5429 let mut top = row;
5430 while top > 0 && !is_blank(top - 1) {
5431 top -= 1;
5432 }
5433 let mut bot = row;
5434 while bot + 1 < lines.len() && !is_blank(bot + 1) {
5435 bot += 1;
5436 }
5437 if !inner && bot + 1 < lines.len() && is_blank(bot + 1) {
5439 bot += 1;
5440 }
5441 let end_col = lines[bot].chars().count();
5442 Some(((top, 0), (bot, end_col)))
5443}
5444
5445fn read_vim_range<H: crate::types::Host>(
5451 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5452 start: (usize, usize),
5453 end: (usize, usize),
5454 kind: MotionKind,
5455) -> String {
5456 let (top, bot) = order(start, end);
5457 ed.sync_buffer_content_from_textarea();
5458 let lines = buf_lines_to_vec(&ed.buffer);
5459 match kind {
5460 MotionKind::Linewise => {
5461 let lo = top.0;
5462 let hi = bot.0.min(lines.len().saturating_sub(1));
5463 let mut text = lines[lo..=hi].join("\n");
5464 text.push('\n');
5465 text
5466 }
5467 MotionKind::Inclusive | MotionKind::Exclusive => {
5468 let inclusive = matches!(kind, MotionKind::Inclusive);
5469 let mut out = String::new();
5471 for row in top.0..=bot.0 {
5472 let line = lines.get(row).map(String::as_str).unwrap_or("");
5473 let lo = if row == top.0 { top.1 } else { 0 };
5474 let hi_unclamped = if row == bot.0 {
5475 if inclusive { bot.1 + 1 } else { bot.1 }
5476 } else {
5477 line.chars().count() + 1
5478 };
5479 let row_chars: Vec<char> = line.chars().collect();
5480 let hi = hi_unclamped.min(row_chars.len());
5481 if lo < hi {
5482 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
5483 }
5484 if row < bot.0 {
5485 out.push('\n');
5486 }
5487 }
5488 out
5489 }
5490 }
5491}
5492
5493fn cut_vim_range<H: crate::types::Host>(
5502 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5503 start: (usize, usize),
5504 end: (usize, usize),
5505 kind: MotionKind,
5506) -> String {
5507 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
5508 let (top, bot) = order(start, end);
5509 ed.sync_buffer_content_from_textarea();
5510 let (buf_start, buf_end, buf_kind) = match kind {
5511 MotionKind::Linewise => (
5512 Position::new(top.0, 0),
5513 Position::new(bot.0, 0),
5514 BufKind::Line,
5515 ),
5516 MotionKind::Inclusive => {
5517 let line_chars = buf_line_chars(&ed.buffer, bot.0);
5518 let next = if bot.1 < line_chars {
5522 Position::new(bot.0, bot.1 + 1)
5523 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
5524 Position::new(bot.0 + 1, 0)
5525 } else {
5526 Position::new(bot.0, line_chars)
5527 };
5528 (Position::new(top.0, top.1), next, BufKind::Char)
5529 }
5530 MotionKind::Exclusive => (
5531 Position::new(top.0, top.1),
5532 Position::new(bot.0, bot.1),
5533 BufKind::Char,
5534 ),
5535 };
5536 let inverse = ed.mutate_edit(Edit::DeleteRange {
5537 start: buf_start,
5538 end: buf_end,
5539 kind: buf_kind,
5540 });
5541 let text = match inverse {
5542 Edit::InsertStr { text, .. } => text,
5543 _ => String::new(),
5544 };
5545 if !text.is_empty() {
5546 ed.record_yank_to_host(text.clone());
5547 ed.record_delete(text.clone(), matches!(kind, MotionKind::Linewise));
5548 }
5549 ed.push_buffer_cursor_to_textarea();
5550 text
5551}
5552
5553fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5559 use hjkl_buffer::{Edit, MotionKind, Position};
5560 ed.sync_buffer_content_from_textarea();
5561 let cursor = buf_cursor_pos(&ed.buffer);
5562 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5563 if cursor.col >= line_chars {
5564 return;
5565 }
5566 let inverse = ed.mutate_edit(Edit::DeleteRange {
5567 start: cursor,
5568 end: Position::new(cursor.row, line_chars),
5569 kind: MotionKind::Char,
5570 });
5571 if let Edit::InsertStr { text, .. } = inverse
5572 && !text.is_empty()
5573 {
5574 ed.record_yank_to_host(text.clone());
5575 ed.vim.yank_linewise = false;
5576 ed.set_yank(text);
5577 }
5578 buf_set_cursor_pos(&mut ed.buffer, cursor);
5579 ed.push_buffer_cursor_to_textarea();
5580}
5581
5582fn do_char_delete<H: crate::types::Host>(
5583 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5584 forward: bool,
5585 count: usize,
5586) {
5587 use hjkl_buffer::{Edit, MotionKind, Position};
5588 ed.push_undo();
5589 ed.sync_buffer_content_from_textarea();
5590 let mut deleted = String::new();
5593 for _ in 0..count {
5594 let cursor = buf_cursor_pos(&ed.buffer);
5595 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5596 if forward {
5597 if cursor.col >= line_chars {
5600 continue;
5601 }
5602 let inverse = ed.mutate_edit(Edit::DeleteRange {
5603 start: cursor,
5604 end: Position::new(cursor.row, cursor.col + 1),
5605 kind: MotionKind::Char,
5606 });
5607 if let Edit::InsertStr { text, .. } = inverse {
5608 deleted.push_str(&text);
5609 }
5610 } else {
5611 if cursor.col == 0 {
5613 continue;
5614 }
5615 let inverse = ed.mutate_edit(Edit::DeleteRange {
5616 start: Position::new(cursor.row, cursor.col - 1),
5617 end: cursor,
5618 kind: MotionKind::Char,
5619 });
5620 if let Edit::InsertStr { text, .. } = inverse {
5621 deleted = text + &deleted;
5624 }
5625 }
5626 }
5627 if !deleted.is_empty() {
5628 ed.record_yank_to_host(deleted.clone());
5629 ed.record_delete(deleted, false);
5630 }
5631 ed.push_buffer_cursor_to_textarea();
5632}
5633
5634fn adjust_number<H: crate::types::Host>(
5638 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5639 delta: i64,
5640) -> bool {
5641 use hjkl_buffer::{Edit, MotionKind, Position};
5642 ed.sync_buffer_content_from_textarea();
5643 let cursor = buf_cursor_pos(&ed.buffer);
5644 let row = cursor.row;
5645 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
5646 Some(l) => l.chars().collect(),
5647 None => return false,
5648 };
5649 let Some(digit_start) = (cursor.col..chars.len()).find(|&i| chars[i].is_ascii_digit()) else {
5650 return false;
5651 };
5652 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
5653 digit_start - 1
5654 } else {
5655 digit_start
5656 };
5657 let mut span_end = digit_start;
5658 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
5659 span_end += 1;
5660 }
5661 let s: String = chars[span_start..span_end].iter().collect();
5662 let Ok(n) = s.parse::<i64>() else {
5663 return false;
5664 };
5665 let new_s = n.saturating_add(delta).to_string();
5666
5667 ed.push_undo();
5668 let span_start_pos = Position::new(row, span_start);
5669 let span_end_pos = Position::new(row, span_end);
5670 ed.mutate_edit(Edit::DeleteRange {
5671 start: span_start_pos,
5672 end: span_end_pos,
5673 kind: MotionKind::Char,
5674 });
5675 ed.mutate_edit(Edit::InsertStr {
5676 at: span_start_pos,
5677 text: new_s.clone(),
5678 });
5679 let new_len = new_s.chars().count();
5680 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
5681 ed.push_buffer_cursor_to_textarea();
5682 true
5683}
5684
5685pub(crate) fn replace_char<H: crate::types::Host>(
5686 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5687 ch: char,
5688 count: usize,
5689) {
5690 use hjkl_buffer::{Edit, MotionKind, Position};
5691 ed.push_undo();
5692 ed.sync_buffer_content_from_textarea();
5693 for _ in 0..count {
5694 let cursor = buf_cursor_pos(&ed.buffer);
5695 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5696 if cursor.col >= line_chars {
5697 break;
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 { at: cursor, ch });
5705 }
5706 crate::motions::move_left(&mut ed.buffer, 1);
5708 ed.push_buffer_cursor_to_textarea();
5709}
5710
5711fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5712 use hjkl_buffer::{Edit, MotionKind, Position};
5713 ed.sync_buffer_content_from_textarea();
5714 let cursor = buf_cursor_pos(&ed.buffer);
5715 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
5716 return;
5717 };
5718 let toggled = if c.is_uppercase() {
5719 c.to_lowercase().next().unwrap_or(c)
5720 } else {
5721 c.to_uppercase().next().unwrap_or(c)
5722 };
5723 ed.mutate_edit(Edit::DeleteRange {
5724 start: cursor,
5725 end: Position::new(cursor.row, cursor.col + 1),
5726 kind: MotionKind::Char,
5727 });
5728 ed.mutate_edit(Edit::InsertChar {
5729 at: cursor,
5730 ch: toggled,
5731 });
5732}
5733
5734fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5735 use hjkl_buffer::{Edit, Position};
5736 ed.sync_buffer_content_from_textarea();
5737 let row = buf_cursor_pos(&ed.buffer).row;
5738 if row + 1 >= buf_row_count(&ed.buffer) {
5739 return;
5740 }
5741 let cur_line = buf_line(&ed.buffer, row).unwrap_or("").to_string();
5742 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or("").to_string();
5743 let next_trimmed = next_raw.trim_start();
5744 let cur_chars = cur_line.chars().count();
5745 let next_chars = next_raw.chars().count();
5746 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
5749 " "
5750 } else {
5751 ""
5752 };
5753 let joined = format!("{cur_line}{separator}{next_trimmed}");
5754 ed.mutate_edit(Edit::Replace {
5755 start: Position::new(row, 0),
5756 end: Position::new(row + 1, next_chars),
5757 with: joined,
5758 });
5759 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
5763 ed.push_buffer_cursor_to_textarea();
5764}
5765
5766fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5769 use hjkl_buffer::Edit;
5770 ed.sync_buffer_content_from_textarea();
5771 let row = buf_cursor_pos(&ed.buffer).row;
5772 if row + 1 >= buf_row_count(&ed.buffer) {
5773 return;
5774 }
5775 let join_col = buf_line_chars(&ed.buffer, row);
5776 ed.mutate_edit(Edit::JoinLines {
5777 row,
5778 count: 1,
5779 with_space: false,
5780 });
5781 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
5783 ed.push_buffer_cursor_to_textarea();
5784}
5785
5786fn do_paste<H: crate::types::Host>(
5787 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5788 before: bool,
5789 count: usize,
5790) {
5791 use hjkl_buffer::{Edit, Position};
5792 ed.push_undo();
5793 let selector = ed.vim.pending_register.take();
5798 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
5799 Some(slot) => (slot.text.clone(), slot.linewise),
5800 None => {
5806 let s = &ed.registers().unnamed;
5807 (s.text.clone(), s.linewise)
5808 }
5809 };
5810 let mut paste_mark: Option<((usize, usize), (usize, usize))> = None;
5814 for _ in 0..count {
5815 ed.sync_buffer_content_from_textarea();
5816 let yank = yank.clone();
5817 if yank.is_empty() {
5818 continue;
5819 }
5820 if linewise {
5821 let text = yank.trim_matches('\n').to_string();
5825 let row = buf_cursor_pos(&ed.buffer).row;
5826 let target_row = if before {
5827 ed.mutate_edit(Edit::InsertStr {
5828 at: Position::new(row, 0),
5829 text: format!("{text}\n"),
5830 });
5831 row
5832 } else {
5833 let line_chars = buf_line_chars(&ed.buffer, row);
5834 ed.mutate_edit(Edit::InsertStr {
5835 at: Position::new(row, line_chars),
5836 text: format!("\n{text}"),
5837 });
5838 row + 1
5839 };
5840 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
5841 crate::motions::move_first_non_blank(&mut ed.buffer);
5842 ed.push_buffer_cursor_to_textarea();
5843 let payload_lines = text.lines().count().max(1);
5845 let bot_row = target_row + payload_lines - 1;
5846 let bot_last_col = buf_line_chars(&ed.buffer, bot_row).saturating_sub(1);
5847 paste_mark = Some(((target_row, 0), (bot_row, bot_last_col)));
5848 } else {
5849 let cursor = buf_cursor_pos(&ed.buffer);
5853 let at = if before {
5854 cursor
5855 } else {
5856 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5857 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
5858 };
5859 ed.mutate_edit(Edit::InsertStr {
5860 at,
5861 text: yank.clone(),
5862 });
5863 crate::motions::move_left(&mut ed.buffer, 1);
5866 ed.push_buffer_cursor_to_textarea();
5867 let lo = (at.row, at.col);
5869 let hi = ed.cursor();
5870 paste_mark = Some((lo, hi));
5871 }
5872 }
5873 if let Some((lo, hi)) = paste_mark {
5874 ed.set_mark('[', lo);
5875 ed.set_mark(']', hi);
5876 }
5877 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
5879}
5880
5881pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5882 if let Some((lines, cursor)) = ed.undo_stack.pop() {
5883 let current = ed.snapshot();
5884 ed.redo_stack.push(current);
5885 ed.restore(lines, cursor);
5886 }
5887 ed.vim.mode = Mode::Normal;
5888 clamp_cursor_to_normal_mode(ed);
5892}
5893
5894pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5895 if let Some((lines, cursor)) = ed.redo_stack.pop() {
5896 let current = ed.snapshot();
5897 ed.undo_stack.push(current);
5898 ed.cap_undo();
5899 ed.restore(lines, cursor);
5900 }
5901 ed.vim.mode = Mode::Normal;
5902}
5903
5904fn replay_insert_and_finish<H: crate::types::Host>(
5911 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5912 text: &str,
5913) {
5914 use hjkl_buffer::{Edit, Position};
5915 let cursor = ed.cursor();
5916 ed.mutate_edit(Edit::InsertStr {
5917 at: Position::new(cursor.0, cursor.1),
5918 text: text.to_string(),
5919 });
5920 if ed.vim.insert_session.take().is_some() {
5921 if ed.cursor().1 > 0 {
5922 crate::motions::move_left(&mut ed.buffer, 1);
5923 ed.push_buffer_cursor_to_textarea();
5924 }
5925 ed.vim.mode = Mode::Normal;
5926 }
5927}
5928
5929fn replay_last_change<H: crate::types::Host>(
5930 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5931 outer_count: usize,
5932) {
5933 let Some(change) = ed.vim.last_change.clone() else {
5934 return;
5935 };
5936 ed.vim.replaying = true;
5937 let scale = if outer_count > 0 { outer_count } else { 1 };
5938 match change {
5939 LastChange::OpMotion {
5940 op,
5941 motion,
5942 count,
5943 inserted,
5944 } => {
5945 let total = count.max(1) * scale;
5946 apply_op_with_motion(ed, op, &motion, total);
5947 if let Some(text) = inserted {
5948 replay_insert_and_finish(ed, &text);
5949 }
5950 }
5951 LastChange::OpTextObj {
5952 op,
5953 obj,
5954 inner,
5955 inserted,
5956 } => {
5957 apply_op_with_text_object(ed, op, obj, inner);
5958 if let Some(text) = inserted {
5959 replay_insert_and_finish(ed, &text);
5960 }
5961 }
5962 LastChange::LineOp {
5963 op,
5964 count,
5965 inserted,
5966 } => {
5967 let total = count.max(1) * scale;
5968 execute_line_op(ed, op, total);
5969 if let Some(text) = inserted {
5970 replay_insert_and_finish(ed, &text);
5971 }
5972 }
5973 LastChange::CharDel { forward, count } => {
5974 do_char_delete(ed, forward, count * scale);
5975 }
5976 LastChange::ReplaceChar { ch, count } => {
5977 replace_char(ed, ch, count * scale);
5978 }
5979 LastChange::ToggleCase { count } => {
5980 for _ in 0..count * scale {
5981 ed.push_undo();
5982 toggle_case_at_cursor(ed);
5983 }
5984 }
5985 LastChange::JoinLine { count } => {
5986 for _ in 0..count * scale {
5987 ed.push_undo();
5988 join_line(ed);
5989 }
5990 }
5991 LastChange::Paste { before, count } => {
5992 do_paste(ed, before, count * scale);
5993 }
5994 LastChange::DeleteToEol { inserted } => {
5995 use hjkl_buffer::{Edit, Position};
5996 ed.push_undo();
5997 delete_to_eol(ed);
5998 if let Some(text) = inserted {
5999 let cursor = ed.cursor();
6000 ed.mutate_edit(Edit::InsertStr {
6001 at: Position::new(cursor.0, cursor.1),
6002 text,
6003 });
6004 }
6005 }
6006 LastChange::OpenLine { above, inserted } => {
6007 use hjkl_buffer::{Edit, Position};
6008 ed.push_undo();
6009 ed.sync_buffer_content_from_textarea();
6010 let row = buf_cursor_pos(&ed.buffer).row;
6011 if above {
6012 ed.mutate_edit(Edit::InsertStr {
6013 at: Position::new(row, 0),
6014 text: "\n".to_string(),
6015 });
6016 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
6017 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
6018 } else {
6019 let line_chars = buf_line_chars(&ed.buffer, row);
6020 ed.mutate_edit(Edit::InsertStr {
6021 at: Position::new(row, line_chars),
6022 text: "\n".to_string(),
6023 });
6024 }
6025 ed.push_buffer_cursor_to_textarea();
6026 let cursor = ed.cursor();
6027 ed.mutate_edit(Edit::InsertStr {
6028 at: Position::new(cursor.0, cursor.1),
6029 text: inserted,
6030 });
6031 }
6032 LastChange::InsertAt {
6033 entry,
6034 inserted,
6035 count,
6036 } => {
6037 use hjkl_buffer::{Edit, Position};
6038 ed.push_undo();
6039 match entry {
6040 InsertEntry::I => {}
6041 InsertEntry::ShiftI => move_first_non_whitespace(ed),
6042 InsertEntry::A => {
6043 crate::motions::move_right_to_end(&mut ed.buffer, 1);
6044 ed.push_buffer_cursor_to_textarea();
6045 }
6046 InsertEntry::ShiftA => {
6047 crate::motions::move_line_end(&mut ed.buffer);
6048 crate::motions::move_right_to_end(&mut ed.buffer, 1);
6049 ed.push_buffer_cursor_to_textarea();
6050 }
6051 }
6052 for _ in 0..count.max(1) {
6053 let cursor = ed.cursor();
6054 ed.mutate_edit(Edit::InsertStr {
6055 at: Position::new(cursor.0, cursor.1),
6056 text: inserted.clone(),
6057 });
6058 }
6059 }
6060 }
6061 ed.vim.replaying = false;
6062}
6063
6064fn extract_inserted(before: &str, after: &str) -> String {
6067 let before_chars: Vec<char> = before.chars().collect();
6068 let after_chars: Vec<char> = after.chars().collect();
6069 if after_chars.len() <= before_chars.len() {
6070 return String::new();
6071 }
6072 let prefix = before_chars
6073 .iter()
6074 .zip(after_chars.iter())
6075 .take_while(|(a, b)| a == b)
6076 .count();
6077 let max_suffix = before_chars.len() - prefix;
6078 let suffix = before_chars
6079 .iter()
6080 .rev()
6081 .zip(after_chars.iter().rev())
6082 .take(max_suffix)
6083 .take_while(|(a, b)| a == b)
6084 .count();
6085 after_chars[prefix..after_chars.len() - suffix]
6086 .iter()
6087 .collect()
6088}
6089
6090#[cfg(all(test, feature = "crossterm"))]
6093mod tests {
6094 use crate::VimMode;
6095 use crate::editor::Editor;
6096 use crate::types::Host;
6097 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
6098
6099 fn run_keys<H: crate::types::Host>(e: &mut Editor<hjkl_buffer::Buffer, H>, keys: &str) {
6100 let mut iter = keys.chars().peekable();
6104 while let Some(c) = iter.next() {
6105 if c == '<' {
6106 let mut tag = String::new();
6107 for ch in iter.by_ref() {
6108 if ch == '>' {
6109 break;
6110 }
6111 tag.push(ch);
6112 }
6113 let ev = match tag.as_str() {
6114 "Esc" => KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
6115 "CR" => KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
6116 "BS" => KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE),
6117 "Space" => KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE),
6118 "Up" => KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
6119 "Down" => KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
6120 "Left" => KeyEvent::new(KeyCode::Left, KeyModifiers::NONE),
6121 "Right" => KeyEvent::new(KeyCode::Right, KeyModifiers::NONE),
6122 "lt" => KeyEvent::new(KeyCode::Char('<'), KeyModifiers::NONE),
6126 s if s.starts_with("C-") => {
6127 let ch = s.chars().nth(2).unwrap();
6128 KeyEvent::new(KeyCode::Char(ch), KeyModifiers::CONTROL)
6129 }
6130 _ => continue,
6131 };
6132 e.handle_key(ev);
6133 } else {
6134 let mods = if c.is_uppercase() {
6135 KeyModifiers::SHIFT
6136 } else {
6137 KeyModifiers::NONE
6138 };
6139 e.handle_key(KeyEvent::new(KeyCode::Char(c), mods));
6140 }
6141 }
6142 }
6143
6144 fn editor_with(content: &str) -> Editor {
6145 let opts = crate::types::Options {
6150 shiftwidth: 2,
6151 ..crate::types::Options::default()
6152 };
6153 let mut e = Editor::new(
6154 hjkl_buffer::Buffer::new(),
6155 crate::types::DefaultHost::new(),
6156 opts,
6157 );
6158 e.set_content(content);
6159 e
6160 }
6161
6162 #[test]
6163 fn f_char_jumps_on_line() {
6164 let mut e = editor_with("hello world");
6165 run_keys(&mut e, "fw");
6166 assert_eq!(e.cursor(), (0, 6));
6167 }
6168
6169 #[test]
6170 fn cap_f_jumps_backward() {
6171 let mut e = editor_with("hello world");
6172 e.jump_cursor(0, 10);
6173 run_keys(&mut e, "Fo");
6174 assert_eq!(e.cursor().1, 7);
6175 }
6176
6177 #[test]
6178 fn t_stops_before_char() {
6179 let mut e = editor_with("hello");
6180 run_keys(&mut e, "tl");
6181 assert_eq!(e.cursor(), (0, 1));
6182 }
6183
6184 #[test]
6185 fn semicolon_repeats_find() {
6186 let mut e = editor_with("aa.bb.cc");
6187 run_keys(&mut e, "f.");
6188 assert_eq!(e.cursor().1, 2);
6189 run_keys(&mut e, ";");
6190 assert_eq!(e.cursor().1, 5);
6191 }
6192
6193 #[test]
6194 fn comma_repeats_find_reverse() {
6195 let mut e = editor_with("aa.bb.cc");
6196 run_keys(&mut e, "f.");
6197 run_keys(&mut e, ";");
6198 run_keys(&mut e, ",");
6199 assert_eq!(e.cursor().1, 2);
6200 }
6201
6202 #[test]
6203 fn di_quote_deletes_content() {
6204 let mut e = editor_with("foo \"bar\" baz");
6205 e.jump_cursor(0, 6); run_keys(&mut e, "di\"");
6207 assert_eq!(e.buffer().lines()[0], "foo \"\" baz");
6208 }
6209
6210 #[test]
6211 fn da_quote_deletes_with_quotes() {
6212 let mut e = editor_with("foo \"bar\" baz");
6215 e.jump_cursor(0, 6);
6216 run_keys(&mut e, "da\"");
6217 assert_eq!(e.buffer().lines()[0], "foo baz");
6218 }
6219
6220 #[test]
6221 fn ci_paren_deletes_and_inserts() {
6222 let mut e = editor_with("fn(a, b, c)");
6223 e.jump_cursor(0, 5);
6224 run_keys(&mut e, "ci(");
6225 assert_eq!(e.vim_mode(), VimMode::Insert);
6226 assert_eq!(e.buffer().lines()[0], "fn()");
6227 }
6228
6229 #[test]
6230 fn diw_deletes_inner_word() {
6231 let mut e = editor_with("hello world");
6232 e.jump_cursor(0, 2);
6233 run_keys(&mut e, "diw");
6234 assert_eq!(e.buffer().lines()[0], " world");
6235 }
6236
6237 #[test]
6238 fn daw_deletes_word_with_trailing_space() {
6239 let mut e = editor_with("hello world");
6240 run_keys(&mut e, "daw");
6241 assert_eq!(e.buffer().lines()[0], "world");
6242 }
6243
6244 #[test]
6245 fn percent_jumps_to_matching_bracket() {
6246 let mut e = editor_with("foo(bar)");
6247 e.jump_cursor(0, 3);
6248 run_keys(&mut e, "%");
6249 assert_eq!(e.cursor().1, 7);
6250 run_keys(&mut e, "%");
6251 assert_eq!(e.cursor().1, 3);
6252 }
6253
6254 #[test]
6255 fn dot_repeats_last_change() {
6256 let mut e = editor_with("aaa bbb ccc");
6257 run_keys(&mut e, "dw");
6258 assert_eq!(e.buffer().lines()[0], "bbb ccc");
6259 run_keys(&mut e, ".");
6260 assert_eq!(e.buffer().lines()[0], "ccc");
6261 }
6262
6263 #[test]
6264 fn dot_repeats_change_operator_with_text() {
6265 let mut e = editor_with("foo foo foo");
6266 run_keys(&mut e, "cwbar<Esc>");
6267 assert_eq!(e.buffer().lines()[0], "bar foo foo");
6268 run_keys(&mut e, "w");
6270 run_keys(&mut e, ".");
6271 assert_eq!(e.buffer().lines()[0], "bar bar foo");
6272 }
6273
6274 #[test]
6275 fn dot_repeats_x() {
6276 let mut e = editor_with("abcdef");
6277 run_keys(&mut e, "x");
6278 run_keys(&mut e, "..");
6279 assert_eq!(e.buffer().lines()[0], "def");
6280 }
6281
6282 #[test]
6283 fn count_operator_motion_compose() {
6284 let mut e = editor_with("one two three four five");
6285 run_keys(&mut e, "d3w");
6286 assert_eq!(e.buffer().lines()[0], "four five");
6287 }
6288
6289 #[test]
6290 fn two_dd_deletes_two_lines() {
6291 let mut e = editor_with("a\nb\nc");
6292 run_keys(&mut e, "2dd");
6293 assert_eq!(e.buffer().lines().len(), 1);
6294 assert_eq!(e.buffer().lines()[0], "c");
6295 }
6296
6297 #[test]
6302 fn dd_in_middle_puts_cursor_on_first_non_blank_of_next() {
6303 let mut e = editor_with("one\ntwo\n three\nfour");
6304 e.jump_cursor(1, 2);
6305 run_keys(&mut e, "dd");
6306 assert_eq!(e.buffer().lines()[1], " three");
6308 assert_eq!(e.cursor(), (1, 4));
6309 }
6310
6311 #[test]
6312 fn dd_on_last_line_puts_cursor_on_first_non_blank_of_prev() {
6313 let mut e = editor_with("one\n two\nthree");
6314 e.jump_cursor(2, 0);
6315 run_keys(&mut e, "dd");
6316 assert_eq!(e.buffer().lines().len(), 2);
6318 assert_eq!(e.cursor(), (1, 2));
6319 }
6320
6321 #[test]
6322 fn dd_on_only_line_leaves_empty_buffer_and_cursor_at_zero() {
6323 let mut e = editor_with("lonely");
6324 run_keys(&mut e, "dd");
6325 assert_eq!(e.buffer().lines().len(), 1);
6326 assert_eq!(e.buffer().lines()[0], "");
6327 assert_eq!(e.cursor(), (0, 0));
6328 }
6329
6330 #[test]
6331 fn count_dd_puts_cursor_on_first_non_blank_of_remaining() {
6332 let mut e = editor_with("a\nb\nc\n d\ne");
6333 e.jump_cursor(1, 0);
6335 run_keys(&mut e, "3dd");
6336 assert_eq!(e.buffer().lines(), &["a".to_string(), "e".to_string()]);
6337 assert_eq!(e.cursor(), (1, 0));
6338 }
6339
6340 #[test]
6341 fn dd_then_j_uses_first_non_blank_not_sticky_col() {
6342 let mut e = editor_with(" line one\n line two\n xyz!");
6361 e.jump_cursor(0, 8);
6363 assert_eq!(e.cursor(), (0, 8));
6364 run_keys(&mut e, "dd");
6367 assert_eq!(
6368 e.cursor(),
6369 (0, 4),
6370 "dd must place cursor on first-non-blank"
6371 );
6372 run_keys(&mut e, "j");
6376 let (row, col) = e.cursor();
6377 assert_eq!(row, 1);
6378 assert_eq!(
6379 col, 4,
6380 "after dd, j should use the column dd established (4), not pre-dd sticky_col (8)"
6381 );
6382 }
6383
6384 #[test]
6385 fn gu_lowercases_motion_range() {
6386 let mut e = editor_with("HELLO WORLD");
6387 run_keys(&mut e, "guw");
6388 assert_eq!(e.buffer().lines()[0], "hello WORLD");
6389 assert_eq!(e.cursor(), (0, 0));
6390 }
6391
6392 #[test]
6393 fn g_u_uppercases_text_object() {
6394 let mut e = editor_with("hello world");
6395 run_keys(&mut e, "gUiw");
6397 assert_eq!(e.buffer().lines()[0], "HELLO world");
6398 assert_eq!(e.cursor(), (0, 0));
6399 }
6400
6401 #[test]
6402 fn g_tilde_toggles_case_of_range() {
6403 let mut e = editor_with("Hello World");
6404 run_keys(&mut e, "g~iw");
6405 assert_eq!(e.buffer().lines()[0], "hELLO World");
6406 }
6407
6408 #[test]
6409 fn g_uu_uppercases_current_line() {
6410 let mut e = editor_with("select 1\nselect 2");
6411 run_keys(&mut e, "gUU");
6412 assert_eq!(e.buffer().lines()[0], "SELECT 1");
6413 assert_eq!(e.buffer().lines()[1], "select 2");
6414 }
6415
6416 #[test]
6417 fn gugu_lowercases_current_line() {
6418 let mut e = editor_with("FOO BAR\nBAZ");
6419 run_keys(&mut e, "gugu");
6420 assert_eq!(e.buffer().lines()[0], "foo bar");
6421 }
6422
6423 #[test]
6424 fn visual_u_uppercases_selection() {
6425 let mut e = editor_with("hello world");
6426 run_keys(&mut e, "veU");
6428 assert_eq!(e.buffer().lines()[0], "HELLO world");
6429 }
6430
6431 #[test]
6432 fn visual_line_u_lowercases_line() {
6433 let mut e = editor_with("HELLO WORLD\nOTHER");
6434 run_keys(&mut e, "Vu");
6435 assert_eq!(e.buffer().lines()[0], "hello world");
6436 assert_eq!(e.buffer().lines()[1], "OTHER");
6437 }
6438
6439 #[test]
6440 fn g_uu_with_count_uppercases_multiple_lines() {
6441 let mut e = editor_with("one\ntwo\nthree\nfour");
6442 run_keys(&mut e, "3gUU");
6444 assert_eq!(e.buffer().lines()[0], "ONE");
6445 assert_eq!(e.buffer().lines()[1], "TWO");
6446 assert_eq!(e.buffer().lines()[2], "THREE");
6447 assert_eq!(e.buffer().lines()[3], "four");
6448 }
6449
6450 #[test]
6451 fn double_gt_indents_current_line() {
6452 let mut e = editor_with("hello");
6453 run_keys(&mut e, ">>");
6454 assert_eq!(e.buffer().lines()[0], " hello");
6455 assert_eq!(e.cursor(), (0, 2));
6457 }
6458
6459 #[test]
6460 fn double_lt_outdents_current_line() {
6461 let mut e = editor_with(" hello");
6462 run_keys(&mut e, "<lt><lt>");
6463 assert_eq!(e.buffer().lines()[0], " hello");
6464 assert_eq!(e.cursor(), (0, 2));
6465 }
6466
6467 #[test]
6468 fn count_double_gt_indents_multiple_lines() {
6469 let mut e = editor_with("a\nb\nc\nd");
6470 run_keys(&mut e, "3>>");
6472 assert_eq!(e.buffer().lines()[0], " a");
6473 assert_eq!(e.buffer().lines()[1], " b");
6474 assert_eq!(e.buffer().lines()[2], " c");
6475 assert_eq!(e.buffer().lines()[3], "d");
6476 }
6477
6478 #[test]
6479 fn outdent_clips_ragged_leading_whitespace() {
6480 let mut e = editor_with(" x");
6483 run_keys(&mut e, "<lt><lt>");
6484 assert_eq!(e.buffer().lines()[0], "x");
6485 }
6486
6487 #[test]
6488 fn indent_motion_is_always_linewise() {
6489 let mut e = editor_with("foo bar");
6492 run_keys(&mut e, ">w");
6493 assert_eq!(e.buffer().lines()[0], " foo bar");
6494 }
6495
6496 #[test]
6497 fn indent_text_object_extends_over_paragraph() {
6498 let mut e = editor_with("a\nb\n\nc\nd");
6499 run_keys(&mut e, ">ap");
6501 assert_eq!(e.buffer().lines()[0], " a");
6502 assert_eq!(e.buffer().lines()[1], " b");
6503 assert_eq!(e.buffer().lines()[2], "");
6504 assert_eq!(e.buffer().lines()[3], "c");
6505 }
6506
6507 #[test]
6508 fn visual_line_indent_shifts_selected_rows() {
6509 let mut e = editor_with("x\ny\nz");
6510 run_keys(&mut e, "Vj>");
6512 assert_eq!(e.buffer().lines()[0], " x");
6513 assert_eq!(e.buffer().lines()[1], " y");
6514 assert_eq!(e.buffer().lines()[2], "z");
6515 }
6516
6517 #[test]
6518 fn outdent_empty_line_is_noop() {
6519 let mut e = editor_with("\nfoo");
6520 run_keys(&mut e, "<lt><lt>");
6521 assert_eq!(e.buffer().lines()[0], "");
6522 }
6523
6524 #[test]
6525 fn indent_skips_empty_lines() {
6526 let mut e = editor_with("");
6529 run_keys(&mut e, ">>");
6530 assert_eq!(e.buffer().lines()[0], "");
6531 }
6532
6533 #[test]
6534 fn insert_ctrl_t_indents_current_line() {
6535 let mut e = editor_with("x");
6536 run_keys(&mut e, "i<C-t>");
6538 assert_eq!(e.buffer().lines()[0], " x");
6539 assert_eq!(e.cursor(), (0, 2));
6542 }
6543
6544 #[test]
6545 fn insert_ctrl_d_outdents_current_line() {
6546 let mut e = editor_with(" x");
6547 run_keys(&mut e, "A<C-d>");
6549 assert_eq!(e.buffer().lines()[0], " x");
6550 }
6551
6552 #[test]
6553 fn h_at_col_zero_does_not_wrap_to_prev_line() {
6554 let mut e = editor_with("first\nsecond");
6555 e.jump_cursor(1, 0);
6556 run_keys(&mut e, "h");
6557 assert_eq!(e.cursor(), (1, 0));
6559 }
6560
6561 #[test]
6562 fn l_at_last_char_does_not_wrap_to_next_line() {
6563 let mut e = editor_with("ab\ncd");
6564 e.jump_cursor(0, 1);
6566 run_keys(&mut e, "l");
6567 assert_eq!(e.cursor(), (0, 1));
6569 }
6570
6571 #[test]
6572 fn count_l_clamps_at_line_end() {
6573 let mut e = editor_with("abcde");
6574 run_keys(&mut e, "20l");
6577 assert_eq!(e.cursor(), (0, 4));
6578 }
6579
6580 #[test]
6581 fn count_h_clamps_at_col_zero() {
6582 let mut e = editor_with("abcde");
6583 e.jump_cursor(0, 3);
6584 run_keys(&mut e, "20h");
6585 assert_eq!(e.cursor(), (0, 0));
6586 }
6587
6588 #[test]
6589 fn dl_on_last_char_still_deletes_it() {
6590 let mut e = editor_with("ab");
6594 e.jump_cursor(0, 1);
6595 run_keys(&mut e, "dl");
6596 assert_eq!(e.buffer().lines()[0], "a");
6597 }
6598
6599 #[test]
6600 fn case_op_preserves_yank_register() {
6601 let mut e = editor_with("target");
6602 run_keys(&mut e, "yy");
6603 let yank_before = e.yank().to_string();
6604 run_keys(&mut e, "gUU");
6606 assert_eq!(e.buffer().lines()[0], "TARGET");
6607 assert_eq!(
6608 e.yank(),
6609 yank_before,
6610 "case ops must preserve the yank buffer"
6611 );
6612 }
6613
6614 #[test]
6615 fn dap_deletes_paragraph() {
6616 let mut e = editor_with("a\nb\n\nc\nd");
6617 run_keys(&mut e, "dap");
6618 assert_eq!(e.buffer().lines().first().map(String::as_str), Some("c"));
6619 }
6620
6621 #[test]
6622 fn dit_deletes_inner_tag_content() {
6623 let mut e = editor_with("<b>hello</b>");
6624 e.jump_cursor(0, 4);
6626 run_keys(&mut e, "dit");
6627 assert_eq!(e.buffer().lines()[0], "<b></b>");
6628 }
6629
6630 #[test]
6631 fn dat_deletes_around_tag() {
6632 let mut e = editor_with("hi <b>foo</b> bye");
6633 e.jump_cursor(0, 6);
6634 run_keys(&mut e, "dat");
6635 assert_eq!(e.buffer().lines()[0], "hi bye");
6636 }
6637
6638 #[test]
6639 fn dit_picks_innermost_tag() {
6640 let mut e = editor_with("<a><b>x</b></a>");
6641 e.jump_cursor(0, 6);
6643 run_keys(&mut e, "dit");
6644 assert_eq!(e.buffer().lines()[0], "<a><b></b></a>");
6646 }
6647
6648 #[test]
6649 fn dat_innermost_tag_pair() {
6650 let mut e = editor_with("<a><b>x</b></a>");
6651 e.jump_cursor(0, 6);
6652 run_keys(&mut e, "dat");
6653 assert_eq!(e.buffer().lines()[0], "<a></a>");
6654 }
6655
6656 #[test]
6657 fn dit_outside_any_tag_no_op() {
6658 let mut e = editor_with("plain text");
6659 e.jump_cursor(0, 3);
6660 run_keys(&mut e, "dit");
6661 assert_eq!(e.buffer().lines()[0], "plain text");
6663 }
6664
6665 #[test]
6666 fn cit_changes_inner_tag_content() {
6667 let mut e = editor_with("<b>hello</b>");
6668 e.jump_cursor(0, 4);
6669 run_keys(&mut e, "citNEW<Esc>");
6670 assert_eq!(e.buffer().lines()[0], "<b>NEW</b>");
6671 }
6672
6673 #[test]
6674 fn cat_changes_around_tag() {
6675 let mut e = editor_with("hi <b>foo</b> bye");
6676 e.jump_cursor(0, 6);
6677 run_keys(&mut e, "catBAR<Esc>");
6678 assert_eq!(e.buffer().lines()[0], "hi BAR bye");
6679 }
6680
6681 #[test]
6682 fn yit_yanks_inner_tag_content() {
6683 let mut e = editor_with("<b>hello</b>");
6684 e.jump_cursor(0, 4);
6685 run_keys(&mut e, "yit");
6686 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6687 }
6688
6689 #[test]
6690 fn yat_yanks_full_tag_pair() {
6691 let mut e = editor_with("hi <b>foo</b> bye");
6692 e.jump_cursor(0, 6);
6693 run_keys(&mut e, "yat");
6694 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6695 }
6696
6697 #[test]
6698 fn vit_visually_selects_inner_tag() {
6699 let mut e = editor_with("<b>hello</b>");
6700 e.jump_cursor(0, 4);
6701 run_keys(&mut e, "vit");
6702 assert_eq!(e.vim_mode(), VimMode::Visual);
6703 run_keys(&mut e, "y");
6704 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6705 }
6706
6707 #[test]
6708 fn vat_visually_selects_around_tag() {
6709 let mut e = editor_with("x<b>foo</b>y");
6710 e.jump_cursor(0, 5);
6711 run_keys(&mut e, "vat");
6712 assert_eq!(e.vim_mode(), VimMode::Visual);
6713 run_keys(&mut e, "y");
6714 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6715 }
6716
6717 #[test]
6720 #[allow(non_snake_case)]
6721 fn diW_deletes_inner_big_word() {
6722 let mut e = editor_with("foo.bar baz");
6723 e.jump_cursor(0, 2);
6724 run_keys(&mut e, "diW");
6725 assert_eq!(e.buffer().lines()[0], " baz");
6727 }
6728
6729 #[test]
6730 #[allow(non_snake_case)]
6731 fn daW_deletes_around_big_word() {
6732 let mut e = editor_with("foo.bar baz");
6733 e.jump_cursor(0, 2);
6734 run_keys(&mut e, "daW");
6735 assert_eq!(e.buffer().lines()[0], "baz");
6736 }
6737
6738 #[test]
6739 fn di_double_quote_deletes_inside() {
6740 let mut e = editor_with("a \"hello\" b");
6741 e.jump_cursor(0, 4);
6742 run_keys(&mut e, "di\"");
6743 assert_eq!(e.buffer().lines()[0], "a \"\" b");
6744 }
6745
6746 #[test]
6747 fn da_double_quote_deletes_around() {
6748 let mut e = editor_with("a \"hello\" b");
6750 e.jump_cursor(0, 4);
6751 run_keys(&mut e, "da\"");
6752 assert_eq!(e.buffer().lines()[0], "a b");
6753 }
6754
6755 #[test]
6756 fn di_single_quote_deletes_inside() {
6757 let mut e = editor_with("x 'foo' y");
6758 e.jump_cursor(0, 4);
6759 run_keys(&mut e, "di'");
6760 assert_eq!(e.buffer().lines()[0], "x '' y");
6761 }
6762
6763 #[test]
6764 fn da_single_quote_deletes_around() {
6765 let mut e = editor_with("x 'foo' y");
6767 e.jump_cursor(0, 4);
6768 run_keys(&mut e, "da'");
6769 assert_eq!(e.buffer().lines()[0], "x y");
6770 }
6771
6772 #[test]
6773 fn di_backtick_deletes_inside() {
6774 let mut e = editor_with("p `q` r");
6775 e.jump_cursor(0, 3);
6776 run_keys(&mut e, "di`");
6777 assert_eq!(e.buffer().lines()[0], "p `` r");
6778 }
6779
6780 #[test]
6781 fn da_backtick_deletes_around() {
6782 let mut e = editor_with("p `q` r");
6784 e.jump_cursor(0, 3);
6785 run_keys(&mut e, "da`");
6786 assert_eq!(e.buffer().lines()[0], "p r");
6787 }
6788
6789 #[test]
6790 fn di_paren_deletes_inside() {
6791 let mut e = editor_with("f(arg)");
6792 e.jump_cursor(0, 3);
6793 run_keys(&mut e, "di(");
6794 assert_eq!(e.buffer().lines()[0], "f()");
6795 }
6796
6797 #[test]
6798 fn di_paren_alias_b_works() {
6799 let mut e = editor_with("f(arg)");
6800 e.jump_cursor(0, 3);
6801 run_keys(&mut e, "dib");
6802 assert_eq!(e.buffer().lines()[0], "f()");
6803 }
6804
6805 #[test]
6806 fn di_bracket_deletes_inside() {
6807 let mut e = editor_with("a[b,c]d");
6808 e.jump_cursor(0, 3);
6809 run_keys(&mut e, "di[");
6810 assert_eq!(e.buffer().lines()[0], "a[]d");
6811 }
6812
6813 #[test]
6814 fn da_bracket_deletes_around() {
6815 let mut e = editor_with("a[b,c]d");
6816 e.jump_cursor(0, 3);
6817 run_keys(&mut e, "da[");
6818 assert_eq!(e.buffer().lines()[0], "ad");
6819 }
6820
6821 #[test]
6822 fn di_brace_deletes_inside() {
6823 let mut e = editor_with("x{y}z");
6824 e.jump_cursor(0, 2);
6825 run_keys(&mut e, "di{");
6826 assert_eq!(e.buffer().lines()[0], "x{}z");
6827 }
6828
6829 #[test]
6830 fn da_brace_deletes_around() {
6831 let mut e = editor_with("x{y}z");
6832 e.jump_cursor(0, 2);
6833 run_keys(&mut e, "da{");
6834 assert_eq!(e.buffer().lines()[0], "xz");
6835 }
6836
6837 #[test]
6838 fn di_brace_alias_capital_b_works() {
6839 let mut e = editor_with("x{y}z");
6840 e.jump_cursor(0, 2);
6841 run_keys(&mut e, "diB");
6842 assert_eq!(e.buffer().lines()[0], "x{}z");
6843 }
6844
6845 #[test]
6846 fn di_angle_deletes_inside() {
6847 let mut e = editor_with("p<q>r");
6848 e.jump_cursor(0, 2);
6849 run_keys(&mut e, "di<lt>");
6851 assert_eq!(e.buffer().lines()[0], "p<>r");
6852 }
6853
6854 #[test]
6855 fn da_angle_deletes_around() {
6856 let mut e = editor_with("p<q>r");
6857 e.jump_cursor(0, 2);
6858 run_keys(&mut e, "da<lt>");
6859 assert_eq!(e.buffer().lines()[0], "pr");
6860 }
6861
6862 #[test]
6863 fn dip_deletes_inner_paragraph() {
6864 let mut e = editor_with("a\nb\nc\n\nd");
6865 e.jump_cursor(1, 0);
6866 run_keys(&mut e, "dip");
6867 assert_eq!(e.buffer().lines(), vec!["".to_string(), "d".into()]);
6870 }
6871
6872 #[test]
6875 fn sentence_motion_close_paren_jumps_forward() {
6876 let mut e = editor_with("Alpha. Beta. Gamma.");
6877 e.jump_cursor(0, 0);
6878 run_keys(&mut e, ")");
6879 assert_eq!(e.cursor(), (0, 7));
6881 run_keys(&mut e, ")");
6882 assert_eq!(e.cursor(), (0, 13));
6883 }
6884
6885 #[test]
6886 fn sentence_motion_open_paren_jumps_backward() {
6887 let mut e = editor_with("Alpha. Beta. Gamma.");
6888 e.jump_cursor(0, 13);
6889 run_keys(&mut e, "(");
6890 assert_eq!(e.cursor(), (0, 7));
6893 run_keys(&mut e, "(");
6894 assert_eq!(e.cursor(), (0, 0));
6895 }
6896
6897 #[test]
6898 fn sentence_motion_count() {
6899 let mut e = editor_with("A. B. C. D.");
6900 e.jump_cursor(0, 0);
6901 run_keys(&mut e, "3)");
6902 assert_eq!(e.cursor(), (0, 9));
6904 }
6905
6906 #[test]
6907 fn dis_deletes_inner_sentence() {
6908 let mut e = editor_with("First one. Second one. Third one.");
6909 e.jump_cursor(0, 13);
6910 run_keys(&mut e, "dis");
6911 assert_eq!(e.buffer().lines()[0], "First one. Third one.");
6913 }
6914
6915 #[test]
6916 fn das_deletes_around_sentence_with_trailing_space() {
6917 let mut e = editor_with("Alpha. Beta. Gamma.");
6918 e.jump_cursor(0, 8);
6919 run_keys(&mut e, "das");
6920 assert_eq!(e.buffer().lines()[0], "Alpha. Gamma.");
6923 }
6924
6925 #[test]
6926 fn dis_handles_double_terminator() {
6927 let mut e = editor_with("Wow!? Next.");
6928 e.jump_cursor(0, 1);
6929 run_keys(&mut e, "dis");
6930 assert_eq!(e.buffer().lines()[0], " Next.");
6933 }
6934
6935 #[test]
6936 fn dis_first_sentence_from_cursor_at_zero() {
6937 let mut e = editor_with("Alpha. Beta.");
6938 e.jump_cursor(0, 0);
6939 run_keys(&mut e, "dis");
6940 assert_eq!(e.buffer().lines()[0], " Beta.");
6941 }
6942
6943 #[test]
6944 fn yis_yanks_inner_sentence() {
6945 let mut e = editor_with("Hello world. Bye.");
6946 e.jump_cursor(0, 5);
6947 run_keys(&mut e, "yis");
6948 assert_eq!(e.registers().read('"').unwrap().text, "Hello world.");
6949 }
6950
6951 #[test]
6952 fn vis_visually_selects_inner_sentence() {
6953 let mut e = editor_with("First. Second.");
6954 e.jump_cursor(0, 1);
6955 run_keys(&mut e, "vis");
6956 assert_eq!(e.vim_mode(), VimMode::Visual);
6957 run_keys(&mut e, "y");
6958 assert_eq!(e.registers().read('"').unwrap().text, "First.");
6959 }
6960
6961 #[test]
6962 fn ciw_changes_inner_word() {
6963 let mut e = editor_with("hello world");
6964 e.jump_cursor(0, 1);
6965 run_keys(&mut e, "ciwHEY<Esc>");
6966 assert_eq!(e.buffer().lines()[0], "HEY world");
6967 }
6968
6969 #[test]
6970 fn yiw_yanks_inner_word() {
6971 let mut e = editor_with("hello world");
6972 e.jump_cursor(0, 1);
6973 run_keys(&mut e, "yiw");
6974 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6975 }
6976
6977 #[test]
6978 fn viw_selects_inner_word() {
6979 let mut e = editor_with("hello world");
6980 e.jump_cursor(0, 2);
6981 run_keys(&mut e, "viw");
6982 assert_eq!(e.vim_mode(), VimMode::Visual);
6983 run_keys(&mut e, "y");
6984 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6985 }
6986
6987 #[test]
6988 fn ci_paren_changes_inside() {
6989 let mut e = editor_with("f(old)");
6990 e.jump_cursor(0, 3);
6991 run_keys(&mut e, "ci(NEW<Esc>");
6992 assert_eq!(e.buffer().lines()[0], "f(NEW)");
6993 }
6994
6995 #[test]
6996 fn yi_double_quote_yanks_inside() {
6997 let mut e = editor_with("say \"hi there\" then");
6998 e.jump_cursor(0, 6);
6999 run_keys(&mut e, "yi\"");
7000 assert_eq!(e.registers().read('"').unwrap().text, "hi there");
7001 }
7002
7003 #[test]
7004 fn vap_visual_selects_around_paragraph() {
7005 let mut e = editor_with("a\nb\n\nc");
7006 e.jump_cursor(0, 0);
7007 run_keys(&mut e, "vap");
7008 assert_eq!(e.vim_mode(), VimMode::VisualLine);
7009 run_keys(&mut e, "y");
7010 let text = e.registers().read('"').unwrap().text.clone();
7012 assert!(text.starts_with("a\nb"));
7013 }
7014
7015 #[test]
7016 fn star_finds_next_occurrence() {
7017 let mut e = editor_with("foo bar foo baz");
7018 run_keys(&mut e, "*");
7019 assert_eq!(e.cursor().1, 8);
7020 }
7021
7022 #[test]
7023 fn star_skips_substring_match() {
7024 let mut e = editor_with("foo foobar baz");
7027 run_keys(&mut e, "*");
7028 assert_eq!(e.cursor().1, 0);
7029 }
7030
7031 #[test]
7032 fn g_star_matches_substring() {
7033 let mut e = editor_with("foo foobar baz");
7036 run_keys(&mut e, "g*");
7037 assert_eq!(e.cursor().1, 4);
7038 }
7039
7040 #[test]
7041 fn g_pound_matches_substring_backward() {
7042 let mut e = editor_with("foo foobar baz foo");
7045 run_keys(&mut e, "$b");
7046 assert_eq!(e.cursor().1, 15);
7047 run_keys(&mut e, "g#");
7048 assert_eq!(e.cursor().1, 4);
7049 }
7050
7051 #[test]
7052 fn n_repeats_last_search_forward() {
7053 let mut e = editor_with("foo bar foo baz foo");
7054 run_keys(&mut e, "/foo<CR>");
7057 assert_eq!(e.cursor().1, 8);
7058 run_keys(&mut e, "n");
7059 assert_eq!(e.cursor().1, 16);
7060 }
7061
7062 #[test]
7063 fn shift_n_reverses_search() {
7064 let mut e = editor_with("foo bar foo baz foo");
7065 run_keys(&mut e, "/foo<CR>");
7066 run_keys(&mut e, "n");
7067 assert_eq!(e.cursor().1, 16);
7068 run_keys(&mut e, "N");
7069 assert_eq!(e.cursor().1, 8);
7070 }
7071
7072 #[test]
7073 fn n_noop_without_pattern() {
7074 let mut e = editor_with("foo bar");
7075 run_keys(&mut e, "n");
7076 assert_eq!(e.cursor(), (0, 0));
7077 }
7078
7079 #[test]
7080 fn visual_line_preserves_cursor_column() {
7081 let mut e = editor_with("hello world\nanother one\nbye");
7084 run_keys(&mut e, "lllll"); run_keys(&mut e, "V");
7086 assert_eq!(e.vim_mode(), VimMode::VisualLine);
7087 assert_eq!(e.cursor(), (0, 5));
7088 run_keys(&mut e, "j");
7089 assert_eq!(e.cursor(), (1, 5));
7090 }
7091
7092 #[test]
7093 fn visual_line_yank_includes_trailing_newline() {
7094 let mut e = editor_with("aaa\nbbb\nccc");
7095 run_keys(&mut e, "Vjy");
7096 assert_eq!(e.last_yank.as_deref(), Some("aaa\nbbb\n"));
7098 }
7099
7100 #[test]
7101 fn visual_line_yank_last_line_trailing_newline() {
7102 let mut e = editor_with("aaa\nbbb\nccc");
7103 run_keys(&mut e, "jj");
7105 run_keys(&mut e, "Vy");
7106 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
7107 }
7108
7109 #[test]
7110 fn yy_on_last_line_has_trailing_newline() {
7111 let mut e = editor_with("aaa\nbbb\nccc");
7112 run_keys(&mut e, "jj");
7113 run_keys(&mut e, "yy");
7114 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
7115 }
7116
7117 #[test]
7118 fn yy_in_middle_has_trailing_newline() {
7119 let mut e = editor_with("aaa\nbbb\nccc");
7120 run_keys(&mut e, "j");
7121 run_keys(&mut e, "yy");
7122 assert_eq!(e.last_yank.as_deref(), Some("bbb\n"));
7123 }
7124
7125 #[test]
7126 fn di_single_quote() {
7127 let mut e = editor_with("say 'hello world' now");
7128 e.jump_cursor(0, 7);
7129 run_keys(&mut e, "di'");
7130 assert_eq!(e.buffer().lines()[0], "say '' now");
7131 }
7132
7133 #[test]
7134 fn da_single_quote() {
7135 let mut e = editor_with("say 'hello' now");
7137 e.jump_cursor(0, 7);
7138 run_keys(&mut e, "da'");
7139 assert_eq!(e.buffer().lines()[0], "say now");
7140 }
7141
7142 #[test]
7143 fn di_backtick() {
7144 let mut e = editor_with("say `hi` now");
7145 e.jump_cursor(0, 5);
7146 run_keys(&mut e, "di`");
7147 assert_eq!(e.buffer().lines()[0], "say `` now");
7148 }
7149
7150 #[test]
7151 fn di_brace() {
7152 let mut e = editor_with("fn { a; b; c }");
7153 e.jump_cursor(0, 7);
7154 run_keys(&mut e, "di{");
7155 assert_eq!(e.buffer().lines()[0], "fn {}");
7156 }
7157
7158 #[test]
7159 fn di_bracket() {
7160 let mut e = editor_with("arr[1, 2, 3]");
7161 e.jump_cursor(0, 5);
7162 run_keys(&mut e, "di[");
7163 assert_eq!(e.buffer().lines()[0], "arr[]");
7164 }
7165
7166 #[test]
7167 fn dab_deletes_around_paren() {
7168 let mut e = editor_with("fn(a, b) + 1");
7169 e.jump_cursor(0, 4);
7170 run_keys(&mut e, "dab");
7171 assert_eq!(e.buffer().lines()[0], "fn + 1");
7172 }
7173
7174 #[test]
7175 fn da_big_b_deletes_around_brace() {
7176 let mut e = editor_with("x = {a: 1}");
7177 e.jump_cursor(0, 6);
7178 run_keys(&mut e, "daB");
7179 assert_eq!(e.buffer().lines()[0], "x = ");
7180 }
7181
7182 #[test]
7183 fn di_big_w_deletes_bigword() {
7184 let mut e = editor_with("foo-bar baz");
7185 e.jump_cursor(0, 2);
7186 run_keys(&mut e, "diW");
7187 assert_eq!(e.buffer().lines()[0], " baz");
7188 }
7189
7190 #[test]
7191 fn visual_select_inner_word() {
7192 let mut e = editor_with("hello world");
7193 e.jump_cursor(0, 2);
7194 run_keys(&mut e, "viw");
7195 assert_eq!(e.vim_mode(), VimMode::Visual);
7196 run_keys(&mut e, "y");
7197 assert_eq!(e.last_yank.as_deref(), Some("hello"));
7198 }
7199
7200 #[test]
7201 fn visual_select_inner_quote() {
7202 let mut e = editor_with("foo \"bar\" baz");
7203 e.jump_cursor(0, 6);
7204 run_keys(&mut e, "vi\"");
7205 run_keys(&mut e, "y");
7206 assert_eq!(e.last_yank.as_deref(), Some("bar"));
7207 }
7208
7209 #[test]
7210 fn visual_select_inner_paren() {
7211 let mut e = editor_with("fn(a, b)");
7212 e.jump_cursor(0, 4);
7213 run_keys(&mut e, "vi(");
7214 run_keys(&mut e, "y");
7215 assert_eq!(e.last_yank.as_deref(), Some("a, b"));
7216 }
7217
7218 #[test]
7219 fn visual_select_outer_brace() {
7220 let mut e = editor_with("{x}");
7221 e.jump_cursor(0, 1);
7222 run_keys(&mut e, "va{");
7223 run_keys(&mut e, "y");
7224 assert_eq!(e.last_yank.as_deref(), Some("{x}"));
7225 }
7226
7227 #[test]
7228 fn ci_paren_forward_scans_when_cursor_before_pair() {
7229 let mut e = editor_with("foo(bar)");
7232 e.jump_cursor(0, 0);
7233 run_keys(&mut e, "ci(NEW<Esc>");
7234 assert_eq!(e.buffer().lines()[0], "foo(NEW)");
7235 }
7236
7237 #[test]
7238 fn ci_paren_forward_scans_across_lines() {
7239 let mut e = editor_with("first\nfoo(bar)\nlast");
7240 e.jump_cursor(0, 0);
7241 run_keys(&mut e, "ci(NEW<Esc>");
7242 assert_eq!(e.buffer().lines()[1], "foo(NEW)");
7243 }
7244
7245 #[test]
7246 fn ci_brace_forward_scans_when_cursor_before_pair() {
7247 let mut e = editor_with("let x = {y};");
7248 e.jump_cursor(0, 0);
7249 run_keys(&mut e, "ci{NEW<Esc>");
7250 assert_eq!(e.buffer().lines()[0], "let x = {NEW};");
7251 }
7252
7253 #[test]
7254 fn cit_forward_scans_when_cursor_before_tag() {
7255 let mut e = editor_with("text <b>hello</b> rest");
7258 e.jump_cursor(0, 0);
7259 run_keys(&mut e, "citNEW<Esc>");
7260 assert_eq!(e.buffer().lines()[0], "text <b>NEW</b> rest");
7261 }
7262
7263 #[test]
7264 fn dat_forward_scans_when_cursor_before_tag() {
7265 let mut e = editor_with("text <b>hello</b> rest");
7267 e.jump_cursor(0, 0);
7268 run_keys(&mut e, "dat");
7269 assert_eq!(e.buffer().lines()[0], "text rest");
7270 }
7271
7272 #[test]
7273 fn ci_paren_still_works_when_cursor_inside() {
7274 let mut e = editor_with("fn(a, b)");
7277 e.jump_cursor(0, 4);
7278 run_keys(&mut e, "ci(NEW<Esc>");
7279 assert_eq!(e.buffer().lines()[0], "fn(NEW)");
7280 }
7281
7282 #[test]
7283 fn caw_changes_word_with_trailing_space() {
7284 let mut e = editor_with("hello world");
7285 run_keys(&mut e, "cawfoo<Esc>");
7286 assert_eq!(e.buffer().lines()[0], "fooworld");
7287 }
7288
7289 #[test]
7290 fn visual_char_yank_preserves_raw_text() {
7291 let mut e = editor_with("hello world");
7292 run_keys(&mut e, "vllly");
7293 assert_eq!(e.last_yank.as_deref(), Some("hell"));
7294 }
7295
7296 #[test]
7297 fn single_line_visual_line_selects_full_line_on_yank() {
7298 let mut e = editor_with("hello world\nbye");
7299 run_keys(&mut e, "V");
7300 run_keys(&mut e, "y");
7303 assert_eq!(e.last_yank.as_deref(), Some("hello world\n"));
7304 }
7305
7306 #[test]
7307 fn visual_line_extends_both_directions() {
7308 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7309 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
7311 assert_eq!(e.cursor(), (3, 0));
7312 run_keys(&mut e, "k");
7313 assert_eq!(e.cursor(), (2, 0));
7315 run_keys(&mut e, "k");
7316 assert_eq!(e.cursor(), (1, 0));
7317 }
7318
7319 #[test]
7320 fn visual_char_preserves_cursor_column() {
7321 let mut e = editor_with("hello world");
7322 run_keys(&mut e, "lllll"); run_keys(&mut e, "v");
7324 assert_eq!(e.cursor(), (0, 5));
7325 run_keys(&mut e, "ll");
7326 assert_eq!(e.cursor(), (0, 7));
7327 }
7328
7329 #[test]
7330 fn visual_char_highlight_bounds_order() {
7331 let mut e = editor_with("abcdef");
7332 run_keys(&mut e, "lll"); run_keys(&mut e, "v");
7334 run_keys(&mut e, "hh"); assert_eq!(e.char_highlight(), Some(((0, 1), (0, 3))));
7337 }
7338
7339 #[test]
7340 fn visual_line_highlight_bounds() {
7341 let mut e = editor_with("a\nb\nc");
7342 run_keys(&mut e, "V");
7343 assert_eq!(e.line_highlight(), Some((0, 0)));
7344 run_keys(&mut e, "j");
7345 assert_eq!(e.line_highlight(), Some((0, 1)));
7346 run_keys(&mut e, "j");
7347 assert_eq!(e.line_highlight(), Some((0, 2)));
7348 }
7349
7350 #[test]
7353 fn h_moves_left() {
7354 let mut e = editor_with("hello");
7355 e.jump_cursor(0, 3);
7356 run_keys(&mut e, "h");
7357 assert_eq!(e.cursor(), (0, 2));
7358 }
7359
7360 #[test]
7361 fn l_moves_right() {
7362 let mut e = editor_with("hello");
7363 run_keys(&mut e, "l");
7364 assert_eq!(e.cursor(), (0, 1));
7365 }
7366
7367 #[test]
7368 fn k_moves_up() {
7369 let mut e = editor_with("a\nb\nc");
7370 e.jump_cursor(2, 0);
7371 run_keys(&mut e, "k");
7372 assert_eq!(e.cursor(), (1, 0));
7373 }
7374
7375 #[test]
7376 fn zero_moves_to_line_start() {
7377 let mut e = editor_with(" hello");
7378 run_keys(&mut e, "$");
7379 run_keys(&mut e, "0");
7380 assert_eq!(e.cursor().1, 0);
7381 }
7382
7383 #[test]
7384 fn caret_moves_to_first_non_blank() {
7385 let mut e = editor_with(" hello");
7386 run_keys(&mut e, "0");
7387 run_keys(&mut e, "^");
7388 assert_eq!(e.cursor().1, 4);
7389 }
7390
7391 #[test]
7392 fn dollar_moves_to_last_char() {
7393 let mut e = editor_with("hello");
7394 run_keys(&mut e, "$");
7395 assert_eq!(e.cursor().1, 4);
7396 }
7397
7398 #[test]
7399 fn dollar_on_empty_line_stays_at_col_zero() {
7400 let mut e = editor_with("");
7401 run_keys(&mut e, "$");
7402 assert_eq!(e.cursor().1, 0);
7403 }
7404
7405 #[test]
7406 fn w_jumps_to_next_word() {
7407 let mut e = editor_with("foo bar baz");
7408 run_keys(&mut e, "w");
7409 assert_eq!(e.cursor().1, 4);
7410 }
7411
7412 #[test]
7413 fn b_jumps_back_a_word() {
7414 let mut e = editor_with("foo bar");
7415 e.jump_cursor(0, 6);
7416 run_keys(&mut e, "b");
7417 assert_eq!(e.cursor().1, 4);
7418 }
7419
7420 #[test]
7421 fn e_jumps_to_word_end() {
7422 let mut e = editor_with("foo bar");
7423 run_keys(&mut e, "e");
7424 assert_eq!(e.cursor().1, 2);
7425 }
7426
7427 #[test]
7430 fn d_dollar_deletes_to_eol() {
7431 let mut e = editor_with("hello world");
7432 e.jump_cursor(0, 5);
7433 run_keys(&mut e, "d$");
7434 assert_eq!(e.buffer().lines()[0], "hello");
7435 }
7436
7437 #[test]
7438 fn d_zero_deletes_to_line_start() {
7439 let mut e = editor_with("hello world");
7440 e.jump_cursor(0, 6);
7441 run_keys(&mut e, "d0");
7442 assert_eq!(e.buffer().lines()[0], "world");
7443 }
7444
7445 #[test]
7446 fn d_caret_deletes_to_first_non_blank() {
7447 let mut e = editor_with(" hello");
7448 e.jump_cursor(0, 6);
7449 run_keys(&mut e, "d^");
7450 assert_eq!(e.buffer().lines()[0], " llo");
7451 }
7452
7453 #[test]
7454 fn d_capital_g_deletes_to_end_of_file() {
7455 let mut e = editor_with("a\nb\nc\nd");
7456 e.jump_cursor(1, 0);
7457 run_keys(&mut e, "dG");
7458 assert_eq!(e.buffer().lines(), &["a".to_string()]);
7459 }
7460
7461 #[test]
7462 fn d_gg_deletes_to_start_of_file() {
7463 let mut e = editor_with("a\nb\nc\nd");
7464 e.jump_cursor(2, 0);
7465 run_keys(&mut e, "dgg");
7466 assert_eq!(e.buffer().lines(), &["d".to_string()]);
7467 }
7468
7469 #[test]
7470 fn cw_is_ce_quirk() {
7471 let mut e = editor_with("foo bar");
7474 run_keys(&mut e, "cwxyz<Esc>");
7475 assert_eq!(e.buffer().lines()[0], "xyz bar");
7476 }
7477
7478 #[test]
7481 fn big_d_deletes_to_eol() {
7482 let mut e = editor_with("hello world");
7483 e.jump_cursor(0, 5);
7484 run_keys(&mut e, "D");
7485 assert_eq!(e.buffer().lines()[0], "hello");
7486 }
7487
7488 #[test]
7489 fn big_c_deletes_to_eol_and_inserts() {
7490 let mut e = editor_with("hello world");
7491 e.jump_cursor(0, 5);
7492 run_keys(&mut e, "C!<Esc>");
7493 assert_eq!(e.buffer().lines()[0], "hello!");
7494 }
7495
7496 #[test]
7497 fn j_joins_next_line_with_space() {
7498 let mut e = editor_with("hello\nworld");
7499 run_keys(&mut e, "J");
7500 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7501 }
7502
7503 #[test]
7504 fn j_strips_leading_whitespace_on_join() {
7505 let mut e = editor_with("hello\n world");
7506 run_keys(&mut e, "J");
7507 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7508 }
7509
7510 #[test]
7511 fn big_x_deletes_char_before_cursor() {
7512 let mut e = editor_with("hello");
7513 e.jump_cursor(0, 3);
7514 run_keys(&mut e, "X");
7515 assert_eq!(e.buffer().lines()[0], "helo");
7516 }
7517
7518 #[test]
7519 fn s_substitutes_char_and_enters_insert() {
7520 let mut e = editor_with("hello");
7521 run_keys(&mut e, "sX<Esc>");
7522 assert_eq!(e.buffer().lines()[0], "Xello");
7523 }
7524
7525 #[test]
7526 fn count_x_deletes_many() {
7527 let mut e = editor_with("abcdef");
7528 run_keys(&mut e, "3x");
7529 assert_eq!(e.buffer().lines()[0], "def");
7530 }
7531
7532 #[test]
7535 fn p_pastes_charwise_after_cursor() {
7536 let mut e = editor_with("hello");
7537 run_keys(&mut e, "yw");
7538 run_keys(&mut e, "$p");
7539 assert_eq!(e.buffer().lines()[0], "hellohello");
7540 }
7541
7542 #[test]
7543 fn capital_p_pastes_charwise_before_cursor() {
7544 let mut e = editor_with("hello");
7545 run_keys(&mut e, "v");
7547 run_keys(&mut e, "l");
7548 run_keys(&mut e, "y");
7549 run_keys(&mut e, "$P");
7550 assert_eq!(e.buffer().lines()[0], "hellheo");
7553 }
7554
7555 #[test]
7556 fn p_pastes_linewise_below() {
7557 let mut e = editor_with("one\ntwo\nthree");
7558 run_keys(&mut e, "yy");
7559 run_keys(&mut e, "p");
7560 assert_eq!(
7561 e.buffer().lines(),
7562 &[
7563 "one".to_string(),
7564 "one".to_string(),
7565 "two".to_string(),
7566 "three".to_string()
7567 ]
7568 );
7569 }
7570
7571 #[test]
7572 fn capital_p_pastes_linewise_above() {
7573 let mut e = editor_with("one\ntwo");
7574 e.jump_cursor(1, 0);
7575 run_keys(&mut e, "yy");
7576 run_keys(&mut e, "P");
7577 assert_eq!(
7578 e.buffer().lines(),
7579 &["one".to_string(), "two".to_string(), "two".to_string()]
7580 );
7581 }
7582
7583 #[test]
7586 fn hash_finds_previous_occurrence() {
7587 let mut e = editor_with("foo bar foo baz foo");
7588 e.jump_cursor(0, 16);
7590 run_keys(&mut e, "#");
7591 assert_eq!(e.cursor().1, 8);
7592 }
7593
7594 #[test]
7597 fn visual_line_delete_removes_full_lines() {
7598 let mut e = editor_with("a\nb\nc\nd");
7599 run_keys(&mut e, "Vjd");
7600 assert_eq!(e.buffer().lines(), &["c".to_string(), "d".to_string()]);
7601 }
7602
7603 #[test]
7604 fn visual_line_change_leaves_blank_line() {
7605 let mut e = editor_with("a\nb\nc");
7606 run_keys(&mut e, "Vjc");
7607 assert_eq!(e.vim_mode(), VimMode::Insert);
7608 run_keys(&mut e, "X<Esc>");
7609 assert_eq!(e.buffer().lines(), &["X".to_string(), "c".to_string()]);
7613 }
7614
7615 #[test]
7616 fn cc_leaves_blank_line() {
7617 let mut e = editor_with("a\nb\nc");
7618 e.jump_cursor(1, 0);
7619 run_keys(&mut e, "ccX<Esc>");
7620 assert_eq!(
7621 e.buffer().lines(),
7622 &["a".to_string(), "X".to_string(), "c".to_string()]
7623 );
7624 }
7625
7626 #[test]
7631 fn big_w_skips_hyphens() {
7632 let mut e = editor_with("foo-bar baz");
7634 run_keys(&mut e, "W");
7635 assert_eq!(e.cursor().1, 8);
7636 }
7637
7638 #[test]
7639 fn big_w_crosses_lines() {
7640 let mut e = editor_with("foo-bar\nbaz-qux");
7641 run_keys(&mut e, "W");
7642 assert_eq!(e.cursor(), (1, 0));
7643 }
7644
7645 #[test]
7646 fn big_b_skips_hyphens() {
7647 let mut e = editor_with("foo-bar baz");
7648 e.jump_cursor(0, 9);
7649 run_keys(&mut e, "B");
7650 assert_eq!(e.cursor().1, 8);
7651 run_keys(&mut e, "B");
7652 assert_eq!(e.cursor().1, 0);
7653 }
7654
7655 #[test]
7656 fn big_e_jumps_to_big_word_end() {
7657 let mut e = editor_with("foo-bar baz");
7658 run_keys(&mut e, "E");
7659 assert_eq!(e.cursor().1, 6);
7660 run_keys(&mut e, "E");
7661 assert_eq!(e.cursor().1, 10);
7662 }
7663
7664 #[test]
7665 fn dw_with_big_word_variant() {
7666 let mut e = editor_with("foo-bar baz");
7668 run_keys(&mut e, "dW");
7669 assert_eq!(e.buffer().lines()[0], "baz");
7670 }
7671
7672 #[test]
7675 fn insert_ctrl_w_deletes_word_back() {
7676 let mut e = editor_with("");
7677 run_keys(&mut e, "i");
7678 for c in "hello world".chars() {
7679 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7680 }
7681 run_keys(&mut e, "<C-w>");
7682 assert_eq!(e.buffer().lines()[0], "hello ");
7683 }
7684
7685 #[test]
7686 fn insert_ctrl_w_at_col0_joins_with_prev_word() {
7687 let mut e = editor_with("hello\nworld");
7691 e.jump_cursor(1, 0);
7692 run_keys(&mut e, "i");
7693 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7694 assert_eq!(e.buffer().lines(), vec!["world".to_string()]);
7697 assert_eq!(e.cursor(), (0, 0));
7698 }
7699
7700 #[test]
7701 fn insert_ctrl_w_at_col0_keeps_prefix_words() {
7702 let mut e = editor_with("foo bar\nbaz");
7703 e.jump_cursor(1, 0);
7704 run_keys(&mut e, "i");
7705 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7706 assert_eq!(e.buffer().lines(), vec!["foo baz".to_string()]);
7708 assert_eq!(e.cursor(), (0, 4));
7709 }
7710
7711 #[test]
7712 fn insert_ctrl_u_deletes_to_line_start() {
7713 let mut e = editor_with("");
7714 run_keys(&mut e, "i");
7715 for c in "hello world".chars() {
7716 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7717 }
7718 run_keys(&mut e, "<C-u>");
7719 assert_eq!(e.buffer().lines()[0], "");
7720 }
7721
7722 #[test]
7723 fn insert_ctrl_o_runs_one_normal_command() {
7724 let mut e = editor_with("hello world");
7725 run_keys(&mut e, "A");
7727 assert_eq!(e.vim_mode(), VimMode::Insert);
7728 e.jump_cursor(0, 0);
7730 run_keys(&mut e, "<C-o>");
7731 assert_eq!(e.vim_mode(), VimMode::Normal);
7732 run_keys(&mut e, "dw");
7733 assert_eq!(e.vim_mode(), VimMode::Insert);
7735 assert_eq!(e.buffer().lines()[0], "world");
7736 }
7737
7738 #[test]
7741 fn j_through_empty_line_preserves_column() {
7742 let mut e = editor_with("hello world\n\nanother line");
7743 run_keys(&mut e, "llllll");
7745 assert_eq!(e.cursor(), (0, 6));
7746 run_keys(&mut e, "j");
7749 assert_eq!(e.cursor(), (1, 0));
7750 run_keys(&mut e, "j");
7752 assert_eq!(e.cursor(), (2, 6));
7753 }
7754
7755 #[test]
7756 fn j_through_shorter_line_preserves_column() {
7757 let mut e = editor_with("hello world\nhi\nanother line");
7758 run_keys(&mut e, "lllllll"); run_keys(&mut e, "j"); assert_eq!(e.cursor(), (1, 1));
7761 run_keys(&mut e, "j");
7762 assert_eq!(e.cursor(), (2, 7));
7763 }
7764
7765 #[test]
7766 fn esc_from_insert_sticky_matches_visible_cursor() {
7767 let mut e = editor_with(" this is a line\n another one of a similar size");
7771 e.jump_cursor(0, 12);
7772 run_keys(&mut e, "I");
7773 assert_eq!(e.cursor(), (0, 4));
7774 run_keys(&mut e, "X<Esc>");
7775 assert_eq!(e.cursor(), (0, 4));
7776 run_keys(&mut e, "j");
7777 assert_eq!(e.cursor(), (1, 4));
7778 }
7779
7780 #[test]
7781 fn esc_from_insert_sticky_tracks_inserted_chars() {
7782 let mut e = editor_with("xxxxxxx\nyyyyyyy");
7783 run_keys(&mut e, "i");
7784 run_keys(&mut e, "abc<Esc>");
7785 assert_eq!(e.cursor(), (0, 2));
7786 run_keys(&mut e, "j");
7787 assert_eq!(e.cursor(), (1, 2));
7788 }
7789
7790 #[test]
7791 fn esc_from_insert_sticky_tracks_arrow_nav() {
7792 let mut e = editor_with("xxxxxx\nyyyyyy");
7793 run_keys(&mut e, "i");
7794 run_keys(&mut e, "abc");
7795 for _ in 0..2 {
7796 e.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
7797 }
7798 run_keys(&mut e, "<Esc>");
7799 assert_eq!(e.cursor(), (0, 0));
7800 run_keys(&mut e, "j");
7801 assert_eq!(e.cursor(), (1, 0));
7802 }
7803
7804 #[test]
7805 fn esc_from_insert_at_col_14_followed_by_j() {
7806 let line = "x".repeat(30);
7809 let buf = format!("{line}\n{line}");
7810 let mut e = editor_with(&buf);
7811 e.jump_cursor(0, 14);
7812 run_keys(&mut e, "i");
7813 for c in "test ".chars() {
7814 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7815 }
7816 run_keys(&mut e, "<Esc>");
7817 assert_eq!(e.cursor(), (0, 18));
7818 run_keys(&mut e, "j");
7819 assert_eq!(e.cursor(), (1, 18));
7820 }
7821
7822 #[test]
7823 fn linewise_paste_resets_sticky_column() {
7824 let mut e = editor_with(" hello\naaaaaaaa\nbye");
7828 run_keys(&mut e, "llllll"); run_keys(&mut e, "yy");
7830 run_keys(&mut e, "j"); run_keys(&mut e, "p"); assert_eq!(e.cursor(), (2, 4));
7834 run_keys(&mut e, "j");
7836 assert_eq!(e.cursor(), (3, 2));
7837 }
7838
7839 #[test]
7840 fn horizontal_motion_resyncs_sticky_column() {
7841 let mut e = editor_with("hello world\n\nanother line");
7845 run_keys(&mut e, "llllll"); run_keys(&mut e, "hhh"); run_keys(&mut e, "jj");
7848 assert_eq!(e.cursor(), (2, 3));
7849 }
7850
7851 #[test]
7854 fn ctrl_v_enters_visual_block() {
7855 let mut e = editor_with("aaa\nbbb\nccc");
7856 run_keys(&mut e, "<C-v>");
7857 assert_eq!(e.vim_mode(), VimMode::VisualBlock);
7858 }
7859
7860 #[test]
7861 fn visual_block_esc_returns_to_normal() {
7862 let mut e = editor_with("aaa\nbbb\nccc");
7863 run_keys(&mut e, "<C-v>");
7864 run_keys(&mut e, "<Esc>");
7865 assert_eq!(e.vim_mode(), VimMode::Normal);
7866 }
7867
7868 #[test]
7869 fn backtick_lt_jumps_to_visual_start_mark() {
7870 let mut e = editor_with("foo bar baz\n");
7874 run_keys(&mut e, "v");
7875 run_keys(&mut e, "w"); run_keys(&mut e, "<Esc>"); assert_eq!(e.cursor(), (0, 4));
7878 run_keys(&mut e, "`<lt>");
7880 assert_eq!(e.cursor(), (0, 0));
7881 }
7882
7883 #[test]
7884 fn backtick_gt_jumps_to_visual_end_mark() {
7885 let mut e = editor_with("foo bar baz\n");
7886 run_keys(&mut e, "v");
7887 run_keys(&mut e, "w"); run_keys(&mut e, "<Esc>");
7889 run_keys(&mut e, "0"); run_keys(&mut e, "`>");
7891 assert_eq!(e.cursor(), (0, 4));
7892 }
7893
7894 #[test]
7895 fn visual_exit_sets_lt_gt_marks() {
7896 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7899 run_keys(&mut e, "V");
7901 run_keys(&mut e, "j");
7902 run_keys(&mut e, "<Esc>");
7903 let lt = e.mark('<').expect("'<' mark must be set on visual exit");
7904 let gt = e.mark('>').expect("'>' mark must be set on visual exit");
7905 assert_eq!(lt.0, 0, "'< row should be the lower bound");
7906 assert_eq!(gt.0, 1, "'> row should be the upper bound");
7907 }
7908
7909 #[test]
7910 fn visual_exit_marks_use_lower_higher_order() {
7911 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7915 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
7917 run_keys(&mut e, "k"); run_keys(&mut e, "<Esc>");
7919 let lt = e.mark('<').unwrap();
7920 let gt = e.mark('>').unwrap();
7921 assert_eq!(lt.0, 2);
7922 assert_eq!(gt.0, 3);
7923 }
7924
7925 #[test]
7926 fn visualline_exit_marks_snap_to_line_edges() {
7927 let mut e = editor_with("aaaaa\nbbbbb\ncc");
7929 run_keys(&mut e, "lll"); run_keys(&mut e, "V");
7931 run_keys(&mut e, "j"); run_keys(&mut e, "<Esc>");
7933 let lt = e.mark('<').unwrap();
7934 let gt = e.mark('>').unwrap();
7935 assert_eq!(lt, (0, 0), "'< should snap to (top_row, 0)");
7936 assert_eq!(gt, (1, 4), "'> should snap to (bot_row, last_col)");
7938 }
7939
7940 #[test]
7941 fn visualblock_exit_marks_use_block_corners() {
7942 let mut e = editor_with("aaaaa\nbbbbb\nccccc");
7946 run_keys(&mut e, "llll"); run_keys(&mut e, "<C-v>");
7948 run_keys(&mut e, "j"); run_keys(&mut e, "hh"); run_keys(&mut e, "<Esc>");
7951 let lt = e.mark('<').unwrap();
7952 let gt = e.mark('>').unwrap();
7953 assert_eq!(lt, (0, 2), "'< should be top-left corner");
7955 assert_eq!(gt, (1, 4), "'> should be bottom-right corner");
7956 }
7957
7958 #[test]
7959 fn visual_block_delete_removes_column_range() {
7960 let mut e = editor_with("hello\nworld\nhappy");
7961 run_keys(&mut e, "l");
7963 run_keys(&mut e, "<C-v>");
7964 run_keys(&mut e, "jj");
7965 run_keys(&mut e, "ll");
7966 run_keys(&mut e, "d");
7967 assert_eq!(
7969 e.buffer().lines(),
7970 &["ho".to_string(), "wd".to_string(), "hy".to_string()]
7971 );
7972 }
7973
7974 #[test]
7975 fn visual_block_yank_joins_with_newlines() {
7976 let mut e = editor_with("hello\nworld\nhappy");
7977 run_keys(&mut e, "<C-v>");
7978 run_keys(&mut e, "jj");
7979 run_keys(&mut e, "ll");
7980 run_keys(&mut e, "y");
7981 assert_eq!(e.last_yank.as_deref(), Some("hel\nwor\nhap"));
7982 }
7983
7984 #[test]
7985 fn visual_block_replace_fills_block() {
7986 let mut e = editor_with("hello\nworld\nhappy");
7987 run_keys(&mut e, "<C-v>");
7988 run_keys(&mut e, "jj");
7989 run_keys(&mut e, "ll");
7990 run_keys(&mut e, "rx");
7991 assert_eq!(
7992 e.buffer().lines(),
7993 &[
7994 "xxxlo".to_string(),
7995 "xxxld".to_string(),
7996 "xxxpy".to_string()
7997 ]
7998 );
7999 }
8000
8001 #[test]
8002 fn visual_block_insert_repeats_across_rows() {
8003 let mut e = editor_with("hello\nworld\nhappy");
8004 run_keys(&mut e, "<C-v>");
8005 run_keys(&mut e, "jj");
8006 run_keys(&mut e, "I");
8007 run_keys(&mut e, "# <Esc>");
8008 assert_eq!(
8009 e.buffer().lines(),
8010 &[
8011 "# hello".to_string(),
8012 "# world".to_string(),
8013 "# happy".to_string()
8014 ]
8015 );
8016 }
8017
8018 #[test]
8019 fn block_highlight_returns_none_outside_block_mode() {
8020 let mut e = editor_with("abc");
8021 assert!(e.block_highlight().is_none());
8022 run_keys(&mut e, "v");
8023 assert!(e.block_highlight().is_none());
8024 run_keys(&mut e, "<Esc>V");
8025 assert!(e.block_highlight().is_none());
8026 }
8027
8028 #[test]
8029 fn block_highlight_bounds_track_anchor_and_cursor() {
8030 let mut e = editor_with("aaaa\nbbbb\ncccc");
8031 run_keys(&mut e, "ll"); run_keys(&mut e, "<C-v>");
8033 run_keys(&mut e, "jh"); assert_eq!(e.block_highlight(), Some((0, 1, 1, 2)));
8036 }
8037
8038 #[test]
8039 fn visual_block_delete_handles_short_lines() {
8040 let mut e = editor_with("hello\nhi\nworld");
8042 run_keys(&mut e, "l"); run_keys(&mut e, "<C-v>");
8044 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
8046 assert_eq!(
8051 e.buffer().lines(),
8052 &["ho".to_string(), "h".to_string(), "wd".to_string()]
8053 );
8054 }
8055
8056 #[test]
8057 fn visual_block_yank_pads_short_lines_with_empties() {
8058 let mut e = editor_with("hello\nhi\nworld");
8059 run_keys(&mut e, "l");
8060 run_keys(&mut e, "<C-v>");
8061 run_keys(&mut e, "jjll");
8062 run_keys(&mut e, "y");
8063 assert_eq!(e.last_yank.as_deref(), Some("ell\ni\norl"));
8065 }
8066
8067 #[test]
8068 fn visual_block_replace_skips_past_eol() {
8069 let mut e = editor_with("ab\ncd\nef");
8072 run_keys(&mut e, "l");
8074 run_keys(&mut e, "<C-v>");
8075 run_keys(&mut e, "jjllllll");
8076 run_keys(&mut e, "rX");
8077 assert_eq!(
8080 e.buffer().lines(),
8081 &["aX".to_string(), "cX".to_string(), "eX".to_string()]
8082 );
8083 }
8084
8085 #[test]
8086 fn visual_block_with_empty_line_in_middle() {
8087 let mut e = editor_with("abcd\n\nefgh");
8088 run_keys(&mut e, "<C-v>");
8089 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
8091 assert_eq!(
8094 e.buffer().lines(),
8095 &["d".to_string(), "".to_string(), "h".to_string()]
8096 );
8097 }
8098
8099 #[test]
8100 fn block_insert_pads_empty_lines_to_block_column() {
8101 let mut e = editor_with("this is a line\n\nthis is a line");
8104 e.jump_cursor(0, 3);
8105 run_keys(&mut e, "<C-v>");
8106 run_keys(&mut e, "jj");
8107 run_keys(&mut e, "I");
8108 run_keys(&mut e, "XX<Esc>");
8109 assert_eq!(
8110 e.buffer().lines(),
8111 &[
8112 "thiXXs is a line".to_string(),
8113 " XX".to_string(),
8114 "thiXXs is a line".to_string()
8115 ]
8116 );
8117 }
8118
8119 #[test]
8120 fn block_insert_pads_short_lines_to_block_column() {
8121 let mut e = editor_with("aaaaa\nbb\naaaaa");
8122 e.jump_cursor(0, 3);
8123 run_keys(&mut e, "<C-v>");
8124 run_keys(&mut e, "jj");
8125 run_keys(&mut e, "I");
8126 run_keys(&mut e, "Y<Esc>");
8127 assert_eq!(
8129 e.buffer().lines(),
8130 &[
8131 "aaaYaa".to_string(),
8132 "bb Y".to_string(),
8133 "aaaYaa".to_string()
8134 ]
8135 );
8136 }
8137
8138 #[test]
8139 fn visual_block_append_repeats_across_rows() {
8140 let mut e = editor_with("foo\nbar\nbaz");
8141 run_keys(&mut e, "<C-v>");
8142 run_keys(&mut e, "jj");
8143 run_keys(&mut e, "A");
8146 run_keys(&mut e, "!<Esc>");
8147 assert_eq!(
8148 e.buffer().lines(),
8149 &["f!oo".to_string(), "b!ar".to_string(), "b!az".to_string()]
8150 );
8151 }
8152
8153 #[test]
8156 fn slash_opens_forward_search_prompt() {
8157 let mut e = editor_with("hello world");
8158 run_keys(&mut e, "/");
8159 let p = e.search_prompt().expect("prompt should be active");
8160 assert!(p.text.is_empty());
8161 assert!(p.forward);
8162 }
8163
8164 #[test]
8165 fn question_opens_backward_search_prompt() {
8166 let mut e = editor_with("hello world");
8167 run_keys(&mut e, "?");
8168 let p = e.search_prompt().expect("prompt should be active");
8169 assert!(!p.forward);
8170 }
8171
8172 #[test]
8173 fn search_prompt_typing_updates_pattern_live() {
8174 let mut e = editor_with("foo bar\nbaz");
8175 run_keys(&mut e, "/bar");
8176 assert_eq!(e.search_prompt().unwrap().text, "bar");
8177 assert!(e.search_state().pattern.is_some());
8179 }
8180
8181 #[test]
8182 fn search_prompt_backspace_and_enter() {
8183 let mut e = editor_with("hello world\nagain");
8184 run_keys(&mut e, "/worlx");
8185 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
8186 assert_eq!(e.search_prompt().unwrap().text, "worl");
8187 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8188 assert!(e.search_prompt().is_none());
8190 assert_eq!(e.last_search(), Some("worl"));
8191 assert_eq!(e.cursor(), (0, 6));
8192 }
8193
8194 #[test]
8195 fn empty_search_prompt_enter_repeats_last_search() {
8196 let mut e = editor_with("foo bar foo baz foo");
8197 run_keys(&mut e, "/foo");
8198 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8199 assert_eq!(e.cursor().1, 8);
8200 run_keys(&mut e, "/");
8202 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8203 assert_eq!(e.cursor().1, 16);
8204 assert_eq!(e.last_search(), Some("foo"));
8205 }
8206
8207 #[test]
8208 fn search_history_records_committed_patterns() {
8209 let mut e = editor_with("alpha beta gamma");
8210 run_keys(&mut e, "/alpha<CR>");
8211 run_keys(&mut e, "/beta<CR>");
8212 let history = e.vim.search_history.clone();
8214 assert_eq!(history, vec!["alpha", "beta"]);
8215 }
8216
8217 #[test]
8218 fn search_history_dedupes_consecutive_repeats() {
8219 let mut e = editor_with("foo bar foo");
8220 run_keys(&mut e, "/foo<CR>");
8221 run_keys(&mut e, "/foo<CR>");
8222 run_keys(&mut e, "/bar<CR>");
8223 run_keys(&mut e, "/bar<CR>");
8224 assert_eq!(e.vim.search_history.clone(), vec!["foo", "bar"]);
8226 }
8227
8228 #[test]
8229 fn ctrl_p_walks_history_backward() {
8230 let mut e = editor_with("alpha beta gamma");
8231 run_keys(&mut e, "/alpha<CR>");
8232 run_keys(&mut e, "/beta<CR>");
8233 run_keys(&mut e, "/");
8235 assert_eq!(e.search_prompt().unwrap().text, "");
8236 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8237 assert_eq!(e.search_prompt().unwrap().text, "beta");
8238 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8239 assert_eq!(e.search_prompt().unwrap().text, "alpha");
8240 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8242 assert_eq!(e.search_prompt().unwrap().text, "alpha");
8243 }
8244
8245 #[test]
8246 fn ctrl_n_walks_history_forward_after_ctrl_p() {
8247 let mut e = editor_with("a b c");
8248 run_keys(&mut e, "/a<CR>");
8249 run_keys(&mut e, "/b<CR>");
8250 run_keys(&mut e, "/c<CR>");
8251 run_keys(&mut e, "/");
8252 for _ in 0..3 {
8254 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8255 }
8256 assert_eq!(e.search_prompt().unwrap().text, "a");
8257 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8258 assert_eq!(e.search_prompt().unwrap().text, "b");
8259 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8260 assert_eq!(e.search_prompt().unwrap().text, "c");
8261 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8263 assert_eq!(e.search_prompt().unwrap().text, "c");
8264 }
8265
8266 #[test]
8267 fn typing_after_history_walk_resets_cursor() {
8268 let mut e = editor_with("foo");
8269 run_keys(&mut e, "/foo<CR>");
8270 run_keys(&mut e, "/");
8271 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8272 assert_eq!(e.search_prompt().unwrap().text, "foo");
8273 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
8276 assert_eq!(e.search_prompt().unwrap().text, "foox");
8277 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8278 assert_eq!(e.search_prompt().unwrap().text, "foo");
8279 }
8280
8281 #[test]
8282 fn empty_backward_search_prompt_enter_repeats_last_search() {
8283 let mut e = editor_with("foo bar foo baz foo");
8284 run_keys(&mut e, "/foo");
8286 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8287 assert_eq!(e.cursor().1, 8);
8288 run_keys(&mut e, "?");
8289 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8290 assert_eq!(e.cursor().1, 0);
8291 assert_eq!(e.last_search(), Some("foo"));
8292 }
8293
8294 #[test]
8295 fn search_prompt_esc_cancels_but_keeps_last_search() {
8296 let mut e = editor_with("foo bar\nbaz");
8297 run_keys(&mut e, "/bar");
8298 e.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
8299 assert!(e.search_prompt().is_none());
8300 assert_eq!(e.last_search(), Some("bar"));
8301 }
8302
8303 #[test]
8304 fn search_then_n_and_shift_n_navigate() {
8305 let mut e = editor_with("foo bar foo baz foo");
8306 run_keys(&mut e, "/foo");
8307 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8308 assert_eq!(e.cursor().1, 8);
8310 run_keys(&mut e, "n");
8311 assert_eq!(e.cursor().1, 16);
8312 run_keys(&mut e, "N");
8313 assert_eq!(e.cursor().1, 8);
8314 }
8315
8316 #[test]
8317 fn question_mark_searches_backward_on_enter() {
8318 let mut e = editor_with("foo bar foo baz");
8319 e.jump_cursor(0, 10);
8320 run_keys(&mut e, "?foo");
8321 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8322 assert_eq!(e.cursor(), (0, 8));
8324 }
8325
8326 #[test]
8329 fn big_y_yanks_to_end_of_line() {
8330 let mut e = editor_with("hello world");
8331 e.jump_cursor(0, 6);
8332 run_keys(&mut e, "Y");
8333 assert_eq!(e.last_yank.as_deref(), Some("world"));
8334 }
8335
8336 #[test]
8337 fn big_y_from_line_start_yanks_full_line() {
8338 let mut e = editor_with("hello world");
8339 run_keys(&mut e, "Y");
8340 assert_eq!(e.last_yank.as_deref(), Some("hello world"));
8341 }
8342
8343 #[test]
8344 fn gj_joins_without_inserting_space() {
8345 let mut e = editor_with("hello\n world");
8346 run_keys(&mut e, "gJ");
8347 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
8349 }
8350
8351 #[test]
8352 fn gj_noop_on_last_line() {
8353 let mut e = editor_with("only");
8354 run_keys(&mut e, "gJ");
8355 assert_eq!(e.buffer().lines(), &["only".to_string()]);
8356 }
8357
8358 #[test]
8359 fn ge_jumps_to_previous_word_end() {
8360 let mut e = editor_with("foo bar baz");
8361 e.jump_cursor(0, 5);
8362 run_keys(&mut e, "ge");
8363 assert_eq!(e.cursor(), (0, 2));
8364 }
8365
8366 #[test]
8367 fn ge_respects_word_class() {
8368 let mut e = editor_with("foo-bar baz");
8371 e.jump_cursor(0, 5);
8372 run_keys(&mut e, "ge");
8373 assert_eq!(e.cursor(), (0, 3));
8374 }
8375
8376 #[test]
8377 fn big_ge_treats_hyphens_as_part_of_word() {
8378 let mut e = editor_with("foo-bar baz");
8381 e.jump_cursor(0, 10);
8382 run_keys(&mut e, "gE");
8383 assert_eq!(e.cursor(), (0, 6));
8384 }
8385
8386 #[test]
8387 fn ge_crosses_line_boundary() {
8388 let mut e = editor_with("foo\nbar");
8389 e.jump_cursor(1, 0);
8390 run_keys(&mut e, "ge");
8391 assert_eq!(e.cursor(), (0, 2));
8392 }
8393
8394 #[test]
8395 fn dge_deletes_to_end_of_previous_word() {
8396 let mut e = editor_with("foo bar baz");
8397 e.jump_cursor(0, 8);
8398 run_keys(&mut e, "dge");
8401 assert_eq!(e.buffer().lines()[0], "foo baaz");
8402 }
8403
8404 #[test]
8405 fn ctrl_scroll_keys_do_not_panic() {
8406 let mut e = editor_with(
8409 (0..50)
8410 .map(|i| format!("line{i}"))
8411 .collect::<Vec<_>>()
8412 .join("\n")
8413 .as_str(),
8414 );
8415 run_keys(&mut e, "<C-f>");
8416 run_keys(&mut e, "<C-b>");
8417 assert!(!e.buffer().lines().is_empty());
8419 }
8420
8421 #[test]
8428 fn count_insert_with_arrow_nav_does_not_leak_rows() {
8429 let mut e = Editor::new(
8430 hjkl_buffer::Buffer::new(),
8431 crate::types::DefaultHost::new(),
8432 crate::types::Options::default(),
8433 );
8434 e.set_content("row0\nrow1\nrow2");
8435 run_keys(&mut e, "3iX<Down><Esc>");
8437 assert!(e.buffer().lines()[0].contains('X'));
8439 assert!(
8442 !e.buffer().lines()[1].contains("row0"),
8443 "row1 leaked row0 contents: {:?}",
8444 e.buffer().lines()[1]
8445 );
8446 assert_eq!(e.buffer().lines().len(), 3);
8449 }
8450
8451 fn editor_with_rows(n: usize, viewport: u16) -> Editor {
8454 let mut e = Editor::new(
8455 hjkl_buffer::Buffer::new(),
8456 crate::types::DefaultHost::new(),
8457 crate::types::Options::default(),
8458 );
8459 let body = (0..n)
8460 .map(|i| format!(" line{}", i))
8461 .collect::<Vec<_>>()
8462 .join("\n");
8463 e.set_content(&body);
8464 e.set_viewport_height(viewport);
8465 e
8466 }
8467
8468 #[test]
8469 fn ctrl_d_moves_cursor_half_page_down() {
8470 let mut e = editor_with_rows(100, 20);
8471 run_keys(&mut e, "<C-d>");
8472 assert_eq!(e.cursor().0, 10);
8473 }
8474
8475 fn editor_with_wrap_lines(lines: &[&str], viewport: u16, text_width: u16) -> Editor {
8476 let mut e = Editor::new(
8477 hjkl_buffer::Buffer::new(),
8478 crate::types::DefaultHost::new(),
8479 crate::types::Options::default(),
8480 );
8481 e.set_content(&lines.join("\n"));
8482 e.set_viewport_height(viewport);
8483 let v = e.host_mut().viewport_mut();
8484 v.height = viewport;
8485 v.width = text_width;
8486 v.text_width = text_width;
8487 v.wrap = hjkl_buffer::Wrap::Char;
8488 e.settings_mut().wrap = hjkl_buffer::Wrap::Char;
8489 e
8490 }
8491
8492 #[test]
8493 fn scrolloff_wrap_keeps_cursor_off_bottom_edge() {
8494 let lines = ["aaaabbbbcccc"; 10];
8498 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8499 e.jump_cursor(4, 0);
8500 e.ensure_cursor_in_scrolloff();
8501 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8502 assert!(csr <= 6, "csr={csr}");
8503 }
8504
8505 #[test]
8506 fn scrolloff_wrap_keeps_cursor_off_top_edge() {
8507 let lines = ["aaaabbbbcccc"; 10];
8508 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8509 e.jump_cursor(7, 0);
8512 e.ensure_cursor_in_scrolloff();
8513 e.jump_cursor(2, 0);
8514 e.ensure_cursor_in_scrolloff();
8515 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8516 assert!(csr >= 5, "csr={csr}");
8518 }
8519
8520 #[test]
8521 fn scrolloff_wrap_clamps_top_at_buffer_end() {
8522 let lines = ["aaaabbbbcccc"; 5];
8523 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8524 e.jump_cursor(4, 11);
8525 e.ensure_cursor_in_scrolloff();
8526 let top = e.host().viewport().top_row;
8531 assert_eq!(top, 1);
8532 }
8533
8534 #[test]
8535 fn ctrl_u_moves_cursor_half_page_up() {
8536 let mut e = editor_with_rows(100, 20);
8537 e.jump_cursor(50, 0);
8538 run_keys(&mut e, "<C-u>");
8539 assert_eq!(e.cursor().0, 40);
8540 }
8541
8542 #[test]
8543 fn ctrl_f_moves_cursor_full_page_down() {
8544 let mut e = editor_with_rows(100, 20);
8545 run_keys(&mut e, "<C-f>");
8546 assert_eq!(e.cursor().0, 18);
8548 }
8549
8550 #[test]
8551 fn ctrl_b_moves_cursor_full_page_up() {
8552 let mut e = editor_with_rows(100, 20);
8553 e.jump_cursor(50, 0);
8554 run_keys(&mut e, "<C-b>");
8555 assert_eq!(e.cursor().0, 32);
8556 }
8557
8558 #[test]
8559 fn ctrl_d_lands_on_first_non_blank() {
8560 let mut e = editor_with_rows(100, 20);
8561 run_keys(&mut e, "<C-d>");
8562 assert_eq!(e.cursor().1, 2);
8564 }
8565
8566 #[test]
8567 fn ctrl_d_clamps_at_end_of_buffer() {
8568 let mut e = editor_with_rows(5, 20);
8569 run_keys(&mut e, "<C-d>");
8570 assert_eq!(e.cursor().0, 4);
8571 }
8572
8573 #[test]
8574 fn capital_h_jumps_to_viewport_top() {
8575 let mut e = editor_with_rows(100, 10);
8576 e.jump_cursor(50, 0);
8577 e.set_viewport_top(45);
8578 let top = e.host().viewport().top_row;
8579 run_keys(&mut e, "H");
8580 assert_eq!(e.cursor().0, top);
8581 assert_eq!(e.cursor().1, 2);
8582 }
8583
8584 #[test]
8585 fn capital_l_jumps_to_viewport_bottom() {
8586 let mut e = editor_with_rows(100, 10);
8587 e.jump_cursor(50, 0);
8588 e.set_viewport_top(45);
8589 let top = e.host().viewport().top_row;
8590 run_keys(&mut e, "L");
8591 assert_eq!(e.cursor().0, top + 9);
8592 }
8593
8594 #[test]
8595 fn capital_m_jumps_to_viewport_middle() {
8596 let mut e = editor_with_rows(100, 10);
8597 e.jump_cursor(50, 0);
8598 e.set_viewport_top(45);
8599 let top = e.host().viewport().top_row;
8600 run_keys(&mut e, "M");
8601 assert_eq!(e.cursor().0, top + 4);
8603 }
8604
8605 #[test]
8606 fn g_capital_m_lands_at_line_midpoint() {
8607 let mut e = editor_with("hello world!"); run_keys(&mut e, "gM");
8609 assert_eq!(e.cursor(), (0, 6));
8611 }
8612
8613 #[test]
8614 fn g_capital_m_on_empty_line_stays_at_zero() {
8615 let mut e = editor_with("");
8616 run_keys(&mut e, "gM");
8617 assert_eq!(e.cursor(), (0, 0));
8618 }
8619
8620 #[test]
8621 fn g_capital_m_uses_current_line_only() {
8622 let mut e = editor_with("a\nlonglongline"); e.jump_cursor(1, 0);
8625 run_keys(&mut e, "gM");
8626 assert_eq!(e.cursor(), (1, 6));
8627 }
8628
8629 #[test]
8630 fn capital_h_count_offsets_from_top() {
8631 let mut e = editor_with_rows(100, 10);
8632 e.jump_cursor(50, 0);
8633 e.set_viewport_top(45);
8634 let top = e.host().viewport().top_row;
8635 run_keys(&mut e, "3H");
8636 assert_eq!(e.cursor().0, top + 2);
8637 }
8638
8639 #[test]
8642 fn ctrl_o_returns_to_pre_g_position() {
8643 let mut e = editor_with_rows(50, 20);
8644 e.jump_cursor(5, 2);
8645 run_keys(&mut e, "G");
8646 assert_eq!(e.cursor().0, 49);
8647 run_keys(&mut e, "<C-o>");
8648 assert_eq!(e.cursor(), (5, 2));
8649 }
8650
8651 #[test]
8652 fn ctrl_i_redoes_jump_after_ctrl_o() {
8653 let mut e = editor_with_rows(50, 20);
8654 e.jump_cursor(5, 2);
8655 run_keys(&mut e, "G");
8656 let post = e.cursor();
8657 run_keys(&mut e, "<C-o>");
8658 run_keys(&mut e, "<C-i>");
8659 assert_eq!(e.cursor(), post);
8660 }
8661
8662 #[test]
8663 fn new_jump_clears_forward_stack() {
8664 let mut e = editor_with_rows(50, 20);
8665 e.jump_cursor(5, 2);
8666 run_keys(&mut e, "G");
8667 run_keys(&mut e, "<C-o>");
8668 run_keys(&mut e, "gg");
8669 run_keys(&mut e, "<C-i>");
8670 assert_eq!(e.cursor().0, 0);
8671 }
8672
8673 #[test]
8674 fn ctrl_o_on_empty_stack_is_noop() {
8675 let mut e = editor_with_rows(10, 20);
8676 e.jump_cursor(3, 1);
8677 run_keys(&mut e, "<C-o>");
8678 assert_eq!(e.cursor(), (3, 1));
8679 }
8680
8681 #[test]
8682 fn asterisk_search_pushes_jump() {
8683 let mut e = editor_with("foo bar\nbaz foo end");
8684 e.jump_cursor(0, 0);
8685 run_keys(&mut e, "*");
8686 let after = e.cursor();
8687 assert_ne!(after, (0, 0));
8688 run_keys(&mut e, "<C-o>");
8689 assert_eq!(e.cursor(), (0, 0));
8690 }
8691
8692 #[test]
8693 fn h_viewport_jump_is_recorded() {
8694 let mut e = editor_with_rows(100, 10);
8695 e.jump_cursor(50, 0);
8696 e.set_viewport_top(45);
8697 let pre = e.cursor();
8698 run_keys(&mut e, "H");
8699 assert_ne!(e.cursor(), pre);
8700 run_keys(&mut e, "<C-o>");
8701 assert_eq!(e.cursor(), pre);
8702 }
8703
8704 #[test]
8705 fn j_k_motion_does_not_push_jump() {
8706 let mut e = editor_with_rows(50, 20);
8707 e.jump_cursor(5, 0);
8708 run_keys(&mut e, "jjj");
8709 run_keys(&mut e, "<C-o>");
8710 assert_eq!(e.cursor().0, 8);
8711 }
8712
8713 #[test]
8714 fn jumplist_caps_at_100() {
8715 let mut e = editor_with_rows(200, 20);
8716 for i in 0..101 {
8717 e.jump_cursor(i, 0);
8718 run_keys(&mut e, "G");
8719 }
8720 assert!(e.vim.jump_back.len() <= 100);
8721 }
8722
8723 #[test]
8724 fn tab_acts_as_ctrl_i() {
8725 let mut e = editor_with_rows(50, 20);
8726 e.jump_cursor(5, 2);
8727 run_keys(&mut e, "G");
8728 let post = e.cursor();
8729 run_keys(&mut e, "<C-o>");
8730 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8731 assert_eq!(e.cursor(), post);
8732 }
8733
8734 #[test]
8737 fn ma_then_backtick_a_jumps_exact() {
8738 let mut e = editor_with_rows(50, 20);
8739 e.jump_cursor(5, 3);
8740 run_keys(&mut e, "ma");
8741 e.jump_cursor(20, 0);
8742 run_keys(&mut e, "`a");
8743 assert_eq!(e.cursor(), (5, 3));
8744 }
8745
8746 #[test]
8747 fn ma_then_apostrophe_a_lands_on_first_non_blank() {
8748 let mut e = editor_with_rows(50, 20);
8749 e.jump_cursor(5, 6);
8751 run_keys(&mut e, "ma");
8752 e.jump_cursor(30, 4);
8753 run_keys(&mut e, "'a");
8754 assert_eq!(e.cursor(), (5, 2));
8755 }
8756
8757 #[test]
8758 fn goto_mark_pushes_jumplist() {
8759 let mut e = editor_with_rows(50, 20);
8760 e.jump_cursor(10, 2);
8761 run_keys(&mut e, "mz");
8762 e.jump_cursor(3, 0);
8763 run_keys(&mut e, "`z");
8764 assert_eq!(e.cursor(), (10, 2));
8765 run_keys(&mut e, "<C-o>");
8766 assert_eq!(e.cursor(), (3, 0));
8767 }
8768
8769 #[test]
8770 fn goto_missing_mark_is_noop() {
8771 let mut e = editor_with_rows(50, 20);
8772 e.jump_cursor(3, 1);
8773 run_keys(&mut e, "`q");
8774 assert_eq!(e.cursor(), (3, 1));
8775 }
8776
8777 #[test]
8778 fn uppercase_mark_stored_under_uppercase_key() {
8779 let mut e = editor_with_rows(50, 20);
8780 e.jump_cursor(5, 3);
8781 run_keys(&mut e, "mA");
8782 assert_eq!(e.mark('A'), Some((5, 3)));
8785 assert!(e.mark('a').is_none());
8786 }
8787
8788 #[test]
8789 fn mark_survives_document_shrink_via_clamp() {
8790 let mut e = editor_with_rows(50, 20);
8791 e.jump_cursor(40, 4);
8792 run_keys(&mut e, "mx");
8793 e.set_content("a\nb\nc\nd\ne");
8795 run_keys(&mut e, "`x");
8796 let (r, _) = e.cursor();
8798 assert!(r <= 4);
8799 }
8800
8801 #[test]
8802 fn g_semicolon_walks_back_through_edits() {
8803 let mut e = editor_with("alpha\nbeta\ngamma");
8804 e.jump_cursor(0, 0);
8807 run_keys(&mut e, "iX<Esc>");
8808 e.jump_cursor(2, 0);
8809 run_keys(&mut e, "iY<Esc>");
8810 run_keys(&mut e, "g;");
8812 assert_eq!(e.cursor(), (2, 1));
8813 run_keys(&mut e, "g;");
8815 assert_eq!(e.cursor(), (0, 1));
8816 run_keys(&mut e, "g;");
8818 assert_eq!(e.cursor(), (0, 1));
8819 }
8820
8821 #[test]
8822 fn g_comma_walks_forward_after_g_semicolon() {
8823 let mut e = editor_with("a\nb\nc");
8824 e.jump_cursor(0, 0);
8825 run_keys(&mut e, "iX<Esc>");
8826 e.jump_cursor(2, 0);
8827 run_keys(&mut e, "iY<Esc>");
8828 run_keys(&mut e, "g;");
8829 run_keys(&mut e, "g;");
8830 assert_eq!(e.cursor(), (0, 1));
8831 run_keys(&mut e, "g,");
8832 assert_eq!(e.cursor(), (2, 1));
8833 }
8834
8835 #[test]
8836 fn new_edit_during_walk_trims_forward_entries() {
8837 let mut e = editor_with("a\nb\nc\nd");
8838 e.jump_cursor(0, 0);
8839 run_keys(&mut e, "iX<Esc>"); e.jump_cursor(2, 0);
8841 run_keys(&mut e, "iY<Esc>"); run_keys(&mut e, "g;");
8844 run_keys(&mut e, "g;");
8845 assert_eq!(e.cursor(), (0, 1));
8846 run_keys(&mut e, "iZ<Esc>");
8848 run_keys(&mut e, "g,");
8850 assert_ne!(e.cursor(), (2, 1));
8852 }
8853
8854 #[test]
8860 fn capital_mark_set_and_jump() {
8861 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
8862 e.jump_cursor(2, 1);
8863 run_keys(&mut e, "mA");
8864 e.jump_cursor(0, 0);
8866 run_keys(&mut e, "'A");
8868 assert_eq!(e.cursor().0, 2);
8870 }
8871
8872 #[test]
8873 fn capital_mark_survives_set_content() {
8874 let mut e = editor_with("first buffer line\nsecond");
8875 e.jump_cursor(1, 3);
8876 run_keys(&mut e, "mA");
8877 e.set_content("totally different content\non many\nrows of text");
8879 e.jump_cursor(0, 0);
8881 run_keys(&mut e, "'A");
8882 assert_eq!(e.cursor().0, 1);
8883 }
8884
8885 #[test]
8890 fn capital_mark_shifts_with_edit() {
8891 let mut e = editor_with("a\nb\nc\nd");
8892 e.jump_cursor(3, 0);
8893 run_keys(&mut e, "mA");
8894 e.jump_cursor(0, 0);
8896 run_keys(&mut e, "dd");
8897 e.jump_cursor(0, 0);
8898 run_keys(&mut e, "'A");
8899 assert_eq!(e.cursor().0, 2);
8900 }
8901
8902 #[test]
8903 fn mark_below_delete_shifts_up() {
8904 let mut e = editor_with("a\nb\nc\nd\ne");
8905 e.jump_cursor(3, 0);
8907 run_keys(&mut e, "ma");
8908 e.jump_cursor(0, 0);
8910 run_keys(&mut e, "dd");
8911 e.jump_cursor(0, 0);
8913 run_keys(&mut e, "'a");
8914 assert_eq!(e.cursor().0, 2);
8915 assert_eq!(e.buffer().line(2).unwrap(), "d");
8916 }
8917
8918 #[test]
8919 fn mark_on_deleted_row_is_dropped() {
8920 let mut e = editor_with("a\nb\nc\nd");
8921 e.jump_cursor(1, 0);
8923 run_keys(&mut e, "ma");
8924 run_keys(&mut e, "dd");
8926 e.jump_cursor(2, 0);
8928 run_keys(&mut e, "'a");
8929 assert_eq!(e.cursor().0, 2);
8931 }
8932
8933 #[test]
8934 fn mark_above_edit_unchanged() {
8935 let mut e = editor_with("a\nb\nc\nd\ne");
8936 e.jump_cursor(0, 0);
8938 run_keys(&mut e, "ma");
8939 e.jump_cursor(3, 0);
8941 run_keys(&mut e, "dd");
8942 e.jump_cursor(2, 0);
8944 run_keys(&mut e, "'a");
8945 assert_eq!(e.cursor().0, 0);
8946 }
8947
8948 #[test]
8949 fn mark_shifts_down_after_insert() {
8950 let mut e = editor_with("a\nb\nc");
8951 e.jump_cursor(2, 0);
8953 run_keys(&mut e, "ma");
8954 e.jump_cursor(0, 0);
8956 run_keys(&mut e, "Onew<Esc>");
8957 e.jump_cursor(0, 0);
8960 run_keys(&mut e, "'a");
8961 assert_eq!(e.cursor().0, 3);
8962 assert_eq!(e.buffer().line(3).unwrap(), "c");
8963 }
8964
8965 #[test]
8968 fn forward_search_commit_pushes_jump() {
8969 let mut e = editor_with("alpha beta\nfoo target end\nmore");
8970 e.jump_cursor(0, 0);
8971 run_keys(&mut e, "/target<CR>");
8972 assert_ne!(e.cursor(), (0, 0));
8974 run_keys(&mut e, "<C-o>");
8976 assert_eq!(e.cursor(), (0, 0));
8977 }
8978
8979 #[test]
8980 fn search_commit_no_match_does_not_push_jump() {
8981 let mut e = editor_with("alpha beta\nfoo end");
8982 e.jump_cursor(0, 3);
8983 let pre_len = e.vim.jump_back.len();
8984 run_keys(&mut e, "/zzznotfound<CR>");
8985 assert_eq!(e.vim.jump_back.len(), pre_len);
8987 }
8988
8989 #[test]
8992 fn buffer_cursor_mirrors_textarea_after_horizontal_motion() {
8993 let mut e = editor_with("hello world");
8994 run_keys(&mut e, "lll");
8995 let (row, col) = e.cursor();
8996 assert_eq!(e.buffer.cursor().row, row);
8997 assert_eq!(e.buffer.cursor().col, col);
8998 }
8999
9000 #[test]
9001 fn buffer_cursor_mirrors_textarea_after_vertical_motion() {
9002 let mut e = editor_with("aaaa\nbbbb\ncccc");
9003 run_keys(&mut e, "jj");
9004 let (row, col) = e.cursor();
9005 assert_eq!(e.buffer.cursor().row, row);
9006 assert_eq!(e.buffer.cursor().col, col);
9007 }
9008
9009 #[test]
9010 fn buffer_cursor_mirrors_textarea_after_word_motion() {
9011 let mut e = editor_with("foo bar baz");
9012 run_keys(&mut e, "ww");
9013 let (row, col) = e.cursor();
9014 assert_eq!(e.buffer.cursor().row, row);
9015 assert_eq!(e.buffer.cursor().col, col);
9016 }
9017
9018 #[test]
9019 fn buffer_cursor_mirrors_textarea_after_jump_motion() {
9020 let mut e = editor_with("a\nb\nc\nd\ne");
9021 run_keys(&mut e, "G");
9022 let (row, col) = e.cursor();
9023 assert_eq!(e.buffer.cursor().row, row);
9024 assert_eq!(e.buffer.cursor().col, col);
9025 }
9026
9027 #[test]
9028 fn editor_sticky_col_tracks_horizontal_motion() {
9029 let mut e = editor_with("longline\nhi\nlongline");
9030 run_keys(&mut e, "fl");
9035 let landed = e.cursor().1;
9036 assert!(landed > 0, "fl should have moved");
9037 run_keys(&mut e, "j");
9038 assert_eq!(e.sticky_col(), Some(landed));
9041 }
9042
9043 #[test]
9044 fn buffer_content_mirrors_textarea_after_insert() {
9045 let mut e = editor_with("hello");
9046 run_keys(&mut e, "iXYZ<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_delete() {
9053 let mut e = editor_with("alpha bravo charlie");
9054 run_keys(&mut e, "dw");
9055 let text = e.buffer().lines().join("\n");
9056 assert_eq!(e.buffer.as_string(), text);
9057 }
9058
9059 #[test]
9060 fn buffer_content_mirrors_textarea_after_dd() {
9061 let mut e = editor_with("a\nb\nc\nd");
9062 run_keys(&mut e, "jdd");
9063 let text = e.buffer().lines().join("\n");
9064 assert_eq!(e.buffer.as_string(), text);
9065 }
9066
9067 #[test]
9068 fn buffer_content_mirrors_textarea_after_open_line() {
9069 let mut e = editor_with("foo\nbar");
9070 run_keys(&mut e, "oNEW<Esc>");
9071 let text = e.buffer().lines().join("\n");
9072 assert_eq!(e.buffer.as_string(), text);
9073 }
9074
9075 #[test]
9076 fn buffer_content_mirrors_textarea_after_paste() {
9077 let mut e = editor_with("hello");
9078 run_keys(&mut e, "yy");
9079 run_keys(&mut e, "p");
9080 let text = e.buffer().lines().join("\n");
9081 assert_eq!(e.buffer.as_string(), text);
9082 }
9083
9084 #[test]
9085 fn buffer_selection_none_in_normal_mode() {
9086 let e = editor_with("foo bar");
9087 assert!(e.buffer_selection().is_none());
9088 }
9089
9090 #[test]
9091 fn buffer_selection_char_in_visual_mode() {
9092 use hjkl_buffer::{Position, Selection};
9093 let mut e = editor_with("hello world");
9094 run_keys(&mut e, "vlll");
9095 assert_eq!(
9096 e.buffer_selection(),
9097 Some(Selection::Char {
9098 anchor: Position::new(0, 0),
9099 head: Position::new(0, 3),
9100 })
9101 );
9102 }
9103
9104 #[test]
9105 fn buffer_selection_line_in_visual_line_mode() {
9106 use hjkl_buffer::Selection;
9107 let mut e = editor_with("a\nb\nc\nd");
9108 run_keys(&mut e, "Vj");
9109 assert_eq!(
9110 e.buffer_selection(),
9111 Some(Selection::Line {
9112 anchor_row: 0,
9113 head_row: 1,
9114 })
9115 );
9116 }
9117
9118 #[test]
9119 fn wrapscan_off_blocks_wrap_around() {
9120 let mut e = editor_with("first\nsecond\nthird\n");
9121 e.settings_mut().wrapscan = false;
9122 e.jump_cursor(2, 0);
9124 run_keys(&mut e, "/first<CR>");
9125 assert_eq!(e.cursor().0, 2, "wrapscan off should block wrap");
9127 e.settings_mut().wrapscan = true;
9129 run_keys(&mut e, "/first<CR>");
9130 assert_eq!(e.cursor().0, 0, "wrapscan on should wrap to row 0");
9131 }
9132
9133 #[test]
9134 fn smartcase_uppercase_pattern_stays_sensitive() {
9135 let mut e = editor_with("foo\nFoo\nBAR\n");
9136 e.settings_mut().ignore_case = true;
9137 e.settings_mut().smartcase = true;
9138 run_keys(&mut e, "/foo<CR>");
9141 let r1 = e
9142 .search_state()
9143 .pattern
9144 .as_ref()
9145 .unwrap()
9146 .as_str()
9147 .to_string();
9148 assert!(r1.starts_with("(?i)"), "lowercase under smartcase: {r1}");
9149 run_keys(&mut e, "/Foo<CR>");
9151 let r2 = e
9152 .search_state()
9153 .pattern
9154 .as_ref()
9155 .unwrap()
9156 .as_str()
9157 .to_string();
9158 assert!(!r2.starts_with("(?i)"), "mixed-case under smartcase: {r2}");
9159 }
9160
9161 #[test]
9162 fn enter_with_autoindent_copies_leading_whitespace() {
9163 let mut e = editor_with(" foo");
9164 e.jump_cursor(0, 7);
9165 run_keys(&mut e, "i<CR>");
9166 assert_eq!(e.buffer.line(1).unwrap(), " ");
9167 }
9168
9169 #[test]
9170 fn enter_without_autoindent_inserts_bare_newline() {
9171 let mut e = editor_with(" foo");
9172 e.settings_mut().autoindent = false;
9173 e.jump_cursor(0, 7);
9174 run_keys(&mut e, "i<CR>");
9175 assert_eq!(e.buffer.line(1).unwrap(), "");
9176 }
9177
9178 #[test]
9179 fn iskeyword_default_treats_alnum_underscore_as_word() {
9180 let mut e = editor_with("foo_bar baz");
9181 e.jump_cursor(0, 0);
9185 run_keys(&mut e, "*");
9186 let p = e
9187 .search_state()
9188 .pattern
9189 .as_ref()
9190 .unwrap()
9191 .as_str()
9192 .to_string();
9193 assert!(p.contains("foo_bar"), "default iskeyword: {p}");
9194 }
9195
9196 #[test]
9197 fn w_motion_respects_custom_iskeyword() {
9198 let mut e = editor_with("foo-bar baz");
9202 run_keys(&mut e, "w");
9203 assert_eq!(e.cursor().1, 3, "default iskeyword: {:?}", e.cursor());
9204 let mut e2 = editor_with("foo-bar baz");
9207 e2.set_iskeyword("@,_,45");
9208 run_keys(&mut e2, "w");
9209 assert_eq!(e2.cursor().1, 8, "dash-as-word: {:?}", e2.cursor());
9210 }
9211
9212 #[test]
9213 fn iskeyword_with_dash_treats_dash_as_word_char() {
9214 let mut e = editor_with("foo-bar baz");
9215 e.settings_mut().iskeyword = "@,_,45".to_string();
9216 e.jump_cursor(0, 0);
9217 run_keys(&mut e, "*");
9218 let p = e
9219 .search_state()
9220 .pattern
9221 .as_ref()
9222 .unwrap()
9223 .as_str()
9224 .to_string();
9225 assert!(p.contains("foo-bar"), "dash-as-word: {p}");
9226 }
9227
9228 #[test]
9229 fn timeoutlen_drops_pending_g_prefix() {
9230 use std::time::{Duration, Instant};
9231 let mut e = editor_with("a\nb\nc");
9232 e.jump_cursor(2, 0);
9233 run_keys(&mut e, "g");
9235 assert!(matches!(e.vim.pending, super::Pending::G));
9236 e.settings.timeout_len = Duration::from_nanos(0);
9244 e.vim.last_input_at = Some(Instant::now() - Duration::from_secs(60));
9245 e.vim.last_input_host_at = Some(Duration::ZERO);
9246 run_keys(&mut e, "g");
9250 assert_eq!(e.cursor().0, 2, "timeout must abandon g-prefix");
9252 }
9253
9254 #[test]
9255 fn undobreak_on_breaks_group_at_arrow_motion() {
9256 let mut e = editor_with("");
9257 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
9259 let line = e.buffer.line(0).unwrap_or("").to_string();
9262 assert!(line.contains("aaa"), "after undobreak: {line:?}");
9263 assert!(!line.contains("bbb"), "bbb should be undone: {line:?}");
9264 }
9265
9266 #[test]
9267 fn undobreak_off_keeps_full_run_in_one_group() {
9268 let mut e = editor_with("");
9269 e.settings_mut().undo_break_on_motion = false;
9270 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
9271 assert_eq!(e.buffer.line(0).unwrap_or(""), "");
9274 }
9275
9276 #[test]
9277 fn undobreak_round_trips_through_options() {
9278 let e = editor_with("");
9279 let opts = e.current_options();
9280 assert!(opts.undo_break_on_motion);
9281 let mut e2 = editor_with("");
9282 let mut new_opts = opts.clone();
9283 new_opts.undo_break_on_motion = false;
9284 e2.apply_options(&new_opts);
9285 assert!(!e2.current_options().undo_break_on_motion);
9286 }
9287
9288 #[test]
9289 fn undo_levels_cap_drops_oldest() {
9290 let mut e = editor_with("abcde");
9291 e.settings_mut().undo_levels = 3;
9292 run_keys(&mut e, "ra");
9293 run_keys(&mut e, "lrb");
9294 run_keys(&mut e, "lrc");
9295 run_keys(&mut e, "lrd");
9296 run_keys(&mut e, "lre");
9297 assert_eq!(e.undo_stack_len(), 3);
9298 }
9299
9300 #[test]
9301 fn tab_inserts_literal_tab_when_noexpandtab() {
9302 let mut e = editor_with("");
9303 e.settings_mut().expandtab = false;
9306 e.settings_mut().softtabstop = 0;
9307 run_keys(&mut e, "i");
9308 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9309 assert_eq!(e.buffer.line(0).unwrap(), "\t");
9310 }
9311
9312 #[test]
9313 fn tab_inserts_spaces_when_expandtab() {
9314 let mut e = editor_with("");
9315 e.settings_mut().expandtab = true;
9316 e.settings_mut().tabstop = 4;
9317 run_keys(&mut e, "i");
9318 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9319 assert_eq!(e.buffer.line(0).unwrap(), " ");
9320 }
9321
9322 #[test]
9323 fn tab_with_softtabstop_fills_to_next_boundary() {
9324 let mut e = editor_with("ab");
9326 e.settings_mut().expandtab = true;
9327 e.settings_mut().tabstop = 8;
9328 e.settings_mut().softtabstop = 4;
9329 run_keys(&mut e, "A"); e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9331 assert_eq!(e.buffer.line(0).unwrap(), "ab ");
9332 }
9333
9334 #[test]
9335 fn backspace_deletes_softtab_run() {
9336 let mut e = editor_with(" x");
9339 e.settings_mut().softtabstop = 4;
9340 run_keys(&mut e, "fxi");
9342 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
9343 assert_eq!(e.buffer.line(0).unwrap(), "x");
9344 }
9345
9346 #[test]
9347 fn backspace_falls_back_to_single_char_when_run_not_aligned() {
9348 let mut e = editor_with(" x");
9351 e.settings_mut().softtabstop = 4;
9352 run_keys(&mut e, "fxi");
9353 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
9354 assert_eq!(e.buffer.line(0).unwrap(), " x");
9355 }
9356
9357 #[test]
9358 fn readonly_blocks_insert_mutation() {
9359 let mut e = editor_with("hello");
9360 e.settings_mut().readonly = true;
9361 run_keys(&mut e, "iX<Esc>");
9362 assert_eq!(e.buffer.line(0).unwrap(), "hello");
9363 }
9364
9365 #[cfg(feature = "ratatui")]
9366 #[test]
9367 fn intern_ratatui_style_dedups_repeated_styles() {
9368 use ratatui::style::{Color, Style};
9369 let mut e = editor_with("");
9370 let red = Style::default().fg(Color::Red);
9371 let blue = Style::default().fg(Color::Blue);
9372 let id_r1 = e.intern_ratatui_style(red);
9373 let id_r2 = e.intern_ratatui_style(red);
9374 let id_b = e.intern_ratatui_style(blue);
9375 assert_eq!(id_r1, id_r2);
9376 assert_ne!(id_r1, id_b);
9377 assert_eq!(e.style_table().len(), 2);
9378 }
9379
9380 #[cfg(feature = "ratatui")]
9381 #[test]
9382 fn install_ratatui_syntax_spans_translates_styled_spans() {
9383 use ratatui::style::{Color, Style};
9384 let mut e = editor_with("SELECT foo");
9385 e.install_ratatui_syntax_spans(vec![vec![(0, 6, Style::default().fg(Color::Red))]]);
9386 let by_row = e.buffer_spans();
9387 assert_eq!(by_row.len(), 1);
9388 assert_eq!(by_row[0].len(), 1);
9389 assert_eq!(by_row[0][0].start_byte, 0);
9390 assert_eq!(by_row[0][0].end_byte, 6);
9391 let id = by_row[0][0].style;
9392 assert_eq!(e.style_table()[id as usize].fg, Some(Color::Red));
9393 }
9394
9395 #[cfg(feature = "ratatui")]
9396 #[test]
9397 fn install_ratatui_syntax_spans_clamps_sentinel_end() {
9398 use ratatui::style::{Color, Style};
9399 let mut e = editor_with("hello");
9400 e.install_ratatui_syntax_spans(vec![vec![(
9401 0,
9402 usize::MAX,
9403 Style::default().fg(Color::Blue),
9404 )]]);
9405 let by_row = e.buffer_spans();
9406 assert_eq!(by_row[0][0].end_byte, 5);
9407 }
9408
9409 #[cfg(feature = "ratatui")]
9410 #[test]
9411 fn install_ratatui_syntax_spans_drops_zero_width() {
9412 use ratatui::style::{Color, Style};
9413 let mut e = editor_with("abc");
9414 e.install_ratatui_syntax_spans(vec![vec![(2, 2, Style::default().fg(Color::Red))]]);
9415 assert!(e.buffer_spans()[0].is_empty());
9416 }
9417
9418 #[test]
9419 fn named_register_yank_into_a_then_paste_from_a() {
9420 let mut e = editor_with("hello world\nsecond");
9421 run_keys(&mut e, "\"ayw");
9422 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
9424 run_keys(&mut e, "j0\"aP");
9426 assert_eq!(e.buffer().lines()[1], "hello second");
9427 }
9428
9429 #[test]
9430 fn capital_r_overstrikes_chars() {
9431 let mut e = editor_with("hello");
9432 e.jump_cursor(0, 0);
9433 run_keys(&mut e, "RXY<Esc>");
9434 assert_eq!(e.buffer().lines()[0], "XYllo");
9436 }
9437
9438 #[test]
9439 fn capital_r_at_eol_appends() {
9440 let mut e = editor_with("hi");
9441 e.jump_cursor(0, 1);
9442 run_keys(&mut e, "RXYZ<Esc>");
9444 assert_eq!(e.buffer().lines()[0], "hXYZ");
9445 }
9446
9447 #[test]
9448 fn capital_r_count_does_not_repeat_overstrike_char_by_char() {
9449 let mut e = editor_with("abc");
9453 e.jump_cursor(0, 0);
9454 run_keys(&mut e, "RX<Esc>");
9455 assert_eq!(e.buffer().lines()[0], "Xbc");
9456 }
9457
9458 #[test]
9459 fn ctrl_r_in_insert_pastes_named_register() {
9460 let mut e = editor_with("hello world");
9461 run_keys(&mut e, "\"ayw");
9463 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
9464 run_keys(&mut e, "o");
9466 assert_eq!(e.vim_mode(), VimMode::Insert);
9467 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9468 e.handle_key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
9469 assert_eq!(e.buffer().lines()[1], "hello ");
9470 assert_eq!(e.cursor(), (1, 6));
9472 assert_eq!(e.vim_mode(), VimMode::Insert);
9474 e.handle_key(KeyEvent::new(KeyCode::Char('X'), KeyModifiers::NONE));
9475 assert_eq!(e.buffer().lines()[1], "hello X");
9476 }
9477
9478 #[test]
9479 fn ctrl_r_with_unnamed_register() {
9480 let mut e = editor_with("foo");
9481 run_keys(&mut e, "yiw");
9482 run_keys(&mut e, "A ");
9483 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9485 e.handle_key(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE));
9486 assert_eq!(e.buffer().lines()[0], "foo foo");
9487 }
9488
9489 #[test]
9490 fn ctrl_r_unknown_selector_is_no_op() {
9491 let mut e = editor_with("abc");
9492 run_keys(&mut e, "A");
9493 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9494 e.handle_key(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE));
9497 e.handle_key(KeyEvent::new(KeyCode::Char('Z'), KeyModifiers::NONE));
9498 assert_eq!(e.buffer().lines()[0], "abcZ");
9499 }
9500
9501 #[test]
9502 fn ctrl_r_multiline_register_pastes_with_newlines() {
9503 let mut e = editor_with("alpha\nbeta\ngamma");
9504 run_keys(&mut e, "\"byy");
9506 run_keys(&mut e, "j\"byy");
9507 run_keys(&mut e, "ggVj\"by");
9511 let payload = e.registers().read('b').unwrap().text.clone();
9512 assert!(payload.contains('\n'));
9513 run_keys(&mut e, "Go");
9514 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9515 e.handle_key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));
9516 let total_lines = e.buffer().lines().len();
9519 assert!(total_lines >= 5);
9520 }
9521
9522 #[test]
9523 fn yank_zero_holds_last_yank_after_delete() {
9524 let mut e = editor_with("hello world");
9525 run_keys(&mut e, "yw");
9526 let yanked = e.registers().read('0').unwrap().text.clone();
9527 assert!(!yanked.is_empty());
9528 run_keys(&mut e, "dw");
9530 assert_eq!(e.registers().read('0').unwrap().text, yanked);
9531 assert!(!e.registers().read('1').unwrap().text.is_empty());
9533 }
9534
9535 #[test]
9536 fn delete_ring_rotates_through_one_through_nine() {
9537 let mut e = editor_with("a b c d e f g h i j");
9538 for _ in 0..3 {
9540 run_keys(&mut e, "dw");
9541 }
9542 let r1 = e.registers().read('1').unwrap().text.clone();
9544 let r2 = e.registers().read('2').unwrap().text.clone();
9545 let r3 = e.registers().read('3').unwrap().text.clone();
9546 assert!(!r1.is_empty() && !r2.is_empty() && !r3.is_empty());
9547 assert_ne!(r1, r2);
9548 assert_ne!(r2, r3);
9549 }
9550
9551 #[test]
9552 fn capital_register_appends_to_lowercase() {
9553 let mut e = editor_with("foo bar");
9554 run_keys(&mut e, "\"ayw");
9555 let first = e.registers().read('a').unwrap().text.clone();
9556 assert!(first.contains("foo"));
9557 run_keys(&mut e, "w\"Ayw");
9559 let combined = e.registers().read('a').unwrap().text.clone();
9560 assert!(combined.starts_with(&first));
9561 assert!(combined.contains("bar"));
9562 }
9563
9564 #[test]
9565 fn zf_in_visual_line_creates_closed_fold() {
9566 let mut e = editor_with("a\nb\nc\nd\ne");
9567 e.jump_cursor(1, 0);
9569 run_keys(&mut e, "Vjjzf");
9570 assert_eq!(e.buffer().folds().len(), 1);
9571 let f = e.buffer().folds()[0];
9572 assert_eq!(f.start_row, 1);
9573 assert_eq!(f.end_row, 3);
9574 assert!(f.closed);
9575 }
9576
9577 #[test]
9578 fn zfj_in_normal_creates_two_row_fold() {
9579 let mut e = editor_with("a\nb\nc\nd\ne");
9580 e.jump_cursor(1, 0);
9581 run_keys(&mut e, "zfj");
9582 assert_eq!(e.buffer().folds().len(), 1);
9583 let f = e.buffer().folds()[0];
9584 assert_eq!(f.start_row, 1);
9585 assert_eq!(f.end_row, 2);
9586 assert!(f.closed);
9587 assert_eq!(e.cursor().0, 1);
9589 }
9590
9591 #[test]
9592 fn zf_with_count_folds_count_rows() {
9593 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9594 e.jump_cursor(0, 0);
9595 run_keys(&mut e, "zf3j");
9597 assert_eq!(e.buffer().folds().len(), 1);
9598 let f = e.buffer().folds()[0];
9599 assert_eq!(f.start_row, 0);
9600 assert_eq!(f.end_row, 3);
9601 }
9602
9603 #[test]
9604 fn zfk_folds_upward_range() {
9605 let mut e = editor_with("a\nb\nc\nd\ne");
9606 e.jump_cursor(3, 0);
9607 run_keys(&mut e, "zfk");
9608 let f = e.buffer().folds()[0];
9609 assert_eq!(f.start_row, 2);
9611 assert_eq!(f.end_row, 3);
9612 }
9613
9614 #[test]
9615 fn zf_capital_g_folds_to_bottom() {
9616 let mut e = editor_with("a\nb\nc\nd\ne");
9617 e.jump_cursor(1, 0);
9618 run_keys(&mut e, "zfG");
9620 let f = e.buffer().folds()[0];
9621 assert_eq!(f.start_row, 1);
9622 assert_eq!(f.end_row, 4);
9623 }
9624
9625 #[test]
9626 fn zfgg_folds_to_top_via_operator_pipeline() {
9627 let mut e = editor_with("a\nb\nc\nd\ne");
9628 e.jump_cursor(3, 0);
9629 run_keys(&mut e, "zfgg");
9633 let f = e.buffer().folds()[0];
9634 assert_eq!(f.start_row, 0);
9635 assert_eq!(f.end_row, 3);
9636 }
9637
9638 #[test]
9639 fn zfip_folds_paragraph_via_text_object() {
9640 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta\nepsilon");
9641 e.jump_cursor(1, 0);
9642 run_keys(&mut e, "zfip");
9644 assert_eq!(e.buffer().folds().len(), 1);
9645 let f = e.buffer().folds()[0];
9646 assert_eq!(f.start_row, 0);
9647 assert_eq!(f.end_row, 2);
9648 }
9649
9650 #[test]
9651 fn zfap_folds_paragraph_with_trailing_blank() {
9652 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta");
9653 e.jump_cursor(0, 0);
9654 run_keys(&mut e, "zfap");
9656 let f = e.buffer().folds()[0];
9657 assert_eq!(f.start_row, 0);
9658 assert_eq!(f.end_row, 3);
9659 }
9660
9661 #[test]
9662 fn zf_paragraph_motion_folds_to_blank() {
9663 let mut e = editor_with("alpha\nbeta\n\ngamma");
9664 e.jump_cursor(0, 0);
9665 run_keys(&mut e, "zf}");
9667 let f = e.buffer().folds()[0];
9668 assert_eq!(f.start_row, 0);
9669 assert_eq!(f.end_row, 2);
9670 }
9671
9672 #[test]
9673 fn za_toggles_fold_under_cursor() {
9674 let mut e = editor_with("a\nb\nc\nd");
9675 e.buffer_mut().add_fold(1, 2, true);
9676 e.jump_cursor(1, 0);
9677 run_keys(&mut e, "za");
9678 assert!(!e.buffer().folds()[0].closed);
9679 run_keys(&mut e, "za");
9680 assert!(e.buffer().folds()[0].closed);
9681 }
9682
9683 #[test]
9684 fn zr_opens_all_folds_zm_closes_all() {
9685 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9686 e.buffer_mut().add_fold(0, 1, true);
9687 e.buffer_mut().add_fold(2, 3, true);
9688 e.buffer_mut().add_fold(4, 5, true);
9689 run_keys(&mut e, "zR");
9690 assert!(e.buffer().folds().iter().all(|f| !f.closed));
9691 run_keys(&mut e, "zM");
9692 assert!(e.buffer().folds().iter().all(|f| f.closed));
9693 }
9694
9695 #[test]
9696 fn ze_clears_all_folds() {
9697 let mut e = editor_with("a\nb\nc\nd");
9698 e.buffer_mut().add_fold(0, 1, true);
9699 e.buffer_mut().add_fold(2, 3, false);
9700 run_keys(&mut e, "zE");
9701 assert!(e.buffer().folds().is_empty());
9702 }
9703
9704 #[test]
9705 fn g_underscore_jumps_to_last_non_blank() {
9706 let mut e = editor_with("hello world ");
9707 run_keys(&mut e, "g_");
9708 assert_eq!(e.cursor().1, 10);
9710 }
9711
9712 #[test]
9713 fn gj_and_gk_alias_j_and_k() {
9714 let mut e = editor_with("a\nb\nc");
9715 run_keys(&mut e, "gj");
9716 assert_eq!(e.cursor().0, 1);
9717 run_keys(&mut e, "gk");
9718 assert_eq!(e.cursor().0, 0);
9719 }
9720
9721 #[test]
9722 fn paragraph_motions_walk_blank_lines() {
9723 let mut e = editor_with("first\nblock\n\nsecond\nblock\n\nthird");
9724 run_keys(&mut e, "}");
9725 assert_eq!(e.cursor().0, 2);
9726 run_keys(&mut e, "}");
9727 assert_eq!(e.cursor().0, 5);
9728 run_keys(&mut e, "{");
9729 assert_eq!(e.cursor().0, 2);
9730 }
9731
9732 #[test]
9733 fn gv_reenters_last_visual_selection() {
9734 let mut e = editor_with("alpha\nbeta\ngamma");
9735 run_keys(&mut e, "Vj");
9736 run_keys(&mut e, "<Esc>");
9738 assert_eq!(e.vim_mode(), VimMode::Normal);
9739 run_keys(&mut e, "gv");
9741 assert_eq!(e.vim_mode(), VimMode::VisualLine);
9742 }
9743
9744 #[test]
9745 fn o_in_visual_swaps_anchor_and_cursor() {
9746 let mut e = editor_with("hello world");
9747 run_keys(&mut e, "vllll");
9749 assert_eq!(e.cursor().1, 4);
9750 run_keys(&mut e, "o");
9752 assert_eq!(e.cursor().1, 0);
9753 assert_eq!(e.vim.visual_anchor, (0, 4));
9755 }
9756
9757 #[test]
9758 fn editing_inside_fold_invalidates_it() {
9759 let mut e = editor_with("a\nb\nc\nd");
9760 e.buffer_mut().add_fold(1, 2, true);
9761 e.jump_cursor(1, 0);
9762 run_keys(&mut e, "iX<Esc>");
9764 assert!(e.buffer().folds().is_empty());
9766 }
9767
9768 #[test]
9769 fn zd_removes_fold_under_cursor() {
9770 let mut e = editor_with("a\nb\nc\nd");
9771 e.buffer_mut().add_fold(1, 2, true);
9772 e.jump_cursor(2, 0);
9773 run_keys(&mut e, "zd");
9774 assert!(e.buffer().folds().is_empty());
9775 }
9776
9777 #[test]
9778 fn take_fold_ops_observes_z_keystroke_dispatch() {
9779 use crate::types::FoldOp;
9784 let mut e = editor_with("a\nb\nc\nd");
9785 e.buffer_mut().add_fold(1, 2, true);
9786 e.jump_cursor(1, 0);
9787 let _ = e.take_fold_ops();
9790 run_keys(&mut e, "zo");
9791 run_keys(&mut e, "zM");
9792 let ops = e.take_fold_ops();
9793 assert_eq!(ops.len(), 2);
9794 assert!(matches!(ops[0], FoldOp::OpenAt(1)));
9795 assert!(matches!(ops[1], FoldOp::CloseAll));
9796 assert!(e.take_fold_ops().is_empty());
9798 }
9799
9800 #[test]
9801 fn edit_pipeline_emits_invalidate_fold_op() {
9802 use crate::types::FoldOp;
9805 let mut e = editor_with("a\nb\nc\nd");
9806 e.buffer_mut().add_fold(1, 2, true);
9807 e.jump_cursor(1, 0);
9808 let _ = e.take_fold_ops();
9809 run_keys(&mut e, "iX<Esc>");
9810 let ops = e.take_fold_ops();
9811 assert!(
9812 ops.iter().any(|op| matches!(op, FoldOp::Invalidate { .. })),
9813 "expected at least one Invalidate op, got {ops:?}"
9814 );
9815 }
9816
9817 #[test]
9818 fn dot_mark_jumps_to_last_edit_position() {
9819 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
9820 e.jump_cursor(2, 0);
9821 run_keys(&mut e, "iX<Esc>");
9823 let after_edit = e.cursor();
9824 run_keys(&mut e, "gg");
9826 assert_eq!(e.cursor().0, 0);
9827 run_keys(&mut e, "'.");
9829 assert_eq!(e.cursor().0, after_edit.0);
9830 }
9831
9832 #[test]
9833 fn quote_quote_returns_to_pre_jump_position() {
9834 let mut e = editor_with_rows(50, 20);
9835 e.jump_cursor(10, 2);
9836 let before = e.cursor();
9837 run_keys(&mut e, "G");
9839 assert_ne!(e.cursor(), before);
9840 run_keys(&mut e, "''");
9842 assert_eq!(e.cursor().0, before.0);
9843 }
9844
9845 #[test]
9846 fn backtick_backtick_restores_exact_pre_jump_pos() {
9847 let mut e = editor_with_rows(50, 20);
9848 e.jump_cursor(7, 3);
9849 let before = e.cursor();
9850 run_keys(&mut e, "G");
9851 run_keys(&mut e, "``");
9852 assert_eq!(e.cursor(), before);
9853 }
9854
9855 #[test]
9856 fn macro_record_and_replay_basic() {
9857 let mut e = editor_with("foo\nbar\nbaz");
9858 run_keys(&mut e, "qaIX<Esc>jq");
9860 assert_eq!(e.buffer().lines()[0], "Xfoo");
9861 run_keys(&mut e, "@a");
9863 assert_eq!(e.buffer().lines()[1], "Xbar");
9864 run_keys(&mut e, "j@@");
9866 assert_eq!(e.buffer().lines()[2], "Xbaz");
9867 }
9868
9869 #[test]
9870 fn macro_count_replays_n_times() {
9871 let mut e = editor_with("a\nb\nc\nd\ne");
9872 run_keys(&mut e, "qajq");
9874 assert_eq!(e.cursor().0, 1);
9875 run_keys(&mut e, "3@a");
9877 assert_eq!(e.cursor().0, 4);
9878 }
9879
9880 #[test]
9881 fn macro_capital_q_appends_to_lowercase_register() {
9882 let mut e = editor_with("hello");
9883 run_keys(&mut e, "qall<Esc>q");
9884 run_keys(&mut e, "qAhh<Esc>q");
9885 let text = e.registers().read('a').unwrap().text.clone();
9888 assert!(text.contains("ll<Esc>"));
9889 assert!(text.contains("hh<Esc>"));
9890 }
9891
9892 #[test]
9893 fn buffer_selection_block_in_visual_block_mode() {
9894 use hjkl_buffer::{Position, Selection};
9895 let mut e = editor_with("aaaa\nbbbb\ncccc");
9896 run_keys(&mut e, "<C-v>jl");
9897 assert_eq!(
9898 e.buffer_selection(),
9899 Some(Selection::Block {
9900 anchor: Position::new(0, 0),
9901 head: Position::new(1, 1),
9902 })
9903 );
9904 }
9905
9906 #[test]
9909 fn n_after_question_mark_keeps_walking_backward() {
9910 let mut e = editor_with("foo bar foo baz foo end");
9913 e.jump_cursor(0, 22);
9914 run_keys(&mut e, "?foo<CR>");
9915 assert_eq!(e.cursor().1, 16);
9916 run_keys(&mut e, "n");
9917 assert_eq!(e.cursor().1, 8);
9918 run_keys(&mut e, "N");
9919 assert_eq!(e.cursor().1, 16);
9920 }
9921
9922 #[test]
9923 fn nested_macro_chord_records_literal_keys() {
9924 let mut e = editor_with("alpha\nbeta\ngamma");
9927 run_keys(&mut e, "qblq");
9929 run_keys(&mut e, "qaIX<Esc>q");
9932 e.jump_cursor(1, 0);
9934 run_keys(&mut e, "@a");
9935 assert_eq!(e.buffer().lines()[1], "Xbeta");
9936 }
9937
9938 #[test]
9939 fn shift_gt_motion_indents_one_line() {
9940 let mut e = editor_with("hello world");
9944 run_keys(&mut e, ">w");
9945 assert_eq!(e.buffer().lines()[0], " hello world");
9946 }
9947
9948 #[test]
9949 fn shift_lt_motion_outdents_one_line() {
9950 let mut e = editor_with(" hello world");
9951 run_keys(&mut e, "<lt>w");
9952 assert_eq!(e.buffer().lines()[0], " hello world");
9954 }
9955
9956 #[test]
9957 fn shift_gt_text_object_indents_paragraph() {
9958 let mut e = editor_with("alpha\nbeta\ngamma\n\nrest");
9959 e.jump_cursor(0, 0);
9960 run_keys(&mut e, ">ip");
9961 assert_eq!(e.buffer().lines()[0], " alpha");
9962 assert_eq!(e.buffer().lines()[1], " beta");
9963 assert_eq!(e.buffer().lines()[2], " gamma");
9964 assert_eq!(e.buffer().lines()[4], "rest");
9966 }
9967
9968 #[test]
9969 fn ctrl_o_runs_exactly_one_normal_command() {
9970 let mut e = editor_with("alpha beta gamma");
9973 e.jump_cursor(0, 0);
9974 run_keys(&mut e, "i");
9975 e.handle_key(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL));
9976 run_keys(&mut e, "dw");
9977 assert_eq!(e.vim_mode(), VimMode::Insert);
9979 run_keys(&mut e, "X");
9981 assert_eq!(e.buffer().lines()[0], "Xbeta gamma");
9982 }
9983
9984 #[test]
9985 fn macro_replay_respects_mode_switching() {
9986 let mut e = editor_with("hi");
9990 run_keys(&mut e, "qaiX<Esc>0q");
9991 assert_eq!(e.vim_mode(), VimMode::Normal);
9992 e.set_content("yo");
9994 run_keys(&mut e, "@a");
9995 assert_eq!(e.vim_mode(), VimMode::Normal);
9996 assert_eq!(e.cursor().1, 0);
9997 assert_eq!(e.buffer().lines()[0], "Xyo");
9998 }
9999
10000 #[test]
10001 fn macro_recorded_text_round_trips_through_register() {
10002 let mut e = editor_with("");
10006 run_keys(&mut e, "qaiX<Esc>q");
10007 let text = e.registers().read('a').unwrap().text.clone();
10008 assert!(text.starts_with("iX"));
10009 run_keys(&mut e, "@a");
10011 assert_eq!(e.buffer().lines()[0], "XX");
10012 }
10013
10014 #[test]
10015 fn dot_after_macro_replays_macros_last_change() {
10016 let mut e = editor_with("ab\ncd\nef");
10019 run_keys(&mut e, "qaIX<Esc>jq");
10022 assert_eq!(e.buffer().lines()[0], "Xab");
10023 run_keys(&mut e, "@a");
10024 assert_eq!(e.buffer().lines()[1], "Xcd");
10025 let row_before_dot = e.cursor().0;
10028 run_keys(&mut e, ".");
10029 assert!(e.buffer().lines()[row_before_dot].starts_with('X'));
10030 }
10031
10032 fn si_editor(content: &str) -> Editor {
10038 let opts = crate::types::Options {
10039 shiftwidth: 4,
10040 softtabstop: 4,
10041 expandtab: true,
10042 smartindent: true,
10043 autoindent: true,
10044 ..crate::types::Options::default()
10045 };
10046 let mut e = Editor::new(
10047 hjkl_buffer::Buffer::new(),
10048 crate::types::DefaultHost::new(),
10049 opts,
10050 );
10051 e.set_content(content);
10052 e
10053 }
10054
10055 #[test]
10056 fn smartindent_bumps_indent_after_open_brace() {
10057 let mut e = si_editor("fn foo() {");
10059 e.jump_cursor(0, 10); run_keys(&mut e, "i<CR>");
10061 assert_eq!(
10062 e.buffer().lines()[1],
10063 " ",
10064 "smartindent should bump one shiftwidth after {{"
10065 );
10066 }
10067
10068 #[test]
10069 fn smartindent_no_bump_when_off() {
10070 let mut e = si_editor("fn foo() {");
10073 e.settings_mut().smartindent = false;
10074 e.jump_cursor(0, 10);
10075 run_keys(&mut e, "i<CR>");
10076 assert_eq!(
10077 e.buffer().lines()[1],
10078 "",
10079 "without smartindent, no bump: new line copies empty leading ws"
10080 );
10081 }
10082
10083 #[test]
10084 fn smartindent_uses_tab_when_noexpandtab() {
10085 let opts = crate::types::Options {
10087 shiftwidth: 4,
10088 softtabstop: 0,
10089 expandtab: false,
10090 smartindent: true,
10091 autoindent: true,
10092 ..crate::types::Options::default()
10093 };
10094 let mut e = Editor::new(
10095 hjkl_buffer::Buffer::new(),
10096 crate::types::DefaultHost::new(),
10097 opts,
10098 );
10099 e.set_content("fn foo() {");
10100 e.jump_cursor(0, 10);
10101 run_keys(&mut e, "i<CR>");
10102 assert_eq!(
10103 e.buffer().lines()[1],
10104 "\t",
10105 "noexpandtab: smartindent bump inserts a literal tab"
10106 );
10107 }
10108
10109 #[test]
10110 fn smartindent_dedent_on_close_brace() {
10111 let mut e = si_editor("fn foo() {");
10114 e.set_content("fn foo() {\n ");
10116 e.jump_cursor(1, 4); run_keys(&mut e, "i}");
10118 assert_eq!(
10119 e.buffer().lines()[1],
10120 "}",
10121 "close brace on whitespace-only line should dedent"
10122 );
10123 assert_eq!(e.cursor(), (1, 1), "cursor should be after the `}}`");
10124 }
10125
10126 #[test]
10127 fn smartindent_no_dedent_when_off() {
10128 let mut e = si_editor("fn foo() {\n ");
10130 e.settings_mut().smartindent = false;
10131 e.jump_cursor(1, 4);
10132 run_keys(&mut e, "i}");
10133 assert_eq!(
10134 e.buffer().lines()[1],
10135 " }",
10136 "without smartindent, `}}` just appends at cursor"
10137 );
10138 }
10139
10140 #[test]
10141 fn smartindent_no_dedent_mid_line() {
10142 let mut e = si_editor(" let x = 1");
10145 e.jump_cursor(0, 13); run_keys(&mut e, "i}");
10147 assert_eq!(
10148 e.buffer().lines()[0],
10149 " let x = 1}",
10150 "mid-line `}}` should not dedent"
10151 );
10152 }
10153
10154 #[test]
10158 fn count_5x_fills_unnamed_register() {
10159 let mut e = editor_with("hello world\n");
10160 e.jump_cursor(0, 0);
10161 run_keys(&mut e, "5x");
10162 assert_eq!(e.buffer().lines()[0], " world");
10163 assert_eq!(e.cursor(), (0, 0));
10164 assert_eq!(e.yank(), "hello");
10165 }
10166
10167 #[test]
10168 fn x_fills_unnamed_register_single_char() {
10169 let mut e = editor_with("abc\n");
10170 e.jump_cursor(0, 0);
10171 run_keys(&mut e, "x");
10172 assert_eq!(e.buffer().lines()[0], "bc");
10173 assert_eq!(e.yank(), "a");
10174 }
10175
10176 #[test]
10177 fn big_x_fills_unnamed_register() {
10178 let mut e = editor_with("hello\n");
10179 e.jump_cursor(0, 3);
10180 run_keys(&mut e, "X");
10181 assert_eq!(e.buffer().lines()[0], "helo");
10182 assert_eq!(e.yank(), "l");
10183 }
10184
10185 #[test]
10187 fn g_motion_trailing_newline_lands_on_last_content_row() {
10188 let mut e = editor_with("foo\nbar\nbaz\n");
10189 e.jump_cursor(0, 0);
10190 run_keys(&mut e, "G");
10191 assert_eq!(
10193 e.cursor().0,
10194 2,
10195 "G should land on row 2 (baz), not row 3 (phantom empty)"
10196 );
10197 }
10198
10199 #[test]
10201 fn dd_last_line_clamps_cursor_to_new_last_row() {
10202 let mut e = editor_with("foo\nbar\n");
10203 e.jump_cursor(1, 0);
10204 run_keys(&mut e, "dd");
10205 assert_eq!(e.buffer().lines()[0], "foo");
10206 assert_eq!(
10207 e.cursor(),
10208 (0, 0),
10209 "cursor should clamp to row 0 after dd on last content line"
10210 );
10211 }
10212
10213 #[test]
10215 fn d_dollar_cursor_on_last_char() {
10216 let mut e = editor_with("hello world\n");
10217 e.jump_cursor(0, 5);
10218 run_keys(&mut e, "d$");
10219 assert_eq!(e.buffer().lines()[0], "hello");
10220 assert_eq!(
10221 e.cursor(),
10222 (0, 4),
10223 "d$ should leave cursor on col 4, not col 5"
10224 );
10225 }
10226
10227 #[test]
10229 fn undo_insert_clamps_cursor_to_last_valid_col() {
10230 let mut e = editor_with("hello\n");
10231 e.jump_cursor(0, 5); run_keys(&mut e, "a world<Esc>u");
10233 assert_eq!(e.buffer().lines()[0], "hello");
10234 assert_eq!(
10235 e.cursor(),
10236 (0, 4),
10237 "undo should clamp cursor to col 4 on 'hello'"
10238 );
10239 }
10240
10241 #[test]
10243 fn da_doublequote_eats_trailing_whitespace() {
10244 let mut e = editor_with("say \"hello\" there\n");
10245 e.jump_cursor(0, 6);
10246 run_keys(&mut e, "da\"");
10247 assert_eq!(e.buffer().lines()[0], "say there");
10248 assert_eq!(e.cursor().1, 4, "cursor should be at col 4 after da\"");
10249 }
10250
10251 #[test]
10253 fn dab_cursor_col_clamped_after_delete() {
10254 let mut e = editor_with("fn x() {\n body\n}\n");
10255 e.jump_cursor(1, 4);
10256 run_keys(&mut e, "daB");
10257 assert_eq!(e.buffer().lines()[0], "fn x() ");
10258 assert_eq!(
10259 e.cursor(),
10260 (0, 6),
10261 "daB should leave cursor at col 6, not 7"
10262 );
10263 }
10264
10265 #[test]
10267 fn dib_preserves_surrounding_newlines() {
10268 let mut e = editor_with("{\n body\n}\n");
10269 e.jump_cursor(1, 4);
10270 run_keys(&mut e, "diB");
10271 assert_eq!(e.buffer().lines()[0], "{");
10272 assert_eq!(e.buffer().lines()[1], "}");
10273 assert_eq!(e.cursor().0, 1, "cursor should be on the '}}' line");
10274 }
10275
10276 #[test]
10277 fn is_chord_pending_tracks_replace_state() {
10278 let mut e = editor_with("abc\n");
10279 assert!(!e.is_chord_pending());
10280 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE));
10282 assert!(e.is_chord_pending(), "engine should be pending after r");
10283 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
10285 assert!(
10286 !e.is_chord_pending(),
10287 "engine pending should clear after replace"
10288 );
10289 }
10290
10291 #[test]
10294 fn yiw_sets_lbr_rbr_marks_around_word() {
10295 let mut e = editor_with("hello world");
10298 run_keys(&mut e, "yiw");
10299 let lo = e.mark('[').expect("'[' must be set after yiw");
10300 let hi = e.mark(']').expect("']' must be set after yiw");
10301 assert_eq!(lo, (0, 0), "'[ should be first char of yanked word");
10302 assert_eq!(hi, (0, 4), "'] should be last char of yanked word");
10303 }
10304
10305 #[test]
10306 fn yj_linewise_sets_marks_at_line_edges() {
10307 let mut e = editor_with("aaaaa\nbbbbb\nccc");
10310 run_keys(&mut e, "yj");
10311 let lo = e.mark('[').expect("'[' must be set after yj");
10312 let hi = e.mark(']').expect("']' must be set after yj");
10313 assert_eq!(lo, (0, 0), "'[ snaps to (top_row, 0) for linewise yank");
10314 assert_eq!(
10315 hi,
10316 (1, 4),
10317 "'] snaps to (bot_row, last_col) for linewise yank"
10318 );
10319 }
10320
10321 #[test]
10322 fn dd_sets_lbr_rbr_marks_to_cursor() {
10323 let mut e = editor_with("aaa\nbbb");
10326 run_keys(&mut e, "dd");
10327 let lo = e.mark('[').expect("'[' must be set after dd");
10328 let hi = e.mark(']').expect("']' must be set after dd");
10329 assert_eq!(lo, hi, "after delete both marks are at the same position");
10330 assert_eq!(lo.0, 0, "post-delete cursor row should be 0");
10331 }
10332
10333 #[test]
10334 fn dw_sets_lbr_rbr_marks_to_cursor() {
10335 let mut e = editor_with("hello world");
10338 run_keys(&mut e, "dw");
10339 let lo = e.mark('[').expect("'[' must be set after dw");
10340 let hi = e.mark(']').expect("']' must be set after dw");
10341 assert_eq!(lo, hi, "after delete both marks are at the same position");
10342 assert_eq!(lo, (0, 0), "post-dw cursor is at col 0");
10343 }
10344
10345 #[test]
10346 fn cw_then_esc_sets_lbr_at_start_rbr_at_inserted_text_end() {
10347 let mut e = editor_with("hello world");
10352 run_keys(&mut e, "cwfoo<Esc>");
10353 let lo = e.mark('[').expect("'[' must be set after cw");
10354 let hi = e.mark(']').expect("']' must be set after cw");
10355 assert_eq!(lo, (0, 0), "'[ should be start of change");
10356 assert_eq!(hi.0, 0, "'] should be on row 0");
10359 assert!(hi.1 >= 2, "'] should be at or past last char of 'foo'");
10360 }
10361
10362 #[test]
10363 fn cw_with_no_insertion_sets_marks_at_change_start() {
10364 let mut e = editor_with("hello world");
10367 run_keys(&mut e, "cw<Esc>");
10368 let lo = e.mark('[').expect("'[' must be set after cw<Esc>");
10369 let hi = e.mark(']').expect("']' must be set after cw<Esc>");
10370 assert_eq!(lo.0, 0, "'[ should be on row 0");
10371 assert_eq!(hi.0, 0, "'] should be on row 0");
10372 assert_eq!(lo, hi, "marks coincide when insert is empty");
10374 }
10375
10376 #[test]
10377 fn p_charwise_sets_marks_around_pasted_text() {
10378 let mut e = editor_with("abc xyz");
10381 run_keys(&mut e, "yiw"); run_keys(&mut e, "p"); let lo = e.mark('[').expect("'[' set after charwise paste");
10384 let hi = e.mark(']').expect("']' set after charwise paste");
10385 assert!(lo <= hi, "'[ must not exceed ']'");
10386 assert_eq!(
10388 hi.1.wrapping_sub(lo.1),
10389 2,
10390 "'] - '[ should span 2 cols for a 3-char paste"
10391 );
10392 }
10393
10394 #[test]
10395 fn p_linewise_sets_marks_at_line_edges() {
10396 let mut e = editor_with("aaa\nbbb\nccc");
10399 run_keys(&mut e, "yj"); run_keys(&mut e, "j"); run_keys(&mut e, "p"); let lo = e.mark('[').expect("'[' set after linewise paste");
10403 let hi = e.mark(']').expect("']' set after linewise paste");
10404 assert_eq!(lo.1, 0, "'[ col must be 0 for linewise paste");
10405 assert!(hi.0 > lo.0, "'] row must be below '[ row for 2-line paste");
10406 assert_eq!(hi.0 - lo.0, 1, "exactly 1 row gap for a 2-line payload");
10407 }
10408
10409 #[test]
10410 fn backtick_lbr_v_backtick_rbr_reselects_yanked_text() {
10411 let mut e = editor_with("hello world");
10415 run_keys(&mut e, "yiw"); run_keys(&mut e, "`[v`]");
10419 assert_eq!(
10421 e.cursor(),
10422 (0, 4),
10423 "visual `[v`] should land on last yanked char"
10424 );
10425 assert_eq!(
10427 e.vim_mode(),
10428 crate::VimMode::Visual,
10429 "should be in Visual mode"
10430 );
10431 }
10432
10433 #[test]
10439 fn mark_dot_jump_to_last_edit_pre_edit_cursor() {
10440 let mut e = editor_with("hello\nworld\n");
10443 e.jump_cursor(0, 0);
10444 run_keys(&mut e, "iX<Esc>j`.");
10445 assert_eq!(
10446 e.cursor(),
10447 (0, 0),
10448 "dot mark should jump to the change-start (col 0), not post-insert col"
10449 );
10450 }
10451
10452 #[test]
10455 fn count_100g_clamps_to_last_content_row() {
10456 let mut e = editor_with("foo\nbar\nbaz\n");
10459 e.jump_cursor(0, 0);
10460 run_keys(&mut e, "100G");
10461 assert_eq!(
10462 e.cursor(),
10463 (2, 0),
10464 "100G on trailing-newline buffer must clamp to row 2 (last content row)"
10465 );
10466 }
10467
10468 #[test]
10471 fn gi_resumes_last_insert_position() {
10472 let mut e = editor_with("world\nhello\n");
10478 e.jump_cursor(0, 0);
10479 run_keys(&mut e, "iHi<Esc>jgi<Esc>");
10480 assert_eq!(
10481 e.vim_mode(),
10482 crate::VimMode::Normal,
10483 "should be in Normal mode after gi<Esc>"
10484 );
10485 assert_eq!(
10486 e.cursor(),
10487 (0, 1),
10488 "gi<Esc> cursor should be at (0,1) — the insert row, step-back col"
10489 );
10490 }
10491
10492 #[test]
10496 fn visual_block_change_cursor_on_last_inserted_char() {
10497 let mut e = editor_with("foo\nbar\nbaz\n");
10501 e.jump_cursor(0, 0);
10502 run_keys(&mut e, "<C-v>jlcZZ<Esc>");
10503 let lines = e.buffer().lines().to_vec();
10504 assert_eq!(lines[0], "ZZo", "row 0 should be 'ZZo'");
10505 assert_eq!(lines[1], "ZZr", "row 1 should be 'ZZr'");
10506 assert_eq!(
10507 e.cursor(),
10508 (0, 1),
10509 "cursor should be on last char of inserted 'ZZ' (col 1)"
10510 );
10511 }
10512
10513 #[test]
10518 fn register_blackhole_delete_preserves_unnamed_register() {
10519 let mut e = editor_with("foo bar baz\n");
10526 e.jump_cursor(0, 0);
10527 run_keys(&mut e, "yiww\"_dwbp");
10528 let lines = e.buffer().lines().to_vec();
10529 assert_eq!(
10530 lines[0], "ffoooo baz",
10531 "black-hole delete must not corrupt unnamed register"
10532 );
10533 assert_eq!(
10534 e.cursor(),
10535 (0, 3),
10536 "cursor should be on last pasted char (col 3)"
10537 );
10538 }
10539
10540 #[test]
10543 fn after_z_zz_sets_viewport_pinned() {
10544 let mut e = editor_with("a\nb\nc\nd\ne");
10545 e.jump_cursor(2, 0);
10546 e.after_z('z', 1);
10547 assert!(e.vim.viewport_pinned, "zz must set viewport_pinned");
10548 }
10549
10550 #[test]
10551 fn after_z_zo_opens_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('o', 1);
10556 assert!(
10557 !e.buffer().folds()[0].closed,
10558 "zo must open the fold at the cursor row"
10559 );
10560 }
10561
10562 #[test]
10563 fn after_z_zm_closes_all_folds() {
10564 let mut e = editor_with("a\nb\nc\nd\ne\nf");
10565 e.buffer_mut().add_fold(0, 1, false);
10566 e.buffer_mut().add_fold(4, 5, false);
10567 e.after_z('M', 1);
10568 assert!(
10569 e.buffer().folds().iter().all(|f| f.closed),
10570 "zM must close all folds"
10571 );
10572 }
10573
10574 #[test]
10575 fn after_z_zd_removes_fold_at_cursor() {
10576 let mut e = editor_with("a\nb\nc\nd");
10577 e.buffer_mut().add_fold(1, 2, true);
10578 e.jump_cursor(1, 0);
10579 e.after_z('d', 1);
10580 assert!(
10581 e.buffer().folds().is_empty(),
10582 "zd must remove the fold at the cursor row"
10583 );
10584 }
10585
10586 #[test]
10587 fn after_z_zf_in_visual_creates_fold() {
10588 let mut e = editor_with("a\nb\nc\nd\ne");
10589 e.jump_cursor(1, 0);
10591 run_keys(&mut e, "V2j");
10592 e.after_z('f', 1);
10594 let folds = e.buffer().folds();
10595 assert_eq!(folds.len(), 1, "zf in visual must create exactly one fold");
10596 assert_eq!(folds[0].start_row, 1);
10597 assert_eq!(folds[0].end_row, 3);
10598 assert!(folds[0].closed);
10599 }
10600
10601 #[test]
10604 fn apply_op_motion_dw_deletes_word() {
10605 let mut e = editor_with("hello world");
10607 e.apply_op_motion(crate::vim::Operator::Delete, 'w', 1);
10608 assert_eq!(
10609 e.buffer().lines().first().cloned().unwrap_or_default(),
10610 "world"
10611 );
10612 }
10613
10614 #[test]
10615 fn apply_op_motion_cw_quirk_leaves_trailing_space() {
10616 let mut e = editor_with("hello world");
10618 e.apply_op_motion(crate::vim::Operator::Change, 'w', 1);
10619 let line = e.buffer().lines().first().cloned().unwrap_or_default();
10622 assert!(
10623 line.starts_with(' ') || line == " world",
10624 "cw quirk: got {line:?}"
10625 );
10626 assert_eq!(e.vim_mode(), VimMode::Insert);
10627 }
10628
10629 #[test]
10630 fn apply_op_double_dd_deletes_line() {
10631 let mut e = editor_with("line1\nline2\nline3");
10632 e.apply_op_double(crate::vim::Operator::Delete, 1);
10634 let lines: Vec<_> = e.buffer().lines().to_vec();
10635 assert_eq!(lines, vec!["line2", "line3"], "dd should delete line1");
10636 }
10637
10638 #[test]
10639 fn apply_op_double_yy_does_not_modify_buffer() {
10640 let mut e = editor_with("hello");
10641 e.apply_op_double(crate::vim::Operator::Yank, 1);
10642 assert_eq!(
10643 e.buffer().lines().first().cloned().unwrap_or_default(),
10644 "hello"
10645 );
10646 }
10647
10648 #[test]
10649 fn enter_op_text_obj_sets_pending() {
10650 let mut e = editor_with("hello world");
10651 e.enter_op_text_obj(crate::vim::Operator::Delete, 1, true);
10652 assert!(e.is_chord_pending(), "OpTextObj should set chord pending");
10653 }
10654
10655 #[test]
10656 fn enter_op_g_sets_pending() {
10657 let mut e = editor_with("hello world");
10658 e.enter_op_g(crate::vim::Operator::Delete, 1);
10659 assert!(e.is_chord_pending(), "OpG should set chord pending");
10660 }
10661
10662 #[test]
10663 fn enter_op_find_sets_pending() {
10664 let mut e = editor_with("hello world");
10665 e.enter_op_find(crate::vim::Operator::Delete, 1, true, false);
10666 assert!(e.is_chord_pending(), "OpFind should set chord pending");
10667 }
10668
10669 #[test]
10670 fn apply_op_double_dd_count2_deletes_two_lines() {
10671 let mut e = editor_with("line1\nline2\nline3");
10672 e.apply_op_double(crate::vim::Operator::Delete, 2);
10673 let lines: Vec<_> = e.buffer().lines().to_vec();
10674 assert_eq!(lines, vec!["line3"], "2dd should delete two lines");
10675 }
10676
10677 #[test]
10678 fn apply_op_motion_unknown_key_is_noop() {
10679 let mut e = editor_with("hello");
10681 let before = e.cursor();
10682 e.apply_op_motion(crate::vim::Operator::Delete, 'X', 1); assert_eq!(e.cursor(), before);
10684 assert_eq!(
10685 e.buffer().lines().first().cloned().unwrap_or_default(),
10686 "hello"
10687 );
10688 }
10689
10690 #[test]
10693 fn apply_op_find_dfx_deletes_to_x() {
10694 let mut e = editor_with("hello x world");
10696 e.apply_op_find(crate::vim::Operator::Delete, 'x', true, false, 1);
10697 assert_eq!(
10698 e.buffer().lines().first().cloned().unwrap_or_default(),
10699 " world",
10700 "dfx must delete 'hello x'"
10701 );
10702 }
10703
10704 #[test]
10705 fn apply_op_find_dtx_deletes_up_to_x() {
10706 let mut e = editor_with("hello x world");
10708 e.apply_op_find(crate::vim::Operator::Delete, 'x', true, true, 1);
10709 assert_eq!(
10710 e.buffer().lines().first().cloned().unwrap_or_default(),
10711 "x world",
10712 "dtx must delete 'hello ' leaving 'x world'"
10713 );
10714 }
10715
10716 #[test]
10717 fn apply_op_find_records_last_find() {
10718 let mut e = editor_with("hello x world");
10720 e.apply_op_find(crate::vim::Operator::Delete, 'x', true, false, 1);
10721 let _ = e.cursor(); }
10728
10729 #[test]
10732 fn apply_op_text_obj_diw_deletes_word() {
10733 let mut e = editor_with("hello world");
10735 e.apply_op_text_obj(crate::vim::Operator::Delete, 'w', true, 1);
10736 let line = e.buffer().lines().first().cloned().unwrap_or_default();
10737 assert!(
10742 !line.contains("hello"),
10743 "diw must delete 'hello', remaining: {line:?}"
10744 );
10745 }
10746
10747 #[test]
10748 fn apply_op_text_obj_daw_deletes_around_word() {
10749 let mut e = editor_with("hello world");
10751 e.apply_op_text_obj(crate::vim::Operator::Delete, 'w', false, 1);
10752 let line = e.buffer().lines().first().cloned().unwrap_or_default();
10753 assert!(
10754 !line.contains("hello"),
10755 "daw must delete 'hello' and surrounding space, remaining: {line:?}"
10756 );
10757 }
10758
10759 #[test]
10760 fn apply_op_text_obj_invalid_char_no_op() {
10761 let mut e = editor_with("hello world");
10763 let before = e.buffer().as_string();
10764 e.apply_op_text_obj(crate::vim::Operator::Delete, 'X', true, 1);
10765 assert_eq!(
10766 e.buffer().as_string(),
10767 before,
10768 "unknown text-object char must be a no-op"
10769 );
10770 }
10771}