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>(
2134 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2135 input: Input,
2136) -> bool {
2137 if let Key::Char(c) = input.key {
2138 ed.set_pending_register(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
2510pub(crate) fn apply_motion_kind<H: crate::types::Host>(
2519 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2520 kind: hjkl_vim::MotionKind,
2521 count: usize,
2522) {
2523 let count = count.max(1);
2524 match kind {
2525 hjkl_vim::MotionKind::CharLeft => {
2526 execute_motion(ed, Motion::Left, count);
2527 }
2528 hjkl_vim::MotionKind::CharRight => {
2529 execute_motion(ed, Motion::Right, count);
2530 }
2531 hjkl_vim::MotionKind::LineDown => {
2532 execute_motion(ed, Motion::Down, count);
2533 }
2534 hjkl_vim::MotionKind::LineUp => {
2535 execute_motion(ed, Motion::Up, count);
2536 }
2537 hjkl_vim::MotionKind::FirstNonBlankDown => {
2538 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2543 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2544 crate::motions::move_first_non_blank(&mut ed.buffer);
2545 ed.push_buffer_cursor_to_textarea();
2546 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2547 ed.sync_buffer_from_textarea();
2548 }
2549 hjkl_vim::MotionKind::FirstNonBlankUp => {
2550 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2553 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2554 crate::motions::move_first_non_blank(&mut ed.buffer);
2555 ed.push_buffer_cursor_to_textarea();
2556 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2557 ed.sync_buffer_from_textarea();
2558 }
2559 hjkl_vim::MotionKind::WordForward => {
2560 execute_motion(ed, Motion::WordFwd, count);
2561 }
2562 hjkl_vim::MotionKind::BigWordForward => {
2563 execute_motion(ed, Motion::BigWordFwd, count);
2564 }
2565 hjkl_vim::MotionKind::WordBackward => {
2566 execute_motion(ed, Motion::WordBack, count);
2567 }
2568 hjkl_vim::MotionKind::BigWordBackward => {
2569 execute_motion(ed, Motion::BigWordBack, count);
2570 }
2571 hjkl_vim::MotionKind::WordEnd => {
2572 execute_motion(ed, Motion::WordEnd, count);
2573 }
2574 hjkl_vim::MotionKind::BigWordEnd => {
2575 execute_motion(ed, Motion::BigWordEnd, count);
2576 }
2577 _ => {
2578 }
2582 }
2583}
2584
2585fn apply_sticky_col<H: crate::types::Host>(
2590 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2591 motion: &Motion,
2592 pre_col: usize,
2593) {
2594 if is_vertical_motion(motion) {
2595 let want = ed.sticky_col.unwrap_or(pre_col);
2596 ed.sticky_col = Some(want);
2599 let (row, _) = ed.cursor();
2600 let line_len = buf_line_chars(&ed.buffer, row);
2601 let max_col = line_len.saturating_sub(1);
2605 let target = want.min(max_col);
2606 ed.jump_cursor(row, target);
2607 } else {
2608 ed.sticky_col = Some(ed.cursor().1);
2611 }
2612}
2613
2614fn is_vertical_motion(motion: &Motion) -> bool {
2615 matches!(
2619 motion,
2620 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown
2621 )
2622}
2623
2624fn apply_motion_cursor<H: crate::types::Host>(
2625 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2626 motion: &Motion,
2627 count: usize,
2628) {
2629 apply_motion_cursor_ctx(ed, motion, count, false)
2630}
2631
2632fn apply_motion_cursor_ctx<H: crate::types::Host>(
2633 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2634 motion: &Motion,
2635 count: usize,
2636 as_operator: bool,
2637) {
2638 match motion {
2639 Motion::Left => {
2640 crate::motions::move_left(&mut ed.buffer, count);
2642 ed.push_buffer_cursor_to_textarea();
2643 }
2644 Motion::Right => {
2645 if as_operator {
2649 crate::motions::move_right_to_end(&mut ed.buffer, count);
2650 } else {
2651 crate::motions::move_right_in_line(&mut ed.buffer, count);
2652 }
2653 ed.push_buffer_cursor_to_textarea();
2654 }
2655 Motion::Up => {
2656 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2660 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2661 ed.push_buffer_cursor_to_textarea();
2662 }
2663 Motion::Down => {
2664 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2665 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2666 ed.push_buffer_cursor_to_textarea();
2667 }
2668 Motion::ScreenUp => {
2669 let v = *ed.host.viewport();
2670 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2671 crate::motions::move_screen_up(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2672 ed.push_buffer_cursor_to_textarea();
2673 }
2674 Motion::ScreenDown => {
2675 let v = *ed.host.viewport();
2676 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2677 crate::motions::move_screen_down(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2678 ed.push_buffer_cursor_to_textarea();
2679 }
2680 Motion::WordFwd => {
2681 crate::motions::move_word_fwd(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2682 ed.push_buffer_cursor_to_textarea();
2683 }
2684 Motion::WordBack => {
2685 crate::motions::move_word_back(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2686 ed.push_buffer_cursor_to_textarea();
2687 }
2688 Motion::WordEnd => {
2689 crate::motions::move_word_end(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2690 ed.push_buffer_cursor_to_textarea();
2691 }
2692 Motion::BigWordFwd => {
2693 crate::motions::move_word_fwd(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2694 ed.push_buffer_cursor_to_textarea();
2695 }
2696 Motion::BigWordBack => {
2697 crate::motions::move_word_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2698 ed.push_buffer_cursor_to_textarea();
2699 }
2700 Motion::BigWordEnd => {
2701 crate::motions::move_word_end(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2702 ed.push_buffer_cursor_to_textarea();
2703 }
2704 Motion::WordEndBack => {
2705 crate::motions::move_word_end_back(
2706 &mut ed.buffer,
2707 false,
2708 count,
2709 &ed.settings.iskeyword,
2710 );
2711 ed.push_buffer_cursor_to_textarea();
2712 }
2713 Motion::BigWordEndBack => {
2714 crate::motions::move_word_end_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2715 ed.push_buffer_cursor_to_textarea();
2716 }
2717 Motion::LineStart => {
2718 crate::motions::move_line_start(&mut ed.buffer);
2719 ed.push_buffer_cursor_to_textarea();
2720 }
2721 Motion::FirstNonBlank => {
2722 crate::motions::move_first_non_blank(&mut ed.buffer);
2723 ed.push_buffer_cursor_to_textarea();
2724 }
2725 Motion::LineEnd => {
2726 crate::motions::move_line_end(&mut ed.buffer);
2728 ed.push_buffer_cursor_to_textarea();
2729 }
2730 Motion::FileTop => {
2731 if count > 1 {
2734 crate::motions::move_bottom(&mut ed.buffer, count);
2735 } else {
2736 crate::motions::move_top(&mut ed.buffer);
2737 }
2738 ed.push_buffer_cursor_to_textarea();
2739 }
2740 Motion::FileBottom => {
2741 if count > 1 {
2744 crate::motions::move_bottom(&mut ed.buffer, count);
2745 } else {
2746 crate::motions::move_bottom(&mut ed.buffer, 0);
2747 }
2748 ed.push_buffer_cursor_to_textarea();
2749 }
2750 Motion::Find { ch, forward, till } => {
2751 for _ in 0..count {
2752 if !find_char_on_line(ed, *ch, *forward, *till) {
2753 break;
2754 }
2755 }
2756 }
2757 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
2759 let _ = matching_bracket(ed);
2760 }
2761 Motion::WordAtCursor {
2762 forward,
2763 whole_word,
2764 } => {
2765 word_at_cursor_search(ed, *forward, *whole_word, count);
2766 }
2767 Motion::SearchNext { reverse } => {
2768 if let Some(pattern) = ed.vim.last_search.clone() {
2772 push_search_pattern(ed, &pattern);
2773 }
2774 if ed.search_state().pattern.is_none() {
2775 return;
2776 }
2777 let forward = ed.vim.last_search_forward != *reverse;
2781 for _ in 0..count.max(1) {
2782 if forward {
2783 ed.search_advance_forward(true);
2784 } else {
2785 ed.search_advance_backward(true);
2786 }
2787 }
2788 ed.push_buffer_cursor_to_textarea();
2789 }
2790 Motion::ViewportTop => {
2791 let v = *ed.host().viewport();
2792 crate::motions::move_viewport_top(&mut ed.buffer, &v, count.saturating_sub(1));
2793 ed.push_buffer_cursor_to_textarea();
2794 }
2795 Motion::ViewportMiddle => {
2796 let v = *ed.host().viewport();
2797 crate::motions::move_viewport_middle(&mut ed.buffer, &v);
2798 ed.push_buffer_cursor_to_textarea();
2799 }
2800 Motion::ViewportBottom => {
2801 let v = *ed.host().viewport();
2802 crate::motions::move_viewport_bottom(&mut ed.buffer, &v, count.saturating_sub(1));
2803 ed.push_buffer_cursor_to_textarea();
2804 }
2805 Motion::LastNonBlank => {
2806 crate::motions::move_last_non_blank(&mut ed.buffer);
2807 ed.push_buffer_cursor_to_textarea();
2808 }
2809 Motion::LineMiddle => {
2810 let row = ed.cursor().0;
2811 let line_chars = buf_line_chars(&ed.buffer, row);
2812 let target = line_chars / 2;
2815 ed.jump_cursor(row, target);
2816 }
2817 Motion::ParagraphPrev => {
2818 crate::motions::move_paragraph_prev(&mut ed.buffer, count);
2819 ed.push_buffer_cursor_to_textarea();
2820 }
2821 Motion::ParagraphNext => {
2822 crate::motions::move_paragraph_next(&mut ed.buffer, count);
2823 ed.push_buffer_cursor_to_textarea();
2824 }
2825 Motion::SentencePrev => {
2826 for _ in 0..count.max(1) {
2827 if let Some((row, col)) = sentence_boundary(ed, false) {
2828 ed.jump_cursor(row, col);
2829 }
2830 }
2831 }
2832 Motion::SentenceNext => {
2833 for _ in 0..count.max(1) {
2834 if let Some((row, col)) = sentence_boundary(ed, true) {
2835 ed.jump_cursor(row, col);
2836 }
2837 }
2838 }
2839 }
2840}
2841
2842fn move_first_non_whitespace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2843 ed.sync_buffer_content_from_textarea();
2849 crate::motions::move_first_non_blank(&mut ed.buffer);
2850 ed.push_buffer_cursor_to_textarea();
2851}
2852
2853fn find_char_on_line<H: crate::types::Host>(
2854 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2855 ch: char,
2856 forward: bool,
2857 till: bool,
2858) -> bool {
2859 let moved = crate::motions::find_char_on_line(&mut ed.buffer, ch, forward, till);
2860 if moved {
2861 ed.push_buffer_cursor_to_textarea();
2862 }
2863 moved
2864}
2865
2866fn matching_bracket<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
2867 let moved = crate::motions::match_bracket(&mut ed.buffer);
2868 if moved {
2869 ed.push_buffer_cursor_to_textarea();
2870 }
2871 moved
2872}
2873
2874fn word_at_cursor_search<H: crate::types::Host>(
2875 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2876 forward: bool,
2877 whole_word: bool,
2878 count: usize,
2879) {
2880 let (row, col) = ed.cursor();
2881 let line: String = buf_line(&ed.buffer, row).unwrap_or("").to_string();
2882 let chars: Vec<char> = line.chars().collect();
2883 if chars.is_empty() {
2884 return;
2885 }
2886 let spec = ed.settings().iskeyword.clone();
2888 let is_word = |c: char| is_keyword_char(c, &spec);
2889 let mut start = col.min(chars.len().saturating_sub(1));
2890 while start > 0 && is_word(chars[start - 1]) {
2891 start -= 1;
2892 }
2893 let mut end = start;
2894 while end < chars.len() && is_word(chars[end]) {
2895 end += 1;
2896 }
2897 if end <= start {
2898 return;
2899 }
2900 let word: String = chars[start..end].iter().collect();
2901 let escaped = regex_escape(&word);
2902 let pattern = if whole_word {
2903 format!(r"\b{escaped}\b")
2904 } else {
2905 escaped
2906 };
2907 push_search_pattern(ed, &pattern);
2908 if ed.search_state().pattern.is_none() {
2909 return;
2910 }
2911 ed.vim.last_search = Some(pattern);
2913 ed.vim.last_search_forward = forward;
2914 for _ in 0..count.max(1) {
2915 if forward {
2916 ed.search_advance_forward(true);
2917 } else {
2918 ed.search_advance_backward(true);
2919 }
2920 }
2921 ed.push_buffer_cursor_to_textarea();
2922}
2923
2924fn regex_escape(s: &str) -> String {
2925 let mut out = String::with_capacity(s.len());
2926 for c in s.chars() {
2927 if matches!(
2928 c,
2929 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
2930 ) {
2931 out.push('\\');
2932 }
2933 out.push(c);
2934 }
2935 out
2936}
2937
2938pub(crate) fn apply_op_motion_key<H: crate::types::Host>(
2952 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2953 op: Operator,
2954 motion_key: char,
2955 total_count: usize,
2956) {
2957 let input = Input {
2958 key: Key::Char(motion_key),
2959 ctrl: false,
2960 alt: false,
2961 shift: false,
2962 };
2963 let Some(motion) = parse_motion(&input) else {
2964 return;
2965 };
2966 let motion = match motion {
2967 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2968 Some((ch, forward, till)) => Motion::Find {
2969 ch,
2970 forward: if reverse { !forward } else { forward },
2971 till,
2972 },
2973 None => return,
2974 },
2975 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
2977 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
2978 m => m,
2979 };
2980 apply_op_with_motion(ed, op, &motion, total_count);
2981 if let Motion::Find { ch, forward, till } = &motion {
2982 ed.vim.last_find = Some((*ch, *forward, *till));
2983 }
2984 if !ed.vim.replaying && op_is_change(op) {
2985 ed.vim.last_change = Some(LastChange::OpMotion {
2986 op,
2987 motion,
2988 count: total_count,
2989 inserted: None,
2990 });
2991 }
2992}
2993
2994pub(crate) fn apply_op_double<H: crate::types::Host>(
2997 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2998 op: Operator,
2999 total_count: usize,
3000) {
3001 execute_line_op(ed, op, total_count);
3002 if !ed.vim.replaying {
3003 ed.vim.last_change = Some(LastChange::LineOp {
3004 op,
3005 count: total_count,
3006 inserted: None,
3007 });
3008 }
3009}
3010
3011fn handle_after_op<H: crate::types::Host>(
3012 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3013 input: Input,
3014 op: Operator,
3015 count1: usize,
3016) -> bool {
3017 if let Key::Char(d @ '0'..='9') = input.key
3019 && !input.ctrl
3020 && (d != '0' || ed.vim.count > 0)
3021 {
3022 ed.vim.count = ed.vim.count.saturating_mul(10) + (d as usize - '0' as usize);
3023 ed.vim.pending = Pending::Op { op, count1 };
3024 return true;
3025 }
3026
3027 if input.key == Key::Esc {
3029 ed.vim.count = 0;
3030 return true;
3031 }
3032
3033 let double_ch = match op {
3037 Operator::Delete => Some('d'),
3038 Operator::Change => Some('c'),
3039 Operator::Yank => Some('y'),
3040 Operator::Indent => Some('>'),
3041 Operator::Outdent => Some('<'),
3042 Operator::Uppercase => Some('U'),
3043 Operator::Lowercase => Some('u'),
3044 Operator::ToggleCase => Some('~'),
3045 Operator::Fold => None,
3046 Operator::Reflow => Some('q'),
3049 };
3050 if let Key::Char(c) = input.key
3051 && !input.ctrl
3052 && Some(c) == double_ch
3053 {
3054 let count2 = take_count(&mut ed.vim);
3055 let total = count1.max(1) * count2.max(1);
3056 execute_line_op(ed, op, total);
3057 if !ed.vim.replaying {
3058 ed.vim.last_change = Some(LastChange::LineOp {
3059 op,
3060 count: total,
3061 inserted: None,
3062 });
3063 }
3064 return true;
3065 }
3066
3067 if let Key::Char('i') | Key::Char('a') = input.key
3069 && !input.ctrl
3070 {
3071 let inner = matches!(input.key, Key::Char('i'));
3072 ed.vim.pending = Pending::OpTextObj { op, count1, inner };
3073 return true;
3074 }
3075
3076 if input.key == Key::Char('g') && !input.ctrl {
3078 ed.vim.pending = Pending::OpG { op, count1 };
3079 return true;
3080 }
3081
3082 if let Some((forward, till)) = find_entry(&input) {
3084 ed.vim.pending = Pending::OpFind {
3085 op,
3086 count1,
3087 forward,
3088 till,
3089 };
3090 return true;
3091 }
3092
3093 let count2 = take_count(&mut ed.vim);
3095 let total = count1.max(1) * count2.max(1);
3096 if let Some(motion) = parse_motion(&input) {
3097 let motion = match motion {
3098 Motion::FindRepeat { reverse } => match ed.vim.last_find {
3099 Some((ch, forward, till)) => Motion::Find {
3100 ch,
3101 forward: if reverse { !forward } else { forward },
3102 till,
3103 },
3104 None => return true,
3105 },
3106 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
3110 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
3111 m => m,
3112 };
3113 apply_op_with_motion(ed, op, &motion, total);
3114 if let Motion::Find { ch, forward, till } = &motion {
3115 ed.vim.last_find = Some((*ch, *forward, *till));
3116 }
3117 if !ed.vim.replaying && op_is_change(op) {
3118 ed.vim.last_change = Some(LastChange::OpMotion {
3119 op,
3120 motion,
3121 count: total,
3122 inserted: None,
3123 });
3124 }
3125 return true;
3126 }
3127
3128 true
3130}
3131
3132pub(crate) fn apply_op_g_inner<H: crate::types::Host>(
3142 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3143 op: Operator,
3144 ch: char,
3145 total_count: usize,
3146) {
3147 if matches!(
3150 op,
3151 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase
3152 ) {
3153 let op_char = match op {
3154 Operator::Uppercase => 'U',
3155 Operator::Lowercase => 'u',
3156 Operator::ToggleCase => '~',
3157 _ => unreachable!(),
3158 };
3159 if ch == op_char {
3160 execute_line_op(ed, op, total_count);
3161 if !ed.vim.replaying {
3162 ed.vim.last_change = Some(LastChange::LineOp {
3163 op,
3164 count: total_count,
3165 inserted: None,
3166 });
3167 }
3168 return;
3169 }
3170 }
3171 let motion = match ch {
3172 'g' => Motion::FileTop,
3173 'e' => Motion::WordEndBack,
3174 'E' => Motion::BigWordEndBack,
3175 'j' => Motion::ScreenDown,
3176 'k' => Motion::ScreenUp,
3177 _ => return, };
3179 apply_op_with_motion(ed, op, &motion, total_count);
3180 if !ed.vim.replaying && op_is_change(op) {
3181 ed.vim.last_change = Some(LastChange::OpMotion {
3182 op,
3183 motion,
3184 count: total_count,
3185 inserted: None,
3186 });
3187 }
3188}
3189
3190fn handle_op_after_g<H: crate::types::Host>(
3191 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3192 input: Input,
3193 op: Operator,
3194 count1: usize,
3195) -> bool {
3196 if input.ctrl {
3197 return true;
3198 }
3199 let count2 = take_count(&mut ed.vim);
3200 let total = count1.max(1) * count2.max(1);
3201 if let Key::Char(ch) = input.key {
3202 apply_op_g_inner(ed, op, ch, total);
3203 }
3204 true
3205}
3206
3207fn handle_after_g<H: crate::types::Host>(
3208 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3209 input: Input,
3210) -> bool {
3211 let count = take_count(&mut ed.vim);
3212 if let Key::Char(ch) = input.key {
3215 apply_after_g(ed, ch, count);
3216 }
3217 true
3218}
3219
3220pub(crate) fn apply_after_g<H: crate::types::Host>(
3225 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3226 ch: char,
3227 count: usize,
3228) {
3229 match ch {
3230 'g' => {
3231 let pre = ed.cursor();
3233 if count > 1 {
3234 ed.jump_cursor(count - 1, 0);
3235 } else {
3236 ed.jump_cursor(0, 0);
3237 }
3238 move_first_non_whitespace(ed);
3239 if ed.cursor() != pre {
3240 push_jump(ed, pre);
3241 }
3242 }
3243 'e' => execute_motion(ed, Motion::WordEndBack, count),
3244 'E' => execute_motion(ed, Motion::BigWordEndBack, count),
3245 '_' => execute_motion(ed, Motion::LastNonBlank, count),
3247 'M' => execute_motion(ed, Motion::LineMiddle, count),
3249 'v' => {
3251 if let Some(snap) = ed.vim.last_visual {
3252 match snap.mode {
3253 Mode::Visual => {
3254 ed.vim.visual_anchor = snap.anchor;
3255 ed.vim.mode = Mode::Visual;
3256 }
3257 Mode::VisualLine => {
3258 ed.vim.visual_line_anchor = snap.anchor.0;
3259 ed.vim.mode = Mode::VisualLine;
3260 }
3261 Mode::VisualBlock => {
3262 ed.vim.block_anchor = snap.anchor;
3263 ed.vim.block_vcol = snap.block_vcol;
3264 ed.vim.mode = Mode::VisualBlock;
3265 }
3266 _ => {}
3267 }
3268 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
3269 }
3270 }
3271 'j' => execute_motion(ed, Motion::ScreenDown, count),
3275 'k' => execute_motion(ed, Motion::ScreenUp, count),
3276 'U' => {
3280 ed.vim.pending = Pending::Op {
3281 op: Operator::Uppercase,
3282 count1: count,
3283 };
3284 }
3285 'u' => {
3286 ed.vim.pending = Pending::Op {
3287 op: Operator::Lowercase,
3288 count1: count,
3289 };
3290 }
3291 '~' => {
3292 ed.vim.pending = Pending::Op {
3293 op: Operator::ToggleCase,
3294 count1: count,
3295 };
3296 }
3297 'q' => {
3298 ed.vim.pending = Pending::Op {
3301 op: Operator::Reflow,
3302 count1: count,
3303 };
3304 }
3305 'J' => {
3306 for _ in 0..count.max(1) {
3308 ed.push_undo();
3309 join_line_raw(ed);
3310 }
3311 if !ed.vim.replaying {
3312 ed.vim.last_change = Some(LastChange::JoinLine {
3313 count: count.max(1),
3314 });
3315 }
3316 }
3317 'd' => {
3318 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
3323 }
3324 'i' => {
3329 if let Some((row, col)) = ed.vim.last_insert_pos {
3330 ed.jump_cursor(row, col);
3331 }
3332 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3333 }
3334 ';' => walk_change_list(ed, -1, count.max(1)),
3337 ',' => walk_change_list(ed, 1, count.max(1)),
3338 '*' => execute_motion(
3342 ed,
3343 Motion::WordAtCursor {
3344 forward: true,
3345 whole_word: false,
3346 },
3347 count,
3348 ),
3349 '#' => execute_motion(
3350 ed,
3351 Motion::WordAtCursor {
3352 forward: false,
3353 whole_word: false,
3354 },
3355 count,
3356 ),
3357 _ => {}
3358 }
3359}
3360
3361fn handle_after_z<H: crate::types::Host>(
3362 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3363 input: Input,
3364) -> bool {
3365 let count = take_count(&mut ed.vim);
3366 if let Key::Char(ch) = input.key {
3369 apply_after_z(ed, ch, count);
3370 }
3371 true
3372}
3373
3374pub(crate) fn apply_after_z<H: crate::types::Host>(
3379 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3380 ch: char,
3381 count: usize,
3382) {
3383 use crate::editor::CursorScrollTarget;
3384 let row = ed.cursor().0;
3385 match ch {
3386 'z' => {
3387 ed.scroll_cursor_to(CursorScrollTarget::Center);
3388 ed.vim.viewport_pinned = true;
3389 }
3390 't' => {
3391 ed.scroll_cursor_to(CursorScrollTarget::Top);
3392 ed.vim.viewport_pinned = true;
3393 }
3394 'b' => {
3395 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
3396 ed.vim.viewport_pinned = true;
3397 }
3398 'o' => {
3403 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
3404 }
3405 'c' => {
3406 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
3407 }
3408 'a' => {
3409 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
3410 }
3411 'R' => {
3412 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
3413 }
3414 'M' => {
3415 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
3416 }
3417 'E' => {
3418 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
3419 }
3420 'd' => {
3421 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
3422 }
3423 'f' => {
3424 if matches!(
3425 ed.vim.mode,
3426 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
3427 ) {
3428 let anchor_row = match ed.vim.mode {
3431 Mode::VisualLine => ed.vim.visual_line_anchor,
3432 Mode::VisualBlock => ed.vim.block_anchor.0,
3433 _ => ed.vim.visual_anchor.0,
3434 };
3435 let cur = ed.cursor().0;
3436 let top = anchor_row.min(cur);
3437 let bot = anchor_row.max(cur);
3438 ed.apply_fold_op(crate::types::FoldOp::Add {
3439 start_row: top,
3440 end_row: bot,
3441 closed: true,
3442 });
3443 ed.vim.mode = Mode::Normal;
3444 } else {
3445 ed.vim.pending = Pending::Op {
3450 op: Operator::Fold,
3451 count1: count,
3452 };
3453 }
3454 }
3455 _ => {}
3456 }
3457}
3458
3459fn handle_replace<H: crate::types::Host>(
3460 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3461 input: Input,
3462) -> bool {
3463 if let Key::Char(ch) = input.key {
3464 if ed.vim.mode == Mode::VisualBlock {
3465 block_replace(ed, ch);
3466 return true;
3467 }
3468 let count = take_count(&mut ed.vim);
3469 replace_char(ed, ch, count.max(1));
3470 if !ed.vim.replaying {
3471 ed.vim.last_change = Some(LastChange::ReplaceChar {
3472 ch,
3473 count: count.max(1),
3474 });
3475 }
3476 }
3477 true
3478}
3479
3480fn handle_find_target<H: crate::types::Host>(
3481 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3482 input: Input,
3483 forward: bool,
3484 till: bool,
3485) -> bool {
3486 let Key::Char(ch) = input.key else {
3487 return true;
3488 };
3489 let count = take_count(&mut ed.vim);
3490 apply_find_char(ed, ch, forward, till, count.max(1));
3491 true
3492}
3493
3494pub(crate) fn apply_find_char<H: crate::types::Host>(
3500 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3501 ch: char,
3502 forward: bool,
3503 till: bool,
3504 count: usize,
3505) {
3506 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
3507 ed.vim.last_find = Some((ch, forward, till));
3508}
3509
3510pub(crate) fn apply_op_find_motion<H: crate::types::Host>(
3516 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3517 op: Operator,
3518 ch: char,
3519 forward: bool,
3520 till: bool,
3521 total_count: usize,
3522) {
3523 let motion = Motion::Find { ch, forward, till };
3524 apply_op_with_motion(ed, op, &motion, total_count);
3525 ed.vim.last_find = Some((ch, forward, till));
3526 if !ed.vim.replaying && op_is_change(op) {
3527 ed.vim.last_change = Some(LastChange::OpMotion {
3528 op,
3529 motion,
3530 count: total_count,
3531 inserted: None,
3532 });
3533 }
3534}
3535
3536fn handle_op_find_target<H: crate::types::Host>(
3537 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3538 input: Input,
3539 op: Operator,
3540 count1: usize,
3541 forward: bool,
3542 till: bool,
3543) -> bool {
3544 let Key::Char(ch) = input.key else {
3545 return true;
3546 };
3547 let count2 = take_count(&mut ed.vim);
3548 let total = count1.max(1) * count2.max(1);
3549 apply_op_find_motion(ed, op, ch, forward, till, total);
3550 true
3551}
3552
3553pub(crate) fn apply_op_text_obj_inner<H: crate::types::Host>(
3563 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3564 op: Operator,
3565 ch: char,
3566 inner: bool,
3567 _total_count: usize,
3568) -> bool {
3569 let obj = match ch {
3572 'w' => TextObject::Word { big: false },
3573 'W' => TextObject::Word { big: true },
3574 '"' | '\'' | '`' => TextObject::Quote(ch),
3575 '(' | ')' | 'b' => TextObject::Bracket('('),
3576 '[' | ']' => TextObject::Bracket('['),
3577 '{' | '}' | 'B' => TextObject::Bracket('{'),
3578 '<' | '>' => TextObject::Bracket('<'),
3579 'p' => TextObject::Paragraph,
3580 't' => TextObject::XmlTag,
3581 's' => TextObject::Sentence,
3582 _ => return false,
3583 };
3584 apply_op_with_text_object(ed, op, obj, inner);
3585 if !ed.vim.replaying && op_is_change(op) {
3586 ed.vim.last_change = Some(LastChange::OpTextObj {
3587 op,
3588 obj,
3589 inner,
3590 inserted: None,
3591 });
3592 }
3593 true
3594}
3595
3596fn handle_text_object<H: crate::types::Host>(
3597 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3598 input: Input,
3599 op: Operator,
3600 _count1: usize,
3601 inner: bool,
3602) -> bool {
3603 let Key::Char(ch) = input.key else {
3604 return true;
3605 };
3606 apply_op_text_obj_inner(ed, op, ch, inner, 1);
3609 true
3610}
3611
3612fn handle_visual_text_obj<H: crate::types::Host>(
3613 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3614 input: Input,
3615 inner: bool,
3616) -> bool {
3617 let Key::Char(ch) = input.key else {
3618 return true;
3619 };
3620 let obj = match ch {
3621 'w' => TextObject::Word { big: false },
3622 'W' => TextObject::Word { big: true },
3623 '"' | '\'' | '`' => TextObject::Quote(ch),
3624 '(' | ')' | 'b' => TextObject::Bracket('('),
3625 '[' | ']' => TextObject::Bracket('['),
3626 '{' | '}' | 'B' => TextObject::Bracket('{'),
3627 '<' | '>' => TextObject::Bracket('<'),
3628 'p' => TextObject::Paragraph,
3629 't' => TextObject::XmlTag,
3630 's' => TextObject::Sentence,
3631 _ => return true,
3632 };
3633 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3634 return true;
3635 };
3636 match kind {
3640 MotionKind::Linewise => {
3641 ed.vim.visual_line_anchor = start.0;
3642 ed.vim.mode = Mode::VisualLine;
3643 ed.jump_cursor(end.0, 0);
3644 }
3645 _ => {
3646 ed.vim.mode = Mode::Visual;
3647 ed.vim.visual_anchor = (start.0, start.1);
3648 let (er, ec) = retreat_one(ed, end);
3649 ed.jump_cursor(er, ec);
3650 }
3651 }
3652 true
3653}
3654
3655fn retreat_one<H: crate::types::Host>(
3657 ed: &Editor<hjkl_buffer::Buffer, H>,
3658 pos: (usize, usize),
3659) -> (usize, usize) {
3660 let (r, c) = pos;
3661 if c > 0 {
3662 (r, c - 1)
3663 } else if r > 0 {
3664 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
3665 (r - 1, prev_len)
3666 } else {
3667 (0, 0)
3668 }
3669}
3670
3671fn op_is_change(op: Operator) -> bool {
3672 matches!(op, Operator::Delete | Operator::Change)
3673}
3674
3675fn handle_normal_only<H: crate::types::Host>(
3678 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3679 input: &Input,
3680 count: usize,
3681) -> bool {
3682 if input.ctrl {
3683 return false;
3684 }
3685 match input.key {
3686 Key::Char('i') => {
3687 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3688 true
3689 }
3690 Key::Char('I') => {
3691 move_first_non_whitespace(ed);
3692 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
3693 true
3694 }
3695 Key::Char('a') => {
3696 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3697 ed.push_buffer_cursor_to_textarea();
3698 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
3699 true
3700 }
3701 Key::Char('A') => {
3702 crate::motions::move_line_end(&mut ed.buffer);
3703 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3704 ed.push_buffer_cursor_to_textarea();
3705 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
3706 true
3707 }
3708 Key::Char('R') => {
3709 begin_insert(ed, count.max(1), InsertReason::Replace);
3712 true
3713 }
3714 Key::Char('o') => {
3715 use hjkl_buffer::{Edit, Position};
3716 ed.push_undo();
3717 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
3720 ed.sync_buffer_content_from_textarea();
3721 let row = buf_cursor_pos(&ed.buffer).row;
3722 let line_chars = buf_line_chars(&ed.buffer, row);
3723 let prev_line = buf_line(&ed.buffer, row).unwrap_or_default();
3726 let indent = compute_enter_indent(&ed.settings, prev_line);
3727 ed.mutate_edit(Edit::InsertStr {
3728 at: Position::new(row, line_chars),
3729 text: format!("\n{indent}"),
3730 });
3731 ed.push_buffer_cursor_to_textarea();
3732 true
3733 }
3734 Key::Char('O') => {
3735 use hjkl_buffer::{Edit, Position};
3736 ed.push_undo();
3737 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
3738 ed.sync_buffer_content_from_textarea();
3739 let row = buf_cursor_pos(&ed.buffer).row;
3740 let indent = if row > 0 {
3744 let above = buf_line(&ed.buffer, row - 1).unwrap_or_default();
3745 compute_enter_indent(&ed.settings, above)
3746 } else {
3747 let cur = buf_line(&ed.buffer, row).unwrap_or_default();
3748 cur.chars()
3749 .take_while(|c| *c == ' ' || *c == '\t')
3750 .collect::<String>()
3751 };
3752 ed.mutate_edit(Edit::InsertStr {
3753 at: Position::new(row, 0),
3754 text: format!("{indent}\n"),
3755 });
3756 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3761 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
3762 let new_row = buf_cursor_pos(&ed.buffer).row;
3763 buf_set_cursor_rc(&mut ed.buffer, new_row, indent.chars().count());
3764 ed.push_buffer_cursor_to_textarea();
3765 true
3766 }
3767 Key::Char('x') => {
3768 do_char_delete(ed, true, count.max(1));
3769 if !ed.vim.replaying {
3770 ed.vim.last_change = Some(LastChange::CharDel {
3771 forward: true,
3772 count: count.max(1),
3773 });
3774 }
3775 true
3776 }
3777 Key::Char('X') => {
3778 do_char_delete(ed, false, count.max(1));
3779 if !ed.vim.replaying {
3780 ed.vim.last_change = Some(LastChange::CharDel {
3781 forward: false,
3782 count: count.max(1),
3783 });
3784 }
3785 true
3786 }
3787 Key::Char('~') => {
3788 for _ in 0..count.max(1) {
3789 ed.push_undo();
3790 toggle_case_at_cursor(ed);
3791 }
3792 if !ed.vim.replaying {
3793 ed.vim.last_change = Some(LastChange::ToggleCase {
3794 count: count.max(1),
3795 });
3796 }
3797 true
3798 }
3799 Key::Char('J') => {
3800 for _ in 0..count.max(1) {
3801 ed.push_undo();
3802 join_line(ed);
3803 }
3804 if !ed.vim.replaying {
3805 ed.vim.last_change = Some(LastChange::JoinLine {
3806 count: count.max(1),
3807 });
3808 }
3809 true
3810 }
3811 Key::Char('D') => {
3812 ed.push_undo();
3813 delete_to_eol(ed);
3814 crate::motions::move_left(&mut ed.buffer, 1);
3816 ed.push_buffer_cursor_to_textarea();
3817 if !ed.vim.replaying {
3818 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
3819 }
3820 true
3821 }
3822 Key::Char('Y') => {
3823 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
3825 true
3826 }
3827 Key::Char('C') => {
3828 ed.push_undo();
3829 delete_to_eol(ed);
3830 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
3831 true
3832 }
3833 Key::Char('s') => {
3834 use hjkl_buffer::{Edit, MotionKind, Position};
3835 ed.push_undo();
3836 ed.sync_buffer_content_from_textarea();
3837 for _ in 0..count.max(1) {
3838 let cursor = buf_cursor_pos(&ed.buffer);
3839 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
3840 if cursor.col >= line_chars {
3841 break;
3842 }
3843 ed.mutate_edit(Edit::DeleteRange {
3844 start: cursor,
3845 end: Position::new(cursor.row, cursor.col + 1),
3846 kind: MotionKind::Char,
3847 });
3848 }
3849 ed.push_buffer_cursor_to_textarea();
3850 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3851 if !ed.vim.replaying {
3853 ed.vim.last_change = Some(LastChange::OpMotion {
3854 op: Operator::Change,
3855 motion: Motion::Right,
3856 count: count.max(1),
3857 inserted: None,
3858 });
3859 }
3860 true
3861 }
3862 Key::Char('p') => {
3863 do_paste(ed, false, count.max(1));
3864 if !ed.vim.replaying {
3865 ed.vim.last_change = Some(LastChange::Paste {
3866 before: false,
3867 count: count.max(1),
3868 });
3869 }
3870 true
3871 }
3872 Key::Char('P') => {
3873 do_paste(ed, true, count.max(1));
3874 if !ed.vim.replaying {
3875 ed.vim.last_change = Some(LastChange::Paste {
3876 before: true,
3877 count: count.max(1),
3878 });
3879 }
3880 true
3881 }
3882 Key::Char('u') => {
3883 do_undo(ed);
3884 true
3885 }
3886 Key::Char('r') => {
3887 ed.vim.count = count;
3888 ed.vim.pending = Pending::Replace;
3889 true
3890 }
3891 Key::Char('/') => {
3892 enter_search(ed, true);
3893 true
3894 }
3895 Key::Char('?') => {
3896 enter_search(ed, false);
3897 true
3898 }
3899 Key::Char('.') => {
3900 replay_last_change(ed, count);
3901 true
3902 }
3903 _ => false,
3904 }
3905}
3906
3907fn begin_insert_noundo<H: crate::types::Host>(
3909 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3910 count: usize,
3911 reason: InsertReason,
3912) {
3913 let reason = if ed.vim.replaying {
3914 InsertReason::ReplayOnly
3915 } else {
3916 reason
3917 };
3918 let (row, _) = ed.cursor();
3919 ed.vim.insert_session = Some(InsertSession {
3920 count,
3921 row_min: row,
3922 row_max: row,
3923 before_lines: buf_lines_to_vec(&ed.buffer),
3924 reason,
3925 });
3926 ed.vim.mode = Mode::Insert;
3927}
3928
3929fn apply_op_with_motion<H: crate::types::Host>(
3932 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3933 op: Operator,
3934 motion: &Motion,
3935 count: usize,
3936) {
3937 let start = ed.cursor();
3938 apply_motion_cursor_ctx(ed, motion, count, true);
3943 let end = ed.cursor();
3944 let kind = motion_kind(motion);
3945 ed.jump_cursor(start.0, start.1);
3947 run_operator_over_range(ed, op, start, end, kind);
3948}
3949
3950fn apply_op_with_text_object<H: crate::types::Host>(
3951 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3952 op: Operator,
3953 obj: TextObject,
3954 inner: bool,
3955) {
3956 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3957 return;
3958 };
3959 ed.jump_cursor(start.0, start.1);
3960 run_operator_over_range(ed, op, start, end, kind);
3961}
3962
3963fn motion_kind(motion: &Motion) -> MotionKind {
3964 match motion {
3965 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => MotionKind::Linewise,
3966 Motion::FileTop | Motion::FileBottom => MotionKind::Linewise,
3967 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
3968 MotionKind::Linewise
3969 }
3970 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
3971 MotionKind::Inclusive
3972 }
3973 Motion::Find { .. } => MotionKind::Inclusive,
3974 Motion::MatchBracket => MotionKind::Inclusive,
3975 Motion::LineEnd => MotionKind::Inclusive,
3977 _ => MotionKind::Exclusive,
3978 }
3979}
3980
3981fn run_operator_over_range<H: crate::types::Host>(
3982 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3983 op: Operator,
3984 start: (usize, usize),
3985 end: (usize, usize),
3986 kind: MotionKind,
3987) {
3988 let (top, bot) = order(start, end);
3989 if top == bot {
3990 return;
3991 }
3992
3993 match op {
3994 Operator::Yank => {
3995 let text = read_vim_range(ed, top, bot, kind);
3996 if !text.is_empty() {
3997 ed.record_yank_to_host(text.clone());
3998 ed.record_yank(text, matches!(kind, MotionKind::Linewise));
3999 }
4000 let rbr = match kind {
4004 MotionKind::Linewise => {
4005 let last_col = buf_line_chars(&ed.buffer, bot.0).saturating_sub(1);
4006 (bot.0, last_col)
4007 }
4008 MotionKind::Inclusive => (bot.0, bot.1),
4009 MotionKind::Exclusive => (bot.0, bot.1.saturating_sub(1)),
4010 };
4011 ed.set_mark('[', top);
4012 ed.set_mark(']', rbr);
4013 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4014 ed.push_buffer_cursor_to_textarea();
4015 }
4016 Operator::Delete => {
4017 ed.push_undo();
4018 cut_vim_range(ed, top, bot, kind);
4019 if !matches!(kind, MotionKind::Linewise) {
4024 clamp_cursor_to_normal_mode(ed);
4025 }
4026 ed.vim.mode = Mode::Normal;
4027 let pos = ed.cursor();
4031 ed.set_mark('[', pos);
4032 ed.set_mark(']', pos);
4033 }
4034 Operator::Change => {
4035 ed.vim.change_mark_start = Some(top);
4040 ed.push_undo();
4041 cut_vim_range(ed, top, bot, kind);
4042 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4043 }
4044 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4045 apply_case_op_to_selection(ed, op, top, bot, kind);
4046 }
4047 Operator::Indent | Operator::Outdent => {
4048 ed.push_undo();
4051 if op == Operator::Indent {
4052 indent_rows(ed, top.0, bot.0, 1);
4053 } else {
4054 outdent_rows(ed, top.0, bot.0, 1);
4055 }
4056 ed.vim.mode = Mode::Normal;
4057 }
4058 Operator::Fold => {
4059 if bot.0 >= top.0 {
4063 ed.apply_fold_op(crate::types::FoldOp::Add {
4064 start_row: top.0,
4065 end_row: bot.0,
4066 closed: true,
4067 });
4068 }
4069 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4070 ed.push_buffer_cursor_to_textarea();
4071 ed.vim.mode = Mode::Normal;
4072 }
4073 Operator::Reflow => {
4074 ed.push_undo();
4075 reflow_rows(ed, top.0, bot.0);
4076 ed.vim.mode = Mode::Normal;
4077 }
4078 }
4079}
4080
4081fn reflow_rows<H: crate::types::Host>(
4086 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4087 top: usize,
4088 bot: usize,
4089) {
4090 let width = ed.settings().textwidth.max(1);
4091 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4092 let bot = bot.min(lines.len().saturating_sub(1));
4093 if top > bot {
4094 return;
4095 }
4096 let original = lines[top..=bot].to_vec();
4097 let mut wrapped: Vec<String> = Vec::new();
4098 let mut paragraph: Vec<String> = Vec::new();
4099 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
4100 if para.is_empty() {
4101 return;
4102 }
4103 let words = para.join(" ");
4104 let mut current = String::new();
4105 for word in words.split_whitespace() {
4106 let extra = if current.is_empty() {
4107 word.chars().count()
4108 } else {
4109 current.chars().count() + 1 + word.chars().count()
4110 };
4111 if extra > width && !current.is_empty() {
4112 out.push(std::mem::take(&mut current));
4113 current.push_str(word);
4114 } else if current.is_empty() {
4115 current.push_str(word);
4116 } else {
4117 current.push(' ');
4118 current.push_str(word);
4119 }
4120 }
4121 if !current.is_empty() {
4122 out.push(current);
4123 }
4124 para.clear();
4125 };
4126 for line in &original {
4127 if line.trim().is_empty() {
4128 flush(&mut paragraph, &mut wrapped, width);
4129 wrapped.push(String::new());
4130 } else {
4131 paragraph.push(line.clone());
4132 }
4133 }
4134 flush(&mut paragraph, &mut wrapped, width);
4135
4136 let after: Vec<String> = lines.split_off(bot + 1);
4138 lines.truncate(top);
4139 lines.extend(wrapped);
4140 lines.extend(after);
4141 ed.restore(lines, (top, 0));
4142 ed.mark_content_dirty();
4143}
4144
4145fn apply_case_op_to_selection<H: crate::types::Host>(
4151 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4152 op: Operator,
4153 top: (usize, usize),
4154 bot: (usize, usize),
4155 kind: MotionKind,
4156) {
4157 use hjkl_buffer::Edit;
4158 ed.push_undo();
4159 let saved_yank = ed.yank().to_string();
4160 let saved_yank_linewise = ed.vim.yank_linewise;
4161 let selection = cut_vim_range(ed, top, bot, kind);
4162 let transformed = match op {
4163 Operator::Uppercase => selection.to_uppercase(),
4164 Operator::Lowercase => selection.to_lowercase(),
4165 Operator::ToggleCase => toggle_case_str(&selection),
4166 _ => unreachable!(),
4167 };
4168 if !transformed.is_empty() {
4169 let cursor = buf_cursor_pos(&ed.buffer);
4170 ed.mutate_edit(Edit::InsertStr {
4171 at: cursor,
4172 text: transformed,
4173 });
4174 }
4175 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4176 ed.push_buffer_cursor_to_textarea();
4177 ed.set_yank(saved_yank);
4178 ed.vim.yank_linewise = saved_yank_linewise;
4179 ed.vim.mode = Mode::Normal;
4180}
4181
4182fn indent_rows<H: crate::types::Host>(
4187 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4188 top: usize,
4189 bot: usize,
4190 count: usize,
4191) {
4192 ed.sync_buffer_content_from_textarea();
4193 let width = ed.settings().shiftwidth * count.max(1);
4194 let pad: String = " ".repeat(width);
4195 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4196 let bot = bot.min(lines.len().saturating_sub(1));
4197 for line in lines.iter_mut().take(bot + 1).skip(top) {
4198 if !line.is_empty() {
4199 line.insert_str(0, &pad);
4200 }
4201 }
4202 ed.restore(lines, (top, 0));
4205 move_first_non_whitespace(ed);
4206}
4207
4208fn outdent_rows<H: crate::types::Host>(
4212 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4213 top: usize,
4214 bot: usize,
4215 count: usize,
4216) {
4217 ed.sync_buffer_content_from_textarea();
4218 let width = ed.settings().shiftwidth * count.max(1);
4219 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4220 let bot = bot.min(lines.len().saturating_sub(1));
4221 for line in lines.iter_mut().take(bot + 1).skip(top) {
4222 let strip: usize = line
4223 .chars()
4224 .take(width)
4225 .take_while(|c| *c == ' ' || *c == '\t')
4226 .count();
4227 if strip > 0 {
4228 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
4229 line.drain(..byte_len);
4230 }
4231 }
4232 ed.restore(lines, (top, 0));
4233 move_first_non_whitespace(ed);
4234}
4235
4236fn toggle_case_str(s: &str) -> String {
4237 s.chars()
4238 .map(|c| {
4239 if c.is_lowercase() {
4240 c.to_uppercase().next().unwrap_or(c)
4241 } else if c.is_uppercase() {
4242 c.to_lowercase().next().unwrap_or(c)
4243 } else {
4244 c
4245 }
4246 })
4247 .collect()
4248}
4249
4250fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
4251 if a <= b { (a, b) } else { (b, a) }
4252}
4253
4254fn clamp_cursor_to_normal_mode<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
4259 let (row, col) = ed.cursor();
4260 let line_chars = buf_line_chars(&ed.buffer, row);
4261 let max_col = line_chars.saturating_sub(1);
4262 if col > max_col {
4263 buf_set_cursor_rc(&mut ed.buffer, row, max_col);
4264 ed.push_buffer_cursor_to_textarea();
4265 }
4266}
4267
4268fn execute_line_op<H: crate::types::Host>(
4271 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4272 op: Operator,
4273 count: usize,
4274) {
4275 let (row, col) = ed.cursor();
4276 let total = buf_row_count(&ed.buffer);
4277 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
4278
4279 match op {
4280 Operator::Yank => {
4281 let text = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4283 if !text.is_empty() {
4284 ed.record_yank_to_host(text.clone());
4285 ed.record_yank(text, true);
4286 }
4287 let last_col = buf_line_chars(&ed.buffer, end_row).saturating_sub(1);
4290 ed.set_mark('[', (row, 0));
4291 ed.set_mark(']', (end_row, last_col));
4292 buf_set_cursor_rc(&mut ed.buffer, row, col);
4293 ed.push_buffer_cursor_to_textarea();
4294 ed.vim.mode = Mode::Normal;
4295 }
4296 Operator::Delete => {
4297 ed.push_undo();
4298 let deleted_through_last = end_row + 1 >= total;
4299 cut_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4300 let total_after = buf_row_count(&ed.buffer);
4304 let raw_target = if deleted_through_last {
4305 row.saturating_sub(1).min(total_after.saturating_sub(1))
4306 } else {
4307 row.min(total_after.saturating_sub(1))
4308 };
4309 let target_row = if raw_target > 0
4315 && raw_target + 1 == total_after
4316 && buf_line(&ed.buffer, raw_target)
4317 .map(str::is_empty)
4318 .unwrap_or(false)
4319 {
4320 raw_target - 1
4321 } else {
4322 raw_target
4323 };
4324 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
4325 ed.push_buffer_cursor_to_textarea();
4326 move_first_non_whitespace(ed);
4327 ed.sticky_col = Some(ed.cursor().1);
4328 ed.vim.mode = Mode::Normal;
4329 let pos = ed.cursor();
4332 ed.set_mark('[', pos);
4333 ed.set_mark(']', pos);
4334 }
4335 Operator::Change => {
4336 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4340 ed.vim.change_mark_start = Some((row, 0));
4342 ed.push_undo();
4343 ed.sync_buffer_content_from_textarea();
4344 let payload = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4346 if end_row > row {
4347 ed.mutate_edit(Edit::DeleteRange {
4348 start: Position::new(row + 1, 0),
4349 end: Position::new(end_row, 0),
4350 kind: BufKind::Line,
4351 });
4352 }
4353 let line_chars = buf_line_chars(&ed.buffer, row);
4354 if line_chars > 0 {
4355 ed.mutate_edit(Edit::DeleteRange {
4356 start: Position::new(row, 0),
4357 end: Position::new(row, line_chars),
4358 kind: BufKind::Char,
4359 });
4360 }
4361 if !payload.is_empty() {
4362 ed.record_yank_to_host(payload.clone());
4363 ed.record_delete(payload, true);
4364 }
4365 buf_set_cursor_rc(&mut ed.buffer, row, 0);
4366 ed.push_buffer_cursor_to_textarea();
4367 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4368 }
4369 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4370 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), MotionKind::Linewise);
4374 move_first_non_whitespace(ed);
4377 }
4378 Operator::Indent | Operator::Outdent => {
4379 ed.push_undo();
4381 if op == Operator::Indent {
4382 indent_rows(ed, row, end_row, 1);
4383 } else {
4384 outdent_rows(ed, row, end_row, 1);
4385 }
4386 ed.sticky_col = Some(ed.cursor().1);
4387 ed.vim.mode = Mode::Normal;
4388 }
4389 Operator::Fold => unreachable!("Fold has no line-op double"),
4391 Operator::Reflow => {
4392 ed.push_undo();
4394 reflow_rows(ed, row, end_row);
4395 move_first_non_whitespace(ed);
4396 ed.sticky_col = Some(ed.cursor().1);
4397 ed.vim.mode = Mode::Normal;
4398 }
4399 }
4400}
4401
4402fn apply_visual_operator<H: crate::types::Host>(
4405 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4406 op: Operator,
4407) {
4408 match ed.vim.mode {
4409 Mode::VisualLine => {
4410 let cursor_row = buf_cursor_pos(&ed.buffer).row;
4411 let top = cursor_row.min(ed.vim.visual_line_anchor);
4412 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4413 ed.vim.yank_linewise = true;
4414 match op {
4415 Operator::Yank => {
4416 let text = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4417 if !text.is_empty() {
4418 ed.record_yank_to_host(text.clone());
4419 ed.record_yank(text, true);
4420 }
4421 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4422 ed.push_buffer_cursor_to_textarea();
4423 ed.vim.mode = Mode::Normal;
4424 }
4425 Operator::Delete => {
4426 ed.push_undo();
4427 cut_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4428 ed.vim.mode = Mode::Normal;
4429 }
4430 Operator::Change => {
4431 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4434 ed.push_undo();
4435 ed.sync_buffer_content_from_textarea();
4436 let payload = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4437 if bot > top {
4438 ed.mutate_edit(Edit::DeleteRange {
4439 start: Position::new(top + 1, 0),
4440 end: Position::new(bot, 0),
4441 kind: BufKind::Line,
4442 });
4443 }
4444 let line_chars = buf_line_chars(&ed.buffer, top);
4445 if line_chars > 0 {
4446 ed.mutate_edit(Edit::DeleteRange {
4447 start: Position::new(top, 0),
4448 end: Position::new(top, line_chars),
4449 kind: BufKind::Char,
4450 });
4451 }
4452 if !payload.is_empty() {
4453 ed.record_yank_to_host(payload.clone());
4454 ed.record_delete(payload, true);
4455 }
4456 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4457 ed.push_buffer_cursor_to_textarea();
4458 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4459 }
4460 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4461 let bot = buf_cursor_pos(&ed.buffer)
4462 .row
4463 .max(ed.vim.visual_line_anchor);
4464 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), MotionKind::Linewise);
4465 move_first_non_whitespace(ed);
4466 }
4467 Operator::Indent | Operator::Outdent => {
4468 ed.push_undo();
4469 let (cursor_row, _) = ed.cursor();
4470 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4471 if op == Operator::Indent {
4472 indent_rows(ed, top, bot, 1);
4473 } else {
4474 outdent_rows(ed, top, bot, 1);
4475 }
4476 ed.vim.mode = Mode::Normal;
4477 }
4478 Operator::Reflow => {
4479 ed.push_undo();
4480 let (cursor_row, _) = ed.cursor();
4481 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4482 reflow_rows(ed, top, bot);
4483 ed.vim.mode = Mode::Normal;
4484 }
4485 Operator::Fold => unreachable!("Visual zf takes its own path"),
4488 }
4489 }
4490 Mode::Visual => {
4491 ed.vim.yank_linewise = false;
4492 let anchor = ed.vim.visual_anchor;
4493 let cursor = ed.cursor();
4494 let (top, bot) = order(anchor, cursor);
4495 match op {
4496 Operator::Yank => {
4497 let text = read_vim_range(ed, top, bot, MotionKind::Inclusive);
4498 if !text.is_empty() {
4499 ed.record_yank_to_host(text.clone());
4500 ed.record_yank(text, false);
4501 }
4502 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4503 ed.push_buffer_cursor_to_textarea();
4504 ed.vim.mode = Mode::Normal;
4505 }
4506 Operator::Delete => {
4507 ed.push_undo();
4508 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4509 ed.vim.mode = Mode::Normal;
4510 }
4511 Operator::Change => {
4512 ed.push_undo();
4513 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4514 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4515 }
4516 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4517 let anchor = ed.vim.visual_anchor;
4519 let cursor = ed.cursor();
4520 let (top, bot) = order(anchor, cursor);
4521 apply_case_op_to_selection(ed, op, top, bot, MotionKind::Inclusive);
4522 }
4523 Operator::Indent | Operator::Outdent => {
4524 ed.push_undo();
4525 let anchor = ed.vim.visual_anchor;
4526 let cursor = ed.cursor();
4527 let (top, bot) = order(anchor, cursor);
4528 if op == Operator::Indent {
4529 indent_rows(ed, top.0, bot.0, 1);
4530 } else {
4531 outdent_rows(ed, top.0, bot.0, 1);
4532 }
4533 ed.vim.mode = Mode::Normal;
4534 }
4535 Operator::Reflow => {
4536 ed.push_undo();
4537 let anchor = ed.vim.visual_anchor;
4538 let cursor = ed.cursor();
4539 let (top, bot) = order(anchor, cursor);
4540 reflow_rows(ed, top.0, bot.0);
4541 ed.vim.mode = Mode::Normal;
4542 }
4543 Operator::Fold => unreachable!("Visual zf takes its own path"),
4544 }
4545 }
4546 Mode::VisualBlock => apply_block_operator(ed, op),
4547 _ => {}
4548 }
4549}
4550
4551fn block_bounds<H: crate::types::Host>(
4556 ed: &Editor<hjkl_buffer::Buffer, H>,
4557) -> (usize, usize, usize, usize) {
4558 let (ar, ac) = ed.vim.block_anchor;
4559 let (cr, _) = ed.cursor();
4560 let cc = ed.vim.block_vcol;
4561 let top = ar.min(cr);
4562 let bot = ar.max(cr);
4563 let left = ac.min(cc);
4564 let right = ac.max(cc);
4565 (top, bot, left, right)
4566}
4567
4568fn update_block_vcol<H: crate::types::Host>(
4573 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4574 motion: &Motion,
4575) {
4576 match motion {
4577 Motion::Left
4578 | Motion::Right
4579 | Motion::WordFwd
4580 | Motion::BigWordFwd
4581 | Motion::WordBack
4582 | Motion::BigWordBack
4583 | Motion::WordEnd
4584 | Motion::BigWordEnd
4585 | Motion::WordEndBack
4586 | Motion::BigWordEndBack
4587 | Motion::LineStart
4588 | Motion::FirstNonBlank
4589 | Motion::LineEnd
4590 | Motion::Find { .. }
4591 | Motion::FindRepeat { .. }
4592 | Motion::MatchBracket => {
4593 ed.vim.block_vcol = ed.cursor().1;
4594 }
4595 _ => {}
4597 }
4598}
4599
4600fn apply_block_operator<H: crate::types::Host>(
4605 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4606 op: Operator,
4607) {
4608 let (top, bot, left, right) = block_bounds(ed);
4609 let yank = block_yank(ed, top, bot, left, right);
4611
4612 match op {
4613 Operator::Yank => {
4614 if !yank.is_empty() {
4615 ed.record_yank_to_host(yank.clone());
4616 ed.record_yank(yank, false);
4617 }
4618 ed.vim.mode = Mode::Normal;
4619 ed.jump_cursor(top, left);
4620 }
4621 Operator::Delete => {
4622 ed.push_undo();
4623 delete_block_contents(ed, top, bot, left, right);
4624 if !yank.is_empty() {
4625 ed.record_yank_to_host(yank.clone());
4626 ed.record_delete(yank, false);
4627 }
4628 ed.vim.mode = Mode::Normal;
4629 ed.jump_cursor(top, left);
4630 }
4631 Operator::Change => {
4632 ed.push_undo();
4633 delete_block_contents(ed, top, bot, left, right);
4634 if !yank.is_empty() {
4635 ed.record_yank_to_host(yank.clone());
4636 ed.record_delete(yank, false);
4637 }
4638 ed.jump_cursor(top, left);
4639 begin_insert_noundo(
4640 ed,
4641 1,
4642 InsertReason::BlockChange {
4643 top,
4644 bot,
4645 col: left,
4646 },
4647 );
4648 }
4649 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4650 ed.push_undo();
4651 transform_block_case(ed, op, top, bot, left, right);
4652 ed.vim.mode = Mode::Normal;
4653 ed.jump_cursor(top, left);
4654 }
4655 Operator::Indent | Operator::Outdent => {
4656 ed.push_undo();
4660 if op == Operator::Indent {
4661 indent_rows(ed, top, bot, 1);
4662 } else {
4663 outdent_rows(ed, top, bot, 1);
4664 }
4665 ed.vim.mode = Mode::Normal;
4666 }
4667 Operator::Fold => unreachable!("Visual zf takes its own path"),
4668 Operator::Reflow => {
4669 ed.push_undo();
4673 reflow_rows(ed, top, bot);
4674 ed.vim.mode = Mode::Normal;
4675 }
4676 }
4677}
4678
4679fn transform_block_case<H: crate::types::Host>(
4683 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4684 op: Operator,
4685 top: usize,
4686 bot: usize,
4687 left: usize,
4688 right: usize,
4689) {
4690 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4691 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4692 let chars: Vec<char> = lines[r].chars().collect();
4693 if left >= chars.len() {
4694 continue;
4695 }
4696 let end = (right + 1).min(chars.len());
4697 let head: String = chars[..left].iter().collect();
4698 let mid: String = chars[left..end].iter().collect();
4699 let tail: String = chars[end..].iter().collect();
4700 let transformed = match op {
4701 Operator::Uppercase => mid.to_uppercase(),
4702 Operator::Lowercase => mid.to_lowercase(),
4703 Operator::ToggleCase => toggle_case_str(&mid),
4704 _ => mid,
4705 };
4706 lines[r] = format!("{head}{transformed}{tail}");
4707 }
4708 let saved_yank = ed.yank().to_string();
4709 let saved_linewise = ed.vim.yank_linewise;
4710 ed.restore(lines, (top, left));
4711 ed.set_yank(saved_yank);
4712 ed.vim.yank_linewise = saved_linewise;
4713}
4714
4715fn block_yank<H: crate::types::Host>(
4716 ed: &Editor<hjkl_buffer::Buffer, H>,
4717 top: usize,
4718 bot: usize,
4719 left: usize,
4720 right: usize,
4721) -> String {
4722 let lines = buf_lines_to_vec(&ed.buffer);
4723 let mut rows: Vec<String> = Vec::new();
4724 for r in top..=bot {
4725 let line = match lines.get(r) {
4726 Some(l) => l,
4727 None => break,
4728 };
4729 let chars: Vec<char> = line.chars().collect();
4730 let end = (right + 1).min(chars.len());
4731 if left >= chars.len() {
4732 rows.push(String::new());
4733 } else {
4734 rows.push(chars[left..end].iter().collect());
4735 }
4736 }
4737 rows.join("\n")
4738}
4739
4740fn delete_block_contents<H: crate::types::Host>(
4741 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4742 top: usize,
4743 bot: usize,
4744 left: usize,
4745 right: usize,
4746) {
4747 use hjkl_buffer::{Edit, MotionKind, Position};
4748 ed.sync_buffer_content_from_textarea();
4749 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
4750 if last_row < top {
4751 return;
4752 }
4753 ed.mutate_edit(Edit::DeleteRange {
4754 start: Position::new(top, left),
4755 end: Position::new(last_row, right),
4756 kind: MotionKind::Block,
4757 });
4758 ed.push_buffer_cursor_to_textarea();
4759}
4760
4761fn block_replace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, ch: char) {
4763 let (top, bot, left, right) = block_bounds(ed);
4764 ed.push_undo();
4765 ed.sync_buffer_content_from_textarea();
4766 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4767 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4768 let chars: Vec<char> = lines[r].chars().collect();
4769 if left >= chars.len() {
4770 continue;
4771 }
4772 let end = (right + 1).min(chars.len());
4773 let before: String = chars[..left].iter().collect();
4774 let middle: String = std::iter::repeat_n(ch, end - left).collect();
4775 let after: String = chars[end..].iter().collect();
4776 lines[r] = format!("{before}{middle}{after}");
4777 }
4778 reset_textarea_lines(ed, lines);
4779 ed.vim.mode = Mode::Normal;
4780 ed.jump_cursor(top, left);
4781}
4782
4783fn reset_textarea_lines<H: crate::types::Host>(
4787 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4788 lines: Vec<String>,
4789) {
4790 let cursor = ed.cursor();
4791 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
4792 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
4793 ed.mark_content_dirty();
4794}
4795
4796type Pos = (usize, usize);
4802
4803fn text_object_range<H: crate::types::Host>(
4807 ed: &Editor<hjkl_buffer::Buffer, H>,
4808 obj: TextObject,
4809 inner: bool,
4810) -> Option<(Pos, Pos, MotionKind)> {
4811 match obj {
4812 TextObject::Word { big } => {
4813 word_text_object(ed, inner, big).map(|(s, e)| (s, e, MotionKind::Exclusive))
4814 }
4815 TextObject::Quote(q) => {
4816 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4817 }
4818 TextObject::Bracket(open) => bracket_text_object(ed, open, inner),
4819 TextObject::Paragraph => {
4820 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Linewise))
4821 }
4822 TextObject::XmlTag => {
4823 tag_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4824 }
4825 TextObject::Sentence => {
4826 sentence_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4827 }
4828 }
4829}
4830
4831fn sentence_boundary<H: crate::types::Host>(
4835 ed: &Editor<hjkl_buffer::Buffer, H>,
4836 forward: bool,
4837) -> Option<(usize, usize)> {
4838 let lines = buf_lines_to_vec(&ed.buffer);
4839 if lines.is_empty() {
4840 return None;
4841 }
4842 let pos_to_idx = |pos: (usize, usize)| -> usize {
4843 let mut idx = 0;
4844 for line in lines.iter().take(pos.0) {
4845 idx += line.chars().count() + 1;
4846 }
4847 idx + pos.1
4848 };
4849 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4850 for (r, line) in lines.iter().enumerate() {
4851 let len = line.chars().count();
4852 if idx <= len {
4853 return (r, idx);
4854 }
4855 idx -= len + 1;
4856 }
4857 let last = lines.len().saturating_sub(1);
4858 (last, lines[last].chars().count())
4859 };
4860 let mut chars: Vec<char> = Vec::new();
4861 for (r, line) in lines.iter().enumerate() {
4862 chars.extend(line.chars());
4863 if r + 1 < lines.len() {
4864 chars.push('\n');
4865 }
4866 }
4867 if chars.is_empty() {
4868 return None;
4869 }
4870 let total = chars.len();
4871 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
4872 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4873
4874 if forward {
4875 let mut i = cursor_idx + 1;
4878 while i < total {
4879 if is_terminator(chars[i]) {
4880 while i + 1 < total && is_terminator(chars[i + 1]) {
4881 i += 1;
4882 }
4883 if i + 1 >= total {
4884 return None;
4885 }
4886 if chars[i + 1].is_whitespace() {
4887 let mut j = i + 1;
4888 while j < total && chars[j].is_whitespace() {
4889 j += 1;
4890 }
4891 if j >= total {
4892 return None;
4893 }
4894 return Some(idx_to_pos(j));
4895 }
4896 }
4897 i += 1;
4898 }
4899 None
4900 } else {
4901 let find_start = |from: usize| -> Option<usize> {
4905 let mut start = from;
4906 while start > 0 {
4907 let prev = chars[start - 1];
4908 if prev.is_whitespace() {
4909 let mut k = start - 1;
4910 while k > 0 && chars[k - 1].is_whitespace() {
4911 k -= 1;
4912 }
4913 if k > 0 && is_terminator(chars[k - 1]) {
4914 break;
4915 }
4916 }
4917 start -= 1;
4918 }
4919 while start < total && chars[start].is_whitespace() {
4920 start += 1;
4921 }
4922 (start < total).then_some(start)
4923 };
4924 let current_start = find_start(cursor_idx)?;
4925 if current_start < cursor_idx {
4926 return Some(idx_to_pos(current_start));
4927 }
4928 let mut k = current_start;
4931 while k > 0 && chars[k - 1].is_whitespace() {
4932 k -= 1;
4933 }
4934 if k == 0 {
4935 return None;
4936 }
4937 let prev_start = find_start(k - 1)?;
4938 Some(idx_to_pos(prev_start))
4939 }
4940}
4941
4942fn sentence_text_object<H: crate::types::Host>(
4948 ed: &Editor<hjkl_buffer::Buffer, H>,
4949 inner: bool,
4950) -> Option<((usize, usize), (usize, usize))> {
4951 let lines = buf_lines_to_vec(&ed.buffer);
4952 if lines.is_empty() {
4953 return None;
4954 }
4955 let pos_to_idx = |pos: (usize, usize)| -> usize {
4958 let mut idx = 0;
4959 for line in lines.iter().take(pos.0) {
4960 idx += line.chars().count() + 1;
4961 }
4962 idx + pos.1
4963 };
4964 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4965 for (r, line) in lines.iter().enumerate() {
4966 let len = line.chars().count();
4967 if idx <= len {
4968 return (r, idx);
4969 }
4970 idx -= len + 1;
4971 }
4972 let last = lines.len().saturating_sub(1);
4973 (last, lines[last].chars().count())
4974 };
4975 let mut chars: Vec<char> = Vec::new();
4976 for (r, line) in lines.iter().enumerate() {
4977 chars.extend(line.chars());
4978 if r + 1 < lines.len() {
4979 chars.push('\n');
4980 }
4981 }
4982 if chars.is_empty() {
4983 return None;
4984 }
4985
4986 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
4987 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4988
4989 let mut start = cursor_idx;
4993 while start > 0 {
4994 let prev = chars[start - 1];
4995 if prev.is_whitespace() {
4996 let mut k = start - 1;
5000 while k > 0 && chars[k - 1].is_whitespace() {
5001 k -= 1;
5002 }
5003 if k > 0 && is_terminator(chars[k - 1]) {
5004 break;
5005 }
5006 }
5007 start -= 1;
5008 }
5009 while start < chars.len() && chars[start].is_whitespace() {
5012 start += 1;
5013 }
5014 if start >= chars.len() {
5015 return None;
5016 }
5017
5018 let mut end = start;
5021 while end < chars.len() {
5022 if is_terminator(chars[end]) {
5023 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
5025 end += 1;
5026 }
5027 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
5030 break;
5031 }
5032 }
5033 end += 1;
5034 }
5035 let end_idx = (end + 1).min(chars.len());
5037
5038 let final_end = if inner {
5039 end_idx
5040 } else {
5041 let mut e = end_idx;
5045 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
5046 e += 1;
5047 }
5048 e
5049 };
5050
5051 Some((idx_to_pos(start), idx_to_pos(final_end)))
5052}
5053
5054fn tag_text_object<H: crate::types::Host>(
5058 ed: &Editor<hjkl_buffer::Buffer, H>,
5059 inner: bool,
5060) -> Option<((usize, usize), (usize, usize))> {
5061 let lines = buf_lines_to_vec(&ed.buffer);
5062 if lines.is_empty() {
5063 return None;
5064 }
5065 let pos_to_idx = |pos: (usize, usize)| -> usize {
5069 let mut idx = 0;
5070 for line in lines.iter().take(pos.0) {
5071 idx += line.chars().count() + 1;
5072 }
5073 idx + pos.1
5074 };
5075 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
5076 for (r, line) in lines.iter().enumerate() {
5077 let len = line.chars().count();
5078 if idx <= len {
5079 return (r, idx);
5080 }
5081 idx -= len + 1;
5082 }
5083 let last = lines.len().saturating_sub(1);
5084 (last, lines[last].chars().count())
5085 };
5086 let mut chars: Vec<char> = Vec::new();
5087 for (r, line) in lines.iter().enumerate() {
5088 chars.extend(line.chars());
5089 if r + 1 < lines.len() {
5090 chars.push('\n');
5091 }
5092 }
5093 let cursor_idx = pos_to_idx(ed.cursor());
5094
5095 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
5103 let mut next_after: Option<(usize, usize, usize, usize)> = None;
5104 let mut i = 0;
5105 while i < chars.len() {
5106 if chars[i] != '<' {
5107 i += 1;
5108 continue;
5109 }
5110 let mut j = i + 1;
5111 while j < chars.len() && chars[j] != '>' {
5112 j += 1;
5113 }
5114 if j >= chars.len() {
5115 break;
5116 }
5117 let inside: String = chars[i + 1..j].iter().collect();
5118 let close_end = j + 1;
5119 let trimmed = inside.trim();
5120 if trimmed.starts_with('!') || trimmed.starts_with('?') {
5121 i = close_end;
5122 continue;
5123 }
5124 if let Some(rest) = trimmed.strip_prefix('/') {
5125 let name = rest.split_whitespace().next().unwrap_or("").to_string();
5126 if !name.is_empty()
5127 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
5128 {
5129 let (open_start, content_start, _) = stack[stack_idx].clone();
5130 stack.truncate(stack_idx);
5131 let content_end = i;
5132 let candidate = (open_start, content_start, content_end, close_end);
5133 if cursor_idx >= content_start && cursor_idx <= content_end {
5134 innermost = match innermost {
5135 Some((_, cs, ce, _)) if cs <= content_start && content_end <= ce => {
5136 Some(candidate)
5137 }
5138 None => Some(candidate),
5139 existing => existing,
5140 };
5141 } else if open_start >= cursor_idx && next_after.is_none() {
5142 next_after = Some(candidate);
5143 }
5144 }
5145 } else if !trimmed.ends_with('/') {
5146 let name: String = trimmed
5147 .split(|c: char| c.is_whitespace() || c == '/')
5148 .next()
5149 .unwrap_or("")
5150 .to_string();
5151 if !name.is_empty() {
5152 stack.push((i, close_end, name));
5153 }
5154 }
5155 i = close_end;
5156 }
5157
5158 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
5159 if inner {
5160 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
5161 } else {
5162 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
5163 }
5164}
5165
5166fn is_wordchar(c: char) -> bool {
5167 c.is_alphanumeric() || c == '_'
5168}
5169
5170pub(crate) use hjkl_buffer::is_keyword_char;
5174
5175fn word_text_object<H: crate::types::Host>(
5176 ed: &Editor<hjkl_buffer::Buffer, H>,
5177 inner: bool,
5178 big: bool,
5179) -> Option<((usize, usize), (usize, usize))> {
5180 let (row, col) = ed.cursor();
5181 let line = buf_line(&ed.buffer, row)?;
5182 let chars: Vec<char> = line.chars().collect();
5183 if chars.is_empty() {
5184 return None;
5185 }
5186 let at = col.min(chars.len().saturating_sub(1));
5187 let classify = |c: char| -> u8 {
5188 if c.is_whitespace() {
5189 0
5190 } else if big || is_wordchar(c) {
5191 1
5192 } else {
5193 2
5194 }
5195 };
5196 let cls = classify(chars[at]);
5197 let mut start = at;
5198 while start > 0 && classify(chars[start - 1]) == cls {
5199 start -= 1;
5200 }
5201 let mut end = at;
5202 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
5203 end += 1;
5204 }
5205 let char_byte = |i: usize| {
5207 if i >= chars.len() {
5208 line.len()
5209 } else {
5210 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
5211 }
5212 };
5213 let mut start_col = char_byte(start);
5214 let mut end_col = char_byte(end + 1);
5216 if !inner {
5217 let mut t = end + 1;
5219 let mut included_trailing = false;
5220 while t < chars.len() && chars[t].is_whitespace() {
5221 included_trailing = true;
5222 t += 1;
5223 }
5224 if included_trailing {
5225 end_col = char_byte(t);
5226 } else {
5227 let mut s = start;
5228 while s > 0 && chars[s - 1].is_whitespace() {
5229 s -= 1;
5230 }
5231 start_col = char_byte(s);
5232 }
5233 }
5234 Some(((row, start_col), (row, end_col)))
5235}
5236
5237fn quote_text_object<H: crate::types::Host>(
5238 ed: &Editor<hjkl_buffer::Buffer, H>,
5239 q: char,
5240 inner: bool,
5241) -> Option<((usize, usize), (usize, usize))> {
5242 let (row, col) = ed.cursor();
5243 let line = buf_line(&ed.buffer, row)?;
5244 let bytes = line.as_bytes();
5245 let q_byte = q as u8;
5246 let mut positions: Vec<usize> = Vec::new();
5248 for (i, &b) in bytes.iter().enumerate() {
5249 if b == q_byte {
5250 positions.push(i);
5251 }
5252 }
5253 if positions.len() < 2 {
5254 return None;
5255 }
5256 let mut open_idx: Option<usize> = None;
5257 let mut close_idx: Option<usize> = None;
5258 for pair in positions.chunks(2) {
5259 if pair.len() < 2 {
5260 break;
5261 }
5262 if col >= pair[0] && col <= pair[1] {
5263 open_idx = Some(pair[0]);
5264 close_idx = Some(pair[1]);
5265 break;
5266 }
5267 if col < pair[0] {
5268 open_idx = Some(pair[0]);
5269 close_idx = Some(pair[1]);
5270 break;
5271 }
5272 }
5273 let open = open_idx?;
5274 let close = close_idx?;
5275 if inner {
5277 if close <= open + 1 {
5278 return None;
5279 }
5280 Some(((row, open + 1), (row, close)))
5281 } else {
5282 let after_close = close + 1; if after_close < bytes.len() && bytes[after_close].is_ascii_whitespace() {
5289 let mut end = after_close;
5291 while end < bytes.len() && bytes[end].is_ascii_whitespace() {
5292 end += 1;
5293 }
5294 Some(((row, open), (row, end)))
5295 } else if open > 0 && bytes[open - 1].is_ascii_whitespace() {
5296 let mut start = open;
5298 while start > 0 && bytes[start - 1].is_ascii_whitespace() {
5299 start -= 1;
5300 }
5301 Some(((row, start), (row, close + 1)))
5302 } else {
5303 Some(((row, open), (row, close + 1)))
5304 }
5305 }
5306}
5307
5308fn bracket_text_object<H: crate::types::Host>(
5309 ed: &Editor<hjkl_buffer::Buffer, H>,
5310 open: char,
5311 inner: bool,
5312) -> Option<(Pos, Pos, MotionKind)> {
5313 let close = match open {
5314 '(' => ')',
5315 '[' => ']',
5316 '{' => '}',
5317 '<' => '>',
5318 _ => return None,
5319 };
5320 let (row, col) = ed.cursor();
5321 let lines = buf_lines_to_vec(&ed.buffer);
5322 let lines = lines.as_slice();
5323 let open_pos = find_open_bracket(lines, row, col, open, close)
5328 .or_else(|| find_next_open(lines, row, col, open))?;
5329 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
5330 if inner {
5332 if close_pos.0 > open_pos.0 + 1 {
5338 let inner_row_start = open_pos.0 + 1;
5340 let inner_row_end = close_pos.0 - 1;
5341 let end_col = lines
5342 .get(inner_row_end)
5343 .map(|l| l.chars().count())
5344 .unwrap_or(0);
5345 return Some((
5346 (inner_row_start, 0),
5347 (inner_row_end, end_col),
5348 MotionKind::Linewise,
5349 ));
5350 }
5351 let inner_start = advance_pos(lines, open_pos);
5352 if inner_start.0 > close_pos.0
5353 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
5354 {
5355 return None;
5356 }
5357 Some((inner_start, close_pos, MotionKind::Exclusive))
5358 } else {
5359 Some((
5360 open_pos,
5361 advance_pos(lines, close_pos),
5362 MotionKind::Exclusive,
5363 ))
5364 }
5365}
5366
5367fn find_open_bracket(
5368 lines: &[String],
5369 row: usize,
5370 col: usize,
5371 open: char,
5372 close: char,
5373) -> Option<(usize, usize)> {
5374 let mut depth: i32 = 0;
5375 let mut r = row;
5376 let mut c = col as isize;
5377 loop {
5378 let cur = &lines[r];
5379 let chars: Vec<char> = cur.chars().collect();
5380 if (c as usize) >= chars.len() {
5384 c = chars.len() as isize - 1;
5385 }
5386 while c >= 0 {
5387 let ch = chars[c as usize];
5388 if ch == close {
5389 depth += 1;
5390 } else if ch == open {
5391 if depth == 0 {
5392 return Some((r, c as usize));
5393 }
5394 depth -= 1;
5395 }
5396 c -= 1;
5397 }
5398 if r == 0 {
5399 return None;
5400 }
5401 r -= 1;
5402 c = lines[r].chars().count() as isize - 1;
5403 }
5404}
5405
5406fn find_close_bracket(
5407 lines: &[String],
5408 row: usize,
5409 start_col: usize,
5410 open: char,
5411 close: char,
5412) -> Option<(usize, usize)> {
5413 let mut depth: i32 = 0;
5414 let mut r = row;
5415 let mut c = start_col;
5416 loop {
5417 let cur = &lines[r];
5418 let chars: Vec<char> = cur.chars().collect();
5419 while c < chars.len() {
5420 let ch = chars[c];
5421 if ch == open {
5422 depth += 1;
5423 } else if ch == close {
5424 if depth == 0 {
5425 return Some((r, c));
5426 }
5427 depth -= 1;
5428 }
5429 c += 1;
5430 }
5431 if r + 1 >= lines.len() {
5432 return None;
5433 }
5434 r += 1;
5435 c = 0;
5436 }
5437}
5438
5439fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
5443 let mut r = row;
5444 let mut c = col;
5445 while r < lines.len() {
5446 let chars: Vec<char> = lines[r].chars().collect();
5447 while c < chars.len() {
5448 if chars[c] == open {
5449 return Some((r, c));
5450 }
5451 c += 1;
5452 }
5453 r += 1;
5454 c = 0;
5455 }
5456 None
5457}
5458
5459fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
5460 let (r, c) = pos;
5461 let line_len = lines[r].chars().count();
5462 if c < line_len {
5463 (r, c + 1)
5464 } else if r + 1 < lines.len() {
5465 (r + 1, 0)
5466 } else {
5467 pos
5468 }
5469}
5470
5471fn paragraph_text_object<H: crate::types::Host>(
5472 ed: &Editor<hjkl_buffer::Buffer, H>,
5473 inner: bool,
5474) -> Option<((usize, usize), (usize, usize))> {
5475 let (row, _) = ed.cursor();
5476 let lines = buf_lines_to_vec(&ed.buffer);
5477 if lines.is_empty() {
5478 return None;
5479 }
5480 let is_blank = |r: usize| lines.get(r).map(|s| s.trim().is_empty()).unwrap_or(true);
5482 if is_blank(row) {
5483 return None;
5484 }
5485 let mut top = row;
5486 while top > 0 && !is_blank(top - 1) {
5487 top -= 1;
5488 }
5489 let mut bot = row;
5490 while bot + 1 < lines.len() && !is_blank(bot + 1) {
5491 bot += 1;
5492 }
5493 if !inner && bot + 1 < lines.len() && is_blank(bot + 1) {
5495 bot += 1;
5496 }
5497 let end_col = lines[bot].chars().count();
5498 Some(((top, 0), (bot, end_col)))
5499}
5500
5501fn read_vim_range<H: crate::types::Host>(
5507 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5508 start: (usize, usize),
5509 end: (usize, usize),
5510 kind: MotionKind,
5511) -> String {
5512 let (top, bot) = order(start, end);
5513 ed.sync_buffer_content_from_textarea();
5514 let lines = buf_lines_to_vec(&ed.buffer);
5515 match kind {
5516 MotionKind::Linewise => {
5517 let lo = top.0;
5518 let hi = bot.0.min(lines.len().saturating_sub(1));
5519 let mut text = lines[lo..=hi].join("\n");
5520 text.push('\n');
5521 text
5522 }
5523 MotionKind::Inclusive | MotionKind::Exclusive => {
5524 let inclusive = matches!(kind, MotionKind::Inclusive);
5525 let mut out = String::new();
5527 for row in top.0..=bot.0 {
5528 let line = lines.get(row).map(String::as_str).unwrap_or("");
5529 let lo = if row == top.0 { top.1 } else { 0 };
5530 let hi_unclamped = if row == bot.0 {
5531 if inclusive { bot.1 + 1 } else { bot.1 }
5532 } else {
5533 line.chars().count() + 1
5534 };
5535 let row_chars: Vec<char> = line.chars().collect();
5536 let hi = hi_unclamped.min(row_chars.len());
5537 if lo < hi {
5538 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
5539 }
5540 if row < bot.0 {
5541 out.push('\n');
5542 }
5543 }
5544 out
5545 }
5546 }
5547}
5548
5549fn cut_vim_range<H: crate::types::Host>(
5558 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5559 start: (usize, usize),
5560 end: (usize, usize),
5561 kind: MotionKind,
5562) -> String {
5563 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
5564 let (top, bot) = order(start, end);
5565 ed.sync_buffer_content_from_textarea();
5566 let (buf_start, buf_end, buf_kind) = match kind {
5567 MotionKind::Linewise => (
5568 Position::new(top.0, 0),
5569 Position::new(bot.0, 0),
5570 BufKind::Line,
5571 ),
5572 MotionKind::Inclusive => {
5573 let line_chars = buf_line_chars(&ed.buffer, bot.0);
5574 let next = if bot.1 < line_chars {
5578 Position::new(bot.0, bot.1 + 1)
5579 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
5580 Position::new(bot.0 + 1, 0)
5581 } else {
5582 Position::new(bot.0, line_chars)
5583 };
5584 (Position::new(top.0, top.1), next, BufKind::Char)
5585 }
5586 MotionKind::Exclusive => (
5587 Position::new(top.0, top.1),
5588 Position::new(bot.0, bot.1),
5589 BufKind::Char,
5590 ),
5591 };
5592 let inverse = ed.mutate_edit(Edit::DeleteRange {
5593 start: buf_start,
5594 end: buf_end,
5595 kind: buf_kind,
5596 });
5597 let text = match inverse {
5598 Edit::InsertStr { text, .. } => text,
5599 _ => String::new(),
5600 };
5601 if !text.is_empty() {
5602 ed.record_yank_to_host(text.clone());
5603 ed.record_delete(text.clone(), matches!(kind, MotionKind::Linewise));
5604 }
5605 ed.push_buffer_cursor_to_textarea();
5606 text
5607}
5608
5609fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5615 use hjkl_buffer::{Edit, MotionKind, Position};
5616 ed.sync_buffer_content_from_textarea();
5617 let cursor = buf_cursor_pos(&ed.buffer);
5618 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5619 if cursor.col >= line_chars {
5620 return;
5621 }
5622 let inverse = ed.mutate_edit(Edit::DeleteRange {
5623 start: cursor,
5624 end: Position::new(cursor.row, line_chars),
5625 kind: MotionKind::Char,
5626 });
5627 if let Edit::InsertStr { text, .. } = inverse
5628 && !text.is_empty()
5629 {
5630 ed.record_yank_to_host(text.clone());
5631 ed.vim.yank_linewise = false;
5632 ed.set_yank(text);
5633 }
5634 buf_set_cursor_pos(&mut ed.buffer, cursor);
5635 ed.push_buffer_cursor_to_textarea();
5636}
5637
5638fn do_char_delete<H: crate::types::Host>(
5639 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5640 forward: bool,
5641 count: usize,
5642) {
5643 use hjkl_buffer::{Edit, MotionKind, Position};
5644 ed.push_undo();
5645 ed.sync_buffer_content_from_textarea();
5646 let mut deleted = String::new();
5649 for _ in 0..count {
5650 let cursor = buf_cursor_pos(&ed.buffer);
5651 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5652 if forward {
5653 if cursor.col >= line_chars {
5656 continue;
5657 }
5658 let inverse = ed.mutate_edit(Edit::DeleteRange {
5659 start: cursor,
5660 end: Position::new(cursor.row, cursor.col + 1),
5661 kind: MotionKind::Char,
5662 });
5663 if let Edit::InsertStr { text, .. } = inverse {
5664 deleted.push_str(&text);
5665 }
5666 } else {
5667 if cursor.col == 0 {
5669 continue;
5670 }
5671 let inverse = ed.mutate_edit(Edit::DeleteRange {
5672 start: Position::new(cursor.row, cursor.col - 1),
5673 end: cursor,
5674 kind: MotionKind::Char,
5675 });
5676 if let Edit::InsertStr { text, .. } = inverse {
5677 deleted = text + &deleted;
5680 }
5681 }
5682 }
5683 if !deleted.is_empty() {
5684 ed.record_yank_to_host(deleted.clone());
5685 ed.record_delete(deleted, false);
5686 }
5687 ed.push_buffer_cursor_to_textarea();
5688}
5689
5690fn adjust_number<H: crate::types::Host>(
5694 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5695 delta: i64,
5696) -> bool {
5697 use hjkl_buffer::{Edit, MotionKind, Position};
5698 ed.sync_buffer_content_from_textarea();
5699 let cursor = buf_cursor_pos(&ed.buffer);
5700 let row = cursor.row;
5701 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
5702 Some(l) => l.chars().collect(),
5703 None => return false,
5704 };
5705 let Some(digit_start) = (cursor.col..chars.len()).find(|&i| chars[i].is_ascii_digit()) else {
5706 return false;
5707 };
5708 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
5709 digit_start - 1
5710 } else {
5711 digit_start
5712 };
5713 let mut span_end = digit_start;
5714 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
5715 span_end += 1;
5716 }
5717 let s: String = chars[span_start..span_end].iter().collect();
5718 let Ok(n) = s.parse::<i64>() else {
5719 return false;
5720 };
5721 let new_s = n.saturating_add(delta).to_string();
5722
5723 ed.push_undo();
5724 let span_start_pos = Position::new(row, span_start);
5725 let span_end_pos = Position::new(row, span_end);
5726 ed.mutate_edit(Edit::DeleteRange {
5727 start: span_start_pos,
5728 end: span_end_pos,
5729 kind: MotionKind::Char,
5730 });
5731 ed.mutate_edit(Edit::InsertStr {
5732 at: span_start_pos,
5733 text: new_s.clone(),
5734 });
5735 let new_len = new_s.chars().count();
5736 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
5737 ed.push_buffer_cursor_to_textarea();
5738 true
5739}
5740
5741pub(crate) fn replace_char<H: crate::types::Host>(
5742 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5743 ch: char,
5744 count: usize,
5745) {
5746 use hjkl_buffer::{Edit, MotionKind, Position};
5747 ed.push_undo();
5748 ed.sync_buffer_content_from_textarea();
5749 for _ in 0..count {
5750 let cursor = buf_cursor_pos(&ed.buffer);
5751 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5752 if cursor.col >= line_chars {
5753 break;
5754 }
5755 ed.mutate_edit(Edit::DeleteRange {
5756 start: cursor,
5757 end: Position::new(cursor.row, cursor.col + 1),
5758 kind: MotionKind::Char,
5759 });
5760 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
5761 }
5762 crate::motions::move_left(&mut ed.buffer, 1);
5764 ed.push_buffer_cursor_to_textarea();
5765}
5766
5767fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5768 use hjkl_buffer::{Edit, MotionKind, Position};
5769 ed.sync_buffer_content_from_textarea();
5770 let cursor = buf_cursor_pos(&ed.buffer);
5771 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
5772 return;
5773 };
5774 let toggled = if c.is_uppercase() {
5775 c.to_lowercase().next().unwrap_or(c)
5776 } else {
5777 c.to_uppercase().next().unwrap_or(c)
5778 };
5779 ed.mutate_edit(Edit::DeleteRange {
5780 start: cursor,
5781 end: Position::new(cursor.row, cursor.col + 1),
5782 kind: MotionKind::Char,
5783 });
5784 ed.mutate_edit(Edit::InsertChar {
5785 at: cursor,
5786 ch: toggled,
5787 });
5788}
5789
5790fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5791 use hjkl_buffer::{Edit, Position};
5792 ed.sync_buffer_content_from_textarea();
5793 let row = buf_cursor_pos(&ed.buffer).row;
5794 if row + 1 >= buf_row_count(&ed.buffer) {
5795 return;
5796 }
5797 let cur_line = buf_line(&ed.buffer, row).unwrap_or("").to_string();
5798 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or("").to_string();
5799 let next_trimmed = next_raw.trim_start();
5800 let cur_chars = cur_line.chars().count();
5801 let next_chars = next_raw.chars().count();
5802 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
5805 " "
5806 } else {
5807 ""
5808 };
5809 let joined = format!("{cur_line}{separator}{next_trimmed}");
5810 ed.mutate_edit(Edit::Replace {
5811 start: Position::new(row, 0),
5812 end: Position::new(row + 1, next_chars),
5813 with: joined,
5814 });
5815 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
5819 ed.push_buffer_cursor_to_textarea();
5820}
5821
5822fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5825 use hjkl_buffer::Edit;
5826 ed.sync_buffer_content_from_textarea();
5827 let row = buf_cursor_pos(&ed.buffer).row;
5828 if row + 1 >= buf_row_count(&ed.buffer) {
5829 return;
5830 }
5831 let join_col = buf_line_chars(&ed.buffer, row);
5832 ed.mutate_edit(Edit::JoinLines {
5833 row,
5834 count: 1,
5835 with_space: false,
5836 });
5837 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
5839 ed.push_buffer_cursor_to_textarea();
5840}
5841
5842fn do_paste<H: crate::types::Host>(
5843 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5844 before: bool,
5845 count: usize,
5846) {
5847 use hjkl_buffer::{Edit, Position};
5848 ed.push_undo();
5849 let selector = ed.vim.pending_register.take();
5854 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
5855 Some(slot) => (slot.text.clone(), slot.linewise),
5856 None => {
5862 let s = &ed.registers().unnamed;
5863 (s.text.clone(), s.linewise)
5864 }
5865 };
5866 let mut paste_mark: Option<((usize, usize), (usize, usize))> = None;
5870 for _ in 0..count {
5871 ed.sync_buffer_content_from_textarea();
5872 let yank = yank.clone();
5873 if yank.is_empty() {
5874 continue;
5875 }
5876 if linewise {
5877 let text = yank.trim_matches('\n').to_string();
5881 let row = buf_cursor_pos(&ed.buffer).row;
5882 let target_row = if before {
5883 ed.mutate_edit(Edit::InsertStr {
5884 at: Position::new(row, 0),
5885 text: format!("{text}\n"),
5886 });
5887 row
5888 } else {
5889 let line_chars = buf_line_chars(&ed.buffer, row);
5890 ed.mutate_edit(Edit::InsertStr {
5891 at: Position::new(row, line_chars),
5892 text: format!("\n{text}"),
5893 });
5894 row + 1
5895 };
5896 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
5897 crate::motions::move_first_non_blank(&mut ed.buffer);
5898 ed.push_buffer_cursor_to_textarea();
5899 let payload_lines = text.lines().count().max(1);
5901 let bot_row = target_row + payload_lines - 1;
5902 let bot_last_col = buf_line_chars(&ed.buffer, bot_row).saturating_sub(1);
5903 paste_mark = Some(((target_row, 0), (bot_row, bot_last_col)));
5904 } else {
5905 let cursor = buf_cursor_pos(&ed.buffer);
5909 let at = if before {
5910 cursor
5911 } else {
5912 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5913 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
5914 };
5915 ed.mutate_edit(Edit::InsertStr {
5916 at,
5917 text: yank.clone(),
5918 });
5919 crate::motions::move_left(&mut ed.buffer, 1);
5922 ed.push_buffer_cursor_to_textarea();
5923 let lo = (at.row, at.col);
5925 let hi = ed.cursor();
5926 paste_mark = Some((lo, hi));
5927 }
5928 }
5929 if let Some((lo, hi)) = paste_mark {
5930 ed.set_mark('[', lo);
5931 ed.set_mark(']', hi);
5932 }
5933 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
5935}
5936
5937pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5938 if let Some((lines, cursor)) = ed.undo_stack.pop() {
5939 let current = ed.snapshot();
5940 ed.redo_stack.push(current);
5941 ed.restore(lines, cursor);
5942 }
5943 ed.vim.mode = Mode::Normal;
5944 clamp_cursor_to_normal_mode(ed);
5948}
5949
5950pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5951 if let Some((lines, cursor)) = ed.redo_stack.pop() {
5952 let current = ed.snapshot();
5953 ed.undo_stack.push(current);
5954 ed.cap_undo();
5955 ed.restore(lines, cursor);
5956 }
5957 ed.vim.mode = Mode::Normal;
5958}
5959
5960fn replay_insert_and_finish<H: crate::types::Host>(
5967 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5968 text: &str,
5969) {
5970 use hjkl_buffer::{Edit, Position};
5971 let cursor = ed.cursor();
5972 ed.mutate_edit(Edit::InsertStr {
5973 at: Position::new(cursor.0, cursor.1),
5974 text: text.to_string(),
5975 });
5976 if ed.vim.insert_session.take().is_some() {
5977 if ed.cursor().1 > 0 {
5978 crate::motions::move_left(&mut ed.buffer, 1);
5979 ed.push_buffer_cursor_to_textarea();
5980 }
5981 ed.vim.mode = Mode::Normal;
5982 }
5983}
5984
5985fn replay_last_change<H: crate::types::Host>(
5986 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5987 outer_count: usize,
5988) {
5989 let Some(change) = ed.vim.last_change.clone() else {
5990 return;
5991 };
5992 ed.vim.replaying = true;
5993 let scale = if outer_count > 0 { outer_count } else { 1 };
5994 match change {
5995 LastChange::OpMotion {
5996 op,
5997 motion,
5998 count,
5999 inserted,
6000 } => {
6001 let total = count.max(1) * scale;
6002 apply_op_with_motion(ed, op, &motion, total);
6003 if let Some(text) = inserted {
6004 replay_insert_and_finish(ed, &text);
6005 }
6006 }
6007 LastChange::OpTextObj {
6008 op,
6009 obj,
6010 inner,
6011 inserted,
6012 } => {
6013 apply_op_with_text_object(ed, op, obj, inner);
6014 if let Some(text) = inserted {
6015 replay_insert_and_finish(ed, &text);
6016 }
6017 }
6018 LastChange::LineOp {
6019 op,
6020 count,
6021 inserted,
6022 } => {
6023 let total = count.max(1) * scale;
6024 execute_line_op(ed, op, total);
6025 if let Some(text) = inserted {
6026 replay_insert_and_finish(ed, &text);
6027 }
6028 }
6029 LastChange::CharDel { forward, count } => {
6030 do_char_delete(ed, forward, count * scale);
6031 }
6032 LastChange::ReplaceChar { ch, count } => {
6033 replace_char(ed, ch, count * scale);
6034 }
6035 LastChange::ToggleCase { count } => {
6036 for _ in 0..count * scale {
6037 ed.push_undo();
6038 toggle_case_at_cursor(ed);
6039 }
6040 }
6041 LastChange::JoinLine { count } => {
6042 for _ in 0..count * scale {
6043 ed.push_undo();
6044 join_line(ed);
6045 }
6046 }
6047 LastChange::Paste { before, count } => {
6048 do_paste(ed, before, count * scale);
6049 }
6050 LastChange::DeleteToEol { inserted } => {
6051 use hjkl_buffer::{Edit, Position};
6052 ed.push_undo();
6053 delete_to_eol(ed);
6054 if let Some(text) = inserted {
6055 let cursor = ed.cursor();
6056 ed.mutate_edit(Edit::InsertStr {
6057 at: Position::new(cursor.0, cursor.1),
6058 text,
6059 });
6060 }
6061 }
6062 LastChange::OpenLine { above, inserted } => {
6063 use hjkl_buffer::{Edit, Position};
6064 ed.push_undo();
6065 ed.sync_buffer_content_from_textarea();
6066 let row = buf_cursor_pos(&ed.buffer).row;
6067 if above {
6068 ed.mutate_edit(Edit::InsertStr {
6069 at: Position::new(row, 0),
6070 text: "\n".to_string(),
6071 });
6072 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
6073 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
6074 } else {
6075 let line_chars = buf_line_chars(&ed.buffer, row);
6076 ed.mutate_edit(Edit::InsertStr {
6077 at: Position::new(row, line_chars),
6078 text: "\n".to_string(),
6079 });
6080 }
6081 ed.push_buffer_cursor_to_textarea();
6082 let cursor = ed.cursor();
6083 ed.mutate_edit(Edit::InsertStr {
6084 at: Position::new(cursor.0, cursor.1),
6085 text: inserted,
6086 });
6087 }
6088 LastChange::InsertAt {
6089 entry,
6090 inserted,
6091 count,
6092 } => {
6093 use hjkl_buffer::{Edit, Position};
6094 ed.push_undo();
6095 match entry {
6096 InsertEntry::I => {}
6097 InsertEntry::ShiftI => move_first_non_whitespace(ed),
6098 InsertEntry::A => {
6099 crate::motions::move_right_to_end(&mut ed.buffer, 1);
6100 ed.push_buffer_cursor_to_textarea();
6101 }
6102 InsertEntry::ShiftA => {
6103 crate::motions::move_line_end(&mut ed.buffer);
6104 crate::motions::move_right_to_end(&mut ed.buffer, 1);
6105 ed.push_buffer_cursor_to_textarea();
6106 }
6107 }
6108 for _ in 0..count.max(1) {
6109 let cursor = ed.cursor();
6110 ed.mutate_edit(Edit::InsertStr {
6111 at: Position::new(cursor.0, cursor.1),
6112 text: inserted.clone(),
6113 });
6114 }
6115 }
6116 }
6117 ed.vim.replaying = false;
6118}
6119
6120fn extract_inserted(before: &str, after: &str) -> String {
6123 let before_chars: Vec<char> = before.chars().collect();
6124 let after_chars: Vec<char> = after.chars().collect();
6125 if after_chars.len() <= before_chars.len() {
6126 return String::new();
6127 }
6128 let prefix = before_chars
6129 .iter()
6130 .zip(after_chars.iter())
6131 .take_while(|(a, b)| a == b)
6132 .count();
6133 let max_suffix = before_chars.len() - prefix;
6134 let suffix = before_chars
6135 .iter()
6136 .rev()
6137 .zip(after_chars.iter().rev())
6138 .take(max_suffix)
6139 .take_while(|(a, b)| a == b)
6140 .count();
6141 after_chars[prefix..after_chars.len() - suffix]
6142 .iter()
6143 .collect()
6144}
6145
6146#[cfg(all(test, feature = "crossterm"))]
6149mod tests {
6150 use crate::VimMode;
6151 use crate::editor::Editor;
6152 use crate::types::Host;
6153 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
6154
6155 fn run_keys<H: crate::types::Host>(e: &mut Editor<hjkl_buffer::Buffer, H>, keys: &str) {
6156 let mut iter = keys.chars().peekable();
6160 while let Some(c) = iter.next() {
6161 if c == '<' {
6162 let mut tag = String::new();
6163 for ch in iter.by_ref() {
6164 if ch == '>' {
6165 break;
6166 }
6167 tag.push(ch);
6168 }
6169 let ev = match tag.as_str() {
6170 "Esc" => KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
6171 "CR" => KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
6172 "BS" => KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE),
6173 "Space" => KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE),
6174 "Up" => KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
6175 "Down" => KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
6176 "Left" => KeyEvent::new(KeyCode::Left, KeyModifiers::NONE),
6177 "Right" => KeyEvent::new(KeyCode::Right, KeyModifiers::NONE),
6178 "lt" => KeyEvent::new(KeyCode::Char('<'), KeyModifiers::NONE),
6182 s if s.starts_with("C-") => {
6183 let ch = s.chars().nth(2).unwrap();
6184 KeyEvent::new(KeyCode::Char(ch), KeyModifiers::CONTROL)
6185 }
6186 _ => continue,
6187 };
6188 e.handle_key(ev);
6189 } else {
6190 let mods = if c.is_uppercase() {
6191 KeyModifiers::SHIFT
6192 } else {
6193 KeyModifiers::NONE
6194 };
6195 e.handle_key(KeyEvent::new(KeyCode::Char(c), mods));
6196 }
6197 }
6198 }
6199
6200 fn editor_with(content: &str) -> Editor {
6201 let opts = crate::types::Options {
6206 shiftwidth: 2,
6207 ..crate::types::Options::default()
6208 };
6209 let mut e = Editor::new(
6210 hjkl_buffer::Buffer::new(),
6211 crate::types::DefaultHost::new(),
6212 opts,
6213 );
6214 e.set_content(content);
6215 e
6216 }
6217
6218 #[test]
6219 fn f_char_jumps_on_line() {
6220 let mut e = editor_with("hello world");
6221 run_keys(&mut e, "fw");
6222 assert_eq!(e.cursor(), (0, 6));
6223 }
6224
6225 #[test]
6226 fn cap_f_jumps_backward() {
6227 let mut e = editor_with("hello world");
6228 e.jump_cursor(0, 10);
6229 run_keys(&mut e, "Fo");
6230 assert_eq!(e.cursor().1, 7);
6231 }
6232
6233 #[test]
6234 fn t_stops_before_char() {
6235 let mut e = editor_with("hello");
6236 run_keys(&mut e, "tl");
6237 assert_eq!(e.cursor(), (0, 1));
6238 }
6239
6240 #[test]
6241 fn semicolon_repeats_find() {
6242 let mut e = editor_with("aa.bb.cc");
6243 run_keys(&mut e, "f.");
6244 assert_eq!(e.cursor().1, 2);
6245 run_keys(&mut e, ";");
6246 assert_eq!(e.cursor().1, 5);
6247 }
6248
6249 #[test]
6250 fn comma_repeats_find_reverse() {
6251 let mut e = editor_with("aa.bb.cc");
6252 run_keys(&mut e, "f.");
6253 run_keys(&mut e, ";");
6254 run_keys(&mut e, ",");
6255 assert_eq!(e.cursor().1, 2);
6256 }
6257
6258 #[test]
6259 fn di_quote_deletes_content() {
6260 let mut e = editor_with("foo \"bar\" baz");
6261 e.jump_cursor(0, 6); run_keys(&mut e, "di\"");
6263 assert_eq!(e.buffer().lines()[0], "foo \"\" baz");
6264 }
6265
6266 #[test]
6267 fn da_quote_deletes_with_quotes() {
6268 let mut e = editor_with("foo \"bar\" baz");
6271 e.jump_cursor(0, 6);
6272 run_keys(&mut e, "da\"");
6273 assert_eq!(e.buffer().lines()[0], "foo baz");
6274 }
6275
6276 #[test]
6277 fn ci_paren_deletes_and_inserts() {
6278 let mut e = editor_with("fn(a, b, c)");
6279 e.jump_cursor(0, 5);
6280 run_keys(&mut e, "ci(");
6281 assert_eq!(e.vim_mode(), VimMode::Insert);
6282 assert_eq!(e.buffer().lines()[0], "fn()");
6283 }
6284
6285 #[test]
6286 fn diw_deletes_inner_word() {
6287 let mut e = editor_with("hello world");
6288 e.jump_cursor(0, 2);
6289 run_keys(&mut e, "diw");
6290 assert_eq!(e.buffer().lines()[0], " world");
6291 }
6292
6293 #[test]
6294 fn daw_deletes_word_with_trailing_space() {
6295 let mut e = editor_with("hello world");
6296 run_keys(&mut e, "daw");
6297 assert_eq!(e.buffer().lines()[0], "world");
6298 }
6299
6300 #[test]
6301 fn percent_jumps_to_matching_bracket() {
6302 let mut e = editor_with("foo(bar)");
6303 e.jump_cursor(0, 3);
6304 run_keys(&mut e, "%");
6305 assert_eq!(e.cursor().1, 7);
6306 run_keys(&mut e, "%");
6307 assert_eq!(e.cursor().1, 3);
6308 }
6309
6310 #[test]
6311 fn dot_repeats_last_change() {
6312 let mut e = editor_with("aaa bbb ccc");
6313 run_keys(&mut e, "dw");
6314 assert_eq!(e.buffer().lines()[0], "bbb ccc");
6315 run_keys(&mut e, ".");
6316 assert_eq!(e.buffer().lines()[0], "ccc");
6317 }
6318
6319 #[test]
6320 fn dot_repeats_change_operator_with_text() {
6321 let mut e = editor_with("foo foo foo");
6322 run_keys(&mut e, "cwbar<Esc>");
6323 assert_eq!(e.buffer().lines()[0], "bar foo foo");
6324 run_keys(&mut e, "w");
6326 run_keys(&mut e, ".");
6327 assert_eq!(e.buffer().lines()[0], "bar bar foo");
6328 }
6329
6330 #[test]
6331 fn dot_repeats_x() {
6332 let mut e = editor_with("abcdef");
6333 run_keys(&mut e, "x");
6334 run_keys(&mut e, "..");
6335 assert_eq!(e.buffer().lines()[0], "def");
6336 }
6337
6338 #[test]
6339 fn count_operator_motion_compose() {
6340 let mut e = editor_with("one two three four five");
6341 run_keys(&mut e, "d3w");
6342 assert_eq!(e.buffer().lines()[0], "four five");
6343 }
6344
6345 #[test]
6346 fn two_dd_deletes_two_lines() {
6347 let mut e = editor_with("a\nb\nc");
6348 run_keys(&mut e, "2dd");
6349 assert_eq!(e.buffer().lines().len(), 1);
6350 assert_eq!(e.buffer().lines()[0], "c");
6351 }
6352
6353 #[test]
6358 fn dd_in_middle_puts_cursor_on_first_non_blank_of_next() {
6359 let mut e = editor_with("one\ntwo\n three\nfour");
6360 e.jump_cursor(1, 2);
6361 run_keys(&mut e, "dd");
6362 assert_eq!(e.buffer().lines()[1], " three");
6364 assert_eq!(e.cursor(), (1, 4));
6365 }
6366
6367 #[test]
6368 fn dd_on_last_line_puts_cursor_on_first_non_blank_of_prev() {
6369 let mut e = editor_with("one\n two\nthree");
6370 e.jump_cursor(2, 0);
6371 run_keys(&mut e, "dd");
6372 assert_eq!(e.buffer().lines().len(), 2);
6374 assert_eq!(e.cursor(), (1, 2));
6375 }
6376
6377 #[test]
6378 fn dd_on_only_line_leaves_empty_buffer_and_cursor_at_zero() {
6379 let mut e = editor_with("lonely");
6380 run_keys(&mut e, "dd");
6381 assert_eq!(e.buffer().lines().len(), 1);
6382 assert_eq!(e.buffer().lines()[0], "");
6383 assert_eq!(e.cursor(), (0, 0));
6384 }
6385
6386 #[test]
6387 fn count_dd_puts_cursor_on_first_non_blank_of_remaining() {
6388 let mut e = editor_with("a\nb\nc\n d\ne");
6389 e.jump_cursor(1, 0);
6391 run_keys(&mut e, "3dd");
6392 assert_eq!(e.buffer().lines(), &["a".to_string(), "e".to_string()]);
6393 assert_eq!(e.cursor(), (1, 0));
6394 }
6395
6396 #[test]
6397 fn dd_then_j_uses_first_non_blank_not_sticky_col() {
6398 let mut e = editor_with(" line one\n line two\n xyz!");
6417 e.jump_cursor(0, 8);
6419 assert_eq!(e.cursor(), (0, 8));
6420 run_keys(&mut e, "dd");
6423 assert_eq!(
6424 e.cursor(),
6425 (0, 4),
6426 "dd must place cursor on first-non-blank"
6427 );
6428 run_keys(&mut e, "j");
6432 let (row, col) = e.cursor();
6433 assert_eq!(row, 1);
6434 assert_eq!(
6435 col, 4,
6436 "after dd, j should use the column dd established (4), not pre-dd sticky_col (8)"
6437 );
6438 }
6439
6440 #[test]
6441 fn gu_lowercases_motion_range() {
6442 let mut e = editor_with("HELLO WORLD");
6443 run_keys(&mut e, "guw");
6444 assert_eq!(e.buffer().lines()[0], "hello WORLD");
6445 assert_eq!(e.cursor(), (0, 0));
6446 }
6447
6448 #[test]
6449 fn g_u_uppercases_text_object() {
6450 let mut e = editor_with("hello world");
6451 run_keys(&mut e, "gUiw");
6453 assert_eq!(e.buffer().lines()[0], "HELLO world");
6454 assert_eq!(e.cursor(), (0, 0));
6455 }
6456
6457 #[test]
6458 fn g_tilde_toggles_case_of_range() {
6459 let mut e = editor_with("Hello World");
6460 run_keys(&mut e, "g~iw");
6461 assert_eq!(e.buffer().lines()[0], "hELLO World");
6462 }
6463
6464 #[test]
6465 fn g_uu_uppercases_current_line() {
6466 let mut e = editor_with("select 1\nselect 2");
6467 run_keys(&mut e, "gUU");
6468 assert_eq!(e.buffer().lines()[0], "SELECT 1");
6469 assert_eq!(e.buffer().lines()[1], "select 2");
6470 }
6471
6472 #[test]
6473 fn gugu_lowercases_current_line() {
6474 let mut e = editor_with("FOO BAR\nBAZ");
6475 run_keys(&mut e, "gugu");
6476 assert_eq!(e.buffer().lines()[0], "foo bar");
6477 }
6478
6479 #[test]
6480 fn visual_u_uppercases_selection() {
6481 let mut e = editor_with("hello world");
6482 run_keys(&mut e, "veU");
6484 assert_eq!(e.buffer().lines()[0], "HELLO world");
6485 }
6486
6487 #[test]
6488 fn visual_line_u_lowercases_line() {
6489 let mut e = editor_with("HELLO WORLD\nOTHER");
6490 run_keys(&mut e, "Vu");
6491 assert_eq!(e.buffer().lines()[0], "hello world");
6492 assert_eq!(e.buffer().lines()[1], "OTHER");
6493 }
6494
6495 #[test]
6496 fn g_uu_with_count_uppercases_multiple_lines() {
6497 let mut e = editor_with("one\ntwo\nthree\nfour");
6498 run_keys(&mut e, "3gUU");
6500 assert_eq!(e.buffer().lines()[0], "ONE");
6501 assert_eq!(e.buffer().lines()[1], "TWO");
6502 assert_eq!(e.buffer().lines()[2], "THREE");
6503 assert_eq!(e.buffer().lines()[3], "four");
6504 }
6505
6506 #[test]
6507 fn double_gt_indents_current_line() {
6508 let mut e = editor_with("hello");
6509 run_keys(&mut e, ">>");
6510 assert_eq!(e.buffer().lines()[0], " hello");
6511 assert_eq!(e.cursor(), (0, 2));
6513 }
6514
6515 #[test]
6516 fn double_lt_outdents_current_line() {
6517 let mut e = editor_with(" hello");
6518 run_keys(&mut e, "<lt><lt>");
6519 assert_eq!(e.buffer().lines()[0], " hello");
6520 assert_eq!(e.cursor(), (0, 2));
6521 }
6522
6523 #[test]
6524 fn count_double_gt_indents_multiple_lines() {
6525 let mut e = editor_with("a\nb\nc\nd");
6526 run_keys(&mut e, "3>>");
6528 assert_eq!(e.buffer().lines()[0], " a");
6529 assert_eq!(e.buffer().lines()[1], " b");
6530 assert_eq!(e.buffer().lines()[2], " c");
6531 assert_eq!(e.buffer().lines()[3], "d");
6532 }
6533
6534 #[test]
6535 fn outdent_clips_ragged_leading_whitespace() {
6536 let mut e = editor_with(" x");
6539 run_keys(&mut e, "<lt><lt>");
6540 assert_eq!(e.buffer().lines()[0], "x");
6541 }
6542
6543 #[test]
6544 fn indent_motion_is_always_linewise() {
6545 let mut e = editor_with("foo bar");
6548 run_keys(&mut e, ">w");
6549 assert_eq!(e.buffer().lines()[0], " foo bar");
6550 }
6551
6552 #[test]
6553 fn indent_text_object_extends_over_paragraph() {
6554 let mut e = editor_with("a\nb\n\nc\nd");
6555 run_keys(&mut e, ">ap");
6557 assert_eq!(e.buffer().lines()[0], " a");
6558 assert_eq!(e.buffer().lines()[1], " b");
6559 assert_eq!(e.buffer().lines()[2], "");
6560 assert_eq!(e.buffer().lines()[3], "c");
6561 }
6562
6563 #[test]
6564 fn visual_line_indent_shifts_selected_rows() {
6565 let mut e = editor_with("x\ny\nz");
6566 run_keys(&mut e, "Vj>");
6568 assert_eq!(e.buffer().lines()[0], " x");
6569 assert_eq!(e.buffer().lines()[1], " y");
6570 assert_eq!(e.buffer().lines()[2], "z");
6571 }
6572
6573 #[test]
6574 fn outdent_empty_line_is_noop() {
6575 let mut e = editor_with("\nfoo");
6576 run_keys(&mut e, "<lt><lt>");
6577 assert_eq!(e.buffer().lines()[0], "");
6578 }
6579
6580 #[test]
6581 fn indent_skips_empty_lines() {
6582 let mut e = editor_with("");
6585 run_keys(&mut e, ">>");
6586 assert_eq!(e.buffer().lines()[0], "");
6587 }
6588
6589 #[test]
6590 fn insert_ctrl_t_indents_current_line() {
6591 let mut e = editor_with("x");
6592 run_keys(&mut e, "i<C-t>");
6594 assert_eq!(e.buffer().lines()[0], " x");
6595 assert_eq!(e.cursor(), (0, 2));
6598 }
6599
6600 #[test]
6601 fn insert_ctrl_d_outdents_current_line() {
6602 let mut e = editor_with(" x");
6603 run_keys(&mut e, "A<C-d>");
6605 assert_eq!(e.buffer().lines()[0], " x");
6606 }
6607
6608 #[test]
6609 fn h_at_col_zero_does_not_wrap_to_prev_line() {
6610 let mut e = editor_with("first\nsecond");
6611 e.jump_cursor(1, 0);
6612 run_keys(&mut e, "h");
6613 assert_eq!(e.cursor(), (1, 0));
6615 }
6616
6617 #[test]
6618 fn l_at_last_char_does_not_wrap_to_next_line() {
6619 let mut e = editor_with("ab\ncd");
6620 e.jump_cursor(0, 1);
6622 run_keys(&mut e, "l");
6623 assert_eq!(e.cursor(), (0, 1));
6625 }
6626
6627 #[test]
6628 fn count_l_clamps_at_line_end() {
6629 let mut e = editor_with("abcde");
6630 run_keys(&mut e, "20l");
6633 assert_eq!(e.cursor(), (0, 4));
6634 }
6635
6636 #[test]
6637 fn count_h_clamps_at_col_zero() {
6638 let mut e = editor_with("abcde");
6639 e.jump_cursor(0, 3);
6640 run_keys(&mut e, "20h");
6641 assert_eq!(e.cursor(), (0, 0));
6642 }
6643
6644 #[test]
6645 fn dl_on_last_char_still_deletes_it() {
6646 let mut e = editor_with("ab");
6650 e.jump_cursor(0, 1);
6651 run_keys(&mut e, "dl");
6652 assert_eq!(e.buffer().lines()[0], "a");
6653 }
6654
6655 #[test]
6656 fn case_op_preserves_yank_register() {
6657 let mut e = editor_with("target");
6658 run_keys(&mut e, "yy");
6659 let yank_before = e.yank().to_string();
6660 run_keys(&mut e, "gUU");
6662 assert_eq!(e.buffer().lines()[0], "TARGET");
6663 assert_eq!(
6664 e.yank(),
6665 yank_before,
6666 "case ops must preserve the yank buffer"
6667 );
6668 }
6669
6670 #[test]
6671 fn dap_deletes_paragraph() {
6672 let mut e = editor_with("a\nb\n\nc\nd");
6673 run_keys(&mut e, "dap");
6674 assert_eq!(e.buffer().lines().first().map(String::as_str), Some("c"));
6675 }
6676
6677 #[test]
6678 fn dit_deletes_inner_tag_content() {
6679 let mut e = editor_with("<b>hello</b>");
6680 e.jump_cursor(0, 4);
6682 run_keys(&mut e, "dit");
6683 assert_eq!(e.buffer().lines()[0], "<b></b>");
6684 }
6685
6686 #[test]
6687 fn dat_deletes_around_tag() {
6688 let mut e = editor_with("hi <b>foo</b> bye");
6689 e.jump_cursor(0, 6);
6690 run_keys(&mut e, "dat");
6691 assert_eq!(e.buffer().lines()[0], "hi bye");
6692 }
6693
6694 #[test]
6695 fn dit_picks_innermost_tag() {
6696 let mut e = editor_with("<a><b>x</b></a>");
6697 e.jump_cursor(0, 6);
6699 run_keys(&mut e, "dit");
6700 assert_eq!(e.buffer().lines()[0], "<a><b></b></a>");
6702 }
6703
6704 #[test]
6705 fn dat_innermost_tag_pair() {
6706 let mut e = editor_with("<a><b>x</b></a>");
6707 e.jump_cursor(0, 6);
6708 run_keys(&mut e, "dat");
6709 assert_eq!(e.buffer().lines()[0], "<a></a>");
6710 }
6711
6712 #[test]
6713 fn dit_outside_any_tag_no_op() {
6714 let mut e = editor_with("plain text");
6715 e.jump_cursor(0, 3);
6716 run_keys(&mut e, "dit");
6717 assert_eq!(e.buffer().lines()[0], "plain text");
6719 }
6720
6721 #[test]
6722 fn cit_changes_inner_tag_content() {
6723 let mut e = editor_with("<b>hello</b>");
6724 e.jump_cursor(0, 4);
6725 run_keys(&mut e, "citNEW<Esc>");
6726 assert_eq!(e.buffer().lines()[0], "<b>NEW</b>");
6727 }
6728
6729 #[test]
6730 fn cat_changes_around_tag() {
6731 let mut e = editor_with("hi <b>foo</b> bye");
6732 e.jump_cursor(0, 6);
6733 run_keys(&mut e, "catBAR<Esc>");
6734 assert_eq!(e.buffer().lines()[0], "hi BAR bye");
6735 }
6736
6737 #[test]
6738 fn yit_yanks_inner_tag_content() {
6739 let mut e = editor_with("<b>hello</b>");
6740 e.jump_cursor(0, 4);
6741 run_keys(&mut e, "yit");
6742 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6743 }
6744
6745 #[test]
6746 fn yat_yanks_full_tag_pair() {
6747 let mut e = editor_with("hi <b>foo</b> bye");
6748 e.jump_cursor(0, 6);
6749 run_keys(&mut e, "yat");
6750 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6751 }
6752
6753 #[test]
6754 fn vit_visually_selects_inner_tag() {
6755 let mut e = editor_with("<b>hello</b>");
6756 e.jump_cursor(0, 4);
6757 run_keys(&mut e, "vit");
6758 assert_eq!(e.vim_mode(), VimMode::Visual);
6759 run_keys(&mut e, "y");
6760 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6761 }
6762
6763 #[test]
6764 fn vat_visually_selects_around_tag() {
6765 let mut e = editor_with("x<b>foo</b>y");
6766 e.jump_cursor(0, 5);
6767 run_keys(&mut e, "vat");
6768 assert_eq!(e.vim_mode(), VimMode::Visual);
6769 run_keys(&mut e, "y");
6770 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6771 }
6772
6773 #[test]
6776 #[allow(non_snake_case)]
6777 fn diW_deletes_inner_big_word() {
6778 let mut e = editor_with("foo.bar baz");
6779 e.jump_cursor(0, 2);
6780 run_keys(&mut e, "diW");
6781 assert_eq!(e.buffer().lines()[0], " baz");
6783 }
6784
6785 #[test]
6786 #[allow(non_snake_case)]
6787 fn daW_deletes_around_big_word() {
6788 let mut e = editor_with("foo.bar baz");
6789 e.jump_cursor(0, 2);
6790 run_keys(&mut e, "daW");
6791 assert_eq!(e.buffer().lines()[0], "baz");
6792 }
6793
6794 #[test]
6795 fn di_double_quote_deletes_inside() {
6796 let mut e = editor_with("a \"hello\" b");
6797 e.jump_cursor(0, 4);
6798 run_keys(&mut e, "di\"");
6799 assert_eq!(e.buffer().lines()[0], "a \"\" b");
6800 }
6801
6802 #[test]
6803 fn da_double_quote_deletes_around() {
6804 let mut e = editor_with("a \"hello\" b");
6806 e.jump_cursor(0, 4);
6807 run_keys(&mut e, "da\"");
6808 assert_eq!(e.buffer().lines()[0], "a b");
6809 }
6810
6811 #[test]
6812 fn di_single_quote_deletes_inside() {
6813 let mut e = editor_with("x 'foo' y");
6814 e.jump_cursor(0, 4);
6815 run_keys(&mut e, "di'");
6816 assert_eq!(e.buffer().lines()[0], "x '' y");
6817 }
6818
6819 #[test]
6820 fn da_single_quote_deletes_around() {
6821 let mut e = editor_with("x 'foo' y");
6823 e.jump_cursor(0, 4);
6824 run_keys(&mut e, "da'");
6825 assert_eq!(e.buffer().lines()[0], "x y");
6826 }
6827
6828 #[test]
6829 fn di_backtick_deletes_inside() {
6830 let mut e = editor_with("p `q` r");
6831 e.jump_cursor(0, 3);
6832 run_keys(&mut e, "di`");
6833 assert_eq!(e.buffer().lines()[0], "p `` r");
6834 }
6835
6836 #[test]
6837 fn da_backtick_deletes_around() {
6838 let mut e = editor_with("p `q` r");
6840 e.jump_cursor(0, 3);
6841 run_keys(&mut e, "da`");
6842 assert_eq!(e.buffer().lines()[0], "p r");
6843 }
6844
6845 #[test]
6846 fn di_paren_deletes_inside() {
6847 let mut e = editor_with("f(arg)");
6848 e.jump_cursor(0, 3);
6849 run_keys(&mut e, "di(");
6850 assert_eq!(e.buffer().lines()[0], "f()");
6851 }
6852
6853 #[test]
6854 fn di_paren_alias_b_works() {
6855 let mut e = editor_with("f(arg)");
6856 e.jump_cursor(0, 3);
6857 run_keys(&mut e, "dib");
6858 assert_eq!(e.buffer().lines()[0], "f()");
6859 }
6860
6861 #[test]
6862 fn di_bracket_deletes_inside() {
6863 let mut e = editor_with("a[b,c]d");
6864 e.jump_cursor(0, 3);
6865 run_keys(&mut e, "di[");
6866 assert_eq!(e.buffer().lines()[0], "a[]d");
6867 }
6868
6869 #[test]
6870 fn da_bracket_deletes_around() {
6871 let mut e = editor_with("a[b,c]d");
6872 e.jump_cursor(0, 3);
6873 run_keys(&mut e, "da[");
6874 assert_eq!(e.buffer().lines()[0], "ad");
6875 }
6876
6877 #[test]
6878 fn di_brace_deletes_inside() {
6879 let mut e = editor_with("x{y}z");
6880 e.jump_cursor(0, 2);
6881 run_keys(&mut e, "di{");
6882 assert_eq!(e.buffer().lines()[0], "x{}z");
6883 }
6884
6885 #[test]
6886 fn da_brace_deletes_around() {
6887 let mut e = editor_with("x{y}z");
6888 e.jump_cursor(0, 2);
6889 run_keys(&mut e, "da{");
6890 assert_eq!(e.buffer().lines()[0], "xz");
6891 }
6892
6893 #[test]
6894 fn di_brace_alias_capital_b_works() {
6895 let mut e = editor_with("x{y}z");
6896 e.jump_cursor(0, 2);
6897 run_keys(&mut e, "diB");
6898 assert_eq!(e.buffer().lines()[0], "x{}z");
6899 }
6900
6901 #[test]
6902 fn di_angle_deletes_inside() {
6903 let mut e = editor_with("p<q>r");
6904 e.jump_cursor(0, 2);
6905 run_keys(&mut e, "di<lt>");
6907 assert_eq!(e.buffer().lines()[0], "p<>r");
6908 }
6909
6910 #[test]
6911 fn da_angle_deletes_around() {
6912 let mut e = editor_with("p<q>r");
6913 e.jump_cursor(0, 2);
6914 run_keys(&mut e, "da<lt>");
6915 assert_eq!(e.buffer().lines()[0], "pr");
6916 }
6917
6918 #[test]
6919 fn dip_deletes_inner_paragraph() {
6920 let mut e = editor_with("a\nb\nc\n\nd");
6921 e.jump_cursor(1, 0);
6922 run_keys(&mut e, "dip");
6923 assert_eq!(e.buffer().lines(), vec!["".to_string(), "d".into()]);
6926 }
6927
6928 #[test]
6931 fn sentence_motion_close_paren_jumps_forward() {
6932 let mut e = editor_with("Alpha. Beta. Gamma.");
6933 e.jump_cursor(0, 0);
6934 run_keys(&mut e, ")");
6935 assert_eq!(e.cursor(), (0, 7));
6937 run_keys(&mut e, ")");
6938 assert_eq!(e.cursor(), (0, 13));
6939 }
6940
6941 #[test]
6942 fn sentence_motion_open_paren_jumps_backward() {
6943 let mut e = editor_with("Alpha. Beta. Gamma.");
6944 e.jump_cursor(0, 13);
6945 run_keys(&mut e, "(");
6946 assert_eq!(e.cursor(), (0, 7));
6949 run_keys(&mut e, "(");
6950 assert_eq!(e.cursor(), (0, 0));
6951 }
6952
6953 #[test]
6954 fn sentence_motion_count() {
6955 let mut e = editor_with("A. B. C. D.");
6956 e.jump_cursor(0, 0);
6957 run_keys(&mut e, "3)");
6958 assert_eq!(e.cursor(), (0, 9));
6960 }
6961
6962 #[test]
6963 fn dis_deletes_inner_sentence() {
6964 let mut e = editor_with("First one. Second one. Third one.");
6965 e.jump_cursor(0, 13);
6966 run_keys(&mut e, "dis");
6967 assert_eq!(e.buffer().lines()[0], "First one. Third one.");
6969 }
6970
6971 #[test]
6972 fn das_deletes_around_sentence_with_trailing_space() {
6973 let mut e = editor_with("Alpha. Beta. Gamma.");
6974 e.jump_cursor(0, 8);
6975 run_keys(&mut e, "das");
6976 assert_eq!(e.buffer().lines()[0], "Alpha. Gamma.");
6979 }
6980
6981 #[test]
6982 fn dis_handles_double_terminator() {
6983 let mut e = editor_with("Wow!? Next.");
6984 e.jump_cursor(0, 1);
6985 run_keys(&mut e, "dis");
6986 assert_eq!(e.buffer().lines()[0], " Next.");
6989 }
6990
6991 #[test]
6992 fn dis_first_sentence_from_cursor_at_zero() {
6993 let mut e = editor_with("Alpha. Beta.");
6994 e.jump_cursor(0, 0);
6995 run_keys(&mut e, "dis");
6996 assert_eq!(e.buffer().lines()[0], " Beta.");
6997 }
6998
6999 #[test]
7000 fn yis_yanks_inner_sentence() {
7001 let mut e = editor_with("Hello world. Bye.");
7002 e.jump_cursor(0, 5);
7003 run_keys(&mut e, "yis");
7004 assert_eq!(e.registers().read('"').unwrap().text, "Hello world.");
7005 }
7006
7007 #[test]
7008 fn vis_visually_selects_inner_sentence() {
7009 let mut e = editor_with("First. Second.");
7010 e.jump_cursor(0, 1);
7011 run_keys(&mut e, "vis");
7012 assert_eq!(e.vim_mode(), VimMode::Visual);
7013 run_keys(&mut e, "y");
7014 assert_eq!(e.registers().read('"').unwrap().text, "First.");
7015 }
7016
7017 #[test]
7018 fn ciw_changes_inner_word() {
7019 let mut e = editor_with("hello world");
7020 e.jump_cursor(0, 1);
7021 run_keys(&mut e, "ciwHEY<Esc>");
7022 assert_eq!(e.buffer().lines()[0], "HEY world");
7023 }
7024
7025 #[test]
7026 fn yiw_yanks_inner_word() {
7027 let mut e = editor_with("hello world");
7028 e.jump_cursor(0, 1);
7029 run_keys(&mut e, "yiw");
7030 assert_eq!(e.registers().read('"').unwrap().text, "hello");
7031 }
7032
7033 #[test]
7034 fn viw_selects_inner_word() {
7035 let mut e = editor_with("hello world");
7036 e.jump_cursor(0, 2);
7037 run_keys(&mut e, "viw");
7038 assert_eq!(e.vim_mode(), VimMode::Visual);
7039 run_keys(&mut e, "y");
7040 assert_eq!(e.registers().read('"').unwrap().text, "hello");
7041 }
7042
7043 #[test]
7044 fn ci_paren_changes_inside() {
7045 let mut e = editor_with("f(old)");
7046 e.jump_cursor(0, 3);
7047 run_keys(&mut e, "ci(NEW<Esc>");
7048 assert_eq!(e.buffer().lines()[0], "f(NEW)");
7049 }
7050
7051 #[test]
7052 fn yi_double_quote_yanks_inside() {
7053 let mut e = editor_with("say \"hi there\" then");
7054 e.jump_cursor(0, 6);
7055 run_keys(&mut e, "yi\"");
7056 assert_eq!(e.registers().read('"').unwrap().text, "hi there");
7057 }
7058
7059 #[test]
7060 fn vap_visual_selects_around_paragraph() {
7061 let mut e = editor_with("a\nb\n\nc");
7062 e.jump_cursor(0, 0);
7063 run_keys(&mut e, "vap");
7064 assert_eq!(e.vim_mode(), VimMode::VisualLine);
7065 run_keys(&mut e, "y");
7066 let text = e.registers().read('"').unwrap().text.clone();
7068 assert!(text.starts_with("a\nb"));
7069 }
7070
7071 #[test]
7072 fn star_finds_next_occurrence() {
7073 let mut e = editor_with("foo bar foo baz");
7074 run_keys(&mut e, "*");
7075 assert_eq!(e.cursor().1, 8);
7076 }
7077
7078 #[test]
7079 fn star_skips_substring_match() {
7080 let mut e = editor_with("foo foobar baz");
7083 run_keys(&mut e, "*");
7084 assert_eq!(e.cursor().1, 0);
7085 }
7086
7087 #[test]
7088 fn g_star_matches_substring() {
7089 let mut e = editor_with("foo foobar baz");
7092 run_keys(&mut e, "g*");
7093 assert_eq!(e.cursor().1, 4);
7094 }
7095
7096 #[test]
7097 fn g_pound_matches_substring_backward() {
7098 let mut e = editor_with("foo foobar baz foo");
7101 run_keys(&mut e, "$b");
7102 assert_eq!(e.cursor().1, 15);
7103 run_keys(&mut e, "g#");
7104 assert_eq!(e.cursor().1, 4);
7105 }
7106
7107 #[test]
7108 fn n_repeats_last_search_forward() {
7109 let mut e = editor_with("foo bar foo baz foo");
7110 run_keys(&mut e, "/foo<CR>");
7113 assert_eq!(e.cursor().1, 8);
7114 run_keys(&mut e, "n");
7115 assert_eq!(e.cursor().1, 16);
7116 }
7117
7118 #[test]
7119 fn shift_n_reverses_search() {
7120 let mut e = editor_with("foo bar foo baz foo");
7121 run_keys(&mut e, "/foo<CR>");
7122 run_keys(&mut e, "n");
7123 assert_eq!(e.cursor().1, 16);
7124 run_keys(&mut e, "N");
7125 assert_eq!(e.cursor().1, 8);
7126 }
7127
7128 #[test]
7129 fn n_noop_without_pattern() {
7130 let mut e = editor_with("foo bar");
7131 run_keys(&mut e, "n");
7132 assert_eq!(e.cursor(), (0, 0));
7133 }
7134
7135 #[test]
7136 fn visual_line_preserves_cursor_column() {
7137 let mut e = editor_with("hello world\nanother one\nbye");
7140 run_keys(&mut e, "lllll"); run_keys(&mut e, "V");
7142 assert_eq!(e.vim_mode(), VimMode::VisualLine);
7143 assert_eq!(e.cursor(), (0, 5));
7144 run_keys(&mut e, "j");
7145 assert_eq!(e.cursor(), (1, 5));
7146 }
7147
7148 #[test]
7149 fn visual_line_yank_includes_trailing_newline() {
7150 let mut e = editor_with("aaa\nbbb\nccc");
7151 run_keys(&mut e, "Vjy");
7152 assert_eq!(e.last_yank.as_deref(), Some("aaa\nbbb\n"));
7154 }
7155
7156 #[test]
7157 fn visual_line_yank_last_line_trailing_newline() {
7158 let mut e = editor_with("aaa\nbbb\nccc");
7159 run_keys(&mut e, "jj");
7161 run_keys(&mut e, "Vy");
7162 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
7163 }
7164
7165 #[test]
7166 fn yy_on_last_line_has_trailing_newline() {
7167 let mut e = editor_with("aaa\nbbb\nccc");
7168 run_keys(&mut e, "jj");
7169 run_keys(&mut e, "yy");
7170 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
7171 }
7172
7173 #[test]
7174 fn yy_in_middle_has_trailing_newline() {
7175 let mut e = editor_with("aaa\nbbb\nccc");
7176 run_keys(&mut e, "j");
7177 run_keys(&mut e, "yy");
7178 assert_eq!(e.last_yank.as_deref(), Some("bbb\n"));
7179 }
7180
7181 #[test]
7182 fn di_single_quote() {
7183 let mut e = editor_with("say 'hello world' now");
7184 e.jump_cursor(0, 7);
7185 run_keys(&mut e, "di'");
7186 assert_eq!(e.buffer().lines()[0], "say '' now");
7187 }
7188
7189 #[test]
7190 fn da_single_quote() {
7191 let mut e = editor_with("say 'hello' now");
7193 e.jump_cursor(0, 7);
7194 run_keys(&mut e, "da'");
7195 assert_eq!(e.buffer().lines()[0], "say now");
7196 }
7197
7198 #[test]
7199 fn di_backtick() {
7200 let mut e = editor_with("say `hi` now");
7201 e.jump_cursor(0, 5);
7202 run_keys(&mut e, "di`");
7203 assert_eq!(e.buffer().lines()[0], "say `` now");
7204 }
7205
7206 #[test]
7207 fn di_brace() {
7208 let mut e = editor_with("fn { a; b; c }");
7209 e.jump_cursor(0, 7);
7210 run_keys(&mut e, "di{");
7211 assert_eq!(e.buffer().lines()[0], "fn {}");
7212 }
7213
7214 #[test]
7215 fn di_bracket() {
7216 let mut e = editor_with("arr[1, 2, 3]");
7217 e.jump_cursor(0, 5);
7218 run_keys(&mut e, "di[");
7219 assert_eq!(e.buffer().lines()[0], "arr[]");
7220 }
7221
7222 #[test]
7223 fn dab_deletes_around_paren() {
7224 let mut e = editor_with("fn(a, b) + 1");
7225 e.jump_cursor(0, 4);
7226 run_keys(&mut e, "dab");
7227 assert_eq!(e.buffer().lines()[0], "fn + 1");
7228 }
7229
7230 #[test]
7231 fn da_big_b_deletes_around_brace() {
7232 let mut e = editor_with("x = {a: 1}");
7233 e.jump_cursor(0, 6);
7234 run_keys(&mut e, "daB");
7235 assert_eq!(e.buffer().lines()[0], "x = ");
7236 }
7237
7238 #[test]
7239 fn di_big_w_deletes_bigword() {
7240 let mut e = editor_with("foo-bar baz");
7241 e.jump_cursor(0, 2);
7242 run_keys(&mut e, "diW");
7243 assert_eq!(e.buffer().lines()[0], " baz");
7244 }
7245
7246 #[test]
7247 fn visual_select_inner_word() {
7248 let mut e = editor_with("hello world");
7249 e.jump_cursor(0, 2);
7250 run_keys(&mut e, "viw");
7251 assert_eq!(e.vim_mode(), VimMode::Visual);
7252 run_keys(&mut e, "y");
7253 assert_eq!(e.last_yank.as_deref(), Some("hello"));
7254 }
7255
7256 #[test]
7257 fn visual_select_inner_quote() {
7258 let mut e = editor_with("foo \"bar\" baz");
7259 e.jump_cursor(0, 6);
7260 run_keys(&mut e, "vi\"");
7261 run_keys(&mut e, "y");
7262 assert_eq!(e.last_yank.as_deref(), Some("bar"));
7263 }
7264
7265 #[test]
7266 fn visual_select_inner_paren() {
7267 let mut e = editor_with("fn(a, b)");
7268 e.jump_cursor(0, 4);
7269 run_keys(&mut e, "vi(");
7270 run_keys(&mut e, "y");
7271 assert_eq!(e.last_yank.as_deref(), Some("a, b"));
7272 }
7273
7274 #[test]
7275 fn visual_select_outer_brace() {
7276 let mut e = editor_with("{x}");
7277 e.jump_cursor(0, 1);
7278 run_keys(&mut e, "va{");
7279 run_keys(&mut e, "y");
7280 assert_eq!(e.last_yank.as_deref(), Some("{x}"));
7281 }
7282
7283 #[test]
7284 fn ci_paren_forward_scans_when_cursor_before_pair() {
7285 let mut e = editor_with("foo(bar)");
7288 e.jump_cursor(0, 0);
7289 run_keys(&mut e, "ci(NEW<Esc>");
7290 assert_eq!(e.buffer().lines()[0], "foo(NEW)");
7291 }
7292
7293 #[test]
7294 fn ci_paren_forward_scans_across_lines() {
7295 let mut e = editor_with("first\nfoo(bar)\nlast");
7296 e.jump_cursor(0, 0);
7297 run_keys(&mut e, "ci(NEW<Esc>");
7298 assert_eq!(e.buffer().lines()[1], "foo(NEW)");
7299 }
7300
7301 #[test]
7302 fn ci_brace_forward_scans_when_cursor_before_pair() {
7303 let mut e = editor_with("let x = {y};");
7304 e.jump_cursor(0, 0);
7305 run_keys(&mut e, "ci{NEW<Esc>");
7306 assert_eq!(e.buffer().lines()[0], "let x = {NEW};");
7307 }
7308
7309 #[test]
7310 fn cit_forward_scans_when_cursor_before_tag() {
7311 let mut e = editor_with("text <b>hello</b> rest");
7314 e.jump_cursor(0, 0);
7315 run_keys(&mut e, "citNEW<Esc>");
7316 assert_eq!(e.buffer().lines()[0], "text <b>NEW</b> rest");
7317 }
7318
7319 #[test]
7320 fn dat_forward_scans_when_cursor_before_tag() {
7321 let mut e = editor_with("text <b>hello</b> rest");
7323 e.jump_cursor(0, 0);
7324 run_keys(&mut e, "dat");
7325 assert_eq!(e.buffer().lines()[0], "text rest");
7326 }
7327
7328 #[test]
7329 fn ci_paren_still_works_when_cursor_inside() {
7330 let mut e = editor_with("fn(a, b)");
7333 e.jump_cursor(0, 4);
7334 run_keys(&mut e, "ci(NEW<Esc>");
7335 assert_eq!(e.buffer().lines()[0], "fn(NEW)");
7336 }
7337
7338 #[test]
7339 fn caw_changes_word_with_trailing_space() {
7340 let mut e = editor_with("hello world");
7341 run_keys(&mut e, "cawfoo<Esc>");
7342 assert_eq!(e.buffer().lines()[0], "fooworld");
7343 }
7344
7345 #[test]
7346 fn visual_char_yank_preserves_raw_text() {
7347 let mut e = editor_with("hello world");
7348 run_keys(&mut e, "vllly");
7349 assert_eq!(e.last_yank.as_deref(), Some("hell"));
7350 }
7351
7352 #[test]
7353 fn single_line_visual_line_selects_full_line_on_yank() {
7354 let mut e = editor_with("hello world\nbye");
7355 run_keys(&mut e, "V");
7356 run_keys(&mut e, "y");
7359 assert_eq!(e.last_yank.as_deref(), Some("hello world\n"));
7360 }
7361
7362 #[test]
7363 fn visual_line_extends_both_directions() {
7364 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7365 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
7367 assert_eq!(e.cursor(), (3, 0));
7368 run_keys(&mut e, "k");
7369 assert_eq!(e.cursor(), (2, 0));
7371 run_keys(&mut e, "k");
7372 assert_eq!(e.cursor(), (1, 0));
7373 }
7374
7375 #[test]
7376 fn visual_char_preserves_cursor_column() {
7377 let mut e = editor_with("hello world");
7378 run_keys(&mut e, "lllll"); run_keys(&mut e, "v");
7380 assert_eq!(e.cursor(), (0, 5));
7381 run_keys(&mut e, "ll");
7382 assert_eq!(e.cursor(), (0, 7));
7383 }
7384
7385 #[test]
7386 fn visual_char_highlight_bounds_order() {
7387 let mut e = editor_with("abcdef");
7388 run_keys(&mut e, "lll"); run_keys(&mut e, "v");
7390 run_keys(&mut e, "hh"); assert_eq!(e.char_highlight(), Some(((0, 1), (0, 3))));
7393 }
7394
7395 #[test]
7396 fn visual_line_highlight_bounds() {
7397 let mut e = editor_with("a\nb\nc");
7398 run_keys(&mut e, "V");
7399 assert_eq!(e.line_highlight(), Some((0, 0)));
7400 run_keys(&mut e, "j");
7401 assert_eq!(e.line_highlight(), Some((0, 1)));
7402 run_keys(&mut e, "j");
7403 assert_eq!(e.line_highlight(), Some((0, 2)));
7404 }
7405
7406 #[test]
7409 fn h_moves_left() {
7410 let mut e = editor_with("hello");
7411 e.jump_cursor(0, 3);
7412 run_keys(&mut e, "h");
7413 assert_eq!(e.cursor(), (0, 2));
7414 }
7415
7416 #[test]
7417 fn l_moves_right() {
7418 let mut e = editor_with("hello");
7419 run_keys(&mut e, "l");
7420 assert_eq!(e.cursor(), (0, 1));
7421 }
7422
7423 #[test]
7424 fn k_moves_up() {
7425 let mut e = editor_with("a\nb\nc");
7426 e.jump_cursor(2, 0);
7427 run_keys(&mut e, "k");
7428 assert_eq!(e.cursor(), (1, 0));
7429 }
7430
7431 #[test]
7432 fn zero_moves_to_line_start() {
7433 let mut e = editor_with(" hello");
7434 run_keys(&mut e, "$");
7435 run_keys(&mut e, "0");
7436 assert_eq!(e.cursor().1, 0);
7437 }
7438
7439 #[test]
7440 fn caret_moves_to_first_non_blank() {
7441 let mut e = editor_with(" hello");
7442 run_keys(&mut e, "0");
7443 run_keys(&mut e, "^");
7444 assert_eq!(e.cursor().1, 4);
7445 }
7446
7447 #[test]
7448 fn dollar_moves_to_last_char() {
7449 let mut e = editor_with("hello");
7450 run_keys(&mut e, "$");
7451 assert_eq!(e.cursor().1, 4);
7452 }
7453
7454 #[test]
7455 fn dollar_on_empty_line_stays_at_col_zero() {
7456 let mut e = editor_with("");
7457 run_keys(&mut e, "$");
7458 assert_eq!(e.cursor().1, 0);
7459 }
7460
7461 #[test]
7462 fn w_jumps_to_next_word() {
7463 let mut e = editor_with("foo bar baz");
7464 run_keys(&mut e, "w");
7465 assert_eq!(e.cursor().1, 4);
7466 }
7467
7468 #[test]
7469 fn b_jumps_back_a_word() {
7470 let mut e = editor_with("foo bar");
7471 e.jump_cursor(0, 6);
7472 run_keys(&mut e, "b");
7473 assert_eq!(e.cursor().1, 4);
7474 }
7475
7476 #[test]
7477 fn e_jumps_to_word_end() {
7478 let mut e = editor_with("foo bar");
7479 run_keys(&mut e, "e");
7480 assert_eq!(e.cursor().1, 2);
7481 }
7482
7483 #[test]
7486 fn d_dollar_deletes_to_eol() {
7487 let mut e = editor_with("hello world");
7488 e.jump_cursor(0, 5);
7489 run_keys(&mut e, "d$");
7490 assert_eq!(e.buffer().lines()[0], "hello");
7491 }
7492
7493 #[test]
7494 fn d_zero_deletes_to_line_start() {
7495 let mut e = editor_with("hello world");
7496 e.jump_cursor(0, 6);
7497 run_keys(&mut e, "d0");
7498 assert_eq!(e.buffer().lines()[0], "world");
7499 }
7500
7501 #[test]
7502 fn d_caret_deletes_to_first_non_blank() {
7503 let mut e = editor_with(" hello");
7504 e.jump_cursor(0, 6);
7505 run_keys(&mut e, "d^");
7506 assert_eq!(e.buffer().lines()[0], " llo");
7507 }
7508
7509 #[test]
7510 fn d_capital_g_deletes_to_end_of_file() {
7511 let mut e = editor_with("a\nb\nc\nd");
7512 e.jump_cursor(1, 0);
7513 run_keys(&mut e, "dG");
7514 assert_eq!(e.buffer().lines(), &["a".to_string()]);
7515 }
7516
7517 #[test]
7518 fn d_gg_deletes_to_start_of_file() {
7519 let mut e = editor_with("a\nb\nc\nd");
7520 e.jump_cursor(2, 0);
7521 run_keys(&mut e, "dgg");
7522 assert_eq!(e.buffer().lines(), &["d".to_string()]);
7523 }
7524
7525 #[test]
7526 fn cw_is_ce_quirk() {
7527 let mut e = editor_with("foo bar");
7530 run_keys(&mut e, "cwxyz<Esc>");
7531 assert_eq!(e.buffer().lines()[0], "xyz bar");
7532 }
7533
7534 #[test]
7537 fn big_d_deletes_to_eol() {
7538 let mut e = editor_with("hello world");
7539 e.jump_cursor(0, 5);
7540 run_keys(&mut e, "D");
7541 assert_eq!(e.buffer().lines()[0], "hello");
7542 }
7543
7544 #[test]
7545 fn big_c_deletes_to_eol_and_inserts() {
7546 let mut e = editor_with("hello world");
7547 e.jump_cursor(0, 5);
7548 run_keys(&mut e, "C!<Esc>");
7549 assert_eq!(e.buffer().lines()[0], "hello!");
7550 }
7551
7552 #[test]
7553 fn j_joins_next_line_with_space() {
7554 let mut e = editor_with("hello\nworld");
7555 run_keys(&mut e, "J");
7556 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7557 }
7558
7559 #[test]
7560 fn j_strips_leading_whitespace_on_join() {
7561 let mut e = editor_with("hello\n world");
7562 run_keys(&mut e, "J");
7563 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7564 }
7565
7566 #[test]
7567 fn big_x_deletes_char_before_cursor() {
7568 let mut e = editor_with("hello");
7569 e.jump_cursor(0, 3);
7570 run_keys(&mut e, "X");
7571 assert_eq!(e.buffer().lines()[0], "helo");
7572 }
7573
7574 #[test]
7575 fn s_substitutes_char_and_enters_insert() {
7576 let mut e = editor_with("hello");
7577 run_keys(&mut e, "sX<Esc>");
7578 assert_eq!(e.buffer().lines()[0], "Xello");
7579 }
7580
7581 #[test]
7582 fn count_x_deletes_many() {
7583 let mut e = editor_with("abcdef");
7584 run_keys(&mut e, "3x");
7585 assert_eq!(e.buffer().lines()[0], "def");
7586 }
7587
7588 #[test]
7591 fn p_pastes_charwise_after_cursor() {
7592 let mut e = editor_with("hello");
7593 run_keys(&mut e, "yw");
7594 run_keys(&mut e, "$p");
7595 assert_eq!(e.buffer().lines()[0], "hellohello");
7596 }
7597
7598 #[test]
7599 fn capital_p_pastes_charwise_before_cursor() {
7600 let mut e = editor_with("hello");
7601 run_keys(&mut e, "v");
7603 run_keys(&mut e, "l");
7604 run_keys(&mut e, "y");
7605 run_keys(&mut e, "$P");
7606 assert_eq!(e.buffer().lines()[0], "hellheo");
7609 }
7610
7611 #[test]
7612 fn p_pastes_linewise_below() {
7613 let mut e = editor_with("one\ntwo\nthree");
7614 run_keys(&mut e, "yy");
7615 run_keys(&mut e, "p");
7616 assert_eq!(
7617 e.buffer().lines(),
7618 &[
7619 "one".to_string(),
7620 "one".to_string(),
7621 "two".to_string(),
7622 "three".to_string()
7623 ]
7624 );
7625 }
7626
7627 #[test]
7628 fn capital_p_pastes_linewise_above() {
7629 let mut e = editor_with("one\ntwo");
7630 e.jump_cursor(1, 0);
7631 run_keys(&mut e, "yy");
7632 run_keys(&mut e, "P");
7633 assert_eq!(
7634 e.buffer().lines(),
7635 &["one".to_string(), "two".to_string(), "two".to_string()]
7636 );
7637 }
7638
7639 #[test]
7642 fn hash_finds_previous_occurrence() {
7643 let mut e = editor_with("foo bar foo baz foo");
7644 e.jump_cursor(0, 16);
7646 run_keys(&mut e, "#");
7647 assert_eq!(e.cursor().1, 8);
7648 }
7649
7650 #[test]
7653 fn visual_line_delete_removes_full_lines() {
7654 let mut e = editor_with("a\nb\nc\nd");
7655 run_keys(&mut e, "Vjd");
7656 assert_eq!(e.buffer().lines(), &["c".to_string(), "d".to_string()]);
7657 }
7658
7659 #[test]
7660 fn visual_line_change_leaves_blank_line() {
7661 let mut e = editor_with("a\nb\nc");
7662 run_keys(&mut e, "Vjc");
7663 assert_eq!(e.vim_mode(), VimMode::Insert);
7664 run_keys(&mut e, "X<Esc>");
7665 assert_eq!(e.buffer().lines(), &["X".to_string(), "c".to_string()]);
7669 }
7670
7671 #[test]
7672 fn cc_leaves_blank_line() {
7673 let mut e = editor_with("a\nb\nc");
7674 e.jump_cursor(1, 0);
7675 run_keys(&mut e, "ccX<Esc>");
7676 assert_eq!(
7677 e.buffer().lines(),
7678 &["a".to_string(), "X".to_string(), "c".to_string()]
7679 );
7680 }
7681
7682 #[test]
7687 fn big_w_skips_hyphens() {
7688 let mut e = editor_with("foo-bar baz");
7690 run_keys(&mut e, "W");
7691 assert_eq!(e.cursor().1, 8);
7692 }
7693
7694 #[test]
7695 fn big_w_crosses_lines() {
7696 let mut e = editor_with("foo-bar\nbaz-qux");
7697 run_keys(&mut e, "W");
7698 assert_eq!(e.cursor(), (1, 0));
7699 }
7700
7701 #[test]
7702 fn big_b_skips_hyphens() {
7703 let mut e = editor_with("foo-bar baz");
7704 e.jump_cursor(0, 9);
7705 run_keys(&mut e, "B");
7706 assert_eq!(e.cursor().1, 8);
7707 run_keys(&mut e, "B");
7708 assert_eq!(e.cursor().1, 0);
7709 }
7710
7711 #[test]
7712 fn big_e_jumps_to_big_word_end() {
7713 let mut e = editor_with("foo-bar baz");
7714 run_keys(&mut e, "E");
7715 assert_eq!(e.cursor().1, 6);
7716 run_keys(&mut e, "E");
7717 assert_eq!(e.cursor().1, 10);
7718 }
7719
7720 #[test]
7721 fn dw_with_big_word_variant() {
7722 let mut e = editor_with("foo-bar baz");
7724 run_keys(&mut e, "dW");
7725 assert_eq!(e.buffer().lines()[0], "baz");
7726 }
7727
7728 #[test]
7731 fn insert_ctrl_w_deletes_word_back() {
7732 let mut e = editor_with("");
7733 run_keys(&mut e, "i");
7734 for c in "hello world".chars() {
7735 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7736 }
7737 run_keys(&mut e, "<C-w>");
7738 assert_eq!(e.buffer().lines()[0], "hello ");
7739 }
7740
7741 #[test]
7742 fn insert_ctrl_w_at_col0_joins_with_prev_word() {
7743 let mut e = editor_with("hello\nworld");
7747 e.jump_cursor(1, 0);
7748 run_keys(&mut e, "i");
7749 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7750 assert_eq!(e.buffer().lines(), vec!["world".to_string()]);
7753 assert_eq!(e.cursor(), (0, 0));
7754 }
7755
7756 #[test]
7757 fn insert_ctrl_w_at_col0_keeps_prefix_words() {
7758 let mut e = editor_with("foo bar\nbaz");
7759 e.jump_cursor(1, 0);
7760 run_keys(&mut e, "i");
7761 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7762 assert_eq!(e.buffer().lines(), vec!["foo baz".to_string()]);
7764 assert_eq!(e.cursor(), (0, 4));
7765 }
7766
7767 #[test]
7768 fn insert_ctrl_u_deletes_to_line_start() {
7769 let mut e = editor_with("");
7770 run_keys(&mut e, "i");
7771 for c in "hello world".chars() {
7772 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7773 }
7774 run_keys(&mut e, "<C-u>");
7775 assert_eq!(e.buffer().lines()[0], "");
7776 }
7777
7778 #[test]
7779 fn insert_ctrl_o_runs_one_normal_command() {
7780 let mut e = editor_with("hello world");
7781 run_keys(&mut e, "A");
7783 assert_eq!(e.vim_mode(), VimMode::Insert);
7784 e.jump_cursor(0, 0);
7786 run_keys(&mut e, "<C-o>");
7787 assert_eq!(e.vim_mode(), VimMode::Normal);
7788 run_keys(&mut e, "dw");
7789 assert_eq!(e.vim_mode(), VimMode::Insert);
7791 assert_eq!(e.buffer().lines()[0], "world");
7792 }
7793
7794 #[test]
7797 fn j_through_empty_line_preserves_column() {
7798 let mut e = editor_with("hello world\n\nanother line");
7799 run_keys(&mut e, "llllll");
7801 assert_eq!(e.cursor(), (0, 6));
7802 run_keys(&mut e, "j");
7805 assert_eq!(e.cursor(), (1, 0));
7806 run_keys(&mut e, "j");
7808 assert_eq!(e.cursor(), (2, 6));
7809 }
7810
7811 #[test]
7812 fn j_through_shorter_line_preserves_column() {
7813 let mut e = editor_with("hello world\nhi\nanother line");
7814 run_keys(&mut e, "lllllll"); run_keys(&mut e, "j"); assert_eq!(e.cursor(), (1, 1));
7817 run_keys(&mut e, "j");
7818 assert_eq!(e.cursor(), (2, 7));
7819 }
7820
7821 #[test]
7822 fn esc_from_insert_sticky_matches_visible_cursor() {
7823 let mut e = editor_with(" this is a line\n another one of a similar size");
7827 e.jump_cursor(0, 12);
7828 run_keys(&mut e, "I");
7829 assert_eq!(e.cursor(), (0, 4));
7830 run_keys(&mut e, "X<Esc>");
7831 assert_eq!(e.cursor(), (0, 4));
7832 run_keys(&mut e, "j");
7833 assert_eq!(e.cursor(), (1, 4));
7834 }
7835
7836 #[test]
7837 fn esc_from_insert_sticky_tracks_inserted_chars() {
7838 let mut e = editor_with("xxxxxxx\nyyyyyyy");
7839 run_keys(&mut e, "i");
7840 run_keys(&mut e, "abc<Esc>");
7841 assert_eq!(e.cursor(), (0, 2));
7842 run_keys(&mut e, "j");
7843 assert_eq!(e.cursor(), (1, 2));
7844 }
7845
7846 #[test]
7847 fn esc_from_insert_sticky_tracks_arrow_nav() {
7848 let mut e = editor_with("xxxxxx\nyyyyyy");
7849 run_keys(&mut e, "i");
7850 run_keys(&mut e, "abc");
7851 for _ in 0..2 {
7852 e.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
7853 }
7854 run_keys(&mut e, "<Esc>");
7855 assert_eq!(e.cursor(), (0, 0));
7856 run_keys(&mut e, "j");
7857 assert_eq!(e.cursor(), (1, 0));
7858 }
7859
7860 #[test]
7861 fn esc_from_insert_at_col_14_followed_by_j() {
7862 let line = "x".repeat(30);
7865 let buf = format!("{line}\n{line}");
7866 let mut e = editor_with(&buf);
7867 e.jump_cursor(0, 14);
7868 run_keys(&mut e, "i");
7869 for c in "test ".chars() {
7870 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7871 }
7872 run_keys(&mut e, "<Esc>");
7873 assert_eq!(e.cursor(), (0, 18));
7874 run_keys(&mut e, "j");
7875 assert_eq!(e.cursor(), (1, 18));
7876 }
7877
7878 #[test]
7879 fn linewise_paste_resets_sticky_column() {
7880 let mut e = editor_with(" hello\naaaaaaaa\nbye");
7884 run_keys(&mut e, "llllll"); run_keys(&mut e, "yy");
7886 run_keys(&mut e, "j"); run_keys(&mut e, "p"); assert_eq!(e.cursor(), (2, 4));
7890 run_keys(&mut e, "j");
7892 assert_eq!(e.cursor(), (3, 2));
7893 }
7894
7895 #[test]
7896 fn horizontal_motion_resyncs_sticky_column() {
7897 let mut e = editor_with("hello world\n\nanother line");
7901 run_keys(&mut e, "llllll"); run_keys(&mut e, "hhh"); run_keys(&mut e, "jj");
7904 assert_eq!(e.cursor(), (2, 3));
7905 }
7906
7907 #[test]
7910 fn ctrl_v_enters_visual_block() {
7911 let mut e = editor_with("aaa\nbbb\nccc");
7912 run_keys(&mut e, "<C-v>");
7913 assert_eq!(e.vim_mode(), VimMode::VisualBlock);
7914 }
7915
7916 #[test]
7917 fn visual_block_esc_returns_to_normal() {
7918 let mut e = editor_with("aaa\nbbb\nccc");
7919 run_keys(&mut e, "<C-v>");
7920 run_keys(&mut e, "<Esc>");
7921 assert_eq!(e.vim_mode(), VimMode::Normal);
7922 }
7923
7924 #[test]
7925 fn backtick_lt_jumps_to_visual_start_mark() {
7926 let mut e = editor_with("foo bar baz\n");
7930 run_keys(&mut e, "v");
7931 run_keys(&mut e, "w"); run_keys(&mut e, "<Esc>"); assert_eq!(e.cursor(), (0, 4));
7934 run_keys(&mut e, "`<lt>");
7936 assert_eq!(e.cursor(), (0, 0));
7937 }
7938
7939 #[test]
7940 fn backtick_gt_jumps_to_visual_end_mark() {
7941 let mut e = editor_with("foo bar baz\n");
7942 run_keys(&mut e, "v");
7943 run_keys(&mut e, "w"); run_keys(&mut e, "<Esc>");
7945 run_keys(&mut e, "0"); run_keys(&mut e, "`>");
7947 assert_eq!(e.cursor(), (0, 4));
7948 }
7949
7950 #[test]
7951 fn visual_exit_sets_lt_gt_marks() {
7952 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7955 run_keys(&mut e, "V");
7957 run_keys(&mut e, "j");
7958 run_keys(&mut e, "<Esc>");
7959 let lt = e.mark('<').expect("'<' mark must be set on visual exit");
7960 let gt = e.mark('>').expect("'>' mark must be set on visual exit");
7961 assert_eq!(lt.0, 0, "'< row should be the lower bound");
7962 assert_eq!(gt.0, 1, "'> row should be the upper bound");
7963 }
7964
7965 #[test]
7966 fn visual_exit_marks_use_lower_higher_order() {
7967 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7971 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
7973 run_keys(&mut e, "k"); run_keys(&mut e, "<Esc>");
7975 let lt = e.mark('<').unwrap();
7976 let gt = e.mark('>').unwrap();
7977 assert_eq!(lt.0, 2);
7978 assert_eq!(gt.0, 3);
7979 }
7980
7981 #[test]
7982 fn visualline_exit_marks_snap_to_line_edges() {
7983 let mut e = editor_with("aaaaa\nbbbbb\ncc");
7985 run_keys(&mut e, "lll"); run_keys(&mut e, "V");
7987 run_keys(&mut e, "j"); run_keys(&mut e, "<Esc>");
7989 let lt = e.mark('<').unwrap();
7990 let gt = e.mark('>').unwrap();
7991 assert_eq!(lt, (0, 0), "'< should snap to (top_row, 0)");
7992 assert_eq!(gt, (1, 4), "'> should snap to (bot_row, last_col)");
7994 }
7995
7996 #[test]
7997 fn visualblock_exit_marks_use_block_corners() {
7998 let mut e = editor_with("aaaaa\nbbbbb\nccccc");
8002 run_keys(&mut e, "llll"); run_keys(&mut e, "<C-v>");
8004 run_keys(&mut e, "j"); run_keys(&mut e, "hh"); run_keys(&mut e, "<Esc>");
8007 let lt = e.mark('<').unwrap();
8008 let gt = e.mark('>').unwrap();
8009 assert_eq!(lt, (0, 2), "'< should be top-left corner");
8011 assert_eq!(gt, (1, 4), "'> should be bottom-right corner");
8012 }
8013
8014 #[test]
8015 fn visual_block_delete_removes_column_range() {
8016 let mut e = editor_with("hello\nworld\nhappy");
8017 run_keys(&mut e, "l");
8019 run_keys(&mut e, "<C-v>");
8020 run_keys(&mut e, "jj");
8021 run_keys(&mut e, "ll");
8022 run_keys(&mut e, "d");
8023 assert_eq!(
8025 e.buffer().lines(),
8026 &["ho".to_string(), "wd".to_string(), "hy".to_string()]
8027 );
8028 }
8029
8030 #[test]
8031 fn visual_block_yank_joins_with_newlines() {
8032 let mut e = editor_with("hello\nworld\nhappy");
8033 run_keys(&mut e, "<C-v>");
8034 run_keys(&mut e, "jj");
8035 run_keys(&mut e, "ll");
8036 run_keys(&mut e, "y");
8037 assert_eq!(e.last_yank.as_deref(), Some("hel\nwor\nhap"));
8038 }
8039
8040 #[test]
8041 fn visual_block_replace_fills_block() {
8042 let mut e = editor_with("hello\nworld\nhappy");
8043 run_keys(&mut e, "<C-v>");
8044 run_keys(&mut e, "jj");
8045 run_keys(&mut e, "ll");
8046 run_keys(&mut e, "rx");
8047 assert_eq!(
8048 e.buffer().lines(),
8049 &[
8050 "xxxlo".to_string(),
8051 "xxxld".to_string(),
8052 "xxxpy".to_string()
8053 ]
8054 );
8055 }
8056
8057 #[test]
8058 fn visual_block_insert_repeats_across_rows() {
8059 let mut e = editor_with("hello\nworld\nhappy");
8060 run_keys(&mut e, "<C-v>");
8061 run_keys(&mut e, "jj");
8062 run_keys(&mut e, "I");
8063 run_keys(&mut e, "# <Esc>");
8064 assert_eq!(
8065 e.buffer().lines(),
8066 &[
8067 "# hello".to_string(),
8068 "# world".to_string(),
8069 "# happy".to_string()
8070 ]
8071 );
8072 }
8073
8074 #[test]
8075 fn block_highlight_returns_none_outside_block_mode() {
8076 let mut e = editor_with("abc");
8077 assert!(e.block_highlight().is_none());
8078 run_keys(&mut e, "v");
8079 assert!(e.block_highlight().is_none());
8080 run_keys(&mut e, "<Esc>V");
8081 assert!(e.block_highlight().is_none());
8082 }
8083
8084 #[test]
8085 fn block_highlight_bounds_track_anchor_and_cursor() {
8086 let mut e = editor_with("aaaa\nbbbb\ncccc");
8087 run_keys(&mut e, "ll"); run_keys(&mut e, "<C-v>");
8089 run_keys(&mut e, "jh"); assert_eq!(e.block_highlight(), Some((0, 1, 1, 2)));
8092 }
8093
8094 #[test]
8095 fn visual_block_delete_handles_short_lines() {
8096 let mut e = editor_with("hello\nhi\nworld");
8098 run_keys(&mut e, "l"); run_keys(&mut e, "<C-v>");
8100 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
8102 assert_eq!(
8107 e.buffer().lines(),
8108 &["ho".to_string(), "h".to_string(), "wd".to_string()]
8109 );
8110 }
8111
8112 #[test]
8113 fn visual_block_yank_pads_short_lines_with_empties() {
8114 let mut e = editor_with("hello\nhi\nworld");
8115 run_keys(&mut e, "l");
8116 run_keys(&mut e, "<C-v>");
8117 run_keys(&mut e, "jjll");
8118 run_keys(&mut e, "y");
8119 assert_eq!(e.last_yank.as_deref(), Some("ell\ni\norl"));
8121 }
8122
8123 #[test]
8124 fn visual_block_replace_skips_past_eol() {
8125 let mut e = editor_with("ab\ncd\nef");
8128 run_keys(&mut e, "l");
8130 run_keys(&mut e, "<C-v>");
8131 run_keys(&mut e, "jjllllll");
8132 run_keys(&mut e, "rX");
8133 assert_eq!(
8136 e.buffer().lines(),
8137 &["aX".to_string(), "cX".to_string(), "eX".to_string()]
8138 );
8139 }
8140
8141 #[test]
8142 fn visual_block_with_empty_line_in_middle() {
8143 let mut e = editor_with("abcd\n\nefgh");
8144 run_keys(&mut e, "<C-v>");
8145 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
8147 assert_eq!(
8150 e.buffer().lines(),
8151 &["d".to_string(), "".to_string(), "h".to_string()]
8152 );
8153 }
8154
8155 #[test]
8156 fn block_insert_pads_empty_lines_to_block_column() {
8157 let mut e = editor_with("this is a line\n\nthis is a line");
8160 e.jump_cursor(0, 3);
8161 run_keys(&mut e, "<C-v>");
8162 run_keys(&mut e, "jj");
8163 run_keys(&mut e, "I");
8164 run_keys(&mut e, "XX<Esc>");
8165 assert_eq!(
8166 e.buffer().lines(),
8167 &[
8168 "thiXXs is a line".to_string(),
8169 " XX".to_string(),
8170 "thiXXs is a line".to_string()
8171 ]
8172 );
8173 }
8174
8175 #[test]
8176 fn block_insert_pads_short_lines_to_block_column() {
8177 let mut e = editor_with("aaaaa\nbb\naaaaa");
8178 e.jump_cursor(0, 3);
8179 run_keys(&mut e, "<C-v>");
8180 run_keys(&mut e, "jj");
8181 run_keys(&mut e, "I");
8182 run_keys(&mut e, "Y<Esc>");
8183 assert_eq!(
8185 e.buffer().lines(),
8186 &[
8187 "aaaYaa".to_string(),
8188 "bb Y".to_string(),
8189 "aaaYaa".to_string()
8190 ]
8191 );
8192 }
8193
8194 #[test]
8195 fn visual_block_append_repeats_across_rows() {
8196 let mut e = editor_with("foo\nbar\nbaz");
8197 run_keys(&mut e, "<C-v>");
8198 run_keys(&mut e, "jj");
8199 run_keys(&mut e, "A");
8202 run_keys(&mut e, "!<Esc>");
8203 assert_eq!(
8204 e.buffer().lines(),
8205 &["f!oo".to_string(), "b!ar".to_string(), "b!az".to_string()]
8206 );
8207 }
8208
8209 #[test]
8212 fn slash_opens_forward_search_prompt() {
8213 let mut e = editor_with("hello world");
8214 run_keys(&mut e, "/");
8215 let p = e.search_prompt().expect("prompt should be active");
8216 assert!(p.text.is_empty());
8217 assert!(p.forward);
8218 }
8219
8220 #[test]
8221 fn question_opens_backward_search_prompt() {
8222 let mut e = editor_with("hello world");
8223 run_keys(&mut e, "?");
8224 let p = e.search_prompt().expect("prompt should be active");
8225 assert!(!p.forward);
8226 }
8227
8228 #[test]
8229 fn search_prompt_typing_updates_pattern_live() {
8230 let mut e = editor_with("foo bar\nbaz");
8231 run_keys(&mut e, "/bar");
8232 assert_eq!(e.search_prompt().unwrap().text, "bar");
8233 assert!(e.search_state().pattern.is_some());
8235 }
8236
8237 #[test]
8238 fn search_prompt_backspace_and_enter() {
8239 let mut e = editor_with("hello world\nagain");
8240 run_keys(&mut e, "/worlx");
8241 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
8242 assert_eq!(e.search_prompt().unwrap().text, "worl");
8243 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8244 assert!(e.search_prompt().is_none());
8246 assert_eq!(e.last_search(), Some("worl"));
8247 assert_eq!(e.cursor(), (0, 6));
8248 }
8249
8250 #[test]
8251 fn empty_search_prompt_enter_repeats_last_search() {
8252 let mut e = editor_with("foo bar foo baz foo");
8253 run_keys(&mut e, "/foo");
8254 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8255 assert_eq!(e.cursor().1, 8);
8256 run_keys(&mut e, "/");
8258 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8259 assert_eq!(e.cursor().1, 16);
8260 assert_eq!(e.last_search(), Some("foo"));
8261 }
8262
8263 #[test]
8264 fn search_history_records_committed_patterns() {
8265 let mut e = editor_with("alpha beta gamma");
8266 run_keys(&mut e, "/alpha<CR>");
8267 run_keys(&mut e, "/beta<CR>");
8268 let history = e.vim.search_history.clone();
8270 assert_eq!(history, vec!["alpha", "beta"]);
8271 }
8272
8273 #[test]
8274 fn search_history_dedupes_consecutive_repeats() {
8275 let mut e = editor_with("foo bar foo");
8276 run_keys(&mut e, "/foo<CR>");
8277 run_keys(&mut e, "/foo<CR>");
8278 run_keys(&mut e, "/bar<CR>");
8279 run_keys(&mut e, "/bar<CR>");
8280 assert_eq!(e.vim.search_history.clone(), vec!["foo", "bar"]);
8282 }
8283
8284 #[test]
8285 fn ctrl_p_walks_history_backward() {
8286 let mut e = editor_with("alpha beta gamma");
8287 run_keys(&mut e, "/alpha<CR>");
8288 run_keys(&mut e, "/beta<CR>");
8289 run_keys(&mut e, "/");
8291 assert_eq!(e.search_prompt().unwrap().text, "");
8292 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8293 assert_eq!(e.search_prompt().unwrap().text, "beta");
8294 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8295 assert_eq!(e.search_prompt().unwrap().text, "alpha");
8296 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8298 assert_eq!(e.search_prompt().unwrap().text, "alpha");
8299 }
8300
8301 #[test]
8302 fn ctrl_n_walks_history_forward_after_ctrl_p() {
8303 let mut e = editor_with("a b c");
8304 run_keys(&mut e, "/a<CR>");
8305 run_keys(&mut e, "/b<CR>");
8306 run_keys(&mut e, "/c<CR>");
8307 run_keys(&mut e, "/");
8308 for _ in 0..3 {
8310 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8311 }
8312 assert_eq!(e.search_prompt().unwrap().text, "a");
8313 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8314 assert_eq!(e.search_prompt().unwrap().text, "b");
8315 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8316 assert_eq!(e.search_prompt().unwrap().text, "c");
8317 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8319 assert_eq!(e.search_prompt().unwrap().text, "c");
8320 }
8321
8322 #[test]
8323 fn typing_after_history_walk_resets_cursor() {
8324 let mut e = editor_with("foo");
8325 run_keys(&mut e, "/foo<CR>");
8326 run_keys(&mut e, "/");
8327 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8328 assert_eq!(e.search_prompt().unwrap().text, "foo");
8329 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
8332 assert_eq!(e.search_prompt().unwrap().text, "foox");
8333 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8334 assert_eq!(e.search_prompt().unwrap().text, "foo");
8335 }
8336
8337 #[test]
8338 fn empty_backward_search_prompt_enter_repeats_last_search() {
8339 let mut e = editor_with("foo bar foo baz foo");
8340 run_keys(&mut e, "/foo");
8342 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8343 assert_eq!(e.cursor().1, 8);
8344 run_keys(&mut e, "?");
8345 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8346 assert_eq!(e.cursor().1, 0);
8347 assert_eq!(e.last_search(), Some("foo"));
8348 }
8349
8350 #[test]
8351 fn search_prompt_esc_cancels_but_keeps_last_search() {
8352 let mut e = editor_with("foo bar\nbaz");
8353 run_keys(&mut e, "/bar");
8354 e.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
8355 assert!(e.search_prompt().is_none());
8356 assert_eq!(e.last_search(), Some("bar"));
8357 }
8358
8359 #[test]
8360 fn search_then_n_and_shift_n_navigate() {
8361 let mut e = editor_with("foo bar foo baz foo");
8362 run_keys(&mut e, "/foo");
8363 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8364 assert_eq!(e.cursor().1, 8);
8366 run_keys(&mut e, "n");
8367 assert_eq!(e.cursor().1, 16);
8368 run_keys(&mut e, "N");
8369 assert_eq!(e.cursor().1, 8);
8370 }
8371
8372 #[test]
8373 fn question_mark_searches_backward_on_enter() {
8374 let mut e = editor_with("foo bar foo baz");
8375 e.jump_cursor(0, 10);
8376 run_keys(&mut e, "?foo");
8377 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8378 assert_eq!(e.cursor(), (0, 8));
8380 }
8381
8382 #[test]
8385 fn big_y_yanks_to_end_of_line() {
8386 let mut e = editor_with("hello world");
8387 e.jump_cursor(0, 6);
8388 run_keys(&mut e, "Y");
8389 assert_eq!(e.last_yank.as_deref(), Some("world"));
8390 }
8391
8392 #[test]
8393 fn big_y_from_line_start_yanks_full_line() {
8394 let mut e = editor_with("hello world");
8395 run_keys(&mut e, "Y");
8396 assert_eq!(e.last_yank.as_deref(), Some("hello world"));
8397 }
8398
8399 #[test]
8400 fn gj_joins_without_inserting_space() {
8401 let mut e = editor_with("hello\n world");
8402 run_keys(&mut e, "gJ");
8403 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
8405 }
8406
8407 #[test]
8408 fn gj_noop_on_last_line() {
8409 let mut e = editor_with("only");
8410 run_keys(&mut e, "gJ");
8411 assert_eq!(e.buffer().lines(), &["only".to_string()]);
8412 }
8413
8414 #[test]
8415 fn ge_jumps_to_previous_word_end() {
8416 let mut e = editor_with("foo bar baz");
8417 e.jump_cursor(0, 5);
8418 run_keys(&mut e, "ge");
8419 assert_eq!(e.cursor(), (0, 2));
8420 }
8421
8422 #[test]
8423 fn ge_respects_word_class() {
8424 let mut e = editor_with("foo-bar baz");
8427 e.jump_cursor(0, 5);
8428 run_keys(&mut e, "ge");
8429 assert_eq!(e.cursor(), (0, 3));
8430 }
8431
8432 #[test]
8433 fn big_ge_treats_hyphens_as_part_of_word() {
8434 let mut e = editor_with("foo-bar baz");
8437 e.jump_cursor(0, 10);
8438 run_keys(&mut e, "gE");
8439 assert_eq!(e.cursor(), (0, 6));
8440 }
8441
8442 #[test]
8443 fn ge_crosses_line_boundary() {
8444 let mut e = editor_with("foo\nbar");
8445 e.jump_cursor(1, 0);
8446 run_keys(&mut e, "ge");
8447 assert_eq!(e.cursor(), (0, 2));
8448 }
8449
8450 #[test]
8451 fn dge_deletes_to_end_of_previous_word() {
8452 let mut e = editor_with("foo bar baz");
8453 e.jump_cursor(0, 8);
8454 run_keys(&mut e, "dge");
8457 assert_eq!(e.buffer().lines()[0], "foo baaz");
8458 }
8459
8460 #[test]
8461 fn ctrl_scroll_keys_do_not_panic() {
8462 let mut e = editor_with(
8465 (0..50)
8466 .map(|i| format!("line{i}"))
8467 .collect::<Vec<_>>()
8468 .join("\n")
8469 .as_str(),
8470 );
8471 run_keys(&mut e, "<C-f>");
8472 run_keys(&mut e, "<C-b>");
8473 assert!(!e.buffer().lines().is_empty());
8475 }
8476
8477 #[test]
8484 fn count_insert_with_arrow_nav_does_not_leak_rows() {
8485 let mut e = Editor::new(
8486 hjkl_buffer::Buffer::new(),
8487 crate::types::DefaultHost::new(),
8488 crate::types::Options::default(),
8489 );
8490 e.set_content("row0\nrow1\nrow2");
8491 run_keys(&mut e, "3iX<Down><Esc>");
8493 assert!(e.buffer().lines()[0].contains('X'));
8495 assert!(
8498 !e.buffer().lines()[1].contains("row0"),
8499 "row1 leaked row0 contents: {:?}",
8500 e.buffer().lines()[1]
8501 );
8502 assert_eq!(e.buffer().lines().len(), 3);
8505 }
8506
8507 fn editor_with_rows(n: usize, viewport: u16) -> Editor {
8510 let mut e = Editor::new(
8511 hjkl_buffer::Buffer::new(),
8512 crate::types::DefaultHost::new(),
8513 crate::types::Options::default(),
8514 );
8515 let body = (0..n)
8516 .map(|i| format!(" line{}", i))
8517 .collect::<Vec<_>>()
8518 .join("\n");
8519 e.set_content(&body);
8520 e.set_viewport_height(viewport);
8521 e
8522 }
8523
8524 #[test]
8525 fn ctrl_d_moves_cursor_half_page_down() {
8526 let mut e = editor_with_rows(100, 20);
8527 run_keys(&mut e, "<C-d>");
8528 assert_eq!(e.cursor().0, 10);
8529 }
8530
8531 fn editor_with_wrap_lines(lines: &[&str], viewport: u16, text_width: u16) -> Editor {
8532 let mut e = Editor::new(
8533 hjkl_buffer::Buffer::new(),
8534 crate::types::DefaultHost::new(),
8535 crate::types::Options::default(),
8536 );
8537 e.set_content(&lines.join("\n"));
8538 e.set_viewport_height(viewport);
8539 let v = e.host_mut().viewport_mut();
8540 v.height = viewport;
8541 v.width = text_width;
8542 v.text_width = text_width;
8543 v.wrap = hjkl_buffer::Wrap::Char;
8544 e.settings_mut().wrap = hjkl_buffer::Wrap::Char;
8545 e
8546 }
8547
8548 #[test]
8549 fn scrolloff_wrap_keeps_cursor_off_bottom_edge() {
8550 let lines = ["aaaabbbbcccc"; 10];
8554 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8555 e.jump_cursor(4, 0);
8556 e.ensure_cursor_in_scrolloff();
8557 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8558 assert!(csr <= 6, "csr={csr}");
8559 }
8560
8561 #[test]
8562 fn scrolloff_wrap_keeps_cursor_off_top_edge() {
8563 let lines = ["aaaabbbbcccc"; 10];
8564 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8565 e.jump_cursor(7, 0);
8568 e.ensure_cursor_in_scrolloff();
8569 e.jump_cursor(2, 0);
8570 e.ensure_cursor_in_scrolloff();
8571 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8572 assert!(csr >= 5, "csr={csr}");
8574 }
8575
8576 #[test]
8577 fn scrolloff_wrap_clamps_top_at_buffer_end() {
8578 let lines = ["aaaabbbbcccc"; 5];
8579 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8580 e.jump_cursor(4, 11);
8581 e.ensure_cursor_in_scrolloff();
8582 let top = e.host().viewport().top_row;
8587 assert_eq!(top, 1);
8588 }
8589
8590 #[test]
8591 fn ctrl_u_moves_cursor_half_page_up() {
8592 let mut e = editor_with_rows(100, 20);
8593 e.jump_cursor(50, 0);
8594 run_keys(&mut e, "<C-u>");
8595 assert_eq!(e.cursor().0, 40);
8596 }
8597
8598 #[test]
8599 fn ctrl_f_moves_cursor_full_page_down() {
8600 let mut e = editor_with_rows(100, 20);
8601 run_keys(&mut e, "<C-f>");
8602 assert_eq!(e.cursor().0, 18);
8604 }
8605
8606 #[test]
8607 fn ctrl_b_moves_cursor_full_page_up() {
8608 let mut e = editor_with_rows(100, 20);
8609 e.jump_cursor(50, 0);
8610 run_keys(&mut e, "<C-b>");
8611 assert_eq!(e.cursor().0, 32);
8612 }
8613
8614 #[test]
8615 fn ctrl_d_lands_on_first_non_blank() {
8616 let mut e = editor_with_rows(100, 20);
8617 run_keys(&mut e, "<C-d>");
8618 assert_eq!(e.cursor().1, 2);
8620 }
8621
8622 #[test]
8623 fn ctrl_d_clamps_at_end_of_buffer() {
8624 let mut e = editor_with_rows(5, 20);
8625 run_keys(&mut e, "<C-d>");
8626 assert_eq!(e.cursor().0, 4);
8627 }
8628
8629 #[test]
8630 fn capital_h_jumps_to_viewport_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, "H");
8636 assert_eq!(e.cursor().0, top);
8637 assert_eq!(e.cursor().1, 2);
8638 }
8639
8640 #[test]
8641 fn capital_l_jumps_to_viewport_bottom() {
8642 let mut e = editor_with_rows(100, 10);
8643 e.jump_cursor(50, 0);
8644 e.set_viewport_top(45);
8645 let top = e.host().viewport().top_row;
8646 run_keys(&mut e, "L");
8647 assert_eq!(e.cursor().0, top + 9);
8648 }
8649
8650 #[test]
8651 fn capital_m_jumps_to_viewport_middle() {
8652 let mut e = editor_with_rows(100, 10);
8653 e.jump_cursor(50, 0);
8654 e.set_viewport_top(45);
8655 let top = e.host().viewport().top_row;
8656 run_keys(&mut e, "M");
8657 assert_eq!(e.cursor().0, top + 4);
8659 }
8660
8661 #[test]
8662 fn g_capital_m_lands_at_line_midpoint() {
8663 let mut e = editor_with("hello world!"); run_keys(&mut e, "gM");
8665 assert_eq!(e.cursor(), (0, 6));
8667 }
8668
8669 #[test]
8670 fn g_capital_m_on_empty_line_stays_at_zero() {
8671 let mut e = editor_with("");
8672 run_keys(&mut e, "gM");
8673 assert_eq!(e.cursor(), (0, 0));
8674 }
8675
8676 #[test]
8677 fn g_capital_m_uses_current_line_only() {
8678 let mut e = editor_with("a\nlonglongline"); e.jump_cursor(1, 0);
8681 run_keys(&mut e, "gM");
8682 assert_eq!(e.cursor(), (1, 6));
8683 }
8684
8685 #[test]
8686 fn capital_h_count_offsets_from_top() {
8687 let mut e = editor_with_rows(100, 10);
8688 e.jump_cursor(50, 0);
8689 e.set_viewport_top(45);
8690 let top = e.host().viewport().top_row;
8691 run_keys(&mut e, "3H");
8692 assert_eq!(e.cursor().0, top + 2);
8693 }
8694
8695 #[test]
8698 fn ctrl_o_returns_to_pre_g_position() {
8699 let mut e = editor_with_rows(50, 20);
8700 e.jump_cursor(5, 2);
8701 run_keys(&mut e, "G");
8702 assert_eq!(e.cursor().0, 49);
8703 run_keys(&mut e, "<C-o>");
8704 assert_eq!(e.cursor(), (5, 2));
8705 }
8706
8707 #[test]
8708 fn ctrl_i_redoes_jump_after_ctrl_o() {
8709 let mut e = editor_with_rows(50, 20);
8710 e.jump_cursor(5, 2);
8711 run_keys(&mut e, "G");
8712 let post = e.cursor();
8713 run_keys(&mut e, "<C-o>");
8714 run_keys(&mut e, "<C-i>");
8715 assert_eq!(e.cursor(), post);
8716 }
8717
8718 #[test]
8719 fn new_jump_clears_forward_stack() {
8720 let mut e = editor_with_rows(50, 20);
8721 e.jump_cursor(5, 2);
8722 run_keys(&mut e, "G");
8723 run_keys(&mut e, "<C-o>");
8724 run_keys(&mut e, "gg");
8725 run_keys(&mut e, "<C-i>");
8726 assert_eq!(e.cursor().0, 0);
8727 }
8728
8729 #[test]
8730 fn ctrl_o_on_empty_stack_is_noop() {
8731 let mut e = editor_with_rows(10, 20);
8732 e.jump_cursor(3, 1);
8733 run_keys(&mut e, "<C-o>");
8734 assert_eq!(e.cursor(), (3, 1));
8735 }
8736
8737 #[test]
8738 fn asterisk_search_pushes_jump() {
8739 let mut e = editor_with("foo bar\nbaz foo end");
8740 e.jump_cursor(0, 0);
8741 run_keys(&mut e, "*");
8742 let after = e.cursor();
8743 assert_ne!(after, (0, 0));
8744 run_keys(&mut e, "<C-o>");
8745 assert_eq!(e.cursor(), (0, 0));
8746 }
8747
8748 #[test]
8749 fn h_viewport_jump_is_recorded() {
8750 let mut e = editor_with_rows(100, 10);
8751 e.jump_cursor(50, 0);
8752 e.set_viewport_top(45);
8753 let pre = e.cursor();
8754 run_keys(&mut e, "H");
8755 assert_ne!(e.cursor(), pre);
8756 run_keys(&mut e, "<C-o>");
8757 assert_eq!(e.cursor(), pre);
8758 }
8759
8760 #[test]
8761 fn j_k_motion_does_not_push_jump() {
8762 let mut e = editor_with_rows(50, 20);
8763 e.jump_cursor(5, 0);
8764 run_keys(&mut e, "jjj");
8765 run_keys(&mut e, "<C-o>");
8766 assert_eq!(e.cursor().0, 8);
8767 }
8768
8769 #[test]
8770 fn jumplist_caps_at_100() {
8771 let mut e = editor_with_rows(200, 20);
8772 for i in 0..101 {
8773 e.jump_cursor(i, 0);
8774 run_keys(&mut e, "G");
8775 }
8776 assert!(e.vim.jump_back.len() <= 100);
8777 }
8778
8779 #[test]
8780 fn tab_acts_as_ctrl_i() {
8781 let mut e = editor_with_rows(50, 20);
8782 e.jump_cursor(5, 2);
8783 run_keys(&mut e, "G");
8784 let post = e.cursor();
8785 run_keys(&mut e, "<C-o>");
8786 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8787 assert_eq!(e.cursor(), post);
8788 }
8789
8790 #[test]
8793 fn ma_then_backtick_a_jumps_exact() {
8794 let mut e = editor_with_rows(50, 20);
8795 e.jump_cursor(5, 3);
8796 run_keys(&mut e, "ma");
8797 e.jump_cursor(20, 0);
8798 run_keys(&mut e, "`a");
8799 assert_eq!(e.cursor(), (5, 3));
8800 }
8801
8802 #[test]
8803 fn ma_then_apostrophe_a_lands_on_first_non_blank() {
8804 let mut e = editor_with_rows(50, 20);
8805 e.jump_cursor(5, 6);
8807 run_keys(&mut e, "ma");
8808 e.jump_cursor(30, 4);
8809 run_keys(&mut e, "'a");
8810 assert_eq!(e.cursor(), (5, 2));
8811 }
8812
8813 #[test]
8814 fn goto_mark_pushes_jumplist() {
8815 let mut e = editor_with_rows(50, 20);
8816 e.jump_cursor(10, 2);
8817 run_keys(&mut e, "mz");
8818 e.jump_cursor(3, 0);
8819 run_keys(&mut e, "`z");
8820 assert_eq!(e.cursor(), (10, 2));
8821 run_keys(&mut e, "<C-o>");
8822 assert_eq!(e.cursor(), (3, 0));
8823 }
8824
8825 #[test]
8826 fn goto_missing_mark_is_noop() {
8827 let mut e = editor_with_rows(50, 20);
8828 e.jump_cursor(3, 1);
8829 run_keys(&mut e, "`q");
8830 assert_eq!(e.cursor(), (3, 1));
8831 }
8832
8833 #[test]
8834 fn uppercase_mark_stored_under_uppercase_key() {
8835 let mut e = editor_with_rows(50, 20);
8836 e.jump_cursor(5, 3);
8837 run_keys(&mut e, "mA");
8838 assert_eq!(e.mark('A'), Some((5, 3)));
8841 assert!(e.mark('a').is_none());
8842 }
8843
8844 #[test]
8845 fn mark_survives_document_shrink_via_clamp() {
8846 let mut e = editor_with_rows(50, 20);
8847 e.jump_cursor(40, 4);
8848 run_keys(&mut e, "mx");
8849 e.set_content("a\nb\nc\nd\ne");
8851 run_keys(&mut e, "`x");
8852 let (r, _) = e.cursor();
8854 assert!(r <= 4);
8855 }
8856
8857 #[test]
8858 fn g_semicolon_walks_back_through_edits() {
8859 let mut e = editor_with("alpha\nbeta\ngamma");
8860 e.jump_cursor(0, 0);
8863 run_keys(&mut e, "iX<Esc>");
8864 e.jump_cursor(2, 0);
8865 run_keys(&mut e, "iY<Esc>");
8866 run_keys(&mut e, "g;");
8868 assert_eq!(e.cursor(), (2, 1));
8869 run_keys(&mut e, "g;");
8871 assert_eq!(e.cursor(), (0, 1));
8872 run_keys(&mut e, "g;");
8874 assert_eq!(e.cursor(), (0, 1));
8875 }
8876
8877 #[test]
8878 fn g_comma_walks_forward_after_g_semicolon() {
8879 let mut e = editor_with("a\nb\nc");
8880 e.jump_cursor(0, 0);
8881 run_keys(&mut e, "iX<Esc>");
8882 e.jump_cursor(2, 0);
8883 run_keys(&mut e, "iY<Esc>");
8884 run_keys(&mut e, "g;");
8885 run_keys(&mut e, "g;");
8886 assert_eq!(e.cursor(), (0, 1));
8887 run_keys(&mut e, "g,");
8888 assert_eq!(e.cursor(), (2, 1));
8889 }
8890
8891 #[test]
8892 fn new_edit_during_walk_trims_forward_entries() {
8893 let mut e = editor_with("a\nb\nc\nd");
8894 e.jump_cursor(0, 0);
8895 run_keys(&mut e, "iX<Esc>"); e.jump_cursor(2, 0);
8897 run_keys(&mut e, "iY<Esc>"); run_keys(&mut e, "g;");
8900 run_keys(&mut e, "g;");
8901 assert_eq!(e.cursor(), (0, 1));
8902 run_keys(&mut e, "iZ<Esc>");
8904 run_keys(&mut e, "g,");
8906 assert_ne!(e.cursor(), (2, 1));
8908 }
8909
8910 #[test]
8916 fn capital_mark_set_and_jump() {
8917 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
8918 e.jump_cursor(2, 1);
8919 run_keys(&mut e, "mA");
8920 e.jump_cursor(0, 0);
8922 run_keys(&mut e, "'A");
8924 assert_eq!(e.cursor().0, 2);
8926 }
8927
8928 #[test]
8929 fn capital_mark_survives_set_content() {
8930 let mut e = editor_with("first buffer line\nsecond");
8931 e.jump_cursor(1, 3);
8932 run_keys(&mut e, "mA");
8933 e.set_content("totally different content\non many\nrows of text");
8935 e.jump_cursor(0, 0);
8937 run_keys(&mut e, "'A");
8938 assert_eq!(e.cursor().0, 1);
8939 }
8940
8941 #[test]
8946 fn capital_mark_shifts_with_edit() {
8947 let mut e = editor_with("a\nb\nc\nd");
8948 e.jump_cursor(3, 0);
8949 run_keys(&mut e, "mA");
8950 e.jump_cursor(0, 0);
8952 run_keys(&mut e, "dd");
8953 e.jump_cursor(0, 0);
8954 run_keys(&mut e, "'A");
8955 assert_eq!(e.cursor().0, 2);
8956 }
8957
8958 #[test]
8959 fn mark_below_delete_shifts_up() {
8960 let mut e = editor_with("a\nb\nc\nd\ne");
8961 e.jump_cursor(3, 0);
8963 run_keys(&mut e, "ma");
8964 e.jump_cursor(0, 0);
8966 run_keys(&mut e, "dd");
8967 e.jump_cursor(0, 0);
8969 run_keys(&mut e, "'a");
8970 assert_eq!(e.cursor().0, 2);
8971 assert_eq!(e.buffer().line(2).unwrap(), "d");
8972 }
8973
8974 #[test]
8975 fn mark_on_deleted_row_is_dropped() {
8976 let mut e = editor_with("a\nb\nc\nd");
8977 e.jump_cursor(1, 0);
8979 run_keys(&mut e, "ma");
8980 run_keys(&mut e, "dd");
8982 e.jump_cursor(2, 0);
8984 run_keys(&mut e, "'a");
8985 assert_eq!(e.cursor().0, 2);
8987 }
8988
8989 #[test]
8990 fn mark_above_edit_unchanged() {
8991 let mut e = editor_with("a\nb\nc\nd\ne");
8992 e.jump_cursor(0, 0);
8994 run_keys(&mut e, "ma");
8995 e.jump_cursor(3, 0);
8997 run_keys(&mut e, "dd");
8998 e.jump_cursor(2, 0);
9000 run_keys(&mut e, "'a");
9001 assert_eq!(e.cursor().0, 0);
9002 }
9003
9004 #[test]
9005 fn mark_shifts_down_after_insert() {
9006 let mut e = editor_with("a\nb\nc");
9007 e.jump_cursor(2, 0);
9009 run_keys(&mut e, "ma");
9010 e.jump_cursor(0, 0);
9012 run_keys(&mut e, "Onew<Esc>");
9013 e.jump_cursor(0, 0);
9016 run_keys(&mut e, "'a");
9017 assert_eq!(e.cursor().0, 3);
9018 assert_eq!(e.buffer().line(3).unwrap(), "c");
9019 }
9020
9021 #[test]
9024 fn forward_search_commit_pushes_jump() {
9025 let mut e = editor_with("alpha beta\nfoo target end\nmore");
9026 e.jump_cursor(0, 0);
9027 run_keys(&mut e, "/target<CR>");
9028 assert_ne!(e.cursor(), (0, 0));
9030 run_keys(&mut e, "<C-o>");
9032 assert_eq!(e.cursor(), (0, 0));
9033 }
9034
9035 #[test]
9036 fn search_commit_no_match_does_not_push_jump() {
9037 let mut e = editor_with("alpha beta\nfoo end");
9038 e.jump_cursor(0, 3);
9039 let pre_len = e.vim.jump_back.len();
9040 run_keys(&mut e, "/zzznotfound<CR>");
9041 assert_eq!(e.vim.jump_back.len(), pre_len);
9043 }
9044
9045 #[test]
9048 fn buffer_cursor_mirrors_textarea_after_horizontal_motion() {
9049 let mut e = editor_with("hello world");
9050 run_keys(&mut e, "lll");
9051 let (row, col) = e.cursor();
9052 assert_eq!(e.buffer.cursor().row, row);
9053 assert_eq!(e.buffer.cursor().col, col);
9054 }
9055
9056 #[test]
9057 fn buffer_cursor_mirrors_textarea_after_vertical_motion() {
9058 let mut e = editor_with("aaaa\nbbbb\ncccc");
9059 run_keys(&mut e, "jj");
9060 let (row, col) = e.cursor();
9061 assert_eq!(e.buffer.cursor().row, row);
9062 assert_eq!(e.buffer.cursor().col, col);
9063 }
9064
9065 #[test]
9066 fn buffer_cursor_mirrors_textarea_after_word_motion() {
9067 let mut e = editor_with("foo bar baz");
9068 run_keys(&mut e, "ww");
9069 let (row, col) = e.cursor();
9070 assert_eq!(e.buffer.cursor().row, row);
9071 assert_eq!(e.buffer.cursor().col, col);
9072 }
9073
9074 #[test]
9075 fn buffer_cursor_mirrors_textarea_after_jump_motion() {
9076 let mut e = editor_with("a\nb\nc\nd\ne");
9077 run_keys(&mut e, "G");
9078 let (row, col) = e.cursor();
9079 assert_eq!(e.buffer.cursor().row, row);
9080 assert_eq!(e.buffer.cursor().col, col);
9081 }
9082
9083 #[test]
9084 fn editor_sticky_col_tracks_horizontal_motion() {
9085 let mut e = editor_with("longline\nhi\nlongline");
9086 run_keys(&mut e, "fl");
9091 let landed = e.cursor().1;
9092 assert!(landed > 0, "fl should have moved");
9093 run_keys(&mut e, "j");
9094 assert_eq!(e.sticky_col(), Some(landed));
9097 }
9098
9099 #[test]
9100 fn buffer_content_mirrors_textarea_after_insert() {
9101 let mut e = editor_with("hello");
9102 run_keys(&mut e, "iXYZ<Esc>");
9103 let text = e.buffer().lines().join("\n");
9104 assert_eq!(e.buffer.as_string(), text);
9105 }
9106
9107 #[test]
9108 fn buffer_content_mirrors_textarea_after_delete() {
9109 let mut e = editor_with("alpha bravo charlie");
9110 run_keys(&mut e, "dw");
9111 let text = e.buffer().lines().join("\n");
9112 assert_eq!(e.buffer.as_string(), text);
9113 }
9114
9115 #[test]
9116 fn buffer_content_mirrors_textarea_after_dd() {
9117 let mut e = editor_with("a\nb\nc\nd");
9118 run_keys(&mut e, "jdd");
9119 let text = e.buffer().lines().join("\n");
9120 assert_eq!(e.buffer.as_string(), text);
9121 }
9122
9123 #[test]
9124 fn buffer_content_mirrors_textarea_after_open_line() {
9125 let mut e = editor_with("foo\nbar");
9126 run_keys(&mut e, "oNEW<Esc>");
9127 let text = e.buffer().lines().join("\n");
9128 assert_eq!(e.buffer.as_string(), text);
9129 }
9130
9131 #[test]
9132 fn buffer_content_mirrors_textarea_after_paste() {
9133 let mut e = editor_with("hello");
9134 run_keys(&mut e, "yy");
9135 run_keys(&mut e, "p");
9136 let text = e.buffer().lines().join("\n");
9137 assert_eq!(e.buffer.as_string(), text);
9138 }
9139
9140 #[test]
9141 fn buffer_selection_none_in_normal_mode() {
9142 let e = editor_with("foo bar");
9143 assert!(e.buffer_selection().is_none());
9144 }
9145
9146 #[test]
9147 fn buffer_selection_char_in_visual_mode() {
9148 use hjkl_buffer::{Position, Selection};
9149 let mut e = editor_with("hello world");
9150 run_keys(&mut e, "vlll");
9151 assert_eq!(
9152 e.buffer_selection(),
9153 Some(Selection::Char {
9154 anchor: Position::new(0, 0),
9155 head: Position::new(0, 3),
9156 })
9157 );
9158 }
9159
9160 #[test]
9161 fn buffer_selection_line_in_visual_line_mode() {
9162 use hjkl_buffer::Selection;
9163 let mut e = editor_with("a\nb\nc\nd");
9164 run_keys(&mut e, "Vj");
9165 assert_eq!(
9166 e.buffer_selection(),
9167 Some(Selection::Line {
9168 anchor_row: 0,
9169 head_row: 1,
9170 })
9171 );
9172 }
9173
9174 #[test]
9175 fn wrapscan_off_blocks_wrap_around() {
9176 let mut e = editor_with("first\nsecond\nthird\n");
9177 e.settings_mut().wrapscan = false;
9178 e.jump_cursor(2, 0);
9180 run_keys(&mut e, "/first<CR>");
9181 assert_eq!(e.cursor().0, 2, "wrapscan off should block wrap");
9183 e.settings_mut().wrapscan = true;
9185 run_keys(&mut e, "/first<CR>");
9186 assert_eq!(e.cursor().0, 0, "wrapscan on should wrap to row 0");
9187 }
9188
9189 #[test]
9190 fn smartcase_uppercase_pattern_stays_sensitive() {
9191 let mut e = editor_with("foo\nFoo\nBAR\n");
9192 e.settings_mut().ignore_case = true;
9193 e.settings_mut().smartcase = true;
9194 run_keys(&mut e, "/foo<CR>");
9197 let r1 = e
9198 .search_state()
9199 .pattern
9200 .as_ref()
9201 .unwrap()
9202 .as_str()
9203 .to_string();
9204 assert!(r1.starts_with("(?i)"), "lowercase under smartcase: {r1}");
9205 run_keys(&mut e, "/Foo<CR>");
9207 let r2 = e
9208 .search_state()
9209 .pattern
9210 .as_ref()
9211 .unwrap()
9212 .as_str()
9213 .to_string();
9214 assert!(!r2.starts_with("(?i)"), "mixed-case under smartcase: {r2}");
9215 }
9216
9217 #[test]
9218 fn enter_with_autoindent_copies_leading_whitespace() {
9219 let mut e = editor_with(" foo");
9220 e.jump_cursor(0, 7);
9221 run_keys(&mut e, "i<CR>");
9222 assert_eq!(e.buffer.line(1).unwrap(), " ");
9223 }
9224
9225 #[test]
9226 fn enter_without_autoindent_inserts_bare_newline() {
9227 let mut e = editor_with(" foo");
9228 e.settings_mut().autoindent = false;
9229 e.jump_cursor(0, 7);
9230 run_keys(&mut e, "i<CR>");
9231 assert_eq!(e.buffer.line(1).unwrap(), "");
9232 }
9233
9234 #[test]
9235 fn iskeyword_default_treats_alnum_underscore_as_word() {
9236 let mut e = editor_with("foo_bar baz");
9237 e.jump_cursor(0, 0);
9241 run_keys(&mut e, "*");
9242 let p = e
9243 .search_state()
9244 .pattern
9245 .as_ref()
9246 .unwrap()
9247 .as_str()
9248 .to_string();
9249 assert!(p.contains("foo_bar"), "default iskeyword: {p}");
9250 }
9251
9252 #[test]
9253 fn w_motion_respects_custom_iskeyword() {
9254 let mut e = editor_with("foo-bar baz");
9258 run_keys(&mut e, "w");
9259 assert_eq!(e.cursor().1, 3, "default iskeyword: {:?}", e.cursor());
9260 let mut e2 = editor_with("foo-bar baz");
9263 e2.set_iskeyword("@,_,45");
9264 run_keys(&mut e2, "w");
9265 assert_eq!(e2.cursor().1, 8, "dash-as-word: {:?}", e2.cursor());
9266 }
9267
9268 #[test]
9269 fn iskeyword_with_dash_treats_dash_as_word_char() {
9270 let mut e = editor_with("foo-bar baz");
9271 e.settings_mut().iskeyword = "@,_,45".to_string();
9272 e.jump_cursor(0, 0);
9273 run_keys(&mut e, "*");
9274 let p = e
9275 .search_state()
9276 .pattern
9277 .as_ref()
9278 .unwrap()
9279 .as_str()
9280 .to_string();
9281 assert!(p.contains("foo-bar"), "dash-as-word: {p}");
9282 }
9283
9284 #[test]
9285 fn timeoutlen_drops_pending_g_prefix() {
9286 use std::time::{Duration, Instant};
9287 let mut e = editor_with("a\nb\nc");
9288 e.jump_cursor(2, 0);
9289 run_keys(&mut e, "g");
9291 assert!(matches!(e.vim.pending, super::Pending::G));
9292 e.settings.timeout_len = Duration::from_nanos(0);
9300 e.vim.last_input_at = Some(Instant::now() - Duration::from_secs(60));
9301 e.vim.last_input_host_at = Some(Duration::ZERO);
9302 run_keys(&mut e, "g");
9306 assert_eq!(e.cursor().0, 2, "timeout must abandon g-prefix");
9308 }
9309
9310 #[test]
9311 fn undobreak_on_breaks_group_at_arrow_motion() {
9312 let mut e = editor_with("");
9313 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
9315 let line = e.buffer.line(0).unwrap_or("").to_string();
9318 assert!(line.contains("aaa"), "after undobreak: {line:?}");
9319 assert!(!line.contains("bbb"), "bbb should be undone: {line:?}");
9320 }
9321
9322 #[test]
9323 fn undobreak_off_keeps_full_run_in_one_group() {
9324 let mut e = editor_with("");
9325 e.settings_mut().undo_break_on_motion = false;
9326 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
9327 assert_eq!(e.buffer.line(0).unwrap_or(""), "");
9330 }
9331
9332 #[test]
9333 fn undobreak_round_trips_through_options() {
9334 let e = editor_with("");
9335 let opts = e.current_options();
9336 assert!(opts.undo_break_on_motion);
9337 let mut e2 = editor_with("");
9338 let mut new_opts = opts.clone();
9339 new_opts.undo_break_on_motion = false;
9340 e2.apply_options(&new_opts);
9341 assert!(!e2.current_options().undo_break_on_motion);
9342 }
9343
9344 #[test]
9345 fn undo_levels_cap_drops_oldest() {
9346 let mut e = editor_with("abcde");
9347 e.settings_mut().undo_levels = 3;
9348 run_keys(&mut e, "ra");
9349 run_keys(&mut e, "lrb");
9350 run_keys(&mut e, "lrc");
9351 run_keys(&mut e, "lrd");
9352 run_keys(&mut e, "lre");
9353 assert_eq!(e.undo_stack_len(), 3);
9354 }
9355
9356 #[test]
9357 fn tab_inserts_literal_tab_when_noexpandtab() {
9358 let mut e = editor_with("");
9359 e.settings_mut().expandtab = false;
9362 e.settings_mut().softtabstop = 0;
9363 run_keys(&mut e, "i");
9364 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9365 assert_eq!(e.buffer.line(0).unwrap(), "\t");
9366 }
9367
9368 #[test]
9369 fn tab_inserts_spaces_when_expandtab() {
9370 let mut e = editor_with("");
9371 e.settings_mut().expandtab = true;
9372 e.settings_mut().tabstop = 4;
9373 run_keys(&mut e, "i");
9374 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9375 assert_eq!(e.buffer.line(0).unwrap(), " ");
9376 }
9377
9378 #[test]
9379 fn tab_with_softtabstop_fills_to_next_boundary() {
9380 let mut e = editor_with("ab");
9382 e.settings_mut().expandtab = true;
9383 e.settings_mut().tabstop = 8;
9384 e.settings_mut().softtabstop = 4;
9385 run_keys(&mut e, "A"); e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9387 assert_eq!(e.buffer.line(0).unwrap(), "ab ");
9388 }
9389
9390 #[test]
9391 fn backspace_deletes_softtab_run() {
9392 let mut e = editor_with(" x");
9395 e.settings_mut().softtabstop = 4;
9396 run_keys(&mut e, "fxi");
9398 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
9399 assert_eq!(e.buffer.line(0).unwrap(), "x");
9400 }
9401
9402 #[test]
9403 fn backspace_falls_back_to_single_char_when_run_not_aligned() {
9404 let mut e = editor_with(" x");
9407 e.settings_mut().softtabstop = 4;
9408 run_keys(&mut e, "fxi");
9409 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
9410 assert_eq!(e.buffer.line(0).unwrap(), " x");
9411 }
9412
9413 #[test]
9414 fn readonly_blocks_insert_mutation() {
9415 let mut e = editor_with("hello");
9416 e.settings_mut().readonly = true;
9417 run_keys(&mut e, "iX<Esc>");
9418 assert_eq!(e.buffer.line(0).unwrap(), "hello");
9419 }
9420
9421 #[cfg(feature = "ratatui")]
9422 #[test]
9423 fn intern_ratatui_style_dedups_repeated_styles() {
9424 use ratatui::style::{Color, Style};
9425 let mut e = editor_with("");
9426 let red = Style::default().fg(Color::Red);
9427 let blue = Style::default().fg(Color::Blue);
9428 let id_r1 = e.intern_ratatui_style(red);
9429 let id_r2 = e.intern_ratatui_style(red);
9430 let id_b = e.intern_ratatui_style(blue);
9431 assert_eq!(id_r1, id_r2);
9432 assert_ne!(id_r1, id_b);
9433 assert_eq!(e.style_table().len(), 2);
9434 }
9435
9436 #[cfg(feature = "ratatui")]
9437 #[test]
9438 fn install_ratatui_syntax_spans_translates_styled_spans() {
9439 use ratatui::style::{Color, Style};
9440 let mut e = editor_with("SELECT foo");
9441 e.install_ratatui_syntax_spans(vec![vec![(0, 6, Style::default().fg(Color::Red))]]);
9442 let by_row = e.buffer_spans();
9443 assert_eq!(by_row.len(), 1);
9444 assert_eq!(by_row[0].len(), 1);
9445 assert_eq!(by_row[0][0].start_byte, 0);
9446 assert_eq!(by_row[0][0].end_byte, 6);
9447 let id = by_row[0][0].style;
9448 assert_eq!(e.style_table()[id as usize].fg, Some(Color::Red));
9449 }
9450
9451 #[cfg(feature = "ratatui")]
9452 #[test]
9453 fn install_ratatui_syntax_spans_clamps_sentinel_end() {
9454 use ratatui::style::{Color, Style};
9455 let mut e = editor_with("hello");
9456 e.install_ratatui_syntax_spans(vec![vec![(
9457 0,
9458 usize::MAX,
9459 Style::default().fg(Color::Blue),
9460 )]]);
9461 let by_row = e.buffer_spans();
9462 assert_eq!(by_row[0][0].end_byte, 5);
9463 }
9464
9465 #[cfg(feature = "ratatui")]
9466 #[test]
9467 fn install_ratatui_syntax_spans_drops_zero_width() {
9468 use ratatui::style::{Color, Style};
9469 let mut e = editor_with("abc");
9470 e.install_ratatui_syntax_spans(vec![vec![(2, 2, Style::default().fg(Color::Red))]]);
9471 assert!(e.buffer_spans()[0].is_empty());
9472 }
9473
9474 #[test]
9475 fn named_register_yank_into_a_then_paste_from_a() {
9476 let mut e = editor_with("hello world\nsecond");
9477 run_keys(&mut e, "\"ayw");
9478 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
9480 run_keys(&mut e, "j0\"aP");
9482 assert_eq!(e.buffer().lines()[1], "hello second");
9483 }
9484
9485 #[test]
9486 fn capital_r_overstrikes_chars() {
9487 let mut e = editor_with("hello");
9488 e.jump_cursor(0, 0);
9489 run_keys(&mut e, "RXY<Esc>");
9490 assert_eq!(e.buffer().lines()[0], "XYllo");
9492 }
9493
9494 #[test]
9495 fn capital_r_at_eol_appends() {
9496 let mut e = editor_with("hi");
9497 e.jump_cursor(0, 1);
9498 run_keys(&mut e, "RXYZ<Esc>");
9500 assert_eq!(e.buffer().lines()[0], "hXYZ");
9501 }
9502
9503 #[test]
9504 fn capital_r_count_does_not_repeat_overstrike_char_by_char() {
9505 let mut e = editor_with("abc");
9509 e.jump_cursor(0, 0);
9510 run_keys(&mut e, "RX<Esc>");
9511 assert_eq!(e.buffer().lines()[0], "Xbc");
9512 }
9513
9514 #[test]
9515 fn ctrl_r_in_insert_pastes_named_register() {
9516 let mut e = editor_with("hello world");
9517 run_keys(&mut e, "\"ayw");
9519 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
9520 run_keys(&mut e, "o");
9522 assert_eq!(e.vim_mode(), VimMode::Insert);
9523 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9524 e.handle_key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
9525 assert_eq!(e.buffer().lines()[1], "hello ");
9526 assert_eq!(e.cursor(), (1, 6));
9528 assert_eq!(e.vim_mode(), VimMode::Insert);
9530 e.handle_key(KeyEvent::new(KeyCode::Char('X'), KeyModifiers::NONE));
9531 assert_eq!(e.buffer().lines()[1], "hello X");
9532 }
9533
9534 #[test]
9535 fn ctrl_r_with_unnamed_register() {
9536 let mut e = editor_with("foo");
9537 run_keys(&mut e, "yiw");
9538 run_keys(&mut e, "A ");
9539 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9541 e.handle_key(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE));
9542 assert_eq!(e.buffer().lines()[0], "foo foo");
9543 }
9544
9545 #[test]
9546 fn ctrl_r_unknown_selector_is_no_op() {
9547 let mut e = editor_with("abc");
9548 run_keys(&mut e, "A");
9549 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9550 e.handle_key(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE));
9553 e.handle_key(KeyEvent::new(KeyCode::Char('Z'), KeyModifiers::NONE));
9554 assert_eq!(e.buffer().lines()[0], "abcZ");
9555 }
9556
9557 #[test]
9558 fn ctrl_r_multiline_register_pastes_with_newlines() {
9559 let mut e = editor_with("alpha\nbeta\ngamma");
9560 run_keys(&mut e, "\"byy");
9562 run_keys(&mut e, "j\"byy");
9563 run_keys(&mut e, "ggVj\"by");
9567 let payload = e.registers().read('b').unwrap().text.clone();
9568 assert!(payload.contains('\n'));
9569 run_keys(&mut e, "Go");
9570 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9571 e.handle_key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));
9572 let total_lines = e.buffer().lines().len();
9575 assert!(total_lines >= 5);
9576 }
9577
9578 #[test]
9579 fn yank_zero_holds_last_yank_after_delete() {
9580 let mut e = editor_with("hello world");
9581 run_keys(&mut e, "yw");
9582 let yanked = e.registers().read('0').unwrap().text.clone();
9583 assert!(!yanked.is_empty());
9584 run_keys(&mut e, "dw");
9586 assert_eq!(e.registers().read('0').unwrap().text, yanked);
9587 assert!(!e.registers().read('1').unwrap().text.is_empty());
9589 }
9590
9591 #[test]
9592 fn delete_ring_rotates_through_one_through_nine() {
9593 let mut e = editor_with("a b c d e f g h i j");
9594 for _ in 0..3 {
9596 run_keys(&mut e, "dw");
9597 }
9598 let r1 = e.registers().read('1').unwrap().text.clone();
9600 let r2 = e.registers().read('2').unwrap().text.clone();
9601 let r3 = e.registers().read('3').unwrap().text.clone();
9602 assert!(!r1.is_empty() && !r2.is_empty() && !r3.is_empty());
9603 assert_ne!(r1, r2);
9604 assert_ne!(r2, r3);
9605 }
9606
9607 #[test]
9608 fn capital_register_appends_to_lowercase() {
9609 let mut e = editor_with("foo bar");
9610 run_keys(&mut e, "\"ayw");
9611 let first = e.registers().read('a').unwrap().text.clone();
9612 assert!(first.contains("foo"));
9613 run_keys(&mut e, "w\"Ayw");
9615 let combined = e.registers().read('a').unwrap().text.clone();
9616 assert!(combined.starts_with(&first));
9617 assert!(combined.contains("bar"));
9618 }
9619
9620 #[test]
9621 fn zf_in_visual_line_creates_closed_fold() {
9622 let mut e = editor_with("a\nb\nc\nd\ne");
9623 e.jump_cursor(1, 0);
9625 run_keys(&mut e, "Vjjzf");
9626 assert_eq!(e.buffer().folds().len(), 1);
9627 let f = e.buffer().folds()[0];
9628 assert_eq!(f.start_row, 1);
9629 assert_eq!(f.end_row, 3);
9630 assert!(f.closed);
9631 }
9632
9633 #[test]
9634 fn zfj_in_normal_creates_two_row_fold() {
9635 let mut e = editor_with("a\nb\nc\nd\ne");
9636 e.jump_cursor(1, 0);
9637 run_keys(&mut e, "zfj");
9638 assert_eq!(e.buffer().folds().len(), 1);
9639 let f = e.buffer().folds()[0];
9640 assert_eq!(f.start_row, 1);
9641 assert_eq!(f.end_row, 2);
9642 assert!(f.closed);
9643 assert_eq!(e.cursor().0, 1);
9645 }
9646
9647 #[test]
9648 fn zf_with_count_folds_count_rows() {
9649 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9650 e.jump_cursor(0, 0);
9651 run_keys(&mut e, "zf3j");
9653 assert_eq!(e.buffer().folds().len(), 1);
9654 let f = e.buffer().folds()[0];
9655 assert_eq!(f.start_row, 0);
9656 assert_eq!(f.end_row, 3);
9657 }
9658
9659 #[test]
9660 fn zfk_folds_upward_range() {
9661 let mut e = editor_with("a\nb\nc\nd\ne");
9662 e.jump_cursor(3, 0);
9663 run_keys(&mut e, "zfk");
9664 let f = e.buffer().folds()[0];
9665 assert_eq!(f.start_row, 2);
9667 assert_eq!(f.end_row, 3);
9668 }
9669
9670 #[test]
9671 fn zf_capital_g_folds_to_bottom() {
9672 let mut e = editor_with("a\nb\nc\nd\ne");
9673 e.jump_cursor(1, 0);
9674 run_keys(&mut e, "zfG");
9676 let f = e.buffer().folds()[0];
9677 assert_eq!(f.start_row, 1);
9678 assert_eq!(f.end_row, 4);
9679 }
9680
9681 #[test]
9682 fn zfgg_folds_to_top_via_operator_pipeline() {
9683 let mut e = editor_with("a\nb\nc\nd\ne");
9684 e.jump_cursor(3, 0);
9685 run_keys(&mut e, "zfgg");
9689 let f = e.buffer().folds()[0];
9690 assert_eq!(f.start_row, 0);
9691 assert_eq!(f.end_row, 3);
9692 }
9693
9694 #[test]
9695 fn zfip_folds_paragraph_via_text_object() {
9696 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta\nepsilon");
9697 e.jump_cursor(1, 0);
9698 run_keys(&mut e, "zfip");
9700 assert_eq!(e.buffer().folds().len(), 1);
9701 let f = e.buffer().folds()[0];
9702 assert_eq!(f.start_row, 0);
9703 assert_eq!(f.end_row, 2);
9704 }
9705
9706 #[test]
9707 fn zfap_folds_paragraph_with_trailing_blank() {
9708 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta");
9709 e.jump_cursor(0, 0);
9710 run_keys(&mut e, "zfap");
9712 let f = e.buffer().folds()[0];
9713 assert_eq!(f.start_row, 0);
9714 assert_eq!(f.end_row, 3);
9715 }
9716
9717 #[test]
9718 fn zf_paragraph_motion_folds_to_blank() {
9719 let mut e = editor_with("alpha\nbeta\n\ngamma");
9720 e.jump_cursor(0, 0);
9721 run_keys(&mut e, "zf}");
9723 let f = e.buffer().folds()[0];
9724 assert_eq!(f.start_row, 0);
9725 assert_eq!(f.end_row, 2);
9726 }
9727
9728 #[test]
9729 fn za_toggles_fold_under_cursor() {
9730 let mut e = editor_with("a\nb\nc\nd");
9731 e.buffer_mut().add_fold(1, 2, true);
9732 e.jump_cursor(1, 0);
9733 run_keys(&mut e, "za");
9734 assert!(!e.buffer().folds()[0].closed);
9735 run_keys(&mut e, "za");
9736 assert!(e.buffer().folds()[0].closed);
9737 }
9738
9739 #[test]
9740 fn zr_opens_all_folds_zm_closes_all() {
9741 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9742 e.buffer_mut().add_fold(0, 1, true);
9743 e.buffer_mut().add_fold(2, 3, true);
9744 e.buffer_mut().add_fold(4, 5, true);
9745 run_keys(&mut e, "zR");
9746 assert!(e.buffer().folds().iter().all(|f| !f.closed));
9747 run_keys(&mut e, "zM");
9748 assert!(e.buffer().folds().iter().all(|f| f.closed));
9749 }
9750
9751 #[test]
9752 fn ze_clears_all_folds() {
9753 let mut e = editor_with("a\nb\nc\nd");
9754 e.buffer_mut().add_fold(0, 1, true);
9755 e.buffer_mut().add_fold(2, 3, false);
9756 run_keys(&mut e, "zE");
9757 assert!(e.buffer().folds().is_empty());
9758 }
9759
9760 #[test]
9761 fn g_underscore_jumps_to_last_non_blank() {
9762 let mut e = editor_with("hello world ");
9763 run_keys(&mut e, "g_");
9764 assert_eq!(e.cursor().1, 10);
9766 }
9767
9768 #[test]
9769 fn gj_and_gk_alias_j_and_k() {
9770 let mut e = editor_with("a\nb\nc");
9771 run_keys(&mut e, "gj");
9772 assert_eq!(e.cursor().0, 1);
9773 run_keys(&mut e, "gk");
9774 assert_eq!(e.cursor().0, 0);
9775 }
9776
9777 #[test]
9778 fn paragraph_motions_walk_blank_lines() {
9779 let mut e = editor_with("first\nblock\n\nsecond\nblock\n\nthird");
9780 run_keys(&mut e, "}");
9781 assert_eq!(e.cursor().0, 2);
9782 run_keys(&mut e, "}");
9783 assert_eq!(e.cursor().0, 5);
9784 run_keys(&mut e, "{");
9785 assert_eq!(e.cursor().0, 2);
9786 }
9787
9788 #[test]
9789 fn gv_reenters_last_visual_selection() {
9790 let mut e = editor_with("alpha\nbeta\ngamma");
9791 run_keys(&mut e, "Vj");
9792 run_keys(&mut e, "<Esc>");
9794 assert_eq!(e.vim_mode(), VimMode::Normal);
9795 run_keys(&mut e, "gv");
9797 assert_eq!(e.vim_mode(), VimMode::VisualLine);
9798 }
9799
9800 #[test]
9801 fn o_in_visual_swaps_anchor_and_cursor() {
9802 let mut e = editor_with("hello world");
9803 run_keys(&mut e, "vllll");
9805 assert_eq!(e.cursor().1, 4);
9806 run_keys(&mut e, "o");
9808 assert_eq!(e.cursor().1, 0);
9809 assert_eq!(e.vim.visual_anchor, (0, 4));
9811 }
9812
9813 #[test]
9814 fn editing_inside_fold_invalidates_it() {
9815 let mut e = editor_with("a\nb\nc\nd");
9816 e.buffer_mut().add_fold(1, 2, true);
9817 e.jump_cursor(1, 0);
9818 run_keys(&mut e, "iX<Esc>");
9820 assert!(e.buffer().folds().is_empty());
9822 }
9823
9824 #[test]
9825 fn zd_removes_fold_under_cursor() {
9826 let mut e = editor_with("a\nb\nc\nd");
9827 e.buffer_mut().add_fold(1, 2, true);
9828 e.jump_cursor(2, 0);
9829 run_keys(&mut e, "zd");
9830 assert!(e.buffer().folds().is_empty());
9831 }
9832
9833 #[test]
9834 fn take_fold_ops_observes_z_keystroke_dispatch() {
9835 use crate::types::FoldOp;
9840 let mut e = editor_with("a\nb\nc\nd");
9841 e.buffer_mut().add_fold(1, 2, true);
9842 e.jump_cursor(1, 0);
9843 let _ = e.take_fold_ops();
9846 run_keys(&mut e, "zo");
9847 run_keys(&mut e, "zM");
9848 let ops = e.take_fold_ops();
9849 assert_eq!(ops.len(), 2);
9850 assert!(matches!(ops[0], FoldOp::OpenAt(1)));
9851 assert!(matches!(ops[1], FoldOp::CloseAll));
9852 assert!(e.take_fold_ops().is_empty());
9854 }
9855
9856 #[test]
9857 fn edit_pipeline_emits_invalidate_fold_op() {
9858 use crate::types::FoldOp;
9861 let mut e = editor_with("a\nb\nc\nd");
9862 e.buffer_mut().add_fold(1, 2, true);
9863 e.jump_cursor(1, 0);
9864 let _ = e.take_fold_ops();
9865 run_keys(&mut e, "iX<Esc>");
9866 let ops = e.take_fold_ops();
9867 assert!(
9868 ops.iter().any(|op| matches!(op, FoldOp::Invalidate { .. })),
9869 "expected at least one Invalidate op, got {ops:?}"
9870 );
9871 }
9872
9873 #[test]
9874 fn dot_mark_jumps_to_last_edit_position() {
9875 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
9876 e.jump_cursor(2, 0);
9877 run_keys(&mut e, "iX<Esc>");
9879 let after_edit = e.cursor();
9880 run_keys(&mut e, "gg");
9882 assert_eq!(e.cursor().0, 0);
9883 run_keys(&mut e, "'.");
9885 assert_eq!(e.cursor().0, after_edit.0);
9886 }
9887
9888 #[test]
9889 fn quote_quote_returns_to_pre_jump_position() {
9890 let mut e = editor_with_rows(50, 20);
9891 e.jump_cursor(10, 2);
9892 let before = e.cursor();
9893 run_keys(&mut e, "G");
9895 assert_ne!(e.cursor(), before);
9896 run_keys(&mut e, "''");
9898 assert_eq!(e.cursor().0, before.0);
9899 }
9900
9901 #[test]
9902 fn backtick_backtick_restores_exact_pre_jump_pos() {
9903 let mut e = editor_with_rows(50, 20);
9904 e.jump_cursor(7, 3);
9905 let before = e.cursor();
9906 run_keys(&mut e, "G");
9907 run_keys(&mut e, "``");
9908 assert_eq!(e.cursor(), before);
9909 }
9910
9911 #[test]
9912 fn macro_record_and_replay_basic() {
9913 let mut e = editor_with("foo\nbar\nbaz");
9914 run_keys(&mut e, "qaIX<Esc>jq");
9916 assert_eq!(e.buffer().lines()[0], "Xfoo");
9917 run_keys(&mut e, "@a");
9919 assert_eq!(e.buffer().lines()[1], "Xbar");
9920 run_keys(&mut e, "j@@");
9922 assert_eq!(e.buffer().lines()[2], "Xbaz");
9923 }
9924
9925 #[test]
9926 fn macro_count_replays_n_times() {
9927 let mut e = editor_with("a\nb\nc\nd\ne");
9928 run_keys(&mut e, "qajq");
9930 assert_eq!(e.cursor().0, 1);
9931 run_keys(&mut e, "3@a");
9933 assert_eq!(e.cursor().0, 4);
9934 }
9935
9936 #[test]
9937 fn macro_capital_q_appends_to_lowercase_register() {
9938 let mut e = editor_with("hello");
9939 run_keys(&mut e, "qall<Esc>q");
9940 run_keys(&mut e, "qAhh<Esc>q");
9941 let text = e.registers().read('a').unwrap().text.clone();
9944 assert!(text.contains("ll<Esc>"));
9945 assert!(text.contains("hh<Esc>"));
9946 }
9947
9948 #[test]
9949 fn buffer_selection_block_in_visual_block_mode() {
9950 use hjkl_buffer::{Position, Selection};
9951 let mut e = editor_with("aaaa\nbbbb\ncccc");
9952 run_keys(&mut e, "<C-v>jl");
9953 assert_eq!(
9954 e.buffer_selection(),
9955 Some(Selection::Block {
9956 anchor: Position::new(0, 0),
9957 head: Position::new(1, 1),
9958 })
9959 );
9960 }
9961
9962 #[test]
9965 fn n_after_question_mark_keeps_walking_backward() {
9966 let mut e = editor_with("foo bar foo baz foo end");
9969 e.jump_cursor(0, 22);
9970 run_keys(&mut e, "?foo<CR>");
9971 assert_eq!(e.cursor().1, 16);
9972 run_keys(&mut e, "n");
9973 assert_eq!(e.cursor().1, 8);
9974 run_keys(&mut e, "N");
9975 assert_eq!(e.cursor().1, 16);
9976 }
9977
9978 #[test]
9979 fn nested_macro_chord_records_literal_keys() {
9980 let mut e = editor_with("alpha\nbeta\ngamma");
9983 run_keys(&mut e, "qblq");
9985 run_keys(&mut e, "qaIX<Esc>q");
9988 e.jump_cursor(1, 0);
9990 run_keys(&mut e, "@a");
9991 assert_eq!(e.buffer().lines()[1], "Xbeta");
9992 }
9993
9994 #[test]
9995 fn shift_gt_motion_indents_one_line() {
9996 let mut e = editor_with("hello world");
10000 run_keys(&mut e, ">w");
10001 assert_eq!(e.buffer().lines()[0], " hello world");
10002 }
10003
10004 #[test]
10005 fn shift_lt_motion_outdents_one_line() {
10006 let mut e = editor_with(" hello world");
10007 run_keys(&mut e, "<lt>w");
10008 assert_eq!(e.buffer().lines()[0], " hello world");
10010 }
10011
10012 #[test]
10013 fn shift_gt_text_object_indents_paragraph() {
10014 let mut e = editor_with("alpha\nbeta\ngamma\n\nrest");
10015 e.jump_cursor(0, 0);
10016 run_keys(&mut e, ">ip");
10017 assert_eq!(e.buffer().lines()[0], " alpha");
10018 assert_eq!(e.buffer().lines()[1], " beta");
10019 assert_eq!(e.buffer().lines()[2], " gamma");
10020 assert_eq!(e.buffer().lines()[4], "rest");
10022 }
10023
10024 #[test]
10025 fn ctrl_o_runs_exactly_one_normal_command() {
10026 let mut e = editor_with("alpha beta gamma");
10029 e.jump_cursor(0, 0);
10030 run_keys(&mut e, "i");
10031 e.handle_key(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL));
10032 run_keys(&mut e, "dw");
10033 assert_eq!(e.vim_mode(), VimMode::Insert);
10035 run_keys(&mut e, "X");
10037 assert_eq!(e.buffer().lines()[0], "Xbeta gamma");
10038 }
10039
10040 #[test]
10041 fn macro_replay_respects_mode_switching() {
10042 let mut e = editor_with("hi");
10046 run_keys(&mut e, "qaiX<Esc>0q");
10047 assert_eq!(e.vim_mode(), VimMode::Normal);
10048 e.set_content("yo");
10050 run_keys(&mut e, "@a");
10051 assert_eq!(e.vim_mode(), VimMode::Normal);
10052 assert_eq!(e.cursor().1, 0);
10053 assert_eq!(e.buffer().lines()[0], "Xyo");
10054 }
10055
10056 #[test]
10057 fn macro_recorded_text_round_trips_through_register() {
10058 let mut e = editor_with("");
10062 run_keys(&mut e, "qaiX<Esc>q");
10063 let text = e.registers().read('a').unwrap().text.clone();
10064 assert!(text.starts_with("iX"));
10065 run_keys(&mut e, "@a");
10067 assert_eq!(e.buffer().lines()[0], "XX");
10068 }
10069
10070 #[test]
10071 fn dot_after_macro_replays_macros_last_change() {
10072 let mut e = editor_with("ab\ncd\nef");
10075 run_keys(&mut e, "qaIX<Esc>jq");
10078 assert_eq!(e.buffer().lines()[0], "Xab");
10079 run_keys(&mut e, "@a");
10080 assert_eq!(e.buffer().lines()[1], "Xcd");
10081 let row_before_dot = e.cursor().0;
10084 run_keys(&mut e, ".");
10085 assert!(e.buffer().lines()[row_before_dot].starts_with('X'));
10086 }
10087
10088 fn si_editor(content: &str) -> Editor {
10094 let opts = crate::types::Options {
10095 shiftwidth: 4,
10096 softtabstop: 4,
10097 expandtab: true,
10098 smartindent: true,
10099 autoindent: true,
10100 ..crate::types::Options::default()
10101 };
10102 let mut e = Editor::new(
10103 hjkl_buffer::Buffer::new(),
10104 crate::types::DefaultHost::new(),
10105 opts,
10106 );
10107 e.set_content(content);
10108 e
10109 }
10110
10111 #[test]
10112 fn smartindent_bumps_indent_after_open_brace() {
10113 let mut e = si_editor("fn foo() {");
10115 e.jump_cursor(0, 10); run_keys(&mut e, "i<CR>");
10117 assert_eq!(
10118 e.buffer().lines()[1],
10119 " ",
10120 "smartindent should bump one shiftwidth after {{"
10121 );
10122 }
10123
10124 #[test]
10125 fn smartindent_no_bump_when_off() {
10126 let mut e = si_editor("fn foo() {");
10129 e.settings_mut().smartindent = false;
10130 e.jump_cursor(0, 10);
10131 run_keys(&mut e, "i<CR>");
10132 assert_eq!(
10133 e.buffer().lines()[1],
10134 "",
10135 "without smartindent, no bump: new line copies empty leading ws"
10136 );
10137 }
10138
10139 #[test]
10140 fn smartindent_uses_tab_when_noexpandtab() {
10141 let opts = crate::types::Options {
10143 shiftwidth: 4,
10144 softtabstop: 0,
10145 expandtab: false,
10146 smartindent: true,
10147 autoindent: true,
10148 ..crate::types::Options::default()
10149 };
10150 let mut e = Editor::new(
10151 hjkl_buffer::Buffer::new(),
10152 crate::types::DefaultHost::new(),
10153 opts,
10154 );
10155 e.set_content("fn foo() {");
10156 e.jump_cursor(0, 10);
10157 run_keys(&mut e, "i<CR>");
10158 assert_eq!(
10159 e.buffer().lines()[1],
10160 "\t",
10161 "noexpandtab: smartindent bump inserts a literal tab"
10162 );
10163 }
10164
10165 #[test]
10166 fn smartindent_dedent_on_close_brace() {
10167 let mut e = si_editor("fn foo() {");
10170 e.set_content("fn foo() {\n ");
10172 e.jump_cursor(1, 4); run_keys(&mut e, "i}");
10174 assert_eq!(
10175 e.buffer().lines()[1],
10176 "}",
10177 "close brace on whitespace-only line should dedent"
10178 );
10179 assert_eq!(e.cursor(), (1, 1), "cursor should be after the `}}`");
10180 }
10181
10182 #[test]
10183 fn smartindent_no_dedent_when_off() {
10184 let mut e = si_editor("fn foo() {\n ");
10186 e.settings_mut().smartindent = false;
10187 e.jump_cursor(1, 4);
10188 run_keys(&mut e, "i}");
10189 assert_eq!(
10190 e.buffer().lines()[1],
10191 " }",
10192 "without smartindent, `}}` just appends at cursor"
10193 );
10194 }
10195
10196 #[test]
10197 fn smartindent_no_dedent_mid_line() {
10198 let mut e = si_editor(" let x = 1");
10201 e.jump_cursor(0, 13); run_keys(&mut e, "i}");
10203 assert_eq!(
10204 e.buffer().lines()[0],
10205 " let x = 1}",
10206 "mid-line `}}` should not dedent"
10207 );
10208 }
10209
10210 #[test]
10214 fn count_5x_fills_unnamed_register() {
10215 let mut e = editor_with("hello world\n");
10216 e.jump_cursor(0, 0);
10217 run_keys(&mut e, "5x");
10218 assert_eq!(e.buffer().lines()[0], " world");
10219 assert_eq!(e.cursor(), (0, 0));
10220 assert_eq!(e.yank(), "hello");
10221 }
10222
10223 #[test]
10224 fn x_fills_unnamed_register_single_char() {
10225 let mut e = editor_with("abc\n");
10226 e.jump_cursor(0, 0);
10227 run_keys(&mut e, "x");
10228 assert_eq!(e.buffer().lines()[0], "bc");
10229 assert_eq!(e.yank(), "a");
10230 }
10231
10232 #[test]
10233 fn big_x_fills_unnamed_register() {
10234 let mut e = editor_with("hello\n");
10235 e.jump_cursor(0, 3);
10236 run_keys(&mut e, "X");
10237 assert_eq!(e.buffer().lines()[0], "helo");
10238 assert_eq!(e.yank(), "l");
10239 }
10240
10241 #[test]
10243 fn g_motion_trailing_newline_lands_on_last_content_row() {
10244 let mut e = editor_with("foo\nbar\nbaz\n");
10245 e.jump_cursor(0, 0);
10246 run_keys(&mut e, "G");
10247 assert_eq!(
10249 e.cursor().0,
10250 2,
10251 "G should land on row 2 (baz), not row 3 (phantom empty)"
10252 );
10253 }
10254
10255 #[test]
10257 fn dd_last_line_clamps_cursor_to_new_last_row() {
10258 let mut e = editor_with("foo\nbar\n");
10259 e.jump_cursor(1, 0);
10260 run_keys(&mut e, "dd");
10261 assert_eq!(e.buffer().lines()[0], "foo");
10262 assert_eq!(
10263 e.cursor(),
10264 (0, 0),
10265 "cursor should clamp to row 0 after dd on last content line"
10266 );
10267 }
10268
10269 #[test]
10271 fn d_dollar_cursor_on_last_char() {
10272 let mut e = editor_with("hello world\n");
10273 e.jump_cursor(0, 5);
10274 run_keys(&mut e, "d$");
10275 assert_eq!(e.buffer().lines()[0], "hello");
10276 assert_eq!(
10277 e.cursor(),
10278 (0, 4),
10279 "d$ should leave cursor on col 4, not col 5"
10280 );
10281 }
10282
10283 #[test]
10285 fn undo_insert_clamps_cursor_to_last_valid_col() {
10286 let mut e = editor_with("hello\n");
10287 e.jump_cursor(0, 5); run_keys(&mut e, "a world<Esc>u");
10289 assert_eq!(e.buffer().lines()[0], "hello");
10290 assert_eq!(
10291 e.cursor(),
10292 (0, 4),
10293 "undo should clamp cursor to col 4 on 'hello'"
10294 );
10295 }
10296
10297 #[test]
10299 fn da_doublequote_eats_trailing_whitespace() {
10300 let mut e = editor_with("say \"hello\" there\n");
10301 e.jump_cursor(0, 6);
10302 run_keys(&mut e, "da\"");
10303 assert_eq!(e.buffer().lines()[0], "say there");
10304 assert_eq!(e.cursor().1, 4, "cursor should be at col 4 after da\"");
10305 }
10306
10307 #[test]
10309 fn dab_cursor_col_clamped_after_delete() {
10310 let mut e = editor_with("fn x() {\n body\n}\n");
10311 e.jump_cursor(1, 4);
10312 run_keys(&mut e, "daB");
10313 assert_eq!(e.buffer().lines()[0], "fn x() ");
10314 assert_eq!(
10315 e.cursor(),
10316 (0, 6),
10317 "daB should leave cursor at col 6, not 7"
10318 );
10319 }
10320
10321 #[test]
10323 fn dib_preserves_surrounding_newlines() {
10324 let mut e = editor_with("{\n body\n}\n");
10325 e.jump_cursor(1, 4);
10326 run_keys(&mut e, "diB");
10327 assert_eq!(e.buffer().lines()[0], "{");
10328 assert_eq!(e.buffer().lines()[1], "}");
10329 assert_eq!(e.cursor().0, 1, "cursor should be on the '}}' line");
10330 }
10331
10332 #[test]
10333 fn is_chord_pending_tracks_replace_state() {
10334 let mut e = editor_with("abc\n");
10335 assert!(!e.is_chord_pending());
10336 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE));
10338 assert!(e.is_chord_pending(), "engine should be pending after r");
10339 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
10341 assert!(
10342 !e.is_chord_pending(),
10343 "engine pending should clear after replace"
10344 );
10345 }
10346
10347 #[test]
10350 fn yiw_sets_lbr_rbr_marks_around_word() {
10351 let mut e = editor_with("hello world");
10354 run_keys(&mut e, "yiw");
10355 let lo = e.mark('[').expect("'[' must be set after yiw");
10356 let hi = e.mark(']').expect("']' must be set after yiw");
10357 assert_eq!(lo, (0, 0), "'[ should be first char of yanked word");
10358 assert_eq!(hi, (0, 4), "'] should be last char of yanked word");
10359 }
10360
10361 #[test]
10362 fn yj_linewise_sets_marks_at_line_edges() {
10363 let mut e = editor_with("aaaaa\nbbbbb\nccc");
10366 run_keys(&mut e, "yj");
10367 let lo = e.mark('[').expect("'[' must be set after yj");
10368 let hi = e.mark(']').expect("']' must be set after yj");
10369 assert_eq!(lo, (0, 0), "'[ snaps to (top_row, 0) for linewise yank");
10370 assert_eq!(
10371 hi,
10372 (1, 4),
10373 "'] snaps to (bot_row, last_col) for linewise yank"
10374 );
10375 }
10376
10377 #[test]
10378 fn dd_sets_lbr_rbr_marks_to_cursor() {
10379 let mut e = editor_with("aaa\nbbb");
10382 run_keys(&mut e, "dd");
10383 let lo = e.mark('[').expect("'[' must be set after dd");
10384 let hi = e.mark(']').expect("']' must be set after dd");
10385 assert_eq!(lo, hi, "after delete both marks are at the same position");
10386 assert_eq!(lo.0, 0, "post-delete cursor row should be 0");
10387 }
10388
10389 #[test]
10390 fn dw_sets_lbr_rbr_marks_to_cursor() {
10391 let mut e = editor_with("hello world");
10394 run_keys(&mut e, "dw");
10395 let lo = e.mark('[').expect("'[' must be set after dw");
10396 let hi = e.mark(']').expect("']' must be set after dw");
10397 assert_eq!(lo, hi, "after delete both marks are at the same position");
10398 assert_eq!(lo, (0, 0), "post-dw cursor is at col 0");
10399 }
10400
10401 #[test]
10402 fn cw_then_esc_sets_lbr_at_start_rbr_at_inserted_text_end() {
10403 let mut e = editor_with("hello world");
10408 run_keys(&mut e, "cwfoo<Esc>");
10409 let lo = e.mark('[').expect("'[' must be set after cw");
10410 let hi = e.mark(']').expect("']' must be set after cw");
10411 assert_eq!(lo, (0, 0), "'[ should be start of change");
10412 assert_eq!(hi.0, 0, "'] should be on row 0");
10415 assert!(hi.1 >= 2, "'] should be at or past last char of 'foo'");
10416 }
10417
10418 #[test]
10419 fn cw_with_no_insertion_sets_marks_at_change_start() {
10420 let mut e = editor_with("hello world");
10423 run_keys(&mut e, "cw<Esc>");
10424 let lo = e.mark('[').expect("'[' must be set after cw<Esc>");
10425 let hi = e.mark(']').expect("']' must be set after cw<Esc>");
10426 assert_eq!(lo.0, 0, "'[ should be on row 0");
10427 assert_eq!(hi.0, 0, "'] should be on row 0");
10428 assert_eq!(lo, hi, "marks coincide when insert is empty");
10430 }
10431
10432 #[test]
10433 fn p_charwise_sets_marks_around_pasted_text() {
10434 let mut e = editor_with("abc xyz");
10437 run_keys(&mut e, "yiw"); run_keys(&mut e, "p"); let lo = e.mark('[').expect("'[' set after charwise paste");
10440 let hi = e.mark(']').expect("']' set after charwise paste");
10441 assert!(lo <= hi, "'[ must not exceed ']'");
10442 assert_eq!(
10444 hi.1.wrapping_sub(lo.1),
10445 2,
10446 "'] - '[ should span 2 cols for a 3-char paste"
10447 );
10448 }
10449
10450 #[test]
10451 fn p_linewise_sets_marks_at_line_edges() {
10452 let mut e = editor_with("aaa\nbbb\nccc");
10455 run_keys(&mut e, "yj"); run_keys(&mut e, "j"); run_keys(&mut e, "p"); let lo = e.mark('[').expect("'[' set after linewise paste");
10459 let hi = e.mark(']').expect("']' set after linewise paste");
10460 assert_eq!(lo.1, 0, "'[ col must be 0 for linewise paste");
10461 assert!(hi.0 > lo.0, "'] row must be below '[ row for 2-line paste");
10462 assert_eq!(hi.0 - lo.0, 1, "exactly 1 row gap for a 2-line payload");
10463 }
10464
10465 #[test]
10466 fn backtick_lbr_v_backtick_rbr_reselects_yanked_text() {
10467 let mut e = editor_with("hello world");
10471 run_keys(&mut e, "yiw"); run_keys(&mut e, "`[v`]");
10475 assert_eq!(
10477 e.cursor(),
10478 (0, 4),
10479 "visual `[v`] should land on last yanked char"
10480 );
10481 assert_eq!(
10483 e.vim_mode(),
10484 crate::VimMode::Visual,
10485 "should be in Visual mode"
10486 );
10487 }
10488
10489 #[test]
10495 fn mark_dot_jump_to_last_edit_pre_edit_cursor() {
10496 let mut e = editor_with("hello\nworld\n");
10499 e.jump_cursor(0, 0);
10500 run_keys(&mut e, "iX<Esc>j`.");
10501 assert_eq!(
10502 e.cursor(),
10503 (0, 0),
10504 "dot mark should jump to the change-start (col 0), not post-insert col"
10505 );
10506 }
10507
10508 #[test]
10511 fn count_100g_clamps_to_last_content_row() {
10512 let mut e = editor_with("foo\nbar\nbaz\n");
10515 e.jump_cursor(0, 0);
10516 run_keys(&mut e, "100G");
10517 assert_eq!(
10518 e.cursor(),
10519 (2, 0),
10520 "100G on trailing-newline buffer must clamp to row 2 (last content row)"
10521 );
10522 }
10523
10524 #[test]
10527 fn gi_resumes_last_insert_position() {
10528 let mut e = editor_with("world\nhello\n");
10534 e.jump_cursor(0, 0);
10535 run_keys(&mut e, "iHi<Esc>jgi<Esc>");
10536 assert_eq!(
10537 e.vim_mode(),
10538 crate::VimMode::Normal,
10539 "should be in Normal mode after gi<Esc>"
10540 );
10541 assert_eq!(
10542 e.cursor(),
10543 (0, 1),
10544 "gi<Esc> cursor should be at (0,1) — the insert row, step-back col"
10545 );
10546 }
10547
10548 #[test]
10552 fn visual_block_change_cursor_on_last_inserted_char() {
10553 let mut e = editor_with("foo\nbar\nbaz\n");
10557 e.jump_cursor(0, 0);
10558 run_keys(&mut e, "<C-v>jlcZZ<Esc>");
10559 let lines = e.buffer().lines().to_vec();
10560 assert_eq!(lines[0], "ZZo", "row 0 should be 'ZZo'");
10561 assert_eq!(lines[1], "ZZr", "row 1 should be 'ZZr'");
10562 assert_eq!(
10563 e.cursor(),
10564 (0, 1),
10565 "cursor should be on last char of inserted 'ZZ' (col 1)"
10566 );
10567 }
10568
10569 #[test]
10574 fn register_blackhole_delete_preserves_unnamed_register() {
10575 let mut e = editor_with("foo bar baz\n");
10582 e.jump_cursor(0, 0);
10583 run_keys(&mut e, "yiww\"_dwbp");
10584 let lines = e.buffer().lines().to_vec();
10585 assert_eq!(
10586 lines[0], "ffoooo baz",
10587 "black-hole delete must not corrupt unnamed register"
10588 );
10589 assert_eq!(
10590 e.cursor(),
10591 (0, 3),
10592 "cursor should be on last pasted char (col 3)"
10593 );
10594 }
10595
10596 #[test]
10599 fn after_z_zz_sets_viewport_pinned() {
10600 let mut e = editor_with("a\nb\nc\nd\ne");
10601 e.jump_cursor(2, 0);
10602 e.after_z('z', 1);
10603 assert!(e.vim.viewport_pinned, "zz must set viewport_pinned");
10604 }
10605
10606 #[test]
10607 fn after_z_zo_opens_fold_at_cursor() {
10608 let mut e = editor_with("a\nb\nc\nd");
10609 e.buffer_mut().add_fold(1, 2, true);
10610 e.jump_cursor(1, 0);
10611 e.after_z('o', 1);
10612 assert!(
10613 !e.buffer().folds()[0].closed,
10614 "zo must open the fold at the cursor row"
10615 );
10616 }
10617
10618 #[test]
10619 fn after_z_zm_closes_all_folds() {
10620 let mut e = editor_with("a\nb\nc\nd\ne\nf");
10621 e.buffer_mut().add_fold(0, 1, false);
10622 e.buffer_mut().add_fold(4, 5, false);
10623 e.after_z('M', 1);
10624 assert!(
10625 e.buffer().folds().iter().all(|f| f.closed),
10626 "zM must close all folds"
10627 );
10628 }
10629
10630 #[test]
10631 fn after_z_zd_removes_fold_at_cursor() {
10632 let mut e = editor_with("a\nb\nc\nd");
10633 e.buffer_mut().add_fold(1, 2, true);
10634 e.jump_cursor(1, 0);
10635 e.after_z('d', 1);
10636 assert!(
10637 e.buffer().folds().is_empty(),
10638 "zd must remove the fold at the cursor row"
10639 );
10640 }
10641
10642 #[test]
10643 fn after_z_zf_in_visual_creates_fold() {
10644 let mut e = editor_with("a\nb\nc\nd\ne");
10645 e.jump_cursor(1, 0);
10647 run_keys(&mut e, "V2j");
10648 e.after_z('f', 1);
10650 let folds = e.buffer().folds();
10651 assert_eq!(folds.len(), 1, "zf in visual must create exactly one fold");
10652 assert_eq!(folds[0].start_row, 1);
10653 assert_eq!(folds[0].end_row, 3);
10654 assert!(folds[0].closed);
10655 }
10656
10657 #[test]
10660 fn apply_op_motion_dw_deletes_word() {
10661 let mut e = editor_with("hello world");
10663 e.apply_op_motion(crate::vim::Operator::Delete, 'w', 1);
10664 assert_eq!(
10665 e.buffer().lines().first().cloned().unwrap_or_default(),
10666 "world"
10667 );
10668 }
10669
10670 #[test]
10671 fn apply_op_motion_cw_quirk_leaves_trailing_space() {
10672 let mut e = editor_with("hello world");
10674 e.apply_op_motion(crate::vim::Operator::Change, 'w', 1);
10675 let line = e.buffer().lines().first().cloned().unwrap_or_default();
10678 assert!(
10679 line.starts_with(' ') || line == " world",
10680 "cw quirk: got {line:?}"
10681 );
10682 assert_eq!(e.vim_mode(), VimMode::Insert);
10683 }
10684
10685 #[test]
10686 fn apply_op_double_dd_deletes_line() {
10687 let mut e = editor_with("line1\nline2\nline3");
10688 e.apply_op_double(crate::vim::Operator::Delete, 1);
10690 let lines: Vec<_> = e.buffer().lines().to_vec();
10691 assert_eq!(lines, vec!["line2", "line3"], "dd should delete line1");
10692 }
10693
10694 #[test]
10695 fn apply_op_double_yy_does_not_modify_buffer() {
10696 let mut e = editor_with("hello");
10697 e.apply_op_double(crate::vim::Operator::Yank, 1);
10698 assert_eq!(
10699 e.buffer().lines().first().cloned().unwrap_or_default(),
10700 "hello"
10701 );
10702 }
10703
10704 #[test]
10705 fn apply_op_double_dd_count2_deletes_two_lines() {
10706 let mut e = editor_with("line1\nline2\nline3");
10707 e.apply_op_double(crate::vim::Operator::Delete, 2);
10708 let lines: Vec<_> = e.buffer().lines().to_vec();
10709 assert_eq!(lines, vec!["line3"], "2dd should delete two lines");
10710 }
10711
10712 #[test]
10713 fn apply_op_motion_unknown_key_is_noop() {
10714 let mut e = editor_with("hello");
10716 let before = e.cursor();
10717 e.apply_op_motion(crate::vim::Operator::Delete, 'X', 1); assert_eq!(e.cursor(), before);
10719 assert_eq!(
10720 e.buffer().lines().first().cloned().unwrap_or_default(),
10721 "hello"
10722 );
10723 }
10724
10725 #[test]
10728 fn apply_op_find_dfx_deletes_to_x() {
10729 let mut e = editor_with("hello x world");
10731 e.apply_op_find(crate::vim::Operator::Delete, 'x', true, false, 1);
10732 assert_eq!(
10733 e.buffer().lines().first().cloned().unwrap_or_default(),
10734 " world",
10735 "dfx must delete 'hello x'"
10736 );
10737 }
10738
10739 #[test]
10740 fn apply_op_find_dtx_deletes_up_to_x() {
10741 let mut e = editor_with("hello x world");
10743 e.apply_op_find(crate::vim::Operator::Delete, 'x', true, true, 1);
10744 assert_eq!(
10745 e.buffer().lines().first().cloned().unwrap_or_default(),
10746 "x world",
10747 "dtx must delete 'hello ' leaving 'x world'"
10748 );
10749 }
10750
10751 #[test]
10752 fn apply_op_find_records_last_find() {
10753 let mut e = editor_with("hello x world");
10755 e.apply_op_find(crate::vim::Operator::Delete, 'x', true, false, 1);
10756 let _ = e.cursor(); }
10763
10764 #[test]
10767 fn apply_op_text_obj_diw_deletes_word() {
10768 let mut e = editor_with("hello world");
10770 e.apply_op_text_obj(crate::vim::Operator::Delete, 'w', true, 1);
10771 let line = e.buffer().lines().first().cloned().unwrap_or_default();
10772 assert!(
10777 !line.contains("hello"),
10778 "diw must delete 'hello', remaining: {line:?}"
10779 );
10780 }
10781
10782 #[test]
10783 fn apply_op_text_obj_daw_deletes_around_word() {
10784 let mut e = editor_with("hello world");
10786 e.apply_op_text_obj(crate::vim::Operator::Delete, 'w', false, 1);
10787 let line = e.buffer().lines().first().cloned().unwrap_or_default();
10788 assert!(
10789 !line.contains("hello"),
10790 "daw must delete 'hello' and surrounding space, remaining: {line:?}"
10791 );
10792 }
10793
10794 #[test]
10795 fn apply_op_text_obj_invalid_char_no_op() {
10796 let mut e = editor_with("hello world");
10798 let before = e.buffer().as_string();
10799 e.apply_op_text_obj(crate::vim::Operator::Delete, 'X', true, 1);
10800 assert_eq!(
10801 e.buffer().as_string(),
10802 before,
10803 "unknown text-object char must be a no-op"
10804 );
10805 }
10806
10807 #[test]
10810 fn apply_op_g_dgg_deletes_to_top() {
10811 let mut e = editor_with("line1\nline2\nline3");
10813 e.apply_op_motion(crate::vim::Operator::Delete, 'j', 1);
10815 e.apply_op_g(crate::vim::Operator::Delete, 'g', 1);
10818 let lines: Vec<_> = e.buffer().lines().to_vec();
10820 assert_eq!(lines, vec!["line3"], "dgg must delete to file top");
10821 }
10822
10823 #[test]
10824 fn apply_op_g_dge_deletes_word_end_back() {
10825 let mut e = editor_with("hello world");
10838 let before = e.buffer().as_string();
10839 e.apply_op_g(crate::vim::Operator::Delete, 'X', 1);
10841 assert_eq!(
10842 e.buffer().as_string(),
10843 before,
10844 "apply_op_g with unknown char must be a no-op"
10845 );
10846 e.apply_op_g(crate::vim::Operator::Delete, 'e', 1);
10848 }
10850
10851 #[test]
10852 fn apply_op_g_dgj_deletes_screen_down() {
10853 let mut e = editor_with("line1\nline2\nline3");
10856 e.apply_op_g(crate::vim::Operator::Delete, 'j', 1);
10857 let lines: Vec<_> = e.buffer().lines().to_vec();
10858 assert_eq!(lines, vec!["line3"], "dgj must delete current+next line");
10860 }
10861
10862 fn blank_editor() -> Editor {
10865 Editor::new(
10866 hjkl_buffer::Buffer::new(),
10867 crate::types::DefaultHost::new(),
10868 crate::types::Options::default(),
10869 )
10870 }
10871
10872 #[test]
10873 fn set_pending_register_valid_letter_sets_field() {
10874 let mut e = blank_editor();
10875 assert!(e.vim.pending_register.is_none());
10876 e.set_pending_register('a');
10877 assert_eq!(e.vim.pending_register, Some('a'));
10878 }
10879
10880 #[test]
10881 fn set_pending_register_invalid_char_no_op() {
10882 let mut e = blank_editor();
10883 e.set_pending_register('!');
10884 assert!(
10885 e.vim.pending_register.is_none(),
10886 "invalid register char must not set pending_register"
10887 );
10888 }
10889
10890 #[test]
10891 fn set_pending_register_special_plus_sets_field() {
10892 let mut e = blank_editor();
10894 e.set_pending_register('+');
10895 assert_eq!(e.vim.pending_register, Some('+'));
10896 }
10897
10898 #[test]
10899 fn set_pending_register_star_sets_field() {
10900 let mut e = blank_editor();
10902 e.set_pending_register('*');
10903 assert_eq!(e.vim.pending_register, Some('*'));
10904 }
10905
10906 #[test]
10907 fn set_pending_register_underscore_sets_field() {
10908 let mut e = blank_editor();
10910 e.set_pending_register('_');
10911 assert_eq!(e.vim.pending_register, Some('_'));
10912 }
10913}