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
2111pub(crate) fn set_mark_at_cursor<H: crate::types::Host>(
2118 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2119 ch: char,
2120) {
2121 if ch.is_ascii_lowercase() || ch.is_ascii_uppercase() {
2122 let pos = ed.cursor();
2127 ed.set_mark(ch, pos);
2128 }
2129 }
2131
2132fn handle_set_mark<H: crate::types::Host>(
2133 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2134 input: Input,
2135) -> bool {
2136 if let Key::Char(c) = input.key {
2137 set_mark_at_cursor(ed, c);
2138 }
2139 true
2140}
2141
2142pub(crate) fn goto_mark<H: crate::types::Host>(
2151 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2152 ch: char,
2153 linewise: bool,
2154) {
2155 let target = match ch {
2157 'a'..='z' | 'A'..='Z' => ed.mark(ch),
2158 '\'' | '`' => ed.vim.jump_back.last().copied(),
2159 '.' => ed.vim.last_edit_pos,
2160 '[' | ']' | '<' | '>' => ed.mark(ch),
2161 _ => None,
2162 };
2163 let Some((row, col)) = target else {
2164 return;
2165 };
2166 let pre = ed.cursor();
2167 let (r, c_clamped) = clamp_pos(ed, (row, col));
2168 if linewise {
2169 buf_set_cursor_rc(&mut ed.buffer, r, 0);
2170 ed.push_buffer_cursor_to_textarea();
2171 move_first_non_whitespace(ed);
2172 } else {
2173 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
2174 ed.push_buffer_cursor_to_textarea();
2175 }
2176 if ed.cursor() != pre {
2177 push_jump(ed, pre);
2178 }
2179 ed.sticky_col = Some(ed.cursor().1);
2180}
2181
2182fn handle_select_register<H: crate::types::Host>(
2188 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2189 input: Input,
2190) -> bool {
2191 if let Key::Char(c) = input.key {
2192 ed.set_pending_register(c);
2193 }
2194 true
2195}
2196
2197fn handle_record_macro_target<H: crate::types::Host>(
2202 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2203 input: Input,
2204) -> bool {
2205 if let Key::Char(c) = input.key
2206 && (c.is_ascii_alphabetic() || c.is_ascii_digit())
2207 {
2208 ed.vim.recording_macro = Some(c);
2209 if c.is_ascii_uppercase() {
2212 let lower = c.to_ascii_lowercase();
2213 let text = ed
2217 .registers()
2218 .read(lower)
2219 .map(|s| s.text.clone())
2220 .unwrap_or_default();
2221 ed.vim.recording_keys = crate::input::decode_macro(&text);
2222 } else {
2223 ed.vim.recording_keys.clear();
2224 }
2225 }
2226 true
2227}
2228
2229fn handle_play_macro_target<H: crate::types::Host>(
2235 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2236 input: Input,
2237 count: usize,
2238) -> bool {
2239 let reg = match input.key {
2240 Key::Char('@') => ed.vim.last_macro,
2241 Key::Char(c) if c.is_ascii_alphabetic() || c.is_ascii_digit() => {
2242 Some(c.to_ascii_lowercase())
2243 }
2244 _ => None,
2245 };
2246 let Some(reg) = reg else {
2247 return true;
2248 };
2249 let text = match ed.registers().read(reg) {
2252 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
2253 _ => return true,
2254 };
2255 let keys = crate::input::decode_macro(&text);
2256 ed.vim.last_macro = Some(reg);
2257 let times = count.max(1);
2258 let was_replaying = ed.vim.replaying_macro;
2259 ed.vim.replaying_macro = true;
2260 for _ in 0..times {
2261 for k in keys.iter().copied() {
2262 step(ed, k);
2263 }
2264 }
2265 ed.vim.replaying_macro = was_replaying;
2266 true
2267}
2268
2269fn handle_goto_mark<H: crate::types::Host>(
2270 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2271 input: Input,
2272 linewise: bool,
2273) -> bool {
2274 let Key::Char(c) = input.key else {
2275 return true;
2276 };
2277 goto_mark(ed, c, linewise);
2281 true
2282}
2283
2284fn take_count(vim: &mut VimState) -> usize {
2285 if vim.count > 0 {
2286 let n = vim.count;
2287 vim.count = 0;
2288 n
2289 } else {
2290 1
2291 }
2292}
2293
2294fn char_to_operator(c: char) -> Option<Operator> {
2295 match c {
2296 'd' => Some(Operator::Delete),
2297 'c' => Some(Operator::Change),
2298 'y' => Some(Operator::Yank),
2299 '>' => Some(Operator::Indent),
2300 '<' => Some(Operator::Outdent),
2301 _ => None,
2302 }
2303}
2304
2305fn visual_operator(input: &Input) -> Option<Operator> {
2306 if input.ctrl {
2307 return None;
2308 }
2309 match input.key {
2310 Key::Char('y') => Some(Operator::Yank),
2311 Key::Char('d') | Key::Char('x') => Some(Operator::Delete),
2312 Key::Char('c') | Key::Char('s') => Some(Operator::Change),
2313 Key::Char('U') => Some(Operator::Uppercase),
2315 Key::Char('u') => Some(Operator::Lowercase),
2316 Key::Char('~') => Some(Operator::ToggleCase),
2317 Key::Char('>') => Some(Operator::Indent),
2319 Key::Char('<') => Some(Operator::Outdent),
2320 _ => None,
2321 }
2322}
2323
2324fn find_entry(input: &Input) -> Option<(bool, bool)> {
2325 if input.ctrl {
2326 return None;
2327 }
2328 match input.key {
2329 Key::Char('f') => Some((true, false)),
2330 Key::Char('F') => Some((false, false)),
2331 Key::Char('t') => Some((true, true)),
2332 Key::Char('T') => Some((false, true)),
2333 _ => None,
2334 }
2335}
2336
2337const JUMPLIST_MAX: usize = 100;
2341
2342fn push_jump<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, from: (usize, usize)) {
2347 ed.vim.jump_back.push(from);
2348 if ed.vim.jump_back.len() > JUMPLIST_MAX {
2349 ed.vim.jump_back.remove(0);
2350 }
2351 ed.vim.jump_fwd.clear();
2352}
2353
2354fn jump_back<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2357 let Some(target) = ed.vim.jump_back.pop() else {
2358 return;
2359 };
2360 let cur = ed.cursor();
2361 ed.vim.jump_fwd.push(cur);
2362 let (r, c) = clamp_pos(ed, target);
2363 ed.jump_cursor(r, c);
2364 ed.sticky_col = Some(c);
2365}
2366
2367fn jump_forward<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2370 let Some(target) = ed.vim.jump_fwd.pop() else {
2371 return;
2372 };
2373 let cur = ed.cursor();
2374 ed.vim.jump_back.push(cur);
2375 if ed.vim.jump_back.len() > JUMPLIST_MAX {
2376 ed.vim.jump_back.remove(0);
2377 }
2378 let (r, c) = clamp_pos(ed, target);
2379 ed.jump_cursor(r, c);
2380 ed.sticky_col = Some(c);
2381}
2382
2383fn clamp_pos<H: crate::types::Host>(
2386 ed: &Editor<hjkl_buffer::Buffer, H>,
2387 pos: (usize, usize),
2388) -> (usize, usize) {
2389 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2390 let r = pos.0.min(last_row);
2391 let line_len = buf_line_chars(&ed.buffer, r);
2392 let c = pos.1.min(line_len.saturating_sub(1));
2393 (r, c)
2394}
2395
2396fn is_big_jump(motion: &Motion) -> bool {
2398 matches!(
2399 motion,
2400 Motion::FileTop
2401 | Motion::FileBottom
2402 | Motion::MatchBracket
2403 | Motion::WordAtCursor { .. }
2404 | Motion::SearchNext { .. }
2405 | Motion::ViewportTop
2406 | Motion::ViewportMiddle
2407 | Motion::ViewportBottom
2408 )
2409}
2410
2411fn viewport_half_rows<H: crate::types::Host>(
2416 ed: &Editor<hjkl_buffer::Buffer, H>,
2417 count: usize,
2418) -> usize {
2419 let h = ed.viewport_height_value() as usize;
2420 (h / 2).max(1).saturating_mul(count.max(1))
2421}
2422
2423fn viewport_full_rows<H: crate::types::Host>(
2426 ed: &Editor<hjkl_buffer::Buffer, H>,
2427 count: usize,
2428) -> usize {
2429 let h = ed.viewport_height_value() as usize;
2430 h.saturating_sub(2).max(1).saturating_mul(count.max(1))
2431}
2432
2433fn scroll_cursor_rows<H: crate::types::Host>(
2438 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2439 delta: isize,
2440) {
2441 if delta == 0 {
2442 return;
2443 }
2444 ed.sync_buffer_content_from_textarea();
2445 let (row, _) = ed.cursor();
2446 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2447 let target = (row as isize + delta).max(0).min(last_row as isize) as usize;
2448 buf_set_cursor_rc(&mut ed.buffer, target, 0);
2449 crate::motions::move_first_non_blank(&mut ed.buffer);
2450 ed.push_buffer_cursor_to_textarea();
2451 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2452}
2453
2454fn parse_motion(input: &Input) -> Option<Motion> {
2457 if input.ctrl {
2458 return None;
2459 }
2460 match input.key {
2461 Key::Char('h') | Key::Backspace | Key::Left => Some(Motion::Left),
2462 Key::Char('l') | Key::Right => Some(Motion::Right),
2463 Key::Char('j') | Key::Down | Key::Enter => Some(Motion::Down),
2464 Key::Char('k') | Key::Up => Some(Motion::Up),
2465 Key::Char('w') => Some(Motion::WordFwd),
2466 Key::Char('W') => Some(Motion::BigWordFwd),
2467 Key::Char('b') => Some(Motion::WordBack),
2468 Key::Char('B') => Some(Motion::BigWordBack),
2469 Key::Char('e') => Some(Motion::WordEnd),
2470 Key::Char('E') => Some(Motion::BigWordEnd),
2471 Key::Char('0') | Key::Home => Some(Motion::LineStart),
2472 Key::Char('^') => Some(Motion::FirstNonBlank),
2473 Key::Char('$') | Key::End => Some(Motion::LineEnd),
2474 Key::Char('G') => Some(Motion::FileBottom),
2475 Key::Char('%') => Some(Motion::MatchBracket),
2476 Key::Char(';') => Some(Motion::FindRepeat { reverse: false }),
2477 Key::Char(',') => Some(Motion::FindRepeat { reverse: true }),
2478 Key::Char('*') => Some(Motion::WordAtCursor {
2479 forward: true,
2480 whole_word: true,
2481 }),
2482 Key::Char('#') => Some(Motion::WordAtCursor {
2483 forward: false,
2484 whole_word: true,
2485 }),
2486 Key::Char('n') => Some(Motion::SearchNext { reverse: false }),
2487 Key::Char('N') => Some(Motion::SearchNext { reverse: true }),
2488 Key::Char('H') => Some(Motion::ViewportTop),
2489 Key::Char('M') => Some(Motion::ViewportMiddle),
2490 Key::Char('L') => Some(Motion::ViewportBottom),
2491 Key::Char('{') => Some(Motion::ParagraphPrev),
2492 Key::Char('}') => Some(Motion::ParagraphNext),
2493 Key::Char('(') => Some(Motion::SentencePrev),
2494 Key::Char(')') => Some(Motion::SentenceNext),
2495 _ => None,
2496 }
2497}
2498
2499pub(crate) fn execute_motion<H: crate::types::Host>(
2502 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2503 motion: Motion,
2504 count: usize,
2505) {
2506 let count = count.max(1);
2507 let motion = match motion {
2509 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2510 Some((ch, forward, till)) => Motion::Find {
2511 ch,
2512 forward: if reverse { !forward } else { forward },
2513 till,
2514 },
2515 None => return,
2516 },
2517 other => other,
2518 };
2519 let pre_pos = ed.cursor();
2520 let pre_col = pre_pos.1;
2521 apply_motion_cursor(ed, &motion, count);
2522 let post_pos = ed.cursor();
2523 if is_big_jump(&motion) && pre_pos != post_pos {
2524 push_jump(ed, pre_pos);
2525 }
2526 apply_sticky_col(ed, &motion, pre_col);
2527 ed.sync_buffer_from_textarea();
2532}
2533
2534fn execute_motion_with_block_vcol<H: crate::types::Host>(
2545 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2546 motion: Motion,
2547 count: usize,
2548) {
2549 let motion_copy = motion.clone();
2550 execute_motion(ed, motion, count);
2551 if ed.vim.mode == Mode::VisualBlock {
2552 update_block_vcol(ed, &motion_copy);
2553 }
2554}
2555
2556pub(crate) fn apply_motion_kind<H: crate::types::Host>(
2588 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2589 kind: hjkl_vim::MotionKind,
2590 count: usize,
2591) {
2592 let count = count.max(1);
2593 match kind {
2594 hjkl_vim::MotionKind::CharLeft => {
2595 execute_motion_with_block_vcol(ed, Motion::Left, count);
2596 }
2597 hjkl_vim::MotionKind::CharRight => {
2598 execute_motion_with_block_vcol(ed, Motion::Right, count);
2599 }
2600 hjkl_vim::MotionKind::LineDown => {
2601 execute_motion_with_block_vcol(ed, Motion::Down, count);
2602 }
2603 hjkl_vim::MotionKind::LineUp => {
2604 execute_motion_with_block_vcol(ed, Motion::Up, count);
2605 }
2606 hjkl_vim::MotionKind::FirstNonBlankDown => {
2607 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2612 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2613 crate::motions::move_first_non_blank(&mut ed.buffer);
2614 ed.push_buffer_cursor_to_textarea();
2615 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2616 ed.sync_buffer_from_textarea();
2617 }
2618 hjkl_vim::MotionKind::FirstNonBlankUp => {
2619 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2622 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2623 crate::motions::move_first_non_blank(&mut ed.buffer);
2624 ed.push_buffer_cursor_to_textarea();
2625 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2626 ed.sync_buffer_from_textarea();
2627 }
2628 hjkl_vim::MotionKind::WordForward => {
2629 execute_motion_with_block_vcol(ed, Motion::WordFwd, count);
2630 }
2631 hjkl_vim::MotionKind::BigWordForward => {
2632 execute_motion_with_block_vcol(ed, Motion::BigWordFwd, count);
2633 }
2634 hjkl_vim::MotionKind::WordBackward => {
2635 execute_motion_with_block_vcol(ed, Motion::WordBack, count);
2636 }
2637 hjkl_vim::MotionKind::BigWordBackward => {
2638 execute_motion_with_block_vcol(ed, Motion::BigWordBack, count);
2639 }
2640 hjkl_vim::MotionKind::WordEnd => {
2641 execute_motion_with_block_vcol(ed, Motion::WordEnd, count);
2642 }
2643 hjkl_vim::MotionKind::BigWordEnd => {
2644 execute_motion_with_block_vcol(ed, Motion::BigWordEnd, count);
2645 }
2646 hjkl_vim::MotionKind::LineStart => {
2647 execute_motion_with_block_vcol(ed, Motion::LineStart, 1);
2650 }
2651 hjkl_vim::MotionKind::FirstNonBlank => {
2652 execute_motion_with_block_vcol(ed, Motion::FirstNonBlank, 1);
2655 }
2656 hjkl_vim::MotionKind::GotoLine => {
2657 execute_motion_with_block_vcol(ed, Motion::FileBottom, count);
2666 }
2667 hjkl_vim::MotionKind::LineEnd => {
2668 execute_motion_with_block_vcol(ed, Motion::LineEnd, 1);
2672 }
2673 hjkl_vim::MotionKind::FindRepeat => {
2674 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: false }, count);
2678 }
2679 hjkl_vim::MotionKind::FindRepeatReverse => {
2680 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: true }, count);
2684 }
2685 hjkl_vim::MotionKind::BracketMatch => {
2686 execute_motion_with_block_vcol(ed, Motion::MatchBracket, count);
2691 }
2692 hjkl_vim::MotionKind::ViewportTop => {
2693 execute_motion_with_block_vcol(ed, Motion::ViewportTop, count);
2696 }
2697 hjkl_vim::MotionKind::ViewportMiddle => {
2698 execute_motion_with_block_vcol(ed, Motion::ViewportMiddle, count);
2701 }
2702 hjkl_vim::MotionKind::ViewportBottom => {
2703 execute_motion_with_block_vcol(ed, Motion::ViewportBottom, count);
2706 }
2707 hjkl_vim::MotionKind::HalfPageDown => {
2708 scroll_cursor_rows(ed, viewport_half_rows(ed, count) as isize);
2714 }
2715 hjkl_vim::MotionKind::HalfPageUp => {
2716 scroll_cursor_rows(ed, -(viewport_half_rows(ed, count) as isize));
2719 }
2720 hjkl_vim::MotionKind::FullPageDown => {
2721 scroll_cursor_rows(ed, viewport_full_rows(ed, count) as isize);
2724 }
2725 hjkl_vim::MotionKind::FullPageUp => {
2726 scroll_cursor_rows(ed, -(viewport_full_rows(ed, count) as isize));
2729 }
2730 _ => {
2731 }
2735 }
2736}
2737
2738fn apply_sticky_col<H: crate::types::Host>(
2743 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2744 motion: &Motion,
2745 pre_col: usize,
2746) {
2747 if is_vertical_motion(motion) {
2748 let want = ed.sticky_col.unwrap_or(pre_col);
2749 ed.sticky_col = Some(want);
2752 let (row, _) = ed.cursor();
2753 let line_len = buf_line_chars(&ed.buffer, row);
2754 let max_col = line_len.saturating_sub(1);
2758 let target = want.min(max_col);
2759 ed.jump_cursor(row, target);
2760 } else {
2761 ed.sticky_col = Some(ed.cursor().1);
2764 }
2765}
2766
2767fn is_vertical_motion(motion: &Motion) -> bool {
2768 matches!(
2772 motion,
2773 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown
2774 )
2775}
2776
2777fn apply_motion_cursor<H: crate::types::Host>(
2778 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2779 motion: &Motion,
2780 count: usize,
2781) {
2782 apply_motion_cursor_ctx(ed, motion, count, false)
2783}
2784
2785fn apply_motion_cursor_ctx<H: crate::types::Host>(
2786 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2787 motion: &Motion,
2788 count: usize,
2789 as_operator: bool,
2790) {
2791 match motion {
2792 Motion::Left => {
2793 crate::motions::move_left(&mut ed.buffer, count);
2795 ed.push_buffer_cursor_to_textarea();
2796 }
2797 Motion::Right => {
2798 if as_operator {
2802 crate::motions::move_right_to_end(&mut ed.buffer, count);
2803 } else {
2804 crate::motions::move_right_in_line(&mut ed.buffer, count);
2805 }
2806 ed.push_buffer_cursor_to_textarea();
2807 }
2808 Motion::Up => {
2809 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2813 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2814 ed.push_buffer_cursor_to_textarea();
2815 }
2816 Motion::Down => {
2817 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2818 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2819 ed.push_buffer_cursor_to_textarea();
2820 }
2821 Motion::ScreenUp => {
2822 let v = *ed.host.viewport();
2823 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2824 crate::motions::move_screen_up(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2825 ed.push_buffer_cursor_to_textarea();
2826 }
2827 Motion::ScreenDown => {
2828 let v = *ed.host.viewport();
2829 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2830 crate::motions::move_screen_down(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2831 ed.push_buffer_cursor_to_textarea();
2832 }
2833 Motion::WordFwd => {
2834 crate::motions::move_word_fwd(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2835 ed.push_buffer_cursor_to_textarea();
2836 }
2837 Motion::WordBack => {
2838 crate::motions::move_word_back(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2839 ed.push_buffer_cursor_to_textarea();
2840 }
2841 Motion::WordEnd => {
2842 crate::motions::move_word_end(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2843 ed.push_buffer_cursor_to_textarea();
2844 }
2845 Motion::BigWordFwd => {
2846 crate::motions::move_word_fwd(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2847 ed.push_buffer_cursor_to_textarea();
2848 }
2849 Motion::BigWordBack => {
2850 crate::motions::move_word_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2851 ed.push_buffer_cursor_to_textarea();
2852 }
2853 Motion::BigWordEnd => {
2854 crate::motions::move_word_end(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2855 ed.push_buffer_cursor_to_textarea();
2856 }
2857 Motion::WordEndBack => {
2858 crate::motions::move_word_end_back(
2859 &mut ed.buffer,
2860 false,
2861 count,
2862 &ed.settings.iskeyword,
2863 );
2864 ed.push_buffer_cursor_to_textarea();
2865 }
2866 Motion::BigWordEndBack => {
2867 crate::motions::move_word_end_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2868 ed.push_buffer_cursor_to_textarea();
2869 }
2870 Motion::LineStart => {
2871 crate::motions::move_line_start(&mut ed.buffer);
2872 ed.push_buffer_cursor_to_textarea();
2873 }
2874 Motion::FirstNonBlank => {
2875 crate::motions::move_first_non_blank(&mut ed.buffer);
2876 ed.push_buffer_cursor_to_textarea();
2877 }
2878 Motion::LineEnd => {
2879 crate::motions::move_line_end(&mut ed.buffer);
2881 ed.push_buffer_cursor_to_textarea();
2882 }
2883 Motion::FileTop => {
2884 if count > 1 {
2887 crate::motions::move_bottom(&mut ed.buffer, count);
2888 } else {
2889 crate::motions::move_top(&mut ed.buffer);
2890 }
2891 ed.push_buffer_cursor_to_textarea();
2892 }
2893 Motion::FileBottom => {
2894 if count > 1 {
2897 crate::motions::move_bottom(&mut ed.buffer, count);
2898 } else {
2899 crate::motions::move_bottom(&mut ed.buffer, 0);
2900 }
2901 ed.push_buffer_cursor_to_textarea();
2902 }
2903 Motion::Find { ch, forward, till } => {
2904 for _ in 0..count {
2905 if !find_char_on_line(ed, *ch, *forward, *till) {
2906 break;
2907 }
2908 }
2909 }
2910 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
2912 let _ = matching_bracket(ed);
2913 }
2914 Motion::WordAtCursor {
2915 forward,
2916 whole_word,
2917 } => {
2918 word_at_cursor_search(ed, *forward, *whole_word, count);
2919 }
2920 Motion::SearchNext { reverse } => {
2921 if let Some(pattern) = ed.vim.last_search.clone() {
2925 push_search_pattern(ed, &pattern);
2926 }
2927 if ed.search_state().pattern.is_none() {
2928 return;
2929 }
2930 let forward = ed.vim.last_search_forward != *reverse;
2934 for _ in 0..count.max(1) {
2935 if forward {
2936 ed.search_advance_forward(true);
2937 } else {
2938 ed.search_advance_backward(true);
2939 }
2940 }
2941 ed.push_buffer_cursor_to_textarea();
2942 }
2943 Motion::ViewportTop => {
2944 let v = *ed.host().viewport();
2945 crate::motions::move_viewport_top(&mut ed.buffer, &v, count.saturating_sub(1));
2946 ed.push_buffer_cursor_to_textarea();
2947 }
2948 Motion::ViewportMiddle => {
2949 let v = *ed.host().viewport();
2950 crate::motions::move_viewport_middle(&mut ed.buffer, &v);
2951 ed.push_buffer_cursor_to_textarea();
2952 }
2953 Motion::ViewportBottom => {
2954 let v = *ed.host().viewport();
2955 crate::motions::move_viewport_bottom(&mut ed.buffer, &v, count.saturating_sub(1));
2956 ed.push_buffer_cursor_to_textarea();
2957 }
2958 Motion::LastNonBlank => {
2959 crate::motions::move_last_non_blank(&mut ed.buffer);
2960 ed.push_buffer_cursor_to_textarea();
2961 }
2962 Motion::LineMiddle => {
2963 let row = ed.cursor().0;
2964 let line_chars = buf_line_chars(&ed.buffer, row);
2965 let target = line_chars / 2;
2968 ed.jump_cursor(row, target);
2969 }
2970 Motion::ParagraphPrev => {
2971 crate::motions::move_paragraph_prev(&mut ed.buffer, count);
2972 ed.push_buffer_cursor_to_textarea();
2973 }
2974 Motion::ParagraphNext => {
2975 crate::motions::move_paragraph_next(&mut ed.buffer, count);
2976 ed.push_buffer_cursor_to_textarea();
2977 }
2978 Motion::SentencePrev => {
2979 for _ in 0..count.max(1) {
2980 if let Some((row, col)) = sentence_boundary(ed, false) {
2981 ed.jump_cursor(row, col);
2982 }
2983 }
2984 }
2985 Motion::SentenceNext => {
2986 for _ in 0..count.max(1) {
2987 if let Some((row, col)) = sentence_boundary(ed, true) {
2988 ed.jump_cursor(row, col);
2989 }
2990 }
2991 }
2992 }
2993}
2994
2995fn move_first_non_whitespace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2996 ed.sync_buffer_content_from_textarea();
3002 crate::motions::move_first_non_blank(&mut ed.buffer);
3003 ed.push_buffer_cursor_to_textarea();
3004}
3005
3006fn find_char_on_line<H: crate::types::Host>(
3007 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3008 ch: char,
3009 forward: bool,
3010 till: bool,
3011) -> bool {
3012 let moved = crate::motions::find_char_on_line(&mut ed.buffer, ch, forward, till);
3013 if moved {
3014 ed.push_buffer_cursor_to_textarea();
3015 }
3016 moved
3017}
3018
3019fn matching_bracket<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
3020 let moved = crate::motions::match_bracket(&mut ed.buffer);
3021 if moved {
3022 ed.push_buffer_cursor_to_textarea();
3023 }
3024 moved
3025}
3026
3027fn word_at_cursor_search<H: crate::types::Host>(
3028 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3029 forward: bool,
3030 whole_word: bool,
3031 count: usize,
3032) {
3033 let (row, col) = ed.cursor();
3034 let line: String = buf_line(&ed.buffer, row).unwrap_or("").to_string();
3035 let chars: Vec<char> = line.chars().collect();
3036 if chars.is_empty() {
3037 return;
3038 }
3039 let spec = ed.settings().iskeyword.clone();
3041 let is_word = |c: char| is_keyword_char(c, &spec);
3042 let mut start = col.min(chars.len().saturating_sub(1));
3043 while start > 0 && is_word(chars[start - 1]) {
3044 start -= 1;
3045 }
3046 let mut end = start;
3047 while end < chars.len() && is_word(chars[end]) {
3048 end += 1;
3049 }
3050 if end <= start {
3051 return;
3052 }
3053 let word: String = chars[start..end].iter().collect();
3054 let escaped = regex_escape(&word);
3055 let pattern = if whole_word {
3056 format!(r"\b{escaped}\b")
3057 } else {
3058 escaped
3059 };
3060 push_search_pattern(ed, &pattern);
3061 if ed.search_state().pattern.is_none() {
3062 return;
3063 }
3064 ed.vim.last_search = Some(pattern);
3066 ed.vim.last_search_forward = forward;
3067 for _ in 0..count.max(1) {
3068 if forward {
3069 ed.search_advance_forward(true);
3070 } else {
3071 ed.search_advance_backward(true);
3072 }
3073 }
3074 ed.push_buffer_cursor_to_textarea();
3075}
3076
3077fn regex_escape(s: &str) -> String {
3078 let mut out = String::with_capacity(s.len());
3079 for c in s.chars() {
3080 if matches!(
3081 c,
3082 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
3083 ) {
3084 out.push('\\');
3085 }
3086 out.push(c);
3087 }
3088 out
3089}
3090
3091pub(crate) fn apply_op_motion_key<H: crate::types::Host>(
3105 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3106 op: Operator,
3107 motion_key: char,
3108 total_count: usize,
3109) {
3110 let input = Input {
3111 key: Key::Char(motion_key),
3112 ctrl: false,
3113 alt: false,
3114 shift: false,
3115 };
3116 let Some(motion) = parse_motion(&input) else {
3117 return;
3118 };
3119 let motion = match motion {
3120 Motion::FindRepeat { reverse } => match ed.vim.last_find {
3121 Some((ch, forward, till)) => Motion::Find {
3122 ch,
3123 forward: if reverse { !forward } else { forward },
3124 till,
3125 },
3126 None => return,
3127 },
3128 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
3130 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
3131 m => m,
3132 };
3133 apply_op_with_motion(ed, op, &motion, total_count);
3134 if let Motion::Find { ch, forward, till } = &motion {
3135 ed.vim.last_find = Some((*ch, *forward, *till));
3136 }
3137 if !ed.vim.replaying && op_is_change(op) {
3138 ed.vim.last_change = Some(LastChange::OpMotion {
3139 op,
3140 motion,
3141 count: total_count,
3142 inserted: None,
3143 });
3144 }
3145}
3146
3147pub(crate) fn apply_op_double<H: crate::types::Host>(
3150 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3151 op: Operator,
3152 total_count: usize,
3153) {
3154 execute_line_op(ed, op, total_count);
3155 if !ed.vim.replaying {
3156 ed.vim.last_change = Some(LastChange::LineOp {
3157 op,
3158 count: total_count,
3159 inserted: None,
3160 });
3161 }
3162}
3163
3164fn handle_after_op<H: crate::types::Host>(
3165 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3166 input: Input,
3167 op: Operator,
3168 count1: usize,
3169) -> bool {
3170 if let Key::Char(d @ '0'..='9') = input.key
3172 && !input.ctrl
3173 && (d != '0' || ed.vim.count > 0)
3174 {
3175 ed.vim.count = ed.vim.count.saturating_mul(10) + (d as usize - '0' as usize);
3176 ed.vim.pending = Pending::Op { op, count1 };
3177 return true;
3178 }
3179
3180 if input.key == Key::Esc {
3182 ed.vim.count = 0;
3183 return true;
3184 }
3185
3186 let double_ch = match op {
3190 Operator::Delete => Some('d'),
3191 Operator::Change => Some('c'),
3192 Operator::Yank => Some('y'),
3193 Operator::Indent => Some('>'),
3194 Operator::Outdent => Some('<'),
3195 Operator::Uppercase => Some('U'),
3196 Operator::Lowercase => Some('u'),
3197 Operator::ToggleCase => Some('~'),
3198 Operator::Fold => None,
3199 Operator::Reflow => Some('q'),
3202 };
3203 if let Key::Char(c) = input.key
3204 && !input.ctrl
3205 && Some(c) == double_ch
3206 {
3207 let count2 = take_count(&mut ed.vim);
3208 let total = count1.max(1) * count2.max(1);
3209 execute_line_op(ed, op, total);
3210 if !ed.vim.replaying {
3211 ed.vim.last_change = Some(LastChange::LineOp {
3212 op,
3213 count: total,
3214 inserted: None,
3215 });
3216 }
3217 return true;
3218 }
3219
3220 if let Key::Char('i') | Key::Char('a') = input.key
3222 && !input.ctrl
3223 {
3224 let inner = matches!(input.key, Key::Char('i'));
3225 ed.vim.pending = Pending::OpTextObj { op, count1, inner };
3226 return true;
3227 }
3228
3229 if input.key == Key::Char('g') && !input.ctrl {
3231 ed.vim.pending = Pending::OpG { op, count1 };
3232 return true;
3233 }
3234
3235 if let Some((forward, till)) = find_entry(&input) {
3237 ed.vim.pending = Pending::OpFind {
3238 op,
3239 count1,
3240 forward,
3241 till,
3242 };
3243 return true;
3244 }
3245
3246 let count2 = take_count(&mut ed.vim);
3248 let total = count1.max(1) * count2.max(1);
3249 if let Some(motion) = parse_motion(&input) {
3250 let motion = match motion {
3251 Motion::FindRepeat { reverse } => match ed.vim.last_find {
3252 Some((ch, forward, till)) => Motion::Find {
3253 ch,
3254 forward: if reverse { !forward } else { forward },
3255 till,
3256 },
3257 None => return true,
3258 },
3259 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
3263 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
3264 m => m,
3265 };
3266 apply_op_with_motion(ed, op, &motion, total);
3267 if let Motion::Find { ch, forward, till } = &motion {
3268 ed.vim.last_find = Some((*ch, *forward, *till));
3269 }
3270 if !ed.vim.replaying && op_is_change(op) {
3271 ed.vim.last_change = Some(LastChange::OpMotion {
3272 op,
3273 motion,
3274 count: total,
3275 inserted: None,
3276 });
3277 }
3278 return true;
3279 }
3280
3281 true
3283}
3284
3285pub(crate) fn apply_op_g_inner<H: crate::types::Host>(
3295 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3296 op: Operator,
3297 ch: char,
3298 total_count: usize,
3299) {
3300 if matches!(
3303 op,
3304 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase
3305 ) {
3306 let op_char = match op {
3307 Operator::Uppercase => 'U',
3308 Operator::Lowercase => 'u',
3309 Operator::ToggleCase => '~',
3310 _ => unreachable!(),
3311 };
3312 if ch == op_char {
3313 execute_line_op(ed, op, total_count);
3314 if !ed.vim.replaying {
3315 ed.vim.last_change = Some(LastChange::LineOp {
3316 op,
3317 count: total_count,
3318 inserted: None,
3319 });
3320 }
3321 return;
3322 }
3323 }
3324 let motion = match ch {
3325 'g' => Motion::FileTop,
3326 'e' => Motion::WordEndBack,
3327 'E' => Motion::BigWordEndBack,
3328 'j' => Motion::ScreenDown,
3329 'k' => Motion::ScreenUp,
3330 _ => return, };
3332 apply_op_with_motion(ed, op, &motion, total_count);
3333 if !ed.vim.replaying && op_is_change(op) {
3334 ed.vim.last_change = Some(LastChange::OpMotion {
3335 op,
3336 motion,
3337 count: total_count,
3338 inserted: None,
3339 });
3340 }
3341}
3342
3343fn handle_op_after_g<H: crate::types::Host>(
3344 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3345 input: Input,
3346 op: Operator,
3347 count1: usize,
3348) -> bool {
3349 if input.ctrl {
3350 return true;
3351 }
3352 let count2 = take_count(&mut ed.vim);
3353 let total = count1.max(1) * count2.max(1);
3354 if let Key::Char(ch) = input.key {
3355 apply_op_g_inner(ed, op, ch, total);
3356 }
3357 true
3358}
3359
3360fn handle_after_g<H: crate::types::Host>(
3361 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3362 input: Input,
3363) -> bool {
3364 let count = take_count(&mut ed.vim);
3365 if let Key::Char(ch) = input.key {
3368 apply_after_g(ed, ch, count);
3369 }
3370 true
3371}
3372
3373pub(crate) fn apply_after_g<H: crate::types::Host>(
3378 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3379 ch: char,
3380 count: usize,
3381) {
3382 match ch {
3383 'g' => {
3384 let pre = ed.cursor();
3386 if count > 1 {
3387 ed.jump_cursor(count - 1, 0);
3388 } else {
3389 ed.jump_cursor(0, 0);
3390 }
3391 move_first_non_whitespace(ed);
3392 if ed.cursor() != pre {
3393 push_jump(ed, pre);
3394 }
3395 }
3396 'e' => execute_motion(ed, Motion::WordEndBack, count),
3397 'E' => execute_motion(ed, Motion::BigWordEndBack, count),
3398 '_' => execute_motion(ed, Motion::LastNonBlank, count),
3400 'M' => execute_motion(ed, Motion::LineMiddle, count),
3402 'v' => {
3404 if let Some(snap) = ed.vim.last_visual {
3405 match snap.mode {
3406 Mode::Visual => {
3407 ed.vim.visual_anchor = snap.anchor;
3408 ed.vim.mode = Mode::Visual;
3409 }
3410 Mode::VisualLine => {
3411 ed.vim.visual_line_anchor = snap.anchor.0;
3412 ed.vim.mode = Mode::VisualLine;
3413 }
3414 Mode::VisualBlock => {
3415 ed.vim.block_anchor = snap.anchor;
3416 ed.vim.block_vcol = snap.block_vcol;
3417 ed.vim.mode = Mode::VisualBlock;
3418 }
3419 _ => {}
3420 }
3421 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
3422 }
3423 }
3424 'j' => execute_motion(ed, Motion::ScreenDown, count),
3428 'k' => execute_motion(ed, Motion::ScreenUp, count),
3429 'U' => {
3433 ed.vim.pending = Pending::Op {
3434 op: Operator::Uppercase,
3435 count1: count,
3436 };
3437 }
3438 'u' => {
3439 ed.vim.pending = Pending::Op {
3440 op: Operator::Lowercase,
3441 count1: count,
3442 };
3443 }
3444 '~' => {
3445 ed.vim.pending = Pending::Op {
3446 op: Operator::ToggleCase,
3447 count1: count,
3448 };
3449 }
3450 'q' => {
3451 ed.vim.pending = Pending::Op {
3454 op: Operator::Reflow,
3455 count1: count,
3456 };
3457 }
3458 'J' => {
3459 for _ in 0..count.max(1) {
3461 ed.push_undo();
3462 join_line_raw(ed);
3463 }
3464 if !ed.vim.replaying {
3465 ed.vim.last_change = Some(LastChange::JoinLine {
3466 count: count.max(1),
3467 });
3468 }
3469 }
3470 'd' => {
3471 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
3476 }
3477 'i' => {
3482 if let Some((row, col)) = ed.vim.last_insert_pos {
3483 ed.jump_cursor(row, col);
3484 }
3485 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3486 }
3487 ';' => walk_change_list(ed, -1, count.max(1)),
3490 ',' => walk_change_list(ed, 1, count.max(1)),
3491 '*' => execute_motion(
3495 ed,
3496 Motion::WordAtCursor {
3497 forward: true,
3498 whole_word: false,
3499 },
3500 count,
3501 ),
3502 '#' => execute_motion(
3503 ed,
3504 Motion::WordAtCursor {
3505 forward: false,
3506 whole_word: false,
3507 },
3508 count,
3509 ),
3510 _ => {}
3511 }
3512}
3513
3514fn handle_after_z<H: crate::types::Host>(
3515 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3516 input: Input,
3517) -> bool {
3518 let count = take_count(&mut ed.vim);
3519 if let Key::Char(ch) = input.key {
3522 apply_after_z(ed, ch, count);
3523 }
3524 true
3525}
3526
3527pub(crate) fn apply_after_z<H: crate::types::Host>(
3532 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3533 ch: char,
3534 count: usize,
3535) {
3536 use crate::editor::CursorScrollTarget;
3537 let row = ed.cursor().0;
3538 match ch {
3539 'z' => {
3540 ed.scroll_cursor_to(CursorScrollTarget::Center);
3541 ed.vim.viewport_pinned = true;
3542 }
3543 't' => {
3544 ed.scroll_cursor_to(CursorScrollTarget::Top);
3545 ed.vim.viewport_pinned = true;
3546 }
3547 'b' => {
3548 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
3549 ed.vim.viewport_pinned = true;
3550 }
3551 'o' => {
3556 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
3557 }
3558 'c' => {
3559 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
3560 }
3561 'a' => {
3562 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
3563 }
3564 'R' => {
3565 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
3566 }
3567 'M' => {
3568 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
3569 }
3570 'E' => {
3571 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
3572 }
3573 'd' => {
3574 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
3575 }
3576 'f' => {
3577 if matches!(
3578 ed.vim.mode,
3579 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
3580 ) {
3581 let anchor_row = match ed.vim.mode {
3584 Mode::VisualLine => ed.vim.visual_line_anchor,
3585 Mode::VisualBlock => ed.vim.block_anchor.0,
3586 _ => ed.vim.visual_anchor.0,
3587 };
3588 let cur = ed.cursor().0;
3589 let top = anchor_row.min(cur);
3590 let bot = anchor_row.max(cur);
3591 ed.apply_fold_op(crate::types::FoldOp::Add {
3592 start_row: top,
3593 end_row: bot,
3594 closed: true,
3595 });
3596 ed.vim.mode = Mode::Normal;
3597 } else {
3598 ed.vim.pending = Pending::Op {
3603 op: Operator::Fold,
3604 count1: count,
3605 };
3606 }
3607 }
3608 _ => {}
3609 }
3610}
3611
3612fn handle_replace<H: crate::types::Host>(
3613 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3614 input: Input,
3615) -> bool {
3616 if let Key::Char(ch) = input.key {
3617 if ed.vim.mode == Mode::VisualBlock {
3618 block_replace(ed, ch);
3619 return true;
3620 }
3621 let count = take_count(&mut ed.vim);
3622 replace_char(ed, ch, count.max(1));
3623 if !ed.vim.replaying {
3624 ed.vim.last_change = Some(LastChange::ReplaceChar {
3625 ch,
3626 count: count.max(1),
3627 });
3628 }
3629 }
3630 true
3631}
3632
3633fn handle_find_target<H: crate::types::Host>(
3634 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3635 input: Input,
3636 forward: bool,
3637 till: bool,
3638) -> bool {
3639 let Key::Char(ch) = input.key else {
3640 return true;
3641 };
3642 let count = take_count(&mut ed.vim);
3643 apply_find_char(ed, ch, forward, till, count.max(1));
3644 true
3645}
3646
3647pub(crate) fn apply_find_char<H: crate::types::Host>(
3653 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3654 ch: char,
3655 forward: bool,
3656 till: bool,
3657 count: usize,
3658) {
3659 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
3660 ed.vim.last_find = Some((ch, forward, till));
3661}
3662
3663pub(crate) fn apply_op_find_motion<H: crate::types::Host>(
3669 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3670 op: Operator,
3671 ch: char,
3672 forward: bool,
3673 till: bool,
3674 total_count: usize,
3675) {
3676 let motion = Motion::Find { ch, forward, till };
3677 apply_op_with_motion(ed, op, &motion, total_count);
3678 ed.vim.last_find = Some((ch, forward, till));
3679 if !ed.vim.replaying && op_is_change(op) {
3680 ed.vim.last_change = Some(LastChange::OpMotion {
3681 op,
3682 motion,
3683 count: total_count,
3684 inserted: None,
3685 });
3686 }
3687}
3688
3689fn handle_op_find_target<H: crate::types::Host>(
3690 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3691 input: Input,
3692 op: Operator,
3693 count1: usize,
3694 forward: bool,
3695 till: bool,
3696) -> bool {
3697 let Key::Char(ch) = input.key else {
3698 return true;
3699 };
3700 let count2 = take_count(&mut ed.vim);
3701 let total = count1.max(1) * count2.max(1);
3702 apply_op_find_motion(ed, op, ch, forward, till, total);
3703 true
3704}
3705
3706pub(crate) fn apply_op_text_obj_inner<H: crate::types::Host>(
3716 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3717 op: Operator,
3718 ch: char,
3719 inner: bool,
3720 _total_count: usize,
3721) -> bool {
3722 let obj = match ch {
3725 'w' => TextObject::Word { big: false },
3726 'W' => TextObject::Word { big: true },
3727 '"' | '\'' | '`' => TextObject::Quote(ch),
3728 '(' | ')' | 'b' => TextObject::Bracket('('),
3729 '[' | ']' => TextObject::Bracket('['),
3730 '{' | '}' | 'B' => TextObject::Bracket('{'),
3731 '<' | '>' => TextObject::Bracket('<'),
3732 'p' => TextObject::Paragraph,
3733 't' => TextObject::XmlTag,
3734 's' => TextObject::Sentence,
3735 _ => return false,
3736 };
3737 apply_op_with_text_object(ed, op, obj, inner);
3738 if !ed.vim.replaying && op_is_change(op) {
3739 ed.vim.last_change = Some(LastChange::OpTextObj {
3740 op,
3741 obj,
3742 inner,
3743 inserted: None,
3744 });
3745 }
3746 true
3747}
3748
3749fn handle_text_object<H: crate::types::Host>(
3750 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3751 input: Input,
3752 op: Operator,
3753 _count1: usize,
3754 inner: bool,
3755) -> bool {
3756 let Key::Char(ch) = input.key else {
3757 return true;
3758 };
3759 apply_op_text_obj_inner(ed, op, ch, inner, 1);
3762 true
3763}
3764
3765fn handle_visual_text_obj<H: crate::types::Host>(
3766 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3767 input: Input,
3768 inner: bool,
3769) -> bool {
3770 let Key::Char(ch) = input.key else {
3771 return true;
3772 };
3773 let obj = match ch {
3774 'w' => TextObject::Word { big: false },
3775 'W' => TextObject::Word { big: true },
3776 '"' | '\'' | '`' => TextObject::Quote(ch),
3777 '(' | ')' | 'b' => TextObject::Bracket('('),
3778 '[' | ']' => TextObject::Bracket('['),
3779 '{' | '}' | 'B' => TextObject::Bracket('{'),
3780 '<' | '>' => TextObject::Bracket('<'),
3781 'p' => TextObject::Paragraph,
3782 't' => TextObject::XmlTag,
3783 's' => TextObject::Sentence,
3784 _ => return true,
3785 };
3786 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3787 return true;
3788 };
3789 match kind {
3793 MotionKind::Linewise => {
3794 ed.vim.visual_line_anchor = start.0;
3795 ed.vim.mode = Mode::VisualLine;
3796 ed.jump_cursor(end.0, 0);
3797 }
3798 _ => {
3799 ed.vim.mode = Mode::Visual;
3800 ed.vim.visual_anchor = (start.0, start.1);
3801 let (er, ec) = retreat_one(ed, end);
3802 ed.jump_cursor(er, ec);
3803 }
3804 }
3805 true
3806}
3807
3808fn retreat_one<H: crate::types::Host>(
3810 ed: &Editor<hjkl_buffer::Buffer, H>,
3811 pos: (usize, usize),
3812) -> (usize, usize) {
3813 let (r, c) = pos;
3814 if c > 0 {
3815 (r, c - 1)
3816 } else if r > 0 {
3817 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
3818 (r - 1, prev_len)
3819 } else {
3820 (0, 0)
3821 }
3822}
3823
3824fn op_is_change(op: Operator) -> bool {
3825 matches!(op, Operator::Delete | Operator::Change)
3826}
3827
3828fn handle_normal_only<H: crate::types::Host>(
3831 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3832 input: &Input,
3833 count: usize,
3834) -> bool {
3835 if input.ctrl {
3836 return false;
3837 }
3838 match input.key {
3839 Key::Char('i') => {
3840 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3841 true
3842 }
3843 Key::Char('I') => {
3844 move_first_non_whitespace(ed);
3845 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
3846 true
3847 }
3848 Key::Char('a') => {
3849 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3850 ed.push_buffer_cursor_to_textarea();
3851 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
3852 true
3853 }
3854 Key::Char('A') => {
3855 crate::motions::move_line_end(&mut ed.buffer);
3856 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3857 ed.push_buffer_cursor_to_textarea();
3858 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
3859 true
3860 }
3861 Key::Char('R') => {
3862 begin_insert(ed, count.max(1), InsertReason::Replace);
3865 true
3866 }
3867 Key::Char('o') => {
3868 use hjkl_buffer::{Edit, Position};
3869 ed.push_undo();
3870 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
3873 ed.sync_buffer_content_from_textarea();
3874 let row = buf_cursor_pos(&ed.buffer).row;
3875 let line_chars = buf_line_chars(&ed.buffer, row);
3876 let prev_line = buf_line(&ed.buffer, row).unwrap_or_default();
3879 let indent = compute_enter_indent(&ed.settings, prev_line);
3880 ed.mutate_edit(Edit::InsertStr {
3881 at: Position::new(row, line_chars),
3882 text: format!("\n{indent}"),
3883 });
3884 ed.push_buffer_cursor_to_textarea();
3885 true
3886 }
3887 Key::Char('O') => {
3888 use hjkl_buffer::{Edit, Position};
3889 ed.push_undo();
3890 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
3891 ed.sync_buffer_content_from_textarea();
3892 let row = buf_cursor_pos(&ed.buffer).row;
3893 let indent = if row > 0 {
3897 let above = buf_line(&ed.buffer, row - 1).unwrap_or_default();
3898 compute_enter_indent(&ed.settings, above)
3899 } else {
3900 let cur = buf_line(&ed.buffer, row).unwrap_or_default();
3901 cur.chars()
3902 .take_while(|c| *c == ' ' || *c == '\t')
3903 .collect::<String>()
3904 };
3905 ed.mutate_edit(Edit::InsertStr {
3906 at: Position::new(row, 0),
3907 text: format!("{indent}\n"),
3908 });
3909 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3914 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
3915 let new_row = buf_cursor_pos(&ed.buffer).row;
3916 buf_set_cursor_rc(&mut ed.buffer, new_row, indent.chars().count());
3917 ed.push_buffer_cursor_to_textarea();
3918 true
3919 }
3920 Key::Char('x') => {
3921 do_char_delete(ed, true, count.max(1));
3922 if !ed.vim.replaying {
3923 ed.vim.last_change = Some(LastChange::CharDel {
3924 forward: true,
3925 count: count.max(1),
3926 });
3927 }
3928 true
3929 }
3930 Key::Char('X') => {
3931 do_char_delete(ed, false, count.max(1));
3932 if !ed.vim.replaying {
3933 ed.vim.last_change = Some(LastChange::CharDel {
3934 forward: false,
3935 count: count.max(1),
3936 });
3937 }
3938 true
3939 }
3940 Key::Char('~') => {
3941 for _ in 0..count.max(1) {
3942 ed.push_undo();
3943 toggle_case_at_cursor(ed);
3944 }
3945 if !ed.vim.replaying {
3946 ed.vim.last_change = Some(LastChange::ToggleCase {
3947 count: count.max(1),
3948 });
3949 }
3950 true
3951 }
3952 Key::Char('J') => {
3953 for _ in 0..count.max(1) {
3954 ed.push_undo();
3955 join_line(ed);
3956 }
3957 if !ed.vim.replaying {
3958 ed.vim.last_change = Some(LastChange::JoinLine {
3959 count: count.max(1),
3960 });
3961 }
3962 true
3963 }
3964 Key::Char('D') => {
3965 ed.push_undo();
3966 delete_to_eol(ed);
3967 crate::motions::move_left(&mut ed.buffer, 1);
3969 ed.push_buffer_cursor_to_textarea();
3970 if !ed.vim.replaying {
3971 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
3972 }
3973 true
3974 }
3975 Key::Char('Y') => {
3976 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
3978 true
3979 }
3980 Key::Char('C') => {
3981 ed.push_undo();
3982 delete_to_eol(ed);
3983 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
3984 true
3985 }
3986 Key::Char('s') => {
3987 use hjkl_buffer::{Edit, MotionKind, Position};
3988 ed.push_undo();
3989 ed.sync_buffer_content_from_textarea();
3990 for _ in 0..count.max(1) {
3991 let cursor = buf_cursor_pos(&ed.buffer);
3992 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
3993 if cursor.col >= line_chars {
3994 break;
3995 }
3996 ed.mutate_edit(Edit::DeleteRange {
3997 start: cursor,
3998 end: Position::new(cursor.row, cursor.col + 1),
3999 kind: MotionKind::Char,
4000 });
4001 }
4002 ed.push_buffer_cursor_to_textarea();
4003 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4004 if !ed.vim.replaying {
4006 ed.vim.last_change = Some(LastChange::OpMotion {
4007 op: Operator::Change,
4008 motion: Motion::Right,
4009 count: count.max(1),
4010 inserted: None,
4011 });
4012 }
4013 true
4014 }
4015 Key::Char('p') => {
4016 do_paste(ed, false, count.max(1));
4017 if !ed.vim.replaying {
4018 ed.vim.last_change = Some(LastChange::Paste {
4019 before: false,
4020 count: count.max(1),
4021 });
4022 }
4023 true
4024 }
4025 Key::Char('P') => {
4026 do_paste(ed, true, count.max(1));
4027 if !ed.vim.replaying {
4028 ed.vim.last_change = Some(LastChange::Paste {
4029 before: true,
4030 count: count.max(1),
4031 });
4032 }
4033 true
4034 }
4035 Key::Char('u') => {
4036 do_undo(ed);
4037 true
4038 }
4039 Key::Char('r') => {
4040 ed.vim.count = count;
4041 ed.vim.pending = Pending::Replace;
4042 true
4043 }
4044 Key::Char('/') => {
4045 enter_search(ed, true);
4046 true
4047 }
4048 Key::Char('?') => {
4049 enter_search(ed, false);
4050 true
4051 }
4052 Key::Char('.') => {
4053 replay_last_change(ed, count);
4054 true
4055 }
4056 _ => false,
4057 }
4058}
4059
4060fn begin_insert_noundo<H: crate::types::Host>(
4062 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4063 count: usize,
4064 reason: InsertReason,
4065) {
4066 let reason = if ed.vim.replaying {
4067 InsertReason::ReplayOnly
4068 } else {
4069 reason
4070 };
4071 let (row, _) = ed.cursor();
4072 ed.vim.insert_session = Some(InsertSession {
4073 count,
4074 row_min: row,
4075 row_max: row,
4076 before_lines: buf_lines_to_vec(&ed.buffer),
4077 reason,
4078 });
4079 ed.vim.mode = Mode::Insert;
4080}
4081
4082fn apply_op_with_motion<H: crate::types::Host>(
4085 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4086 op: Operator,
4087 motion: &Motion,
4088 count: usize,
4089) {
4090 let start = ed.cursor();
4091 apply_motion_cursor_ctx(ed, motion, count, true);
4096 let end = ed.cursor();
4097 let kind = motion_kind(motion);
4098 ed.jump_cursor(start.0, start.1);
4100 run_operator_over_range(ed, op, start, end, kind);
4101}
4102
4103fn apply_op_with_text_object<H: crate::types::Host>(
4104 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4105 op: Operator,
4106 obj: TextObject,
4107 inner: bool,
4108) {
4109 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
4110 return;
4111 };
4112 ed.jump_cursor(start.0, start.1);
4113 run_operator_over_range(ed, op, start, end, kind);
4114}
4115
4116fn motion_kind(motion: &Motion) -> MotionKind {
4117 match motion {
4118 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => MotionKind::Linewise,
4119 Motion::FileTop | Motion::FileBottom => MotionKind::Linewise,
4120 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
4121 MotionKind::Linewise
4122 }
4123 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
4124 MotionKind::Inclusive
4125 }
4126 Motion::Find { .. } => MotionKind::Inclusive,
4127 Motion::MatchBracket => MotionKind::Inclusive,
4128 Motion::LineEnd => MotionKind::Inclusive,
4130 _ => MotionKind::Exclusive,
4131 }
4132}
4133
4134fn run_operator_over_range<H: crate::types::Host>(
4135 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4136 op: Operator,
4137 start: (usize, usize),
4138 end: (usize, usize),
4139 kind: MotionKind,
4140) {
4141 let (top, bot) = order(start, end);
4142 if top == bot && !matches!(kind, MotionKind::Linewise) {
4146 return;
4147 }
4148
4149 match op {
4150 Operator::Yank => {
4151 let text = read_vim_range(ed, top, bot, kind);
4152 if !text.is_empty() {
4153 ed.record_yank_to_host(text.clone());
4154 ed.record_yank(text, matches!(kind, MotionKind::Linewise));
4155 }
4156 let rbr = match kind {
4160 MotionKind::Linewise => {
4161 let last_col = buf_line_chars(&ed.buffer, bot.0).saturating_sub(1);
4162 (bot.0, last_col)
4163 }
4164 MotionKind::Inclusive => (bot.0, bot.1),
4165 MotionKind::Exclusive => (bot.0, bot.1.saturating_sub(1)),
4166 };
4167 ed.set_mark('[', top);
4168 ed.set_mark(']', rbr);
4169 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4170 ed.push_buffer_cursor_to_textarea();
4171 }
4172 Operator::Delete => {
4173 ed.push_undo();
4174 cut_vim_range(ed, top, bot, kind);
4175 if !matches!(kind, MotionKind::Linewise) {
4180 clamp_cursor_to_normal_mode(ed);
4181 }
4182 ed.vim.mode = Mode::Normal;
4183 let pos = ed.cursor();
4187 ed.set_mark('[', pos);
4188 ed.set_mark(']', pos);
4189 }
4190 Operator::Change => {
4191 ed.vim.change_mark_start = Some(top);
4196 ed.push_undo();
4197 cut_vim_range(ed, top, bot, kind);
4198 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4199 }
4200 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4201 apply_case_op_to_selection(ed, op, top, bot, kind);
4202 }
4203 Operator::Indent | Operator::Outdent => {
4204 ed.push_undo();
4207 if op == Operator::Indent {
4208 indent_rows(ed, top.0, bot.0, 1);
4209 } else {
4210 outdent_rows(ed, top.0, bot.0, 1);
4211 }
4212 ed.vim.mode = Mode::Normal;
4213 }
4214 Operator::Fold => {
4215 if bot.0 >= top.0 {
4219 ed.apply_fold_op(crate::types::FoldOp::Add {
4220 start_row: top.0,
4221 end_row: bot.0,
4222 closed: true,
4223 });
4224 }
4225 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4226 ed.push_buffer_cursor_to_textarea();
4227 ed.vim.mode = Mode::Normal;
4228 }
4229 Operator::Reflow => {
4230 ed.push_undo();
4231 reflow_rows(ed, top.0, bot.0);
4232 ed.vim.mode = Mode::Normal;
4233 }
4234 }
4235}
4236
4237pub(crate) fn delete_range_bridge<H: crate::types::Host>(
4254 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4255 start: (usize, usize),
4256 end: (usize, usize),
4257 kind: MotionKind,
4258 register: char,
4259) {
4260 ed.vim.pending_register = Some(register);
4261 run_operator_over_range(ed, Operator::Delete, start, end, kind);
4262}
4263
4264pub(crate) fn yank_range_bridge<H: crate::types::Host>(
4267 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4268 start: (usize, usize),
4269 end: (usize, usize),
4270 kind: MotionKind,
4271 register: char,
4272) {
4273 ed.vim.pending_register = Some(register);
4274 run_operator_over_range(ed, Operator::Yank, start, end, kind);
4275}
4276
4277pub(crate) fn change_range_bridge<H: crate::types::Host>(
4282 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4283 start: (usize, usize),
4284 end: (usize, usize),
4285 kind: MotionKind,
4286 register: char,
4287) {
4288 ed.vim.pending_register = Some(register);
4289 run_operator_over_range(ed, Operator::Change, start, end, kind);
4290}
4291
4292pub(crate) fn indent_range_bridge<H: crate::types::Host>(
4297 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4298 start: (usize, usize),
4299 end: (usize, usize),
4300 count: i32,
4301 shiftwidth: u32,
4302) {
4303 if count == 0 {
4304 return;
4305 }
4306 let (top_row, bot_row) = if start.0 <= end.0 {
4307 (start.0, end.0)
4308 } else {
4309 (end.0, start.0)
4310 };
4311 let original_sw = ed.settings().shiftwidth;
4313 if shiftwidth > 0 {
4314 ed.settings_mut().shiftwidth = shiftwidth as usize;
4315 }
4316 ed.push_undo();
4317 let abs_count = count.unsigned_abs() as usize;
4318 if count > 0 {
4319 indent_rows(ed, top_row, bot_row, abs_count);
4320 } else {
4321 outdent_rows(ed, top_row, bot_row, abs_count);
4322 }
4323 if shiftwidth > 0 {
4324 ed.settings_mut().shiftwidth = original_sw;
4325 }
4326 ed.vim.mode = Mode::Normal;
4327}
4328
4329pub(crate) fn case_range_bridge<H: crate::types::Host>(
4333 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4334 start: (usize, usize),
4335 end: (usize, usize),
4336 kind: MotionKind,
4337 op: Operator,
4338) {
4339 match op {
4340 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {}
4341 _ => return,
4342 }
4343 let (top, bot) = order(start, end);
4344 apply_case_op_to_selection(ed, op, top, bot, kind);
4345}
4346
4347pub(crate) fn delete_block_bridge<H: crate::types::Host>(
4368 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4369 top_row: usize,
4370 bot_row: usize,
4371 left_col: usize,
4372 right_col: usize,
4373 register: char,
4374) {
4375 ed.vim.pending_register = Some(register);
4376 let saved_anchor = ed.vim.block_anchor;
4377 let saved_vcol = ed.vim.block_vcol;
4378 ed.vim.block_anchor = (top_row, left_col);
4379 ed.vim.block_vcol = right_col;
4380 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
4382 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
4384 apply_block_operator(ed, Operator::Delete);
4385 ed.vim.block_anchor = saved_anchor;
4389 ed.vim.block_vcol = saved_vcol;
4390}
4391
4392pub(crate) fn yank_block_bridge<H: crate::types::Host>(
4394 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4395 top_row: usize,
4396 bot_row: usize,
4397 left_col: usize,
4398 right_col: usize,
4399 register: char,
4400) {
4401 ed.vim.pending_register = Some(register);
4402 let saved_anchor = ed.vim.block_anchor;
4403 let saved_vcol = ed.vim.block_vcol;
4404 ed.vim.block_anchor = (top_row, left_col);
4405 ed.vim.block_vcol = right_col;
4406 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
4407 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
4408 apply_block_operator(ed, Operator::Yank);
4409 ed.vim.block_anchor = saved_anchor;
4410 ed.vim.block_vcol = saved_vcol;
4411}
4412
4413pub(crate) fn change_block_bridge<H: crate::types::Host>(
4416 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4417 top_row: usize,
4418 bot_row: usize,
4419 left_col: usize,
4420 right_col: usize,
4421 register: char,
4422) {
4423 ed.vim.pending_register = Some(register);
4424 let saved_anchor = ed.vim.block_anchor;
4425 let saved_vcol = ed.vim.block_vcol;
4426 ed.vim.block_anchor = (top_row, left_col);
4427 ed.vim.block_vcol = right_col;
4428 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
4429 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
4430 apply_block_operator(ed, Operator::Change);
4431 ed.vim.block_anchor = saved_anchor;
4432 ed.vim.block_vcol = saved_vcol;
4433}
4434
4435pub(crate) fn indent_block_bridge<H: crate::types::Host>(
4439 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4440 top_row: usize,
4441 bot_row: usize,
4442 count: i32,
4443) {
4444 if count == 0 {
4445 return;
4446 }
4447 ed.push_undo();
4448 let abs = count.unsigned_abs() as usize;
4449 if count > 0 {
4450 indent_rows(ed, top_row, bot_row, abs);
4451 } else {
4452 outdent_rows(ed, top_row, bot_row, abs);
4453 }
4454 ed.vim.mode = Mode::Normal;
4455}
4456
4457pub(crate) fn text_object_inner_word_bridge<H: crate::types::Host>(
4468 ed: &Editor<hjkl_buffer::Buffer, H>,
4469) -> Option<((usize, usize), (usize, usize))> {
4470 word_text_object(ed, true, false)
4471}
4472
4473pub(crate) fn text_object_around_word_bridge<H: crate::types::Host>(
4476 ed: &Editor<hjkl_buffer::Buffer, H>,
4477) -> Option<((usize, usize), (usize, usize))> {
4478 word_text_object(ed, false, false)
4479}
4480
4481pub(crate) fn text_object_inner_big_word_bridge<H: crate::types::Host>(
4484 ed: &Editor<hjkl_buffer::Buffer, H>,
4485) -> Option<((usize, usize), (usize, usize))> {
4486 word_text_object(ed, true, true)
4487}
4488
4489pub(crate) fn text_object_around_big_word_bridge<H: crate::types::Host>(
4492 ed: &Editor<hjkl_buffer::Buffer, H>,
4493) -> Option<((usize, usize), (usize, usize))> {
4494 word_text_object(ed, false, true)
4495}
4496
4497pub(crate) fn text_object_inner_quote_bridge<H: crate::types::Host>(
4513 ed: &Editor<hjkl_buffer::Buffer, H>,
4514 quote: char,
4515) -> Option<((usize, usize), (usize, usize))> {
4516 quote_text_object(ed, quote, true)
4517}
4518
4519pub(crate) fn text_object_around_quote_bridge<H: crate::types::Host>(
4522 ed: &Editor<hjkl_buffer::Buffer, H>,
4523 quote: char,
4524) -> Option<((usize, usize), (usize, usize))> {
4525 quote_text_object(ed, quote, false)
4526}
4527
4528pub(crate) fn text_object_inner_bracket_bridge<H: crate::types::Host>(
4536 ed: &Editor<hjkl_buffer::Buffer, H>,
4537 open: char,
4538) -> Option<((usize, usize), (usize, usize))> {
4539 bracket_text_object(ed, open, true).map(|(s, e, _kind)| (s, e))
4540}
4541
4542pub(crate) fn text_object_around_bracket_bridge<H: crate::types::Host>(
4546 ed: &Editor<hjkl_buffer::Buffer, H>,
4547 open: char,
4548) -> Option<((usize, usize), (usize, usize))> {
4549 bracket_text_object(ed, open, false).map(|(s, e, _kind)| (s, e))
4550}
4551
4552pub(crate) fn text_object_inner_sentence_bridge<H: crate::types::Host>(
4557 ed: &Editor<hjkl_buffer::Buffer, H>,
4558) -> Option<((usize, usize), (usize, usize))> {
4559 sentence_text_object(ed, true)
4560}
4561
4562pub(crate) fn text_object_around_sentence_bridge<H: crate::types::Host>(
4565 ed: &Editor<hjkl_buffer::Buffer, H>,
4566) -> Option<((usize, usize), (usize, usize))> {
4567 sentence_text_object(ed, false)
4568}
4569
4570pub(crate) fn text_object_inner_paragraph_bridge<H: crate::types::Host>(
4575 ed: &Editor<hjkl_buffer::Buffer, H>,
4576) -> Option<((usize, usize), (usize, usize))> {
4577 paragraph_text_object(ed, true)
4578}
4579
4580pub(crate) fn text_object_around_paragraph_bridge<H: crate::types::Host>(
4583 ed: &Editor<hjkl_buffer::Buffer, H>,
4584) -> Option<((usize, usize), (usize, usize))> {
4585 paragraph_text_object(ed, false)
4586}
4587
4588pub(crate) fn text_object_inner_tag_bridge<H: crate::types::Host>(
4594 ed: &Editor<hjkl_buffer::Buffer, H>,
4595) -> Option<((usize, usize), (usize, usize))> {
4596 tag_text_object(ed, true)
4597}
4598
4599pub(crate) fn text_object_around_tag_bridge<H: crate::types::Host>(
4602 ed: &Editor<hjkl_buffer::Buffer, H>,
4603) -> Option<((usize, usize), (usize, usize))> {
4604 tag_text_object(ed, false)
4605}
4606
4607fn reflow_rows<H: crate::types::Host>(
4612 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4613 top: usize,
4614 bot: usize,
4615) {
4616 let width = ed.settings().textwidth.max(1);
4617 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4618 let bot = bot.min(lines.len().saturating_sub(1));
4619 if top > bot {
4620 return;
4621 }
4622 let original = lines[top..=bot].to_vec();
4623 let mut wrapped: Vec<String> = Vec::new();
4624 let mut paragraph: Vec<String> = Vec::new();
4625 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
4626 if para.is_empty() {
4627 return;
4628 }
4629 let words = para.join(" ");
4630 let mut current = String::new();
4631 for word in words.split_whitespace() {
4632 let extra = if current.is_empty() {
4633 word.chars().count()
4634 } else {
4635 current.chars().count() + 1 + word.chars().count()
4636 };
4637 if extra > width && !current.is_empty() {
4638 out.push(std::mem::take(&mut current));
4639 current.push_str(word);
4640 } else if current.is_empty() {
4641 current.push_str(word);
4642 } else {
4643 current.push(' ');
4644 current.push_str(word);
4645 }
4646 }
4647 if !current.is_empty() {
4648 out.push(current);
4649 }
4650 para.clear();
4651 };
4652 for line in &original {
4653 if line.trim().is_empty() {
4654 flush(&mut paragraph, &mut wrapped, width);
4655 wrapped.push(String::new());
4656 } else {
4657 paragraph.push(line.clone());
4658 }
4659 }
4660 flush(&mut paragraph, &mut wrapped, width);
4661
4662 let after: Vec<String> = lines.split_off(bot + 1);
4664 lines.truncate(top);
4665 lines.extend(wrapped);
4666 lines.extend(after);
4667 ed.restore(lines, (top, 0));
4668 ed.mark_content_dirty();
4669}
4670
4671fn apply_case_op_to_selection<H: crate::types::Host>(
4677 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4678 op: Operator,
4679 top: (usize, usize),
4680 bot: (usize, usize),
4681 kind: MotionKind,
4682) {
4683 use hjkl_buffer::Edit;
4684 ed.push_undo();
4685 let saved_yank = ed.yank().to_string();
4686 let saved_yank_linewise = ed.vim.yank_linewise;
4687 let selection = cut_vim_range(ed, top, bot, kind);
4688 let transformed = match op {
4689 Operator::Uppercase => selection.to_uppercase(),
4690 Operator::Lowercase => selection.to_lowercase(),
4691 Operator::ToggleCase => toggle_case_str(&selection),
4692 _ => unreachable!(),
4693 };
4694 if !transformed.is_empty() {
4695 let cursor = buf_cursor_pos(&ed.buffer);
4696 ed.mutate_edit(Edit::InsertStr {
4697 at: cursor,
4698 text: transformed,
4699 });
4700 }
4701 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4702 ed.push_buffer_cursor_to_textarea();
4703 ed.set_yank(saved_yank);
4704 ed.vim.yank_linewise = saved_yank_linewise;
4705 ed.vim.mode = Mode::Normal;
4706}
4707
4708fn indent_rows<H: crate::types::Host>(
4713 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4714 top: usize,
4715 bot: usize,
4716 count: usize,
4717) {
4718 ed.sync_buffer_content_from_textarea();
4719 let width = ed.settings().shiftwidth * count.max(1);
4720 let pad: String = " ".repeat(width);
4721 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4722 let bot = bot.min(lines.len().saturating_sub(1));
4723 for line in lines.iter_mut().take(bot + 1).skip(top) {
4724 if !line.is_empty() {
4725 line.insert_str(0, &pad);
4726 }
4727 }
4728 ed.restore(lines, (top, 0));
4731 move_first_non_whitespace(ed);
4732}
4733
4734fn outdent_rows<H: crate::types::Host>(
4738 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4739 top: usize,
4740 bot: usize,
4741 count: usize,
4742) {
4743 ed.sync_buffer_content_from_textarea();
4744 let width = ed.settings().shiftwidth * count.max(1);
4745 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4746 let bot = bot.min(lines.len().saturating_sub(1));
4747 for line in lines.iter_mut().take(bot + 1).skip(top) {
4748 let strip: usize = line
4749 .chars()
4750 .take(width)
4751 .take_while(|c| *c == ' ' || *c == '\t')
4752 .count();
4753 if strip > 0 {
4754 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
4755 line.drain(..byte_len);
4756 }
4757 }
4758 ed.restore(lines, (top, 0));
4759 move_first_non_whitespace(ed);
4760}
4761
4762fn toggle_case_str(s: &str) -> String {
4763 s.chars()
4764 .map(|c| {
4765 if c.is_lowercase() {
4766 c.to_uppercase().next().unwrap_or(c)
4767 } else if c.is_uppercase() {
4768 c.to_lowercase().next().unwrap_or(c)
4769 } else {
4770 c
4771 }
4772 })
4773 .collect()
4774}
4775
4776fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
4777 if a <= b { (a, b) } else { (b, a) }
4778}
4779
4780fn clamp_cursor_to_normal_mode<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
4785 let (row, col) = ed.cursor();
4786 let line_chars = buf_line_chars(&ed.buffer, row);
4787 let max_col = line_chars.saturating_sub(1);
4788 if col > max_col {
4789 buf_set_cursor_rc(&mut ed.buffer, row, max_col);
4790 ed.push_buffer_cursor_to_textarea();
4791 }
4792}
4793
4794fn execute_line_op<H: crate::types::Host>(
4797 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4798 op: Operator,
4799 count: usize,
4800) {
4801 let (row, col) = ed.cursor();
4802 let total = buf_row_count(&ed.buffer);
4803 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
4804
4805 match op {
4806 Operator::Yank => {
4807 let text = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4809 if !text.is_empty() {
4810 ed.record_yank_to_host(text.clone());
4811 ed.record_yank(text, true);
4812 }
4813 let last_col = buf_line_chars(&ed.buffer, end_row).saturating_sub(1);
4816 ed.set_mark('[', (row, 0));
4817 ed.set_mark(']', (end_row, last_col));
4818 buf_set_cursor_rc(&mut ed.buffer, row, col);
4819 ed.push_buffer_cursor_to_textarea();
4820 ed.vim.mode = Mode::Normal;
4821 }
4822 Operator::Delete => {
4823 ed.push_undo();
4824 let deleted_through_last = end_row + 1 >= total;
4825 cut_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4826 let total_after = buf_row_count(&ed.buffer);
4830 let raw_target = if deleted_through_last {
4831 row.saturating_sub(1).min(total_after.saturating_sub(1))
4832 } else {
4833 row.min(total_after.saturating_sub(1))
4834 };
4835 let target_row = if raw_target > 0
4841 && raw_target + 1 == total_after
4842 && buf_line(&ed.buffer, raw_target)
4843 .map(str::is_empty)
4844 .unwrap_or(false)
4845 {
4846 raw_target - 1
4847 } else {
4848 raw_target
4849 };
4850 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
4851 ed.push_buffer_cursor_to_textarea();
4852 move_first_non_whitespace(ed);
4853 ed.sticky_col = Some(ed.cursor().1);
4854 ed.vim.mode = Mode::Normal;
4855 let pos = ed.cursor();
4858 ed.set_mark('[', pos);
4859 ed.set_mark(']', pos);
4860 }
4861 Operator::Change => {
4862 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4866 ed.vim.change_mark_start = Some((row, 0));
4868 ed.push_undo();
4869 ed.sync_buffer_content_from_textarea();
4870 let payload = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4872 if end_row > row {
4873 ed.mutate_edit(Edit::DeleteRange {
4874 start: Position::new(row + 1, 0),
4875 end: Position::new(end_row, 0),
4876 kind: BufKind::Line,
4877 });
4878 }
4879 let line_chars = buf_line_chars(&ed.buffer, row);
4880 if line_chars > 0 {
4881 ed.mutate_edit(Edit::DeleteRange {
4882 start: Position::new(row, 0),
4883 end: Position::new(row, line_chars),
4884 kind: BufKind::Char,
4885 });
4886 }
4887 if !payload.is_empty() {
4888 ed.record_yank_to_host(payload.clone());
4889 ed.record_delete(payload, true);
4890 }
4891 buf_set_cursor_rc(&mut ed.buffer, row, 0);
4892 ed.push_buffer_cursor_to_textarea();
4893 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4894 }
4895 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4896 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), MotionKind::Linewise);
4900 move_first_non_whitespace(ed);
4903 }
4904 Operator::Indent | Operator::Outdent => {
4905 ed.push_undo();
4907 if op == Operator::Indent {
4908 indent_rows(ed, row, end_row, 1);
4909 } else {
4910 outdent_rows(ed, row, end_row, 1);
4911 }
4912 ed.sticky_col = Some(ed.cursor().1);
4913 ed.vim.mode = Mode::Normal;
4914 }
4915 Operator::Fold => unreachable!("Fold has no line-op double"),
4917 Operator::Reflow => {
4918 ed.push_undo();
4920 reflow_rows(ed, row, end_row);
4921 move_first_non_whitespace(ed);
4922 ed.sticky_col = Some(ed.cursor().1);
4923 ed.vim.mode = Mode::Normal;
4924 }
4925 }
4926}
4927
4928fn apply_visual_operator<H: crate::types::Host>(
4931 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4932 op: Operator,
4933) {
4934 match ed.vim.mode {
4935 Mode::VisualLine => {
4936 let cursor_row = buf_cursor_pos(&ed.buffer).row;
4937 let top = cursor_row.min(ed.vim.visual_line_anchor);
4938 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4939 ed.vim.yank_linewise = true;
4940 match op {
4941 Operator::Yank => {
4942 let text = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4943 if !text.is_empty() {
4944 ed.record_yank_to_host(text.clone());
4945 ed.record_yank(text, true);
4946 }
4947 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4948 ed.push_buffer_cursor_to_textarea();
4949 ed.vim.mode = Mode::Normal;
4950 }
4951 Operator::Delete => {
4952 ed.push_undo();
4953 cut_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4954 ed.vim.mode = Mode::Normal;
4955 }
4956 Operator::Change => {
4957 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4960 ed.push_undo();
4961 ed.sync_buffer_content_from_textarea();
4962 let payload = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4963 if bot > top {
4964 ed.mutate_edit(Edit::DeleteRange {
4965 start: Position::new(top + 1, 0),
4966 end: Position::new(bot, 0),
4967 kind: BufKind::Line,
4968 });
4969 }
4970 let line_chars = buf_line_chars(&ed.buffer, top);
4971 if line_chars > 0 {
4972 ed.mutate_edit(Edit::DeleteRange {
4973 start: Position::new(top, 0),
4974 end: Position::new(top, line_chars),
4975 kind: BufKind::Char,
4976 });
4977 }
4978 if !payload.is_empty() {
4979 ed.record_yank_to_host(payload.clone());
4980 ed.record_delete(payload, true);
4981 }
4982 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4983 ed.push_buffer_cursor_to_textarea();
4984 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4985 }
4986 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4987 let bot = buf_cursor_pos(&ed.buffer)
4988 .row
4989 .max(ed.vim.visual_line_anchor);
4990 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), MotionKind::Linewise);
4991 move_first_non_whitespace(ed);
4992 }
4993 Operator::Indent | Operator::Outdent => {
4994 ed.push_undo();
4995 let (cursor_row, _) = ed.cursor();
4996 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4997 if op == Operator::Indent {
4998 indent_rows(ed, top, bot, 1);
4999 } else {
5000 outdent_rows(ed, top, bot, 1);
5001 }
5002 ed.vim.mode = Mode::Normal;
5003 }
5004 Operator::Reflow => {
5005 ed.push_undo();
5006 let (cursor_row, _) = ed.cursor();
5007 let bot = cursor_row.max(ed.vim.visual_line_anchor);
5008 reflow_rows(ed, top, bot);
5009 ed.vim.mode = Mode::Normal;
5010 }
5011 Operator::Fold => unreachable!("Visual zf takes its own path"),
5014 }
5015 }
5016 Mode::Visual => {
5017 ed.vim.yank_linewise = false;
5018 let anchor = ed.vim.visual_anchor;
5019 let cursor = ed.cursor();
5020 let (top, bot) = order(anchor, cursor);
5021 match op {
5022 Operator::Yank => {
5023 let text = read_vim_range(ed, top, bot, MotionKind::Inclusive);
5024 if !text.is_empty() {
5025 ed.record_yank_to_host(text.clone());
5026 ed.record_yank(text, false);
5027 }
5028 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
5029 ed.push_buffer_cursor_to_textarea();
5030 ed.vim.mode = Mode::Normal;
5031 }
5032 Operator::Delete => {
5033 ed.push_undo();
5034 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
5035 ed.vim.mode = Mode::Normal;
5036 }
5037 Operator::Change => {
5038 ed.push_undo();
5039 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
5040 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
5041 }
5042 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
5043 let anchor = ed.vim.visual_anchor;
5045 let cursor = ed.cursor();
5046 let (top, bot) = order(anchor, cursor);
5047 apply_case_op_to_selection(ed, op, top, bot, MotionKind::Inclusive);
5048 }
5049 Operator::Indent | Operator::Outdent => {
5050 ed.push_undo();
5051 let anchor = ed.vim.visual_anchor;
5052 let cursor = ed.cursor();
5053 let (top, bot) = order(anchor, cursor);
5054 if op == Operator::Indent {
5055 indent_rows(ed, top.0, bot.0, 1);
5056 } else {
5057 outdent_rows(ed, top.0, bot.0, 1);
5058 }
5059 ed.vim.mode = Mode::Normal;
5060 }
5061 Operator::Reflow => {
5062 ed.push_undo();
5063 let anchor = ed.vim.visual_anchor;
5064 let cursor = ed.cursor();
5065 let (top, bot) = order(anchor, cursor);
5066 reflow_rows(ed, top.0, bot.0);
5067 ed.vim.mode = Mode::Normal;
5068 }
5069 Operator::Fold => unreachable!("Visual zf takes its own path"),
5070 }
5071 }
5072 Mode::VisualBlock => apply_block_operator(ed, op),
5073 _ => {}
5074 }
5075}
5076
5077fn block_bounds<H: crate::types::Host>(
5082 ed: &Editor<hjkl_buffer::Buffer, H>,
5083) -> (usize, usize, usize, usize) {
5084 let (ar, ac) = ed.vim.block_anchor;
5085 let (cr, _) = ed.cursor();
5086 let cc = ed.vim.block_vcol;
5087 let top = ar.min(cr);
5088 let bot = ar.max(cr);
5089 let left = ac.min(cc);
5090 let right = ac.max(cc);
5091 (top, bot, left, right)
5092}
5093
5094fn update_block_vcol<H: crate::types::Host>(
5099 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5100 motion: &Motion,
5101) {
5102 match motion {
5103 Motion::Left
5104 | Motion::Right
5105 | Motion::WordFwd
5106 | Motion::BigWordFwd
5107 | Motion::WordBack
5108 | Motion::BigWordBack
5109 | Motion::WordEnd
5110 | Motion::BigWordEnd
5111 | Motion::WordEndBack
5112 | Motion::BigWordEndBack
5113 | Motion::LineStart
5114 | Motion::FirstNonBlank
5115 | Motion::LineEnd
5116 | Motion::Find { .. }
5117 | Motion::FindRepeat { .. }
5118 | Motion::MatchBracket => {
5119 ed.vim.block_vcol = ed.cursor().1;
5120 }
5121 _ => {}
5123 }
5124}
5125
5126fn apply_block_operator<H: crate::types::Host>(
5131 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5132 op: Operator,
5133) {
5134 let (top, bot, left, right) = block_bounds(ed);
5135 let yank = block_yank(ed, top, bot, left, right);
5137
5138 match op {
5139 Operator::Yank => {
5140 if !yank.is_empty() {
5141 ed.record_yank_to_host(yank.clone());
5142 ed.record_yank(yank, false);
5143 }
5144 ed.vim.mode = Mode::Normal;
5145 ed.jump_cursor(top, left);
5146 }
5147 Operator::Delete => {
5148 ed.push_undo();
5149 delete_block_contents(ed, top, bot, left, right);
5150 if !yank.is_empty() {
5151 ed.record_yank_to_host(yank.clone());
5152 ed.record_delete(yank, false);
5153 }
5154 ed.vim.mode = Mode::Normal;
5155 ed.jump_cursor(top, left);
5156 }
5157 Operator::Change => {
5158 ed.push_undo();
5159 delete_block_contents(ed, top, bot, left, right);
5160 if !yank.is_empty() {
5161 ed.record_yank_to_host(yank.clone());
5162 ed.record_delete(yank, false);
5163 }
5164 ed.jump_cursor(top, left);
5165 begin_insert_noundo(
5166 ed,
5167 1,
5168 InsertReason::BlockChange {
5169 top,
5170 bot,
5171 col: left,
5172 },
5173 );
5174 }
5175 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
5176 ed.push_undo();
5177 transform_block_case(ed, op, top, bot, left, right);
5178 ed.vim.mode = Mode::Normal;
5179 ed.jump_cursor(top, left);
5180 }
5181 Operator::Indent | Operator::Outdent => {
5182 ed.push_undo();
5186 if op == Operator::Indent {
5187 indent_rows(ed, top, bot, 1);
5188 } else {
5189 outdent_rows(ed, top, bot, 1);
5190 }
5191 ed.vim.mode = Mode::Normal;
5192 }
5193 Operator::Fold => unreachable!("Visual zf takes its own path"),
5194 Operator::Reflow => {
5195 ed.push_undo();
5199 reflow_rows(ed, top, bot);
5200 ed.vim.mode = Mode::Normal;
5201 }
5202 }
5203}
5204
5205fn transform_block_case<H: crate::types::Host>(
5209 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5210 op: Operator,
5211 top: usize,
5212 bot: usize,
5213 left: usize,
5214 right: usize,
5215) {
5216 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
5217 for r in top..=bot.min(lines.len().saturating_sub(1)) {
5218 let chars: Vec<char> = lines[r].chars().collect();
5219 if left >= chars.len() {
5220 continue;
5221 }
5222 let end = (right + 1).min(chars.len());
5223 let head: String = chars[..left].iter().collect();
5224 let mid: String = chars[left..end].iter().collect();
5225 let tail: String = chars[end..].iter().collect();
5226 let transformed = match op {
5227 Operator::Uppercase => mid.to_uppercase(),
5228 Operator::Lowercase => mid.to_lowercase(),
5229 Operator::ToggleCase => toggle_case_str(&mid),
5230 _ => mid,
5231 };
5232 lines[r] = format!("{head}{transformed}{tail}");
5233 }
5234 let saved_yank = ed.yank().to_string();
5235 let saved_linewise = ed.vim.yank_linewise;
5236 ed.restore(lines, (top, left));
5237 ed.set_yank(saved_yank);
5238 ed.vim.yank_linewise = saved_linewise;
5239}
5240
5241fn block_yank<H: crate::types::Host>(
5242 ed: &Editor<hjkl_buffer::Buffer, H>,
5243 top: usize,
5244 bot: usize,
5245 left: usize,
5246 right: usize,
5247) -> String {
5248 let lines = buf_lines_to_vec(&ed.buffer);
5249 let mut rows: Vec<String> = Vec::new();
5250 for r in top..=bot {
5251 let line = match lines.get(r) {
5252 Some(l) => l,
5253 None => break,
5254 };
5255 let chars: Vec<char> = line.chars().collect();
5256 let end = (right + 1).min(chars.len());
5257 if left >= chars.len() {
5258 rows.push(String::new());
5259 } else {
5260 rows.push(chars[left..end].iter().collect());
5261 }
5262 }
5263 rows.join("\n")
5264}
5265
5266fn delete_block_contents<H: crate::types::Host>(
5267 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5268 top: usize,
5269 bot: usize,
5270 left: usize,
5271 right: usize,
5272) {
5273 use hjkl_buffer::{Edit, MotionKind, Position};
5274 ed.sync_buffer_content_from_textarea();
5275 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
5276 if last_row < top {
5277 return;
5278 }
5279 ed.mutate_edit(Edit::DeleteRange {
5280 start: Position::new(top, left),
5281 end: Position::new(last_row, right),
5282 kind: MotionKind::Block,
5283 });
5284 ed.push_buffer_cursor_to_textarea();
5285}
5286
5287fn block_replace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, ch: char) {
5289 let (top, bot, left, right) = block_bounds(ed);
5290 ed.push_undo();
5291 ed.sync_buffer_content_from_textarea();
5292 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
5293 for r in top..=bot.min(lines.len().saturating_sub(1)) {
5294 let chars: Vec<char> = lines[r].chars().collect();
5295 if left >= chars.len() {
5296 continue;
5297 }
5298 let end = (right + 1).min(chars.len());
5299 let before: String = chars[..left].iter().collect();
5300 let middle: String = std::iter::repeat_n(ch, end - left).collect();
5301 let after: String = chars[end..].iter().collect();
5302 lines[r] = format!("{before}{middle}{after}");
5303 }
5304 reset_textarea_lines(ed, lines);
5305 ed.vim.mode = Mode::Normal;
5306 ed.jump_cursor(top, left);
5307}
5308
5309fn reset_textarea_lines<H: crate::types::Host>(
5313 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5314 lines: Vec<String>,
5315) {
5316 let cursor = ed.cursor();
5317 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
5318 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
5319 ed.mark_content_dirty();
5320}
5321
5322type Pos = (usize, usize);
5328
5329fn text_object_range<H: crate::types::Host>(
5333 ed: &Editor<hjkl_buffer::Buffer, H>,
5334 obj: TextObject,
5335 inner: bool,
5336) -> Option<(Pos, Pos, MotionKind)> {
5337 match obj {
5338 TextObject::Word { big } => {
5339 word_text_object(ed, inner, big).map(|(s, e)| (s, e, MotionKind::Exclusive))
5340 }
5341 TextObject::Quote(q) => {
5342 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
5343 }
5344 TextObject::Bracket(open) => bracket_text_object(ed, open, inner),
5345 TextObject::Paragraph => {
5346 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Linewise))
5347 }
5348 TextObject::XmlTag => {
5349 tag_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
5350 }
5351 TextObject::Sentence => {
5352 sentence_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
5353 }
5354 }
5355}
5356
5357fn sentence_boundary<H: crate::types::Host>(
5361 ed: &Editor<hjkl_buffer::Buffer, H>,
5362 forward: bool,
5363) -> Option<(usize, usize)> {
5364 let lines = buf_lines_to_vec(&ed.buffer);
5365 if lines.is_empty() {
5366 return None;
5367 }
5368 let pos_to_idx = |pos: (usize, usize)| -> usize {
5369 let mut idx = 0;
5370 for line in lines.iter().take(pos.0) {
5371 idx += line.chars().count() + 1;
5372 }
5373 idx + pos.1
5374 };
5375 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
5376 for (r, line) in lines.iter().enumerate() {
5377 let len = line.chars().count();
5378 if idx <= len {
5379 return (r, idx);
5380 }
5381 idx -= len + 1;
5382 }
5383 let last = lines.len().saturating_sub(1);
5384 (last, lines[last].chars().count())
5385 };
5386 let mut chars: Vec<char> = Vec::new();
5387 for (r, line) in lines.iter().enumerate() {
5388 chars.extend(line.chars());
5389 if r + 1 < lines.len() {
5390 chars.push('\n');
5391 }
5392 }
5393 if chars.is_empty() {
5394 return None;
5395 }
5396 let total = chars.len();
5397 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
5398 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
5399
5400 if forward {
5401 let mut i = cursor_idx + 1;
5404 while i < total {
5405 if is_terminator(chars[i]) {
5406 while i + 1 < total && is_terminator(chars[i + 1]) {
5407 i += 1;
5408 }
5409 if i + 1 >= total {
5410 return None;
5411 }
5412 if chars[i + 1].is_whitespace() {
5413 let mut j = i + 1;
5414 while j < total && chars[j].is_whitespace() {
5415 j += 1;
5416 }
5417 if j >= total {
5418 return None;
5419 }
5420 return Some(idx_to_pos(j));
5421 }
5422 }
5423 i += 1;
5424 }
5425 None
5426 } else {
5427 let find_start = |from: usize| -> Option<usize> {
5431 let mut start = from;
5432 while start > 0 {
5433 let prev = chars[start - 1];
5434 if prev.is_whitespace() {
5435 let mut k = start - 1;
5436 while k > 0 && chars[k - 1].is_whitespace() {
5437 k -= 1;
5438 }
5439 if k > 0 && is_terminator(chars[k - 1]) {
5440 break;
5441 }
5442 }
5443 start -= 1;
5444 }
5445 while start < total && chars[start].is_whitespace() {
5446 start += 1;
5447 }
5448 (start < total).then_some(start)
5449 };
5450 let current_start = find_start(cursor_idx)?;
5451 if current_start < cursor_idx {
5452 return Some(idx_to_pos(current_start));
5453 }
5454 let mut k = current_start;
5457 while k > 0 && chars[k - 1].is_whitespace() {
5458 k -= 1;
5459 }
5460 if k == 0 {
5461 return None;
5462 }
5463 let prev_start = find_start(k - 1)?;
5464 Some(idx_to_pos(prev_start))
5465 }
5466}
5467
5468fn sentence_text_object<H: crate::types::Host>(
5474 ed: &Editor<hjkl_buffer::Buffer, H>,
5475 inner: bool,
5476) -> Option<((usize, usize), (usize, usize))> {
5477 let lines = buf_lines_to_vec(&ed.buffer);
5478 if lines.is_empty() {
5479 return None;
5480 }
5481 let pos_to_idx = |pos: (usize, usize)| -> usize {
5484 let mut idx = 0;
5485 for line in lines.iter().take(pos.0) {
5486 idx += line.chars().count() + 1;
5487 }
5488 idx + pos.1
5489 };
5490 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
5491 for (r, line) in lines.iter().enumerate() {
5492 let len = line.chars().count();
5493 if idx <= len {
5494 return (r, idx);
5495 }
5496 idx -= len + 1;
5497 }
5498 let last = lines.len().saturating_sub(1);
5499 (last, lines[last].chars().count())
5500 };
5501 let mut chars: Vec<char> = Vec::new();
5502 for (r, line) in lines.iter().enumerate() {
5503 chars.extend(line.chars());
5504 if r + 1 < lines.len() {
5505 chars.push('\n');
5506 }
5507 }
5508 if chars.is_empty() {
5509 return None;
5510 }
5511
5512 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
5513 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
5514
5515 let mut start = cursor_idx;
5519 while start > 0 {
5520 let prev = chars[start - 1];
5521 if prev.is_whitespace() {
5522 let mut k = start - 1;
5526 while k > 0 && chars[k - 1].is_whitespace() {
5527 k -= 1;
5528 }
5529 if k > 0 && is_terminator(chars[k - 1]) {
5530 break;
5531 }
5532 }
5533 start -= 1;
5534 }
5535 while start < chars.len() && chars[start].is_whitespace() {
5538 start += 1;
5539 }
5540 if start >= chars.len() {
5541 return None;
5542 }
5543
5544 let mut end = start;
5547 while end < chars.len() {
5548 if is_terminator(chars[end]) {
5549 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
5551 end += 1;
5552 }
5553 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
5556 break;
5557 }
5558 }
5559 end += 1;
5560 }
5561 let end_idx = (end + 1).min(chars.len());
5563
5564 let final_end = if inner {
5565 end_idx
5566 } else {
5567 let mut e = end_idx;
5571 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
5572 e += 1;
5573 }
5574 e
5575 };
5576
5577 Some((idx_to_pos(start), idx_to_pos(final_end)))
5578}
5579
5580fn tag_text_object<H: crate::types::Host>(
5584 ed: &Editor<hjkl_buffer::Buffer, H>,
5585 inner: bool,
5586) -> Option<((usize, usize), (usize, usize))> {
5587 let lines = buf_lines_to_vec(&ed.buffer);
5588 if lines.is_empty() {
5589 return None;
5590 }
5591 let pos_to_idx = |pos: (usize, usize)| -> usize {
5595 let mut idx = 0;
5596 for line in lines.iter().take(pos.0) {
5597 idx += line.chars().count() + 1;
5598 }
5599 idx + pos.1
5600 };
5601 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
5602 for (r, line) in lines.iter().enumerate() {
5603 let len = line.chars().count();
5604 if idx <= len {
5605 return (r, idx);
5606 }
5607 idx -= len + 1;
5608 }
5609 let last = lines.len().saturating_sub(1);
5610 (last, lines[last].chars().count())
5611 };
5612 let mut chars: Vec<char> = Vec::new();
5613 for (r, line) in lines.iter().enumerate() {
5614 chars.extend(line.chars());
5615 if r + 1 < lines.len() {
5616 chars.push('\n');
5617 }
5618 }
5619 let cursor_idx = pos_to_idx(ed.cursor());
5620
5621 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
5629 let mut next_after: Option<(usize, usize, usize, usize)> = None;
5630 let mut i = 0;
5631 while i < chars.len() {
5632 if chars[i] != '<' {
5633 i += 1;
5634 continue;
5635 }
5636 let mut j = i + 1;
5637 while j < chars.len() && chars[j] != '>' {
5638 j += 1;
5639 }
5640 if j >= chars.len() {
5641 break;
5642 }
5643 let inside: String = chars[i + 1..j].iter().collect();
5644 let close_end = j + 1;
5645 let trimmed = inside.trim();
5646 if trimmed.starts_with('!') || trimmed.starts_with('?') {
5647 i = close_end;
5648 continue;
5649 }
5650 if let Some(rest) = trimmed.strip_prefix('/') {
5651 let name = rest.split_whitespace().next().unwrap_or("").to_string();
5652 if !name.is_empty()
5653 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
5654 {
5655 let (open_start, content_start, _) = stack[stack_idx].clone();
5656 stack.truncate(stack_idx);
5657 let content_end = i;
5658 let candidate = (open_start, content_start, content_end, close_end);
5659 if cursor_idx >= content_start && cursor_idx <= content_end {
5660 innermost = match innermost {
5661 Some((_, cs, ce, _)) if cs <= content_start && content_end <= ce => {
5662 Some(candidate)
5663 }
5664 None => Some(candidate),
5665 existing => existing,
5666 };
5667 } else if open_start >= cursor_idx && next_after.is_none() {
5668 next_after = Some(candidate);
5669 }
5670 }
5671 } else if !trimmed.ends_with('/') {
5672 let name: String = trimmed
5673 .split(|c: char| c.is_whitespace() || c == '/')
5674 .next()
5675 .unwrap_or("")
5676 .to_string();
5677 if !name.is_empty() {
5678 stack.push((i, close_end, name));
5679 }
5680 }
5681 i = close_end;
5682 }
5683
5684 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
5685 if inner {
5686 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
5687 } else {
5688 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
5689 }
5690}
5691
5692fn is_wordchar(c: char) -> bool {
5693 c.is_alphanumeric() || c == '_'
5694}
5695
5696pub(crate) use hjkl_buffer::is_keyword_char;
5700
5701fn word_text_object<H: crate::types::Host>(
5702 ed: &Editor<hjkl_buffer::Buffer, H>,
5703 inner: bool,
5704 big: bool,
5705) -> Option<((usize, usize), (usize, usize))> {
5706 let (row, col) = ed.cursor();
5707 let line = buf_line(&ed.buffer, row)?;
5708 let chars: Vec<char> = line.chars().collect();
5709 if chars.is_empty() {
5710 return None;
5711 }
5712 let at = col.min(chars.len().saturating_sub(1));
5713 let classify = |c: char| -> u8 {
5714 if c.is_whitespace() {
5715 0
5716 } else if big || is_wordchar(c) {
5717 1
5718 } else {
5719 2
5720 }
5721 };
5722 let cls = classify(chars[at]);
5723 let mut start = at;
5724 while start > 0 && classify(chars[start - 1]) == cls {
5725 start -= 1;
5726 }
5727 let mut end = at;
5728 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
5729 end += 1;
5730 }
5731 let char_byte = |i: usize| {
5733 if i >= chars.len() {
5734 line.len()
5735 } else {
5736 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
5737 }
5738 };
5739 let mut start_col = char_byte(start);
5740 let mut end_col = char_byte(end + 1);
5742 if !inner {
5743 let mut t = end + 1;
5745 let mut included_trailing = false;
5746 while t < chars.len() && chars[t].is_whitespace() {
5747 included_trailing = true;
5748 t += 1;
5749 }
5750 if included_trailing {
5751 end_col = char_byte(t);
5752 } else {
5753 let mut s = start;
5754 while s > 0 && chars[s - 1].is_whitespace() {
5755 s -= 1;
5756 }
5757 start_col = char_byte(s);
5758 }
5759 }
5760 Some(((row, start_col), (row, end_col)))
5761}
5762
5763fn quote_text_object<H: crate::types::Host>(
5764 ed: &Editor<hjkl_buffer::Buffer, H>,
5765 q: char,
5766 inner: bool,
5767) -> Option<((usize, usize), (usize, usize))> {
5768 let (row, col) = ed.cursor();
5769 let line = buf_line(&ed.buffer, row)?;
5770 let bytes = line.as_bytes();
5771 let q_byte = q as u8;
5772 let mut positions: Vec<usize> = Vec::new();
5774 for (i, &b) in bytes.iter().enumerate() {
5775 if b == q_byte {
5776 positions.push(i);
5777 }
5778 }
5779 if positions.len() < 2 {
5780 return None;
5781 }
5782 let mut open_idx: Option<usize> = None;
5783 let mut close_idx: Option<usize> = None;
5784 for pair in positions.chunks(2) {
5785 if pair.len() < 2 {
5786 break;
5787 }
5788 if col >= pair[0] && col <= pair[1] {
5789 open_idx = Some(pair[0]);
5790 close_idx = Some(pair[1]);
5791 break;
5792 }
5793 if col < pair[0] {
5794 open_idx = Some(pair[0]);
5795 close_idx = Some(pair[1]);
5796 break;
5797 }
5798 }
5799 let open = open_idx?;
5800 let close = close_idx?;
5801 if inner {
5803 if close <= open + 1 {
5804 return None;
5805 }
5806 Some(((row, open + 1), (row, close)))
5807 } else {
5808 let after_close = close + 1; if after_close < bytes.len() && bytes[after_close].is_ascii_whitespace() {
5815 let mut end = after_close;
5817 while end < bytes.len() && bytes[end].is_ascii_whitespace() {
5818 end += 1;
5819 }
5820 Some(((row, open), (row, end)))
5821 } else if open > 0 && bytes[open - 1].is_ascii_whitespace() {
5822 let mut start = open;
5824 while start > 0 && bytes[start - 1].is_ascii_whitespace() {
5825 start -= 1;
5826 }
5827 Some(((row, start), (row, close + 1)))
5828 } else {
5829 Some(((row, open), (row, close + 1)))
5830 }
5831 }
5832}
5833
5834fn bracket_text_object<H: crate::types::Host>(
5835 ed: &Editor<hjkl_buffer::Buffer, H>,
5836 open: char,
5837 inner: bool,
5838) -> Option<(Pos, Pos, MotionKind)> {
5839 let close = match open {
5840 '(' => ')',
5841 '[' => ']',
5842 '{' => '}',
5843 '<' => '>',
5844 _ => return None,
5845 };
5846 let (row, col) = ed.cursor();
5847 let lines = buf_lines_to_vec(&ed.buffer);
5848 let lines = lines.as_slice();
5849 let open_pos = find_open_bracket(lines, row, col, open, close)
5854 .or_else(|| find_next_open(lines, row, col, open))?;
5855 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
5856 if inner {
5858 if close_pos.0 > open_pos.0 + 1 {
5864 let inner_row_start = open_pos.0 + 1;
5866 let inner_row_end = close_pos.0 - 1;
5867 let end_col = lines
5868 .get(inner_row_end)
5869 .map(|l| l.chars().count())
5870 .unwrap_or(0);
5871 return Some((
5872 (inner_row_start, 0),
5873 (inner_row_end, end_col),
5874 MotionKind::Linewise,
5875 ));
5876 }
5877 let inner_start = advance_pos(lines, open_pos);
5878 if inner_start.0 > close_pos.0
5879 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
5880 {
5881 return None;
5882 }
5883 Some((inner_start, close_pos, MotionKind::Exclusive))
5884 } else {
5885 Some((
5886 open_pos,
5887 advance_pos(lines, close_pos),
5888 MotionKind::Exclusive,
5889 ))
5890 }
5891}
5892
5893fn find_open_bracket(
5894 lines: &[String],
5895 row: usize,
5896 col: usize,
5897 open: char,
5898 close: char,
5899) -> Option<(usize, usize)> {
5900 let mut depth: i32 = 0;
5901 let mut r = row;
5902 let mut c = col as isize;
5903 loop {
5904 let cur = &lines[r];
5905 let chars: Vec<char> = cur.chars().collect();
5906 if (c as usize) >= chars.len() {
5910 c = chars.len() as isize - 1;
5911 }
5912 while c >= 0 {
5913 let ch = chars[c as usize];
5914 if ch == close {
5915 depth += 1;
5916 } else if ch == open {
5917 if depth == 0 {
5918 return Some((r, c as usize));
5919 }
5920 depth -= 1;
5921 }
5922 c -= 1;
5923 }
5924 if r == 0 {
5925 return None;
5926 }
5927 r -= 1;
5928 c = lines[r].chars().count() as isize - 1;
5929 }
5930}
5931
5932fn find_close_bracket(
5933 lines: &[String],
5934 row: usize,
5935 start_col: usize,
5936 open: char,
5937 close: char,
5938) -> Option<(usize, usize)> {
5939 let mut depth: i32 = 0;
5940 let mut r = row;
5941 let mut c = start_col;
5942 loop {
5943 let cur = &lines[r];
5944 let chars: Vec<char> = cur.chars().collect();
5945 while c < chars.len() {
5946 let ch = chars[c];
5947 if ch == open {
5948 depth += 1;
5949 } else if ch == close {
5950 if depth == 0 {
5951 return Some((r, c));
5952 }
5953 depth -= 1;
5954 }
5955 c += 1;
5956 }
5957 if r + 1 >= lines.len() {
5958 return None;
5959 }
5960 r += 1;
5961 c = 0;
5962 }
5963}
5964
5965fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
5969 let mut r = row;
5970 let mut c = col;
5971 while r < lines.len() {
5972 let chars: Vec<char> = lines[r].chars().collect();
5973 while c < chars.len() {
5974 if chars[c] == open {
5975 return Some((r, c));
5976 }
5977 c += 1;
5978 }
5979 r += 1;
5980 c = 0;
5981 }
5982 None
5983}
5984
5985fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
5986 let (r, c) = pos;
5987 let line_len = lines[r].chars().count();
5988 if c < line_len {
5989 (r, c + 1)
5990 } else if r + 1 < lines.len() {
5991 (r + 1, 0)
5992 } else {
5993 pos
5994 }
5995}
5996
5997fn paragraph_text_object<H: crate::types::Host>(
5998 ed: &Editor<hjkl_buffer::Buffer, H>,
5999 inner: bool,
6000) -> Option<((usize, usize), (usize, usize))> {
6001 let (row, _) = ed.cursor();
6002 let lines = buf_lines_to_vec(&ed.buffer);
6003 if lines.is_empty() {
6004 return None;
6005 }
6006 let is_blank = |r: usize| lines.get(r).map(|s| s.trim().is_empty()).unwrap_or(true);
6008 if is_blank(row) {
6009 return None;
6010 }
6011 let mut top = row;
6012 while top > 0 && !is_blank(top - 1) {
6013 top -= 1;
6014 }
6015 let mut bot = row;
6016 while bot + 1 < lines.len() && !is_blank(bot + 1) {
6017 bot += 1;
6018 }
6019 if !inner && bot + 1 < lines.len() && is_blank(bot + 1) {
6021 bot += 1;
6022 }
6023 let end_col = lines[bot].chars().count();
6024 Some(((top, 0), (bot, end_col)))
6025}
6026
6027fn read_vim_range<H: crate::types::Host>(
6033 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6034 start: (usize, usize),
6035 end: (usize, usize),
6036 kind: MotionKind,
6037) -> String {
6038 let (top, bot) = order(start, end);
6039 ed.sync_buffer_content_from_textarea();
6040 let lines = buf_lines_to_vec(&ed.buffer);
6041 match kind {
6042 MotionKind::Linewise => {
6043 let lo = top.0;
6044 let hi = bot.0.min(lines.len().saturating_sub(1));
6045 let mut text = lines[lo..=hi].join("\n");
6046 text.push('\n');
6047 text
6048 }
6049 MotionKind::Inclusive | MotionKind::Exclusive => {
6050 let inclusive = matches!(kind, MotionKind::Inclusive);
6051 let mut out = String::new();
6053 for row in top.0..=bot.0 {
6054 let line = lines.get(row).map(String::as_str).unwrap_or("");
6055 let lo = if row == top.0 { top.1 } else { 0 };
6056 let hi_unclamped = if row == bot.0 {
6057 if inclusive { bot.1 + 1 } else { bot.1 }
6058 } else {
6059 line.chars().count() + 1
6060 };
6061 let row_chars: Vec<char> = line.chars().collect();
6062 let hi = hi_unclamped.min(row_chars.len());
6063 if lo < hi {
6064 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
6065 }
6066 if row < bot.0 {
6067 out.push('\n');
6068 }
6069 }
6070 out
6071 }
6072 }
6073}
6074
6075fn cut_vim_range<H: crate::types::Host>(
6084 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6085 start: (usize, usize),
6086 end: (usize, usize),
6087 kind: MotionKind,
6088) -> String {
6089 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
6090 let (top, bot) = order(start, end);
6091 ed.sync_buffer_content_from_textarea();
6092 let (buf_start, buf_end, buf_kind) = match kind {
6093 MotionKind::Linewise => (
6094 Position::new(top.0, 0),
6095 Position::new(bot.0, 0),
6096 BufKind::Line,
6097 ),
6098 MotionKind::Inclusive => {
6099 let line_chars = buf_line_chars(&ed.buffer, bot.0);
6100 let next = if bot.1 < line_chars {
6104 Position::new(bot.0, bot.1 + 1)
6105 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
6106 Position::new(bot.0 + 1, 0)
6107 } else {
6108 Position::new(bot.0, line_chars)
6109 };
6110 (Position::new(top.0, top.1), next, BufKind::Char)
6111 }
6112 MotionKind::Exclusive => (
6113 Position::new(top.0, top.1),
6114 Position::new(bot.0, bot.1),
6115 BufKind::Char,
6116 ),
6117 };
6118 let inverse = ed.mutate_edit(Edit::DeleteRange {
6119 start: buf_start,
6120 end: buf_end,
6121 kind: buf_kind,
6122 });
6123 let text = match inverse {
6124 Edit::InsertStr { text, .. } => text,
6125 _ => String::new(),
6126 };
6127 if !text.is_empty() {
6128 ed.record_yank_to_host(text.clone());
6129 ed.record_delete(text.clone(), matches!(kind, MotionKind::Linewise));
6130 }
6131 ed.push_buffer_cursor_to_textarea();
6132 text
6133}
6134
6135fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6141 use hjkl_buffer::{Edit, MotionKind, Position};
6142 ed.sync_buffer_content_from_textarea();
6143 let cursor = buf_cursor_pos(&ed.buffer);
6144 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
6145 if cursor.col >= line_chars {
6146 return;
6147 }
6148 let inverse = ed.mutate_edit(Edit::DeleteRange {
6149 start: cursor,
6150 end: Position::new(cursor.row, line_chars),
6151 kind: MotionKind::Char,
6152 });
6153 if let Edit::InsertStr { text, .. } = inverse
6154 && !text.is_empty()
6155 {
6156 ed.record_yank_to_host(text.clone());
6157 ed.vim.yank_linewise = false;
6158 ed.set_yank(text);
6159 }
6160 buf_set_cursor_pos(&mut ed.buffer, cursor);
6161 ed.push_buffer_cursor_to_textarea();
6162}
6163
6164fn do_char_delete<H: crate::types::Host>(
6165 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6166 forward: bool,
6167 count: usize,
6168) {
6169 use hjkl_buffer::{Edit, MotionKind, Position};
6170 ed.push_undo();
6171 ed.sync_buffer_content_from_textarea();
6172 let mut deleted = String::new();
6175 for _ in 0..count {
6176 let cursor = buf_cursor_pos(&ed.buffer);
6177 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
6178 if forward {
6179 if cursor.col >= line_chars {
6182 continue;
6183 }
6184 let inverse = ed.mutate_edit(Edit::DeleteRange {
6185 start: cursor,
6186 end: Position::new(cursor.row, cursor.col + 1),
6187 kind: MotionKind::Char,
6188 });
6189 if let Edit::InsertStr { text, .. } = inverse {
6190 deleted.push_str(&text);
6191 }
6192 } else {
6193 if cursor.col == 0 {
6195 continue;
6196 }
6197 let inverse = ed.mutate_edit(Edit::DeleteRange {
6198 start: Position::new(cursor.row, cursor.col - 1),
6199 end: cursor,
6200 kind: MotionKind::Char,
6201 });
6202 if let Edit::InsertStr { text, .. } = inverse {
6203 deleted = text + &deleted;
6206 }
6207 }
6208 }
6209 if !deleted.is_empty() {
6210 ed.record_yank_to_host(deleted.clone());
6211 ed.record_delete(deleted, false);
6212 }
6213 ed.push_buffer_cursor_to_textarea();
6214}
6215
6216fn adjust_number<H: crate::types::Host>(
6220 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6221 delta: i64,
6222) -> bool {
6223 use hjkl_buffer::{Edit, MotionKind, Position};
6224 ed.sync_buffer_content_from_textarea();
6225 let cursor = buf_cursor_pos(&ed.buffer);
6226 let row = cursor.row;
6227 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
6228 Some(l) => l.chars().collect(),
6229 None => return false,
6230 };
6231 let Some(digit_start) = (cursor.col..chars.len()).find(|&i| chars[i].is_ascii_digit()) else {
6232 return false;
6233 };
6234 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
6235 digit_start - 1
6236 } else {
6237 digit_start
6238 };
6239 let mut span_end = digit_start;
6240 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
6241 span_end += 1;
6242 }
6243 let s: String = chars[span_start..span_end].iter().collect();
6244 let Ok(n) = s.parse::<i64>() else {
6245 return false;
6246 };
6247 let new_s = n.saturating_add(delta).to_string();
6248
6249 ed.push_undo();
6250 let span_start_pos = Position::new(row, span_start);
6251 let span_end_pos = Position::new(row, span_end);
6252 ed.mutate_edit(Edit::DeleteRange {
6253 start: span_start_pos,
6254 end: span_end_pos,
6255 kind: MotionKind::Char,
6256 });
6257 ed.mutate_edit(Edit::InsertStr {
6258 at: span_start_pos,
6259 text: new_s.clone(),
6260 });
6261 let new_len = new_s.chars().count();
6262 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
6263 ed.push_buffer_cursor_to_textarea();
6264 true
6265}
6266
6267pub(crate) fn replace_char<H: crate::types::Host>(
6268 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6269 ch: char,
6270 count: usize,
6271) {
6272 use hjkl_buffer::{Edit, MotionKind, Position};
6273 ed.push_undo();
6274 ed.sync_buffer_content_from_textarea();
6275 for _ in 0..count {
6276 let cursor = buf_cursor_pos(&ed.buffer);
6277 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
6278 if cursor.col >= line_chars {
6279 break;
6280 }
6281 ed.mutate_edit(Edit::DeleteRange {
6282 start: cursor,
6283 end: Position::new(cursor.row, cursor.col + 1),
6284 kind: MotionKind::Char,
6285 });
6286 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
6287 }
6288 crate::motions::move_left(&mut ed.buffer, 1);
6290 ed.push_buffer_cursor_to_textarea();
6291}
6292
6293fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6294 use hjkl_buffer::{Edit, MotionKind, Position};
6295 ed.sync_buffer_content_from_textarea();
6296 let cursor = buf_cursor_pos(&ed.buffer);
6297 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
6298 return;
6299 };
6300 let toggled = if c.is_uppercase() {
6301 c.to_lowercase().next().unwrap_or(c)
6302 } else {
6303 c.to_uppercase().next().unwrap_or(c)
6304 };
6305 ed.mutate_edit(Edit::DeleteRange {
6306 start: cursor,
6307 end: Position::new(cursor.row, cursor.col + 1),
6308 kind: MotionKind::Char,
6309 });
6310 ed.mutate_edit(Edit::InsertChar {
6311 at: cursor,
6312 ch: toggled,
6313 });
6314}
6315
6316fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6317 use hjkl_buffer::{Edit, Position};
6318 ed.sync_buffer_content_from_textarea();
6319 let row = buf_cursor_pos(&ed.buffer).row;
6320 if row + 1 >= buf_row_count(&ed.buffer) {
6321 return;
6322 }
6323 let cur_line = buf_line(&ed.buffer, row).unwrap_or("").to_string();
6324 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or("").to_string();
6325 let next_trimmed = next_raw.trim_start();
6326 let cur_chars = cur_line.chars().count();
6327 let next_chars = next_raw.chars().count();
6328 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
6331 " "
6332 } else {
6333 ""
6334 };
6335 let joined = format!("{cur_line}{separator}{next_trimmed}");
6336 ed.mutate_edit(Edit::Replace {
6337 start: Position::new(row, 0),
6338 end: Position::new(row + 1, next_chars),
6339 with: joined,
6340 });
6341 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
6345 ed.push_buffer_cursor_to_textarea();
6346}
6347
6348fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6351 use hjkl_buffer::Edit;
6352 ed.sync_buffer_content_from_textarea();
6353 let row = buf_cursor_pos(&ed.buffer).row;
6354 if row + 1 >= buf_row_count(&ed.buffer) {
6355 return;
6356 }
6357 let join_col = buf_line_chars(&ed.buffer, row);
6358 ed.mutate_edit(Edit::JoinLines {
6359 row,
6360 count: 1,
6361 with_space: false,
6362 });
6363 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
6365 ed.push_buffer_cursor_to_textarea();
6366}
6367
6368fn do_paste<H: crate::types::Host>(
6369 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6370 before: bool,
6371 count: usize,
6372) {
6373 use hjkl_buffer::{Edit, Position};
6374 ed.push_undo();
6375 let selector = ed.vim.pending_register.take();
6380 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
6381 Some(slot) => (slot.text.clone(), slot.linewise),
6382 None => {
6388 let s = &ed.registers().unnamed;
6389 (s.text.clone(), s.linewise)
6390 }
6391 };
6392 let mut paste_mark: Option<((usize, usize), (usize, usize))> = None;
6396 for _ in 0..count {
6397 ed.sync_buffer_content_from_textarea();
6398 let yank = yank.clone();
6399 if yank.is_empty() {
6400 continue;
6401 }
6402 if linewise {
6403 let text = yank.trim_matches('\n').to_string();
6407 let row = buf_cursor_pos(&ed.buffer).row;
6408 let target_row = if before {
6409 ed.mutate_edit(Edit::InsertStr {
6410 at: Position::new(row, 0),
6411 text: format!("{text}\n"),
6412 });
6413 row
6414 } else {
6415 let line_chars = buf_line_chars(&ed.buffer, row);
6416 ed.mutate_edit(Edit::InsertStr {
6417 at: Position::new(row, line_chars),
6418 text: format!("\n{text}"),
6419 });
6420 row + 1
6421 };
6422 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
6423 crate::motions::move_first_non_blank(&mut ed.buffer);
6424 ed.push_buffer_cursor_to_textarea();
6425 let payload_lines = text.lines().count().max(1);
6427 let bot_row = target_row + payload_lines - 1;
6428 let bot_last_col = buf_line_chars(&ed.buffer, bot_row).saturating_sub(1);
6429 paste_mark = Some(((target_row, 0), (bot_row, bot_last_col)));
6430 } else {
6431 let cursor = buf_cursor_pos(&ed.buffer);
6435 let at = if before {
6436 cursor
6437 } else {
6438 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
6439 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
6440 };
6441 ed.mutate_edit(Edit::InsertStr {
6442 at,
6443 text: yank.clone(),
6444 });
6445 crate::motions::move_left(&mut ed.buffer, 1);
6448 ed.push_buffer_cursor_to_textarea();
6449 let lo = (at.row, at.col);
6451 let hi = ed.cursor();
6452 paste_mark = Some((lo, hi));
6453 }
6454 }
6455 if let Some((lo, hi)) = paste_mark {
6456 ed.set_mark('[', lo);
6457 ed.set_mark(']', hi);
6458 }
6459 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
6461}
6462
6463pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6464 if let Some((lines, cursor)) = ed.undo_stack.pop() {
6465 let current = ed.snapshot();
6466 ed.redo_stack.push(current);
6467 ed.restore(lines, cursor);
6468 }
6469 ed.vim.mode = Mode::Normal;
6470 clamp_cursor_to_normal_mode(ed);
6474}
6475
6476pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6477 if let Some((lines, cursor)) = ed.redo_stack.pop() {
6478 let current = ed.snapshot();
6479 ed.undo_stack.push(current);
6480 ed.cap_undo();
6481 ed.restore(lines, cursor);
6482 }
6483 ed.vim.mode = Mode::Normal;
6484}
6485
6486fn replay_insert_and_finish<H: crate::types::Host>(
6493 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6494 text: &str,
6495) {
6496 use hjkl_buffer::{Edit, Position};
6497 let cursor = ed.cursor();
6498 ed.mutate_edit(Edit::InsertStr {
6499 at: Position::new(cursor.0, cursor.1),
6500 text: text.to_string(),
6501 });
6502 if ed.vim.insert_session.take().is_some() {
6503 if ed.cursor().1 > 0 {
6504 crate::motions::move_left(&mut ed.buffer, 1);
6505 ed.push_buffer_cursor_to_textarea();
6506 }
6507 ed.vim.mode = Mode::Normal;
6508 }
6509}
6510
6511pub(crate) fn replay_last_change<H: crate::types::Host>(
6512 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6513 outer_count: usize,
6514) {
6515 let Some(change) = ed.vim.last_change.clone() else {
6516 return;
6517 };
6518 ed.vim.replaying = true;
6519 let scale = if outer_count > 0 { outer_count } else { 1 };
6520 match change {
6521 LastChange::OpMotion {
6522 op,
6523 motion,
6524 count,
6525 inserted,
6526 } => {
6527 let total = count.max(1) * scale;
6528 apply_op_with_motion(ed, op, &motion, total);
6529 if let Some(text) = inserted {
6530 replay_insert_and_finish(ed, &text);
6531 }
6532 }
6533 LastChange::OpTextObj {
6534 op,
6535 obj,
6536 inner,
6537 inserted,
6538 } => {
6539 apply_op_with_text_object(ed, op, obj, inner);
6540 if let Some(text) = inserted {
6541 replay_insert_and_finish(ed, &text);
6542 }
6543 }
6544 LastChange::LineOp {
6545 op,
6546 count,
6547 inserted,
6548 } => {
6549 let total = count.max(1) * scale;
6550 execute_line_op(ed, op, total);
6551 if let Some(text) = inserted {
6552 replay_insert_and_finish(ed, &text);
6553 }
6554 }
6555 LastChange::CharDel { forward, count } => {
6556 do_char_delete(ed, forward, count * scale);
6557 }
6558 LastChange::ReplaceChar { ch, count } => {
6559 replace_char(ed, ch, count * scale);
6560 }
6561 LastChange::ToggleCase { count } => {
6562 for _ in 0..count * scale {
6563 ed.push_undo();
6564 toggle_case_at_cursor(ed);
6565 }
6566 }
6567 LastChange::JoinLine { count } => {
6568 for _ in 0..count * scale {
6569 ed.push_undo();
6570 join_line(ed);
6571 }
6572 }
6573 LastChange::Paste { before, count } => {
6574 do_paste(ed, before, count * scale);
6575 }
6576 LastChange::DeleteToEol { inserted } => {
6577 use hjkl_buffer::{Edit, Position};
6578 ed.push_undo();
6579 delete_to_eol(ed);
6580 if let Some(text) = inserted {
6581 let cursor = ed.cursor();
6582 ed.mutate_edit(Edit::InsertStr {
6583 at: Position::new(cursor.0, cursor.1),
6584 text,
6585 });
6586 }
6587 }
6588 LastChange::OpenLine { above, inserted } => {
6589 use hjkl_buffer::{Edit, Position};
6590 ed.push_undo();
6591 ed.sync_buffer_content_from_textarea();
6592 let row = buf_cursor_pos(&ed.buffer).row;
6593 if above {
6594 ed.mutate_edit(Edit::InsertStr {
6595 at: Position::new(row, 0),
6596 text: "\n".to_string(),
6597 });
6598 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
6599 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
6600 } else {
6601 let line_chars = buf_line_chars(&ed.buffer, row);
6602 ed.mutate_edit(Edit::InsertStr {
6603 at: Position::new(row, line_chars),
6604 text: "\n".to_string(),
6605 });
6606 }
6607 ed.push_buffer_cursor_to_textarea();
6608 let cursor = ed.cursor();
6609 ed.mutate_edit(Edit::InsertStr {
6610 at: Position::new(cursor.0, cursor.1),
6611 text: inserted,
6612 });
6613 }
6614 LastChange::InsertAt {
6615 entry,
6616 inserted,
6617 count,
6618 } => {
6619 use hjkl_buffer::{Edit, Position};
6620 ed.push_undo();
6621 match entry {
6622 InsertEntry::I => {}
6623 InsertEntry::ShiftI => move_first_non_whitespace(ed),
6624 InsertEntry::A => {
6625 crate::motions::move_right_to_end(&mut ed.buffer, 1);
6626 ed.push_buffer_cursor_to_textarea();
6627 }
6628 InsertEntry::ShiftA => {
6629 crate::motions::move_line_end(&mut ed.buffer);
6630 crate::motions::move_right_to_end(&mut ed.buffer, 1);
6631 ed.push_buffer_cursor_to_textarea();
6632 }
6633 }
6634 for _ in 0..count.max(1) {
6635 let cursor = ed.cursor();
6636 ed.mutate_edit(Edit::InsertStr {
6637 at: Position::new(cursor.0, cursor.1),
6638 text: inserted.clone(),
6639 });
6640 }
6641 }
6642 }
6643 ed.vim.replaying = false;
6644}
6645
6646fn extract_inserted(before: &str, after: &str) -> String {
6649 let before_chars: Vec<char> = before.chars().collect();
6650 let after_chars: Vec<char> = after.chars().collect();
6651 if after_chars.len() <= before_chars.len() {
6652 return String::new();
6653 }
6654 let prefix = before_chars
6655 .iter()
6656 .zip(after_chars.iter())
6657 .take_while(|(a, b)| a == b)
6658 .count();
6659 let max_suffix = before_chars.len() - prefix;
6660 let suffix = before_chars
6661 .iter()
6662 .rev()
6663 .zip(after_chars.iter().rev())
6664 .take(max_suffix)
6665 .take_while(|(a, b)| a == b)
6666 .count();
6667 after_chars[prefix..after_chars.len() - suffix]
6668 .iter()
6669 .collect()
6670}
6671
6672#[cfg(all(test, feature = "crossterm"))]
6675mod tests {
6676 use crate::VimMode;
6677 use crate::editor::Editor;
6678 use crate::types::Host;
6679 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
6680
6681 fn run_keys<H: crate::types::Host>(e: &mut Editor<hjkl_buffer::Buffer, H>, keys: &str) {
6682 let mut iter = keys.chars().peekable();
6686 while let Some(c) = iter.next() {
6687 if c == '<' {
6688 let mut tag = String::new();
6689 for ch in iter.by_ref() {
6690 if ch == '>' {
6691 break;
6692 }
6693 tag.push(ch);
6694 }
6695 let ev = match tag.as_str() {
6696 "Esc" => KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
6697 "CR" => KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
6698 "BS" => KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE),
6699 "Space" => KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE),
6700 "Up" => KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
6701 "Down" => KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
6702 "Left" => KeyEvent::new(KeyCode::Left, KeyModifiers::NONE),
6703 "Right" => KeyEvent::new(KeyCode::Right, KeyModifiers::NONE),
6704 "lt" => KeyEvent::new(KeyCode::Char('<'), KeyModifiers::NONE),
6708 s if s.starts_with("C-") => {
6709 let ch = s.chars().nth(2).unwrap();
6710 KeyEvent::new(KeyCode::Char(ch), KeyModifiers::CONTROL)
6711 }
6712 _ => continue,
6713 };
6714 e.handle_key(ev);
6715 } else {
6716 let mods = if c.is_uppercase() {
6717 KeyModifiers::SHIFT
6718 } else {
6719 KeyModifiers::NONE
6720 };
6721 e.handle_key(KeyEvent::new(KeyCode::Char(c), mods));
6722 }
6723 }
6724 }
6725
6726 fn editor_with(content: &str) -> Editor {
6727 let opts = crate::types::Options {
6732 shiftwidth: 2,
6733 ..crate::types::Options::default()
6734 };
6735 let mut e = Editor::new(
6736 hjkl_buffer::Buffer::new(),
6737 crate::types::DefaultHost::new(),
6738 opts,
6739 );
6740 e.set_content(content);
6741 e
6742 }
6743
6744 #[test]
6745 fn f_char_jumps_on_line() {
6746 let mut e = editor_with("hello world");
6747 run_keys(&mut e, "fw");
6748 assert_eq!(e.cursor(), (0, 6));
6749 }
6750
6751 #[test]
6752 fn cap_f_jumps_backward() {
6753 let mut e = editor_with("hello world");
6754 e.jump_cursor(0, 10);
6755 run_keys(&mut e, "Fo");
6756 assert_eq!(e.cursor().1, 7);
6757 }
6758
6759 #[test]
6760 fn t_stops_before_char() {
6761 let mut e = editor_with("hello");
6762 run_keys(&mut e, "tl");
6763 assert_eq!(e.cursor(), (0, 1));
6764 }
6765
6766 #[test]
6767 fn semicolon_repeats_find() {
6768 let mut e = editor_with("aa.bb.cc");
6769 run_keys(&mut e, "f.");
6770 assert_eq!(e.cursor().1, 2);
6771 run_keys(&mut e, ";");
6772 assert_eq!(e.cursor().1, 5);
6773 }
6774
6775 #[test]
6776 fn comma_repeats_find_reverse() {
6777 let mut e = editor_with("aa.bb.cc");
6778 run_keys(&mut e, "f.");
6779 run_keys(&mut e, ";");
6780 run_keys(&mut e, ",");
6781 assert_eq!(e.cursor().1, 2);
6782 }
6783
6784 #[test]
6785 fn di_quote_deletes_content() {
6786 let mut e = editor_with("foo \"bar\" baz");
6787 e.jump_cursor(0, 6); run_keys(&mut e, "di\"");
6789 assert_eq!(e.buffer().lines()[0], "foo \"\" baz");
6790 }
6791
6792 #[test]
6793 fn da_quote_deletes_with_quotes() {
6794 let mut e = editor_with("foo \"bar\" baz");
6797 e.jump_cursor(0, 6);
6798 run_keys(&mut e, "da\"");
6799 assert_eq!(e.buffer().lines()[0], "foo baz");
6800 }
6801
6802 #[test]
6803 fn ci_paren_deletes_and_inserts() {
6804 let mut e = editor_with("fn(a, b, c)");
6805 e.jump_cursor(0, 5);
6806 run_keys(&mut e, "ci(");
6807 assert_eq!(e.vim_mode(), VimMode::Insert);
6808 assert_eq!(e.buffer().lines()[0], "fn()");
6809 }
6810
6811 #[test]
6812 fn diw_deletes_inner_word() {
6813 let mut e = editor_with("hello world");
6814 e.jump_cursor(0, 2);
6815 run_keys(&mut e, "diw");
6816 assert_eq!(e.buffer().lines()[0], " world");
6817 }
6818
6819 #[test]
6820 fn daw_deletes_word_with_trailing_space() {
6821 let mut e = editor_with("hello world");
6822 run_keys(&mut e, "daw");
6823 assert_eq!(e.buffer().lines()[0], "world");
6824 }
6825
6826 #[test]
6827 fn percent_jumps_to_matching_bracket() {
6828 let mut e = editor_with("foo(bar)");
6829 e.jump_cursor(0, 3);
6830 run_keys(&mut e, "%");
6831 assert_eq!(e.cursor().1, 7);
6832 run_keys(&mut e, "%");
6833 assert_eq!(e.cursor().1, 3);
6834 }
6835
6836 #[test]
6837 fn dot_repeats_last_change() {
6838 let mut e = editor_with("aaa bbb ccc");
6839 run_keys(&mut e, "dw");
6840 assert_eq!(e.buffer().lines()[0], "bbb ccc");
6841 run_keys(&mut e, ".");
6842 assert_eq!(e.buffer().lines()[0], "ccc");
6843 }
6844
6845 #[test]
6846 fn dot_repeats_change_operator_with_text() {
6847 let mut e = editor_with("foo foo foo");
6848 run_keys(&mut e, "cwbar<Esc>");
6849 assert_eq!(e.buffer().lines()[0], "bar foo foo");
6850 run_keys(&mut e, "w");
6852 run_keys(&mut e, ".");
6853 assert_eq!(e.buffer().lines()[0], "bar bar foo");
6854 }
6855
6856 #[test]
6857 fn dot_repeats_x() {
6858 let mut e = editor_with("abcdef");
6859 run_keys(&mut e, "x");
6860 run_keys(&mut e, "..");
6861 assert_eq!(e.buffer().lines()[0], "def");
6862 }
6863
6864 #[test]
6865 fn count_operator_motion_compose() {
6866 let mut e = editor_with("one two three four five");
6867 run_keys(&mut e, "d3w");
6868 assert_eq!(e.buffer().lines()[0], "four five");
6869 }
6870
6871 #[test]
6872 fn two_dd_deletes_two_lines() {
6873 let mut e = editor_with("a\nb\nc");
6874 run_keys(&mut e, "2dd");
6875 assert_eq!(e.buffer().lines().len(), 1);
6876 assert_eq!(e.buffer().lines()[0], "c");
6877 }
6878
6879 #[test]
6884 fn dd_in_middle_puts_cursor_on_first_non_blank_of_next() {
6885 let mut e = editor_with("one\ntwo\n three\nfour");
6886 e.jump_cursor(1, 2);
6887 run_keys(&mut e, "dd");
6888 assert_eq!(e.buffer().lines()[1], " three");
6890 assert_eq!(e.cursor(), (1, 4));
6891 }
6892
6893 #[test]
6894 fn dd_on_last_line_puts_cursor_on_first_non_blank_of_prev() {
6895 let mut e = editor_with("one\n two\nthree");
6896 e.jump_cursor(2, 0);
6897 run_keys(&mut e, "dd");
6898 assert_eq!(e.buffer().lines().len(), 2);
6900 assert_eq!(e.cursor(), (1, 2));
6901 }
6902
6903 #[test]
6904 fn dd_on_only_line_leaves_empty_buffer_and_cursor_at_zero() {
6905 let mut e = editor_with("lonely");
6906 run_keys(&mut e, "dd");
6907 assert_eq!(e.buffer().lines().len(), 1);
6908 assert_eq!(e.buffer().lines()[0], "");
6909 assert_eq!(e.cursor(), (0, 0));
6910 }
6911
6912 #[test]
6913 fn count_dd_puts_cursor_on_first_non_blank_of_remaining() {
6914 let mut e = editor_with("a\nb\nc\n d\ne");
6915 e.jump_cursor(1, 0);
6917 run_keys(&mut e, "3dd");
6918 assert_eq!(e.buffer().lines(), &["a".to_string(), "e".to_string()]);
6919 assert_eq!(e.cursor(), (1, 0));
6920 }
6921
6922 #[test]
6923 fn dd_then_j_uses_first_non_blank_not_sticky_col() {
6924 let mut e = editor_with(" line one\n line two\n xyz!");
6943 e.jump_cursor(0, 8);
6945 assert_eq!(e.cursor(), (0, 8));
6946 run_keys(&mut e, "dd");
6949 assert_eq!(
6950 e.cursor(),
6951 (0, 4),
6952 "dd must place cursor on first-non-blank"
6953 );
6954 run_keys(&mut e, "j");
6958 let (row, col) = e.cursor();
6959 assert_eq!(row, 1);
6960 assert_eq!(
6961 col, 4,
6962 "after dd, j should use the column dd established (4), not pre-dd sticky_col (8)"
6963 );
6964 }
6965
6966 #[test]
6967 fn gu_lowercases_motion_range() {
6968 let mut e = editor_with("HELLO WORLD");
6969 run_keys(&mut e, "guw");
6970 assert_eq!(e.buffer().lines()[0], "hello WORLD");
6971 assert_eq!(e.cursor(), (0, 0));
6972 }
6973
6974 #[test]
6975 fn g_u_uppercases_text_object() {
6976 let mut e = editor_with("hello world");
6977 run_keys(&mut e, "gUiw");
6979 assert_eq!(e.buffer().lines()[0], "HELLO world");
6980 assert_eq!(e.cursor(), (0, 0));
6981 }
6982
6983 #[test]
6984 fn g_tilde_toggles_case_of_range() {
6985 let mut e = editor_with("Hello World");
6986 run_keys(&mut e, "g~iw");
6987 assert_eq!(e.buffer().lines()[0], "hELLO World");
6988 }
6989
6990 #[test]
6991 fn g_uu_uppercases_current_line() {
6992 let mut e = editor_with("select 1\nselect 2");
6993 run_keys(&mut e, "gUU");
6994 assert_eq!(e.buffer().lines()[0], "SELECT 1");
6995 assert_eq!(e.buffer().lines()[1], "select 2");
6996 }
6997
6998 #[test]
6999 fn gugu_lowercases_current_line() {
7000 let mut e = editor_with("FOO BAR\nBAZ");
7001 run_keys(&mut e, "gugu");
7002 assert_eq!(e.buffer().lines()[0], "foo bar");
7003 }
7004
7005 #[test]
7006 fn visual_u_uppercases_selection() {
7007 let mut e = editor_with("hello world");
7008 run_keys(&mut e, "veU");
7010 assert_eq!(e.buffer().lines()[0], "HELLO world");
7011 }
7012
7013 #[test]
7014 fn visual_line_u_lowercases_line() {
7015 let mut e = editor_with("HELLO WORLD\nOTHER");
7016 run_keys(&mut e, "Vu");
7017 assert_eq!(e.buffer().lines()[0], "hello world");
7018 assert_eq!(e.buffer().lines()[1], "OTHER");
7019 }
7020
7021 #[test]
7022 fn g_uu_with_count_uppercases_multiple_lines() {
7023 let mut e = editor_with("one\ntwo\nthree\nfour");
7024 run_keys(&mut e, "3gUU");
7026 assert_eq!(e.buffer().lines()[0], "ONE");
7027 assert_eq!(e.buffer().lines()[1], "TWO");
7028 assert_eq!(e.buffer().lines()[2], "THREE");
7029 assert_eq!(e.buffer().lines()[3], "four");
7030 }
7031
7032 #[test]
7033 fn double_gt_indents_current_line() {
7034 let mut e = editor_with("hello");
7035 run_keys(&mut e, ">>");
7036 assert_eq!(e.buffer().lines()[0], " hello");
7037 assert_eq!(e.cursor(), (0, 2));
7039 }
7040
7041 #[test]
7042 fn double_lt_outdents_current_line() {
7043 let mut e = editor_with(" hello");
7044 run_keys(&mut e, "<lt><lt>");
7045 assert_eq!(e.buffer().lines()[0], " hello");
7046 assert_eq!(e.cursor(), (0, 2));
7047 }
7048
7049 #[test]
7050 fn count_double_gt_indents_multiple_lines() {
7051 let mut e = editor_with("a\nb\nc\nd");
7052 run_keys(&mut e, "3>>");
7054 assert_eq!(e.buffer().lines()[0], " a");
7055 assert_eq!(e.buffer().lines()[1], " b");
7056 assert_eq!(e.buffer().lines()[2], " c");
7057 assert_eq!(e.buffer().lines()[3], "d");
7058 }
7059
7060 #[test]
7061 fn outdent_clips_ragged_leading_whitespace() {
7062 let mut e = editor_with(" x");
7065 run_keys(&mut e, "<lt><lt>");
7066 assert_eq!(e.buffer().lines()[0], "x");
7067 }
7068
7069 #[test]
7070 fn indent_motion_is_always_linewise() {
7071 let mut e = editor_with("foo bar");
7074 run_keys(&mut e, ">w");
7075 assert_eq!(e.buffer().lines()[0], " foo bar");
7076 }
7077
7078 #[test]
7079 fn indent_text_object_extends_over_paragraph() {
7080 let mut e = editor_with("a\nb\n\nc\nd");
7081 run_keys(&mut e, ">ap");
7083 assert_eq!(e.buffer().lines()[0], " a");
7084 assert_eq!(e.buffer().lines()[1], " b");
7085 assert_eq!(e.buffer().lines()[2], "");
7086 assert_eq!(e.buffer().lines()[3], "c");
7087 }
7088
7089 #[test]
7090 fn visual_line_indent_shifts_selected_rows() {
7091 let mut e = editor_with("x\ny\nz");
7092 run_keys(&mut e, "Vj>");
7094 assert_eq!(e.buffer().lines()[0], " x");
7095 assert_eq!(e.buffer().lines()[1], " y");
7096 assert_eq!(e.buffer().lines()[2], "z");
7097 }
7098
7099 #[test]
7100 fn outdent_empty_line_is_noop() {
7101 let mut e = editor_with("\nfoo");
7102 run_keys(&mut e, "<lt><lt>");
7103 assert_eq!(e.buffer().lines()[0], "");
7104 }
7105
7106 #[test]
7107 fn indent_skips_empty_lines() {
7108 let mut e = editor_with("");
7111 run_keys(&mut e, ">>");
7112 assert_eq!(e.buffer().lines()[0], "");
7113 }
7114
7115 #[test]
7116 fn insert_ctrl_t_indents_current_line() {
7117 let mut e = editor_with("x");
7118 run_keys(&mut e, "i<C-t>");
7120 assert_eq!(e.buffer().lines()[0], " x");
7121 assert_eq!(e.cursor(), (0, 2));
7124 }
7125
7126 #[test]
7127 fn insert_ctrl_d_outdents_current_line() {
7128 let mut e = editor_with(" x");
7129 run_keys(&mut e, "A<C-d>");
7131 assert_eq!(e.buffer().lines()[0], " x");
7132 }
7133
7134 #[test]
7135 fn h_at_col_zero_does_not_wrap_to_prev_line() {
7136 let mut e = editor_with("first\nsecond");
7137 e.jump_cursor(1, 0);
7138 run_keys(&mut e, "h");
7139 assert_eq!(e.cursor(), (1, 0));
7141 }
7142
7143 #[test]
7144 fn l_at_last_char_does_not_wrap_to_next_line() {
7145 let mut e = editor_with("ab\ncd");
7146 e.jump_cursor(0, 1);
7148 run_keys(&mut e, "l");
7149 assert_eq!(e.cursor(), (0, 1));
7151 }
7152
7153 #[test]
7154 fn count_l_clamps_at_line_end() {
7155 let mut e = editor_with("abcde");
7156 run_keys(&mut e, "20l");
7159 assert_eq!(e.cursor(), (0, 4));
7160 }
7161
7162 #[test]
7163 fn count_h_clamps_at_col_zero() {
7164 let mut e = editor_with("abcde");
7165 e.jump_cursor(0, 3);
7166 run_keys(&mut e, "20h");
7167 assert_eq!(e.cursor(), (0, 0));
7168 }
7169
7170 #[test]
7171 fn dl_on_last_char_still_deletes_it() {
7172 let mut e = editor_with("ab");
7176 e.jump_cursor(0, 1);
7177 run_keys(&mut e, "dl");
7178 assert_eq!(e.buffer().lines()[0], "a");
7179 }
7180
7181 #[test]
7182 fn case_op_preserves_yank_register() {
7183 let mut e = editor_with("target");
7184 run_keys(&mut e, "yy");
7185 let yank_before = e.yank().to_string();
7186 run_keys(&mut e, "gUU");
7188 assert_eq!(e.buffer().lines()[0], "TARGET");
7189 assert_eq!(
7190 e.yank(),
7191 yank_before,
7192 "case ops must preserve the yank buffer"
7193 );
7194 }
7195
7196 #[test]
7197 fn dap_deletes_paragraph() {
7198 let mut e = editor_with("a\nb\n\nc\nd");
7199 run_keys(&mut e, "dap");
7200 assert_eq!(e.buffer().lines().first().map(String::as_str), Some("c"));
7201 }
7202
7203 #[test]
7204 fn dit_deletes_inner_tag_content() {
7205 let mut e = editor_with("<b>hello</b>");
7206 e.jump_cursor(0, 4);
7208 run_keys(&mut e, "dit");
7209 assert_eq!(e.buffer().lines()[0], "<b></b>");
7210 }
7211
7212 #[test]
7213 fn dat_deletes_around_tag() {
7214 let mut e = editor_with("hi <b>foo</b> bye");
7215 e.jump_cursor(0, 6);
7216 run_keys(&mut e, "dat");
7217 assert_eq!(e.buffer().lines()[0], "hi bye");
7218 }
7219
7220 #[test]
7221 fn dit_picks_innermost_tag() {
7222 let mut e = editor_with("<a><b>x</b></a>");
7223 e.jump_cursor(0, 6);
7225 run_keys(&mut e, "dit");
7226 assert_eq!(e.buffer().lines()[0], "<a><b></b></a>");
7228 }
7229
7230 #[test]
7231 fn dat_innermost_tag_pair() {
7232 let mut e = editor_with("<a><b>x</b></a>");
7233 e.jump_cursor(0, 6);
7234 run_keys(&mut e, "dat");
7235 assert_eq!(e.buffer().lines()[0], "<a></a>");
7236 }
7237
7238 #[test]
7239 fn dit_outside_any_tag_no_op() {
7240 let mut e = editor_with("plain text");
7241 e.jump_cursor(0, 3);
7242 run_keys(&mut e, "dit");
7243 assert_eq!(e.buffer().lines()[0], "plain text");
7245 }
7246
7247 #[test]
7248 fn cit_changes_inner_tag_content() {
7249 let mut e = editor_with("<b>hello</b>");
7250 e.jump_cursor(0, 4);
7251 run_keys(&mut e, "citNEW<Esc>");
7252 assert_eq!(e.buffer().lines()[0], "<b>NEW</b>");
7253 }
7254
7255 #[test]
7256 fn cat_changes_around_tag() {
7257 let mut e = editor_with("hi <b>foo</b> bye");
7258 e.jump_cursor(0, 6);
7259 run_keys(&mut e, "catBAR<Esc>");
7260 assert_eq!(e.buffer().lines()[0], "hi BAR bye");
7261 }
7262
7263 #[test]
7264 fn yit_yanks_inner_tag_content() {
7265 let mut e = editor_with("<b>hello</b>");
7266 e.jump_cursor(0, 4);
7267 run_keys(&mut e, "yit");
7268 assert_eq!(e.registers().read('"').unwrap().text, "hello");
7269 }
7270
7271 #[test]
7272 fn yat_yanks_full_tag_pair() {
7273 let mut e = editor_with("hi <b>foo</b> bye");
7274 e.jump_cursor(0, 6);
7275 run_keys(&mut e, "yat");
7276 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
7277 }
7278
7279 #[test]
7280 fn vit_visually_selects_inner_tag() {
7281 let mut e = editor_with("<b>hello</b>");
7282 e.jump_cursor(0, 4);
7283 run_keys(&mut e, "vit");
7284 assert_eq!(e.vim_mode(), VimMode::Visual);
7285 run_keys(&mut e, "y");
7286 assert_eq!(e.registers().read('"').unwrap().text, "hello");
7287 }
7288
7289 #[test]
7290 fn vat_visually_selects_around_tag() {
7291 let mut e = editor_with("x<b>foo</b>y");
7292 e.jump_cursor(0, 5);
7293 run_keys(&mut e, "vat");
7294 assert_eq!(e.vim_mode(), VimMode::Visual);
7295 run_keys(&mut e, "y");
7296 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
7297 }
7298
7299 #[test]
7302 #[allow(non_snake_case)]
7303 fn diW_deletes_inner_big_word() {
7304 let mut e = editor_with("foo.bar baz");
7305 e.jump_cursor(0, 2);
7306 run_keys(&mut e, "diW");
7307 assert_eq!(e.buffer().lines()[0], " baz");
7309 }
7310
7311 #[test]
7312 #[allow(non_snake_case)]
7313 fn daW_deletes_around_big_word() {
7314 let mut e = editor_with("foo.bar baz");
7315 e.jump_cursor(0, 2);
7316 run_keys(&mut e, "daW");
7317 assert_eq!(e.buffer().lines()[0], "baz");
7318 }
7319
7320 #[test]
7321 fn di_double_quote_deletes_inside() {
7322 let mut e = editor_with("a \"hello\" b");
7323 e.jump_cursor(0, 4);
7324 run_keys(&mut e, "di\"");
7325 assert_eq!(e.buffer().lines()[0], "a \"\" b");
7326 }
7327
7328 #[test]
7329 fn da_double_quote_deletes_around() {
7330 let mut e = editor_with("a \"hello\" b");
7332 e.jump_cursor(0, 4);
7333 run_keys(&mut e, "da\"");
7334 assert_eq!(e.buffer().lines()[0], "a b");
7335 }
7336
7337 #[test]
7338 fn di_single_quote_deletes_inside() {
7339 let mut e = editor_with("x 'foo' y");
7340 e.jump_cursor(0, 4);
7341 run_keys(&mut e, "di'");
7342 assert_eq!(e.buffer().lines()[0], "x '' y");
7343 }
7344
7345 #[test]
7346 fn da_single_quote_deletes_around() {
7347 let mut e = editor_with("x 'foo' y");
7349 e.jump_cursor(0, 4);
7350 run_keys(&mut e, "da'");
7351 assert_eq!(e.buffer().lines()[0], "x y");
7352 }
7353
7354 #[test]
7355 fn di_backtick_deletes_inside() {
7356 let mut e = editor_with("p `q` r");
7357 e.jump_cursor(0, 3);
7358 run_keys(&mut e, "di`");
7359 assert_eq!(e.buffer().lines()[0], "p `` r");
7360 }
7361
7362 #[test]
7363 fn da_backtick_deletes_around() {
7364 let mut e = editor_with("p `q` r");
7366 e.jump_cursor(0, 3);
7367 run_keys(&mut e, "da`");
7368 assert_eq!(e.buffer().lines()[0], "p r");
7369 }
7370
7371 #[test]
7372 fn di_paren_deletes_inside() {
7373 let mut e = editor_with("f(arg)");
7374 e.jump_cursor(0, 3);
7375 run_keys(&mut e, "di(");
7376 assert_eq!(e.buffer().lines()[0], "f()");
7377 }
7378
7379 #[test]
7380 fn di_paren_alias_b_works() {
7381 let mut e = editor_with("f(arg)");
7382 e.jump_cursor(0, 3);
7383 run_keys(&mut e, "dib");
7384 assert_eq!(e.buffer().lines()[0], "f()");
7385 }
7386
7387 #[test]
7388 fn di_bracket_deletes_inside() {
7389 let mut e = editor_with("a[b,c]d");
7390 e.jump_cursor(0, 3);
7391 run_keys(&mut e, "di[");
7392 assert_eq!(e.buffer().lines()[0], "a[]d");
7393 }
7394
7395 #[test]
7396 fn da_bracket_deletes_around() {
7397 let mut e = editor_with("a[b,c]d");
7398 e.jump_cursor(0, 3);
7399 run_keys(&mut e, "da[");
7400 assert_eq!(e.buffer().lines()[0], "ad");
7401 }
7402
7403 #[test]
7404 fn di_brace_deletes_inside() {
7405 let mut e = editor_with("x{y}z");
7406 e.jump_cursor(0, 2);
7407 run_keys(&mut e, "di{");
7408 assert_eq!(e.buffer().lines()[0], "x{}z");
7409 }
7410
7411 #[test]
7412 fn da_brace_deletes_around() {
7413 let mut e = editor_with("x{y}z");
7414 e.jump_cursor(0, 2);
7415 run_keys(&mut e, "da{");
7416 assert_eq!(e.buffer().lines()[0], "xz");
7417 }
7418
7419 #[test]
7420 fn di_brace_alias_capital_b_works() {
7421 let mut e = editor_with("x{y}z");
7422 e.jump_cursor(0, 2);
7423 run_keys(&mut e, "diB");
7424 assert_eq!(e.buffer().lines()[0], "x{}z");
7425 }
7426
7427 #[test]
7428 fn di_angle_deletes_inside() {
7429 let mut e = editor_with("p<q>r");
7430 e.jump_cursor(0, 2);
7431 run_keys(&mut e, "di<lt>");
7433 assert_eq!(e.buffer().lines()[0], "p<>r");
7434 }
7435
7436 #[test]
7437 fn da_angle_deletes_around() {
7438 let mut e = editor_with("p<q>r");
7439 e.jump_cursor(0, 2);
7440 run_keys(&mut e, "da<lt>");
7441 assert_eq!(e.buffer().lines()[0], "pr");
7442 }
7443
7444 #[test]
7445 fn dip_deletes_inner_paragraph() {
7446 let mut e = editor_with("a\nb\nc\n\nd");
7447 e.jump_cursor(1, 0);
7448 run_keys(&mut e, "dip");
7449 assert_eq!(e.buffer().lines(), vec!["".to_string(), "d".into()]);
7452 }
7453
7454 #[test]
7457 fn sentence_motion_close_paren_jumps_forward() {
7458 let mut e = editor_with("Alpha. Beta. Gamma.");
7459 e.jump_cursor(0, 0);
7460 run_keys(&mut e, ")");
7461 assert_eq!(e.cursor(), (0, 7));
7463 run_keys(&mut e, ")");
7464 assert_eq!(e.cursor(), (0, 13));
7465 }
7466
7467 #[test]
7468 fn sentence_motion_open_paren_jumps_backward() {
7469 let mut e = editor_with("Alpha. Beta. Gamma.");
7470 e.jump_cursor(0, 13);
7471 run_keys(&mut e, "(");
7472 assert_eq!(e.cursor(), (0, 7));
7475 run_keys(&mut e, "(");
7476 assert_eq!(e.cursor(), (0, 0));
7477 }
7478
7479 #[test]
7480 fn sentence_motion_count() {
7481 let mut e = editor_with("A. B. C. D.");
7482 e.jump_cursor(0, 0);
7483 run_keys(&mut e, "3)");
7484 assert_eq!(e.cursor(), (0, 9));
7486 }
7487
7488 #[test]
7489 fn dis_deletes_inner_sentence() {
7490 let mut e = editor_with("First one. Second one. Third one.");
7491 e.jump_cursor(0, 13);
7492 run_keys(&mut e, "dis");
7493 assert_eq!(e.buffer().lines()[0], "First one. Third one.");
7495 }
7496
7497 #[test]
7498 fn das_deletes_around_sentence_with_trailing_space() {
7499 let mut e = editor_with("Alpha. Beta. Gamma.");
7500 e.jump_cursor(0, 8);
7501 run_keys(&mut e, "das");
7502 assert_eq!(e.buffer().lines()[0], "Alpha. Gamma.");
7505 }
7506
7507 #[test]
7508 fn dis_handles_double_terminator() {
7509 let mut e = editor_with("Wow!? Next.");
7510 e.jump_cursor(0, 1);
7511 run_keys(&mut e, "dis");
7512 assert_eq!(e.buffer().lines()[0], " Next.");
7515 }
7516
7517 #[test]
7518 fn dis_first_sentence_from_cursor_at_zero() {
7519 let mut e = editor_with("Alpha. Beta.");
7520 e.jump_cursor(0, 0);
7521 run_keys(&mut e, "dis");
7522 assert_eq!(e.buffer().lines()[0], " Beta.");
7523 }
7524
7525 #[test]
7526 fn yis_yanks_inner_sentence() {
7527 let mut e = editor_with("Hello world. Bye.");
7528 e.jump_cursor(0, 5);
7529 run_keys(&mut e, "yis");
7530 assert_eq!(e.registers().read('"').unwrap().text, "Hello world.");
7531 }
7532
7533 #[test]
7534 fn vis_visually_selects_inner_sentence() {
7535 let mut e = editor_with("First. Second.");
7536 e.jump_cursor(0, 1);
7537 run_keys(&mut e, "vis");
7538 assert_eq!(e.vim_mode(), VimMode::Visual);
7539 run_keys(&mut e, "y");
7540 assert_eq!(e.registers().read('"').unwrap().text, "First.");
7541 }
7542
7543 #[test]
7544 fn ciw_changes_inner_word() {
7545 let mut e = editor_with("hello world");
7546 e.jump_cursor(0, 1);
7547 run_keys(&mut e, "ciwHEY<Esc>");
7548 assert_eq!(e.buffer().lines()[0], "HEY world");
7549 }
7550
7551 #[test]
7552 fn yiw_yanks_inner_word() {
7553 let mut e = editor_with("hello world");
7554 e.jump_cursor(0, 1);
7555 run_keys(&mut e, "yiw");
7556 assert_eq!(e.registers().read('"').unwrap().text, "hello");
7557 }
7558
7559 #[test]
7560 fn viw_selects_inner_word() {
7561 let mut e = editor_with("hello world");
7562 e.jump_cursor(0, 2);
7563 run_keys(&mut e, "viw");
7564 assert_eq!(e.vim_mode(), VimMode::Visual);
7565 run_keys(&mut e, "y");
7566 assert_eq!(e.registers().read('"').unwrap().text, "hello");
7567 }
7568
7569 #[test]
7570 fn ci_paren_changes_inside() {
7571 let mut e = editor_with("f(old)");
7572 e.jump_cursor(0, 3);
7573 run_keys(&mut e, "ci(NEW<Esc>");
7574 assert_eq!(e.buffer().lines()[0], "f(NEW)");
7575 }
7576
7577 #[test]
7578 fn yi_double_quote_yanks_inside() {
7579 let mut e = editor_with("say \"hi there\" then");
7580 e.jump_cursor(0, 6);
7581 run_keys(&mut e, "yi\"");
7582 assert_eq!(e.registers().read('"').unwrap().text, "hi there");
7583 }
7584
7585 #[test]
7586 fn vap_visual_selects_around_paragraph() {
7587 let mut e = editor_with("a\nb\n\nc");
7588 e.jump_cursor(0, 0);
7589 run_keys(&mut e, "vap");
7590 assert_eq!(e.vim_mode(), VimMode::VisualLine);
7591 run_keys(&mut e, "y");
7592 let text = e.registers().read('"').unwrap().text.clone();
7594 assert!(text.starts_with("a\nb"));
7595 }
7596
7597 #[test]
7598 fn star_finds_next_occurrence() {
7599 let mut e = editor_with("foo bar foo baz");
7600 run_keys(&mut e, "*");
7601 assert_eq!(e.cursor().1, 8);
7602 }
7603
7604 #[test]
7605 fn star_skips_substring_match() {
7606 let mut e = editor_with("foo foobar baz");
7609 run_keys(&mut e, "*");
7610 assert_eq!(e.cursor().1, 0);
7611 }
7612
7613 #[test]
7614 fn g_star_matches_substring() {
7615 let mut e = editor_with("foo foobar baz");
7618 run_keys(&mut e, "g*");
7619 assert_eq!(e.cursor().1, 4);
7620 }
7621
7622 #[test]
7623 fn g_pound_matches_substring_backward() {
7624 let mut e = editor_with("foo foobar baz foo");
7627 run_keys(&mut e, "$b");
7628 assert_eq!(e.cursor().1, 15);
7629 run_keys(&mut e, "g#");
7630 assert_eq!(e.cursor().1, 4);
7631 }
7632
7633 #[test]
7634 fn n_repeats_last_search_forward() {
7635 let mut e = editor_with("foo bar foo baz foo");
7636 run_keys(&mut e, "/foo<CR>");
7639 assert_eq!(e.cursor().1, 8);
7640 run_keys(&mut e, "n");
7641 assert_eq!(e.cursor().1, 16);
7642 }
7643
7644 #[test]
7645 fn shift_n_reverses_search() {
7646 let mut e = editor_with("foo bar foo baz foo");
7647 run_keys(&mut e, "/foo<CR>");
7648 run_keys(&mut e, "n");
7649 assert_eq!(e.cursor().1, 16);
7650 run_keys(&mut e, "N");
7651 assert_eq!(e.cursor().1, 8);
7652 }
7653
7654 #[test]
7655 fn n_noop_without_pattern() {
7656 let mut e = editor_with("foo bar");
7657 run_keys(&mut e, "n");
7658 assert_eq!(e.cursor(), (0, 0));
7659 }
7660
7661 #[test]
7662 fn visual_line_preserves_cursor_column() {
7663 let mut e = editor_with("hello world\nanother one\nbye");
7666 run_keys(&mut e, "lllll"); run_keys(&mut e, "V");
7668 assert_eq!(e.vim_mode(), VimMode::VisualLine);
7669 assert_eq!(e.cursor(), (0, 5));
7670 run_keys(&mut e, "j");
7671 assert_eq!(e.cursor(), (1, 5));
7672 }
7673
7674 #[test]
7675 fn visual_line_yank_includes_trailing_newline() {
7676 let mut e = editor_with("aaa\nbbb\nccc");
7677 run_keys(&mut e, "Vjy");
7678 assert_eq!(e.last_yank.as_deref(), Some("aaa\nbbb\n"));
7680 }
7681
7682 #[test]
7683 fn visual_line_yank_last_line_trailing_newline() {
7684 let mut e = editor_with("aaa\nbbb\nccc");
7685 run_keys(&mut e, "jj");
7687 run_keys(&mut e, "Vy");
7688 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
7689 }
7690
7691 #[test]
7692 fn yy_on_last_line_has_trailing_newline() {
7693 let mut e = editor_with("aaa\nbbb\nccc");
7694 run_keys(&mut e, "jj");
7695 run_keys(&mut e, "yy");
7696 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
7697 }
7698
7699 #[test]
7700 fn yy_in_middle_has_trailing_newline() {
7701 let mut e = editor_with("aaa\nbbb\nccc");
7702 run_keys(&mut e, "j");
7703 run_keys(&mut e, "yy");
7704 assert_eq!(e.last_yank.as_deref(), Some("bbb\n"));
7705 }
7706
7707 #[test]
7708 fn di_single_quote() {
7709 let mut e = editor_with("say 'hello world' now");
7710 e.jump_cursor(0, 7);
7711 run_keys(&mut e, "di'");
7712 assert_eq!(e.buffer().lines()[0], "say '' now");
7713 }
7714
7715 #[test]
7716 fn da_single_quote() {
7717 let mut e = editor_with("say 'hello' now");
7719 e.jump_cursor(0, 7);
7720 run_keys(&mut e, "da'");
7721 assert_eq!(e.buffer().lines()[0], "say now");
7722 }
7723
7724 #[test]
7725 fn di_backtick() {
7726 let mut e = editor_with("say `hi` now");
7727 e.jump_cursor(0, 5);
7728 run_keys(&mut e, "di`");
7729 assert_eq!(e.buffer().lines()[0], "say `` now");
7730 }
7731
7732 #[test]
7733 fn di_brace() {
7734 let mut e = editor_with("fn { a; b; c }");
7735 e.jump_cursor(0, 7);
7736 run_keys(&mut e, "di{");
7737 assert_eq!(e.buffer().lines()[0], "fn {}");
7738 }
7739
7740 #[test]
7741 fn di_bracket() {
7742 let mut e = editor_with("arr[1, 2, 3]");
7743 e.jump_cursor(0, 5);
7744 run_keys(&mut e, "di[");
7745 assert_eq!(e.buffer().lines()[0], "arr[]");
7746 }
7747
7748 #[test]
7749 fn dab_deletes_around_paren() {
7750 let mut e = editor_with("fn(a, b) + 1");
7751 e.jump_cursor(0, 4);
7752 run_keys(&mut e, "dab");
7753 assert_eq!(e.buffer().lines()[0], "fn + 1");
7754 }
7755
7756 #[test]
7757 fn da_big_b_deletes_around_brace() {
7758 let mut e = editor_with("x = {a: 1}");
7759 e.jump_cursor(0, 6);
7760 run_keys(&mut e, "daB");
7761 assert_eq!(e.buffer().lines()[0], "x = ");
7762 }
7763
7764 #[test]
7765 fn di_big_w_deletes_bigword() {
7766 let mut e = editor_with("foo-bar baz");
7767 e.jump_cursor(0, 2);
7768 run_keys(&mut e, "diW");
7769 assert_eq!(e.buffer().lines()[0], " baz");
7770 }
7771
7772 #[test]
7773 fn visual_select_inner_word() {
7774 let mut e = editor_with("hello world");
7775 e.jump_cursor(0, 2);
7776 run_keys(&mut e, "viw");
7777 assert_eq!(e.vim_mode(), VimMode::Visual);
7778 run_keys(&mut e, "y");
7779 assert_eq!(e.last_yank.as_deref(), Some("hello"));
7780 }
7781
7782 #[test]
7783 fn visual_select_inner_quote() {
7784 let mut e = editor_with("foo \"bar\" baz");
7785 e.jump_cursor(0, 6);
7786 run_keys(&mut e, "vi\"");
7787 run_keys(&mut e, "y");
7788 assert_eq!(e.last_yank.as_deref(), Some("bar"));
7789 }
7790
7791 #[test]
7792 fn visual_select_inner_paren() {
7793 let mut e = editor_with("fn(a, b)");
7794 e.jump_cursor(0, 4);
7795 run_keys(&mut e, "vi(");
7796 run_keys(&mut e, "y");
7797 assert_eq!(e.last_yank.as_deref(), Some("a, b"));
7798 }
7799
7800 #[test]
7801 fn visual_select_outer_brace() {
7802 let mut e = editor_with("{x}");
7803 e.jump_cursor(0, 1);
7804 run_keys(&mut e, "va{");
7805 run_keys(&mut e, "y");
7806 assert_eq!(e.last_yank.as_deref(), Some("{x}"));
7807 }
7808
7809 #[test]
7810 fn ci_paren_forward_scans_when_cursor_before_pair() {
7811 let mut e = editor_with("foo(bar)");
7814 e.jump_cursor(0, 0);
7815 run_keys(&mut e, "ci(NEW<Esc>");
7816 assert_eq!(e.buffer().lines()[0], "foo(NEW)");
7817 }
7818
7819 #[test]
7820 fn ci_paren_forward_scans_across_lines() {
7821 let mut e = editor_with("first\nfoo(bar)\nlast");
7822 e.jump_cursor(0, 0);
7823 run_keys(&mut e, "ci(NEW<Esc>");
7824 assert_eq!(e.buffer().lines()[1], "foo(NEW)");
7825 }
7826
7827 #[test]
7828 fn ci_brace_forward_scans_when_cursor_before_pair() {
7829 let mut e = editor_with("let x = {y};");
7830 e.jump_cursor(0, 0);
7831 run_keys(&mut e, "ci{NEW<Esc>");
7832 assert_eq!(e.buffer().lines()[0], "let x = {NEW};");
7833 }
7834
7835 #[test]
7836 fn cit_forward_scans_when_cursor_before_tag() {
7837 let mut e = editor_with("text <b>hello</b> rest");
7840 e.jump_cursor(0, 0);
7841 run_keys(&mut e, "citNEW<Esc>");
7842 assert_eq!(e.buffer().lines()[0], "text <b>NEW</b> rest");
7843 }
7844
7845 #[test]
7846 fn dat_forward_scans_when_cursor_before_tag() {
7847 let mut e = editor_with("text <b>hello</b> rest");
7849 e.jump_cursor(0, 0);
7850 run_keys(&mut e, "dat");
7851 assert_eq!(e.buffer().lines()[0], "text rest");
7852 }
7853
7854 #[test]
7855 fn ci_paren_still_works_when_cursor_inside() {
7856 let mut e = editor_with("fn(a, b)");
7859 e.jump_cursor(0, 4);
7860 run_keys(&mut e, "ci(NEW<Esc>");
7861 assert_eq!(e.buffer().lines()[0], "fn(NEW)");
7862 }
7863
7864 #[test]
7865 fn caw_changes_word_with_trailing_space() {
7866 let mut e = editor_with("hello world");
7867 run_keys(&mut e, "cawfoo<Esc>");
7868 assert_eq!(e.buffer().lines()[0], "fooworld");
7869 }
7870
7871 #[test]
7872 fn visual_char_yank_preserves_raw_text() {
7873 let mut e = editor_with("hello world");
7874 run_keys(&mut e, "vllly");
7875 assert_eq!(e.last_yank.as_deref(), Some("hell"));
7876 }
7877
7878 #[test]
7879 fn single_line_visual_line_selects_full_line_on_yank() {
7880 let mut e = editor_with("hello world\nbye");
7881 run_keys(&mut e, "V");
7882 run_keys(&mut e, "y");
7885 assert_eq!(e.last_yank.as_deref(), Some("hello world\n"));
7886 }
7887
7888 #[test]
7889 fn visual_line_extends_both_directions() {
7890 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7891 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
7893 assert_eq!(e.cursor(), (3, 0));
7894 run_keys(&mut e, "k");
7895 assert_eq!(e.cursor(), (2, 0));
7897 run_keys(&mut e, "k");
7898 assert_eq!(e.cursor(), (1, 0));
7899 }
7900
7901 #[test]
7902 fn visual_char_preserves_cursor_column() {
7903 let mut e = editor_with("hello world");
7904 run_keys(&mut e, "lllll"); run_keys(&mut e, "v");
7906 assert_eq!(e.cursor(), (0, 5));
7907 run_keys(&mut e, "ll");
7908 assert_eq!(e.cursor(), (0, 7));
7909 }
7910
7911 #[test]
7912 fn visual_char_highlight_bounds_order() {
7913 let mut e = editor_with("abcdef");
7914 run_keys(&mut e, "lll"); run_keys(&mut e, "v");
7916 run_keys(&mut e, "hh"); assert_eq!(e.char_highlight(), Some(((0, 1), (0, 3))));
7919 }
7920
7921 #[test]
7922 fn visual_line_highlight_bounds() {
7923 let mut e = editor_with("a\nb\nc");
7924 run_keys(&mut e, "V");
7925 assert_eq!(e.line_highlight(), Some((0, 0)));
7926 run_keys(&mut e, "j");
7927 assert_eq!(e.line_highlight(), Some((0, 1)));
7928 run_keys(&mut e, "j");
7929 assert_eq!(e.line_highlight(), Some((0, 2)));
7930 }
7931
7932 #[test]
7935 fn h_moves_left() {
7936 let mut e = editor_with("hello");
7937 e.jump_cursor(0, 3);
7938 run_keys(&mut e, "h");
7939 assert_eq!(e.cursor(), (0, 2));
7940 }
7941
7942 #[test]
7943 fn l_moves_right() {
7944 let mut e = editor_with("hello");
7945 run_keys(&mut e, "l");
7946 assert_eq!(e.cursor(), (0, 1));
7947 }
7948
7949 #[test]
7950 fn k_moves_up() {
7951 let mut e = editor_with("a\nb\nc");
7952 e.jump_cursor(2, 0);
7953 run_keys(&mut e, "k");
7954 assert_eq!(e.cursor(), (1, 0));
7955 }
7956
7957 #[test]
7958 fn zero_moves_to_line_start() {
7959 let mut e = editor_with(" hello");
7960 run_keys(&mut e, "$");
7961 run_keys(&mut e, "0");
7962 assert_eq!(e.cursor().1, 0);
7963 }
7964
7965 #[test]
7966 fn caret_moves_to_first_non_blank() {
7967 let mut e = editor_with(" hello");
7968 run_keys(&mut e, "0");
7969 run_keys(&mut e, "^");
7970 assert_eq!(e.cursor().1, 4);
7971 }
7972
7973 #[test]
7974 fn dollar_moves_to_last_char() {
7975 let mut e = editor_with("hello");
7976 run_keys(&mut e, "$");
7977 assert_eq!(e.cursor().1, 4);
7978 }
7979
7980 #[test]
7981 fn dollar_on_empty_line_stays_at_col_zero() {
7982 let mut e = editor_with("");
7983 run_keys(&mut e, "$");
7984 assert_eq!(e.cursor().1, 0);
7985 }
7986
7987 #[test]
7988 fn w_jumps_to_next_word() {
7989 let mut e = editor_with("foo bar baz");
7990 run_keys(&mut e, "w");
7991 assert_eq!(e.cursor().1, 4);
7992 }
7993
7994 #[test]
7995 fn b_jumps_back_a_word() {
7996 let mut e = editor_with("foo bar");
7997 e.jump_cursor(0, 6);
7998 run_keys(&mut e, "b");
7999 assert_eq!(e.cursor().1, 4);
8000 }
8001
8002 #[test]
8003 fn e_jumps_to_word_end() {
8004 let mut e = editor_with("foo bar");
8005 run_keys(&mut e, "e");
8006 assert_eq!(e.cursor().1, 2);
8007 }
8008
8009 #[test]
8012 fn d_dollar_deletes_to_eol() {
8013 let mut e = editor_with("hello world");
8014 e.jump_cursor(0, 5);
8015 run_keys(&mut e, "d$");
8016 assert_eq!(e.buffer().lines()[0], "hello");
8017 }
8018
8019 #[test]
8020 fn d_zero_deletes_to_line_start() {
8021 let mut e = editor_with("hello world");
8022 e.jump_cursor(0, 6);
8023 run_keys(&mut e, "d0");
8024 assert_eq!(e.buffer().lines()[0], "world");
8025 }
8026
8027 #[test]
8028 fn d_caret_deletes_to_first_non_blank() {
8029 let mut e = editor_with(" hello");
8030 e.jump_cursor(0, 6);
8031 run_keys(&mut e, "d^");
8032 assert_eq!(e.buffer().lines()[0], " llo");
8033 }
8034
8035 #[test]
8036 fn d_capital_g_deletes_to_end_of_file() {
8037 let mut e = editor_with("a\nb\nc\nd");
8038 e.jump_cursor(1, 0);
8039 run_keys(&mut e, "dG");
8040 assert_eq!(e.buffer().lines(), &["a".to_string()]);
8041 }
8042
8043 #[test]
8044 fn d_gg_deletes_to_start_of_file() {
8045 let mut e = editor_with("a\nb\nc\nd");
8046 e.jump_cursor(2, 0);
8047 run_keys(&mut e, "dgg");
8048 assert_eq!(e.buffer().lines(), &["d".to_string()]);
8049 }
8050
8051 #[test]
8052 fn cw_is_ce_quirk() {
8053 let mut e = editor_with("foo bar");
8056 run_keys(&mut e, "cwxyz<Esc>");
8057 assert_eq!(e.buffer().lines()[0], "xyz bar");
8058 }
8059
8060 #[test]
8063 fn big_d_deletes_to_eol() {
8064 let mut e = editor_with("hello world");
8065 e.jump_cursor(0, 5);
8066 run_keys(&mut e, "D");
8067 assert_eq!(e.buffer().lines()[0], "hello");
8068 }
8069
8070 #[test]
8071 fn big_c_deletes_to_eol_and_inserts() {
8072 let mut e = editor_with("hello world");
8073 e.jump_cursor(0, 5);
8074 run_keys(&mut e, "C!<Esc>");
8075 assert_eq!(e.buffer().lines()[0], "hello!");
8076 }
8077
8078 #[test]
8079 fn j_joins_next_line_with_space() {
8080 let mut e = editor_with("hello\nworld");
8081 run_keys(&mut e, "J");
8082 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
8083 }
8084
8085 #[test]
8086 fn j_strips_leading_whitespace_on_join() {
8087 let mut e = editor_with("hello\n world");
8088 run_keys(&mut e, "J");
8089 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
8090 }
8091
8092 #[test]
8093 fn big_x_deletes_char_before_cursor() {
8094 let mut e = editor_with("hello");
8095 e.jump_cursor(0, 3);
8096 run_keys(&mut e, "X");
8097 assert_eq!(e.buffer().lines()[0], "helo");
8098 }
8099
8100 #[test]
8101 fn s_substitutes_char_and_enters_insert() {
8102 let mut e = editor_with("hello");
8103 run_keys(&mut e, "sX<Esc>");
8104 assert_eq!(e.buffer().lines()[0], "Xello");
8105 }
8106
8107 #[test]
8108 fn count_x_deletes_many() {
8109 let mut e = editor_with("abcdef");
8110 run_keys(&mut e, "3x");
8111 assert_eq!(e.buffer().lines()[0], "def");
8112 }
8113
8114 #[test]
8117 fn p_pastes_charwise_after_cursor() {
8118 let mut e = editor_with("hello");
8119 run_keys(&mut e, "yw");
8120 run_keys(&mut e, "$p");
8121 assert_eq!(e.buffer().lines()[0], "hellohello");
8122 }
8123
8124 #[test]
8125 fn capital_p_pastes_charwise_before_cursor() {
8126 let mut e = editor_with("hello");
8127 run_keys(&mut e, "v");
8129 run_keys(&mut e, "l");
8130 run_keys(&mut e, "y");
8131 run_keys(&mut e, "$P");
8132 assert_eq!(e.buffer().lines()[0], "hellheo");
8135 }
8136
8137 #[test]
8138 fn p_pastes_linewise_below() {
8139 let mut e = editor_with("one\ntwo\nthree");
8140 run_keys(&mut e, "yy");
8141 run_keys(&mut e, "p");
8142 assert_eq!(
8143 e.buffer().lines(),
8144 &[
8145 "one".to_string(),
8146 "one".to_string(),
8147 "two".to_string(),
8148 "three".to_string()
8149 ]
8150 );
8151 }
8152
8153 #[test]
8154 fn capital_p_pastes_linewise_above() {
8155 let mut e = editor_with("one\ntwo");
8156 e.jump_cursor(1, 0);
8157 run_keys(&mut e, "yy");
8158 run_keys(&mut e, "P");
8159 assert_eq!(
8160 e.buffer().lines(),
8161 &["one".to_string(), "two".to_string(), "two".to_string()]
8162 );
8163 }
8164
8165 #[test]
8168 fn hash_finds_previous_occurrence() {
8169 let mut e = editor_with("foo bar foo baz foo");
8170 e.jump_cursor(0, 16);
8172 run_keys(&mut e, "#");
8173 assert_eq!(e.cursor().1, 8);
8174 }
8175
8176 #[test]
8179 fn visual_line_delete_removes_full_lines() {
8180 let mut e = editor_with("a\nb\nc\nd");
8181 run_keys(&mut e, "Vjd");
8182 assert_eq!(e.buffer().lines(), &["c".to_string(), "d".to_string()]);
8183 }
8184
8185 #[test]
8186 fn visual_line_change_leaves_blank_line() {
8187 let mut e = editor_with("a\nb\nc");
8188 run_keys(&mut e, "Vjc");
8189 assert_eq!(e.vim_mode(), VimMode::Insert);
8190 run_keys(&mut e, "X<Esc>");
8191 assert_eq!(e.buffer().lines(), &["X".to_string(), "c".to_string()]);
8195 }
8196
8197 #[test]
8198 fn cc_leaves_blank_line() {
8199 let mut e = editor_with("a\nb\nc");
8200 e.jump_cursor(1, 0);
8201 run_keys(&mut e, "ccX<Esc>");
8202 assert_eq!(
8203 e.buffer().lines(),
8204 &["a".to_string(), "X".to_string(), "c".to_string()]
8205 );
8206 }
8207
8208 #[test]
8213 fn big_w_skips_hyphens() {
8214 let mut e = editor_with("foo-bar baz");
8216 run_keys(&mut e, "W");
8217 assert_eq!(e.cursor().1, 8);
8218 }
8219
8220 #[test]
8221 fn big_w_crosses_lines() {
8222 let mut e = editor_with("foo-bar\nbaz-qux");
8223 run_keys(&mut e, "W");
8224 assert_eq!(e.cursor(), (1, 0));
8225 }
8226
8227 #[test]
8228 fn big_b_skips_hyphens() {
8229 let mut e = editor_with("foo-bar baz");
8230 e.jump_cursor(0, 9);
8231 run_keys(&mut e, "B");
8232 assert_eq!(e.cursor().1, 8);
8233 run_keys(&mut e, "B");
8234 assert_eq!(e.cursor().1, 0);
8235 }
8236
8237 #[test]
8238 fn big_e_jumps_to_big_word_end() {
8239 let mut e = editor_with("foo-bar baz");
8240 run_keys(&mut e, "E");
8241 assert_eq!(e.cursor().1, 6);
8242 run_keys(&mut e, "E");
8243 assert_eq!(e.cursor().1, 10);
8244 }
8245
8246 #[test]
8247 fn dw_with_big_word_variant() {
8248 let mut e = editor_with("foo-bar baz");
8250 run_keys(&mut e, "dW");
8251 assert_eq!(e.buffer().lines()[0], "baz");
8252 }
8253
8254 #[test]
8257 fn insert_ctrl_w_deletes_word_back() {
8258 let mut e = editor_with("");
8259 run_keys(&mut e, "i");
8260 for c in "hello world".chars() {
8261 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
8262 }
8263 run_keys(&mut e, "<C-w>");
8264 assert_eq!(e.buffer().lines()[0], "hello ");
8265 }
8266
8267 #[test]
8268 fn insert_ctrl_w_at_col0_joins_with_prev_word() {
8269 let mut e = editor_with("hello\nworld");
8273 e.jump_cursor(1, 0);
8274 run_keys(&mut e, "i");
8275 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
8276 assert_eq!(e.buffer().lines(), vec!["world".to_string()]);
8279 assert_eq!(e.cursor(), (0, 0));
8280 }
8281
8282 #[test]
8283 fn insert_ctrl_w_at_col0_keeps_prefix_words() {
8284 let mut e = editor_with("foo bar\nbaz");
8285 e.jump_cursor(1, 0);
8286 run_keys(&mut e, "i");
8287 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
8288 assert_eq!(e.buffer().lines(), vec!["foo baz".to_string()]);
8290 assert_eq!(e.cursor(), (0, 4));
8291 }
8292
8293 #[test]
8294 fn insert_ctrl_u_deletes_to_line_start() {
8295 let mut e = editor_with("");
8296 run_keys(&mut e, "i");
8297 for c in "hello world".chars() {
8298 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
8299 }
8300 run_keys(&mut e, "<C-u>");
8301 assert_eq!(e.buffer().lines()[0], "");
8302 }
8303
8304 #[test]
8305 fn insert_ctrl_o_runs_one_normal_command() {
8306 let mut e = editor_with("hello world");
8307 run_keys(&mut e, "A");
8309 assert_eq!(e.vim_mode(), VimMode::Insert);
8310 e.jump_cursor(0, 0);
8312 run_keys(&mut e, "<C-o>");
8313 assert_eq!(e.vim_mode(), VimMode::Normal);
8314 run_keys(&mut e, "dw");
8315 assert_eq!(e.vim_mode(), VimMode::Insert);
8317 assert_eq!(e.buffer().lines()[0], "world");
8318 }
8319
8320 #[test]
8323 fn j_through_empty_line_preserves_column() {
8324 let mut e = editor_with("hello world\n\nanother line");
8325 run_keys(&mut e, "llllll");
8327 assert_eq!(e.cursor(), (0, 6));
8328 run_keys(&mut e, "j");
8331 assert_eq!(e.cursor(), (1, 0));
8332 run_keys(&mut e, "j");
8334 assert_eq!(e.cursor(), (2, 6));
8335 }
8336
8337 #[test]
8338 fn j_through_shorter_line_preserves_column() {
8339 let mut e = editor_with("hello world\nhi\nanother line");
8340 run_keys(&mut e, "lllllll"); run_keys(&mut e, "j"); assert_eq!(e.cursor(), (1, 1));
8343 run_keys(&mut e, "j");
8344 assert_eq!(e.cursor(), (2, 7));
8345 }
8346
8347 #[test]
8348 fn esc_from_insert_sticky_matches_visible_cursor() {
8349 let mut e = editor_with(" this is a line\n another one of a similar size");
8353 e.jump_cursor(0, 12);
8354 run_keys(&mut e, "I");
8355 assert_eq!(e.cursor(), (0, 4));
8356 run_keys(&mut e, "X<Esc>");
8357 assert_eq!(e.cursor(), (0, 4));
8358 run_keys(&mut e, "j");
8359 assert_eq!(e.cursor(), (1, 4));
8360 }
8361
8362 #[test]
8363 fn esc_from_insert_sticky_tracks_inserted_chars() {
8364 let mut e = editor_with("xxxxxxx\nyyyyyyy");
8365 run_keys(&mut e, "i");
8366 run_keys(&mut e, "abc<Esc>");
8367 assert_eq!(e.cursor(), (0, 2));
8368 run_keys(&mut e, "j");
8369 assert_eq!(e.cursor(), (1, 2));
8370 }
8371
8372 #[test]
8373 fn esc_from_insert_sticky_tracks_arrow_nav() {
8374 let mut e = editor_with("xxxxxx\nyyyyyy");
8375 run_keys(&mut e, "i");
8376 run_keys(&mut e, "abc");
8377 for _ in 0..2 {
8378 e.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
8379 }
8380 run_keys(&mut e, "<Esc>");
8381 assert_eq!(e.cursor(), (0, 0));
8382 run_keys(&mut e, "j");
8383 assert_eq!(e.cursor(), (1, 0));
8384 }
8385
8386 #[test]
8387 fn esc_from_insert_at_col_14_followed_by_j() {
8388 let line = "x".repeat(30);
8391 let buf = format!("{line}\n{line}");
8392 let mut e = editor_with(&buf);
8393 e.jump_cursor(0, 14);
8394 run_keys(&mut e, "i");
8395 for c in "test ".chars() {
8396 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
8397 }
8398 run_keys(&mut e, "<Esc>");
8399 assert_eq!(e.cursor(), (0, 18));
8400 run_keys(&mut e, "j");
8401 assert_eq!(e.cursor(), (1, 18));
8402 }
8403
8404 #[test]
8405 fn linewise_paste_resets_sticky_column() {
8406 let mut e = editor_with(" hello\naaaaaaaa\nbye");
8410 run_keys(&mut e, "llllll"); run_keys(&mut e, "yy");
8412 run_keys(&mut e, "j"); run_keys(&mut e, "p"); assert_eq!(e.cursor(), (2, 4));
8416 run_keys(&mut e, "j");
8418 assert_eq!(e.cursor(), (3, 2));
8419 }
8420
8421 #[test]
8422 fn horizontal_motion_resyncs_sticky_column() {
8423 let mut e = editor_with("hello world\n\nanother line");
8427 run_keys(&mut e, "llllll"); run_keys(&mut e, "hhh"); run_keys(&mut e, "jj");
8430 assert_eq!(e.cursor(), (2, 3));
8431 }
8432
8433 #[test]
8436 fn ctrl_v_enters_visual_block() {
8437 let mut e = editor_with("aaa\nbbb\nccc");
8438 run_keys(&mut e, "<C-v>");
8439 assert_eq!(e.vim_mode(), VimMode::VisualBlock);
8440 }
8441
8442 #[test]
8443 fn visual_block_esc_returns_to_normal() {
8444 let mut e = editor_with("aaa\nbbb\nccc");
8445 run_keys(&mut e, "<C-v>");
8446 run_keys(&mut e, "<Esc>");
8447 assert_eq!(e.vim_mode(), VimMode::Normal);
8448 }
8449
8450 #[test]
8451 fn backtick_lt_jumps_to_visual_start_mark() {
8452 let mut e = editor_with("foo bar baz\n");
8456 run_keys(&mut e, "v");
8457 run_keys(&mut e, "w"); run_keys(&mut e, "<Esc>"); assert_eq!(e.cursor(), (0, 4));
8460 run_keys(&mut e, "`<lt>");
8462 assert_eq!(e.cursor(), (0, 0));
8463 }
8464
8465 #[test]
8466 fn backtick_gt_jumps_to_visual_end_mark() {
8467 let mut e = editor_with("foo bar baz\n");
8468 run_keys(&mut e, "v");
8469 run_keys(&mut e, "w"); run_keys(&mut e, "<Esc>");
8471 run_keys(&mut e, "0"); run_keys(&mut e, "`>");
8473 assert_eq!(e.cursor(), (0, 4));
8474 }
8475
8476 #[test]
8477 fn visual_exit_sets_lt_gt_marks() {
8478 let mut e = editor_with("aaa\nbbb\nccc\nddd");
8481 run_keys(&mut e, "V");
8483 run_keys(&mut e, "j");
8484 run_keys(&mut e, "<Esc>");
8485 let lt = e.mark('<').expect("'<' mark must be set on visual exit");
8486 let gt = e.mark('>').expect("'>' mark must be set on visual exit");
8487 assert_eq!(lt.0, 0, "'< row should be the lower bound");
8488 assert_eq!(gt.0, 1, "'> row should be the upper bound");
8489 }
8490
8491 #[test]
8492 fn visual_exit_marks_use_lower_higher_order() {
8493 let mut e = editor_with("aaa\nbbb\nccc\nddd");
8497 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
8499 run_keys(&mut e, "k"); run_keys(&mut e, "<Esc>");
8501 let lt = e.mark('<').unwrap();
8502 let gt = e.mark('>').unwrap();
8503 assert_eq!(lt.0, 2);
8504 assert_eq!(gt.0, 3);
8505 }
8506
8507 #[test]
8508 fn visualline_exit_marks_snap_to_line_edges() {
8509 let mut e = editor_with("aaaaa\nbbbbb\ncc");
8511 run_keys(&mut e, "lll"); run_keys(&mut e, "V");
8513 run_keys(&mut e, "j"); run_keys(&mut e, "<Esc>");
8515 let lt = e.mark('<').unwrap();
8516 let gt = e.mark('>').unwrap();
8517 assert_eq!(lt, (0, 0), "'< should snap to (top_row, 0)");
8518 assert_eq!(gt, (1, 4), "'> should snap to (bot_row, last_col)");
8520 }
8521
8522 #[test]
8523 fn visualblock_exit_marks_use_block_corners() {
8524 let mut e = editor_with("aaaaa\nbbbbb\nccccc");
8528 run_keys(&mut e, "llll"); run_keys(&mut e, "<C-v>");
8530 run_keys(&mut e, "j"); run_keys(&mut e, "hh"); run_keys(&mut e, "<Esc>");
8533 let lt = e.mark('<').unwrap();
8534 let gt = e.mark('>').unwrap();
8535 assert_eq!(lt, (0, 2), "'< should be top-left corner");
8537 assert_eq!(gt, (1, 4), "'> should be bottom-right corner");
8538 }
8539
8540 #[test]
8541 fn visual_block_delete_removes_column_range() {
8542 let mut e = editor_with("hello\nworld\nhappy");
8543 run_keys(&mut e, "l");
8545 run_keys(&mut e, "<C-v>");
8546 run_keys(&mut e, "jj");
8547 run_keys(&mut e, "ll");
8548 run_keys(&mut e, "d");
8549 assert_eq!(
8551 e.buffer().lines(),
8552 &["ho".to_string(), "wd".to_string(), "hy".to_string()]
8553 );
8554 }
8555
8556 #[test]
8557 fn visual_block_yank_joins_with_newlines() {
8558 let mut e = editor_with("hello\nworld\nhappy");
8559 run_keys(&mut e, "<C-v>");
8560 run_keys(&mut e, "jj");
8561 run_keys(&mut e, "ll");
8562 run_keys(&mut e, "y");
8563 assert_eq!(e.last_yank.as_deref(), Some("hel\nwor\nhap"));
8564 }
8565
8566 #[test]
8567 fn visual_block_replace_fills_block() {
8568 let mut e = editor_with("hello\nworld\nhappy");
8569 run_keys(&mut e, "<C-v>");
8570 run_keys(&mut e, "jj");
8571 run_keys(&mut e, "ll");
8572 run_keys(&mut e, "rx");
8573 assert_eq!(
8574 e.buffer().lines(),
8575 &[
8576 "xxxlo".to_string(),
8577 "xxxld".to_string(),
8578 "xxxpy".to_string()
8579 ]
8580 );
8581 }
8582
8583 #[test]
8584 fn visual_block_insert_repeats_across_rows() {
8585 let mut e = editor_with("hello\nworld\nhappy");
8586 run_keys(&mut e, "<C-v>");
8587 run_keys(&mut e, "jj");
8588 run_keys(&mut e, "I");
8589 run_keys(&mut e, "# <Esc>");
8590 assert_eq!(
8591 e.buffer().lines(),
8592 &[
8593 "# hello".to_string(),
8594 "# world".to_string(),
8595 "# happy".to_string()
8596 ]
8597 );
8598 }
8599
8600 #[test]
8601 fn block_highlight_returns_none_outside_block_mode() {
8602 let mut e = editor_with("abc");
8603 assert!(e.block_highlight().is_none());
8604 run_keys(&mut e, "v");
8605 assert!(e.block_highlight().is_none());
8606 run_keys(&mut e, "<Esc>V");
8607 assert!(e.block_highlight().is_none());
8608 }
8609
8610 #[test]
8611 fn block_highlight_bounds_track_anchor_and_cursor() {
8612 let mut e = editor_with("aaaa\nbbbb\ncccc");
8613 run_keys(&mut e, "ll"); run_keys(&mut e, "<C-v>");
8615 run_keys(&mut e, "jh"); assert_eq!(e.block_highlight(), Some((0, 1, 1, 2)));
8618 }
8619
8620 #[test]
8621 fn visual_block_delete_handles_short_lines() {
8622 let mut e = editor_with("hello\nhi\nworld");
8624 run_keys(&mut e, "l"); run_keys(&mut e, "<C-v>");
8626 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
8628 assert_eq!(
8633 e.buffer().lines(),
8634 &["ho".to_string(), "h".to_string(), "wd".to_string()]
8635 );
8636 }
8637
8638 #[test]
8639 fn visual_block_yank_pads_short_lines_with_empties() {
8640 let mut e = editor_with("hello\nhi\nworld");
8641 run_keys(&mut e, "l");
8642 run_keys(&mut e, "<C-v>");
8643 run_keys(&mut e, "jjll");
8644 run_keys(&mut e, "y");
8645 assert_eq!(e.last_yank.as_deref(), Some("ell\ni\norl"));
8647 }
8648
8649 #[test]
8650 fn visual_block_replace_skips_past_eol() {
8651 let mut e = editor_with("ab\ncd\nef");
8654 run_keys(&mut e, "l");
8656 run_keys(&mut e, "<C-v>");
8657 run_keys(&mut e, "jjllllll");
8658 run_keys(&mut e, "rX");
8659 assert_eq!(
8662 e.buffer().lines(),
8663 &["aX".to_string(), "cX".to_string(), "eX".to_string()]
8664 );
8665 }
8666
8667 #[test]
8668 fn visual_block_with_empty_line_in_middle() {
8669 let mut e = editor_with("abcd\n\nefgh");
8670 run_keys(&mut e, "<C-v>");
8671 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
8673 assert_eq!(
8676 e.buffer().lines(),
8677 &["d".to_string(), "".to_string(), "h".to_string()]
8678 );
8679 }
8680
8681 #[test]
8682 fn block_insert_pads_empty_lines_to_block_column() {
8683 let mut e = editor_with("this is a line\n\nthis is a line");
8686 e.jump_cursor(0, 3);
8687 run_keys(&mut e, "<C-v>");
8688 run_keys(&mut e, "jj");
8689 run_keys(&mut e, "I");
8690 run_keys(&mut e, "XX<Esc>");
8691 assert_eq!(
8692 e.buffer().lines(),
8693 &[
8694 "thiXXs is a line".to_string(),
8695 " XX".to_string(),
8696 "thiXXs is a line".to_string()
8697 ]
8698 );
8699 }
8700
8701 #[test]
8702 fn block_insert_pads_short_lines_to_block_column() {
8703 let mut e = editor_with("aaaaa\nbb\naaaaa");
8704 e.jump_cursor(0, 3);
8705 run_keys(&mut e, "<C-v>");
8706 run_keys(&mut e, "jj");
8707 run_keys(&mut e, "I");
8708 run_keys(&mut e, "Y<Esc>");
8709 assert_eq!(
8711 e.buffer().lines(),
8712 &[
8713 "aaaYaa".to_string(),
8714 "bb Y".to_string(),
8715 "aaaYaa".to_string()
8716 ]
8717 );
8718 }
8719
8720 #[test]
8721 fn visual_block_append_repeats_across_rows() {
8722 let mut e = editor_with("foo\nbar\nbaz");
8723 run_keys(&mut e, "<C-v>");
8724 run_keys(&mut e, "jj");
8725 run_keys(&mut e, "A");
8728 run_keys(&mut e, "!<Esc>");
8729 assert_eq!(
8730 e.buffer().lines(),
8731 &["f!oo".to_string(), "b!ar".to_string(), "b!az".to_string()]
8732 );
8733 }
8734
8735 #[test]
8738 fn slash_opens_forward_search_prompt() {
8739 let mut e = editor_with("hello world");
8740 run_keys(&mut e, "/");
8741 let p = e.search_prompt().expect("prompt should be active");
8742 assert!(p.text.is_empty());
8743 assert!(p.forward);
8744 }
8745
8746 #[test]
8747 fn question_opens_backward_search_prompt() {
8748 let mut e = editor_with("hello world");
8749 run_keys(&mut e, "?");
8750 let p = e.search_prompt().expect("prompt should be active");
8751 assert!(!p.forward);
8752 }
8753
8754 #[test]
8755 fn search_prompt_typing_updates_pattern_live() {
8756 let mut e = editor_with("foo bar\nbaz");
8757 run_keys(&mut e, "/bar");
8758 assert_eq!(e.search_prompt().unwrap().text, "bar");
8759 assert!(e.search_state().pattern.is_some());
8761 }
8762
8763 #[test]
8764 fn search_prompt_backspace_and_enter() {
8765 let mut e = editor_with("hello world\nagain");
8766 run_keys(&mut e, "/worlx");
8767 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
8768 assert_eq!(e.search_prompt().unwrap().text, "worl");
8769 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8770 assert!(e.search_prompt().is_none());
8772 assert_eq!(e.last_search(), Some("worl"));
8773 assert_eq!(e.cursor(), (0, 6));
8774 }
8775
8776 #[test]
8777 fn empty_search_prompt_enter_repeats_last_search() {
8778 let mut e = editor_with("foo bar foo baz foo");
8779 run_keys(&mut e, "/foo");
8780 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8781 assert_eq!(e.cursor().1, 8);
8782 run_keys(&mut e, "/");
8784 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8785 assert_eq!(e.cursor().1, 16);
8786 assert_eq!(e.last_search(), Some("foo"));
8787 }
8788
8789 #[test]
8790 fn search_history_records_committed_patterns() {
8791 let mut e = editor_with("alpha beta gamma");
8792 run_keys(&mut e, "/alpha<CR>");
8793 run_keys(&mut e, "/beta<CR>");
8794 let history = e.vim.search_history.clone();
8796 assert_eq!(history, vec!["alpha", "beta"]);
8797 }
8798
8799 #[test]
8800 fn search_history_dedupes_consecutive_repeats() {
8801 let mut e = editor_with("foo bar foo");
8802 run_keys(&mut e, "/foo<CR>");
8803 run_keys(&mut e, "/foo<CR>");
8804 run_keys(&mut e, "/bar<CR>");
8805 run_keys(&mut e, "/bar<CR>");
8806 assert_eq!(e.vim.search_history.clone(), vec!["foo", "bar"]);
8808 }
8809
8810 #[test]
8811 fn ctrl_p_walks_history_backward() {
8812 let mut e = editor_with("alpha beta gamma");
8813 run_keys(&mut e, "/alpha<CR>");
8814 run_keys(&mut e, "/beta<CR>");
8815 run_keys(&mut e, "/");
8817 assert_eq!(e.search_prompt().unwrap().text, "");
8818 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8819 assert_eq!(e.search_prompt().unwrap().text, "beta");
8820 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8821 assert_eq!(e.search_prompt().unwrap().text, "alpha");
8822 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8824 assert_eq!(e.search_prompt().unwrap().text, "alpha");
8825 }
8826
8827 #[test]
8828 fn ctrl_n_walks_history_forward_after_ctrl_p() {
8829 let mut e = editor_with("a b c");
8830 run_keys(&mut e, "/a<CR>");
8831 run_keys(&mut e, "/b<CR>");
8832 run_keys(&mut e, "/c<CR>");
8833 run_keys(&mut e, "/");
8834 for _ in 0..3 {
8836 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8837 }
8838 assert_eq!(e.search_prompt().unwrap().text, "a");
8839 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8840 assert_eq!(e.search_prompt().unwrap().text, "b");
8841 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8842 assert_eq!(e.search_prompt().unwrap().text, "c");
8843 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8845 assert_eq!(e.search_prompt().unwrap().text, "c");
8846 }
8847
8848 #[test]
8849 fn typing_after_history_walk_resets_cursor() {
8850 let mut e = editor_with("foo");
8851 run_keys(&mut e, "/foo<CR>");
8852 run_keys(&mut e, "/");
8853 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8854 assert_eq!(e.search_prompt().unwrap().text, "foo");
8855 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
8858 assert_eq!(e.search_prompt().unwrap().text, "foox");
8859 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8860 assert_eq!(e.search_prompt().unwrap().text, "foo");
8861 }
8862
8863 #[test]
8864 fn empty_backward_search_prompt_enter_repeats_last_search() {
8865 let mut e = editor_with("foo bar foo baz foo");
8866 run_keys(&mut e, "/foo");
8868 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8869 assert_eq!(e.cursor().1, 8);
8870 run_keys(&mut e, "?");
8871 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8872 assert_eq!(e.cursor().1, 0);
8873 assert_eq!(e.last_search(), Some("foo"));
8874 }
8875
8876 #[test]
8877 fn search_prompt_esc_cancels_but_keeps_last_search() {
8878 let mut e = editor_with("foo bar\nbaz");
8879 run_keys(&mut e, "/bar");
8880 e.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
8881 assert!(e.search_prompt().is_none());
8882 assert_eq!(e.last_search(), Some("bar"));
8883 }
8884
8885 #[test]
8886 fn search_then_n_and_shift_n_navigate() {
8887 let mut e = editor_with("foo bar foo baz foo");
8888 run_keys(&mut e, "/foo");
8889 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8890 assert_eq!(e.cursor().1, 8);
8892 run_keys(&mut e, "n");
8893 assert_eq!(e.cursor().1, 16);
8894 run_keys(&mut e, "N");
8895 assert_eq!(e.cursor().1, 8);
8896 }
8897
8898 #[test]
8899 fn question_mark_searches_backward_on_enter() {
8900 let mut e = editor_with("foo bar foo baz");
8901 e.jump_cursor(0, 10);
8902 run_keys(&mut e, "?foo");
8903 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8904 assert_eq!(e.cursor(), (0, 8));
8906 }
8907
8908 #[test]
8911 fn big_y_yanks_to_end_of_line() {
8912 let mut e = editor_with("hello world");
8913 e.jump_cursor(0, 6);
8914 run_keys(&mut e, "Y");
8915 assert_eq!(e.last_yank.as_deref(), Some("world"));
8916 }
8917
8918 #[test]
8919 fn big_y_from_line_start_yanks_full_line() {
8920 let mut e = editor_with("hello world");
8921 run_keys(&mut e, "Y");
8922 assert_eq!(e.last_yank.as_deref(), Some("hello world"));
8923 }
8924
8925 #[test]
8926 fn gj_joins_without_inserting_space() {
8927 let mut e = editor_with("hello\n world");
8928 run_keys(&mut e, "gJ");
8929 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
8931 }
8932
8933 #[test]
8934 fn gj_noop_on_last_line() {
8935 let mut e = editor_with("only");
8936 run_keys(&mut e, "gJ");
8937 assert_eq!(e.buffer().lines(), &["only".to_string()]);
8938 }
8939
8940 #[test]
8941 fn ge_jumps_to_previous_word_end() {
8942 let mut e = editor_with("foo bar baz");
8943 e.jump_cursor(0, 5);
8944 run_keys(&mut e, "ge");
8945 assert_eq!(e.cursor(), (0, 2));
8946 }
8947
8948 #[test]
8949 fn ge_respects_word_class() {
8950 let mut e = editor_with("foo-bar baz");
8953 e.jump_cursor(0, 5);
8954 run_keys(&mut e, "ge");
8955 assert_eq!(e.cursor(), (0, 3));
8956 }
8957
8958 #[test]
8959 fn big_ge_treats_hyphens_as_part_of_word() {
8960 let mut e = editor_with("foo-bar baz");
8963 e.jump_cursor(0, 10);
8964 run_keys(&mut e, "gE");
8965 assert_eq!(e.cursor(), (0, 6));
8966 }
8967
8968 #[test]
8969 fn ge_crosses_line_boundary() {
8970 let mut e = editor_with("foo\nbar");
8971 e.jump_cursor(1, 0);
8972 run_keys(&mut e, "ge");
8973 assert_eq!(e.cursor(), (0, 2));
8974 }
8975
8976 #[test]
8977 fn dge_deletes_to_end_of_previous_word() {
8978 let mut e = editor_with("foo bar baz");
8979 e.jump_cursor(0, 8);
8980 run_keys(&mut e, "dge");
8983 assert_eq!(e.buffer().lines()[0], "foo baaz");
8984 }
8985
8986 #[test]
8987 fn ctrl_scroll_keys_do_not_panic() {
8988 let mut e = editor_with(
8991 (0..50)
8992 .map(|i| format!("line{i}"))
8993 .collect::<Vec<_>>()
8994 .join("\n")
8995 .as_str(),
8996 );
8997 run_keys(&mut e, "<C-f>");
8998 run_keys(&mut e, "<C-b>");
8999 assert!(!e.buffer().lines().is_empty());
9001 }
9002
9003 #[test]
9010 fn count_insert_with_arrow_nav_does_not_leak_rows() {
9011 let mut e = Editor::new(
9012 hjkl_buffer::Buffer::new(),
9013 crate::types::DefaultHost::new(),
9014 crate::types::Options::default(),
9015 );
9016 e.set_content("row0\nrow1\nrow2");
9017 run_keys(&mut e, "3iX<Down><Esc>");
9019 assert!(e.buffer().lines()[0].contains('X'));
9021 assert!(
9024 !e.buffer().lines()[1].contains("row0"),
9025 "row1 leaked row0 contents: {:?}",
9026 e.buffer().lines()[1]
9027 );
9028 assert_eq!(e.buffer().lines().len(), 3);
9031 }
9032
9033 fn editor_with_rows(n: usize, viewport: u16) -> Editor {
9036 let mut e = Editor::new(
9037 hjkl_buffer::Buffer::new(),
9038 crate::types::DefaultHost::new(),
9039 crate::types::Options::default(),
9040 );
9041 let body = (0..n)
9042 .map(|i| format!(" line{}", i))
9043 .collect::<Vec<_>>()
9044 .join("\n");
9045 e.set_content(&body);
9046 e.set_viewport_height(viewport);
9047 e
9048 }
9049
9050 #[test]
9051 fn ctrl_d_moves_cursor_half_page_down() {
9052 let mut e = editor_with_rows(100, 20);
9053 run_keys(&mut e, "<C-d>");
9054 assert_eq!(e.cursor().0, 10);
9055 }
9056
9057 fn editor_with_wrap_lines(lines: &[&str], viewport: u16, text_width: u16) -> Editor {
9058 let mut e = Editor::new(
9059 hjkl_buffer::Buffer::new(),
9060 crate::types::DefaultHost::new(),
9061 crate::types::Options::default(),
9062 );
9063 e.set_content(&lines.join("\n"));
9064 e.set_viewport_height(viewport);
9065 let v = e.host_mut().viewport_mut();
9066 v.height = viewport;
9067 v.width = text_width;
9068 v.text_width = text_width;
9069 v.wrap = hjkl_buffer::Wrap::Char;
9070 e.settings_mut().wrap = hjkl_buffer::Wrap::Char;
9071 e
9072 }
9073
9074 #[test]
9075 fn scrolloff_wrap_keeps_cursor_off_bottom_edge() {
9076 let lines = ["aaaabbbbcccc"; 10];
9080 let mut e = editor_with_wrap_lines(&lines, 12, 4);
9081 e.jump_cursor(4, 0);
9082 e.ensure_cursor_in_scrolloff();
9083 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
9084 assert!(csr <= 6, "csr={csr}");
9085 }
9086
9087 #[test]
9088 fn scrolloff_wrap_keeps_cursor_off_top_edge() {
9089 let lines = ["aaaabbbbcccc"; 10];
9090 let mut e = editor_with_wrap_lines(&lines, 12, 4);
9091 e.jump_cursor(7, 0);
9094 e.ensure_cursor_in_scrolloff();
9095 e.jump_cursor(2, 0);
9096 e.ensure_cursor_in_scrolloff();
9097 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
9098 assert!(csr >= 5, "csr={csr}");
9100 }
9101
9102 #[test]
9103 fn scrolloff_wrap_clamps_top_at_buffer_end() {
9104 let lines = ["aaaabbbbcccc"; 5];
9105 let mut e = editor_with_wrap_lines(&lines, 12, 4);
9106 e.jump_cursor(4, 11);
9107 e.ensure_cursor_in_scrolloff();
9108 let top = e.host().viewport().top_row;
9113 assert_eq!(top, 1);
9114 }
9115
9116 #[test]
9117 fn ctrl_u_moves_cursor_half_page_up() {
9118 let mut e = editor_with_rows(100, 20);
9119 e.jump_cursor(50, 0);
9120 run_keys(&mut e, "<C-u>");
9121 assert_eq!(e.cursor().0, 40);
9122 }
9123
9124 #[test]
9125 fn ctrl_f_moves_cursor_full_page_down() {
9126 let mut e = editor_with_rows(100, 20);
9127 run_keys(&mut e, "<C-f>");
9128 assert_eq!(e.cursor().0, 18);
9130 }
9131
9132 #[test]
9133 fn ctrl_b_moves_cursor_full_page_up() {
9134 let mut e = editor_with_rows(100, 20);
9135 e.jump_cursor(50, 0);
9136 run_keys(&mut e, "<C-b>");
9137 assert_eq!(e.cursor().0, 32);
9138 }
9139
9140 #[test]
9141 fn ctrl_d_lands_on_first_non_blank() {
9142 let mut e = editor_with_rows(100, 20);
9143 run_keys(&mut e, "<C-d>");
9144 assert_eq!(e.cursor().1, 2);
9146 }
9147
9148 #[test]
9149 fn ctrl_d_clamps_at_end_of_buffer() {
9150 let mut e = editor_with_rows(5, 20);
9151 run_keys(&mut e, "<C-d>");
9152 assert_eq!(e.cursor().0, 4);
9153 }
9154
9155 #[test]
9156 fn capital_h_jumps_to_viewport_top() {
9157 let mut e = editor_with_rows(100, 10);
9158 e.jump_cursor(50, 0);
9159 e.set_viewport_top(45);
9160 let top = e.host().viewport().top_row;
9161 run_keys(&mut e, "H");
9162 assert_eq!(e.cursor().0, top);
9163 assert_eq!(e.cursor().1, 2);
9164 }
9165
9166 #[test]
9167 fn capital_l_jumps_to_viewport_bottom() {
9168 let mut e = editor_with_rows(100, 10);
9169 e.jump_cursor(50, 0);
9170 e.set_viewport_top(45);
9171 let top = e.host().viewport().top_row;
9172 run_keys(&mut e, "L");
9173 assert_eq!(e.cursor().0, top + 9);
9174 }
9175
9176 #[test]
9177 fn capital_m_jumps_to_viewport_middle() {
9178 let mut e = editor_with_rows(100, 10);
9179 e.jump_cursor(50, 0);
9180 e.set_viewport_top(45);
9181 let top = e.host().viewport().top_row;
9182 run_keys(&mut e, "M");
9183 assert_eq!(e.cursor().0, top + 4);
9185 }
9186
9187 #[test]
9188 fn g_capital_m_lands_at_line_midpoint() {
9189 let mut e = editor_with("hello world!"); run_keys(&mut e, "gM");
9191 assert_eq!(e.cursor(), (0, 6));
9193 }
9194
9195 #[test]
9196 fn g_capital_m_on_empty_line_stays_at_zero() {
9197 let mut e = editor_with("");
9198 run_keys(&mut e, "gM");
9199 assert_eq!(e.cursor(), (0, 0));
9200 }
9201
9202 #[test]
9203 fn g_capital_m_uses_current_line_only() {
9204 let mut e = editor_with("a\nlonglongline"); e.jump_cursor(1, 0);
9207 run_keys(&mut e, "gM");
9208 assert_eq!(e.cursor(), (1, 6));
9209 }
9210
9211 #[test]
9212 fn capital_h_count_offsets_from_top() {
9213 let mut e = editor_with_rows(100, 10);
9214 e.jump_cursor(50, 0);
9215 e.set_viewport_top(45);
9216 let top = e.host().viewport().top_row;
9217 run_keys(&mut e, "3H");
9218 assert_eq!(e.cursor().0, top + 2);
9219 }
9220
9221 #[test]
9224 fn ctrl_o_returns_to_pre_g_position() {
9225 let mut e = editor_with_rows(50, 20);
9226 e.jump_cursor(5, 2);
9227 run_keys(&mut e, "G");
9228 assert_eq!(e.cursor().0, 49);
9229 run_keys(&mut e, "<C-o>");
9230 assert_eq!(e.cursor(), (5, 2));
9231 }
9232
9233 #[test]
9234 fn ctrl_i_redoes_jump_after_ctrl_o() {
9235 let mut e = editor_with_rows(50, 20);
9236 e.jump_cursor(5, 2);
9237 run_keys(&mut e, "G");
9238 let post = e.cursor();
9239 run_keys(&mut e, "<C-o>");
9240 run_keys(&mut e, "<C-i>");
9241 assert_eq!(e.cursor(), post);
9242 }
9243
9244 #[test]
9245 fn new_jump_clears_forward_stack() {
9246 let mut e = editor_with_rows(50, 20);
9247 e.jump_cursor(5, 2);
9248 run_keys(&mut e, "G");
9249 run_keys(&mut e, "<C-o>");
9250 run_keys(&mut e, "gg");
9251 run_keys(&mut e, "<C-i>");
9252 assert_eq!(e.cursor().0, 0);
9253 }
9254
9255 #[test]
9256 fn ctrl_o_on_empty_stack_is_noop() {
9257 let mut e = editor_with_rows(10, 20);
9258 e.jump_cursor(3, 1);
9259 run_keys(&mut e, "<C-o>");
9260 assert_eq!(e.cursor(), (3, 1));
9261 }
9262
9263 #[test]
9264 fn asterisk_search_pushes_jump() {
9265 let mut e = editor_with("foo bar\nbaz foo end");
9266 e.jump_cursor(0, 0);
9267 run_keys(&mut e, "*");
9268 let after = e.cursor();
9269 assert_ne!(after, (0, 0));
9270 run_keys(&mut e, "<C-o>");
9271 assert_eq!(e.cursor(), (0, 0));
9272 }
9273
9274 #[test]
9275 fn h_viewport_jump_is_recorded() {
9276 let mut e = editor_with_rows(100, 10);
9277 e.jump_cursor(50, 0);
9278 e.set_viewport_top(45);
9279 let pre = e.cursor();
9280 run_keys(&mut e, "H");
9281 assert_ne!(e.cursor(), pre);
9282 run_keys(&mut e, "<C-o>");
9283 assert_eq!(e.cursor(), pre);
9284 }
9285
9286 #[test]
9287 fn j_k_motion_does_not_push_jump() {
9288 let mut e = editor_with_rows(50, 20);
9289 e.jump_cursor(5, 0);
9290 run_keys(&mut e, "jjj");
9291 run_keys(&mut e, "<C-o>");
9292 assert_eq!(e.cursor().0, 8);
9293 }
9294
9295 #[test]
9296 fn jumplist_caps_at_100() {
9297 let mut e = editor_with_rows(200, 20);
9298 for i in 0..101 {
9299 e.jump_cursor(i, 0);
9300 run_keys(&mut e, "G");
9301 }
9302 assert!(e.vim.jump_back.len() <= 100);
9303 }
9304
9305 #[test]
9306 fn tab_acts_as_ctrl_i() {
9307 let mut e = editor_with_rows(50, 20);
9308 e.jump_cursor(5, 2);
9309 run_keys(&mut e, "G");
9310 let post = e.cursor();
9311 run_keys(&mut e, "<C-o>");
9312 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9313 assert_eq!(e.cursor(), post);
9314 }
9315
9316 #[test]
9319 fn ma_then_backtick_a_jumps_exact() {
9320 let mut e = editor_with_rows(50, 20);
9321 e.jump_cursor(5, 3);
9322 run_keys(&mut e, "ma");
9323 e.jump_cursor(20, 0);
9324 run_keys(&mut e, "`a");
9325 assert_eq!(e.cursor(), (5, 3));
9326 }
9327
9328 #[test]
9329 fn ma_then_apostrophe_a_lands_on_first_non_blank() {
9330 let mut e = editor_with_rows(50, 20);
9331 e.jump_cursor(5, 6);
9333 run_keys(&mut e, "ma");
9334 e.jump_cursor(30, 4);
9335 run_keys(&mut e, "'a");
9336 assert_eq!(e.cursor(), (5, 2));
9337 }
9338
9339 #[test]
9340 fn goto_mark_pushes_jumplist() {
9341 let mut e = editor_with_rows(50, 20);
9342 e.jump_cursor(10, 2);
9343 run_keys(&mut e, "mz");
9344 e.jump_cursor(3, 0);
9345 run_keys(&mut e, "`z");
9346 assert_eq!(e.cursor(), (10, 2));
9347 run_keys(&mut e, "<C-o>");
9348 assert_eq!(e.cursor(), (3, 0));
9349 }
9350
9351 #[test]
9352 fn goto_missing_mark_is_noop() {
9353 let mut e = editor_with_rows(50, 20);
9354 e.jump_cursor(3, 1);
9355 run_keys(&mut e, "`q");
9356 assert_eq!(e.cursor(), (3, 1));
9357 }
9358
9359 #[test]
9360 fn uppercase_mark_stored_under_uppercase_key() {
9361 let mut e = editor_with_rows(50, 20);
9362 e.jump_cursor(5, 3);
9363 run_keys(&mut e, "mA");
9364 assert_eq!(e.mark('A'), Some((5, 3)));
9367 assert!(e.mark('a').is_none());
9368 }
9369
9370 #[test]
9371 fn mark_survives_document_shrink_via_clamp() {
9372 let mut e = editor_with_rows(50, 20);
9373 e.jump_cursor(40, 4);
9374 run_keys(&mut e, "mx");
9375 e.set_content("a\nb\nc\nd\ne");
9377 run_keys(&mut e, "`x");
9378 let (r, _) = e.cursor();
9380 assert!(r <= 4);
9381 }
9382
9383 #[test]
9384 fn g_semicolon_walks_back_through_edits() {
9385 let mut e = editor_with("alpha\nbeta\ngamma");
9386 e.jump_cursor(0, 0);
9389 run_keys(&mut e, "iX<Esc>");
9390 e.jump_cursor(2, 0);
9391 run_keys(&mut e, "iY<Esc>");
9392 run_keys(&mut e, "g;");
9394 assert_eq!(e.cursor(), (2, 1));
9395 run_keys(&mut e, "g;");
9397 assert_eq!(e.cursor(), (0, 1));
9398 run_keys(&mut e, "g;");
9400 assert_eq!(e.cursor(), (0, 1));
9401 }
9402
9403 #[test]
9404 fn g_comma_walks_forward_after_g_semicolon() {
9405 let mut e = editor_with("a\nb\nc");
9406 e.jump_cursor(0, 0);
9407 run_keys(&mut e, "iX<Esc>");
9408 e.jump_cursor(2, 0);
9409 run_keys(&mut e, "iY<Esc>");
9410 run_keys(&mut e, "g;");
9411 run_keys(&mut e, "g;");
9412 assert_eq!(e.cursor(), (0, 1));
9413 run_keys(&mut e, "g,");
9414 assert_eq!(e.cursor(), (2, 1));
9415 }
9416
9417 #[test]
9418 fn new_edit_during_walk_trims_forward_entries() {
9419 let mut e = editor_with("a\nb\nc\nd");
9420 e.jump_cursor(0, 0);
9421 run_keys(&mut e, "iX<Esc>"); e.jump_cursor(2, 0);
9423 run_keys(&mut e, "iY<Esc>"); run_keys(&mut e, "g;");
9426 run_keys(&mut e, "g;");
9427 assert_eq!(e.cursor(), (0, 1));
9428 run_keys(&mut e, "iZ<Esc>");
9430 run_keys(&mut e, "g,");
9432 assert_ne!(e.cursor(), (2, 1));
9434 }
9435
9436 #[test]
9442 fn capital_mark_set_and_jump() {
9443 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
9444 e.jump_cursor(2, 1);
9445 run_keys(&mut e, "mA");
9446 e.jump_cursor(0, 0);
9448 run_keys(&mut e, "'A");
9450 assert_eq!(e.cursor().0, 2);
9452 }
9453
9454 #[test]
9455 fn capital_mark_survives_set_content() {
9456 let mut e = editor_with("first buffer line\nsecond");
9457 e.jump_cursor(1, 3);
9458 run_keys(&mut e, "mA");
9459 e.set_content("totally different content\non many\nrows of text");
9461 e.jump_cursor(0, 0);
9463 run_keys(&mut e, "'A");
9464 assert_eq!(e.cursor().0, 1);
9465 }
9466
9467 #[test]
9472 fn capital_mark_shifts_with_edit() {
9473 let mut e = editor_with("a\nb\nc\nd");
9474 e.jump_cursor(3, 0);
9475 run_keys(&mut e, "mA");
9476 e.jump_cursor(0, 0);
9478 run_keys(&mut e, "dd");
9479 e.jump_cursor(0, 0);
9480 run_keys(&mut e, "'A");
9481 assert_eq!(e.cursor().0, 2);
9482 }
9483
9484 #[test]
9485 fn mark_below_delete_shifts_up() {
9486 let mut e = editor_with("a\nb\nc\nd\ne");
9487 e.jump_cursor(3, 0);
9489 run_keys(&mut e, "ma");
9490 e.jump_cursor(0, 0);
9492 run_keys(&mut e, "dd");
9493 e.jump_cursor(0, 0);
9495 run_keys(&mut e, "'a");
9496 assert_eq!(e.cursor().0, 2);
9497 assert_eq!(e.buffer().line(2).unwrap(), "d");
9498 }
9499
9500 #[test]
9501 fn mark_on_deleted_row_is_dropped() {
9502 let mut e = editor_with("a\nb\nc\nd");
9503 e.jump_cursor(1, 0);
9505 run_keys(&mut e, "ma");
9506 run_keys(&mut e, "dd");
9508 e.jump_cursor(2, 0);
9510 run_keys(&mut e, "'a");
9511 assert_eq!(e.cursor().0, 2);
9513 }
9514
9515 #[test]
9516 fn mark_above_edit_unchanged() {
9517 let mut e = editor_with("a\nb\nc\nd\ne");
9518 e.jump_cursor(0, 0);
9520 run_keys(&mut e, "ma");
9521 e.jump_cursor(3, 0);
9523 run_keys(&mut e, "dd");
9524 e.jump_cursor(2, 0);
9526 run_keys(&mut e, "'a");
9527 assert_eq!(e.cursor().0, 0);
9528 }
9529
9530 #[test]
9531 fn mark_shifts_down_after_insert() {
9532 let mut e = editor_with("a\nb\nc");
9533 e.jump_cursor(2, 0);
9535 run_keys(&mut e, "ma");
9536 e.jump_cursor(0, 0);
9538 run_keys(&mut e, "Onew<Esc>");
9539 e.jump_cursor(0, 0);
9542 run_keys(&mut e, "'a");
9543 assert_eq!(e.cursor().0, 3);
9544 assert_eq!(e.buffer().line(3).unwrap(), "c");
9545 }
9546
9547 #[test]
9550 fn forward_search_commit_pushes_jump() {
9551 let mut e = editor_with("alpha beta\nfoo target end\nmore");
9552 e.jump_cursor(0, 0);
9553 run_keys(&mut e, "/target<CR>");
9554 assert_ne!(e.cursor(), (0, 0));
9556 run_keys(&mut e, "<C-o>");
9558 assert_eq!(e.cursor(), (0, 0));
9559 }
9560
9561 #[test]
9562 fn search_commit_no_match_does_not_push_jump() {
9563 let mut e = editor_with("alpha beta\nfoo end");
9564 e.jump_cursor(0, 3);
9565 let pre_len = e.vim.jump_back.len();
9566 run_keys(&mut e, "/zzznotfound<CR>");
9567 assert_eq!(e.vim.jump_back.len(), pre_len);
9569 }
9570
9571 #[test]
9574 fn buffer_cursor_mirrors_textarea_after_horizontal_motion() {
9575 let mut e = editor_with("hello world");
9576 run_keys(&mut e, "lll");
9577 let (row, col) = e.cursor();
9578 assert_eq!(e.buffer.cursor().row, row);
9579 assert_eq!(e.buffer.cursor().col, col);
9580 }
9581
9582 #[test]
9583 fn buffer_cursor_mirrors_textarea_after_vertical_motion() {
9584 let mut e = editor_with("aaaa\nbbbb\ncccc");
9585 run_keys(&mut e, "jj");
9586 let (row, col) = e.cursor();
9587 assert_eq!(e.buffer.cursor().row, row);
9588 assert_eq!(e.buffer.cursor().col, col);
9589 }
9590
9591 #[test]
9592 fn buffer_cursor_mirrors_textarea_after_word_motion() {
9593 let mut e = editor_with("foo bar baz");
9594 run_keys(&mut e, "ww");
9595 let (row, col) = e.cursor();
9596 assert_eq!(e.buffer.cursor().row, row);
9597 assert_eq!(e.buffer.cursor().col, col);
9598 }
9599
9600 #[test]
9601 fn buffer_cursor_mirrors_textarea_after_jump_motion() {
9602 let mut e = editor_with("a\nb\nc\nd\ne");
9603 run_keys(&mut e, "G");
9604 let (row, col) = e.cursor();
9605 assert_eq!(e.buffer.cursor().row, row);
9606 assert_eq!(e.buffer.cursor().col, col);
9607 }
9608
9609 #[test]
9610 fn editor_sticky_col_tracks_horizontal_motion() {
9611 let mut e = editor_with("longline\nhi\nlongline");
9612 run_keys(&mut e, "fl");
9617 let landed = e.cursor().1;
9618 assert!(landed > 0, "fl should have moved");
9619 run_keys(&mut e, "j");
9620 assert_eq!(e.sticky_col(), Some(landed));
9623 }
9624
9625 #[test]
9626 fn buffer_content_mirrors_textarea_after_insert() {
9627 let mut e = editor_with("hello");
9628 run_keys(&mut e, "iXYZ<Esc>");
9629 let text = e.buffer().lines().join("\n");
9630 assert_eq!(e.buffer.as_string(), text);
9631 }
9632
9633 #[test]
9634 fn buffer_content_mirrors_textarea_after_delete() {
9635 let mut e = editor_with("alpha bravo charlie");
9636 run_keys(&mut e, "dw");
9637 let text = e.buffer().lines().join("\n");
9638 assert_eq!(e.buffer.as_string(), text);
9639 }
9640
9641 #[test]
9642 fn buffer_content_mirrors_textarea_after_dd() {
9643 let mut e = editor_with("a\nb\nc\nd");
9644 run_keys(&mut e, "jdd");
9645 let text = e.buffer().lines().join("\n");
9646 assert_eq!(e.buffer.as_string(), text);
9647 }
9648
9649 #[test]
9650 fn buffer_content_mirrors_textarea_after_open_line() {
9651 let mut e = editor_with("foo\nbar");
9652 run_keys(&mut e, "oNEW<Esc>");
9653 let text = e.buffer().lines().join("\n");
9654 assert_eq!(e.buffer.as_string(), text);
9655 }
9656
9657 #[test]
9658 fn buffer_content_mirrors_textarea_after_paste() {
9659 let mut e = editor_with("hello");
9660 run_keys(&mut e, "yy");
9661 run_keys(&mut e, "p");
9662 let text = e.buffer().lines().join("\n");
9663 assert_eq!(e.buffer.as_string(), text);
9664 }
9665
9666 #[test]
9667 fn buffer_selection_none_in_normal_mode() {
9668 let e = editor_with("foo bar");
9669 assert!(e.buffer_selection().is_none());
9670 }
9671
9672 #[test]
9673 fn buffer_selection_char_in_visual_mode() {
9674 use hjkl_buffer::{Position, Selection};
9675 let mut e = editor_with("hello world");
9676 run_keys(&mut e, "vlll");
9677 assert_eq!(
9678 e.buffer_selection(),
9679 Some(Selection::Char {
9680 anchor: Position::new(0, 0),
9681 head: Position::new(0, 3),
9682 })
9683 );
9684 }
9685
9686 #[test]
9687 fn buffer_selection_line_in_visual_line_mode() {
9688 use hjkl_buffer::Selection;
9689 let mut e = editor_with("a\nb\nc\nd");
9690 run_keys(&mut e, "Vj");
9691 assert_eq!(
9692 e.buffer_selection(),
9693 Some(Selection::Line {
9694 anchor_row: 0,
9695 head_row: 1,
9696 })
9697 );
9698 }
9699
9700 #[test]
9701 fn wrapscan_off_blocks_wrap_around() {
9702 let mut e = editor_with("first\nsecond\nthird\n");
9703 e.settings_mut().wrapscan = false;
9704 e.jump_cursor(2, 0);
9706 run_keys(&mut e, "/first<CR>");
9707 assert_eq!(e.cursor().0, 2, "wrapscan off should block wrap");
9709 e.settings_mut().wrapscan = true;
9711 run_keys(&mut e, "/first<CR>");
9712 assert_eq!(e.cursor().0, 0, "wrapscan on should wrap to row 0");
9713 }
9714
9715 #[test]
9716 fn smartcase_uppercase_pattern_stays_sensitive() {
9717 let mut e = editor_with("foo\nFoo\nBAR\n");
9718 e.settings_mut().ignore_case = true;
9719 e.settings_mut().smartcase = true;
9720 run_keys(&mut e, "/foo<CR>");
9723 let r1 = e
9724 .search_state()
9725 .pattern
9726 .as_ref()
9727 .unwrap()
9728 .as_str()
9729 .to_string();
9730 assert!(r1.starts_with("(?i)"), "lowercase under smartcase: {r1}");
9731 run_keys(&mut e, "/Foo<CR>");
9733 let r2 = e
9734 .search_state()
9735 .pattern
9736 .as_ref()
9737 .unwrap()
9738 .as_str()
9739 .to_string();
9740 assert!(!r2.starts_with("(?i)"), "mixed-case under smartcase: {r2}");
9741 }
9742
9743 #[test]
9744 fn enter_with_autoindent_copies_leading_whitespace() {
9745 let mut e = editor_with(" foo");
9746 e.jump_cursor(0, 7);
9747 run_keys(&mut e, "i<CR>");
9748 assert_eq!(e.buffer.line(1).unwrap(), " ");
9749 }
9750
9751 #[test]
9752 fn enter_without_autoindent_inserts_bare_newline() {
9753 let mut e = editor_with(" foo");
9754 e.settings_mut().autoindent = false;
9755 e.jump_cursor(0, 7);
9756 run_keys(&mut e, "i<CR>");
9757 assert_eq!(e.buffer.line(1).unwrap(), "");
9758 }
9759
9760 #[test]
9761 fn iskeyword_default_treats_alnum_underscore_as_word() {
9762 let mut e = editor_with("foo_bar baz");
9763 e.jump_cursor(0, 0);
9767 run_keys(&mut e, "*");
9768 let p = e
9769 .search_state()
9770 .pattern
9771 .as_ref()
9772 .unwrap()
9773 .as_str()
9774 .to_string();
9775 assert!(p.contains("foo_bar"), "default iskeyword: {p}");
9776 }
9777
9778 #[test]
9779 fn w_motion_respects_custom_iskeyword() {
9780 let mut e = editor_with("foo-bar baz");
9784 run_keys(&mut e, "w");
9785 assert_eq!(e.cursor().1, 3, "default iskeyword: {:?}", e.cursor());
9786 let mut e2 = editor_with("foo-bar baz");
9789 e2.set_iskeyword("@,_,45");
9790 run_keys(&mut e2, "w");
9791 assert_eq!(e2.cursor().1, 8, "dash-as-word: {:?}", e2.cursor());
9792 }
9793
9794 #[test]
9795 fn iskeyword_with_dash_treats_dash_as_word_char() {
9796 let mut e = editor_with("foo-bar baz");
9797 e.settings_mut().iskeyword = "@,_,45".to_string();
9798 e.jump_cursor(0, 0);
9799 run_keys(&mut e, "*");
9800 let p = e
9801 .search_state()
9802 .pattern
9803 .as_ref()
9804 .unwrap()
9805 .as_str()
9806 .to_string();
9807 assert!(p.contains("foo-bar"), "dash-as-word: {p}");
9808 }
9809
9810 #[test]
9811 fn timeoutlen_drops_pending_g_prefix() {
9812 use std::time::{Duration, Instant};
9813 let mut e = editor_with("a\nb\nc");
9814 e.jump_cursor(2, 0);
9815 run_keys(&mut e, "g");
9817 assert!(matches!(e.vim.pending, super::Pending::G));
9818 e.settings.timeout_len = Duration::from_nanos(0);
9826 e.vim.last_input_at = Some(Instant::now() - Duration::from_secs(60));
9827 e.vim.last_input_host_at = Some(Duration::ZERO);
9828 run_keys(&mut e, "g");
9832 assert_eq!(e.cursor().0, 2, "timeout must abandon g-prefix");
9834 }
9835
9836 #[test]
9837 fn undobreak_on_breaks_group_at_arrow_motion() {
9838 let mut e = editor_with("");
9839 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
9841 let line = e.buffer.line(0).unwrap_or("").to_string();
9844 assert!(line.contains("aaa"), "after undobreak: {line:?}");
9845 assert!(!line.contains("bbb"), "bbb should be undone: {line:?}");
9846 }
9847
9848 #[test]
9849 fn undobreak_off_keeps_full_run_in_one_group() {
9850 let mut e = editor_with("");
9851 e.settings_mut().undo_break_on_motion = false;
9852 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
9853 assert_eq!(e.buffer.line(0).unwrap_or(""), "");
9856 }
9857
9858 #[test]
9859 fn undobreak_round_trips_through_options() {
9860 let e = editor_with("");
9861 let opts = e.current_options();
9862 assert!(opts.undo_break_on_motion);
9863 let mut e2 = editor_with("");
9864 let mut new_opts = opts.clone();
9865 new_opts.undo_break_on_motion = false;
9866 e2.apply_options(&new_opts);
9867 assert!(!e2.current_options().undo_break_on_motion);
9868 }
9869
9870 #[test]
9871 fn undo_levels_cap_drops_oldest() {
9872 let mut e = editor_with("abcde");
9873 e.settings_mut().undo_levels = 3;
9874 run_keys(&mut e, "ra");
9875 run_keys(&mut e, "lrb");
9876 run_keys(&mut e, "lrc");
9877 run_keys(&mut e, "lrd");
9878 run_keys(&mut e, "lre");
9879 assert_eq!(e.undo_stack_len(), 3);
9880 }
9881
9882 #[test]
9883 fn tab_inserts_literal_tab_when_noexpandtab() {
9884 let mut e = editor_with("");
9885 e.settings_mut().expandtab = false;
9888 e.settings_mut().softtabstop = 0;
9889 run_keys(&mut e, "i");
9890 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9891 assert_eq!(e.buffer.line(0).unwrap(), "\t");
9892 }
9893
9894 #[test]
9895 fn tab_inserts_spaces_when_expandtab() {
9896 let mut e = editor_with("");
9897 e.settings_mut().expandtab = true;
9898 e.settings_mut().tabstop = 4;
9899 run_keys(&mut e, "i");
9900 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9901 assert_eq!(e.buffer.line(0).unwrap(), " ");
9902 }
9903
9904 #[test]
9905 fn tab_with_softtabstop_fills_to_next_boundary() {
9906 let mut e = editor_with("ab");
9908 e.settings_mut().expandtab = true;
9909 e.settings_mut().tabstop = 8;
9910 e.settings_mut().softtabstop = 4;
9911 run_keys(&mut e, "A"); e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9913 assert_eq!(e.buffer.line(0).unwrap(), "ab ");
9914 }
9915
9916 #[test]
9917 fn backspace_deletes_softtab_run() {
9918 let mut e = editor_with(" x");
9921 e.settings_mut().softtabstop = 4;
9922 run_keys(&mut e, "fxi");
9924 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
9925 assert_eq!(e.buffer.line(0).unwrap(), "x");
9926 }
9927
9928 #[test]
9929 fn backspace_falls_back_to_single_char_when_run_not_aligned() {
9930 let mut e = editor_with(" x");
9933 e.settings_mut().softtabstop = 4;
9934 run_keys(&mut e, "fxi");
9935 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
9936 assert_eq!(e.buffer.line(0).unwrap(), " x");
9937 }
9938
9939 #[test]
9940 fn readonly_blocks_insert_mutation() {
9941 let mut e = editor_with("hello");
9942 e.settings_mut().readonly = true;
9943 run_keys(&mut e, "iX<Esc>");
9944 assert_eq!(e.buffer.line(0).unwrap(), "hello");
9945 }
9946
9947 #[cfg(feature = "ratatui")]
9948 #[test]
9949 fn intern_ratatui_style_dedups_repeated_styles() {
9950 use ratatui::style::{Color, Style};
9951 let mut e = editor_with("");
9952 let red = Style::default().fg(Color::Red);
9953 let blue = Style::default().fg(Color::Blue);
9954 let id_r1 = e.intern_ratatui_style(red);
9955 let id_r2 = e.intern_ratatui_style(red);
9956 let id_b = e.intern_ratatui_style(blue);
9957 assert_eq!(id_r1, id_r2);
9958 assert_ne!(id_r1, id_b);
9959 assert_eq!(e.style_table().len(), 2);
9960 }
9961
9962 #[cfg(feature = "ratatui")]
9963 #[test]
9964 fn install_ratatui_syntax_spans_translates_styled_spans() {
9965 use ratatui::style::{Color, Style};
9966 let mut e = editor_with("SELECT foo");
9967 e.install_ratatui_syntax_spans(vec![vec![(0, 6, Style::default().fg(Color::Red))]]);
9968 let by_row = e.buffer_spans();
9969 assert_eq!(by_row.len(), 1);
9970 assert_eq!(by_row[0].len(), 1);
9971 assert_eq!(by_row[0][0].start_byte, 0);
9972 assert_eq!(by_row[0][0].end_byte, 6);
9973 let id = by_row[0][0].style;
9974 assert_eq!(e.style_table()[id as usize].fg, Some(Color::Red));
9975 }
9976
9977 #[cfg(feature = "ratatui")]
9978 #[test]
9979 fn install_ratatui_syntax_spans_clamps_sentinel_end() {
9980 use ratatui::style::{Color, Style};
9981 let mut e = editor_with("hello");
9982 e.install_ratatui_syntax_spans(vec![vec![(
9983 0,
9984 usize::MAX,
9985 Style::default().fg(Color::Blue),
9986 )]]);
9987 let by_row = e.buffer_spans();
9988 assert_eq!(by_row[0][0].end_byte, 5);
9989 }
9990
9991 #[cfg(feature = "ratatui")]
9992 #[test]
9993 fn install_ratatui_syntax_spans_drops_zero_width() {
9994 use ratatui::style::{Color, Style};
9995 let mut e = editor_with("abc");
9996 e.install_ratatui_syntax_spans(vec![vec![(2, 2, Style::default().fg(Color::Red))]]);
9997 assert!(e.buffer_spans()[0].is_empty());
9998 }
9999
10000 #[test]
10001 fn named_register_yank_into_a_then_paste_from_a() {
10002 let mut e = editor_with("hello world\nsecond");
10003 run_keys(&mut e, "\"ayw");
10004 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
10006 run_keys(&mut e, "j0\"aP");
10008 assert_eq!(e.buffer().lines()[1], "hello second");
10009 }
10010
10011 #[test]
10012 fn capital_r_overstrikes_chars() {
10013 let mut e = editor_with("hello");
10014 e.jump_cursor(0, 0);
10015 run_keys(&mut e, "RXY<Esc>");
10016 assert_eq!(e.buffer().lines()[0], "XYllo");
10018 }
10019
10020 #[test]
10021 fn capital_r_at_eol_appends() {
10022 let mut e = editor_with("hi");
10023 e.jump_cursor(0, 1);
10024 run_keys(&mut e, "RXYZ<Esc>");
10026 assert_eq!(e.buffer().lines()[0], "hXYZ");
10027 }
10028
10029 #[test]
10030 fn capital_r_count_does_not_repeat_overstrike_char_by_char() {
10031 let mut e = editor_with("abc");
10035 e.jump_cursor(0, 0);
10036 run_keys(&mut e, "RX<Esc>");
10037 assert_eq!(e.buffer().lines()[0], "Xbc");
10038 }
10039
10040 #[test]
10041 fn ctrl_r_in_insert_pastes_named_register() {
10042 let mut e = editor_with("hello world");
10043 run_keys(&mut e, "\"ayw");
10045 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
10046 run_keys(&mut e, "o");
10048 assert_eq!(e.vim_mode(), VimMode::Insert);
10049 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
10050 e.handle_key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
10051 assert_eq!(e.buffer().lines()[1], "hello ");
10052 assert_eq!(e.cursor(), (1, 6));
10054 assert_eq!(e.vim_mode(), VimMode::Insert);
10056 e.handle_key(KeyEvent::new(KeyCode::Char('X'), KeyModifiers::NONE));
10057 assert_eq!(e.buffer().lines()[1], "hello X");
10058 }
10059
10060 #[test]
10061 fn ctrl_r_with_unnamed_register() {
10062 let mut e = editor_with("foo");
10063 run_keys(&mut e, "yiw");
10064 run_keys(&mut e, "A ");
10065 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
10067 e.handle_key(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE));
10068 assert_eq!(e.buffer().lines()[0], "foo foo");
10069 }
10070
10071 #[test]
10072 fn ctrl_r_unknown_selector_is_no_op() {
10073 let mut e = editor_with("abc");
10074 run_keys(&mut e, "A");
10075 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
10076 e.handle_key(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE));
10079 e.handle_key(KeyEvent::new(KeyCode::Char('Z'), KeyModifiers::NONE));
10080 assert_eq!(e.buffer().lines()[0], "abcZ");
10081 }
10082
10083 #[test]
10084 fn ctrl_r_multiline_register_pastes_with_newlines() {
10085 let mut e = editor_with("alpha\nbeta\ngamma");
10086 run_keys(&mut e, "\"byy");
10088 run_keys(&mut e, "j\"byy");
10089 run_keys(&mut e, "ggVj\"by");
10093 let payload = e.registers().read('b').unwrap().text.clone();
10094 assert!(payload.contains('\n'));
10095 run_keys(&mut e, "Go");
10096 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
10097 e.handle_key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));
10098 let total_lines = e.buffer().lines().len();
10101 assert!(total_lines >= 5);
10102 }
10103
10104 #[test]
10105 fn yank_zero_holds_last_yank_after_delete() {
10106 let mut e = editor_with("hello world");
10107 run_keys(&mut e, "yw");
10108 let yanked = e.registers().read('0').unwrap().text.clone();
10109 assert!(!yanked.is_empty());
10110 run_keys(&mut e, "dw");
10112 assert_eq!(e.registers().read('0').unwrap().text, yanked);
10113 assert!(!e.registers().read('1').unwrap().text.is_empty());
10115 }
10116
10117 #[test]
10118 fn delete_ring_rotates_through_one_through_nine() {
10119 let mut e = editor_with("a b c d e f g h i j");
10120 for _ in 0..3 {
10122 run_keys(&mut e, "dw");
10123 }
10124 let r1 = e.registers().read('1').unwrap().text.clone();
10126 let r2 = e.registers().read('2').unwrap().text.clone();
10127 let r3 = e.registers().read('3').unwrap().text.clone();
10128 assert!(!r1.is_empty() && !r2.is_empty() && !r3.is_empty());
10129 assert_ne!(r1, r2);
10130 assert_ne!(r2, r3);
10131 }
10132
10133 #[test]
10134 fn capital_register_appends_to_lowercase() {
10135 let mut e = editor_with("foo bar");
10136 run_keys(&mut e, "\"ayw");
10137 let first = e.registers().read('a').unwrap().text.clone();
10138 assert!(first.contains("foo"));
10139 run_keys(&mut e, "w\"Ayw");
10141 let combined = e.registers().read('a').unwrap().text.clone();
10142 assert!(combined.starts_with(&first));
10143 assert!(combined.contains("bar"));
10144 }
10145
10146 #[test]
10147 fn zf_in_visual_line_creates_closed_fold() {
10148 let mut e = editor_with("a\nb\nc\nd\ne");
10149 e.jump_cursor(1, 0);
10151 run_keys(&mut e, "Vjjzf");
10152 assert_eq!(e.buffer().folds().len(), 1);
10153 let f = e.buffer().folds()[0];
10154 assert_eq!(f.start_row, 1);
10155 assert_eq!(f.end_row, 3);
10156 assert!(f.closed);
10157 }
10158
10159 #[test]
10160 fn zfj_in_normal_creates_two_row_fold() {
10161 let mut e = editor_with("a\nb\nc\nd\ne");
10162 e.jump_cursor(1, 0);
10163 run_keys(&mut e, "zfj");
10164 assert_eq!(e.buffer().folds().len(), 1);
10165 let f = e.buffer().folds()[0];
10166 assert_eq!(f.start_row, 1);
10167 assert_eq!(f.end_row, 2);
10168 assert!(f.closed);
10169 assert_eq!(e.cursor().0, 1);
10171 }
10172
10173 #[test]
10174 fn zf_with_count_folds_count_rows() {
10175 let mut e = editor_with("a\nb\nc\nd\ne\nf");
10176 e.jump_cursor(0, 0);
10177 run_keys(&mut e, "zf3j");
10179 assert_eq!(e.buffer().folds().len(), 1);
10180 let f = e.buffer().folds()[0];
10181 assert_eq!(f.start_row, 0);
10182 assert_eq!(f.end_row, 3);
10183 }
10184
10185 #[test]
10186 fn zfk_folds_upward_range() {
10187 let mut e = editor_with("a\nb\nc\nd\ne");
10188 e.jump_cursor(3, 0);
10189 run_keys(&mut e, "zfk");
10190 let f = e.buffer().folds()[0];
10191 assert_eq!(f.start_row, 2);
10193 assert_eq!(f.end_row, 3);
10194 }
10195
10196 #[test]
10197 fn zf_capital_g_folds_to_bottom() {
10198 let mut e = editor_with("a\nb\nc\nd\ne");
10199 e.jump_cursor(1, 0);
10200 run_keys(&mut e, "zfG");
10202 let f = e.buffer().folds()[0];
10203 assert_eq!(f.start_row, 1);
10204 assert_eq!(f.end_row, 4);
10205 }
10206
10207 #[test]
10208 fn zfgg_folds_to_top_via_operator_pipeline() {
10209 let mut e = editor_with("a\nb\nc\nd\ne");
10210 e.jump_cursor(3, 0);
10211 run_keys(&mut e, "zfgg");
10215 let f = e.buffer().folds()[0];
10216 assert_eq!(f.start_row, 0);
10217 assert_eq!(f.end_row, 3);
10218 }
10219
10220 #[test]
10221 fn zfip_folds_paragraph_via_text_object() {
10222 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta\nepsilon");
10223 e.jump_cursor(1, 0);
10224 run_keys(&mut e, "zfip");
10226 assert_eq!(e.buffer().folds().len(), 1);
10227 let f = e.buffer().folds()[0];
10228 assert_eq!(f.start_row, 0);
10229 assert_eq!(f.end_row, 2);
10230 }
10231
10232 #[test]
10233 fn zfap_folds_paragraph_with_trailing_blank() {
10234 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta");
10235 e.jump_cursor(0, 0);
10236 run_keys(&mut e, "zfap");
10238 let f = e.buffer().folds()[0];
10239 assert_eq!(f.start_row, 0);
10240 assert_eq!(f.end_row, 3);
10241 }
10242
10243 #[test]
10244 fn zf_paragraph_motion_folds_to_blank() {
10245 let mut e = editor_with("alpha\nbeta\n\ngamma");
10246 e.jump_cursor(0, 0);
10247 run_keys(&mut e, "zf}");
10249 let f = e.buffer().folds()[0];
10250 assert_eq!(f.start_row, 0);
10251 assert_eq!(f.end_row, 2);
10252 }
10253
10254 #[test]
10255 fn za_toggles_fold_under_cursor() {
10256 let mut e = editor_with("a\nb\nc\nd");
10257 e.buffer_mut().add_fold(1, 2, true);
10258 e.jump_cursor(1, 0);
10259 run_keys(&mut e, "za");
10260 assert!(!e.buffer().folds()[0].closed);
10261 run_keys(&mut e, "za");
10262 assert!(e.buffer().folds()[0].closed);
10263 }
10264
10265 #[test]
10266 fn zr_opens_all_folds_zm_closes_all() {
10267 let mut e = editor_with("a\nb\nc\nd\ne\nf");
10268 e.buffer_mut().add_fold(0, 1, true);
10269 e.buffer_mut().add_fold(2, 3, true);
10270 e.buffer_mut().add_fold(4, 5, true);
10271 run_keys(&mut e, "zR");
10272 assert!(e.buffer().folds().iter().all(|f| !f.closed));
10273 run_keys(&mut e, "zM");
10274 assert!(e.buffer().folds().iter().all(|f| f.closed));
10275 }
10276
10277 #[test]
10278 fn ze_clears_all_folds() {
10279 let mut e = editor_with("a\nb\nc\nd");
10280 e.buffer_mut().add_fold(0, 1, true);
10281 e.buffer_mut().add_fold(2, 3, false);
10282 run_keys(&mut e, "zE");
10283 assert!(e.buffer().folds().is_empty());
10284 }
10285
10286 #[test]
10287 fn g_underscore_jumps_to_last_non_blank() {
10288 let mut e = editor_with("hello world ");
10289 run_keys(&mut e, "g_");
10290 assert_eq!(e.cursor().1, 10);
10292 }
10293
10294 #[test]
10295 fn gj_and_gk_alias_j_and_k() {
10296 let mut e = editor_with("a\nb\nc");
10297 run_keys(&mut e, "gj");
10298 assert_eq!(e.cursor().0, 1);
10299 run_keys(&mut e, "gk");
10300 assert_eq!(e.cursor().0, 0);
10301 }
10302
10303 #[test]
10304 fn paragraph_motions_walk_blank_lines() {
10305 let mut e = editor_with("first\nblock\n\nsecond\nblock\n\nthird");
10306 run_keys(&mut e, "}");
10307 assert_eq!(e.cursor().0, 2);
10308 run_keys(&mut e, "}");
10309 assert_eq!(e.cursor().0, 5);
10310 run_keys(&mut e, "{");
10311 assert_eq!(e.cursor().0, 2);
10312 }
10313
10314 #[test]
10315 fn gv_reenters_last_visual_selection() {
10316 let mut e = editor_with("alpha\nbeta\ngamma");
10317 run_keys(&mut e, "Vj");
10318 run_keys(&mut e, "<Esc>");
10320 assert_eq!(e.vim_mode(), VimMode::Normal);
10321 run_keys(&mut e, "gv");
10323 assert_eq!(e.vim_mode(), VimMode::VisualLine);
10324 }
10325
10326 #[test]
10327 fn o_in_visual_swaps_anchor_and_cursor() {
10328 let mut e = editor_with("hello world");
10329 run_keys(&mut e, "vllll");
10331 assert_eq!(e.cursor().1, 4);
10332 run_keys(&mut e, "o");
10334 assert_eq!(e.cursor().1, 0);
10335 assert_eq!(e.vim.visual_anchor, (0, 4));
10337 }
10338
10339 #[test]
10340 fn editing_inside_fold_invalidates_it() {
10341 let mut e = editor_with("a\nb\nc\nd");
10342 e.buffer_mut().add_fold(1, 2, true);
10343 e.jump_cursor(1, 0);
10344 run_keys(&mut e, "iX<Esc>");
10346 assert!(e.buffer().folds().is_empty());
10348 }
10349
10350 #[test]
10351 fn zd_removes_fold_under_cursor() {
10352 let mut e = editor_with("a\nb\nc\nd");
10353 e.buffer_mut().add_fold(1, 2, true);
10354 e.jump_cursor(2, 0);
10355 run_keys(&mut e, "zd");
10356 assert!(e.buffer().folds().is_empty());
10357 }
10358
10359 #[test]
10360 fn take_fold_ops_observes_z_keystroke_dispatch() {
10361 use crate::types::FoldOp;
10366 let mut e = editor_with("a\nb\nc\nd");
10367 e.buffer_mut().add_fold(1, 2, true);
10368 e.jump_cursor(1, 0);
10369 let _ = e.take_fold_ops();
10372 run_keys(&mut e, "zo");
10373 run_keys(&mut e, "zM");
10374 let ops = e.take_fold_ops();
10375 assert_eq!(ops.len(), 2);
10376 assert!(matches!(ops[0], FoldOp::OpenAt(1)));
10377 assert!(matches!(ops[1], FoldOp::CloseAll));
10378 assert!(e.take_fold_ops().is_empty());
10380 }
10381
10382 #[test]
10383 fn edit_pipeline_emits_invalidate_fold_op() {
10384 use crate::types::FoldOp;
10387 let mut e = editor_with("a\nb\nc\nd");
10388 e.buffer_mut().add_fold(1, 2, true);
10389 e.jump_cursor(1, 0);
10390 let _ = e.take_fold_ops();
10391 run_keys(&mut e, "iX<Esc>");
10392 let ops = e.take_fold_ops();
10393 assert!(
10394 ops.iter().any(|op| matches!(op, FoldOp::Invalidate { .. })),
10395 "expected at least one Invalidate op, got {ops:?}"
10396 );
10397 }
10398
10399 #[test]
10400 fn dot_mark_jumps_to_last_edit_position() {
10401 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
10402 e.jump_cursor(2, 0);
10403 run_keys(&mut e, "iX<Esc>");
10405 let after_edit = e.cursor();
10406 run_keys(&mut e, "gg");
10408 assert_eq!(e.cursor().0, 0);
10409 run_keys(&mut e, "'.");
10411 assert_eq!(e.cursor().0, after_edit.0);
10412 }
10413
10414 #[test]
10415 fn quote_quote_returns_to_pre_jump_position() {
10416 let mut e = editor_with_rows(50, 20);
10417 e.jump_cursor(10, 2);
10418 let before = e.cursor();
10419 run_keys(&mut e, "G");
10421 assert_ne!(e.cursor(), before);
10422 run_keys(&mut e, "''");
10424 assert_eq!(e.cursor().0, before.0);
10425 }
10426
10427 #[test]
10428 fn backtick_backtick_restores_exact_pre_jump_pos() {
10429 let mut e = editor_with_rows(50, 20);
10430 e.jump_cursor(7, 3);
10431 let before = e.cursor();
10432 run_keys(&mut e, "G");
10433 run_keys(&mut e, "``");
10434 assert_eq!(e.cursor(), before);
10435 }
10436
10437 #[test]
10438 fn macro_record_and_replay_basic() {
10439 let mut e = editor_with("foo\nbar\nbaz");
10440 run_keys(&mut e, "qaIX<Esc>jq");
10442 assert_eq!(e.buffer().lines()[0], "Xfoo");
10443 run_keys(&mut e, "@a");
10445 assert_eq!(e.buffer().lines()[1], "Xbar");
10446 run_keys(&mut e, "j@@");
10448 assert_eq!(e.buffer().lines()[2], "Xbaz");
10449 }
10450
10451 #[test]
10452 fn macro_count_replays_n_times() {
10453 let mut e = editor_with("a\nb\nc\nd\ne");
10454 run_keys(&mut e, "qajq");
10456 assert_eq!(e.cursor().0, 1);
10457 run_keys(&mut e, "3@a");
10459 assert_eq!(e.cursor().0, 4);
10460 }
10461
10462 #[test]
10463 fn macro_capital_q_appends_to_lowercase_register() {
10464 let mut e = editor_with("hello");
10465 run_keys(&mut e, "qall<Esc>q");
10466 run_keys(&mut e, "qAhh<Esc>q");
10467 let text = e.registers().read('a').unwrap().text.clone();
10470 assert!(text.contains("ll<Esc>"));
10471 assert!(text.contains("hh<Esc>"));
10472 }
10473
10474 #[test]
10475 fn buffer_selection_block_in_visual_block_mode() {
10476 use hjkl_buffer::{Position, Selection};
10477 let mut e = editor_with("aaaa\nbbbb\ncccc");
10478 run_keys(&mut e, "<C-v>jl");
10479 assert_eq!(
10480 e.buffer_selection(),
10481 Some(Selection::Block {
10482 anchor: Position::new(0, 0),
10483 head: Position::new(1, 1),
10484 })
10485 );
10486 }
10487
10488 #[test]
10491 fn n_after_question_mark_keeps_walking_backward() {
10492 let mut e = editor_with("foo bar foo baz foo end");
10495 e.jump_cursor(0, 22);
10496 run_keys(&mut e, "?foo<CR>");
10497 assert_eq!(e.cursor().1, 16);
10498 run_keys(&mut e, "n");
10499 assert_eq!(e.cursor().1, 8);
10500 run_keys(&mut e, "N");
10501 assert_eq!(e.cursor().1, 16);
10502 }
10503
10504 #[test]
10505 fn nested_macro_chord_records_literal_keys() {
10506 let mut e = editor_with("alpha\nbeta\ngamma");
10509 run_keys(&mut e, "qblq");
10511 run_keys(&mut e, "qaIX<Esc>q");
10514 e.jump_cursor(1, 0);
10516 run_keys(&mut e, "@a");
10517 assert_eq!(e.buffer().lines()[1], "Xbeta");
10518 }
10519
10520 #[test]
10521 fn shift_gt_motion_indents_one_line() {
10522 let mut e = editor_with("hello world");
10526 run_keys(&mut e, ">w");
10527 assert_eq!(e.buffer().lines()[0], " hello world");
10528 }
10529
10530 #[test]
10531 fn shift_lt_motion_outdents_one_line() {
10532 let mut e = editor_with(" hello world");
10533 run_keys(&mut e, "<lt>w");
10534 assert_eq!(e.buffer().lines()[0], " hello world");
10536 }
10537
10538 #[test]
10539 fn shift_gt_text_object_indents_paragraph() {
10540 let mut e = editor_with("alpha\nbeta\ngamma\n\nrest");
10541 e.jump_cursor(0, 0);
10542 run_keys(&mut e, ">ip");
10543 assert_eq!(e.buffer().lines()[0], " alpha");
10544 assert_eq!(e.buffer().lines()[1], " beta");
10545 assert_eq!(e.buffer().lines()[2], " gamma");
10546 assert_eq!(e.buffer().lines()[4], "rest");
10548 }
10549
10550 #[test]
10551 fn ctrl_o_runs_exactly_one_normal_command() {
10552 let mut e = editor_with("alpha beta gamma");
10555 e.jump_cursor(0, 0);
10556 run_keys(&mut e, "i");
10557 e.handle_key(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL));
10558 run_keys(&mut e, "dw");
10559 assert_eq!(e.vim_mode(), VimMode::Insert);
10561 run_keys(&mut e, "X");
10563 assert_eq!(e.buffer().lines()[0], "Xbeta gamma");
10564 }
10565
10566 #[test]
10567 fn macro_replay_respects_mode_switching() {
10568 let mut e = editor_with("hi");
10572 run_keys(&mut e, "qaiX<Esc>0q");
10573 assert_eq!(e.vim_mode(), VimMode::Normal);
10574 e.set_content("yo");
10576 run_keys(&mut e, "@a");
10577 assert_eq!(e.vim_mode(), VimMode::Normal);
10578 assert_eq!(e.cursor().1, 0);
10579 assert_eq!(e.buffer().lines()[0], "Xyo");
10580 }
10581
10582 #[test]
10583 fn macro_recorded_text_round_trips_through_register() {
10584 let mut e = editor_with("");
10588 run_keys(&mut e, "qaiX<Esc>q");
10589 let text = e.registers().read('a').unwrap().text.clone();
10590 assert!(text.starts_with("iX"));
10591 run_keys(&mut e, "@a");
10593 assert_eq!(e.buffer().lines()[0], "XX");
10594 }
10595
10596 #[test]
10597 fn dot_after_macro_replays_macros_last_change() {
10598 let mut e = editor_with("ab\ncd\nef");
10601 run_keys(&mut e, "qaIX<Esc>jq");
10604 assert_eq!(e.buffer().lines()[0], "Xab");
10605 run_keys(&mut e, "@a");
10606 assert_eq!(e.buffer().lines()[1], "Xcd");
10607 let row_before_dot = e.cursor().0;
10610 run_keys(&mut e, ".");
10611 assert!(e.buffer().lines()[row_before_dot].starts_with('X'));
10612 }
10613
10614 fn si_editor(content: &str) -> Editor {
10620 let opts = crate::types::Options {
10621 shiftwidth: 4,
10622 softtabstop: 4,
10623 expandtab: true,
10624 smartindent: true,
10625 autoindent: true,
10626 ..crate::types::Options::default()
10627 };
10628 let mut e = Editor::new(
10629 hjkl_buffer::Buffer::new(),
10630 crate::types::DefaultHost::new(),
10631 opts,
10632 );
10633 e.set_content(content);
10634 e
10635 }
10636
10637 #[test]
10638 fn smartindent_bumps_indent_after_open_brace() {
10639 let mut e = si_editor("fn foo() {");
10641 e.jump_cursor(0, 10); run_keys(&mut e, "i<CR>");
10643 assert_eq!(
10644 e.buffer().lines()[1],
10645 " ",
10646 "smartindent should bump one shiftwidth after {{"
10647 );
10648 }
10649
10650 #[test]
10651 fn smartindent_no_bump_when_off() {
10652 let mut e = si_editor("fn foo() {");
10655 e.settings_mut().smartindent = false;
10656 e.jump_cursor(0, 10);
10657 run_keys(&mut e, "i<CR>");
10658 assert_eq!(
10659 e.buffer().lines()[1],
10660 "",
10661 "without smartindent, no bump: new line copies empty leading ws"
10662 );
10663 }
10664
10665 #[test]
10666 fn smartindent_uses_tab_when_noexpandtab() {
10667 let opts = crate::types::Options {
10669 shiftwidth: 4,
10670 softtabstop: 0,
10671 expandtab: false,
10672 smartindent: true,
10673 autoindent: true,
10674 ..crate::types::Options::default()
10675 };
10676 let mut e = Editor::new(
10677 hjkl_buffer::Buffer::new(),
10678 crate::types::DefaultHost::new(),
10679 opts,
10680 );
10681 e.set_content("fn foo() {");
10682 e.jump_cursor(0, 10);
10683 run_keys(&mut e, "i<CR>");
10684 assert_eq!(
10685 e.buffer().lines()[1],
10686 "\t",
10687 "noexpandtab: smartindent bump inserts a literal tab"
10688 );
10689 }
10690
10691 #[test]
10692 fn smartindent_dedent_on_close_brace() {
10693 let mut e = si_editor("fn foo() {");
10696 e.set_content("fn foo() {\n ");
10698 e.jump_cursor(1, 4); run_keys(&mut e, "i}");
10700 assert_eq!(
10701 e.buffer().lines()[1],
10702 "}",
10703 "close brace on whitespace-only line should dedent"
10704 );
10705 assert_eq!(e.cursor(), (1, 1), "cursor should be after the `}}`");
10706 }
10707
10708 #[test]
10709 fn smartindent_no_dedent_when_off() {
10710 let mut e = si_editor("fn foo() {\n ");
10712 e.settings_mut().smartindent = false;
10713 e.jump_cursor(1, 4);
10714 run_keys(&mut e, "i}");
10715 assert_eq!(
10716 e.buffer().lines()[1],
10717 " }",
10718 "without smartindent, `}}` just appends at cursor"
10719 );
10720 }
10721
10722 #[test]
10723 fn smartindent_no_dedent_mid_line() {
10724 let mut e = si_editor(" let x = 1");
10727 e.jump_cursor(0, 13); run_keys(&mut e, "i}");
10729 assert_eq!(
10730 e.buffer().lines()[0],
10731 " let x = 1}",
10732 "mid-line `}}` should not dedent"
10733 );
10734 }
10735
10736 #[test]
10740 fn count_5x_fills_unnamed_register() {
10741 let mut e = editor_with("hello world\n");
10742 e.jump_cursor(0, 0);
10743 run_keys(&mut e, "5x");
10744 assert_eq!(e.buffer().lines()[0], " world");
10745 assert_eq!(e.cursor(), (0, 0));
10746 assert_eq!(e.yank(), "hello");
10747 }
10748
10749 #[test]
10750 fn x_fills_unnamed_register_single_char() {
10751 let mut e = editor_with("abc\n");
10752 e.jump_cursor(0, 0);
10753 run_keys(&mut e, "x");
10754 assert_eq!(e.buffer().lines()[0], "bc");
10755 assert_eq!(e.yank(), "a");
10756 }
10757
10758 #[test]
10759 fn big_x_fills_unnamed_register() {
10760 let mut e = editor_with("hello\n");
10761 e.jump_cursor(0, 3);
10762 run_keys(&mut e, "X");
10763 assert_eq!(e.buffer().lines()[0], "helo");
10764 assert_eq!(e.yank(), "l");
10765 }
10766
10767 #[test]
10769 fn g_motion_trailing_newline_lands_on_last_content_row() {
10770 let mut e = editor_with("foo\nbar\nbaz\n");
10771 e.jump_cursor(0, 0);
10772 run_keys(&mut e, "G");
10773 assert_eq!(
10775 e.cursor().0,
10776 2,
10777 "G should land on row 2 (baz), not row 3 (phantom empty)"
10778 );
10779 }
10780
10781 #[test]
10783 fn dd_last_line_clamps_cursor_to_new_last_row() {
10784 let mut e = editor_with("foo\nbar\n");
10785 e.jump_cursor(1, 0);
10786 run_keys(&mut e, "dd");
10787 assert_eq!(e.buffer().lines()[0], "foo");
10788 assert_eq!(
10789 e.cursor(),
10790 (0, 0),
10791 "cursor should clamp to row 0 after dd on last content line"
10792 );
10793 }
10794
10795 #[test]
10797 fn d_dollar_cursor_on_last_char() {
10798 let mut e = editor_with("hello world\n");
10799 e.jump_cursor(0, 5);
10800 run_keys(&mut e, "d$");
10801 assert_eq!(e.buffer().lines()[0], "hello");
10802 assert_eq!(
10803 e.cursor(),
10804 (0, 4),
10805 "d$ should leave cursor on col 4, not col 5"
10806 );
10807 }
10808
10809 #[test]
10811 fn undo_insert_clamps_cursor_to_last_valid_col() {
10812 let mut e = editor_with("hello\n");
10813 e.jump_cursor(0, 5); run_keys(&mut e, "a world<Esc>u");
10815 assert_eq!(e.buffer().lines()[0], "hello");
10816 assert_eq!(
10817 e.cursor(),
10818 (0, 4),
10819 "undo should clamp cursor to col 4 on 'hello'"
10820 );
10821 }
10822
10823 #[test]
10825 fn da_doublequote_eats_trailing_whitespace() {
10826 let mut e = editor_with("say \"hello\" there\n");
10827 e.jump_cursor(0, 6);
10828 run_keys(&mut e, "da\"");
10829 assert_eq!(e.buffer().lines()[0], "say there");
10830 assert_eq!(e.cursor().1, 4, "cursor should be at col 4 after da\"");
10831 }
10832
10833 #[test]
10835 fn dab_cursor_col_clamped_after_delete() {
10836 let mut e = editor_with("fn x() {\n body\n}\n");
10837 e.jump_cursor(1, 4);
10838 run_keys(&mut e, "daB");
10839 assert_eq!(e.buffer().lines()[0], "fn x() ");
10840 assert_eq!(
10841 e.cursor(),
10842 (0, 6),
10843 "daB should leave cursor at col 6, not 7"
10844 );
10845 }
10846
10847 #[test]
10849 fn dib_preserves_surrounding_newlines() {
10850 let mut e = editor_with("{\n body\n}\n");
10851 e.jump_cursor(1, 4);
10852 run_keys(&mut e, "diB");
10853 assert_eq!(e.buffer().lines()[0], "{");
10854 assert_eq!(e.buffer().lines()[1], "}");
10855 assert_eq!(e.cursor().0, 1, "cursor should be on the '}}' line");
10856 }
10857
10858 #[test]
10859 fn is_chord_pending_tracks_replace_state() {
10860 let mut e = editor_with("abc\n");
10861 assert!(!e.is_chord_pending());
10862 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE));
10864 assert!(e.is_chord_pending(), "engine should be pending after r");
10865 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
10867 assert!(
10868 !e.is_chord_pending(),
10869 "engine pending should clear after replace"
10870 );
10871 }
10872
10873 #[test]
10876 fn yiw_sets_lbr_rbr_marks_around_word() {
10877 let mut e = editor_with("hello world");
10880 run_keys(&mut e, "yiw");
10881 let lo = e.mark('[').expect("'[' must be set after yiw");
10882 let hi = e.mark(']').expect("']' must be set after yiw");
10883 assert_eq!(lo, (0, 0), "'[ should be first char of yanked word");
10884 assert_eq!(hi, (0, 4), "'] should be last char of yanked word");
10885 }
10886
10887 #[test]
10888 fn yj_linewise_sets_marks_at_line_edges() {
10889 let mut e = editor_with("aaaaa\nbbbbb\nccc");
10892 run_keys(&mut e, "yj");
10893 let lo = e.mark('[').expect("'[' must be set after yj");
10894 let hi = e.mark(']').expect("']' must be set after yj");
10895 assert_eq!(lo, (0, 0), "'[ snaps to (top_row, 0) for linewise yank");
10896 assert_eq!(
10897 hi,
10898 (1, 4),
10899 "'] snaps to (bot_row, last_col) for linewise yank"
10900 );
10901 }
10902
10903 #[test]
10904 fn dd_sets_lbr_rbr_marks_to_cursor() {
10905 let mut e = editor_with("aaa\nbbb");
10908 run_keys(&mut e, "dd");
10909 let lo = e.mark('[').expect("'[' must be set after dd");
10910 let hi = e.mark(']').expect("']' must be set after dd");
10911 assert_eq!(lo, hi, "after delete both marks are at the same position");
10912 assert_eq!(lo.0, 0, "post-delete cursor row should be 0");
10913 }
10914
10915 #[test]
10916 fn dw_sets_lbr_rbr_marks_to_cursor() {
10917 let mut e = editor_with("hello world");
10920 run_keys(&mut e, "dw");
10921 let lo = e.mark('[').expect("'[' must be set after dw");
10922 let hi = e.mark(']').expect("']' must be set after dw");
10923 assert_eq!(lo, hi, "after delete both marks are at the same position");
10924 assert_eq!(lo, (0, 0), "post-dw cursor is at col 0");
10925 }
10926
10927 #[test]
10928 fn cw_then_esc_sets_lbr_at_start_rbr_at_inserted_text_end() {
10929 let mut e = editor_with("hello world");
10934 run_keys(&mut e, "cwfoo<Esc>");
10935 let lo = e.mark('[').expect("'[' must be set after cw");
10936 let hi = e.mark(']').expect("']' must be set after cw");
10937 assert_eq!(lo, (0, 0), "'[ should be start of change");
10938 assert_eq!(hi.0, 0, "'] should be on row 0");
10941 assert!(hi.1 >= 2, "'] should be at or past last char of 'foo'");
10942 }
10943
10944 #[test]
10945 fn cw_with_no_insertion_sets_marks_at_change_start() {
10946 let mut e = editor_with("hello world");
10949 run_keys(&mut e, "cw<Esc>");
10950 let lo = e.mark('[').expect("'[' must be set after cw<Esc>");
10951 let hi = e.mark(']').expect("']' must be set after cw<Esc>");
10952 assert_eq!(lo.0, 0, "'[ should be on row 0");
10953 assert_eq!(hi.0, 0, "'] should be on row 0");
10954 assert_eq!(lo, hi, "marks coincide when insert is empty");
10956 }
10957
10958 #[test]
10959 fn p_charwise_sets_marks_around_pasted_text() {
10960 let mut e = editor_with("abc xyz");
10963 run_keys(&mut e, "yiw"); run_keys(&mut e, "p"); let lo = e.mark('[').expect("'[' set after charwise paste");
10966 let hi = e.mark(']').expect("']' set after charwise paste");
10967 assert!(lo <= hi, "'[ must not exceed ']'");
10968 assert_eq!(
10970 hi.1.wrapping_sub(lo.1),
10971 2,
10972 "'] - '[ should span 2 cols for a 3-char paste"
10973 );
10974 }
10975
10976 #[test]
10977 fn p_linewise_sets_marks_at_line_edges() {
10978 let mut e = editor_with("aaa\nbbb\nccc");
10981 run_keys(&mut e, "yj"); run_keys(&mut e, "j"); run_keys(&mut e, "p"); let lo = e.mark('[').expect("'[' set after linewise paste");
10985 let hi = e.mark(']').expect("']' set after linewise paste");
10986 assert_eq!(lo.1, 0, "'[ col must be 0 for linewise paste");
10987 assert!(hi.0 > lo.0, "'] row must be below '[ row for 2-line paste");
10988 assert_eq!(hi.0 - lo.0, 1, "exactly 1 row gap for a 2-line payload");
10989 }
10990
10991 #[test]
10992 fn backtick_lbr_v_backtick_rbr_reselects_yanked_text() {
10993 let mut e = editor_with("hello world");
10997 run_keys(&mut e, "yiw"); run_keys(&mut e, "`[v`]");
11001 assert_eq!(
11003 e.cursor(),
11004 (0, 4),
11005 "visual `[v`] should land on last yanked char"
11006 );
11007 assert_eq!(
11009 e.vim_mode(),
11010 crate::VimMode::Visual,
11011 "should be in Visual mode"
11012 );
11013 }
11014
11015 #[test]
11021 fn mark_dot_jump_to_last_edit_pre_edit_cursor() {
11022 let mut e = editor_with("hello\nworld\n");
11025 e.jump_cursor(0, 0);
11026 run_keys(&mut e, "iX<Esc>j`.");
11027 assert_eq!(
11028 e.cursor(),
11029 (0, 0),
11030 "dot mark should jump to the change-start (col 0), not post-insert col"
11031 );
11032 }
11033
11034 #[test]
11037 fn count_100g_clamps_to_last_content_row() {
11038 let mut e = editor_with("foo\nbar\nbaz\n");
11041 e.jump_cursor(0, 0);
11042 run_keys(&mut e, "100G");
11043 assert_eq!(
11044 e.cursor(),
11045 (2, 0),
11046 "100G on trailing-newline buffer must clamp to row 2 (last content row)"
11047 );
11048 }
11049
11050 #[test]
11053 fn gi_resumes_last_insert_position() {
11054 let mut e = editor_with("world\nhello\n");
11060 e.jump_cursor(0, 0);
11061 run_keys(&mut e, "iHi<Esc>jgi<Esc>");
11062 assert_eq!(
11063 e.vim_mode(),
11064 crate::VimMode::Normal,
11065 "should be in Normal mode after gi<Esc>"
11066 );
11067 assert_eq!(
11068 e.cursor(),
11069 (0, 1),
11070 "gi<Esc> cursor should be at (0,1) — the insert row, step-back col"
11071 );
11072 }
11073
11074 #[test]
11078 fn visual_block_change_cursor_on_last_inserted_char() {
11079 let mut e = editor_with("foo\nbar\nbaz\n");
11083 e.jump_cursor(0, 0);
11084 run_keys(&mut e, "<C-v>jlcZZ<Esc>");
11085 let lines = e.buffer().lines().to_vec();
11086 assert_eq!(lines[0], "ZZo", "row 0 should be 'ZZo'");
11087 assert_eq!(lines[1], "ZZr", "row 1 should be 'ZZr'");
11088 assert_eq!(
11089 e.cursor(),
11090 (0, 1),
11091 "cursor should be on last char of inserted 'ZZ' (col 1)"
11092 );
11093 }
11094
11095 #[test]
11100 fn register_blackhole_delete_preserves_unnamed_register() {
11101 let mut e = editor_with("foo bar baz\n");
11108 e.jump_cursor(0, 0);
11109 run_keys(&mut e, "yiww\"_dwbp");
11110 let lines = e.buffer().lines().to_vec();
11111 assert_eq!(
11112 lines[0], "ffoooo baz",
11113 "black-hole delete must not corrupt unnamed register"
11114 );
11115 assert_eq!(
11116 e.cursor(),
11117 (0, 3),
11118 "cursor should be on last pasted char (col 3)"
11119 );
11120 }
11121
11122 #[test]
11125 fn after_z_zz_sets_viewport_pinned() {
11126 let mut e = editor_with("a\nb\nc\nd\ne");
11127 e.jump_cursor(2, 0);
11128 e.after_z('z', 1);
11129 assert!(e.vim.viewport_pinned, "zz must set viewport_pinned");
11130 }
11131
11132 #[test]
11133 fn after_z_zo_opens_fold_at_cursor() {
11134 let mut e = editor_with("a\nb\nc\nd");
11135 e.buffer_mut().add_fold(1, 2, true);
11136 e.jump_cursor(1, 0);
11137 e.after_z('o', 1);
11138 assert!(
11139 !e.buffer().folds()[0].closed,
11140 "zo must open the fold at the cursor row"
11141 );
11142 }
11143
11144 #[test]
11145 fn after_z_zm_closes_all_folds() {
11146 let mut e = editor_with("a\nb\nc\nd\ne\nf");
11147 e.buffer_mut().add_fold(0, 1, false);
11148 e.buffer_mut().add_fold(4, 5, false);
11149 e.after_z('M', 1);
11150 assert!(
11151 e.buffer().folds().iter().all(|f| f.closed),
11152 "zM must close all folds"
11153 );
11154 }
11155
11156 #[test]
11157 fn after_z_zd_removes_fold_at_cursor() {
11158 let mut e = editor_with("a\nb\nc\nd");
11159 e.buffer_mut().add_fold(1, 2, true);
11160 e.jump_cursor(1, 0);
11161 e.after_z('d', 1);
11162 assert!(
11163 e.buffer().folds().is_empty(),
11164 "zd must remove the fold at the cursor row"
11165 );
11166 }
11167
11168 #[test]
11169 fn after_z_zf_in_visual_creates_fold() {
11170 let mut e = editor_with("a\nb\nc\nd\ne");
11171 e.jump_cursor(1, 0);
11173 run_keys(&mut e, "V2j");
11174 e.after_z('f', 1);
11176 let folds = e.buffer().folds();
11177 assert_eq!(folds.len(), 1, "zf in visual must create exactly one fold");
11178 assert_eq!(folds[0].start_row, 1);
11179 assert_eq!(folds[0].end_row, 3);
11180 assert!(folds[0].closed);
11181 }
11182
11183 #[test]
11186 fn apply_op_motion_dw_deletes_word() {
11187 let mut e = editor_with("hello world");
11189 e.apply_op_motion(crate::vim::Operator::Delete, 'w', 1);
11190 assert_eq!(
11191 e.buffer().lines().first().cloned().unwrap_or_default(),
11192 "world"
11193 );
11194 }
11195
11196 #[test]
11197 fn apply_op_motion_cw_quirk_leaves_trailing_space() {
11198 let mut e = editor_with("hello world");
11200 e.apply_op_motion(crate::vim::Operator::Change, 'w', 1);
11201 let line = e.buffer().lines().first().cloned().unwrap_or_default();
11204 assert!(
11205 line.starts_with(' ') || line == " world",
11206 "cw quirk: got {line:?}"
11207 );
11208 assert_eq!(e.vim_mode(), VimMode::Insert);
11209 }
11210
11211 #[test]
11212 fn apply_op_double_dd_deletes_line() {
11213 let mut e = editor_with("line1\nline2\nline3");
11214 e.apply_op_double(crate::vim::Operator::Delete, 1);
11216 let lines: Vec<_> = e.buffer().lines().to_vec();
11217 assert_eq!(lines, vec!["line2", "line3"], "dd should delete line1");
11218 }
11219
11220 #[test]
11221 fn apply_op_double_yy_does_not_modify_buffer() {
11222 let mut e = editor_with("hello");
11223 e.apply_op_double(crate::vim::Operator::Yank, 1);
11224 assert_eq!(
11225 e.buffer().lines().first().cloned().unwrap_or_default(),
11226 "hello"
11227 );
11228 }
11229
11230 #[test]
11231 fn apply_op_double_dd_count2_deletes_two_lines() {
11232 let mut e = editor_with("line1\nline2\nline3");
11233 e.apply_op_double(crate::vim::Operator::Delete, 2);
11234 let lines: Vec<_> = e.buffer().lines().to_vec();
11235 assert_eq!(lines, vec!["line3"], "2dd should delete two lines");
11236 }
11237
11238 #[test]
11239 fn apply_op_motion_unknown_key_is_noop() {
11240 let mut e = editor_with("hello");
11242 let before = e.cursor();
11243 e.apply_op_motion(crate::vim::Operator::Delete, 'X', 1); assert_eq!(e.cursor(), before);
11245 assert_eq!(
11246 e.buffer().lines().first().cloned().unwrap_or_default(),
11247 "hello"
11248 );
11249 }
11250
11251 #[test]
11254 fn apply_op_find_dfx_deletes_to_x() {
11255 let mut e = editor_with("hello x world");
11257 e.apply_op_find(crate::vim::Operator::Delete, 'x', true, false, 1);
11258 assert_eq!(
11259 e.buffer().lines().first().cloned().unwrap_or_default(),
11260 " world",
11261 "dfx must delete 'hello x'"
11262 );
11263 }
11264
11265 #[test]
11266 fn apply_op_find_dtx_deletes_up_to_x() {
11267 let mut e = editor_with("hello x world");
11269 e.apply_op_find(crate::vim::Operator::Delete, 'x', true, true, 1);
11270 assert_eq!(
11271 e.buffer().lines().first().cloned().unwrap_or_default(),
11272 "x world",
11273 "dtx must delete 'hello ' leaving 'x world'"
11274 );
11275 }
11276
11277 #[test]
11278 fn apply_op_find_records_last_find() {
11279 let mut e = editor_with("hello x world");
11281 e.apply_op_find(crate::vim::Operator::Delete, 'x', true, false, 1);
11282 let _ = e.cursor(); }
11289
11290 #[test]
11293 fn apply_op_text_obj_diw_deletes_word() {
11294 let mut e = editor_with("hello world");
11296 e.apply_op_text_obj(crate::vim::Operator::Delete, 'w', true, 1);
11297 let line = e.buffer().lines().first().cloned().unwrap_or_default();
11298 assert!(
11303 !line.contains("hello"),
11304 "diw must delete 'hello', remaining: {line:?}"
11305 );
11306 }
11307
11308 #[test]
11309 fn apply_op_text_obj_daw_deletes_around_word() {
11310 let mut e = editor_with("hello world");
11312 e.apply_op_text_obj(crate::vim::Operator::Delete, 'w', false, 1);
11313 let line = e.buffer().lines().first().cloned().unwrap_or_default();
11314 assert!(
11315 !line.contains("hello"),
11316 "daw must delete 'hello' and surrounding space, remaining: {line:?}"
11317 );
11318 }
11319
11320 #[test]
11321 fn apply_op_text_obj_invalid_char_no_op() {
11322 let mut e = editor_with("hello world");
11324 let before = e.buffer().as_string();
11325 e.apply_op_text_obj(crate::vim::Operator::Delete, 'X', true, 1);
11326 assert_eq!(
11327 e.buffer().as_string(),
11328 before,
11329 "unknown text-object char must be a no-op"
11330 );
11331 }
11332
11333 #[test]
11336 fn apply_op_g_dgg_deletes_to_top() {
11337 let mut e = editor_with("line1\nline2\nline3");
11350 e.jump_cursor(1, 0);
11352 e.apply_op_g(crate::vim::Operator::Delete, 'g', 1);
11355 let lines: Vec<_> = e.buffer().lines().to_vec();
11356 assert_eq!(lines, vec!["line3"], "dgg must delete to file top");
11357 }
11358
11359 #[test]
11360 fn apply_op_g_dge_deletes_word_end_back() {
11361 let mut e = editor_with("hello world");
11374 let before = e.buffer().as_string();
11375 e.apply_op_g(crate::vim::Operator::Delete, 'X', 1);
11377 assert_eq!(
11378 e.buffer().as_string(),
11379 before,
11380 "apply_op_g with unknown char must be a no-op"
11381 );
11382 e.apply_op_g(crate::vim::Operator::Delete, 'e', 1);
11384 }
11386
11387 #[test]
11388 fn apply_op_g_dgj_deletes_screen_down() {
11389 let mut e = editor_with("line1\nline2\nline3");
11392 e.apply_op_g(crate::vim::Operator::Delete, 'j', 1);
11393 let lines: Vec<_> = e.buffer().lines().to_vec();
11394 assert_eq!(lines, vec!["line3"], "dgj must delete current+next line");
11396 }
11397
11398 fn blank_editor() -> Editor {
11401 Editor::new(
11402 hjkl_buffer::Buffer::new(),
11403 crate::types::DefaultHost::new(),
11404 crate::types::Options::default(),
11405 )
11406 }
11407
11408 #[test]
11409 fn set_pending_register_valid_letter_sets_field() {
11410 let mut e = blank_editor();
11411 assert!(e.vim.pending_register.is_none());
11412 e.set_pending_register('a');
11413 assert_eq!(e.vim.pending_register, Some('a'));
11414 }
11415
11416 #[test]
11417 fn set_pending_register_invalid_char_no_op() {
11418 let mut e = blank_editor();
11419 e.set_pending_register('!');
11420 assert!(
11421 e.vim.pending_register.is_none(),
11422 "invalid register char must not set pending_register"
11423 );
11424 }
11425
11426 #[test]
11427 fn set_pending_register_special_plus_sets_field() {
11428 let mut e = blank_editor();
11430 e.set_pending_register('+');
11431 assert_eq!(e.vim.pending_register, Some('+'));
11432 }
11433
11434 #[test]
11435 fn set_pending_register_star_sets_field() {
11436 let mut e = blank_editor();
11438 e.set_pending_register('*');
11439 assert_eq!(e.vim.pending_register, Some('*'));
11440 }
11441
11442 #[test]
11443 fn set_pending_register_underscore_sets_field() {
11444 let mut e = blank_editor();
11446 e.set_pending_register('_');
11447 assert_eq!(e.vim.pending_register, Some('_'));
11448 }
11449}