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