1pub use hjkl_vim_types::{
75 Abbrev, AbbrevKind, AbbrevTrigger, CHANGE_LIST_MAX, InsertDir, InsertEntry, InsertReason,
76 InsertSession, JUMPLIST_MAX, LastChange, LastHorizontalMotion, LastVisual, Mode, Motion,
77 Operator, Pending, RangeKind, SEARCH_HISTORY_MAX, ScrollDir, SearchPrompt, TextObject,
78};
79
80use crate::VimMode;
81use crate::input::{Input, Key};
82
83use crate::buf_helpers::{
84 buf_cursor_pos, buf_line, buf_line_bytes, buf_line_chars, buf_row_count, buf_set_cursor_pos,
85 buf_set_cursor_rc,
86};
87use crate::editor::Editor;
88
89pub(crate) fn rot13_str(s: &str) -> String {
95 s.chars()
96 .map(|c| match c {
97 'a'..='z' => (((c as u8 - b'a' + 13) % 26) + b'a') as char,
98 'A'..='Z' => (((c as u8 - b'A' + 13) % 26) + b'A') as char,
99 _ => c,
100 })
101 .collect()
102}
103
104#[derive(Default)]
109pub struct VimState {
110 pub mode: Mode,
115 pub pending: Pending,
117 pub count: usize,
120 pub last_find: Option<(char, bool, bool)>,
122 pub last_change: Option<LastChange>,
124 pub insert_session: Option<InsertSession>,
126 pub visual_anchor: (usize, usize),
130 pub visual_line_anchor: usize,
132 pub block_anchor: (usize, usize),
135 pub block_vcol: usize,
141 pub yank_linewise: bool,
143 pub pending_register: Option<char>,
146 pub recording_macro: Option<char>,
150 pub recording_keys: Vec<crate::input::Input>,
155 pub replaying_macro: bool,
158 pub last_macro: Option<char>,
160 pub last_insert_pos: Option<(usize, usize)>,
164 pub last_visual: Option<LastVisual>,
167 pub viewport_pinned: bool,
171 pub scroll_anim_hint: bool,
174 pub replaying: bool,
176 pub one_shot_normal: bool,
179 pub search_prompt: Option<SearchPrompt>,
181 pub last_search: Option<String>,
185 pub last_search_forward: bool,
189 pub last_insert_text: Option<String>,
192 pub jump_back: Vec<(usize, usize)>,
197 pub jump_fwd: Vec<(usize, usize)>,
200 pub insert_pending_register: bool,
204 pub change_mark_start: Option<(usize, usize)>,
210 pub search_history: Vec<String>,
214 pub search_history_cursor: Option<usize>,
219 pub last_input_at: Option<std::time::Instant>,
228 pub last_input_host_at: Option<core::time::Duration>,
232 pub current_mode: crate::VimMode,
238 pub last_substitute: Option<crate::substitute::SubstituteCmd>,
240 pub pending_closes: Vec<(usize, usize, char)>,
248 pub last_sneak: Option<((char, char), bool)>,
251 pub last_horizontal_motion: LastHorizontalMotion,
254 pub abbrevs: Vec<Abbrev>,
257}
258
259impl VimState {
260 pub fn public_mode(&self) -> VimMode {
261 match self.mode {
262 Mode::Normal => VimMode::Normal,
263 Mode::Insert => VimMode::Insert,
264 Mode::Visual => VimMode::Visual,
265 Mode::VisualLine => VimMode::VisualLine,
266 Mode::VisualBlock => VimMode::VisualBlock,
267 }
268 }
269
270 pub fn force_normal(&mut self) {
271 self.mode = Mode::Normal;
272 self.pending = Pending::None;
273 self.count = 0;
274 self.insert_session = None;
275 self.current_mode = crate::VimMode::Normal;
277 }
278
279 pub fn clear_pending_prefix(&mut self) {
289 self.pending = Pending::None;
290 self.count = 0;
291 self.pending_register = None;
292 self.insert_pending_register = false;
293 }
294
295 pub(crate) fn widen_insert_row(&mut self, row: usize) {
300 if let Some(ref mut session) = self.insert_session {
301 session.row_min = session.row_min.min(row);
302 session.row_max = session.row_max.max(row);
303 }
304 }
305
306 pub fn is_visual(&self) -> bool {
307 matches!(
308 self.mode,
309 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
310 )
311 }
312
313 pub fn is_visual_char(&self) -> bool {
314 self.mode == Mode::Visual
315 }
316
317 pub(crate) fn pending_count_val(&self) -> Option<u32> {
320 if self.count == 0 {
321 None
322 } else {
323 Some(self.count as u32)
324 }
325 }
326
327 pub(crate) fn is_chord_pending(&self) -> bool {
330 !matches!(self.pending, Pending::None)
331 }
332
333 pub(crate) fn pending_op_char(&self) -> Option<char> {
337 let op = match &self.pending {
338 Pending::Op { op, .. }
339 | Pending::OpTextObj { op, .. }
340 | Pending::OpG { op, .. }
341 | Pending::OpFind { op, .. }
342 | Pending::OpSquareBracketOpen { op, .. }
343 | Pending::OpSquareBracketClose { op, .. } => Some(*op),
344 _ => None,
345 };
346 op.map(|o| match o {
347 Operator::Delete => 'd',
348 Operator::Change => 'c',
349 Operator::Yank => 'y',
350 Operator::Uppercase => 'U',
351 Operator::Lowercase => 'u',
352 Operator::ToggleCase => '~',
353 Operator::Indent => '>',
354 Operator::Outdent => '<',
355 Operator::Fold => 'z',
356 Operator::Reflow => 'q',
357 Operator::ReflowKeepCursor => 'w',
358 Operator::AutoIndent => '=',
359 Operator::Filter => '!',
360 Operator::Comment => 'c',
362 Operator::Rot13 => '?',
364 })
365 }
366}
367
368pub(crate) fn enter_search<H: crate::types::Host>(
374 ed: &mut Editor<hjkl_buffer::Buffer, H>,
375 forward: bool,
376) {
377 ed.vim.search_prompt = Some(SearchPrompt {
378 text: String::new(),
379 cursor: 0,
380 forward,
381 operator: None,
382 });
383 ed.vim.search_history_cursor = None;
384 ed.set_search_pattern(None);
388}
389
390pub(crate) fn enter_search_op<H: crate::types::Host>(
394 ed: &mut Editor<hjkl_buffer::Buffer, H>,
395 forward: bool,
396 op: Operator,
397 count: usize,
398) {
399 let origin = ed.cursor();
400 ed.vim.search_prompt = Some(SearchPrompt {
401 text: String::new(),
402 cursor: 0,
403 forward,
404 operator: Some((op, count.max(1), origin)),
405 });
406 ed.vim.search_history_cursor = None;
407 ed.set_search_pattern(None);
408}
409
410pub(crate) fn apply_op_search_range<H: crate::types::Host>(
414 ed: &mut Editor<hjkl_buffer::Buffer, H>,
415 op: Operator,
416 origin: (usize, usize),
417) {
418 let target = ed.cursor();
419 run_operator_over_range(ed, op, origin, target, RangeKind::Exclusive);
420}
421
422fn walk_change_list<H: crate::types::Host>(
426 ed: &mut Editor<hjkl_buffer::Buffer, H>,
427 dir: isize,
428 count: usize,
429) {
430 if ed.change_list.is_empty() {
431 return;
432 }
433 let len = ed.change_list.len();
434 let mut idx: isize = match (ed.change_list_cursor, dir) {
435 (None, -1) => len as isize - 1,
436 (None, 1) => return, (Some(i), -1) => i as isize - 1,
438 (Some(i), 1) => i as isize + 1,
439 _ => return,
440 };
441 for _ in 1..count {
442 let next = idx + dir;
443 if next < 0 || next >= len as isize {
444 break;
445 }
446 idx = next;
447 }
448 if idx < 0 || idx >= len as isize {
449 return;
450 }
451 let idx = idx as usize;
452 ed.change_list_cursor = Some(idx);
453 let (row, col) = ed.change_list[idx];
454 ed.jump_cursor(row, col);
455}
456
457fn insert_register_text<H: crate::types::Host>(
462 ed: &mut Editor<hjkl_buffer::Buffer, H>,
463 selector: char,
464) {
465 use hjkl_buffer::Edit;
466 let text = match selector {
469 '/' => match &ed.vim.last_search {
470 Some(s) if !s.is_empty() => s.clone(),
471 _ => return,
472 },
473 '.' => match &ed.vim.last_insert_text {
474 Some(s) if !s.is_empty() => s.clone(),
475 _ => return,
476 },
477 _ => match ed.registers().read(selector) {
478 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
479 _ => return,
480 },
481 };
482 ed.sync_buffer_content_from_textarea();
483 let cursor = buf_cursor_pos(&ed.buffer);
484 ed.mutate_edit(Edit::InsertStr {
485 at: cursor,
486 text: text.clone(),
487 });
488 let mut row = cursor.row;
491 let mut col = cursor.col;
492 for ch in text.chars() {
493 if ch == '\n' {
494 row += 1;
495 col = 0;
496 } else {
497 col += 1;
498 }
499 }
500 buf_set_cursor_rc(&mut ed.buffer, row, col);
501 ed.push_buffer_cursor_to_textarea();
502 ed.mark_content_dirty();
503 if let Some(ref mut session) = ed.vim.insert_session {
504 session.row_min = session.row_min.min(row);
505 session.row_max = session.row_max.max(row);
506 }
507}
508
509pub(super) fn compute_enter_indent(settings: &crate::editor::Settings, prev_line: &str) -> String {
528 if !settings.autoindent {
529 return String::new();
530 }
531 let base: String = prev_line
533 .chars()
534 .take_while(|c| *c == ' ' || *c == '\t')
535 .collect();
536
537 if settings.smartindent {
538 let unit = if settings.expandtab {
539 if settings.softtabstop > 0 {
540 " ".repeat(settings.softtabstop)
541 } else {
542 " ".repeat(settings.shiftwidth)
543 }
544 } else {
545 "\t".to_string()
546 };
547
548 let last_non_ws = prev_line.chars().rev().find(|c| !c.is_whitespace());
550 if matches!(last_non_ws, Some('{' | '(' | '[')) {
551 return format!("{base}{unit}");
552 }
553
554 if is_html_filetype(&settings.filetype) {
559 let trimmed_end_len = prev_line
560 .trim_end_matches(|c: char| c.is_whitespace())
561 .len();
562 let trimmed = &prev_line[..trimmed_end_len];
563 if let Some(stripped) = trimmed.strip_suffix('>')
564 && scan_tag_opener(trimmed, stripped.len()).is_some()
565 {
566 return format!("{base}{unit}");
567 }
568 }
569 }
570
571 base
572}
573
574fn comment_prefixes_for_lang(lang: &str) -> &'static [&'static str] {
580 match lang {
581 "rust" => &["/// ", "//! ", "// "],
582 "c" | "cpp" => &["// "],
583 "python" | "sh" | "bash" | "zsh" | "fish" | "toml" | "yaml" => &["# "],
584 "lua" => &["-- "],
585 "sql" => &["-- "],
586 "vim" | "viml" => &["\" "],
587 _ => &[],
588 }
589}
590
591pub(crate) fn detect_comment_on_line(lang: &str, line: &str) -> Option<(String, &'static str)> {
597 let indent_end = line
598 .char_indices()
599 .find(|(_, c)| *c != ' ' && *c != '\t')
600 .map(|(i, _)| i)
601 .unwrap_or(line.len());
602 let indent = line[..indent_end].to_string();
603 let rest = &line[indent_end..];
604 for &prefix in comment_prefixes_for_lang(lang) {
605 if rest.starts_with(prefix) {
606 return Some((indent, prefix));
607 }
608 let bare = prefix.trim_end_matches(' ');
611 if rest == bare || rest.starts_with(&format!("{bare} ")) {
612 return Some((indent, prefix));
613 }
614 }
615 None
616}
617
618pub(crate) fn continue_comment(
625 buffer: &hjkl_buffer::Buffer,
626 settings: &crate::editor::Settings,
627 row: usize,
628) -> Option<String> {
629 if settings.filetype.is_empty() {
630 return None;
631 }
632 let line = crate::buf_helpers::buf_line(buffer, row)?;
633 let (indent, prefix) = detect_comment_on_line(&settings.filetype, &line)?;
634 Some(format!("{indent}{prefix}"))
635}
636
637fn try_dedent_close_bracket<H: crate::types::Host>(
647 ed: &mut Editor<hjkl_buffer::Buffer, H>,
648 cursor: hjkl_buffer::Position,
649 ch: char,
650) -> bool {
651 use hjkl_buffer::{Edit, MotionKind, Position};
652
653 if !ed.settings.smartindent {
654 return false;
655 }
656 if !matches!(ch, '}' | ')' | ']') {
657 return false;
658 }
659
660 let line = match buf_line(&ed.buffer, cursor.row) {
661 Some(l) => l.to_string(),
662 None => return false,
663 };
664
665 let before: String = line.chars().take(cursor.col).collect();
667 if !before.chars().all(|c| c == ' ' || c == '\t') {
668 return false;
669 }
670 if before.is_empty() {
671 return false;
673 }
674
675 let unit_len: usize = if ed.settings.expandtab {
677 if ed.settings.softtabstop > 0 {
678 ed.settings.softtabstop
679 } else {
680 ed.settings.shiftwidth
681 }
682 } else {
683 1
685 };
686
687 let strip_len = if ed.settings.expandtab {
689 let spaces = before.chars().filter(|c| *c == ' ').count();
691 if spaces < unit_len {
692 return false;
693 }
694 unit_len
695 } else {
696 if !before.starts_with('\t') {
698 return false;
699 }
700 1
701 };
702
703 ed.mutate_edit(Edit::DeleteRange {
705 start: Position::new(cursor.row, 0),
706 end: Position::new(cursor.row, strip_len),
707 kind: MotionKind::Char,
708 });
709 let new_col = cursor.col.saturating_sub(strip_len);
714 ed.mutate_edit(Edit::InsertChar {
715 at: Position::new(cursor.row, new_col),
716 ch,
717 });
718 true
719}
720
721fn finish_insert_session<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
722 let Some(session) = ed.vim.insert_session.take() else {
723 return;
724 };
725 let after_rope = crate::types::Query::rope(&ed.buffer);
726 let before_n = session.before_rope.len_lines();
730 let after_n = after_rope.len_lines();
731 let after_end = session.row_max.min(after_n.saturating_sub(1));
732 let before_end = session.row_max.min(before_n.saturating_sub(1));
733 let before = if before_end >= session.row_min && session.row_min < before_n {
734 rope_row_range_str(&session.before_rope, session.row_min, before_end)
735 } else {
736 String::new()
737 };
738 let after = if after_end >= session.row_min && session.row_min < after_n {
739 rope_row_range_str(&after_rope, session.row_min, after_end)
740 } else {
741 String::new()
742 };
743 let inserted = if matches!(session.reason, InsertReason::Replace) {
747 changed_run(&before, &after)
748 } else {
749 extract_inserted(&before, &after)
750 };
751 if !ed.vim.replaying && !inserted.is_empty() {
753 ed.vim.last_insert_text = Some(inserted.clone());
754 }
755 let open_line = matches!(session.reason, InsertReason::Open { .. });
756 if session.count > 1 && !ed.vim.replaying {
757 use hjkl_buffer::{Edit, Position};
758 if open_line {
759 let (start_row, _) = ed.cursor();
764 let typed = buf_line(&ed.buffer, start_row).unwrap_or_default();
765 for at_row in start_row..start_row + (session.count - 1) {
766 let end = buf_line_chars(&ed.buffer, at_row);
767 ed.mutate_edit(Edit::InsertStr {
768 at: Position::new(at_row, end),
769 text: format!("\n{typed}"),
770 });
771 }
772 } else if !inserted.is_empty() {
773 for _ in 0..session.count - 1 {
775 let (row, col) = ed.cursor();
776 ed.mutate_edit(Edit::InsertStr {
777 at: Position::new(row, col),
778 text: inserted.clone(),
779 });
780 }
781 }
782 }
783 fn replicate_block_text<H: crate::types::Host>(
787 ed: &mut Editor<hjkl_buffer::Buffer, H>,
788 inserted: &str,
789 top: usize,
790 bot: usize,
791 col: usize,
792 ) {
793 use hjkl_buffer::{Edit, Position};
794 for r in (top + 1)..=bot {
795 let line_len = buf_line_chars(&ed.buffer, r);
796 if col > line_len {
797 let pad: String = std::iter::repeat_n(' ', col - line_len).collect();
798 ed.mutate_edit(Edit::InsertStr {
799 at: Position::new(r, line_len),
800 text: pad,
801 });
802 }
803 ed.mutate_edit(Edit::InsertStr {
804 at: Position::new(r, col),
805 text: inserted.to_string(),
806 });
807 }
808 }
809
810 if let InsertReason::BlockEdge { top, bot, col } = session.reason {
811 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
814 replicate_block_text(ed, &inserted, top, bot, col);
815 buf_set_cursor_rc(&mut ed.buffer, top, col);
816 ed.push_buffer_cursor_to_textarea();
817 }
818 return;
819 }
820 if let InsertReason::BlockChange { top, bot, col } = session.reason {
821 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
825 replicate_block_text(ed, &inserted, top, bot, col);
826 let ins_chars = inserted.chars().count();
827 let line_len = buf_line_chars(&ed.buffer, top);
828 let target_col = (col + ins_chars).min(line_len);
829 buf_set_cursor_rc(&mut ed.buffer, top, target_col);
830 ed.push_buffer_cursor_to_textarea();
831 }
832 return;
833 }
834 if ed.vim.replaying {
835 return;
836 }
837 match session.reason {
838 InsertReason::Enter(entry) => {
839 ed.vim.last_change = Some(LastChange::InsertAt {
840 entry,
841 inserted,
842 count: session.count,
843 });
844 }
845 InsertReason::Open { above } => {
846 ed.vim.last_change = Some(LastChange::OpenLine { above, inserted });
847 }
848 InsertReason::AfterChange => {
849 if let Some(
850 LastChange::OpMotion { inserted: ins, .. }
851 | LastChange::OpTextObj { inserted: ins, .. }
852 | LastChange::LineOp { inserted: ins, .. }
853 | LastChange::GnOp { inserted: ins, .. },
854 ) = ed.vim.last_change.as_mut()
855 {
856 *ins = Some(inserted);
857 }
858 if let Some(start) = ed.vim.change_mark_start.take() {
864 let end = ed.cursor();
865 ed.set_mark('[', start);
866 ed.set_mark(']', end);
867 }
868 }
869 InsertReason::DeleteToEol => {
870 ed.vim.last_change = Some(LastChange::DeleteToEol {
871 inserted: Some(inserted),
872 });
873 }
874 InsertReason::ReplayOnly => {}
875 InsertReason::BlockEdge { .. } => unreachable!("handled above"),
876 InsertReason::BlockChange { .. } => unreachable!("handled above"),
877 InsertReason::Replace => {
878 ed.vim.last_change = Some(LastChange::ReplaceMode { text: inserted });
881 }
882 }
883}
884
885pub(crate) fn begin_insert<H: crate::types::Host>(
886 ed: &mut Editor<hjkl_buffer::Buffer, H>,
887 count: usize,
888 reason: InsertReason,
889) {
890 if !ed.settings.modifiable {
892 return;
893 }
894 if ed.view == crate::ViewMode::Blame {
896 ed.view = crate::ViewMode::Normal;
897 return;
898 }
899 let record = !matches!(reason, InsertReason::ReplayOnly);
900 if record {
901 ed.push_undo();
902 }
903 let reason = if ed.vim.replaying {
904 InsertReason::ReplayOnly
905 } else {
906 reason
907 };
908 let (row, col) = ed.cursor();
909 ed.vim.insert_session = Some(InsertSession {
910 count,
911 row_min: row,
912 row_max: row,
913 before_rope: crate::types::Query::rope(&ed.buffer),
914 reason,
915 start_row: row,
916 start_col: col,
917 });
918 ed.vim.mode = Mode::Insert;
919 ed.vim.current_mode = crate::VimMode::Insert;
921 drop_blame_if_left_normal(ed);
922}
923
924pub(crate) fn break_undo_group_in_insert<H: crate::types::Host>(
939 ed: &mut Editor<hjkl_buffer::Buffer, H>,
940) {
941 if !ed.settings.undo_break_on_motion {
942 return;
943 }
944 if ed.vim.replaying {
945 return;
946 }
947 if ed.vim.insert_session.is_none() {
948 return;
949 }
950 ed.push_undo();
951 let before_rope = crate::types::Query::rope(&ed.buffer);
952 let row = crate::types::Cursor::cursor(&ed.buffer).line as usize;
953 if let Some(ref mut session) = ed.vim.insert_session {
954 session.before_rope = before_rope;
955 session.row_min = row;
956 session.row_max = row;
957 }
958}
959
960pub(crate) fn maybe_word_undo_break<H: crate::types::Host>(
984 ed: &mut Editor<hjkl_buffer::Buffer, H>,
985 next: char,
986) {
987 use crate::buf_helpers::{buf_cursor_pos, buf_line};
988 use crate::editor::UndoGranularity;
989
990 if ed.settings.undo_granularity != UndoGranularity::Word {
992 return;
993 }
994 if ed.vim.replaying {
996 return;
997 }
998 let session = match ed.vim.insert_session.as_ref() {
999 Some(s) => s,
1000 None => return,
1001 };
1002
1003 let cursor = buf_cursor_pos(&ed.buffer);
1004
1005 let is_first_pos = cursor.row == session.start_row && cursor.col == session.start_col;
1007 if is_first_pos {
1008 return;
1009 }
1010
1011 let should_break = if next == '\n' {
1013 true
1014 } else if next.is_whitespace() {
1015 false
1017 } else {
1018 let prev_char = buf_line(&ed.buffer, cursor.row)
1021 .as_deref()
1022 .and_then(|line| line.chars().nth(cursor.col.wrapping_sub(1)));
1023 match prev_char {
1024 Some(p) if p.is_whitespace() => true,
1026 None if cursor.col == 0 => false, _ => false,
1032 }
1033 };
1034
1035 if should_break {
1036 ed.push_undo();
1039 let before_rope = crate::types::Query::rope(&ed.buffer);
1040 let row = cursor.row;
1041 if let Some(ref mut session) = ed.vim.insert_session {
1042 session.before_rope = before_rope;
1043 session.row_min = row;
1044 session.row_max = row;
1045 }
1046 }
1047}
1048
1049fn autopair_close_for(
1081 ch: char,
1082 filetype: &str,
1083 prev_char: Option<char>,
1084 prev2_char: Option<char>,
1085) -> Option<char> {
1086 let is_triple_quote_third =
1092 matches!(ch, '"' | '`' | '\'') && prev_char == Some(ch) && prev2_char == Some(ch);
1093
1094 match ch {
1095 '(' => Some(')'),
1096 '[' => Some(']'),
1097 '{' => Some('}'),
1098 '"' => {
1099 if is_triple_quote_third {
1100 None
1101 } else {
1102 Some('"')
1103 }
1104 }
1105 '`' => {
1106 if is_triple_quote_third {
1107 None
1108 } else {
1109 Some('`')
1110 }
1111 }
1112 '<' => {
1113 if is_html_filetype(filetype) {
1114 Some('>')
1115 } else {
1116 None
1117 }
1118 }
1119 '\'' => {
1120 if is_triple_quote_third {
1121 return None;
1122 }
1123 if prev_char.map(|c| c.is_ascii_alphabetic()).unwrap_or(false) {
1126 None
1127 } else {
1128 Some('\'')
1129 }
1130 }
1131 _ => None,
1132 }
1133}
1134
1135fn detect_code_fence_opener(line: &str, cursor_col: usize) -> Option<String> {
1150 if cursor_col != line.chars().count() {
1151 return None;
1152 }
1153 let trimmed = line.trim_start();
1154 let backtick_run = trimmed.chars().take_while(|c| *c == '`').count();
1155 if backtick_run < 3 {
1156 return None;
1157 }
1158 let rest = &trimmed[backtick_run..];
1159 if rest.is_empty() {
1160 return None;
1161 }
1162 let all_lang_chars = rest
1163 .chars()
1164 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '+' || c == '-');
1165 if !all_lang_chars {
1166 return None;
1167 }
1168 Some("`".repeat(backtick_run))
1169}
1170
1171fn is_html_filetype(ft: &str) -> bool {
1173 matches!(
1174 ft,
1175 "html" | "xml" | "svg" | "jsx" | "tsx" | "vue" | "svelte"
1176 )
1177}
1178
1179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1195enum TagKind {
1196 Open,
1197 Close,
1198}
1199
1200#[derive(Debug, Clone, PartialEq, Eq)]
1202struct TagSpan {
1203 kind: TagKind,
1204 name: String,
1205 row: usize,
1207 name_start_col: usize,
1209 name_end_col: usize,
1210}
1211
1212fn detect_tag_at_cursor(line: &str, row: usize, col: usize) -> Option<TagSpan> {
1216 let chars: Vec<char> = line.chars().collect();
1217 let mut lt = None;
1219 let mut i = col.min(chars.len());
1220 while i > 0 {
1221 i -= 1;
1222 let c = chars[i];
1223 if c == '<' {
1224 lt = Some(i);
1225 break;
1226 }
1227 if c == '>' {
1229 return None;
1230 }
1231 }
1232 let lt = lt?;
1233 let (kind, name_start) = if chars.get(lt + 1) == Some(&'/') {
1235 (TagKind::Close, lt + 2)
1236 } else {
1237 (TagKind::Open, lt + 1)
1238 };
1239 let first = chars.get(name_start)?;
1241 if !first.is_ascii_alphabetic() {
1242 return None;
1243 }
1244 let mut name_end = name_start;
1246 while name_end < chars.len()
1247 && (chars[name_end].is_ascii_alphanumeric() || chars[name_end] == '-')
1248 {
1249 name_end += 1;
1250 }
1251 if col < name_start || col > name_end {
1255 return None;
1256 }
1257 let name: String = chars[name_start..name_end].iter().collect();
1258 Some(TagSpan {
1259 kind,
1260 name,
1261 row,
1262 name_start_col: name_start,
1263 name_end_col: name_end,
1264 })
1265}
1266
1267fn find_matching_tag(buffer: &hjkl_buffer::Buffer, anchor: &TagSpan) -> Option<TagSpan> {
1281 let row_count = buffer.row_count();
1282 let scan_forward = anchor.kind == TagKind::Open;
1283 let row_iter: Box<dyn Iterator<Item = usize>> = if scan_forward {
1284 Box::new(anchor.row..row_count)
1285 } else {
1286 Box::new((0..=anchor.row).rev())
1287 };
1288 let push_kind = if scan_forward {
1289 TagKind::Open
1290 } else {
1291 TagKind::Close
1292 };
1293 let mut depth: usize = 1;
1294
1295 for r in row_iter {
1296 let line = buf_line(buffer, r)?;
1297 let chars: Vec<char> = line.chars().collect();
1298 let tags = scan_line_tags(&chars, r);
1299 let tags_iter: Box<dyn Iterator<Item = TagSpan>> = if scan_forward {
1300 Box::new(tags.into_iter())
1301 } else {
1302 Box::new(tags.into_iter().rev())
1303 };
1304 for tag in tags_iter {
1305 if r == anchor.row
1307 && tag.name_start_col == anchor.name_start_col
1308 && tag.kind == anchor.kind
1309 {
1310 continue;
1311 }
1312 if r == anchor.row {
1316 if scan_forward && tag.name_start_col < anchor.name_start_col {
1317 continue;
1318 }
1319 if !scan_forward && tag.name_start_col > anchor.name_start_col {
1320 continue;
1321 }
1322 }
1323 if tag.kind == push_kind {
1324 depth += 1;
1325 } else {
1326 depth -= 1;
1327 if depth == 0 {
1328 return Some(tag);
1329 }
1330 }
1331 }
1332 }
1333 None
1334}
1335
1336fn scan_line_tags(chars: &[char], row: usize) -> Vec<TagSpan> {
1340 let mut out = Vec::new();
1341 let n = chars.len();
1342 let mut i = 0;
1343 while i < n {
1344 if chars[i] != '<' {
1345 i += 1;
1346 continue;
1347 }
1348 if chars[i..].starts_with(&['<', '!', '-', '-']) {
1350 let mut j = i + 4;
1351 while j + 2 < n && !(chars[j] == '-' && chars[j + 1] == '-' && chars[j + 2] == '>') {
1352 j += 1;
1353 }
1354 i = (j + 3).min(n);
1355 continue;
1356 }
1357 let (kind, name_start) = if chars.get(i + 1) == Some(&'/') {
1358 (TagKind::Close, i + 2)
1359 } else {
1360 (TagKind::Open, i + 1)
1361 };
1362 if chars
1364 .get(name_start)
1365 .is_none_or(|c| !c.is_ascii_alphabetic())
1366 {
1367 i += 1;
1368 continue;
1369 }
1370 let mut name_end = name_start;
1371 while name_end < n && (chars[name_end].is_ascii_alphanumeric() || chars[name_end] == '-') {
1372 name_end += 1;
1373 }
1374 let mut k = name_end;
1376 let mut self_closing = false;
1377 while k < n {
1378 if chars[k] == '>' {
1379 if k > name_end && chars[k - 1] == '/' {
1380 self_closing = true;
1381 }
1382 break;
1383 }
1384 k += 1;
1385 }
1386 if k >= n {
1387 break;
1389 }
1390 let name: String = chars[name_start..name_end].iter().collect();
1391 if !(self_closing || kind == TagKind::Open && is_void_element(&name)) {
1393 out.push(TagSpan {
1394 kind,
1395 name,
1396 row,
1397 name_start_col: name_start,
1398 name_end_col: name_end,
1399 });
1400 }
1401 i = k + 1;
1402 }
1403 out
1404}
1405
1406pub(crate) fn sync_paired_tag_on_exit<H: crate::types::Host>(
1411 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1412) {
1413 if !is_html_filetype(&ed.settings.filetype) {
1414 return;
1415 }
1416 let (row, col) = ed.cursor();
1417 let line = match buf_line(&ed.buffer, row) {
1418 Some(l) => l,
1419 None => return,
1420 };
1421 let anchor = match detect_tag_at_cursor(&line, row, col) {
1422 Some(t) => t,
1423 None => return,
1424 };
1425 let partner = match find_matching_tag(&ed.buffer, &anchor) {
1426 Some(t) => t,
1427 None => return,
1428 };
1429 if partner.name == anchor.name {
1430 return;
1431 }
1432 use hjkl_buffer::{Edit, MotionKind, Position};
1434 let start = Position::new(partner.row, partner.name_start_col);
1435 let end = Position::new(partner.row, partner.name_end_col);
1436 ed.mutate_edit(Edit::DeleteRange {
1437 start,
1438 end,
1439 kind: MotionKind::Char,
1440 });
1441 ed.mutate_edit(Edit::InsertStr {
1442 at: start,
1443 text: anchor.name.clone(),
1444 });
1445 buf_set_cursor_rc(&mut ed.buffer, row, col);
1448 ed.push_buffer_cursor_to_textarea();
1449}
1450
1451pub fn matching_tag_pair(
1457 buffer: &hjkl_buffer::Buffer,
1458 row: usize,
1459 col: usize,
1460) -> Option<[(usize, usize, usize); 2]> {
1461 let line = buf_line(buffer, row)?;
1462 let anchor = detect_tag_at_cursor(&line, row, col)?;
1463 let partner = find_matching_tag(buffer, &anchor)?;
1464 Some([
1465 (anchor.row, anchor.name_start_col, anchor.name_end_col),
1466 (partner.row, partner.name_start_col, partner.name_end_col),
1467 ])
1468}
1469
1470fn is_void_element(tag: &str) -> bool {
1472 matches!(
1473 tag.to_ascii_lowercase().as_str(),
1474 "area"
1475 | "base"
1476 | "br"
1477 | "col"
1478 | "embed"
1479 | "hr"
1480 | "img"
1481 | "input"
1482 | "link"
1483 | "meta"
1484 | "param"
1485 | "source"
1486 | "track"
1487 | "wbr"
1488 )
1489}
1490
1491fn scan_tag_opener(line: &str, col: usize) -> Option<String> {
1501 let before = if col > 0 { &line[..col] } else { return None };
1504
1505 let lt_pos = before.rfind('<')?;
1507 let inner = &before[lt_pos + 1..]; if inner.starts_with('!') {
1511 return None;
1512 }
1513 if inner.trim_end().ends_with('/') {
1515 return None;
1516 }
1517
1518 let tag: String = inner
1520 .chars()
1521 .take_while(|c| c.is_ascii_alphanumeric() || *c == '-')
1522 .collect();
1523 if tag.is_empty() {
1524 return None;
1525 }
1526 if !tag
1528 .chars()
1529 .next()
1530 .map(|c| c.is_ascii_alphabetic())
1531 .unwrap_or(false)
1532 {
1533 return None;
1534 }
1535 if is_void_element(&tag) {
1536 return None;
1537 }
1538 Some(tag)
1539}
1540
1541pub(crate) fn insert_char_bridge<H: crate::types::Host>(
1546 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1547 ch: char,
1548) -> bool {
1549 use hjkl_buffer::{Edit, MotionKind, Position};
1550 ed.sync_buffer_content_from_textarea();
1551 let in_replace = matches!(
1552 ed.vim.insert_session.as_ref().map(|s| &s.reason),
1553 Some(InsertReason::Replace)
1554 );
1555
1556 if !in_replace && !ed.vim.abbrevs.is_empty() {
1563 let iskeyword = ed.settings.iskeyword.clone();
1564 if !is_keyword_char(ch, &iskeyword) {
1565 check_and_apply_abbrev(ed, AbbrevTrigger::NonKeyword(ch));
1567 }
1569 }
1570 maybe_word_undo_break(ed, ch);
1575
1576 let cursor = buf_cursor_pos(&ed.buffer);
1578 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1579
1580 if !in_replace
1589 && !ed.vim.pending_closes.is_empty()
1590 && let Some(&(pr, _pc, pch)) = ed.vim.pending_closes.last()
1591 && ch == pch
1592 && cursor.row == pr
1593 {
1594 let char_at_cursor =
1595 buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col));
1596 if char_at_cursor == Some(ch) {
1597 ed.vim.pending_closes.pop();
1598 let filetype = ed.settings.filetype.clone();
1600 let autoclose_tag = ed.settings.autoclose_tag;
1601 if ch == '>' && autoclose_tag && is_html_filetype(&filetype) {
1602 let new_col = cursor.col + 1;
1604 buf_set_cursor_rc(&mut ed.buffer, cursor.row, new_col);
1605 if let Some(line) = buf_line(&ed.buffer, cursor.row) {
1609 let char_col = new_col.saturating_sub(1);
1610 let byte_col = line
1611 .char_indices()
1612 .nth(char_col)
1613 .map(|(b, _)| b)
1614 .unwrap_or(line.len());
1615 if let Some(tag) = scan_tag_opener(&line, byte_col) {
1616 let close_tag = format!("</{tag}>");
1617 let insert_pos = Position::new(cursor.row, new_col);
1618 ed.mutate_edit(Edit::InsertStr {
1619 at: insert_pos,
1620 text: close_tag,
1621 });
1622 buf_set_cursor_rc(&mut ed.buffer, cursor.row, new_col);
1624 }
1625 }
1626 } else {
1627 buf_set_cursor_rc(&mut ed.buffer, cursor.row, cursor.col + 1);
1628 }
1629 ed.push_buffer_cursor_to_textarea();
1630 return true;
1631 }
1632 }
1633
1634 if in_replace && cursor.col < line_chars {
1635 ed.vim.pending_closes.clear();
1637 ed.mutate_edit(Edit::DeleteRange {
1638 start: cursor,
1639 end: Position::new(cursor.row, cursor.col + 1),
1640 kind: MotionKind::Char,
1641 });
1642 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1643 } else if !try_dedent_close_bracket(ed, cursor, ch) {
1644 let autopair = ed.settings.autopair;
1646 let filetype = ed.settings.filetype.clone();
1647 let autoclose_tag = ed.settings.autoclose_tag;
1648
1649 let (prev_char, prev2_char) = {
1650 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
1651 let chars: Vec<char> = line.chars().collect();
1652 let p1 = if cursor.col > 0 {
1653 chars.get(cursor.col - 1).copied()
1654 } else {
1655 None
1656 };
1657 let p2 = if cursor.col > 1 {
1658 chars.get(cursor.col - 2).copied()
1659 } else {
1660 None
1661 };
1662 (p1, p2)
1663 };
1664
1665 if autopair {
1666 if let Some(close) = autopair_close_for(ch, &filetype, prev_char, prev2_char) {
1667 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1669 let after = Position::new(cursor.row, cursor.col + 1);
1672 ed.mutate_edit(Edit::InsertChar {
1673 at: after,
1674 ch: close,
1675 });
1676 let between_col = cursor.col + 1;
1679 buf_set_cursor_rc(&mut ed.buffer, cursor.row, between_col);
1680 ed.vim.pending_closes.push((cursor.row, between_col, close));
1685 ed.push_buffer_cursor_to_textarea();
1686 return true;
1687 }
1688
1689 if ch == '>' && autoclose_tag && is_html_filetype(&filetype) {
1693 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1694 let new_col = cursor.col + 1;
1695 if let Some(line) = buf_line(&ed.buffer, cursor.row) {
1700 let char_col = new_col.saturating_sub(1);
1701 let byte_col = line
1702 .char_indices()
1703 .nth(char_col)
1704 .map(|(b, _)| b)
1705 .unwrap_or(line.len());
1706 if let Some(tag) = scan_tag_opener(&line, byte_col) {
1707 let close_tag = format!("</{tag}>");
1708 let insert_pos = Position::new(cursor.row, new_col);
1709 ed.mutate_edit(Edit::InsertStr {
1710 at: insert_pos,
1711 text: close_tag,
1712 });
1713 buf_set_cursor_rc(&mut ed.buffer, cursor.row, new_col);
1715 }
1716 }
1717 ed.push_buffer_cursor_to_textarea();
1718 return true;
1719 }
1720 }
1721
1722 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1727 }
1728 ed.push_buffer_cursor_to_textarea();
1729 true
1730}
1731
1732pub(crate) fn insert_newline_bridge<H: crate::types::Host>(
1738 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1739) -> bool {
1740 use hjkl_buffer::Edit;
1741 ed.sync_buffer_content_from_textarea();
1742
1743 if !ed.vim.abbrevs.is_empty() {
1747 check_and_apply_abbrev(ed, AbbrevTrigger::Cr);
1748 }
1749
1750 maybe_word_undo_break(ed, '\n');
1754
1755 let cursor = buf_cursor_pos(&ed.buffer);
1756 let prev_line = buf_line(&ed.buffer, cursor.row)
1757 .unwrap_or_default()
1758 .to_string();
1759
1760 if ed.settings.autopair && !ed.vim.pending_closes.is_empty() {
1764 let prev_char = if cursor.col > 0 {
1767 prev_line.chars().nth(cursor.col - 1)
1768 } else {
1769 None
1770 };
1771 let next_char = prev_line.chars().nth(cursor.col);
1772 let is_open_pair = matches!(
1773 (prev_char, next_char),
1774 (Some('{'), Some('}')) | (Some('('), Some(')')) | (Some('['), Some(']'))
1775 );
1776 if is_open_pair {
1777 ed.vim.pending_closes.clear();
1780 let base_indent: String = prev_line
1782 .chars()
1783 .take_while(|c| *c == ' ' || *c == '\t')
1784 .collect();
1785 let inner_indent = if ed.settings.expandtab {
1786 let unit = if ed.settings.softtabstop > 0 {
1787 ed.settings.softtabstop
1788 } else {
1789 ed.settings.shiftwidth
1790 };
1791 format!("{base_indent}{}", " ".repeat(unit))
1792 } else {
1793 format!("{base_indent}\t")
1794 };
1795 let text = format!("\n{inner_indent}\n{base_indent}");
1798 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
1799 let new_row = cursor.row + 1;
1801 let new_col = inner_indent.len();
1802 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
1803 ed.push_buffer_cursor_to_textarea();
1804 return true;
1805 }
1806 }
1807
1808 if ed.settings.autopair
1817 && let Some(fence) = detect_code_fence_opener(&prev_line, cursor.col)
1818 {
1819 ed.vim.pending_closes.clear();
1820 let base_indent: String = prev_line
1821 .chars()
1822 .take_while(|c| *c == ' ' || *c == '\t')
1823 .collect();
1824 let text = format!("\n{base_indent}\n{base_indent}{fence}");
1825 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
1826 let new_row = cursor.row + 1;
1827 let new_col = base_indent.chars().count();
1828 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
1829 ed.push_buffer_cursor_to_textarea();
1830 return true;
1831 }
1832
1833 let comment_cont = if ed.settings.formatoptions.contains('r') {
1835 continue_comment(&ed.buffer, &ed.settings, cursor.row)
1836 } else {
1837 None
1838 };
1839
1840 ed.vim.pending_closes.clear();
1842
1843 let text = if let Some(cont) = comment_cont {
1844 format!("\n{cont}")
1847 } else {
1848 let indent = compute_enter_indent(&ed.settings, &prev_line);
1849 format!("\n{indent}")
1850 };
1851 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
1852 ed.push_buffer_cursor_to_textarea();
1853 true
1854}
1855
1856pub(crate) fn insert_tab_bridge<H: crate::types::Host>(
1859 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1860) -> bool {
1861 use hjkl_buffer::Edit;
1862 ed.sync_buffer_content_from_textarea();
1863 let cursor = buf_cursor_pos(&ed.buffer);
1864 if ed.settings.expandtab {
1865 let sts = ed.settings.softtabstop;
1866 let n = if sts > 0 {
1867 sts - (cursor.col % sts)
1868 } else {
1869 ed.settings.tabstop.max(1)
1870 };
1871 ed.mutate_edit(Edit::InsertStr {
1872 at: cursor,
1873 text: " ".repeat(n),
1874 });
1875 } else {
1876 ed.mutate_edit(Edit::InsertChar {
1877 at: cursor,
1878 ch: '\t',
1879 });
1880 }
1881 ed.push_buffer_cursor_to_textarea();
1882 true
1883}
1884
1885pub(crate) fn insert_backspace_bridge<H: crate::types::Host>(
1896 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1897) -> bool {
1898 use hjkl_buffer::{Edit, MotionKind, Position};
1899 ed.sync_buffer_content_from_textarea();
1900 let cursor = buf_cursor_pos(&ed.buffer);
1901
1902 if cursor.col > 0 {
1905 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
1906 if let Some((indent, prefix)) = detect_comment_on_line(&ed.settings.filetype, &line) {
1907 let full_prefix = format!("{indent}{prefix}");
1908 let line_trimmed = line.trim_end_matches(' ');
1911 let prefix_trimmed = full_prefix.trim_end_matches(' ');
1912 if line_trimmed == prefix_trimmed && cursor.col == full_prefix.chars().count() {
1913 ed.mutate_edit(Edit::DeleteRange {
1915 start: Position::new(cursor.row, 0),
1916 end: cursor,
1917 kind: MotionKind::Char,
1918 });
1919 ed.push_buffer_cursor_to_textarea();
1920 return true;
1921 }
1922 }
1923 }
1924
1925 let sts = ed.settings.softtabstop;
1926 if sts > 0 && cursor.col >= sts && cursor.col.is_multiple_of(sts) {
1927 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
1928 let chars: Vec<char> = line.chars().collect();
1929 let run_start = cursor.col - sts;
1930 if (run_start..cursor.col).all(|i| chars.get(i).copied() == Some(' ')) {
1931 ed.mutate_edit(Edit::DeleteRange {
1932 start: Position::new(cursor.row, run_start),
1933 end: cursor,
1934 kind: MotionKind::Char,
1935 });
1936 ed.push_buffer_cursor_to_textarea();
1937 return true;
1938 }
1939 }
1940 let result = if cursor.col > 0 {
1941 ed.mutate_edit(Edit::DeleteRange {
1942 start: Position::new(cursor.row, cursor.col - 1),
1943 end: cursor,
1944 kind: MotionKind::Char,
1945 });
1946 true
1947 } else if cursor.row > 0 {
1948 let prev_row = cursor.row - 1;
1949 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1950 ed.mutate_edit(Edit::JoinLines {
1951 row: prev_row,
1952 count: 1,
1953 with_space: false,
1954 });
1955 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1956 true
1957 } else {
1958 false
1959 };
1960 ed.push_buffer_cursor_to_textarea();
1961 result
1962}
1963
1964pub(crate) fn insert_delete_bridge<H: crate::types::Host>(
1967 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1968) -> bool {
1969 use hjkl_buffer::{Edit, MotionKind, Position};
1970 ed.sync_buffer_content_from_textarea();
1971 let cursor = buf_cursor_pos(&ed.buffer);
1972 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1973 let result = if cursor.col < line_chars {
1974 ed.mutate_edit(Edit::DeleteRange {
1975 start: cursor,
1976 end: Position::new(cursor.row, cursor.col + 1),
1977 kind: MotionKind::Char,
1978 });
1979 buf_set_cursor_pos(&mut ed.buffer, cursor);
1980 true
1981 } else if cursor.row + 1 < buf_row_count(&ed.buffer) {
1982 ed.mutate_edit(Edit::JoinLines {
1983 row: cursor.row,
1984 count: 1,
1985 with_space: false,
1986 });
1987 buf_set_cursor_pos(&mut ed.buffer, cursor);
1988 true
1989 } else {
1990 false
1991 };
1992 ed.push_buffer_cursor_to_textarea();
1993 result
1994}
1995
1996pub(crate) fn insert_arrow_bridge<H: crate::types::Host>(
2000 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2001 dir: InsertDir,
2002) -> bool {
2003 ed.sync_buffer_content_from_textarea();
2004 ed.vim.pending_closes.clear();
2005 match dir {
2006 InsertDir::Left => {
2007 crate::motions::move_left(&mut ed.buffer, 1);
2008 }
2009 InsertDir::Right => {
2010 crate::motions::move_right_to_end(&mut ed.buffer, 1);
2011 }
2012 InsertDir::Up => {
2013 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2014 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
2015 }
2016 InsertDir::Down => {
2017 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2018 crate::motions::move_down(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
2019 }
2020 }
2021 break_undo_group_in_insert(ed);
2022 ed.push_buffer_cursor_to_textarea();
2023 false
2024}
2025
2026pub(crate) fn insert_home_bridge<H: crate::types::Host>(
2029 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2030) -> bool {
2031 ed.sync_buffer_content_from_textarea();
2032 ed.vim.pending_closes.clear();
2033 crate::motions::move_line_start(&mut ed.buffer);
2034 break_undo_group_in_insert(ed);
2035 ed.push_buffer_cursor_to_textarea();
2036 false
2037}
2038
2039pub(crate) fn insert_end_bridge<H: crate::types::Host>(
2042 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2043) -> bool {
2044 ed.sync_buffer_content_from_textarea();
2045 ed.vim.pending_closes.clear();
2046 crate::motions::move_line_end(&mut ed.buffer);
2047 break_undo_group_in_insert(ed);
2048 ed.push_buffer_cursor_to_textarea();
2049 false
2050}
2051
2052pub(crate) fn insert_pageup_bridge<H: crate::types::Host>(
2055 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2056 viewport_h: u16,
2057) -> bool {
2058 let rows = viewport_h.saturating_sub(2).max(1) as isize;
2059 scroll_cursor_rows(ed, -rows);
2060 false
2061}
2062
2063pub(crate) fn insert_pagedown_bridge<H: crate::types::Host>(
2066 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2067 viewport_h: u16,
2068) -> bool {
2069 let rows = viewport_h.saturating_sub(2).max(1) as isize;
2070 scroll_cursor_rows(ed, rows);
2071 false
2072}
2073
2074pub(crate) fn insert_ctrl_w_bridge<H: crate::types::Host>(
2078 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2079) -> bool {
2080 use hjkl_buffer::{Edit, MotionKind};
2081 ed.sync_buffer_content_from_textarea();
2082 let cursor = buf_cursor_pos(&ed.buffer);
2083 if cursor.row == 0 && cursor.col == 0 {
2084 return true;
2085 }
2086 crate::motions::move_word_back(&mut ed.buffer, false, 1, &ed.settings.iskeyword);
2087 let word_start = buf_cursor_pos(&ed.buffer);
2088 if word_start == cursor {
2089 return true;
2090 }
2091 buf_set_cursor_pos(&mut ed.buffer, cursor);
2092 ed.mutate_edit(Edit::DeleteRange {
2093 start: word_start,
2094 end: cursor,
2095 kind: MotionKind::Char,
2096 });
2097 ed.push_buffer_cursor_to_textarea();
2098 true
2099}
2100
2101pub(crate) fn insert_ctrl_u_bridge<H: crate::types::Host>(
2104 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2105) -> bool {
2106 use hjkl_buffer::{Edit, MotionKind, Position};
2107 ed.sync_buffer_content_from_textarea();
2108 let cursor = buf_cursor_pos(&ed.buffer);
2109 if cursor.col > 0 {
2110 ed.mutate_edit(Edit::DeleteRange {
2111 start: Position::new(cursor.row, 0),
2112 end: cursor,
2113 kind: MotionKind::Char,
2114 });
2115 ed.push_buffer_cursor_to_textarea();
2116 }
2117 true
2118}
2119
2120pub(crate) fn insert_ctrl_h_bridge<H: crate::types::Host>(
2124 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2125) -> bool {
2126 use hjkl_buffer::{Edit, MotionKind, Position};
2127 ed.sync_buffer_content_from_textarea();
2128 let cursor = buf_cursor_pos(&ed.buffer);
2129 if cursor.col > 0 {
2130 ed.mutate_edit(Edit::DeleteRange {
2131 start: Position::new(cursor.row, cursor.col - 1),
2132 end: cursor,
2133 kind: MotionKind::Char,
2134 });
2135 } else if cursor.row > 0 {
2136 let prev_row = cursor.row - 1;
2137 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
2138 ed.mutate_edit(Edit::JoinLines {
2139 row: prev_row,
2140 count: 1,
2141 with_space: false,
2142 });
2143 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
2144 }
2145 ed.push_buffer_cursor_to_textarea();
2146 true
2147}
2148
2149pub(crate) fn insert_ctrl_t_bridge<H: crate::types::Host>(
2152 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2153) -> bool {
2154 let (row, col) = ed.cursor();
2155 let sw = ed.settings().shiftwidth;
2156 indent_rows(ed, row, row, 1);
2157 ed.jump_cursor(row, col + sw);
2158 true
2159}
2160
2161pub(crate) fn insert_ctrl_d_bridge<H: crate::types::Host>(
2164 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2165) -> bool {
2166 let (row, col) = ed.cursor();
2167 let before_len = buf_line_bytes(&ed.buffer, row);
2168 outdent_rows(ed, row, row, 1);
2169 let after_len = buf_line_bytes(&ed.buffer, row);
2170 let stripped = before_len.saturating_sub(after_len);
2171 let new_col = col.saturating_sub(stripped);
2172 ed.jump_cursor(row, new_col);
2173 true
2174}
2175
2176pub(crate) fn insert_ctrl_o_bridge<H: crate::types::Host>(
2180 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2181) -> bool {
2182 ed.vim.one_shot_normal = true;
2183 ed.vim.mode = Mode::Normal;
2184 ed.vim.current_mode = crate::VimMode::Normal;
2186 false
2187}
2188
2189pub(crate) fn insert_ctrl_r_bridge<H: crate::types::Host>(
2193 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2194) -> bool {
2195 ed.vim.insert_pending_register = true;
2196 false
2197}
2198
2199pub(crate) fn insert_paste_register_bridge<H: crate::types::Host>(
2203 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2204 reg: char,
2205) -> bool {
2206 insert_register_text(ed, reg);
2207 true
2210}
2211
2212pub(crate) fn leave_insert_to_normal_bridge<H: crate::types::Host>(
2218 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2219) -> bool {
2220 ed.vim.pending_closes.clear();
2221
2222 if !ed.vim.abbrevs.is_empty() {
2225 check_and_apply_abbrev(ed, AbbrevTrigger::Esc);
2226 }
2227
2228 finish_insert_session(ed);
2229 sync_paired_tag_on_exit(ed);
2233 ed.vim.mode = Mode::Normal;
2234 ed.vim.current_mode = crate::VimMode::Normal;
2236 let col = ed.cursor().1;
2237 ed.vim.last_insert_pos = Some(ed.cursor());
2238 if col > 0 {
2239 crate::motions::move_left(&mut ed.buffer, 1);
2240 ed.push_buffer_cursor_to_textarea();
2241 }
2242 ed.sticky_col = Some(ed.cursor().1);
2243 true
2244}
2245
2246pub(crate) fn enter_insert_i_bridge<H: crate::types::Host>(
2253 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2254 count: usize,
2255) {
2256 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
2257}
2258
2259pub(crate) fn enter_insert_shift_i_bridge<H: crate::types::Host>(
2261 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2262 count: usize,
2263) {
2264 move_first_non_whitespace(ed);
2265 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
2266}
2267
2268pub(crate) fn enter_insert_a_bridge<H: crate::types::Host>(
2270 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2271 count: usize,
2272) {
2273 crate::motions::move_right_to_end(&mut ed.buffer, 1);
2274 ed.push_buffer_cursor_to_textarea();
2275 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
2276}
2277
2278pub(crate) fn enter_insert_shift_a_bridge<H: crate::types::Host>(
2280 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2281 count: usize,
2282) {
2283 crate::motions::move_line_end(&mut ed.buffer);
2284 crate::motions::move_right_to_end(&mut ed.buffer, 1);
2285 ed.push_buffer_cursor_to_textarea();
2286 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
2287}
2288
2289pub(crate) fn open_line_below_bridge<H: crate::types::Host>(
2293 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2294 count: usize,
2295) {
2296 use hjkl_buffer::{Edit, Position};
2297 ed.push_undo();
2298 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
2299 ed.sync_buffer_content_from_textarea();
2300 let row = buf_cursor_pos(&ed.buffer).row;
2301 let line_chars = buf_line_chars(&ed.buffer, row);
2302 let prev_line = buf_line(&ed.buffer, row).unwrap_or_default();
2303
2304 let comment_cont = if ed.settings.formatoptions.contains('o') {
2306 continue_comment(&ed.buffer, &ed.settings, row)
2307 } else {
2308 None
2309 };
2310
2311 let suffix = if let Some(cont) = comment_cont {
2312 format!("\n{cont}")
2313 } else {
2314 let indent = compute_enter_indent(&ed.settings, &prev_line);
2315 format!("\n{indent}")
2316 };
2317 ed.mutate_edit(Edit::InsertStr {
2318 at: Position::new(row, line_chars),
2319 text: suffix,
2320 });
2321 ed.push_buffer_cursor_to_textarea();
2322}
2323
2324pub(crate) fn open_line_above_bridge<H: crate::types::Host>(
2328 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2329 count: usize,
2330) {
2331 use hjkl_buffer::{Edit, Position};
2332 ed.push_undo();
2333 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
2334 ed.sync_buffer_content_from_textarea();
2335 let row = buf_cursor_pos(&ed.buffer).row;
2336
2337 let comment_cont = if ed.settings.formatoptions.contains('o') {
2339 continue_comment(&ed.buffer, &ed.settings, row)
2340 } else {
2341 None
2342 };
2343
2344 let (insert_text, new_line_content) = if let Some(cont) = comment_cont {
2347 let content = cont.clone();
2348 (format!("{cont}\n"), content)
2349 } else {
2350 let cur = buf_line(&ed.buffer, row).unwrap_or_default();
2356 let indent = compute_enter_indent(&ed.settings, &cur);
2357 let content = indent.clone();
2358 (format!("{indent}\n"), content)
2359 };
2360 ed.mutate_edit(Edit::InsertStr {
2361 at: Position::new(row, 0),
2362 text: insert_text,
2363 });
2364 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2365 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
2366 let new_row = buf_cursor_pos(&ed.buffer).row;
2367 buf_set_cursor_rc(&mut ed.buffer, new_row, new_line_content.chars().count());
2368 ed.push_buffer_cursor_to_textarea();
2369}
2370
2371pub(crate) fn enter_replace_mode_bridge<H: crate::types::Host>(
2373 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2374 count: usize,
2375) {
2376 begin_insert(ed, count.max(1), InsertReason::Replace);
2378}
2379
2380pub(crate) fn delete_char_forward_bridge<H: crate::types::Host>(
2385 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2386 count: usize,
2387) {
2388 do_char_delete(ed, true, count.max(1));
2389 if !ed.vim.replaying {
2390 ed.vim.last_change = Some(LastChange::CharDel {
2391 forward: true,
2392 count: count.max(1),
2393 });
2394 }
2395}
2396
2397pub(crate) fn delete_char_backward_bridge<H: crate::types::Host>(
2400 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2401 count: usize,
2402) {
2403 do_char_delete(ed, false, count.max(1));
2404 if !ed.vim.replaying {
2405 ed.vim.last_change = Some(LastChange::CharDel {
2406 forward: false,
2407 count: count.max(1),
2408 });
2409 }
2410}
2411
2412pub(crate) fn substitute_char_bridge<H: crate::types::Host>(
2415 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2416 count: usize,
2417) {
2418 use hjkl_buffer::{Edit, MotionKind, Position};
2419 ed.push_undo();
2420 ed.sync_buffer_content_from_textarea();
2421 for _ in 0..count.max(1) {
2422 let cursor = buf_cursor_pos(&ed.buffer);
2423 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
2424 if cursor.col >= line_chars {
2425 break;
2426 }
2427 ed.mutate_edit(Edit::DeleteRange {
2428 start: cursor,
2429 end: Position::new(cursor.row, cursor.col + 1),
2430 kind: MotionKind::Char,
2431 });
2432 }
2433 ed.push_buffer_cursor_to_textarea();
2434 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
2435 if !ed.vim.replaying {
2436 ed.vim.last_change = Some(LastChange::OpMotion {
2437 op: Operator::Change,
2438 motion: Motion::Right,
2439 count: count.max(1),
2440 inserted: None,
2441 });
2442 }
2443}
2444
2445pub(crate) fn substitute_line_bridge<H: crate::types::Host>(
2448 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2449 count: usize,
2450) {
2451 execute_line_op(ed, Operator::Change, count.max(1));
2452 if !ed.vim.replaying {
2453 ed.vim.last_change = Some(LastChange::LineOp {
2454 op: Operator::Change,
2455 count: count.max(1),
2456 inserted: None,
2457 });
2458 }
2459}
2460
2461pub(crate) fn delete_to_eol_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2464 ed.push_undo();
2465 delete_to_eol(ed);
2466 crate::motions::move_left(&mut ed.buffer, 1);
2467 ed.push_buffer_cursor_to_textarea();
2468 if !ed.vim.replaying {
2469 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
2470 }
2471}
2472
2473pub(crate) fn change_to_eol_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2476 ed.push_undo();
2477 delete_to_eol(ed);
2478 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
2479}
2480
2481pub(crate) fn yank_to_eol_bridge<H: crate::types::Host>(
2483 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2484 count: usize,
2485) {
2486 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
2487}
2488
2489pub(crate) fn join_line_bridge<H: crate::types::Host>(
2492 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2493 count: usize,
2494) {
2495 let joins = count.max(2) - 1;
2498 for _ in 0..joins {
2499 ed.push_undo();
2500 join_line(ed);
2501 }
2502 if !ed.vim.replaying {
2503 ed.vim.last_change = Some(LastChange::JoinLine { count: joins });
2504 }
2505}
2506
2507pub(crate) fn toggle_case_at_cursor_bridge<H: crate::types::Host>(
2510 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2511 count: usize,
2512) {
2513 for _ in 0..count.max(1) {
2514 ed.push_undo();
2515 toggle_case_at_cursor(ed);
2516 }
2517 if !ed.vim.replaying {
2518 ed.vim.last_change = Some(LastChange::ToggleCase {
2519 count: count.max(1),
2520 });
2521 }
2522}
2523
2524pub(crate) fn paste_after_bridge<H: crate::types::Host>(
2528 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2529 count: usize,
2530) {
2531 paste_bridge(ed, false, count, false, false);
2532}
2533
2534pub(crate) fn paste_before_bridge<H: crate::types::Host>(
2538 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2539 count: usize,
2540) {
2541 paste_bridge(ed, true, count, false, false);
2542}
2543
2544pub(crate) fn paste_bridge<H: crate::types::Host>(
2547 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2548 before: bool,
2549 count: usize,
2550 cursor_after: bool,
2551 reindent: bool,
2552) {
2553 do_paste(ed, before, count.max(1), cursor_after, reindent);
2554 if !ed.vim.replaying {
2555 ed.vim.last_change = Some(LastChange::Paste {
2556 before,
2557 count: count.max(1),
2558 cursor_after,
2559 reindent,
2560 });
2561 }
2562}
2563
2564pub(crate) fn jump_back_bridge<H: crate::types::Host>(
2569 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2570 count: usize,
2571) {
2572 for _ in 0..count.max(1) {
2573 jump_back(ed);
2574 }
2575}
2576
2577pub(crate) fn jump_forward_bridge<H: crate::types::Host>(
2580 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2581 count: usize,
2582) {
2583 for _ in 0..count.max(1) {
2584 jump_forward(ed);
2585 }
2586}
2587
2588pub(crate) fn scroll_full_page_bridge<H: crate::types::Host>(
2593 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2594 dir: ScrollDir,
2595 count: usize,
2596) {
2597 ed.vim.scroll_anim_hint = true;
2598 let rows = viewport_full_rows(ed, count) as isize;
2599 match dir {
2600 ScrollDir::Down => scroll_cursor_rows(ed, rows),
2601 ScrollDir::Up => scroll_cursor_rows(ed, -rows),
2602 }
2603}
2604
2605pub(crate) fn scroll_half_page_bridge<H: crate::types::Host>(
2608 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2609 dir: ScrollDir,
2610 count: usize,
2611) {
2612 ed.vim.scroll_anim_hint = true;
2613 let rows = viewport_half_rows(ed, count) as isize;
2614 match dir {
2615 ScrollDir::Down => scroll_cursor_rows(ed, rows),
2616 ScrollDir::Up => scroll_cursor_rows(ed, -rows),
2617 }
2618}
2619
2620pub(crate) fn scroll_line_bridge<H: crate::types::Host>(
2624 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2625 dir: ScrollDir,
2626 count: usize,
2627) {
2628 let n = count.max(1);
2629 let total = buf_row_count(&ed.buffer);
2630 let last = total.saturating_sub(1);
2631 let h = ed.viewport_height_value() as usize;
2632 let vp = ed.host().viewport();
2633 let cur_top = vp.top_row;
2634 let new_top = match dir {
2635 ScrollDir::Down => (cur_top + n).min(last),
2636 ScrollDir::Up => cur_top.saturating_sub(n),
2637 };
2638 ed.set_viewport_top(new_top);
2639 let (row, col) = ed.cursor();
2641 let bot = (new_top + h).saturating_sub(1).min(last);
2642 let clamped = row.max(new_top).min(bot);
2643 if clamped != row {
2644 buf_set_cursor_rc(&mut ed.buffer, clamped, col);
2645 ed.push_buffer_cursor_to_textarea();
2646 }
2647}
2648
2649pub(crate) fn search_repeat_bridge<H: crate::types::Host>(
2654 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2655 forward: bool,
2656 count: usize,
2657) {
2658 if let Some(pattern) = ed.vim.last_search.clone() {
2659 ed.push_search_pattern(&pattern);
2660 }
2661 if ed.search_state().pattern.is_none() {
2662 return;
2663 }
2664 let go_forward = ed.vim.last_search_forward == forward;
2665 for _ in 0..count.max(1) {
2666 if go_forward {
2667 ed.search_advance_forward(true);
2668 } else {
2669 ed.search_advance_backward(true);
2670 }
2671 }
2672 ed.push_buffer_cursor_to_textarea();
2673}
2674
2675pub(crate) fn word_search_bridge<H: crate::types::Host>(
2679 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2680 forward: bool,
2681 whole_word: bool,
2682 count: usize,
2683) {
2684 word_at_cursor_search(ed, forward, whole_word, count.max(1));
2685}
2686
2687#[allow(dead_code)]
2692#[inline]
2693pub(crate) fn do_undo_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2694 do_undo(ed);
2695}
2696
2697#[inline]
2714pub fn drop_blame_if_left_normal<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2715 if ed.vim.current_mode != crate::VimMode::Normal {
2716 ed.view = crate::ViewMode::Normal;
2717 }
2718}
2719
2720#[inline]
2724pub(crate) fn set_vim_mode_bridge<H: crate::types::Host>(
2725 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2726 mode: Mode,
2727) {
2728 ed.vim.mode = mode;
2729 ed.vim.current_mode = ed.vim.public_mode();
2730 drop_blame_if_left_normal(ed);
2731}
2732
2733pub(crate) fn enter_visual_char_bridge<H: crate::types::Host>(
2736 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2737) {
2738 let cur = ed.cursor();
2739 ed.vim.visual_anchor = cur;
2740 set_vim_mode_bridge(ed, Mode::Visual);
2741}
2742
2743pub(crate) fn enter_visual_line_bridge<H: crate::types::Host>(
2746 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2747) {
2748 let (row, _) = ed.cursor();
2749 ed.vim.visual_line_anchor = row;
2750 set_vim_mode_bridge(ed, Mode::VisualLine);
2751}
2752
2753pub(crate) fn enter_visual_block_bridge<H: crate::types::Host>(
2757 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2758) {
2759 let cur = ed.cursor();
2760 ed.vim.block_anchor = cur;
2761 ed.vim.block_vcol = cur.1;
2762 set_vim_mode_bridge(ed, Mode::VisualBlock);
2763}
2764
2765pub(crate) fn exit_visual_to_normal_bridge<H: crate::types::Host>(
2770 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2771) {
2772 let snap: Option<LastVisual> = match ed.vim.mode {
2774 Mode::Visual => Some(LastVisual {
2775 mode: Mode::Visual,
2776 anchor: ed.vim.visual_anchor,
2777 cursor: ed.cursor(),
2778 block_vcol: 0,
2779 }),
2780 Mode::VisualLine => Some(LastVisual {
2781 mode: Mode::VisualLine,
2782 anchor: (ed.vim.visual_line_anchor, 0),
2783 cursor: ed.cursor(),
2784 block_vcol: 0,
2785 }),
2786 Mode::VisualBlock => Some(LastVisual {
2787 mode: Mode::VisualBlock,
2788 anchor: ed.vim.block_anchor,
2789 cursor: ed.cursor(),
2790 block_vcol: ed.vim.block_vcol,
2791 }),
2792 _ => None,
2793 };
2794 ed.vim.pending = Pending::None;
2796 ed.vim.count = 0;
2797 ed.vim.insert_session = None;
2798 set_vim_mode_bridge(ed, Mode::Normal);
2799 if let Some(snap) = snap {
2803 let (lo, hi) = match snap.mode {
2804 Mode::Visual => {
2805 if snap.anchor <= snap.cursor {
2806 (snap.anchor, snap.cursor)
2807 } else {
2808 (snap.cursor, snap.anchor)
2809 }
2810 }
2811 Mode::VisualLine => {
2812 let r_lo = snap.anchor.0.min(snap.cursor.0);
2813 let r_hi = snap.anchor.0.max(snap.cursor.0);
2814 let vl_rope = ed.buffer().rope();
2815 let r_hi_clamped = r_hi.min(vl_rope.len_lines().saturating_sub(1));
2816 let last_col = hjkl_buffer::rope_line_str(&vl_rope, r_hi_clamped)
2817 .chars()
2818 .count()
2819 .saturating_sub(1);
2820 ((r_lo, 0), (r_hi, last_col))
2821 }
2822 Mode::VisualBlock => {
2823 let (r1, c1) = snap.anchor;
2824 let (r2, c2) = snap.cursor;
2825 ((r1.min(r2), c1.min(c2)), (r1.max(r2), c1.max(c2)))
2826 }
2827 _ => {
2828 if snap.anchor <= snap.cursor {
2829 (snap.anchor, snap.cursor)
2830 } else {
2831 (snap.cursor, snap.anchor)
2832 }
2833 }
2834 };
2835 ed.set_mark('<', lo);
2836 ed.set_mark('>', hi);
2837 ed.vim.last_visual = Some(snap);
2838 }
2839}
2840
2841pub(crate) fn visual_o_toggle_bridge<H: crate::types::Host>(
2847 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2848) {
2849 match ed.vim.mode {
2850 Mode::Visual => {
2851 let cur = ed.cursor();
2852 let anchor = ed.vim.visual_anchor;
2853 ed.vim.visual_anchor = cur;
2854 ed.jump_cursor(anchor.0, anchor.1);
2855 }
2856 Mode::VisualLine => {
2857 let cur_row = ed.cursor().0;
2858 let anchor_row = ed.vim.visual_line_anchor;
2859 ed.vim.visual_line_anchor = cur_row;
2860 ed.jump_cursor(anchor_row, 0);
2861 }
2862 Mode::VisualBlock => {
2863 let cur = ed.cursor();
2864 let anchor = ed.vim.block_anchor;
2865 ed.vim.block_anchor = cur;
2866 ed.vim.block_vcol = anchor.1;
2867 ed.jump_cursor(anchor.0, anchor.1);
2868 }
2869 _ => {}
2870 }
2871}
2872
2873pub(crate) fn reenter_last_visual_bridge<H: crate::types::Host>(
2877 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2878) {
2879 if let Some(snap) = ed.vim.last_visual {
2880 match snap.mode {
2881 Mode::Visual => {
2882 ed.vim.visual_anchor = snap.anchor;
2883 set_vim_mode_bridge(ed, Mode::Visual);
2884 }
2885 Mode::VisualLine => {
2886 ed.vim.visual_line_anchor = snap.anchor.0;
2887 set_vim_mode_bridge(ed, Mode::VisualLine);
2888 }
2889 Mode::VisualBlock => {
2890 ed.vim.block_anchor = snap.anchor;
2891 ed.vim.block_vcol = snap.block_vcol;
2892 set_vim_mode_bridge(ed, Mode::VisualBlock);
2893 }
2894 _ => {}
2895 }
2896 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
2897 }
2898}
2899
2900pub(crate) fn set_mode_bridge<H: crate::types::Host>(
2906 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2907 mode: crate::VimMode,
2908) {
2909 let internal = match mode {
2910 crate::VimMode::Normal => Mode::Normal,
2911 crate::VimMode::Insert => Mode::Insert,
2912 crate::VimMode::Visual => Mode::Visual,
2913 crate::VimMode::VisualLine => Mode::VisualLine,
2914 crate::VimMode::VisualBlock => Mode::VisualBlock,
2915 };
2916 ed.vim.mode = internal;
2917 ed.vim.current_mode = mode;
2918 drop_blame_if_left_normal(ed);
2919}
2920
2921pub(crate) fn set_mark_at_cursor<H: crate::types::Host>(
2938 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2939 ch: char,
2940) {
2941 if ch.is_ascii_lowercase() {
2942 let pos = ed.cursor();
2943 ed.set_mark(ch, pos);
2944 } else if ch.is_ascii_uppercase() {
2945 let pos = ed.cursor();
2946 let bid = ed.current_buffer_id();
2947 ed.set_global_mark(ch, bid, pos);
2948 tracing::debug!(
2949 mark = ch as u32,
2950 buffer_id = bid,
2951 row = pos.0,
2952 col = pos.1,
2953 "global mark set"
2954 );
2955 }
2956 }
2958
2959pub(crate) fn goto_mark<H: crate::types::Host>(
2968 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2969 ch: char,
2970 linewise: bool,
2971) {
2972 let target = match ch {
2973 'a'..='z' => ed.mark(ch),
2974 '\'' | '`' => ed.vim.jump_back.last().copied(),
2975 '.' => ed.last_edit_pos,
2976 '[' | ']' | '<' | '>' => ed.mark(ch),
2977 _ => None,
2978 };
2979 let Some((row, col)) = target else {
2980 return;
2981 };
2982 let pre = ed.cursor();
2983 let (r, c_clamped) = clamp_pos(ed, (row, col));
2984 if linewise {
2985 buf_set_cursor_rc(&mut ed.buffer, r, 0);
2986 ed.push_buffer_cursor_to_textarea();
2987 move_first_non_whitespace(ed);
2988 } else {
2989 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
2990 ed.push_buffer_cursor_to_textarea();
2991 }
2992 if ed.cursor() != pre {
2993 ed.push_jump(pre);
2994 }
2995 ed.sticky_col = Some(ed.cursor().1);
2996}
2997
2998pub(crate) fn try_goto_mark<H: crate::types::Host>(
3007 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3008 ch: char,
3009 linewise: bool,
3010) -> crate::editor::MarkJump {
3011 use crate::editor::MarkJump;
3012 match ch {
3013 'A'..='Z' => {
3014 let Some((bid, row, col)) = ed.global_mark(ch) else {
3015 return MarkJump::Unset;
3016 };
3017 if bid != ed.current_buffer_id() {
3018 tracing::debug!(
3019 mark = ch as u32,
3020 buffer_id = bid,
3021 row,
3022 col,
3023 "global mark cross-buffer jump"
3024 );
3025 return MarkJump::CrossBuffer {
3026 buffer_id: bid,
3027 row,
3028 col,
3029 };
3030 }
3031 let pre = ed.cursor();
3033 let (r, c_clamped) = clamp_pos(ed, (row, col));
3034 if linewise {
3035 buf_set_cursor_rc(&mut ed.buffer, r, 0);
3036 ed.push_buffer_cursor_to_textarea();
3037 move_first_non_whitespace(ed);
3038 } else {
3039 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
3040 ed.push_buffer_cursor_to_textarea();
3041 }
3042 if ed.cursor() != pre {
3043 ed.push_jump(pre);
3044 }
3045 ed.sticky_col = Some(ed.cursor().1);
3046 MarkJump::SameBuffer
3047 }
3048 'a'..='z' | '\'' | '`' | '.' | '[' | ']' | '<' | '>' => {
3049 goto_mark(ed, ch, linewise);
3050 MarkJump::SameBuffer
3051 }
3052 _ => MarkJump::Unset,
3053 }
3054}
3055
3056pub fn op_is_change(op: Operator) -> bool {
3060 matches!(op, Operator::Delete | Operator::Change)
3061}
3062
3063fn jump_back<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
3068 let Some(target) = ed.vim.jump_back.pop() else {
3069 return;
3070 };
3071 let cur = ed.cursor();
3072 ed.vim.jump_fwd.push(cur);
3073 let (r, c) = clamp_pos(ed, target);
3074 ed.jump_cursor(r, c);
3075 ed.sticky_col = Some(c);
3076}
3077
3078fn jump_forward<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
3081 let Some(target) = ed.vim.jump_fwd.pop() else {
3082 return;
3083 };
3084 let cur = ed.cursor();
3085 ed.vim.jump_back.push(cur);
3086 if ed.vim.jump_back.len() > JUMPLIST_MAX {
3087 ed.vim.jump_back.remove(0);
3088 }
3089 let (r, c) = clamp_pos(ed, target);
3090 ed.jump_cursor(r, c);
3091 ed.sticky_col = Some(c);
3092}
3093
3094fn clamp_pos<H: crate::types::Host>(
3097 ed: &Editor<hjkl_buffer::Buffer, H>,
3098 pos: (usize, usize),
3099) -> (usize, usize) {
3100 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
3101 let r = pos.0.min(last_row);
3102 let line_len = buf_line_chars(&ed.buffer, r);
3103 let c = pos.1.min(line_len.saturating_sub(1));
3104 (r, c)
3105}
3106
3107fn is_big_jump(motion: &Motion) -> bool {
3109 matches!(
3110 motion,
3111 Motion::FileTop
3112 | Motion::FileBottom
3113 | Motion::MatchBracket
3114 | Motion::WordAtCursor { .. }
3115 | Motion::SearchNext { .. }
3116 | Motion::ViewportTop
3117 | Motion::ViewportMiddle
3118 | Motion::ViewportBottom
3119 )
3120}
3121
3122fn viewport_half_rows<H: crate::types::Host>(
3127 ed: &Editor<hjkl_buffer::Buffer, H>,
3128 count: usize,
3129) -> usize {
3130 let h = ed.viewport_height_value() as usize;
3131 (h / 2).max(1).saturating_mul(count.max(1))
3132}
3133
3134fn viewport_full_rows<H: crate::types::Host>(
3137 ed: &Editor<hjkl_buffer::Buffer, H>,
3138 count: usize,
3139) -> usize {
3140 let h = ed.viewport_height_value() as usize;
3141 h.saturating_sub(2).max(1).saturating_mul(count.max(1))
3142}
3143
3144fn scroll_cursor_rows<H: crate::types::Host>(
3149 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3150 delta: isize,
3151) {
3152 if delta == 0 {
3153 return;
3154 }
3155 ed.sync_buffer_content_from_textarea();
3156 let (row, _) = ed.cursor();
3157 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
3158 let target = (row as isize + delta).max(0).min(last_row as isize) as usize;
3159 buf_set_cursor_rc(&mut ed.buffer, target, 0);
3160 crate::motions::move_first_non_blank(&mut ed.buffer);
3161 ed.push_buffer_cursor_to_textarea();
3162 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
3163}
3164
3165pub fn parse_motion(input: &Input) -> Option<Motion> {
3171 if input.ctrl {
3172 if input.key == Key::Char('h') {
3176 return Some(Motion::BackspaceBack);
3177 }
3178 return None;
3179 }
3180 match input.key {
3181 Key::Char('h') | Key::Left => Some(Motion::Left),
3182 Key::Char('l') | Key::Right => Some(Motion::Right),
3183 Key::Char(' ') => Some(Motion::SpaceFwd),
3187 Key::Backspace => Some(Motion::BackspaceBack),
3188 Key::Char('j') | Key::Down => Some(Motion::Down),
3189 Key::Char('+') | Key::Enter => Some(Motion::FirstNonBlankNextLine),
3191 Key::Char('-') => Some(Motion::FirstNonBlankPrevLine),
3193 Key::Char('_') => Some(Motion::FirstNonBlankLine),
3195 Key::Char('k') | Key::Up => Some(Motion::Up),
3196 Key::Char('w') => Some(Motion::WordFwd),
3197 Key::Char('W') => Some(Motion::BigWordFwd),
3198 Key::Char('b') => Some(Motion::WordBack),
3199 Key::Char('B') => Some(Motion::BigWordBack),
3200 Key::Char('e') => Some(Motion::WordEnd),
3201 Key::Char('E') => Some(Motion::BigWordEnd),
3202 Key::Char('0') | Key::Home => Some(Motion::LineStart),
3203 Key::Char('^') => Some(Motion::FirstNonBlank),
3204 Key::Char('$') | Key::End => Some(Motion::LineEnd),
3205 Key::Char('G') => Some(Motion::FileBottom),
3206 Key::Char('%') => Some(Motion::MatchBracket),
3207 Key::Char(';') => Some(Motion::FindRepeat { reverse: false }),
3208 Key::Char(',') => Some(Motion::FindRepeat { reverse: true }),
3209 Key::Char('*') => Some(Motion::WordAtCursor {
3210 forward: true,
3211 whole_word: true,
3212 }),
3213 Key::Char('#') => Some(Motion::WordAtCursor {
3214 forward: false,
3215 whole_word: true,
3216 }),
3217 Key::Char('n') => Some(Motion::SearchNext { reverse: false }),
3218 Key::Char('N') => Some(Motion::SearchNext { reverse: true }),
3219 Key::Char('H') => Some(Motion::ViewportTop),
3220 Key::Char('M') => Some(Motion::ViewportMiddle),
3221 Key::Char('L') => Some(Motion::ViewportBottom),
3222 Key::Char('{') => Some(Motion::ParagraphPrev),
3223 Key::Char('}') => Some(Motion::ParagraphNext),
3224 Key::Char('(') => Some(Motion::SentencePrev),
3225 Key::Char(')') => Some(Motion::SentenceNext),
3226 Key::Char('|') => Some(Motion::GotoColumn),
3227 _ => None,
3228 }
3229}
3230
3231pub(crate) fn execute_motion<H: crate::types::Host>(
3234 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3235 motion: Motion,
3236 count: usize,
3237) {
3238 let count = count.max(1);
3239 if let Motion::FindRepeat { reverse } = motion
3242 && ed.vim.last_horizontal_motion == LastHorizontalMotion::Sneak
3243 {
3244 if let Some(((c1, c2), fwd)) = ed.vim.last_sneak {
3245 let effective_fwd = if reverse { !fwd } else { fwd };
3246 apply_sneak(ed, c1, c2, effective_fwd, count);
3247 }
3248 return;
3249 }
3250 let motion = match motion {
3252 Motion::FindRepeat { reverse } => match ed.vim.last_find {
3253 Some((ch, forward, till)) => Motion::Find {
3254 ch,
3255 forward: if reverse { !forward } else { forward },
3256 till,
3257 },
3258 None => return,
3259 },
3260 other => other,
3261 };
3262 let pre_pos = ed.cursor();
3263 let pre_col = pre_pos.1;
3264 apply_motion_cursor(ed, &motion, count);
3265 let post_pos = ed.cursor();
3266 if is_big_jump(&motion) && pre_pos != post_pos {
3267 ed.push_jump(pre_pos);
3268 }
3269 apply_sticky_col(ed, &motion, pre_col);
3270 ed.sync_buffer_from_textarea();
3275}
3276
3277fn execute_motion_with_block_vcol<H: crate::types::Host>(
3288 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3289 motion: Motion,
3290 count: usize,
3291) {
3292 let motion_copy = motion.clone();
3293 execute_motion(ed, motion, count);
3294 if ed.vim.mode == Mode::VisualBlock {
3295 update_block_vcol(ed, &motion_copy);
3296 }
3297}
3298
3299pub(crate) fn apply_motion_kind<H: crate::types::Host>(
3327 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3328 kind: crate::MotionKind,
3329 count: usize,
3330) {
3331 let count = count.max(1);
3332 match kind {
3333 crate::MotionKind::CharLeft => {
3334 execute_motion_with_block_vcol(ed, Motion::Left, count);
3335 }
3336 crate::MotionKind::CharRight => {
3337 execute_motion_with_block_vcol(ed, Motion::Right, count);
3338 }
3339 crate::MotionKind::LineDown => {
3340 execute_motion_with_block_vcol(ed, Motion::Down, count);
3341 }
3342 crate::MotionKind::LineUp => {
3343 execute_motion_with_block_vcol(ed, Motion::Up, count);
3344 }
3345 crate::MotionKind::FirstNonBlankDown => {
3346 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3351 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
3352 crate::motions::move_first_non_blank(&mut ed.buffer);
3353 ed.push_buffer_cursor_to_textarea();
3354 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
3355 ed.sync_buffer_from_textarea();
3356 }
3357 crate::MotionKind::FirstNonBlankUp => {
3358 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3361 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
3362 crate::motions::move_first_non_blank(&mut ed.buffer);
3363 ed.push_buffer_cursor_to_textarea();
3364 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
3365 ed.sync_buffer_from_textarea();
3366 }
3367 crate::MotionKind::WordForward => {
3368 execute_motion_with_block_vcol(ed, Motion::WordFwd, count);
3369 }
3370 crate::MotionKind::BigWordForward => {
3371 execute_motion_with_block_vcol(ed, Motion::BigWordFwd, count);
3372 }
3373 crate::MotionKind::WordBackward => {
3374 execute_motion_with_block_vcol(ed, Motion::WordBack, count);
3375 }
3376 crate::MotionKind::BigWordBackward => {
3377 execute_motion_with_block_vcol(ed, Motion::BigWordBack, count);
3378 }
3379 crate::MotionKind::WordEnd => {
3380 execute_motion_with_block_vcol(ed, Motion::WordEnd, count);
3381 }
3382 crate::MotionKind::BigWordEnd => {
3383 execute_motion_with_block_vcol(ed, Motion::BigWordEnd, count);
3384 }
3385 crate::MotionKind::LineStart => {
3386 execute_motion_with_block_vcol(ed, Motion::LineStart, 1);
3389 }
3390 crate::MotionKind::FirstNonBlank => {
3391 execute_motion_with_block_vcol(ed, Motion::FirstNonBlank, 1);
3394 }
3395 crate::MotionKind::GotoLine => {
3396 execute_motion_with_block_vcol(ed, Motion::FileBottom, count);
3405 }
3406 crate::MotionKind::LineEnd => {
3407 execute_motion_with_block_vcol(ed, Motion::LineEnd, 1);
3411 }
3412 crate::MotionKind::FindRepeat => {
3413 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: false }, count);
3417 }
3418 crate::MotionKind::FindRepeatReverse => {
3419 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: true }, count);
3423 }
3424 crate::MotionKind::BracketMatch => {
3425 execute_motion_with_block_vcol(ed, Motion::MatchBracket, count);
3430 }
3431 crate::MotionKind::ViewportTop => {
3432 execute_motion_with_block_vcol(ed, Motion::ViewportTop, count);
3435 }
3436 crate::MotionKind::ViewportMiddle => {
3437 execute_motion_with_block_vcol(ed, Motion::ViewportMiddle, count);
3440 }
3441 crate::MotionKind::ViewportBottom => {
3442 execute_motion_with_block_vcol(ed, Motion::ViewportBottom, count);
3445 }
3446 crate::MotionKind::HalfPageDown => {
3447 scroll_cursor_rows(ed, viewport_half_rows(ed, count) as isize);
3451 }
3452 crate::MotionKind::HalfPageUp => {
3453 scroll_cursor_rows(ed, -(viewport_half_rows(ed, count) as isize));
3456 }
3457 crate::MotionKind::FullPageDown => {
3458 scroll_cursor_rows(ed, viewport_full_rows(ed, count) as isize);
3461 }
3462 crate::MotionKind::FullPageUp => {
3463 scroll_cursor_rows(ed, -(viewport_full_rows(ed, count) as isize));
3466 }
3467 crate::MotionKind::FirstNonBlankLine => {
3468 execute_motion_with_block_vcol(ed, Motion::FirstNonBlankLine, count);
3469 }
3470 crate::MotionKind::SectionBackward => {
3471 execute_motion_with_block_vcol(ed, Motion::SectionBackward, count);
3472 }
3473 crate::MotionKind::SectionForward => {
3474 execute_motion_with_block_vcol(ed, Motion::SectionForward, count);
3475 }
3476 crate::MotionKind::SectionEndBackward => {
3477 execute_motion_with_block_vcol(ed, Motion::SectionEndBackward, count);
3478 }
3479 crate::MotionKind::SectionEndForward => {
3480 execute_motion_with_block_vcol(ed, Motion::SectionEndForward, count);
3481 }
3482 }
3483}
3484
3485fn apply_sticky_col<H: crate::types::Host>(
3490 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3491 motion: &Motion,
3492 pre_col: usize,
3493) {
3494 if is_vertical_motion(motion) {
3495 let want = ed.sticky_col.unwrap_or(pre_col);
3496 ed.sticky_col = Some(want);
3499 let (row, _) = ed.cursor();
3500 let line_len = buf_line_chars(&ed.buffer, row);
3501 let max_col = line_len.saturating_sub(1);
3505 let target = want.min(max_col);
3506 buf_set_cursor_rc(&mut ed.buffer, row, target);
3510 } else {
3511 ed.sticky_col = Some(ed.cursor().1);
3514 }
3515}
3516
3517fn is_vertical_motion(motion: &Motion) -> bool {
3518 matches!(
3522 motion,
3523 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown
3524 )
3525}
3526
3527fn apply_motion_cursor<H: crate::types::Host>(
3528 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3529 motion: &Motion,
3530 count: usize,
3531) {
3532 apply_motion_cursor_ctx(ed, motion, count, false)
3533}
3534
3535pub(crate) fn apply_motion_cursor_ctx<H: crate::types::Host>(
3536 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3537 motion: &Motion,
3538 count: usize,
3539 as_operator: bool,
3540) {
3541 match motion {
3542 Motion::Left => {
3543 crate::motions::move_left(&mut ed.buffer, count);
3545 ed.push_buffer_cursor_to_textarea();
3546 }
3547 Motion::Right => {
3548 if as_operator {
3552 crate::motions::move_right_to_end(&mut ed.buffer, count);
3553 } else {
3554 crate::motions::move_right_in_line(&mut ed.buffer, count);
3555 }
3556 ed.push_buffer_cursor_to_textarea();
3557 }
3558 Motion::SpaceFwd => {
3559 if as_operator {
3562 crate::motions::move_right_to_end(&mut ed.buffer, count);
3563 } else {
3564 crate::motions::move_space_fwd(&mut ed.buffer, count);
3565 }
3566 ed.push_buffer_cursor_to_textarea();
3567 }
3568 Motion::BackspaceBack => {
3569 if as_operator {
3572 crate::motions::move_left(&mut ed.buffer, count);
3573 } else {
3574 crate::motions::move_backspace_back(&mut ed.buffer, count);
3575 }
3576 ed.push_buffer_cursor_to_textarea();
3577 }
3578 Motion::Up => {
3579 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3583 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
3584 ed.push_buffer_cursor_to_textarea();
3585 }
3586 Motion::Down => {
3587 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3588 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
3589 ed.push_buffer_cursor_to_textarea();
3590 }
3591 Motion::ScreenUp => {
3592 let v = *ed.host.viewport();
3593 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3594 crate::motions::move_screen_up(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
3595 ed.push_buffer_cursor_to_textarea();
3596 }
3597 Motion::ScreenDown => {
3598 let v = *ed.host.viewport();
3599 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3600 crate::motions::move_screen_down(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
3601 ed.push_buffer_cursor_to_textarea();
3602 }
3603 Motion::WordFwd => {
3604 crate::motions::move_word_fwd(&mut ed.buffer, false, count, &ed.settings.iskeyword);
3605 ed.push_buffer_cursor_to_textarea();
3606 }
3607 Motion::WordBack => {
3608 crate::motions::move_word_back(&mut ed.buffer, false, count, &ed.settings.iskeyword);
3609 ed.push_buffer_cursor_to_textarea();
3610 }
3611 Motion::WordEnd => {
3612 crate::motions::move_word_end(&mut ed.buffer, false, count, &ed.settings.iskeyword);
3613 ed.push_buffer_cursor_to_textarea();
3614 }
3615 Motion::BigWordFwd => {
3616 crate::motions::move_word_fwd(&mut ed.buffer, true, count, &ed.settings.iskeyword);
3617 ed.push_buffer_cursor_to_textarea();
3618 }
3619 Motion::BigWordBack => {
3620 crate::motions::move_word_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
3621 ed.push_buffer_cursor_to_textarea();
3622 }
3623 Motion::BigWordEnd => {
3624 crate::motions::move_word_end(&mut ed.buffer, true, count, &ed.settings.iskeyword);
3625 ed.push_buffer_cursor_to_textarea();
3626 }
3627 Motion::WordEndBack => {
3628 crate::motions::move_word_end_back(
3629 &mut ed.buffer,
3630 false,
3631 count,
3632 &ed.settings.iskeyword,
3633 );
3634 ed.push_buffer_cursor_to_textarea();
3635 }
3636 Motion::BigWordEndBack => {
3637 crate::motions::move_word_end_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
3638 ed.push_buffer_cursor_to_textarea();
3639 }
3640 Motion::LineStart => {
3641 crate::motions::move_line_start(&mut ed.buffer);
3642 ed.push_buffer_cursor_to_textarea();
3643 }
3644 Motion::FirstNonBlank => {
3645 crate::motions::move_first_non_blank(&mut ed.buffer);
3646 ed.push_buffer_cursor_to_textarea();
3647 }
3648 Motion::LineEnd => {
3649 crate::motions::move_line_end(&mut ed.buffer);
3651 ed.push_buffer_cursor_to_textarea();
3652 }
3653 Motion::FileTop => {
3654 if count > 1 {
3657 crate::motions::move_bottom(&mut ed.buffer, count);
3658 } else {
3659 crate::motions::move_top(&mut ed.buffer);
3660 }
3661 ed.push_buffer_cursor_to_textarea();
3662 }
3663 Motion::FileBottom => {
3664 if count > 1 {
3667 crate::motions::move_bottom(&mut ed.buffer, count);
3668 } else {
3669 crate::motions::move_bottom(&mut ed.buffer, 0);
3670 }
3671 ed.push_buffer_cursor_to_textarea();
3672 }
3673 Motion::Find { ch, forward, till } => {
3674 for _ in 0..count {
3675 if !find_char_on_line(ed, *ch, *forward, *till) {
3676 break;
3677 }
3678 }
3679 }
3680 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
3682 let _ = matching_bracket(ed);
3683 }
3684 Motion::UnmatchedBracket { forward, open } => {
3685 goto_unmatched_bracket(ed, *forward, *open, count);
3686 }
3687 Motion::WordAtCursor {
3688 forward,
3689 whole_word,
3690 } => {
3691 word_at_cursor_search(ed, *forward, *whole_word, count);
3692 }
3693 Motion::SearchNext { reverse } => {
3694 if let Some(pattern) = ed.vim.last_search.clone() {
3698 ed.push_search_pattern(&pattern);
3699 }
3700 if ed.search_state().pattern.is_none() {
3701 return;
3702 }
3703 let forward = ed.vim.last_search_forward != *reverse;
3707 for _ in 0..count.max(1) {
3708 if forward {
3709 ed.search_advance_forward(true);
3710 } else {
3711 ed.search_advance_backward(true);
3712 }
3713 }
3714 ed.push_buffer_cursor_to_textarea();
3715 }
3716 Motion::ViewportTop => {
3717 let v = *ed.host().viewport();
3718 crate::motions::move_viewport_top(&mut ed.buffer, &v, count.saturating_sub(1));
3719 ed.push_buffer_cursor_to_textarea();
3720 }
3721 Motion::ViewportMiddle => {
3722 let v = *ed.host().viewport();
3723 crate::motions::move_viewport_middle(&mut ed.buffer, &v);
3724 ed.push_buffer_cursor_to_textarea();
3725 }
3726 Motion::ViewportBottom => {
3727 let v = *ed.host().viewport();
3728 crate::motions::move_viewport_bottom(&mut ed.buffer, &v, count.saturating_sub(1));
3729 ed.push_buffer_cursor_to_textarea();
3730 }
3731 Motion::LastNonBlank => {
3732 crate::motions::move_last_non_blank(&mut ed.buffer);
3733 ed.push_buffer_cursor_to_textarea();
3734 }
3735 Motion::LineMiddle => {
3736 let row = ed.cursor().0;
3737 let line_chars = buf_line_chars(&ed.buffer, row);
3738 let target = line_chars / 2;
3741 ed.jump_cursor(row, target);
3742 }
3743 Motion::ScreenLineMiddle => {
3744 let row = ed.cursor().0;
3747 let width = ed.host().viewport().width as usize;
3748 let last = buf_line_chars(&ed.buffer, row).saturating_sub(1);
3749 let target = (width / 2).min(last);
3750 ed.jump_cursor(row, target);
3751 }
3752 Motion::ParagraphPrev => {
3753 crate::motions::move_paragraph_prev(&mut ed.buffer, count);
3754 ed.push_buffer_cursor_to_textarea();
3755 }
3756 Motion::ParagraphNext => {
3757 crate::motions::move_paragraph_next(&mut ed.buffer, count);
3758 ed.push_buffer_cursor_to_textarea();
3759 }
3760 Motion::SentencePrev => {
3761 for _ in 0..count.max(1) {
3762 if let Some((row, col)) = sentence_boundary(ed, false) {
3763 ed.jump_cursor(row, col);
3764 }
3765 }
3766 }
3767 Motion::SentenceNext => {
3768 for _ in 0..count.max(1) {
3769 if let Some((row, col)) = sentence_boundary(ed, true) {
3770 ed.jump_cursor(row, col);
3771 }
3772 }
3773 }
3774 Motion::SectionBackward => {
3775 crate::motions::move_section_backward(&mut ed.buffer, count);
3776 ed.push_buffer_cursor_to_textarea();
3777 }
3778 Motion::SectionForward => {
3779 crate::motions::move_section_forward(&mut ed.buffer, count);
3780 ed.push_buffer_cursor_to_textarea();
3781 }
3782 Motion::SectionEndBackward => {
3783 crate::motions::move_section_end_backward(&mut ed.buffer, count);
3784 ed.push_buffer_cursor_to_textarea();
3785 }
3786 Motion::SectionEndForward => {
3787 crate::motions::move_section_end_forward(&mut ed.buffer, count);
3788 ed.push_buffer_cursor_to_textarea();
3789 }
3790 Motion::FirstNonBlankNextLine => {
3791 crate::motions::move_first_non_blank_next_line(&mut ed.buffer, count);
3792 ed.push_buffer_cursor_to_textarea();
3793 }
3794 Motion::FirstNonBlankPrevLine => {
3795 crate::motions::move_first_non_blank_prev_line(&mut ed.buffer, count);
3796 ed.push_buffer_cursor_to_textarea();
3797 }
3798 Motion::FirstNonBlankLine => {
3799 crate::motions::move_first_non_blank_line(&mut ed.buffer, count);
3800 ed.push_buffer_cursor_to_textarea();
3801 }
3802 Motion::GotoColumn => {
3803 crate::motions::move_goto_column(&mut ed.buffer, count);
3804 ed.push_buffer_cursor_to_textarea();
3805 }
3806 }
3807}
3808
3809fn move_first_non_whitespace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
3810 ed.sync_buffer_content_from_textarea();
3816 crate::motions::move_first_non_blank(&mut ed.buffer);
3817 ed.push_buffer_cursor_to_textarea();
3818}
3819
3820fn find_char_on_line<H: crate::types::Host>(
3821 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3822 ch: char,
3823 forward: bool,
3824 till: bool,
3825) -> bool {
3826 let moved = crate::motions::find_char_on_line(&mut ed.buffer, ch, forward, till);
3827 if moved {
3828 ed.push_buffer_cursor_to_textarea();
3829 }
3830 moved
3831}
3832
3833fn matching_bracket<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
3834 let moved = crate::motions::match_bracket(&mut ed.buffer);
3835 if moved {
3836 ed.push_buffer_cursor_to_textarea();
3837 }
3838 moved
3839}
3840
3841fn goto_unmatched_bracket<H: crate::types::Host>(
3845 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3846 forward: bool,
3847 open: char,
3848 count: usize,
3849) {
3850 let close = match open {
3851 '(' => ')',
3852 '{' => '}',
3853 _ => return,
3854 };
3855 let cursor = buf_cursor_pos(&ed.buffer);
3856 let rows = buf_row_count(&ed.buffer);
3857 let target = count.max(1);
3858 let mut found = 0usize;
3859 let mut depth = 0i32;
3860
3861 if forward {
3862 let mut r = cursor.row;
3863 let mut from_col = cursor.col + 1;
3864 while r < rows {
3865 let line: Vec<char> = buf_line(&ed.buffer, r)
3866 .unwrap_or_default()
3867 .chars()
3868 .collect();
3869 let mut ci = from_col;
3870 while ci < line.len() {
3871 let ch = line[ci];
3872 if ch == open {
3873 depth += 1;
3874 } else if ch == close {
3875 if depth == 0 {
3876 found += 1;
3877 if found == target {
3878 buf_set_cursor_rc(&mut ed.buffer, r, ci);
3879 ed.push_buffer_cursor_to_textarea();
3880 return;
3881 }
3882 } else {
3883 depth -= 1;
3884 }
3885 }
3886 ci += 1;
3887 }
3888 r += 1;
3889 from_col = 0;
3890 }
3891 } else {
3892 let mut r = cursor.row as isize;
3893 let mut from_col = cursor.col as isize - 1;
3896 while r >= 0 {
3897 let line: Vec<char> = buf_line(&ed.buffer, r as usize)
3898 .unwrap_or_default()
3899 .chars()
3900 .collect();
3901 let mut ci = from_col.min(line.len() as isize - 1);
3902 while ci >= 0 {
3903 let ch = line[ci as usize];
3904 if ch == close {
3905 depth += 1;
3906 } else if ch == open {
3907 if depth == 0 {
3908 found += 1;
3909 if found == target {
3910 buf_set_cursor_rc(&mut ed.buffer, r as usize, ci as usize);
3911 ed.push_buffer_cursor_to_textarea();
3912 return;
3913 }
3914 } else {
3915 depth -= 1;
3916 }
3917 }
3918 ci -= 1;
3919 }
3920 r -= 1;
3921 from_col = isize::MAX;
3922 }
3923 }
3924}
3925
3926fn word_at_cursor_search<H: crate::types::Host>(
3927 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3928 forward: bool,
3929 whole_word: bool,
3930 count: usize,
3931) {
3932 let (row, col) = ed.cursor();
3933 let line: String = buf_line(&ed.buffer, row).unwrap_or_default();
3934 let chars: Vec<char> = line.chars().collect();
3935 if chars.is_empty() {
3936 return;
3937 }
3938 let spec = ed.settings().iskeyword.clone();
3940 let is_word = |c: char| is_keyword_char(c, &spec);
3941 let mut start = col.min(chars.len().saturating_sub(1));
3942 while start > 0 && is_word(chars[start - 1]) {
3943 start -= 1;
3944 }
3945 let mut end = start;
3946 while end < chars.len() && is_word(chars[end]) {
3947 end += 1;
3948 }
3949 if end <= start {
3950 return;
3951 }
3952 let word: String = chars[start..end].iter().collect();
3953 let escaped = regex_escape(&word);
3954 let pattern = if whole_word {
3955 format!(r"\b{escaped}\b")
3956 } else {
3957 escaped
3958 };
3959 ed.push_search_pattern(&pattern);
3960 if ed.search_state().pattern.is_none() {
3961 return;
3962 }
3963 ed.vim.last_search = Some(pattern);
3965 ed.vim.last_search_forward = forward;
3966 for _ in 0..count.max(1) {
3967 if forward {
3968 ed.search_advance_forward(true);
3969 } else {
3970 ed.search_advance_backward(true);
3971 }
3972 }
3973 ed.push_buffer_cursor_to_textarea();
3974}
3975
3976fn regex_escape(s: &str) -> String {
3977 let mut out = String::with_capacity(s.len());
3978 for c in s.chars() {
3979 if matches!(
3980 c,
3981 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
3982 ) {
3983 out.push('\\');
3984 }
3985 out.push(c);
3986 }
3987 out
3988}
3989
3990pub(crate) fn apply_op_motion_key<H: crate::types::Host>(
4004 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4005 op: Operator,
4006 motion_key: char,
4007 total_count: usize,
4008) {
4009 let input = Input {
4010 key: Key::Char(motion_key),
4011 ctrl: false,
4012 alt: false,
4013 shift: false,
4014 };
4015 let Some(motion) = parse_motion(&input) else {
4016 return;
4017 };
4018 let motion = match motion {
4019 Motion::FindRepeat { reverse } => match ed.vim.last_find {
4020 Some((ch, forward, till)) => Motion::Find {
4021 ch,
4022 forward: if reverse { !forward } else { forward },
4023 till,
4024 },
4025 None => return,
4026 },
4027 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
4029 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
4030 m => m,
4031 };
4032 apply_op_with_motion(ed, op, &motion, total_count);
4033 if let Motion::Find { ch, forward, till } = &motion {
4034 ed.vim.last_find = Some((*ch, *forward, *till));
4035 }
4036 if !ed.vim.replaying && op_is_change(op) {
4037 ed.vim.last_change = Some(LastChange::OpMotion {
4038 op,
4039 motion,
4040 count: total_count,
4041 inserted: None,
4042 });
4043 }
4044}
4045
4046pub(crate) fn apply_op_double<H: crate::types::Host>(
4049 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4050 op: Operator,
4051 total_count: usize,
4052) {
4053 if op == Operator::Comment {
4054 let row = buf_cursor_pos(&ed.buffer).row;
4056 let end_row = (row + total_count.max(1) - 1).min(ed.buffer.row_count().saturating_sub(1));
4057 ed.toggle_comment_range(row, end_row);
4058 ed.vim.mode = Mode::Normal;
4059 if !ed.vim.replaying {
4060 ed.vim.last_change = Some(LastChange::LineOp {
4061 op,
4062 count: total_count,
4063 inserted: None,
4064 });
4065 }
4066 return;
4067 }
4068 execute_line_op(ed, op, total_count);
4069 if !ed.vim.replaying {
4070 ed.vim.last_change = Some(LastChange::LineOp {
4071 op,
4072 count: total_count,
4073 inserted: None,
4074 });
4075 }
4076}
4077
4078fn gn_find_range<H: crate::types::Host>(
4083 ed: &Editor<hjkl_buffer::Buffer, H>,
4084 re: ®ex::Regex,
4085 forward: bool,
4086) -> Option<(crate::types::Pos, crate::types::Pos)> {
4087 use crate::types::{Cursor, Pos, Search};
4088 let cursor = Cursor::cursor(&ed.buffer);
4089 let contains =
4090 Search::find_prev(&ed.buffer, cursor, re).filter(|m| m.start <= cursor && cursor < m.end);
4091 let range = if let Some(m) = contains {
4092 m
4093 } else if forward {
4094 Search::find_next(&ed.buffer, cursor, re)?
4095 } else {
4096 Search::find_prev(&ed.buffer, cursor, re)?
4097 };
4098 let end_incl = if range.end.col > 0 {
4099 Pos::new(range.end.line, range.end.col - 1)
4100 } else {
4101 range.end
4102 };
4103 Some((range.start, end_incl))
4104}
4105
4106pub(crate) fn gn_operate<H: crate::types::Host>(
4111 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4112 op: Option<Operator>,
4113 forward: bool,
4114 count: usize,
4115) {
4116 use crate::types::{Cursor, Pos};
4117 if let Some(p) = ed.vim.last_search.clone() {
4119 ed.push_search_pattern(&p);
4120 }
4121 let Some(re) = ed.search_state().pattern.clone() else {
4122 return;
4123 };
4124 ed.sync_buffer_content_from_textarea();
4125
4126 let Some(mut range) = gn_find_range(ed, &re, forward) else {
4127 return;
4128 };
4129 for _ in 1..count.max(1) {
4131 let past = Pos::new(range.1.line, range.1.col + 1);
4132 Cursor::set_cursor(&mut ed.buffer, past);
4133 match gn_find_range(ed, &re, forward) {
4134 Some(r) => range = r,
4135 None => break,
4136 }
4137 }
4138 let start_t = (range.0.line as usize, range.0.col as usize);
4139 let end_t = (range.1.line as usize, range.1.col as usize);
4140
4141 match op {
4142 None => {
4143 ed.vim.visual_anchor = start_t;
4145 buf_set_cursor_rc(&mut ed.buffer, end_t.0, end_t.1);
4146 ed.vim.mode = Mode::Visual;
4147 ed.vim.current_mode = crate::VimMode::Visual;
4148 ed.push_buffer_cursor_to_textarea();
4149 }
4150 Some(Operator::Delete) => {
4151 ed.push_undo();
4152 cut_vim_range(ed, start_t, end_t, RangeKind::Inclusive);
4153 clamp_cursor_to_normal_mode(ed);
4156 ed.push_buffer_cursor_to_textarea();
4157 if !ed.vim.replaying {
4158 ed.vim.last_change = Some(LastChange::GnOp {
4159 op: Operator::Delete,
4160 forward,
4161 inserted: None,
4162 });
4163 }
4164 }
4165 Some(Operator::Change) => {
4166 ed.push_undo();
4167 ed.vim.change_mark_start = Some(start_t);
4168 cut_vim_range(ed, start_t, end_t, RangeKind::Inclusive);
4169 if !ed.vim.replaying {
4170 ed.vim.last_change = Some(LastChange::GnOp {
4171 op: Operator::Change,
4172 forward,
4173 inserted: None,
4174 });
4175 }
4176 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4177 }
4178 Some(Operator::Yank) => {
4179 let text = read_vim_range(ed, start_t, end_t, RangeKind::Inclusive);
4180 if !text.is_empty() {
4181 ed.record_yank_to_host(text.clone());
4182 ed.record_yank(text, false);
4183 }
4184 buf_set_cursor_rc(&mut ed.buffer, start_t.0, start_t.1);
4185 ed.push_buffer_cursor_to_textarea();
4186 }
4187 Some(other @ (Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase)) => {
4188 ed.push_undo();
4191 apply_case_op_to_selection(ed, other, start_t, end_t, RangeKind::Inclusive);
4192 }
4193 Some(_) => {}
4194 }
4195}
4196
4197pub(crate) fn apply_op_g_inner<H: crate::types::Host>(
4208 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4209 op: Operator,
4210 ch: char,
4211 total_count: usize,
4212) {
4213 if matches!(
4216 op,
4217 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase | Operator::Rot13
4218 ) {
4219 let op_char = match op {
4220 Operator::Uppercase => 'U',
4221 Operator::Lowercase => 'u',
4222 Operator::ToggleCase => '~',
4223 Operator::Rot13 => '?',
4224 _ => unreachable!(),
4225 };
4226 if ch == op_char {
4227 execute_line_op(ed, op, total_count);
4228 if !ed.vim.replaying {
4229 ed.vim.last_change = Some(LastChange::LineOp {
4230 op,
4231 count: total_count,
4232 inserted: None,
4233 });
4234 }
4235 return;
4236 }
4237 }
4238 if ch == 'n' || ch == 'N' {
4240 gn_operate(ed, Some(op), ch == 'n', total_count);
4241 return;
4242 }
4243 let motion = match ch {
4244 'g' => Motion::FileTop,
4245 'e' => Motion::WordEndBack,
4246 'E' => Motion::BigWordEndBack,
4247 'j' => Motion::ScreenDown,
4248 'k' => Motion::ScreenUp,
4249 _ => return, };
4251 apply_op_with_motion(ed, op, &motion, total_count);
4252 if !ed.vim.replaying && op_is_change(op) {
4253 ed.vim.last_change = Some(LastChange::OpMotion {
4254 op,
4255 motion,
4256 count: total_count,
4257 inserted: None,
4258 });
4259 }
4260}
4261
4262pub(crate) fn apply_after_g<H: crate::types::Host>(
4267 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4268 ch: char,
4269 count: usize,
4270) {
4271 match ch {
4272 'g' => {
4273 let pre = ed.cursor();
4275 if count > 1 {
4276 ed.jump_cursor(count - 1, 0);
4277 } else {
4278 ed.jump_cursor(0, 0);
4279 }
4280 move_first_non_whitespace(ed);
4281 ed.sticky_col = Some(ed.cursor().1);
4284 if ed.cursor() != pre {
4285 ed.push_jump(pre);
4286 }
4287 }
4288 'e' => execute_motion(ed, Motion::WordEndBack, count),
4289 'E' => execute_motion(ed, Motion::BigWordEndBack, count),
4290 '_' => execute_motion(ed, Motion::LastNonBlank, count),
4292 'M' => execute_motion(ed, Motion::LineMiddle, count),
4294 'm' => execute_motion(ed, Motion::ScreenLineMiddle, count),
4296 'v' => ed.reenter_last_visual(),
4299 'j' => execute_motion(ed, Motion::ScreenDown, count),
4303 'k' => execute_motion(ed, Motion::ScreenUp, count),
4304 'U' => {
4308 ed.vim.pending = Pending::Op {
4309 op: Operator::Uppercase,
4310 count1: count,
4311 };
4312 }
4313 'u' => {
4314 ed.vim.pending = Pending::Op {
4315 op: Operator::Lowercase,
4316 count1: count,
4317 };
4318 }
4319 '~' => {
4320 ed.vim.pending = Pending::Op {
4321 op: Operator::ToggleCase,
4322 count1: count,
4323 };
4324 }
4325 '?' => {
4326 ed.vim.pending = Pending::Op {
4328 op: Operator::Rot13,
4329 count1: count,
4330 };
4331 }
4332 'q' => {
4333 ed.vim.pending = Pending::Op {
4336 op: Operator::Reflow,
4337 count1: count,
4338 };
4339 }
4340 'w' => {
4341 ed.vim.pending = Pending::Op {
4344 op: Operator::ReflowKeepCursor,
4345 count1: count,
4346 };
4347 }
4348 'J' => {
4349 let joins = count.max(2) - 1;
4352 for _ in 0..joins {
4353 ed.push_undo();
4354 join_line_raw(ed);
4355 }
4356 if !ed.vim.replaying {
4357 ed.vim.last_change = Some(LastChange::JoinLine { count: joins });
4358 }
4359 }
4360 'd' => {
4361 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
4366 }
4367 'i' => {
4372 if let Some((row, col)) = ed.vim.last_insert_pos {
4373 ed.jump_cursor(row, col);
4374 }
4375 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
4376 }
4377 'c' => {
4382 ed.vim.pending = Pending::Op {
4383 op: Operator::Comment,
4384 count1: count,
4385 };
4386 }
4387 'p' => paste_bridge(ed, false, count.max(1), true, false),
4390 'P' => paste_bridge(ed, true, count.max(1), true, false),
4391 'n' => gn_operate(ed, None, true, count.max(1)),
4393 'N' => gn_operate(ed, None, false, count.max(1)),
4394 ';' => walk_change_list(ed, -1, count.max(1)),
4397 ',' => walk_change_list(ed, 1, count.max(1)),
4398 '*' => execute_motion(
4402 ed,
4403 Motion::WordAtCursor {
4404 forward: true,
4405 whole_word: false,
4406 },
4407 count,
4408 ),
4409 '#' => execute_motion(
4410 ed,
4411 Motion::WordAtCursor {
4412 forward: false,
4413 whole_word: false,
4414 },
4415 count,
4416 ),
4417 '&' => {
4420 let cmd = match ed.vim.last_substitute.clone() {
4421 Some(c) => c,
4422 None => {
4423 return;
4428 }
4429 };
4430 let last_row = buf_row_count(&ed.buffer).saturating_sub(1) as u32;
4431 let r = 0u32..=last_row;
4432 let _ = crate::substitute::apply_substitute(ed, &cmd, r);
4435 ed.vim.last_substitute = Some(cmd);
4438 }
4439 _ => {}
4440 }
4441}
4442
4443pub(crate) fn ampersand_repeat<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
4447 let Some(mut cmd) = ed.vim.last_substitute.clone() else {
4448 return;
4449 };
4450 cmd.flags = crate::substitute::SubstFlags::default();
4451 let row = buf_cursor_pos(&ed.buffer).row as u32;
4452 let _ = crate::substitute::apply_substitute(ed, &cmd, row..=row);
4453}
4454
4455pub(crate) fn apply_after_z<H: crate::types::Host>(
4460 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4461 ch: char,
4462 count: usize,
4463) {
4464 use crate::editor::CursorScrollTarget;
4465 let row = ed.cursor().0;
4466 match ch {
4467 'z' => {
4468 ed.scroll_cursor_to(CursorScrollTarget::Center);
4469 ed.vim.viewport_pinned = true;
4470 ed.vim.scroll_anim_hint = true;
4471 }
4472 't' => {
4473 ed.scroll_cursor_to(CursorScrollTarget::Top);
4474 ed.vim.viewport_pinned = true;
4475 ed.vim.scroll_anim_hint = true;
4476 }
4477 'b' => {
4478 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
4479 ed.vim.viewport_pinned = true;
4480 ed.vim.scroll_anim_hint = true;
4481 }
4482 'o' => {
4487 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
4488 }
4489 'c' => {
4490 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
4491 }
4492 'a' => {
4493 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
4494 }
4495 'R' => {
4496 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
4497 }
4498 'M' => {
4499 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
4500 }
4501 'E' => {
4502 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
4503 }
4504 'd' => {
4505 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
4506 }
4507 'f' => {
4508 if matches!(
4509 ed.vim.mode,
4510 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
4511 ) {
4512 let anchor_row = match ed.vim.mode {
4515 Mode::VisualLine => ed.vim.visual_line_anchor,
4516 Mode::VisualBlock => ed.vim.block_anchor.0,
4517 _ => ed.vim.visual_anchor.0,
4518 };
4519 let cur = ed.cursor().0;
4520 let top = anchor_row.min(cur);
4521 let bot = anchor_row.max(cur);
4522 ed.apply_fold_op(crate::types::FoldOp::Add {
4523 start_row: top,
4524 end_row: bot,
4525 closed: true,
4526 });
4527 ed.vim.mode = Mode::Normal;
4528 } else {
4529 ed.vim.pending = Pending::Op {
4534 op: Operator::Fold,
4535 count1: count,
4536 };
4537 }
4538 }
4539 _ => {}
4540 }
4541}
4542
4543pub(crate) fn apply_find_char<H: crate::types::Host>(
4549 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4550 ch: char,
4551 forward: bool,
4552 till: bool,
4553 count: usize,
4554) {
4555 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
4556 ed.vim.last_find = Some((ch, forward, till));
4557 ed.vim.last_horizontal_motion = LastHorizontalMotion::FindChar;
4558}
4559
4560pub(crate) fn apply_sneak<H: crate::types::Host>(
4572 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4573 c1: char,
4574 c2: char,
4575 forward: bool,
4576 count: usize,
4577) {
4578 let count = count.max(1);
4579 let (start_row, start_col) = ed.cursor();
4580 let row_count = buf_row_count(&ed.buffer);
4581
4582 let result = if forward {
4583 sneak_scan_forward(ed, start_row, start_col, c1, c2, count)
4584 } else {
4585 sneak_scan_backward(ed, start_row, start_col, c1, c2, count)
4586 };
4587
4588 if let Some((row, col)) = result {
4589 buf_set_cursor_rc(&mut ed.buffer, row, col);
4590 ed.push_buffer_cursor_to_textarea();
4591 let _ = row_count; }
4593
4594 ed.vim.last_sneak = Some(((c1, c2), forward));
4595 ed.vim.last_horizontal_motion = LastHorizontalMotion::Sneak;
4596}
4597
4598fn sneak_scan_forward<H: crate::types::Host>(
4601 ed: &Editor<hjkl_buffer::Buffer, H>,
4602 start_row: usize,
4603 start_col: usize,
4604 c1: char,
4605 c2: char,
4606 count: usize,
4607) -> Option<(usize, usize)> {
4608 let row_count = buf_row_count(&ed.buffer);
4609 let mut hits = 0usize;
4610 for row in start_row..row_count {
4611 let line = buf_line(&ed.buffer, row).unwrap_or_default();
4612 let chars: Vec<char> = line.chars().collect();
4613 let col_start = if row == start_row { start_col + 1 } else { 0 };
4615 if col_start + 1 > chars.len() {
4616 continue;
4617 }
4618 for col in col_start..chars.len().saturating_sub(1) {
4619 if chars[col] == c1 && chars[col + 1] == c2 {
4620 hits += 1;
4621 if hits == count {
4622 return Some((row, col));
4623 }
4624 }
4625 }
4626 }
4627 None
4628}
4629
4630fn sneak_scan_backward<H: crate::types::Host>(
4633 ed: &Editor<hjkl_buffer::Buffer, H>,
4634 start_row: usize,
4635 start_col: usize,
4636 c1: char,
4637 c2: char,
4638 count: usize,
4639) -> Option<(usize, usize)> {
4640 let row_count = buf_row_count(&ed.buffer);
4641 let mut hits = 0usize;
4642 let rows_to_scan = (0..row_count).rev().skip(row_count - start_row - 1);
4644 for row in rows_to_scan {
4645 let line = buf_line(&ed.buffer, row).unwrap_or_default();
4646 let chars: Vec<char> = line.chars().collect();
4647 let col_end = if row == start_row {
4649 start_col.saturating_sub(1)
4650 } else if chars.is_empty() {
4651 continue;
4652 } else {
4653 chars.len().saturating_sub(1)
4654 };
4655 if col_end == 0 {
4656 continue;
4657 }
4658 for col in (0..col_end).rev() {
4660 if col + 1 < chars.len() && chars[col] == c1 && chars[col + 1] == c2 {
4661 hits += 1;
4662 if hits == count {
4663 return Some((row, col));
4664 }
4665 }
4666 }
4667 }
4668 None
4669}
4670
4671pub(crate) fn apply_op_sneak<H: crate::types::Host>(
4678 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4679 op: Operator,
4680 c1: char,
4681 c2: char,
4682 forward: bool,
4683 total_count: usize,
4684) {
4685 let start = ed.cursor();
4686 let result = if forward {
4687 sneak_scan_forward(ed, start.0, start.1, c1, c2, total_count)
4688 } else {
4689 sneak_scan_backward(ed, start.0, start.1, c1, c2, total_count)
4690 };
4691 let Some(end) = result else {
4692 return;
4693 };
4694 ed.jump_cursor(end.0, end.1);
4697 let end_cur = ed.cursor();
4698 ed.jump_cursor(start.0, start.1);
4699 run_operator_over_range(ed, op, start, end_cur, RangeKind::Exclusive);
4700 ed.vim.last_sneak = Some(((c1, c2), forward));
4701 ed.vim.last_horizontal_motion = LastHorizontalMotion::Sneak;
4702 if !ed.vim.replaying && op_is_change(op) {
4703 }
4707}
4708
4709pub(crate) fn apply_op_find_motion<H: crate::types::Host>(
4715 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4716 op: Operator,
4717 ch: char,
4718 forward: bool,
4719 till: bool,
4720 total_count: usize,
4721) {
4722 let motion = Motion::Find { ch, forward, till };
4723 apply_op_with_motion(ed, op, &motion, total_count);
4724 ed.vim.last_find = Some((ch, forward, till));
4725 if !ed.vim.replaying && op_is_change(op) {
4726 ed.vim.last_change = Some(LastChange::OpMotion {
4727 op,
4728 motion,
4729 count: total_count,
4730 inserted: None,
4731 });
4732 }
4733}
4734
4735pub(crate) fn apply_op_text_obj_inner<H: crate::types::Host>(
4744 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4745 op: Operator,
4746 ch: char,
4747 inner: bool,
4748 total_count: usize,
4749) -> bool {
4750 let obj = match ch {
4753 'w' => TextObject::Word { big: false },
4754 'W' => TextObject::Word { big: true },
4755 '"' | '\'' | '`' => TextObject::Quote(ch),
4756 '(' | ')' | 'b' => TextObject::Bracket('('),
4757 '[' | ']' => TextObject::Bracket('['),
4758 '{' | '}' | 'B' => TextObject::Bracket('{'),
4759 '<' | '>' => TextObject::Bracket('<'),
4760 'p' => TextObject::Paragraph,
4761 't' => TextObject::XmlTag,
4762 's' => TextObject::Sentence,
4763 _ => return false,
4764 };
4765 apply_op_with_text_object(ed, op, obj, inner, total_count.max(1));
4766 if !ed.vim.replaying && op_is_change(op) {
4767 ed.vim.last_change = Some(LastChange::OpTextObj {
4768 op,
4769 obj,
4770 inner,
4771 inserted: None,
4772 });
4773 }
4774 true
4775}
4776
4777pub(crate) fn retreat_one<H: crate::types::Host>(
4779 ed: &Editor<hjkl_buffer::Buffer, H>,
4780 pos: (usize, usize),
4781) -> (usize, usize) {
4782 let (r, c) = pos;
4783 if c > 0 {
4784 (r, c - 1)
4785 } else if r > 0 {
4786 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
4787 (r - 1, prev_len)
4788 } else {
4789 (0, 0)
4790 }
4791}
4792
4793fn begin_insert_noundo<H: crate::types::Host>(
4795 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4796 count: usize,
4797 reason: InsertReason,
4798) {
4799 let reason = if ed.vim.replaying {
4800 InsertReason::ReplayOnly
4801 } else {
4802 reason
4803 };
4804 let (row, col) = ed.cursor();
4805 ed.vim.insert_session = Some(InsertSession {
4806 count,
4807 row_min: row,
4808 row_max: row,
4809 before_rope: crate::types::Query::rope(&ed.buffer),
4810 reason,
4811 start_row: row,
4812 start_col: col,
4813 });
4814 ed.vim.mode = Mode::Insert;
4815 ed.vim.current_mode = crate::VimMode::Insert;
4817 drop_blame_if_left_normal(ed);
4818}
4819
4820pub(crate) fn apply_op_with_motion<H: crate::types::Host>(
4823 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4824 op: Operator,
4825 motion: &Motion,
4826 count: usize,
4827) {
4828 let start = ed.cursor();
4829 apply_motion_cursor_ctx(ed, motion, count, true);
4834 let end = ed.cursor();
4835 let kind = motion_kind(motion);
4836 ed.jump_cursor(start.0, start.1);
4838
4839 if op == Operator::Comment {
4841 let top = start.0.min(end.0);
4842 let bot = start.0.max(end.0);
4843 ed.toggle_comment_range(top, bot);
4844 ed.vim.mode = Mode::Normal;
4845 return;
4846 }
4847
4848 run_operator_over_range(ed, op, start, end, kind);
4849}
4850
4851fn apply_op_with_text_object<H: crate::types::Host>(
4852 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4853 op: Operator,
4854 obj: TextObject,
4855 inner: bool,
4856 count: usize,
4857) {
4858 let Some((mut start, mut end, mut kind)) = text_object_range(ed, obj, inner, count) else {
4859 return;
4860 };
4861 if inner
4870 && matches!(obj, TextObject::Bracket(_))
4871 && kind == RangeKind::Exclusive
4872 && end.0 > start.0
4873 && end.1 == 0
4874 {
4875 let prev = end.0 - 1;
4876 let prev_len = buf_line_chars(&ed.buffer, prev);
4877 let fnb = buf_line(&ed.buffer, start.0)
4878 .unwrap_or_default()
4879 .chars()
4880 .take_while(|c| *c == ' ' || *c == '\t')
4881 .count();
4882 if start.1 <= fnb {
4883 start = (start.0, 0);
4884 end = (prev, prev_len);
4885 kind = RangeKind::Linewise;
4886 } else {
4887 end = (prev, prev_len.saturating_sub(1));
4888 kind = RangeKind::Inclusive;
4889 }
4890 }
4891 ed.jump_cursor(start.0, start.1);
4892 run_operator_over_range(ed, op, start, end, kind);
4893}
4894
4895fn motion_kind(motion: &Motion) -> RangeKind {
4896 match motion {
4897 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => RangeKind::Linewise,
4898 Motion::FileTop | Motion::FileBottom => RangeKind::Linewise,
4899 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
4900 RangeKind::Linewise
4901 }
4902 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
4903 RangeKind::Inclusive
4904 }
4905 Motion::Find { .. } => RangeKind::Inclusive,
4906 Motion::MatchBracket => RangeKind::Inclusive,
4907 Motion::UnmatchedBracket { .. } => RangeKind::Exclusive,
4910 Motion::LineEnd => RangeKind::Inclusive,
4912 Motion::FirstNonBlankNextLine
4914 | Motion::FirstNonBlankPrevLine
4915 | Motion::FirstNonBlankLine => RangeKind::Linewise,
4916 Motion::SectionBackward
4918 | Motion::SectionForward
4919 | Motion::SectionEndBackward
4920 | Motion::SectionEndForward => RangeKind::Exclusive,
4921 _ => RangeKind::Exclusive,
4922 }
4923}
4924
4925fn change_linewise_rows<H: crate::types::Host>(
4934 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4935 top_row: usize,
4936 end_row: usize,
4937) {
4938 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4939 ed.vim.change_mark_start = Some((top_row, 0));
4941 ed.push_undo();
4942 ed.sync_buffer_content_from_textarea();
4943 let payload = read_vim_range(ed, (top_row, 0), (end_row, 0), RangeKind::Linewise);
4945 if end_row > top_row {
4947 ed.mutate_edit(Edit::DeleteRange {
4948 start: Position::new(top_row + 1, 0),
4949 end: Position::new(end_row, 0),
4950 kind: BufKind::Line,
4951 });
4952 }
4953 let indent_chars = if ed.settings.autoindent {
4956 let line = hjkl_buffer::rope_line_str(&crate::types::Query::rope(&ed.buffer), top_row);
4957 line.chars().take_while(|c| *c == ' ' || *c == '\t').count()
4958 } else {
4959 0
4960 };
4961 let line_chars = buf_line_chars(&ed.buffer, top_row);
4962 if line_chars > indent_chars {
4963 ed.mutate_edit(Edit::DeleteRange {
4964 start: Position::new(top_row, indent_chars),
4965 end: Position::new(top_row, line_chars),
4966 kind: BufKind::Char,
4967 });
4968 }
4969 if !payload.is_empty() {
4970 ed.record_yank_to_host(payload.clone());
4971 ed.record_delete(payload, true);
4972 }
4973 buf_set_cursor_rc(&mut ed.buffer, top_row, indent_chars);
4974 ed.push_buffer_cursor_to_textarea();
4975 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4976}
4977
4978fn run_operator_over_range<H: crate::types::Host>(
4979 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4980 op: Operator,
4981 start: (usize, usize),
4982 end: (usize, usize),
4983 kind: RangeKind,
4984) {
4985 let (top, bot) = order(start, end);
4986 if top == bot && !matches!(kind, RangeKind::Linewise) {
4991 if op == Operator::Change {
4992 ed.vim.change_mark_start = Some(top);
4993 ed.push_undo();
4994 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4995 }
4996 return;
4997 }
4998
4999 match op {
5000 Operator::Yank => {
5001 let text = read_vim_range(ed, top, bot, kind);
5002 if !text.is_empty() {
5003 ed.record_yank_to_host(text.clone());
5004 ed.record_yank(text, matches!(kind, RangeKind::Linewise));
5005 }
5006 let rbr = match kind {
5010 RangeKind::Linewise => {
5011 let last_col = buf_line_chars(&ed.buffer, bot.0).saturating_sub(1);
5012 (bot.0, last_col)
5013 }
5014 RangeKind::Inclusive => (bot.0, bot.1),
5015 RangeKind::Exclusive => (bot.0, bot.1.saturating_sub(1)),
5016 };
5017 ed.set_mark('[', top);
5018 ed.set_mark(']', rbr);
5019 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
5020 ed.push_buffer_cursor_to_textarea();
5021 }
5022 Operator::Delete => {
5023 ed.push_undo();
5024 cut_vim_range(ed, top, bot, kind);
5025 if !matches!(kind, RangeKind::Linewise) {
5030 clamp_cursor_to_normal_mode(ed);
5031 }
5032 ed.vim.mode = Mode::Normal;
5033 let pos = ed.cursor();
5037 ed.set_mark('[', pos);
5038 ed.set_mark(']', pos);
5039 }
5040 Operator::Change => {
5041 if matches!(kind, RangeKind::Linewise) {
5046 change_linewise_rows(ed, top.0, bot.0);
5050 } else {
5051 ed.vim.change_mark_start = Some(top);
5053 ed.push_undo();
5054 cut_vim_range(ed, top, bot, kind);
5055 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
5056 }
5057 }
5058 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase | Operator::Rot13 => {
5059 apply_case_op_to_selection(ed, op, top, bot, kind);
5060 }
5061 Operator::Indent | Operator::Outdent => {
5062 ed.push_undo();
5065 if op == Operator::Indent {
5066 indent_rows(ed, top.0, bot.0, 1);
5067 } else {
5068 outdent_rows(ed, top.0, bot.0, 1);
5069 }
5070 ed.vim.mode = Mode::Normal;
5071 }
5072 Operator::Fold => {
5073 if bot.0 >= top.0 {
5077 ed.apply_fold_op(crate::types::FoldOp::Add {
5078 start_row: top.0,
5079 end_row: bot.0,
5080 closed: true,
5081 });
5082 }
5083 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
5084 ed.push_buffer_cursor_to_textarea();
5085 ed.vim.mode = Mode::Normal;
5086 }
5087 Operator::Reflow => {
5088 ed.push_undo();
5089 reflow_rows(ed, top.0, bot.0);
5090 ed.vim.mode = Mode::Normal;
5091 }
5092 Operator::ReflowKeepCursor => {
5093 let saved = ed.cursor();
5096 ed.push_undo();
5097 let (before, after) = reflow_rows_keep_cursor(ed, top.0, bot.0);
5098 let (new_row, new_col) = reflow_keep_cursor(top.0, saved.0, saved.1, &before, &after);
5099 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
5100 ed.push_buffer_cursor_to_textarea();
5101 ed.sticky_col = Some(new_col);
5102 ed.vim.mode = Mode::Normal;
5103 }
5104 Operator::AutoIndent => {
5105 ed.push_undo();
5107 auto_indent_rows(ed, top.0, bot.0);
5108 ed.vim.mode = Mode::Normal;
5109 }
5110 Operator::Filter => {
5111 }
5116 Operator::Comment => {
5117 }
5120 }
5121}
5122
5123pub(crate) fn delete_range_bridge<H: crate::types::Host>(
5140 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5141 start: (usize, usize),
5142 end: (usize, usize),
5143 kind: RangeKind,
5144 register: char,
5145) {
5146 ed.vim.pending_register = Some(register);
5147 run_operator_over_range(ed, Operator::Delete, start, end, kind);
5148}
5149
5150pub(crate) fn yank_range_bridge<H: crate::types::Host>(
5153 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5154 start: (usize, usize),
5155 end: (usize, usize),
5156 kind: RangeKind,
5157 register: char,
5158) {
5159 ed.vim.pending_register = Some(register);
5160 run_operator_over_range(ed, Operator::Yank, start, end, kind);
5161}
5162
5163pub(crate) fn change_range_bridge<H: crate::types::Host>(
5168 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5169 start: (usize, usize),
5170 end: (usize, usize),
5171 kind: RangeKind,
5172 register: char,
5173) {
5174 ed.vim.pending_register = Some(register);
5175 run_operator_over_range(ed, Operator::Change, start, end, kind);
5176}
5177
5178pub(crate) fn indent_range_bridge<H: crate::types::Host>(
5183 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5184 start: (usize, usize),
5185 end: (usize, usize),
5186 count: i32,
5187 shiftwidth: u32,
5188) {
5189 if count == 0 {
5190 return;
5191 }
5192 let (top_row, bot_row) = if start.0 <= end.0 {
5193 (start.0, end.0)
5194 } else {
5195 (end.0, start.0)
5196 };
5197 let original_sw = ed.settings().shiftwidth;
5199 if shiftwidth > 0 {
5200 ed.settings_mut().shiftwidth = shiftwidth as usize;
5201 }
5202 ed.push_undo();
5203 let abs_count = count.unsigned_abs() as usize;
5204 if count > 0 {
5205 indent_rows(ed, top_row, bot_row, abs_count);
5206 } else {
5207 outdent_rows(ed, top_row, bot_row, abs_count);
5208 }
5209 if shiftwidth > 0 {
5210 ed.settings_mut().shiftwidth = original_sw;
5211 }
5212 ed.vim.mode = Mode::Normal;
5213}
5214
5215pub(crate) fn case_range_bridge<H: crate::types::Host>(
5219 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5220 start: (usize, usize),
5221 end: (usize, usize),
5222 kind: RangeKind,
5223 op: Operator,
5224) {
5225 match op {
5226 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase | Operator::Rot13 => {}
5227 _ => return,
5228 }
5229 let (top, bot) = order(start, end);
5230 apply_case_op_to_selection(ed, op, top, bot, kind);
5231}
5232
5233pub(crate) fn delete_block_bridge<H: crate::types::Host>(
5254 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5255 top_row: usize,
5256 bot_row: usize,
5257 left_col: usize,
5258 right_col: usize,
5259 register: char,
5260) {
5261 ed.vim.pending_register = Some(register);
5262 let saved_anchor = ed.vim.block_anchor;
5263 let saved_vcol = ed.vim.block_vcol;
5264 ed.vim.block_anchor = (top_row, left_col);
5265 ed.vim.block_vcol = right_col;
5266 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
5268 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
5270 apply_block_operator(ed, Operator::Delete, 1);
5271 ed.vim.block_anchor = saved_anchor;
5275 ed.vim.block_vcol = saved_vcol;
5276}
5277
5278pub(crate) fn yank_block_bridge<H: crate::types::Host>(
5280 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5281 top_row: usize,
5282 bot_row: usize,
5283 left_col: usize,
5284 right_col: usize,
5285 register: char,
5286) {
5287 ed.vim.pending_register = Some(register);
5288 let saved_anchor = ed.vim.block_anchor;
5289 let saved_vcol = ed.vim.block_vcol;
5290 ed.vim.block_anchor = (top_row, left_col);
5291 ed.vim.block_vcol = right_col;
5292 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
5293 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
5294 apply_block_operator(ed, Operator::Yank, 1);
5295 ed.vim.block_anchor = saved_anchor;
5296 ed.vim.block_vcol = saved_vcol;
5297}
5298
5299pub(crate) fn change_block_bridge<H: crate::types::Host>(
5302 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5303 top_row: usize,
5304 bot_row: usize,
5305 left_col: usize,
5306 right_col: usize,
5307 register: char,
5308) {
5309 ed.vim.pending_register = Some(register);
5310 let saved_anchor = ed.vim.block_anchor;
5311 let saved_vcol = ed.vim.block_vcol;
5312 ed.vim.block_anchor = (top_row, left_col);
5313 ed.vim.block_vcol = right_col;
5314 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
5315 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
5316 apply_block_operator(ed, Operator::Change, 1);
5317 ed.vim.block_anchor = saved_anchor;
5318 ed.vim.block_vcol = saved_vcol;
5319}
5320
5321pub(crate) fn indent_block_bridge<H: crate::types::Host>(
5325 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5326 top_row: usize,
5327 bot_row: usize,
5328 count: i32,
5329) {
5330 if count == 0 {
5331 return;
5332 }
5333 ed.push_undo();
5334 let abs = count.unsigned_abs() as usize;
5335 if count > 0 {
5336 indent_rows(ed, top_row, bot_row, abs);
5337 } else {
5338 outdent_rows(ed, top_row, bot_row, abs);
5339 }
5340 ed.vim.mode = Mode::Normal;
5341}
5342
5343pub(crate) fn auto_indent_range_bridge<H: crate::types::Host>(
5347 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5348 start: (usize, usize),
5349 end: (usize, usize),
5350) {
5351 let (top_row, bot_row) = if start.0 <= end.0 {
5352 (start.0, end.0)
5353 } else {
5354 (end.0, start.0)
5355 };
5356 ed.push_undo();
5357 auto_indent_rows(ed, top_row, bot_row);
5358 ed.vim.mode = Mode::Normal;
5359}
5360
5361pub(crate) fn text_object_inner_word_bridge<H: crate::types::Host>(
5372 ed: &Editor<hjkl_buffer::Buffer, H>,
5373) -> Option<((usize, usize), (usize, usize))> {
5374 word_text_object(ed, true, false)
5375}
5376
5377pub(crate) fn text_object_around_word_bridge<H: crate::types::Host>(
5380 ed: &Editor<hjkl_buffer::Buffer, H>,
5381) -> Option<((usize, usize), (usize, usize))> {
5382 word_text_object(ed, false, false)
5383}
5384
5385pub(crate) fn text_object_inner_big_word_bridge<H: crate::types::Host>(
5388 ed: &Editor<hjkl_buffer::Buffer, H>,
5389) -> Option<((usize, usize), (usize, usize))> {
5390 word_text_object(ed, true, true)
5391}
5392
5393pub(crate) fn text_object_around_big_word_bridge<H: crate::types::Host>(
5396 ed: &Editor<hjkl_buffer::Buffer, H>,
5397) -> Option<((usize, usize), (usize, usize))> {
5398 word_text_object(ed, false, true)
5399}
5400
5401pub(crate) fn text_object_inner_quote_bridge<H: crate::types::Host>(
5417 ed: &Editor<hjkl_buffer::Buffer, H>,
5418 quote: char,
5419) -> Option<((usize, usize), (usize, usize))> {
5420 quote_text_object(ed, quote, true)
5421}
5422
5423pub(crate) fn text_object_around_quote_bridge<H: crate::types::Host>(
5426 ed: &Editor<hjkl_buffer::Buffer, H>,
5427 quote: char,
5428) -> Option<((usize, usize), (usize, usize))> {
5429 quote_text_object(ed, quote, false)
5430}
5431
5432pub(crate) fn text_object_inner_bracket_bridge<H: crate::types::Host>(
5440 ed: &Editor<hjkl_buffer::Buffer, H>,
5441 open: char,
5442) -> Option<((usize, usize), (usize, usize))> {
5443 bracket_text_object(ed, open, true, 1).map(|(s, e, _kind)| (s, e))
5444}
5445
5446pub(crate) fn text_object_around_bracket_bridge<H: crate::types::Host>(
5450 ed: &Editor<hjkl_buffer::Buffer, H>,
5451 open: char,
5452) -> Option<((usize, usize), (usize, usize))> {
5453 bracket_text_object(ed, open, false, 1).map(|(s, e, _kind)| (s, e))
5454}
5455
5456pub(crate) fn text_object_inner_sentence_bridge<H: crate::types::Host>(
5461 ed: &Editor<hjkl_buffer::Buffer, H>,
5462) -> Option<((usize, usize), (usize, usize))> {
5463 sentence_text_object(ed, true)
5464}
5465
5466pub(crate) fn text_object_around_sentence_bridge<H: crate::types::Host>(
5469 ed: &Editor<hjkl_buffer::Buffer, H>,
5470) -> Option<((usize, usize), (usize, usize))> {
5471 sentence_text_object(ed, false)
5472}
5473
5474pub(crate) fn text_object_inner_paragraph_bridge<H: crate::types::Host>(
5479 ed: &Editor<hjkl_buffer::Buffer, H>,
5480) -> Option<((usize, usize), (usize, usize))> {
5481 paragraph_text_object(ed, true)
5482}
5483
5484pub(crate) fn text_object_around_paragraph_bridge<H: crate::types::Host>(
5487 ed: &Editor<hjkl_buffer::Buffer, H>,
5488) -> Option<((usize, usize), (usize, usize))> {
5489 paragraph_text_object(ed, false)
5490}
5491
5492pub(crate) fn text_object_inner_tag_bridge<H: crate::types::Host>(
5498 ed: &Editor<hjkl_buffer::Buffer, H>,
5499) -> Option<((usize, usize), (usize, usize))> {
5500 tag_text_object(ed, true)
5501}
5502
5503pub(crate) fn text_object_around_tag_bridge<H: crate::types::Host>(
5506 ed: &Editor<hjkl_buffer::Buffer, H>,
5507) -> Option<((usize, usize), (usize, usize))> {
5508 tag_text_object(ed, false)
5509}
5510
5511pub(crate) fn rope_line_to_str(rope: &ropey::Rope, r: usize) -> String {
5516 let s = rope.line(r).to_string();
5517 if s.ends_with('\n') {
5519 s[..s.len() - 1].to_string()
5520 } else {
5521 s
5522 }
5523}
5524
5525pub(crate) fn rope_row_range_str(rope: &ropey::Rope, lo: usize, hi: usize) -> String {
5528 let n = rope.len_lines();
5529 let lo = lo.min(n.saturating_sub(1));
5530 let hi = hi.min(n.saturating_sub(1));
5531 if lo > hi {
5532 return String::new();
5533 }
5534 let start_byte = rope.line_to_byte(lo);
5536 let end_byte = if hi + 1 < n {
5539 rope.line_to_byte(hi + 1).saturating_sub(1)
5542 } else {
5543 rope.len_bytes()
5544 };
5545 rope.byte_slice(start_byte..end_byte).to_string()
5546}
5547
5548pub(crate) fn rope_to_lines_vec(rope: &ropey::Rope) -> Vec<String> {
5552 let n = rope.len_lines();
5553 (0..n).map(|r| rope_line_to_str(rope, r)).collect()
5554}
5555
5556fn greedy_wrap(original: &[String], width: usize) -> Vec<String> {
5560 let mut wrapped: Vec<String> = Vec::new();
5561 let mut paragraph: Vec<String> = Vec::new();
5562 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
5563 if para.is_empty() {
5564 return;
5565 }
5566 let words = para.join(" ");
5567 let mut current = String::new();
5568 for word in words.split_whitespace() {
5569 let extra = if current.is_empty() {
5570 word.chars().count()
5571 } else {
5572 current.chars().count() + 1 + word.chars().count()
5573 };
5574 if extra > width && !current.is_empty() {
5575 out.push(std::mem::take(&mut current));
5576 current.push_str(word);
5577 } else if current.is_empty() {
5578 current.push_str(word);
5579 } else {
5580 current.push(' ');
5581 current.push_str(word);
5582 }
5583 }
5584 if !current.is_empty() {
5585 out.push(current);
5586 }
5587 para.clear();
5588 };
5589 for line in original {
5590 if line.trim().is_empty() {
5591 flush(&mut paragraph, &mut wrapped, width);
5592 wrapped.push(String::new());
5593 } else {
5594 paragraph.push(line.clone());
5595 }
5596 }
5597 flush(&mut paragraph, &mut wrapped, width);
5598 wrapped
5599}
5600
5601fn reflow_rows<H: crate::types::Host>(
5607 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5608 top: usize,
5609 bot: usize,
5610) {
5611 let width = ed.settings().textwidth.max(1);
5612 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
5613 let bot = bot.min(lines.len().saturating_sub(1));
5614 if top > bot {
5615 return;
5616 }
5617 let original = lines[top..=bot].to_vec();
5618 let wrapped = greedy_wrap(&original, width);
5619
5620 let last_offset = wrapped
5623 .iter()
5624 .rposition(|l| !l.trim().is_empty())
5625 .unwrap_or(0);
5626 let last_row = top + last_offset;
5627
5628 let after: Vec<String> = lines.split_off(bot + 1);
5630 lines.truncate(top);
5631 lines.extend(wrapped);
5632 lines.extend(after);
5633 ed.restore(lines, (last_row, 0));
5634 move_first_non_whitespace(ed);
5635 ed.mark_content_dirty();
5636}
5637
5638fn reflow_rows_keep_cursor<H: crate::types::Host>(
5642 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5643 top: usize,
5644 bot: usize,
5645) -> (Vec<String>, Vec<String>) {
5646 let width = ed.settings().textwidth.max(1);
5647 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
5648 let bot = bot.min(lines.len().saturating_sub(1));
5649 if top > bot {
5650 return (Vec::new(), Vec::new());
5651 }
5652 let original = lines[top..=bot].to_vec();
5653 let wrapped = greedy_wrap(&original, width);
5654
5655 let after: Vec<String> = lines.split_off(bot + 1);
5656 lines.truncate(top);
5657 lines.extend(wrapped.clone());
5658 lines.extend(after);
5659 ed.restore(lines, (top, 0));
5660 ed.mark_content_dirty();
5661 (original, wrapped)
5662}
5663
5664fn reflow_keep_cursor(
5676 top: usize,
5677 cursor_row: usize,
5678 cursor_col: usize,
5679 before_lines: &[String],
5680 after_lines: &[String],
5681) -> (usize, usize) {
5682 let relative_row = cursor_row.saturating_sub(top);
5702 let mut char_offset: usize = 0;
5703 for (i, line) in before_lines.iter().enumerate() {
5704 if i == relative_row {
5705 let line_len = line.chars().count();
5707 char_offset += cursor_col.min(line_len);
5708 break;
5709 }
5710 char_offset += line.chars().count() + 1;
5712 }
5713
5714 let mut remaining = char_offset;
5716 for (i, line) in after_lines.iter().enumerate() {
5717 let len = line.chars().count();
5718 if remaining <= len {
5719 let col = remaining.min(if len == 0 { 0 } else { len.saturating_sub(1) });
5721 return (top + i, col);
5722 }
5723 remaining = remaining.saturating_sub(len + 1);
5725 }
5726
5727 let last = after_lines.len().saturating_sub(1);
5729 let last_len = after_lines
5730 .get(last)
5731 .map(|l| l.chars().count())
5732 .unwrap_or(0);
5733 let col = if last_len == 0 { 0 } else { last_len - 1 };
5734 (top + last, col)
5735}
5736
5737fn apply_case_op_to_selection<H: crate::types::Host>(
5743 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5744 op: Operator,
5745 top: (usize, usize),
5746 bot: (usize, usize),
5747 kind: RangeKind,
5748) {
5749 use hjkl_buffer::Edit;
5750 ed.push_undo();
5751 let saved_yank = ed.yank().to_string();
5752 let saved_yank_linewise = ed.vim.yank_linewise;
5753 let selection = cut_vim_range(ed, top, bot, kind);
5754 let transformed = match op {
5755 Operator::Uppercase => selection.to_uppercase(),
5756 Operator::Lowercase => selection.to_lowercase(),
5757 Operator::ToggleCase => toggle_case_str(&selection),
5758 Operator::Rot13 => rot13_str(&selection),
5759 _ => unreachable!(),
5760 };
5761 if !transformed.is_empty() {
5762 let cursor = buf_cursor_pos(&ed.buffer);
5763 ed.mutate_edit(Edit::InsertStr {
5764 at: cursor,
5765 text: transformed,
5766 });
5767 }
5768 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
5769 ed.push_buffer_cursor_to_textarea();
5770 ed.set_yank(saved_yank);
5771 ed.vim.yank_linewise = saved_yank_linewise;
5772 ed.vim.mode = Mode::Normal;
5773}
5774
5775fn indent_rows<H: crate::types::Host>(
5780 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5781 top: usize,
5782 bot: usize,
5783 count: usize,
5784) {
5785 ed.sync_buffer_content_from_textarea();
5786 let width = ed.settings().shiftwidth * count.max(1);
5787 let pad: String = if ed.settings().expandtab {
5791 " ".repeat(width)
5792 } else {
5793 let tabstop = ed.settings().tabstop.max(1);
5794 let tabs = width / tabstop;
5795 let spaces = width % tabstop;
5796 format!("{}{}", "\t".repeat(tabs), " ".repeat(spaces))
5797 };
5798 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
5799 let bot = bot.min(lines.len().saturating_sub(1));
5800 for line in lines.iter_mut().take(bot + 1).skip(top) {
5801 if !line.is_empty() {
5802 line.insert_str(0, &pad);
5803 }
5804 }
5805 ed.restore(lines, (top, 0));
5808 move_first_non_whitespace(ed);
5809}
5810
5811fn outdent_rows<H: crate::types::Host>(
5815 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5816 top: usize,
5817 bot: usize,
5818 count: usize,
5819) {
5820 ed.sync_buffer_content_from_textarea();
5821 let width = ed.settings().shiftwidth * count.max(1);
5822 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
5823 let bot = bot.min(lines.len().saturating_sub(1));
5824 for line in lines.iter_mut().take(bot + 1).skip(top) {
5825 let strip: usize = line
5826 .chars()
5827 .take(width)
5828 .take_while(|c| *c == ' ' || *c == '\t')
5829 .count();
5830 if strip > 0 {
5831 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
5832 line.drain(..byte_len);
5833 }
5834 }
5835 ed.restore(lines, (top, 0));
5836 move_first_non_whitespace(ed);
5837}
5838
5839fn bracket_net(line: &str) -> i32 {
5866 let mut net: i32 = 0;
5867 let mut chars = line.chars().peekable();
5868 while let Some(ch) = chars.next() {
5869 match ch {
5870 '/' if chars.peek() == Some(&'/') => return net,
5872 '"' => {
5873 while let Some(c) = chars.next() {
5875 match c {
5876 '\\' => {
5877 chars.next();
5878 } '"' => break,
5880 _ => {}
5881 }
5882 }
5883 }
5884 '\'' => {
5885 let saved: Vec<char> = chars.clone().take(5).collect();
5894 let close_idx = if saved.first() == Some(&'\\') {
5895 saved.iter().skip(2).position(|&c| c == '\'').map(|p| p + 2)
5896 } else {
5897 saved.iter().skip(1).position(|&c| c == '\'').map(|p| p + 1)
5898 };
5899 if let Some(idx) = close_idx {
5900 for _ in 0..=idx {
5901 chars.next();
5902 }
5903 }
5904 }
5906 '{' | '(' | '[' => net += 1,
5907 '}' | ')' | ']' => net -= 1,
5908 _ => {}
5909 }
5910 }
5911 net
5912}
5913
5914fn auto_indent_rows<H: crate::types::Host>(
5936 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5937 top: usize,
5938 bot: usize,
5939) {
5940 ed.sync_buffer_content_from_textarea();
5941 let shiftwidth = ed.settings().shiftwidth;
5942 let expandtab = ed.settings().expandtab;
5943 let indent_unit: String = if expandtab {
5944 " ".repeat(shiftwidth)
5945 } else {
5946 "\t".to_string()
5947 };
5948
5949 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
5950 let bot = bot.min(lines.len().saturating_sub(1));
5951
5952 let mut depth: i32 = 0;
5955 for line in lines.iter().take(top) {
5956 depth += bracket_net(line);
5957 if depth < 0 {
5958 depth = 0;
5959 }
5960 }
5961
5962 for line in lines.iter_mut().take(bot + 1).skip(top) {
5963 let trimmed_owned = line.trim_start().to_owned();
5964 if trimmed_owned.is_empty() {
5966 *line = String::new();
5967 continue;
5969 }
5970
5971 let starts_with_close = trimmed_owned
5973 .chars()
5974 .next()
5975 .is_some_and(|c| matches!(c, '}' | ')' | ']'));
5976 let starts_with_dot = trimmed_owned.starts_with('.')
5986 && !trimmed_owned.starts_with("..")
5987 && !trimmed_owned.starts_with(".;");
5988 let effective_depth = if starts_with_close {
5989 depth.saturating_sub(1)
5990 } else if starts_with_dot {
5991 depth.saturating_add(1)
5992 } else {
5993 depth
5994 } as usize;
5995
5996 let new_line = format!("{}{}", indent_unit.repeat(effective_depth), trimmed_owned);
5998
5999 depth += bracket_net(&trimmed_owned);
6001 if depth < 0 {
6002 depth = 0;
6003 }
6004
6005 *line = new_line;
6006 }
6007
6008 ed.restore(lines, (top, 0));
6010 move_first_non_whitespace(ed);
6011 ed.last_indent_range = Some((top, bot));
6013}
6014
6015fn toggle_case_str(s: &str) -> String {
6016 s.chars()
6017 .map(|c| {
6018 if c.is_lowercase() {
6019 c.to_uppercase().next().unwrap_or(c)
6020 } else if c.is_uppercase() {
6021 c.to_lowercase().next().unwrap_or(c)
6022 } else {
6023 c
6024 }
6025 })
6026 .collect()
6027}
6028
6029fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
6030 if a <= b { (a, b) } else { (b, a) }
6031}
6032
6033fn clamp_cursor_to_normal_mode<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6038 let (row, col) = ed.cursor();
6039 let line_chars = buf_line_chars(&ed.buffer, row);
6040 let max_col = line_chars.saturating_sub(1);
6041 if col > max_col {
6042 buf_set_cursor_rc(&mut ed.buffer, row, max_col);
6043 ed.push_buffer_cursor_to_textarea();
6044 }
6045}
6046
6047fn expand_linewise_over_closed_folds(
6053 buf: &hjkl_buffer::Buffer,
6054 mut start: usize,
6055 mut end: usize,
6056) -> (usize, usize) {
6057 let folds = buf.folds();
6058 if folds.is_empty() {
6059 return (start, end);
6060 }
6061 loop {
6062 let mut changed = false;
6063 for f in &folds {
6064 if !f.closed {
6065 continue;
6066 }
6067 if f.start_row <= end && f.end_row >= start {
6069 if f.start_row < start {
6070 start = f.start_row;
6071 changed = true;
6072 }
6073 if f.end_row > end {
6074 end = f.end_row;
6075 changed = true;
6076 }
6077 }
6078 }
6079 if !changed {
6080 break;
6081 }
6082 }
6083 (start, end)
6084}
6085
6086fn execute_line_op<H: crate::types::Host>(
6087 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6088 op: Operator,
6089 count: usize,
6090) {
6091 let (row, col) = ed.cursor();
6092 let total = buf_row_count(&ed.buffer);
6093 let last_content_row = if total >= 2
6102 && buf_line(&ed.buffer, total - 1)
6103 .map(|s| s.is_empty())
6104 .unwrap_or(false)
6105 {
6106 total - 2
6107 } else {
6108 total.saturating_sub(1)
6109 };
6110 if count >= 2 && row >= last_content_row {
6111 return;
6112 }
6113 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
6114
6115 let (row, end_row) = expand_linewise_over_closed_folds(&ed.buffer, row, end_row);
6120
6121 match op {
6122 Operator::Yank => {
6123 let text = read_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
6125 if !text.is_empty() {
6126 ed.record_yank_to_host(text.clone());
6127 ed.record_yank(text, true);
6128 }
6129 let last_col = buf_line_chars(&ed.buffer, end_row).saturating_sub(1);
6132 ed.set_mark('[', (row, 0));
6133 ed.set_mark(']', (end_row, last_col));
6134 buf_set_cursor_rc(&mut ed.buffer, row, col);
6135 ed.push_buffer_cursor_to_textarea();
6136 ed.vim.mode = Mode::Normal;
6137 }
6138 Operator::Delete => {
6139 ed.push_undo();
6140 let deleted_through_last = end_row + 1 >= total;
6141 cut_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
6142 let total_after = buf_row_count(&ed.buffer);
6146 let raw_target = if deleted_through_last {
6147 row.saturating_sub(1).min(total_after.saturating_sub(1))
6148 } else {
6149 row.min(total_after.saturating_sub(1))
6150 };
6151 let target_row = if raw_target > 0
6157 && raw_target + 1 == total_after
6158 && buf_line(&ed.buffer, raw_target)
6159 .map(|s| s.is_empty())
6160 .unwrap_or(false)
6161 {
6162 raw_target - 1
6163 } else {
6164 raw_target
6165 };
6166 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
6167 ed.push_buffer_cursor_to_textarea();
6168 move_first_non_whitespace(ed);
6169 ed.sticky_col = Some(ed.cursor().1);
6170 ed.vim.mode = Mode::Normal;
6171 let pos = ed.cursor();
6174 ed.set_mark('[', pos);
6175 ed.set_mark(']', pos);
6176 }
6177 Operator::Change => {
6178 change_linewise_rows(ed, row, end_row);
6182 }
6183 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase | Operator::Rot13 => {
6184 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), RangeKind::Linewise);
6188 move_first_non_whitespace(ed);
6191 }
6192 Operator::Indent | Operator::Outdent => {
6193 ed.push_undo();
6195 if op == Operator::Indent {
6196 indent_rows(ed, row, end_row, 1);
6197 } else {
6198 outdent_rows(ed, row, end_row, 1);
6199 }
6200 ed.sticky_col = Some(ed.cursor().1);
6201 ed.vim.mode = Mode::Normal;
6202 }
6203 Operator::Fold => unreachable!("Fold has no line-op double"),
6205 Operator::Reflow => {
6206 ed.push_undo();
6208 reflow_rows(ed, row, end_row);
6209 move_first_non_whitespace(ed);
6210 ed.sticky_col = Some(ed.cursor().1);
6211 ed.vim.mode = Mode::Normal;
6212 }
6213 Operator::ReflowKeepCursor => {
6214 let saved = ed.cursor();
6217 ed.push_undo();
6218 let (before, after) = reflow_rows_keep_cursor(ed, row, end_row);
6219 let (new_row, new_col) = reflow_keep_cursor(row, saved.0, saved.1, &before, &after);
6220 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
6221 ed.push_buffer_cursor_to_textarea();
6222 ed.sticky_col = Some(new_col);
6223 ed.vim.mode = Mode::Normal;
6224 }
6225 Operator::AutoIndent => {
6226 ed.push_undo();
6228 auto_indent_rows(ed, row, end_row);
6229 ed.sticky_col = Some(ed.cursor().1);
6230 ed.vim.mode = Mode::Normal;
6231 }
6232 Operator::Filter => {
6233 }
6235 Operator::Comment => {
6236 }
6241 }
6242}
6243
6244pub(crate) fn apply_visual_operator<H: crate::types::Host>(
6247 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6248 op: Operator,
6249 count: usize,
6250) {
6251 let levels = count.max(1);
6254 match ed.vim.mode {
6255 Mode::VisualLine => {
6256 let cursor_row = buf_cursor_pos(&ed.buffer).row;
6257 let top = cursor_row.min(ed.vim.visual_line_anchor);
6258 let bot = cursor_row.max(ed.vim.visual_line_anchor);
6259 ed.vim.yank_linewise = true;
6260 match op {
6261 Operator::Yank => {
6262 let text = read_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
6263 if !text.is_empty() {
6264 ed.record_yank_to_host(text.clone());
6265 ed.record_yank(text, true);
6266 }
6267 buf_set_cursor_rc(&mut ed.buffer, top, 0);
6268 ed.push_buffer_cursor_to_textarea();
6269 ed.vim.mode = Mode::Normal;
6270 }
6271 Operator::Delete => {
6272 ed.push_undo();
6273 cut_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
6274 ed.vim.mode = Mode::Normal;
6275 }
6276 Operator::Change => {
6277 change_linewise_rows(ed, top, bot);
6280 }
6281 Operator::Uppercase
6282 | Operator::Lowercase
6283 | Operator::ToggleCase
6284 | Operator::Rot13 => {
6285 let bot = buf_cursor_pos(&ed.buffer)
6286 .row
6287 .max(ed.vim.visual_line_anchor);
6288 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), RangeKind::Linewise);
6289 move_first_non_whitespace(ed);
6290 }
6291 Operator::Indent | Operator::Outdent => {
6292 ed.push_undo();
6293 let (cursor_row, _) = ed.cursor();
6294 let bot = cursor_row.max(ed.vim.visual_line_anchor);
6295 if op == Operator::Indent {
6296 indent_rows(ed, top, bot, levels);
6297 } else {
6298 outdent_rows(ed, top, bot, levels);
6299 }
6300 ed.vim.mode = Mode::Normal;
6301 }
6302 Operator::Reflow => {
6303 ed.push_undo();
6304 let (cursor_row, _) = ed.cursor();
6305 let bot = cursor_row.max(ed.vim.visual_line_anchor);
6306 reflow_rows(ed, top, bot);
6307 ed.vim.mode = Mode::Normal;
6308 }
6309 Operator::ReflowKeepCursor => {
6310 let saved = ed.cursor();
6311 ed.push_undo();
6312 let (cursor_row, _) = ed.cursor();
6313 let bot = cursor_row.max(ed.vim.visual_line_anchor);
6314 let (before, after) = reflow_rows_keep_cursor(ed, top, bot);
6315 let (new_row, new_col) =
6316 reflow_keep_cursor(top, saved.0, saved.1, &before, &after);
6317 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
6318 ed.push_buffer_cursor_to_textarea();
6319 ed.vim.mode = Mode::Normal;
6320 }
6321 Operator::AutoIndent => {
6322 ed.push_undo();
6323 let (cursor_row, _) = ed.cursor();
6324 let bot = cursor_row.max(ed.vim.visual_line_anchor);
6325 auto_indent_rows(ed, top, bot);
6326 ed.vim.mode = Mode::Normal;
6327 }
6328 Operator::Filter => {}
6330 Operator::Comment => {}
6332 Operator::Fold => unreachable!("Visual zf takes its own path"),
6335 }
6336 }
6337 Mode::Visual => {
6338 ed.vim.yank_linewise = false;
6339 let anchor = ed.vim.visual_anchor;
6340 let cursor = ed.cursor();
6341 let (top, bot) = order(anchor, cursor);
6342 match op {
6343 Operator::Yank => {
6344 let text = read_vim_range(ed, top, bot, RangeKind::Inclusive);
6345 if !text.is_empty() {
6346 ed.record_yank_to_host(text.clone());
6347 ed.record_yank(text, false);
6348 }
6349 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
6350 ed.push_buffer_cursor_to_textarea();
6351 ed.vim.mode = Mode::Normal;
6352 }
6353 Operator::Delete => {
6354 ed.push_undo();
6355 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
6356 ed.vim.mode = Mode::Normal;
6357 }
6358 Operator::Change => {
6359 ed.push_undo();
6360 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
6361 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
6362 }
6363 Operator::Uppercase
6364 | Operator::Lowercase
6365 | Operator::ToggleCase
6366 | Operator::Rot13 => {
6367 let anchor = ed.vim.visual_anchor;
6369 let cursor = ed.cursor();
6370 let (top, bot) = order(anchor, cursor);
6371 apply_case_op_to_selection(ed, op, top, bot, RangeKind::Inclusive);
6372 }
6373 Operator::Indent | Operator::Outdent => {
6374 ed.push_undo();
6375 let anchor = ed.vim.visual_anchor;
6376 let cursor = ed.cursor();
6377 let (top, bot) = order(anchor, cursor);
6378 if op == Operator::Indent {
6379 indent_rows(ed, top.0, bot.0, levels);
6380 } else {
6381 outdent_rows(ed, top.0, bot.0, levels);
6382 }
6383 ed.vim.mode = Mode::Normal;
6384 }
6385 Operator::Reflow => {
6386 ed.push_undo();
6387 let anchor = ed.vim.visual_anchor;
6388 let cursor = ed.cursor();
6389 let (top, bot) = order(anchor, cursor);
6390 reflow_rows(ed, top.0, bot.0);
6391 ed.vim.mode = Mode::Normal;
6392 }
6393 Operator::ReflowKeepCursor => {
6394 let saved = ed.cursor();
6395 ed.push_undo();
6396 let anchor = ed.vim.visual_anchor;
6397 let cursor = ed.cursor();
6398 let (top, bot) = order(anchor, cursor);
6399 let (before, after) = reflow_rows_keep_cursor(ed, top.0, bot.0);
6400 let (new_row, new_col) =
6401 reflow_keep_cursor(top.0, saved.0, saved.1, &before, &after);
6402 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
6403 ed.push_buffer_cursor_to_textarea();
6404 ed.vim.mode = Mode::Normal;
6405 }
6406 Operator::AutoIndent => {
6407 ed.push_undo();
6408 let anchor = ed.vim.visual_anchor;
6409 let cursor = ed.cursor();
6410 let (top, bot) = order(anchor, cursor);
6411 auto_indent_rows(ed, top.0, bot.0);
6412 ed.vim.mode = Mode::Normal;
6413 }
6414 Operator::Filter => {}
6416 Operator::Comment => {}
6418 Operator::Fold => unreachable!("Visual zf takes its own path"),
6419 }
6420 }
6421 Mode::VisualBlock => apply_block_operator(ed, op, levels),
6422 _ => {}
6423 }
6424}
6425
6426fn block_bounds<H: crate::types::Host>(
6431 ed: &Editor<hjkl_buffer::Buffer, H>,
6432) -> (usize, usize, usize, usize) {
6433 let (ar, ac) = ed.vim.block_anchor;
6434 let (cr, _) = ed.cursor();
6435 let cc = ed.vim.block_vcol;
6436 let top = ar.min(cr);
6437 let bot = ar.max(cr);
6438 let left = ac.min(cc);
6439 let right = ac.max(cc);
6440 (top, bot, left, right)
6441}
6442
6443pub(crate) fn update_block_vcol<H: crate::types::Host>(
6448 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6449 motion: &Motion,
6450) {
6451 match motion {
6452 Motion::Left
6453 | Motion::Right
6454 | Motion::SpaceFwd
6455 | Motion::BackspaceBack
6456 | Motion::WordFwd
6457 | Motion::BigWordFwd
6458 | Motion::WordBack
6459 | Motion::BigWordBack
6460 | Motion::WordEnd
6461 | Motion::BigWordEnd
6462 | Motion::WordEndBack
6463 | Motion::BigWordEndBack
6464 | Motion::LineStart
6465 | Motion::FirstNonBlank
6466 | Motion::LineEnd
6467 | Motion::Find { .. }
6468 | Motion::FindRepeat { .. }
6469 | Motion::MatchBracket => {
6470 ed.vim.block_vcol = ed.cursor().1;
6471 }
6472 _ => {}
6474 }
6475}
6476
6477fn apply_block_operator<H: crate::types::Host>(
6482 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6483 op: Operator,
6484 count: usize,
6485) {
6486 let (top, bot, left, right) = block_bounds(ed);
6487 let yank = block_yank(ed, top, bot, left, right);
6489
6490 match op {
6491 Operator::Yank => {
6492 if !yank.is_empty() {
6493 ed.record_yank_to_host(yank.clone());
6494 ed.record_yank(yank, false);
6495 }
6496 ed.vim.mode = Mode::Normal;
6497 ed.jump_cursor(top, left);
6498 }
6499 Operator::Delete => {
6500 ed.push_undo();
6501 delete_block_contents(ed, top, bot, left, right);
6502 if !yank.is_empty() {
6503 ed.record_yank_to_host(yank.clone());
6504 ed.record_delete(yank, false);
6505 }
6506 ed.vim.mode = Mode::Normal;
6507 ed.jump_cursor(top, left);
6508 }
6509 Operator::Change => {
6510 ed.push_undo();
6511 delete_block_contents(ed, top, bot, left, right);
6512 if !yank.is_empty() {
6513 ed.record_yank_to_host(yank.clone());
6514 ed.record_delete(yank, false);
6515 }
6516 ed.jump_cursor(top, left);
6517 begin_insert_noundo(
6518 ed,
6519 1,
6520 InsertReason::BlockChange {
6521 top,
6522 bot,
6523 col: left,
6524 },
6525 );
6526 }
6527 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase | Operator::Rot13 => {
6528 ed.push_undo();
6529 transform_block_case(ed, op, top, bot, left, right);
6530 ed.vim.mode = Mode::Normal;
6531 ed.jump_cursor(top, left);
6532 }
6533 Operator::Indent | Operator::Outdent => {
6534 ed.push_undo();
6538 if op == Operator::Indent {
6539 indent_rows(ed, top, bot, count.max(1));
6540 } else {
6541 outdent_rows(ed, top, bot, count.max(1));
6542 }
6543 ed.vim.mode = Mode::Normal;
6544 }
6545 Operator::Fold => unreachable!("Visual zf takes its own path"),
6546 Operator::Reflow => {
6547 ed.push_undo();
6551 reflow_rows(ed, top, bot);
6552 ed.vim.mode = Mode::Normal;
6553 }
6554 Operator::ReflowKeepCursor => {
6555 let saved = ed.cursor();
6557 ed.push_undo();
6558 let (before, after) = reflow_rows_keep_cursor(ed, top, bot);
6559 let (new_row, new_col) = reflow_keep_cursor(top, saved.0, saved.1, &before, &after);
6560 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
6561 ed.push_buffer_cursor_to_textarea();
6562 ed.vim.mode = Mode::Normal;
6563 }
6564 Operator::AutoIndent => {
6565 ed.push_undo();
6568 auto_indent_rows(ed, top, bot);
6569 ed.vim.mode = Mode::Normal;
6570 }
6571 Operator::Filter => {}
6573 Operator::Comment => {}
6575 }
6576}
6577
6578fn transform_block_case<H: crate::types::Host>(
6582 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6583 op: Operator,
6584 top: usize,
6585 bot: usize,
6586 left: usize,
6587 right: usize,
6588) {
6589 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
6590 for r in top..=bot.min(lines.len().saturating_sub(1)) {
6591 let chars: Vec<char> = lines[r].chars().collect();
6592 if left >= chars.len() {
6593 continue;
6594 }
6595 let end = (right + 1).min(chars.len());
6596 let head: String = chars[..left].iter().collect();
6597 let mid: String = chars[left..end].iter().collect();
6598 let tail: String = chars[end..].iter().collect();
6599 let transformed = match op {
6600 Operator::Uppercase => mid.to_uppercase(),
6601 Operator::Lowercase => mid.to_lowercase(),
6602 Operator::ToggleCase => toggle_case_str(&mid),
6603 Operator::Rot13 => rot13_str(&mid),
6604 _ => mid,
6605 };
6606 lines[r] = format!("{head}{transformed}{tail}");
6607 }
6608 let saved_yank = ed.yank().to_string();
6609 let saved_linewise = ed.vim.yank_linewise;
6610 ed.restore(lines, (top, left));
6611 ed.set_yank(saved_yank);
6612 ed.vim.yank_linewise = saved_linewise;
6613}
6614
6615fn block_yank<H: crate::types::Host>(
6616 ed: &Editor<hjkl_buffer::Buffer, H>,
6617 top: usize,
6618 bot: usize,
6619 left: usize,
6620 right: usize,
6621) -> String {
6622 let rope = crate::types::Query::rope(&ed.buffer);
6623 let n = rope.len_lines();
6624 let mut rows: Vec<String> = Vec::new();
6625 for r in top..=bot {
6626 if r >= n {
6627 break;
6628 }
6629 let line = rope_line_to_str(&rope, r);
6630 let chars: Vec<char> = line.chars().collect();
6631 let end = (right + 1).min(chars.len());
6632 if left >= chars.len() {
6633 rows.push(String::new());
6634 } else {
6635 rows.push(chars[left..end].iter().collect());
6636 }
6637 }
6638 rows.join("\n")
6639}
6640
6641fn delete_block_contents<H: crate::types::Host>(
6642 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6643 top: usize,
6644 bot: usize,
6645 left: usize,
6646 right: usize,
6647) {
6648 use hjkl_buffer::{Edit, MotionKind, Position};
6649 ed.sync_buffer_content_from_textarea();
6650 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
6651 if last_row < top {
6652 return;
6653 }
6654 ed.mutate_edit(Edit::DeleteRange {
6655 start: Position::new(top, left),
6656 end: Position::new(last_row, right),
6657 kind: MotionKind::Block,
6658 });
6659 ed.push_buffer_cursor_to_textarea();
6660}
6661
6662pub(crate) fn block_replace<H: crate::types::Host>(
6664 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6665 ch: char,
6666) {
6667 let (top, bot, left, right) = block_bounds(ed);
6668 ed.push_undo();
6669 ed.sync_buffer_content_from_textarea();
6670 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
6671 for r in top..=bot.min(lines.len().saturating_sub(1)) {
6672 let chars: Vec<char> = lines[r].chars().collect();
6673 if left >= chars.len() {
6674 continue;
6675 }
6676 let end = (right + 1).min(chars.len());
6677 let before: String = chars[..left].iter().collect();
6678 let middle: String = std::iter::repeat_n(ch, end - left).collect();
6679 let after: String = chars[end..].iter().collect();
6680 lines[r] = format!("{before}{middle}{after}");
6681 }
6682 reset_textarea_lines(ed, lines);
6683 ed.vim.mode = Mode::Normal;
6684 ed.jump_cursor(top, left);
6685}
6686
6687fn reset_textarea_lines<H: crate::types::Host>(
6691 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6692 lines: Vec<String>,
6693) {
6694 let cursor = ed.cursor();
6695 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
6696 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
6697 ed.mark_content_dirty();
6698}
6699
6700type Pos = (usize, usize);
6706
6707pub(crate) fn text_object_range<H: crate::types::Host>(
6711 ed: &Editor<hjkl_buffer::Buffer, H>,
6712 obj: TextObject,
6713 inner: bool,
6714 count: usize,
6715) -> Option<(Pos, Pos, RangeKind)> {
6716 match obj {
6717 TextObject::Word { big } => {
6718 word_text_object(ed, inner, big).map(|(s, e)| (s, e, RangeKind::Exclusive))
6719 }
6720 TextObject::Quote(q) => {
6721 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, RangeKind::Exclusive))
6722 }
6723 TextObject::Bracket(open) => bracket_text_object(ed, open, inner, count),
6724 TextObject::Paragraph => {
6725 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Linewise))
6726 }
6727 TextObject::XmlTag => tag_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Exclusive)),
6728 TextObject::Sentence => {
6729 sentence_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Exclusive))
6730 }
6731 }
6732}
6733
6734fn sentence_boundary<H: crate::types::Host>(
6738 ed: &Editor<hjkl_buffer::Buffer, H>,
6739 forward: bool,
6740) -> Option<(usize, usize)> {
6741 let rope = crate::types::Query::rope(&ed.buffer);
6742 let n_lines = rope.len_lines();
6743 if n_lines == 0 {
6744 return None;
6745 }
6746 let line_lens: Vec<usize> = (0..n_lines)
6748 .map(|r| rope_line_to_str(&rope, r).chars().count())
6749 .collect();
6750 let pos_to_idx = |pos: (usize, usize)| -> usize {
6751 let idx: usize = line_lens.iter().take(pos.0).map(|&len| len + 1).sum();
6752 idx + pos.1
6753 };
6754 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
6755 for (r, &len) in line_lens.iter().enumerate() {
6756 if idx <= len {
6757 return (r, idx);
6758 }
6759 idx -= len + 1;
6760 }
6761 let last = n_lines.saturating_sub(1);
6762 (last, line_lens[last])
6763 };
6764 let mut chars: Vec<char> = rope.chars().collect();
6767 if chars.last() == Some(&'\n') {
6769 chars.pop();
6770 }
6771 if chars.is_empty() {
6772 return None;
6773 }
6774 let total = chars.len();
6775 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
6776 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
6777
6778 if forward {
6779 let mut i = cursor_idx + 1;
6782 while i < total {
6783 if is_terminator(chars[i]) {
6784 while i + 1 < total && is_terminator(chars[i + 1]) {
6785 i += 1;
6786 }
6787 if i + 1 >= total {
6788 return None;
6789 }
6790 if chars[i + 1].is_whitespace() {
6791 let mut j = i + 1;
6792 while j < total && chars[j].is_whitespace() {
6793 j += 1;
6794 }
6795 if j >= total {
6796 return None;
6797 }
6798 return Some(idx_to_pos(j));
6799 }
6800 }
6801 i += 1;
6802 }
6803 None
6804 } else {
6805 let find_start = |from: usize| -> Option<usize> {
6809 let mut start = from;
6810 while start > 0 {
6811 let prev = chars[start - 1];
6812 if prev.is_whitespace() {
6813 let mut k = start - 1;
6814 while k > 0 && chars[k - 1].is_whitespace() {
6815 k -= 1;
6816 }
6817 if k > 0 && is_terminator(chars[k - 1]) {
6818 break;
6819 }
6820 }
6821 start -= 1;
6822 }
6823 while start < total && chars[start].is_whitespace() {
6824 start += 1;
6825 }
6826 (start < total).then_some(start)
6827 };
6828 let current_start = find_start(cursor_idx)?;
6829 if current_start < cursor_idx {
6830 return Some(idx_to_pos(current_start));
6831 }
6832 let mut k = current_start;
6835 while k > 0 && chars[k - 1].is_whitespace() {
6836 k -= 1;
6837 }
6838 if k == 0 {
6839 return None;
6840 }
6841 let prev_start = find_start(k - 1)?;
6842 Some(idx_to_pos(prev_start))
6843 }
6844}
6845
6846fn sentence_text_object<H: crate::types::Host>(
6852 ed: &Editor<hjkl_buffer::Buffer, H>,
6853 inner: bool,
6854) -> Option<((usize, usize), (usize, usize))> {
6855 let rope = crate::types::Query::rope(&ed.buffer);
6856 let n_lines = rope.len_lines();
6857 if n_lines == 0 {
6858 return None;
6859 }
6860 let line_lens: Vec<usize> = (0..n_lines)
6863 .map(|r| rope_line_to_str(&rope, r).chars().count())
6864 .collect();
6865 let pos_to_idx = |pos: (usize, usize)| -> usize {
6866 let idx: usize = line_lens.iter().take(pos.0).map(|&len| len + 1).sum();
6867 idx + pos.1
6868 };
6869 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
6870 for (r, &len) in line_lens.iter().enumerate() {
6871 if idx <= len {
6872 return (r, idx);
6873 }
6874 idx -= len + 1;
6875 }
6876 let last = n_lines.saturating_sub(1);
6877 (last, line_lens[last])
6878 };
6879 let mut chars: Vec<char> = rope.chars().collect();
6880 if chars.last() == Some(&'\n') {
6881 chars.pop();
6882 }
6883 if chars.is_empty() {
6884 return None;
6885 }
6886
6887 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
6888 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
6889
6890 let mut start = cursor_idx;
6894 while start > 0 {
6895 let prev = chars[start - 1];
6896 if prev.is_whitespace() {
6897 let mut k = start - 1;
6901 while k > 0 && chars[k - 1].is_whitespace() {
6902 k -= 1;
6903 }
6904 if k > 0 && is_terminator(chars[k - 1]) {
6905 break;
6906 }
6907 }
6908 start -= 1;
6909 }
6910 while start < chars.len() && chars[start].is_whitespace() {
6913 start += 1;
6914 }
6915 if start >= chars.len() {
6916 return None;
6917 }
6918
6919 let mut end = start;
6922 while end < chars.len() {
6923 if is_terminator(chars[end]) {
6924 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
6926 end += 1;
6927 }
6928 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
6931 break;
6932 }
6933 }
6934 end += 1;
6935 }
6936 let end_idx = (end + 1).min(chars.len());
6938
6939 let final_end = if inner {
6940 end_idx
6941 } else {
6942 let mut e = end_idx;
6946 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
6947 e += 1;
6948 }
6949 e
6950 };
6951
6952 Some((idx_to_pos(start), idx_to_pos(final_end)))
6953}
6954
6955fn tag_text_object<H: crate::types::Host>(
6959 ed: &Editor<hjkl_buffer::Buffer, H>,
6960 inner: bool,
6961) -> Option<((usize, usize), (usize, usize))> {
6962 let rope = crate::types::Query::rope(&ed.buffer);
6963 let n_lines = rope.len_lines();
6964 if n_lines == 0 {
6965 return None;
6966 }
6967 let line_lens: Vec<usize> = (0..n_lines)
6971 .map(|r| rope_line_to_str(&rope, r).chars().count())
6972 .collect();
6973 let pos_to_idx = |pos: (usize, usize)| -> usize {
6974 let idx: usize = line_lens.iter().take(pos.0).map(|&len| len + 1).sum();
6975 idx + pos.1
6976 };
6977 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
6978 for (r, &len) in line_lens.iter().enumerate() {
6979 if idx <= len {
6980 return (r, idx);
6981 }
6982 idx -= len + 1;
6983 }
6984 let last = n_lines.saturating_sub(1);
6985 (last, line_lens[last])
6986 };
6987 let mut chars: Vec<char> = rope.chars().collect();
6988 if chars.last() == Some(&'\n') {
6989 chars.pop();
6990 }
6991 let cursor_idx = pos_to_idx(ed.cursor());
6992
6993 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
7001 let mut next_after: Option<(usize, usize, usize, usize)> = None;
7002 let mut i = 0;
7003 while i < chars.len() {
7004 if chars[i] != '<' {
7005 i += 1;
7006 continue;
7007 }
7008 let mut j = i + 1;
7009 while j < chars.len() && chars[j] != '>' {
7010 j += 1;
7011 }
7012 if j >= chars.len() {
7013 break;
7014 }
7015 let inside: String = chars[i + 1..j].iter().collect();
7016 let close_end = j + 1;
7017 let trimmed = inside.trim();
7018 if trimmed.starts_with('!') || trimmed.starts_with('?') {
7019 i = close_end;
7020 continue;
7021 }
7022 if let Some(rest) = trimmed.strip_prefix('/') {
7023 let name = rest.split_whitespace().next().unwrap_or("").to_string();
7024 if !name.is_empty()
7025 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
7026 {
7027 let (open_start, content_start, _) = stack[stack_idx].clone();
7028 stack.truncate(stack_idx);
7029 let content_end = i;
7030 let candidate = (open_start, content_start, content_end, close_end);
7031 if cursor_idx >= open_start && cursor_idx < close_end {
7038 innermost = match innermost {
7039 Some((os, _, _, ce)) if os <= open_start && close_end <= ce => {
7040 Some(candidate)
7041 }
7042 None => Some(candidate),
7043 existing => existing,
7044 };
7045 } else if open_start >= cursor_idx && next_after.is_none() {
7046 next_after = Some(candidate);
7047 }
7048 }
7049 } else if !trimmed.ends_with('/') {
7050 let name: String = trimmed
7051 .split(|c: char| c.is_whitespace() || c == '/')
7052 .next()
7053 .unwrap_or("")
7054 .to_string();
7055 if !name.is_empty() {
7056 stack.push((i, close_end, name));
7057 }
7058 }
7059 i = close_end;
7060 }
7061
7062 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
7063 if inner {
7064 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
7065 } else {
7066 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
7067 }
7068}
7069
7070fn is_wordchar(c: char) -> bool {
7071 c.is_alphanumeric() || c == '_'
7072}
7073
7074pub(crate) use hjkl_buffer::is_keyword_char;
7078
7079pub(crate) fn abbrev_kind(lhs: &str, iskeyword: &str) -> AbbrevKind {
7080 let chars: Vec<char> = lhs.chars().collect();
7081 if chars.is_empty() {
7082 return AbbrevKind::NonKw;
7083 }
7084 let last = *chars.last().unwrap();
7085 let last_is_kw = is_keyword_char(last, iskeyword);
7086 if !last_is_kw {
7087 return AbbrevKind::NonKw;
7088 }
7089 let all_kw = chars.iter().all(|&c| is_keyword_char(c, iskeyword));
7091 if all_kw {
7092 AbbrevKind::Full
7093 } else {
7094 AbbrevKind::End
7095 }
7096}
7097
7098pub(crate) fn try_abbrev_expand(
7116 abbrevs: &[Abbrev],
7117 line_before: &str,
7118 mincol: usize,
7119 trigger: AbbrevTrigger,
7120 iskeyword: &str,
7121) -> Option<(usize, String)> {
7122 let chars: Vec<char> = line_before.chars().collect();
7123 let cursor_col = chars.len(); for abbrev in abbrevs {
7126 if !abbrev.insert {
7127 continue;
7128 }
7129 let lhs_chars: Vec<char> = abbrev.lhs.chars().collect();
7130 if lhs_chars.is_empty() {
7131 continue;
7132 }
7133 let lhs_len = lhs_chars.len();
7134
7135 let kind = abbrev_kind(&abbrev.lhs, iskeyword);
7137
7138 match kind {
7140 AbbrevKind::Full | AbbrevKind::End => {
7141 let trigger_char_is_kw = match trigger {
7144 AbbrevTrigger::NonKeyword(c) => is_keyword_char(c, iskeyword),
7145 AbbrevTrigger::CtrlBracket | AbbrevTrigger::Cr | AbbrevTrigger::Esc => false,
7146 };
7147 if trigger_char_is_kw {
7148 continue;
7150 }
7151 }
7152 AbbrevKind::NonKw => {
7153 match trigger {
7155 AbbrevTrigger::Cr | AbbrevTrigger::Esc | AbbrevTrigger::CtrlBracket => {}
7156 AbbrevTrigger::NonKeyword(_) => continue,
7157 }
7158 }
7159 }
7160
7161 if cursor_col < lhs_len {
7163 continue;
7164 }
7165 let lhs_start_col = cursor_col - lhs_len;
7166
7167 if lhs_start_col < mincol {
7169 continue;
7170 }
7171
7172 let text_slice: &[char] = &chars[lhs_start_col..cursor_col];
7174 if text_slice != lhs_chars.as_slice() {
7175 continue;
7176 }
7177
7178 if lhs_start_col > 0 {
7180 let ch_before = chars[lhs_start_col - 1];
7181 match kind {
7182 AbbrevKind::Full => {
7183 if is_keyword_char(ch_before, iskeyword) {
7194 continue; }
7196 if lhs_len == 1 && ch_before != ' ' && ch_before != '\t' {
7197 continue;
7199 }
7200 }
7201 AbbrevKind::End => {
7202 }
7206 AbbrevKind::NonKw => {
7207 if ch_before != ' ' && ch_before != '\t' {
7210 continue;
7211 }
7212 }
7213 }
7214 }
7215 return Some((lhs_len, abbrev.rhs.clone()));
7219 }
7220
7221 None
7222}
7223
7224pub(crate) fn check_and_apply_abbrev<H: crate::types::Host>(
7233 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7234 trigger: AbbrevTrigger,
7235) -> bool {
7236 use hjkl_buffer::{Edit, Position};
7237
7238 let cursor = buf_cursor_pos(&ed.buffer);
7240 let row = cursor.row;
7241 let col = cursor.col;
7242 let line_before: String = {
7243 let line = buf_line(&ed.buffer, row).unwrap_or_default();
7244 line.chars().take(col).collect()
7245 };
7246 let (mincol, on_start_row) = if let Some(ref s) = ed.vim.insert_session {
7247 if row == s.start_row {
7248 (s.start_col, true)
7249 } else {
7250 (0, false)
7251 }
7252 } else {
7253 (0, false)
7254 };
7255 if on_start_row && col <= mincol {
7257 return false;
7258 }
7259
7260 let iskeyword = ed.settings.iskeyword.clone();
7261 let abbrevs = ed.vim.abbrevs.clone();
7262
7263 let Some((lhs_len, rhs)) =
7264 try_abbrev_expand(&abbrevs, &line_before, mincol, trigger, &iskeyword)
7265 else {
7266 return false;
7267 };
7268
7269 let lhs_start = col.saturating_sub(lhs_len);
7271 if lhs_len > 0 {
7272 ed.mutate_edit(Edit::DeleteRange {
7273 start: Position::new(row, lhs_start),
7274 end: Position::new(row, col),
7275 kind: hjkl_buffer::MotionKind::Char,
7276 });
7277 }
7278
7279 let insert_pos = Position::new(row, lhs_start);
7281 if !rhs.is_empty() {
7282 ed.mutate_edit(Edit::InsertStr {
7283 at: insert_pos,
7284 text: rhs.clone(),
7285 });
7286 }
7287
7288 let new_col = lhs_start + rhs.chars().count();
7290 buf_set_cursor_rc(&mut ed.buffer, row, new_col);
7291 ed.push_buffer_cursor_to_textarea();
7292
7293 true
7294}
7295
7296fn word_text_object<H: crate::types::Host>(
7297 ed: &Editor<hjkl_buffer::Buffer, H>,
7298 inner: bool,
7299 big: bool,
7300) -> Option<((usize, usize), (usize, usize))> {
7301 let (row, col) = ed.cursor();
7302 let line = buf_line(&ed.buffer, row)?;
7303 let chars: Vec<char> = line.chars().collect();
7304 if chars.is_empty() {
7305 return None;
7306 }
7307 let at = col.min(chars.len().saturating_sub(1));
7308 let classify = |c: char| -> u8 {
7309 if c.is_whitespace() {
7310 0
7311 } else if big || is_wordchar(c) {
7312 1
7313 } else {
7314 2
7315 }
7316 };
7317 let cls = classify(chars[at]);
7318 let mut start = at;
7319 while start > 0 && classify(chars[start - 1]) == cls {
7320 start -= 1;
7321 }
7322 let mut end = at;
7323 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
7324 end += 1;
7325 }
7326 let char_byte = |i: usize| {
7328 if i >= chars.len() {
7329 line.len()
7330 } else {
7331 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
7332 }
7333 };
7334 let mut start_col = char_byte(start);
7335 let mut end_col = char_byte(end + 1);
7337 if !inner {
7338 let mut t = end + 1;
7340 let mut included_trailing = false;
7341 while t < chars.len() && chars[t].is_whitespace() {
7342 included_trailing = true;
7343 t += 1;
7344 }
7345 if included_trailing {
7346 end_col = char_byte(t);
7347 } else {
7348 let mut s = start;
7349 while s > 0 && chars[s - 1].is_whitespace() {
7350 s -= 1;
7351 }
7352 start_col = char_byte(s);
7353 }
7354 }
7355 Some(((row, start_col), (row, end_col)))
7356}
7357
7358fn quote_text_object<H: crate::types::Host>(
7359 ed: &Editor<hjkl_buffer::Buffer, H>,
7360 q: char,
7361 inner: bool,
7362) -> Option<((usize, usize), (usize, usize))> {
7363 let (row, col) = ed.cursor();
7364 let line = buf_line(&ed.buffer, row)?;
7365 let bytes = line.as_bytes();
7366 let q_byte = q as u8;
7367 let mut positions: Vec<usize> = Vec::new();
7369 for (i, &b) in bytes.iter().enumerate() {
7370 if b == q_byte {
7371 positions.push(i);
7372 }
7373 }
7374 if positions.len() < 2 {
7375 return None;
7376 }
7377 let mut open_idx: Option<usize> = None;
7378 let mut close_idx: Option<usize> = None;
7379 for pair in positions.chunks(2) {
7380 if pair.len() < 2 {
7381 break;
7382 }
7383 if col >= pair[0] && col <= pair[1] {
7384 open_idx = Some(pair[0]);
7385 close_idx = Some(pair[1]);
7386 break;
7387 }
7388 if col < pair[0] {
7389 open_idx = Some(pair[0]);
7390 close_idx = Some(pair[1]);
7391 break;
7392 }
7393 }
7394 let open = open_idx?;
7395 let close = close_idx?;
7396 if inner {
7398 if close <= open + 1 {
7399 return None;
7400 }
7401 Some(((row, open + 1), (row, close)))
7402 } else {
7403 let after_close = close + 1; if after_close < bytes.len() && bytes[after_close].is_ascii_whitespace() {
7410 let mut end = after_close;
7412 while end < bytes.len() && bytes[end].is_ascii_whitespace() {
7413 end += 1;
7414 }
7415 Some(((row, open), (row, end)))
7416 } else if open > 0 && bytes[open - 1].is_ascii_whitespace() {
7417 let mut start = open;
7419 while start > 0 && bytes[start - 1].is_ascii_whitespace() {
7420 start -= 1;
7421 }
7422 Some(((row, start), (row, close + 1)))
7423 } else {
7424 Some(((row, open), (row, close + 1)))
7425 }
7426 }
7427}
7428
7429fn bracket_text_object<H: crate::types::Host>(
7430 ed: &Editor<hjkl_buffer::Buffer, H>,
7431 open: char,
7432 inner: bool,
7433 count: usize,
7434) -> Option<(Pos, Pos, RangeKind)> {
7435 let close = match open {
7436 '(' => ')',
7437 '[' => ']',
7438 '{' => '}',
7439 '<' => '>',
7440 _ => return None,
7441 };
7442 let (row, col) = ed.cursor();
7443 let lines = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
7444 let lines = lines.as_slice();
7445 let cursor_char = lines.get(row).and_then(|l| l.chars().nth(col));
7451 let (open_pos, close_pos) = if cursor_char == Some(close) {
7452 let open_pos = if col > 0 {
7453 find_open_bracket(lines, row, col - 1, open, close)
7454 } else if row > 0 {
7455 let pr = row - 1;
7456 let pc = lines[pr].chars().count().saturating_sub(1);
7457 find_open_bracket(lines, pr, pc, open, close)
7458 } else {
7459 None
7460 }?;
7461 (open_pos, (row, col))
7462 } else {
7463 let open_pos = find_open_bracket(lines, row, col, open, close)
7468 .or_else(|| find_next_open(lines, row, col, open))?;
7469 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
7470 (open_pos, close_pos)
7471 };
7472 let (open_pos, close_pos) = {
7476 let (mut op, mut cp) = (open_pos, close_pos);
7477 for _ in 1..count.max(1) {
7478 let outer = if op.1 > 0 {
7479 find_open_bracket(lines, op.0, op.1 - 1, open, close)
7480 } else if op.0 > 0 {
7481 let pr = op.0 - 1;
7482 let pc = lines[pr].chars().count().saturating_sub(1);
7483 find_open_bracket(lines, pr, pc, open, close)
7484 } else {
7485 None
7486 };
7487 let Some(oo) = outer else { break };
7488 let Some(oc) = find_close_bracket(lines, oo.0, oo.1 + 1, open, close) else {
7489 break;
7490 };
7491 op = oo;
7492 cp = oc;
7493 }
7494 (op, cp)
7495 };
7496 if inner {
7498 let open_line_len = lines[open_pos.0].chars().count();
7509 let inner_start = if open_pos.1 + 1 >= open_line_len && open_pos.0 + 1 < lines.len() {
7510 (open_pos.0 + 1, 0)
7511 } else {
7512 advance_pos(lines, open_pos)
7513 };
7514 if inner_start.0 > close_pos.0
7517 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
7518 {
7519 return Some((inner_start, inner_start, RangeKind::Exclusive));
7520 }
7521 if close_pos.0 > open_pos.0 {
7528 let mut saw_ws = false;
7529 let mut saw_other = false;
7530 for r in inner_start.0..=close_pos.0 {
7531 let line: Vec<char> = lines
7532 .get(r)
7533 .map(|l| l.chars().collect())
7534 .unwrap_or_default();
7535 let from = if r == inner_start.0 { inner_start.1 } else { 0 };
7536 let to = if r == close_pos.0 {
7537 close_pos.1
7538 } else {
7539 line.len()
7540 };
7541 for &c in line
7542 .iter()
7543 .take(to.min(line.len()))
7544 .skip(from.min(line.len()))
7545 {
7546 if c == ' ' || c == '\t' {
7547 saw_ws = true;
7548 } else {
7549 saw_other = true;
7550 }
7551 }
7552 }
7553 if saw_ws && !saw_other {
7554 return Some((inner_start, inner_start, RangeKind::Exclusive));
7555 }
7556 }
7557 Some((inner_start, close_pos, RangeKind::Exclusive))
7558 } else {
7559 Some((
7560 open_pos,
7561 advance_pos(lines, close_pos),
7562 RangeKind::Exclusive,
7563 ))
7564 }
7565}
7566
7567fn find_open_bracket(
7568 lines: &[String],
7569 row: usize,
7570 col: usize,
7571 open: char,
7572 close: char,
7573) -> Option<(usize, usize)> {
7574 let mut depth: i32 = 0;
7575 let mut r = row;
7576 let mut c = col as isize;
7577 loop {
7578 let cur = &lines[r];
7579 let chars: Vec<char> = cur.chars().collect();
7580 if (c as usize) >= chars.len() {
7584 c = chars.len() as isize - 1;
7585 }
7586 while c >= 0 {
7587 let ch = chars[c as usize];
7588 if ch == close {
7589 depth += 1;
7590 } else if ch == open {
7591 if depth == 0 {
7592 return Some((r, c as usize));
7593 }
7594 depth -= 1;
7595 }
7596 c -= 1;
7597 }
7598 if r == 0 {
7599 return None;
7600 }
7601 r -= 1;
7602 c = lines[r].chars().count() as isize - 1;
7603 }
7604}
7605
7606fn find_close_bracket(
7607 lines: &[String],
7608 row: usize,
7609 start_col: usize,
7610 open: char,
7611 close: char,
7612) -> Option<(usize, usize)> {
7613 let mut depth: i32 = 0;
7614 let mut r = row;
7615 let mut c = start_col;
7616 loop {
7617 let cur = &lines[r];
7618 let chars: Vec<char> = cur.chars().collect();
7619 while c < chars.len() {
7620 let ch = chars[c];
7621 if ch == open {
7622 depth += 1;
7623 } else if ch == close {
7624 if depth == 0 {
7625 return Some((r, c));
7626 }
7627 depth -= 1;
7628 }
7629 c += 1;
7630 }
7631 if r + 1 >= lines.len() {
7632 return None;
7633 }
7634 r += 1;
7635 c = 0;
7636 }
7637}
7638
7639fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
7643 let mut r = row;
7644 let mut c = col;
7645 while r < lines.len() {
7646 let chars: Vec<char> = lines[r].chars().collect();
7647 while c < chars.len() {
7648 if chars[c] == open {
7649 return Some((r, c));
7650 }
7651 c += 1;
7652 }
7653 r += 1;
7654 c = 0;
7655 }
7656 None
7657}
7658
7659fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
7660 let (r, c) = pos;
7661 let line_len = lines[r].chars().count();
7662 if c < line_len {
7663 (r, c + 1)
7664 } else if r + 1 < lines.len() {
7665 (r + 1, 0)
7666 } else {
7667 pos
7668 }
7669}
7670
7671fn paragraph_text_object<H: crate::types::Host>(
7672 ed: &Editor<hjkl_buffer::Buffer, H>,
7673 inner: bool,
7674) -> Option<((usize, usize), (usize, usize))> {
7675 let (row, _) = ed.cursor();
7676 let rope = crate::types::Query::rope(&ed.buffer);
7677 let n_lines = rope.len_lines();
7678 if n_lines == 0 {
7679 return None;
7680 }
7681 let is_blank = |r: usize| -> bool {
7683 if r >= n_lines {
7684 return true;
7685 }
7686 rope_line_to_str(&rope, r).trim().is_empty()
7687 };
7688 if is_blank(row) {
7689 return None;
7690 }
7691 let mut top = row;
7692 while top > 0 && !is_blank(top - 1) {
7693 top -= 1;
7694 }
7695 let mut bot = row;
7696 while bot + 1 < n_lines && !is_blank(bot + 1) {
7697 bot += 1;
7698 }
7699 if !inner && bot + 1 < n_lines && is_blank(bot + 1) {
7701 bot += 1;
7702 }
7703 let end_col = rope_line_to_str(&rope, bot).chars().count();
7704 Some(((top, 0), (bot, end_col)))
7705}
7706
7707fn read_vim_range<H: crate::types::Host>(
7713 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7714 start: (usize, usize),
7715 end: (usize, usize),
7716 kind: RangeKind,
7717) -> String {
7718 let (top, bot) = order(start, end);
7719 ed.sync_buffer_content_from_textarea();
7720 let rope = crate::types::Query::rope(&ed.buffer);
7721 let n_lines = rope.len_lines();
7722 match kind {
7723 RangeKind::Linewise => {
7724 let lo = top.0;
7725 let hi = bot.0.min(n_lines.saturating_sub(1));
7726 let mut text = rope_row_range_str(&rope, lo, hi);
7727 text.push('\n');
7728 text
7729 }
7730 RangeKind::Inclusive | RangeKind::Exclusive => {
7731 let inclusive = matches!(kind, RangeKind::Inclusive);
7732 let mut out = String::new();
7734 for row in top.0..=bot.0 {
7735 if row >= n_lines {
7736 break;
7737 }
7738 let line = rope_line_to_str(&rope, row);
7739 let lo = if row == top.0 { top.1 } else { 0 };
7740 let hi_unclamped = if row == bot.0 {
7741 if inclusive { bot.1 + 1 } else { bot.1 }
7742 } else {
7743 line.chars().count() + 1
7744 };
7745 let row_chars: Vec<char> = line.chars().collect();
7746 let hi = hi_unclamped.min(row_chars.len());
7747 if lo < hi {
7748 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
7749 }
7750 if row < bot.0 {
7751 out.push('\n');
7752 }
7753 }
7754 out
7755 }
7756 }
7757}
7758
7759fn cut_vim_range<H: crate::types::Host>(
7768 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7769 start: (usize, usize),
7770 end: (usize, usize),
7771 kind: RangeKind,
7772) -> String {
7773 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
7774 let (top, bot) = order(start, end);
7775 ed.sync_buffer_content_from_textarea();
7776 let (buf_start, buf_end, buf_kind) = match kind {
7777 RangeKind::Linewise => (
7778 Position::new(top.0, 0),
7779 Position::new(bot.0, 0),
7780 BufKind::Line,
7781 ),
7782 RangeKind::Inclusive => {
7783 let line_chars = buf_line_chars(&ed.buffer, bot.0);
7784 let next = if bot.1 < line_chars {
7788 Position::new(bot.0, bot.1 + 1)
7789 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
7790 Position::new(bot.0 + 1, 0)
7791 } else {
7792 Position::new(bot.0, line_chars)
7793 };
7794 (Position::new(top.0, top.1), next, BufKind::Char)
7795 }
7796 RangeKind::Exclusive => (
7797 Position::new(top.0, top.1),
7798 Position::new(bot.0, bot.1),
7799 BufKind::Char,
7800 ),
7801 };
7802 let inverse = ed.mutate_edit(Edit::DeleteRange {
7803 start: buf_start,
7804 end: buf_end,
7805 kind: buf_kind,
7806 });
7807 let text = match inverse {
7808 Edit::InsertStr { text, .. } => text,
7809 _ => String::new(),
7810 };
7811 if !text.is_empty() {
7812 ed.record_yank_to_host(text.clone());
7813 ed.record_delete(text.clone(), matches!(kind, RangeKind::Linewise));
7814 }
7815 ed.push_buffer_cursor_to_textarea();
7816 text
7817}
7818
7819fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
7825 use hjkl_buffer::{Edit, MotionKind, Position};
7826 ed.sync_buffer_content_from_textarea();
7827 let cursor = buf_cursor_pos(&ed.buffer);
7828 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
7829 if cursor.col >= line_chars {
7830 return;
7831 }
7832 let inverse = ed.mutate_edit(Edit::DeleteRange {
7833 start: cursor,
7834 end: Position::new(cursor.row, line_chars),
7835 kind: MotionKind::Char,
7836 });
7837 if let Edit::InsertStr { text, .. } = inverse
7838 && !text.is_empty()
7839 {
7840 ed.record_yank_to_host(text.clone());
7841 ed.vim.yank_linewise = false;
7842 ed.set_yank(text);
7843 }
7844 buf_set_cursor_pos(&mut ed.buffer, cursor);
7845 ed.push_buffer_cursor_to_textarea();
7846}
7847
7848fn do_char_delete<H: crate::types::Host>(
7849 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7850 forward: bool,
7851 count: usize,
7852) {
7853 use hjkl_buffer::{Edit, MotionKind, Position};
7854 ed.push_undo();
7855 ed.sync_buffer_content_from_textarea();
7856 let mut deleted = String::new();
7859 for _ in 0..count {
7860 let cursor = buf_cursor_pos(&ed.buffer);
7861 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
7862 if forward {
7863 if cursor.col >= line_chars {
7866 continue;
7867 }
7868 let inverse = ed.mutate_edit(Edit::DeleteRange {
7869 start: cursor,
7870 end: Position::new(cursor.row, cursor.col + 1),
7871 kind: MotionKind::Char,
7872 });
7873 if let Edit::InsertStr { text, .. } = inverse {
7874 deleted.push_str(&text);
7875 }
7876 } else {
7877 if cursor.col == 0 {
7879 continue;
7880 }
7881 let inverse = ed.mutate_edit(Edit::DeleteRange {
7882 start: Position::new(cursor.row, cursor.col - 1),
7883 end: cursor,
7884 kind: MotionKind::Char,
7885 });
7886 if let Edit::InsertStr { text, .. } = inverse {
7887 deleted = text + &deleted;
7890 }
7891 }
7892 }
7893 if !deleted.is_empty() {
7894 ed.record_yank_to_host(deleted.clone());
7895 ed.record_delete(deleted, false);
7896 }
7897 ed.push_buffer_cursor_to_textarea();
7898}
7899
7900pub(crate) fn adjust_number<H: crate::types::Host>(
7905 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7906 delta: i64,
7907) -> bool {
7908 use hjkl_buffer::{Edit, MotionKind, Position};
7909 ed.sync_buffer_content_from_textarea();
7910 let cursor = buf_cursor_pos(&ed.buffer);
7911 let row = cursor.row;
7912 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
7913 Some(l) => l.chars().collect(),
7914 None => return false,
7915 };
7916 let len = chars.len();
7917
7918 let is_hex_prefix = |i: usize| {
7921 chars[i] == '0'
7922 && i + 1 < len
7923 && matches!(chars[i + 1], 'x' | 'X')
7924 && chars.get(i + 2).is_some_and(|c| c.is_ascii_hexdigit())
7925 };
7926 let mut i = cursor.col;
7927 let mut hex = false;
7928 loop {
7929 if i >= len {
7930 return false;
7931 }
7932 if is_hex_prefix(i) {
7933 hex = true;
7934 break;
7935 }
7936 if chars[i].is_ascii_digit() {
7937 break;
7938 }
7939 i += 1;
7940 }
7941
7942 let (span_start, span_end, new_s) = if hex {
7943 let digits_start = i + 2;
7945 let mut digits_end = digits_start;
7946 while digits_end < len && chars[digits_end].is_ascii_hexdigit() {
7947 digits_end += 1;
7948 }
7949 let hexs: String = chars[digits_start..digits_end].iter().collect();
7950 let Ok(n) = u64::from_str_radix(&hexs, 16) else {
7951 return false;
7952 };
7953 let new_val = (n as i128 + delta as i128).max(0) as u64;
7954 let width = digits_end - digits_start;
7955 let prefix: String = chars[i..digits_start].iter().collect();
7956 (i, digits_end, format!("{prefix}{new_val:0width$x}"))
7957 } else {
7958 let digit_start = i;
7960 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
7961 digit_start - 1
7962 } else {
7963 digit_start
7964 };
7965 let mut span_end = digit_start;
7966 while span_end < len && chars[span_end].is_ascii_digit() {
7967 span_end += 1;
7968 }
7969 let s: String = chars[span_start..span_end].iter().collect();
7970 let Ok(n) = s.parse::<i64>() else {
7971 return false;
7972 };
7973 (span_start, span_end, n.saturating_add(delta).to_string())
7974 };
7975
7976 ed.push_undo();
7977 let span_start_pos = Position::new(row, span_start);
7978 let span_end_pos = Position::new(row, span_end);
7979 ed.mutate_edit(Edit::DeleteRange {
7980 start: span_start_pos,
7981 end: span_end_pos,
7982 kind: MotionKind::Char,
7983 });
7984 ed.mutate_edit(Edit::InsertStr {
7985 at: span_start_pos,
7986 text: new_s.clone(),
7987 });
7988 let new_len = new_s.chars().count();
7989 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
7990 ed.push_buffer_cursor_to_textarea();
7991 true
7992}
7993
7994pub(crate) fn replace_char<H: crate::types::Host>(
7995 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7996 ch: char,
7997 count: usize,
7998) {
7999 use hjkl_buffer::{Edit, MotionKind, Position};
8000 ed.push_undo();
8001 ed.sync_buffer_content_from_textarea();
8002 for _ in 0..count {
8003 let cursor = buf_cursor_pos(&ed.buffer);
8004 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
8005 if cursor.col >= line_chars {
8006 break;
8007 }
8008 ed.mutate_edit(Edit::DeleteRange {
8009 start: cursor,
8010 end: Position::new(cursor.row, cursor.col + 1),
8011 kind: MotionKind::Char,
8012 });
8013 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
8014 }
8015 crate::motions::move_left(&mut ed.buffer, 1);
8017 ed.push_buffer_cursor_to_textarea();
8018}
8019
8020fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
8021 use hjkl_buffer::{Edit, MotionKind, Position};
8022 ed.sync_buffer_content_from_textarea();
8023 let cursor = buf_cursor_pos(&ed.buffer);
8024 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
8025 return;
8026 };
8027 let toggled = if c.is_uppercase() {
8028 c.to_lowercase().next().unwrap_or(c)
8029 } else {
8030 c.to_uppercase().next().unwrap_or(c)
8031 };
8032 ed.mutate_edit(Edit::DeleteRange {
8033 start: cursor,
8034 end: Position::new(cursor.row, cursor.col + 1),
8035 kind: MotionKind::Char,
8036 });
8037 ed.mutate_edit(Edit::InsertChar {
8038 at: cursor,
8039 ch: toggled,
8040 });
8041}
8042
8043fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
8044 use hjkl_buffer::{Edit, Position};
8045 ed.sync_buffer_content_from_textarea();
8046 let row = buf_cursor_pos(&ed.buffer).row;
8047 if row + 1 >= buf_row_count(&ed.buffer) {
8048 return;
8049 }
8050 let cur_line = buf_line(&ed.buffer, row).unwrap_or_default();
8051 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or_default();
8052 let next_trimmed = next_raw.trim_start();
8053 let cur_chars = cur_line.chars().count();
8054 let next_chars = next_raw.chars().count();
8055 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
8058 " "
8059 } else {
8060 ""
8061 };
8062 let joined = format!("{cur_line}{separator}{next_trimmed}");
8063 ed.mutate_edit(Edit::Replace {
8064 start: Position::new(row, 0),
8065 end: Position::new(row + 1, next_chars),
8066 with: joined,
8067 });
8068 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
8072 ed.push_buffer_cursor_to_textarea();
8073}
8074
8075fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
8078 use hjkl_buffer::Edit;
8079 ed.sync_buffer_content_from_textarea();
8080 let row = buf_cursor_pos(&ed.buffer).row;
8081 if row + 1 >= buf_row_count(&ed.buffer) {
8082 return;
8083 }
8084 let join_col = buf_line_chars(&ed.buffer, row);
8085 ed.mutate_edit(Edit::JoinLines {
8086 row,
8087 count: 1,
8088 with_space: false,
8089 });
8090 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
8092 ed.push_buffer_cursor_to_textarea();
8093}
8094
8095pub(crate) fn visual_join<H: crate::types::Host>(
8099 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8100 with_space: bool,
8101) {
8102 let cursor_row = buf_cursor_pos(&ed.buffer).row;
8103 let (top, bot) = match ed.vim.mode {
8104 Mode::VisualLine => (
8105 cursor_row.min(ed.vim.visual_line_anchor),
8106 cursor_row.max(ed.vim.visual_line_anchor),
8107 ),
8108 Mode::VisualBlock => {
8109 let a = ed.vim.block_anchor.0;
8110 (a.min(cursor_row), a.max(cursor_row))
8111 }
8112 Mode::Visual => {
8113 let a = ed.vim.visual_anchor.0;
8114 (a.min(cursor_row), a.max(cursor_row))
8115 }
8116 _ => return,
8117 };
8118 let joins = (bot - top).max(1);
8121 ed.push_undo();
8122 buf_set_cursor_rc(&mut ed.buffer, top, 0);
8123 ed.push_buffer_cursor_to_textarea();
8124 for _ in 0..joins {
8125 if with_space {
8126 join_line(ed);
8127 } else {
8128 join_line_raw(ed);
8129 }
8130 }
8131 ed.vim.mode = Mode::Normal;
8132 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
8133}
8134
8135pub(crate) fn goto_percent<H: crate::types::Host>(
8138 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8139 count: usize,
8140) {
8141 let rows = buf_row_count(&ed.buffer);
8142 if rows == 0 {
8143 return;
8144 }
8145 let total = if rows >= 2
8148 && buf_line(&ed.buffer, rows - 1)
8149 .map(|s| s.is_empty())
8150 .unwrap_or(false)
8151 {
8152 rows - 1
8153 } else {
8154 rows
8155 };
8156 let line = (count * total).div_ceil(100).clamp(1, total);
8158 let pre = ed.cursor();
8159 ed.jump_cursor(line - 1, 0);
8160 move_first_non_whitespace(ed);
8161 ed.sticky_col = Some(ed.cursor().1);
8162 if ed.cursor() != pre {
8163 ed.push_jump(pre);
8164 }
8165}
8166
8167fn indent_width(s: &str, tabstop: usize) -> usize {
8170 let ts = tabstop.max(1);
8171 let mut w = 0usize;
8172 for c in s.chars() {
8173 match c {
8174 ' ' => w += 1,
8175 '\t' => w += ts - (w % ts),
8176 _ => break,
8177 }
8178 }
8179 w
8180}
8181
8182fn build_indent(width: usize, settings: &crate::editor::Settings) -> String {
8185 if settings.expandtab {
8186 return " ".repeat(width);
8187 }
8188 let ts = settings.tabstop.max(1);
8189 let tabs = width / ts;
8190 let spaces = width % ts;
8191 format!("{}{}", "\t".repeat(tabs), " ".repeat(spaces))
8192}
8193
8194fn reindent_block(text: &str, target_width: usize, settings: &crate::editor::Settings) -> String {
8197 let ts = settings.tabstop.max(1);
8198 let lines: Vec<&str> = text.split('\n').collect();
8199 let first_width = lines.first().map(|l| indent_width(l, ts)).unwrap_or(0);
8200 let delta = target_width as isize - first_width as isize;
8201 lines
8202 .iter()
8203 .map(|line| {
8204 let trimmed = line.trim_start_matches([' ', '\t']);
8205 if trimmed.is_empty() {
8206 return String::new();
8208 }
8209 let old_w = indent_width(line, ts) as isize;
8210 let new_w = (old_w + delta).max(0) as usize;
8211 format!("{}{}", build_indent(new_w, settings), trimmed)
8212 })
8213 .collect::<Vec<_>>()
8214 .join("\n")
8215}
8216
8217fn do_paste<H: crate::types::Host>(
8218 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8219 before: bool,
8220 count: usize,
8221 cursor_after: bool,
8222 reindent: bool,
8223) {
8224 use hjkl_buffer::{Edit, Position};
8225 ed.push_undo();
8226 let selector = ed.vim.pending_register.take();
8231 let (yank, linewise) = {
8232 let regs = ed.registers();
8233 match selector.and_then(|c| regs.read(c)) {
8234 Some(slot) => (slot.text.clone(), slot.linewise),
8235 None => (regs.unnamed.text.clone(), regs.unnamed.linewise),
8241 }
8242 };
8243 let mut paste_mark: Option<((usize, usize), (usize, usize))> = None;
8247 let original_row_for_linewise_after = if linewise && !before {
8253 let r = buf_cursor_pos(&ed.buffer).row;
8256 let (_, fold_end) = expand_linewise_over_closed_folds(&ed.buffer, r, r);
8257 Some(fold_end)
8258 } else {
8259 None
8260 };
8261 for _ in 0..count {
8262 ed.sync_buffer_content_from_textarea();
8263 let yank = yank.clone();
8264 if yank.is_empty() {
8265 continue;
8266 }
8267 if linewise {
8268 let mut text = yank.trim_matches('\n').to_string();
8272 let row = buf_cursor_pos(&ed.buffer).row;
8273 if reindent {
8275 let cur_line = buf_line(&ed.buffer, row).unwrap_or_default();
8276 let target_w = indent_width(&cur_line, ed.settings.tabstop.max(1));
8277 text = reindent_block(&text, target_w, &ed.settings);
8278 }
8279 let (fold_start, fold_end) = expand_linewise_over_closed_folds(&ed.buffer, row, row);
8283 let target_row = if before {
8284 ed.mutate_edit(Edit::InsertStr {
8285 at: Position::new(fold_start, 0),
8286 text: format!("{text}\n"),
8287 });
8288 fold_start
8289 } else {
8290 let line_chars = buf_line_chars(&ed.buffer, fold_end);
8291 ed.mutate_edit(Edit::InsertStr {
8292 at: Position::new(fold_end, line_chars),
8293 text: format!("\n{text}"),
8294 });
8295 fold_end + 1
8296 };
8297 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
8298 crate::motions::move_first_non_blank(&mut ed.buffer);
8299 ed.push_buffer_cursor_to_textarea();
8300 let payload_lines = text.lines().count().max(1);
8302 let bot_row = target_row + payload_lines - 1;
8303 let bot_last_col = buf_line_chars(&ed.buffer, bot_row).saturating_sub(1);
8304 paste_mark = Some(((target_row, 0), (bot_row, bot_last_col)));
8305 } else {
8306 let cursor = buf_cursor_pos(&ed.buffer);
8310 let at = if before {
8311 cursor
8312 } else {
8313 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
8314 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
8315 };
8316 ed.mutate_edit(Edit::InsertStr {
8317 at,
8318 text: yank.clone(),
8319 });
8320 if !cursor_after && ed.cursor().1 > 0 {
8325 crate::motions::move_left(&mut ed.buffer, 1);
8326 ed.push_buffer_cursor_to_textarea();
8327 }
8328 let lo = (at.row, at.col);
8330 let hi = if cursor_after {
8331 let c = ed.cursor();
8332 (c.0, c.1.saturating_sub(1))
8333 } else {
8334 ed.cursor()
8335 };
8336 paste_mark = Some((lo, hi));
8337 }
8338 }
8339 if let Some((lo, hi)) = paste_mark {
8340 ed.set_mark('[', lo);
8341 ed.set_mark(']', hi);
8342 }
8343 if cursor_after && linewise {
8346 if let Some((_, (bot_row, _))) = paste_mark {
8347 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
8348 let target = (bot_row + 1).min(last_row);
8349 buf_set_cursor_rc(&mut ed.buffer, target, 0);
8350 ed.push_buffer_cursor_to_textarea();
8351 }
8352 } else if let Some(orig_row) = original_row_for_linewise_after {
8353 let first_target = orig_row.saturating_add(1);
8358 buf_set_cursor_rc(&mut ed.buffer, first_target, 0);
8359 crate::motions::move_first_non_blank(&mut ed.buffer);
8360 ed.push_buffer_cursor_to_textarea();
8361 }
8362 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
8364}
8365
8366pub(crate) fn visual_paste<H: crate::types::Host>(
8371 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8372 before: bool,
8373) {
8374 use hjkl_buffer::{Edit, Position};
8375 ed.sync_buffer_content_from_textarea();
8376
8377 let selector = ed.vim.pending_register.take();
8380 let (reg_text, reg_linewise) = {
8381 let regs = ed.registers();
8382 match selector.and_then(|c| regs.read(c)) {
8383 Some(slot) => (slot.text.clone(), slot.linewise),
8384 None => (regs.unnamed.text.clone(), regs.unnamed.linewise),
8385 }
8386 };
8387 let saved_unnamed = before.then(|| ed.registers().unnamed.clone());
8389
8390 let mode = ed.vim.mode;
8391 ed.push_undo();
8392
8393 match mode {
8394 Mode::VisualLine => {
8395 let cursor_row = buf_cursor_pos(&ed.buffer).row;
8396 let top = cursor_row.min(ed.vim.visual_line_anchor);
8397 let bot = cursor_row.max(ed.vim.visual_line_anchor);
8398 cut_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
8400 let text = reg_text.trim_matches('\n').to_string();
8402 let line_count = buf_row_count(&ed.buffer);
8403 if top >= line_count {
8404 let last = line_count.saturating_sub(1);
8407 let lc = buf_line_chars(&ed.buffer, last);
8408 ed.mutate_edit(Edit::InsertStr {
8409 at: Position::new(last, lc),
8410 text: format!("\n{text}"),
8411 });
8412 buf_set_cursor_rc(&mut ed.buffer, last + 1, 0);
8413 } else {
8414 ed.mutate_edit(Edit::InsertStr {
8415 at: Position::new(top, 0),
8416 text: format!("{text}\n"),
8417 });
8418 buf_set_cursor_rc(&mut ed.buffer, top, 0);
8419 }
8420 crate::motions::move_first_non_blank(&mut ed.buffer);
8421 ed.push_buffer_cursor_to_textarea();
8422 }
8423 Mode::Visual | Mode::VisualBlock => {
8424 let anchor = if mode == Mode::VisualBlock {
8425 ed.vim.block_anchor
8426 } else {
8427 ed.vim.visual_anchor
8428 };
8429 let cursor = ed.cursor();
8430 let (top, bot) = order(anchor, cursor);
8431 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
8433 if reg_linewise {
8435 let text = reg_text.trim_matches('\n').to_string();
8437 let lc = buf_line_chars(&ed.buffer, top.0);
8438 ed.mutate_edit(Edit::InsertStr {
8439 at: Position::new(top.0, lc),
8440 text: format!("\n{text}"),
8441 });
8442 buf_set_cursor_rc(&mut ed.buffer, top.0 + 1, 0);
8443 crate::motions::move_first_non_blank(&mut ed.buffer);
8444 } else {
8445 ed.mutate_edit(Edit::InsertStr {
8446 at: Position::new(top.0, top.1),
8447 text: reg_text.clone(),
8448 });
8449 let inserted_len = reg_text.chars().count();
8451 let last_col = top.1 + inserted_len.saturating_sub(1);
8452 buf_set_cursor_rc(&mut ed.buffer, top.0, last_col);
8453 }
8454 ed.push_buffer_cursor_to_textarea();
8455 }
8456 _ => {}
8457 }
8458
8459 if let Some(slot) = saved_unnamed {
8461 ed.registers_mut().unnamed = slot;
8462 }
8463 ed.vim.mode = Mode::Normal;
8464 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
8465}
8466
8467pub(crate) fn adjust_number_visual<H: crate::types::Host>(
8472 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8473 delta: i64,
8474 sequential: bool,
8475) {
8476 use hjkl_buffer::{Edit, MotionKind, Position};
8477 ed.sync_buffer_content_from_textarea();
8478 let mode = ed.vim.mode;
8479 let cursor = buf_cursor_pos(&ed.buffer);
8480
8481 let (top, bot, mut scan_col_first, block_left) = match mode {
8483 Mode::VisualLine => {
8484 let t = cursor.row.min(ed.vim.visual_line_anchor);
8485 let b = cursor.row.max(ed.vim.visual_line_anchor);
8486 (t, b, 0usize, None)
8487 }
8488 Mode::Visual => {
8489 let (a, c) = order(ed.vim.visual_anchor, (cursor.row, cursor.col));
8490 (a.0, c.0, a.1, None)
8491 }
8492 Mode::VisualBlock => {
8493 let (a, c) = order(ed.vim.block_anchor, (cursor.row, cursor.col));
8494 let left = a.1.min(c.1);
8495 (a.0, c.0, left, Some(left))
8496 }
8497 _ => return,
8498 };
8499
8500 ed.push_undo();
8501 let mut found_count: i64 = 0;
8502 for row in top..=bot {
8503 let start_col = match block_left {
8504 Some(left) => left,
8505 None => {
8506 let c = if row == top { scan_col_first } else { 0 };
8509 scan_col_first = 0;
8510 c
8511 }
8512 };
8513 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
8514 Some(l) => l.chars().collect(),
8515 None => continue,
8516 };
8517 let Some(digit_start) =
8518 (start_col.min(chars.len())..chars.len()).find(|&i| chars[i].is_ascii_digit())
8519 else {
8520 continue;
8521 };
8522 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
8523 digit_start - 1
8524 } else {
8525 digit_start
8526 };
8527 let mut span_end = digit_start;
8528 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
8529 span_end += 1;
8530 }
8531 let s: String = chars[span_start..span_end].iter().collect();
8532 let Ok(n) = s.parse::<i64>() else {
8533 continue;
8534 };
8535 found_count += 1;
8536 let this_delta = if sequential {
8537 delta.saturating_mul(found_count)
8538 } else {
8539 delta
8540 };
8541 let new_s = n.saturating_add(this_delta).to_string();
8542 let span_start_pos = Position::new(row, span_start);
8543 let span_end_pos = Position::new(row, span_end);
8544 ed.mutate_edit(Edit::DeleteRange {
8545 start: span_start_pos,
8546 end: span_end_pos,
8547 kind: MotionKind::Char,
8548 });
8549 ed.mutate_edit(Edit::InsertStr {
8550 at: span_start_pos,
8551 text: new_s,
8552 });
8553 }
8554 buf_set_cursor_rc(&mut ed.buffer, top, block_left.unwrap_or(0));
8556 ed.push_buffer_cursor_to_textarea();
8557 ed.vim.mode = Mode::Normal;
8558 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
8559}
8560
8561pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
8562 if let Some(entry) = ed.buffer.pop_undo_entry() {
8563 let (cur_rope, cur_cursor) = ed.snapshot();
8564 ed.buffer.push_redo_entry(hjkl_buffer::UndoEntry {
8565 rope: cur_rope,
8566 cursor: cur_cursor,
8567 timestamp: entry.timestamp,
8568 });
8569 ed.restore_rope(entry.rope, entry.cursor);
8570 }
8571 ed.vim.mode = Mode::Normal;
8572 clamp_cursor_to_normal_mode(ed);
8576}
8577
8578pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
8579 if let Some(entry) = ed.buffer.pop_redo_entry() {
8580 let (cur_rope, cur_cursor) = ed.snapshot();
8581 let before = cur_rope.clone();
8582 ed.buffer.push_undo_entry(hjkl_buffer::UndoEntry {
8583 rope: cur_rope,
8584 cursor: cur_cursor,
8585 timestamp: entry.timestamp,
8586 });
8587 ed.cap_undo();
8588 ed.restore_rope(entry.rope, entry.cursor);
8589 let after = crate::types::Query::rope(&ed.buffer);
8593 if let Some((row, col)) = first_diff_pos(&before, &after) {
8594 buf_set_cursor_rc(&mut ed.buffer, row, col);
8595 ed.push_buffer_cursor_to_textarea();
8596 }
8597 }
8598 ed.vim.mode = Mode::Normal;
8599 clamp_cursor_to_normal_mode(ed);
8600}
8601
8602fn first_diff_pos(a: &ropey::Rope, b: &ropey::Rope) -> Option<(usize, usize)> {
8605 let rows = a.len_lines().max(b.len_lines());
8606 for r in 0..rows {
8607 let la = if r < a.len_lines() {
8608 hjkl_buffer::rope_line_str(a, r)
8609 } else {
8610 String::new()
8611 };
8612 let lb = if r < b.len_lines() {
8613 hjkl_buffer::rope_line_str(b, r)
8614 } else {
8615 String::new()
8616 };
8617 if la != lb {
8618 let col = la
8619 .chars()
8620 .zip(lb.chars())
8621 .take_while(|(x, y)| x == y)
8622 .count();
8623 return Some((r, col));
8624 }
8625 }
8626 None
8627}
8628
8629fn replay_insert_and_finish<H: crate::types::Host>(
8636 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8637 text: &str,
8638) {
8639 use hjkl_buffer::{Edit, Position};
8640 let cursor = ed.cursor();
8641 ed.mutate_edit(Edit::InsertStr {
8642 at: Position::new(cursor.0, cursor.1),
8643 text: text.to_string(),
8644 });
8645 if ed.vim.insert_session.take().is_some() {
8646 if ed.cursor().1 > 0 {
8647 crate::motions::move_left(&mut ed.buffer, 1);
8648 ed.push_buffer_cursor_to_textarea();
8649 }
8650 ed.vim.mode = Mode::Normal;
8651 }
8652}
8653
8654pub(crate) fn replay_last_change<H: crate::types::Host>(
8655 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8656 outer_count: usize,
8657) {
8658 let Some(change) = ed.vim.last_change.clone() else {
8659 return;
8660 };
8661 ed.vim.replaying = true;
8662 let scale = if outer_count > 0 { outer_count } else { 1 };
8663 match change {
8664 LastChange::OpMotion {
8665 op,
8666 motion,
8667 count,
8668 inserted,
8669 } => {
8670 let total = count.max(1) * scale;
8671 apply_op_with_motion(ed, op, &motion, total);
8672 if let Some(text) = inserted {
8673 replay_insert_and_finish(ed, &text);
8674 }
8675 }
8676 LastChange::OpTextObj {
8677 op,
8678 obj,
8679 inner,
8680 inserted,
8681 } => {
8682 apply_op_with_text_object(ed, op, obj, inner, 1);
8685 if let Some(text) = inserted {
8686 replay_insert_and_finish(ed, &text);
8687 }
8688 }
8689 LastChange::LineOp {
8690 op,
8691 count,
8692 inserted,
8693 } => {
8694 let total = count.max(1) * scale;
8695 execute_line_op(ed, op, total);
8696 if let Some(text) = inserted {
8697 replay_insert_and_finish(ed, &text);
8698 }
8699 }
8700 LastChange::CharDel { forward, count } => {
8701 do_char_delete(ed, forward, count * scale);
8702 }
8703 LastChange::ReplaceChar { ch, count } => {
8704 replace_char(ed, ch, count * scale);
8705 }
8706 LastChange::ToggleCase { count } => {
8707 for _ in 0..count * scale {
8708 ed.push_undo();
8709 toggle_case_at_cursor(ed);
8710 }
8711 }
8712 LastChange::JoinLine { count } => {
8713 for _ in 0..count * scale {
8714 ed.push_undo();
8715 join_line(ed);
8716 }
8717 }
8718 LastChange::Paste {
8719 before,
8720 count,
8721 cursor_after,
8722 reindent,
8723 } => {
8724 do_paste(ed, before, count * scale, cursor_after, reindent);
8725 }
8726 LastChange::GnOp {
8727 op,
8728 forward,
8729 inserted,
8730 } => {
8731 gn_operate(ed, Some(op), forward, 1);
8732 if let Some(text) = inserted {
8733 replay_insert_and_finish(ed, &text);
8734 }
8735 }
8736 LastChange::ReplaceMode { text } => {
8737 use hjkl_buffer::{Edit, MotionKind, Position};
8738 ed.push_undo();
8739 for ch in text.chars() {
8740 let cursor = buf_cursor_pos(&ed.buffer);
8741 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
8742 if cursor.col < line_chars {
8743 ed.mutate_edit(Edit::DeleteRange {
8745 start: cursor,
8746 end: Position::new(cursor.row, cursor.col + 1),
8747 kind: MotionKind::Char,
8748 });
8749 }
8750 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
8751 buf_set_cursor_rc(&mut ed.buffer, cursor.row, cursor.col + 1);
8752 }
8753 if ed.cursor().1 > 0 {
8755 crate::motions::move_left(&mut ed.buffer, 1);
8756 }
8757 ed.push_buffer_cursor_to_textarea();
8758 }
8759 LastChange::DeleteToEol { inserted } => {
8760 use hjkl_buffer::{Edit, Position};
8761 ed.push_undo();
8762 delete_to_eol(ed);
8763 if let Some(text) = inserted {
8764 let cursor = ed.cursor();
8765 ed.mutate_edit(Edit::InsertStr {
8766 at: Position::new(cursor.0, cursor.1),
8767 text,
8768 });
8769 }
8770 }
8771 LastChange::OpenLine { above, inserted } => {
8772 use hjkl_buffer::{Edit, Position};
8773 ed.push_undo();
8774 ed.sync_buffer_content_from_textarea();
8775 let row = buf_cursor_pos(&ed.buffer).row;
8776 if above {
8777 ed.mutate_edit(Edit::InsertStr {
8778 at: Position::new(row, 0),
8779 text: "\n".to_string(),
8780 });
8781 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
8782 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
8783 } else {
8784 let line_chars = buf_line_chars(&ed.buffer, row);
8785 ed.mutate_edit(Edit::InsertStr {
8786 at: Position::new(row, line_chars),
8787 text: "\n".to_string(),
8788 });
8789 }
8790 ed.push_buffer_cursor_to_textarea();
8791 let cursor = ed.cursor();
8792 ed.mutate_edit(Edit::InsertStr {
8793 at: Position::new(cursor.0, cursor.1),
8794 text: inserted,
8795 });
8796 }
8797 LastChange::InsertAt {
8798 entry,
8799 inserted,
8800 count,
8801 } => {
8802 use hjkl_buffer::{Edit, Position};
8803 ed.push_undo();
8804 match entry {
8805 InsertEntry::I => {}
8806 InsertEntry::ShiftI => move_first_non_whitespace(ed),
8807 InsertEntry::A => {
8808 crate::motions::move_right_to_end(&mut ed.buffer, 1);
8809 ed.push_buffer_cursor_to_textarea();
8810 }
8811 InsertEntry::ShiftA => {
8812 crate::motions::move_line_end(&mut ed.buffer);
8813 crate::motions::move_right_to_end(&mut ed.buffer, 1);
8814 ed.push_buffer_cursor_to_textarea();
8815 }
8816 }
8817 for _ in 0..count.max(1) {
8818 let cursor = ed.cursor();
8819 ed.mutate_edit(Edit::InsertStr {
8820 at: Position::new(cursor.0, cursor.1),
8821 text: inserted.clone(),
8822 });
8823 }
8824 }
8825 }
8826 ed.vim.replaying = false;
8827}
8828
8829fn changed_run(before: &str, after: &str) -> String {
8835 let a: Vec<char> = before.chars().collect();
8836 let b: Vec<char> = after.chars().collect();
8837 let prefix = a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count();
8838 let max_suffix = a.len().min(b.len()) - prefix;
8839 let suffix = a
8840 .iter()
8841 .rev()
8842 .zip(b.iter().rev())
8843 .take(max_suffix)
8844 .take_while(|(x, y)| x == y)
8845 .count();
8846 b[prefix..b.len() - suffix].iter().collect()
8847}
8848
8849fn extract_inserted(before: &str, after: &str) -> String {
8850 let before_chars: Vec<char> = before.chars().collect();
8851 let after_chars: Vec<char> = after.chars().collect();
8852 if after_chars.len() <= before_chars.len() {
8853 return String::new();
8854 }
8855 let prefix = before_chars
8856 .iter()
8857 .zip(after_chars.iter())
8858 .take_while(|(a, b)| a == b)
8859 .count();
8860 let max_suffix = before_chars.len() - prefix;
8861 let suffix = before_chars
8862 .iter()
8863 .rev()
8864 .zip(after_chars.iter().rev())
8865 .take(max_suffix)
8866 .take_while(|(a, b)| a == b)
8867 .count();
8868 after_chars[prefix..after_chars.len() - suffix]
8869 .iter()
8870 .collect()
8871}
8872
8873#[cfg(test)]
8876mod comment_continuation_tests {
8877 use super::*;
8878 use crate::{DefaultHost, Editor, Options};
8879 use hjkl_buffer::Buffer;
8880
8881 fn make_editor_with_lang(lang: &str, content: &str) -> Editor<Buffer, DefaultHost> {
8882 let buf = Buffer::from_str(content);
8883 let host = DefaultHost::new();
8884 let opts = Options {
8885 filetype: lang.to_string(),
8886 formatoptions: "ro".to_string(),
8887 ..Options::default()
8888 };
8889 Editor::new(buf, host, opts)
8890 }
8891
8892 #[test]
8893 fn detect_rust_doc_comment() {
8894 let result = detect_comment_on_line("rust", "/// foo bar");
8895 assert!(result.is_some());
8896 let (indent, prefix) = result.unwrap();
8897 assert_eq!(indent, "");
8898 assert_eq!(prefix, "/// ");
8899 }
8900
8901 #[test]
8902 fn detect_rust_inner_doc_comment() {
8903 let result = detect_comment_on_line("rust", "//! crate docs");
8904 assert!(result.is_some());
8905 let (_, prefix) = result.unwrap();
8906 assert_eq!(prefix, "//! ");
8907 }
8908
8909 #[test]
8910 fn detect_rust_plain_comment() {
8911 let result = detect_comment_on_line("rust", "// normal comment");
8912 assert!(result.is_some());
8913 let (_, prefix) = result.unwrap();
8914 assert_eq!(prefix, "// ");
8915 }
8916
8917 #[test]
8918 fn detect_indented_comment() {
8919 let result = detect_comment_on_line("rust", " // indented");
8920 assert!(result.is_some());
8921 let (indent, prefix) = result.unwrap();
8922 assert_eq!(indent, " ");
8923 assert_eq!(prefix, "// ");
8924 }
8925
8926 #[test]
8927 fn detect_python_hash() {
8928 let result = detect_comment_on_line("python", "# comment");
8929 assert!(result.is_some());
8930 let (_, prefix) = result.unwrap();
8931 assert_eq!(prefix, "# ");
8932 }
8933
8934 #[test]
8935 fn detect_lua_double_dash() {
8936 let result = detect_comment_on_line("lua", "-- a lua comment");
8937 assert!(result.is_some());
8938 let (_, prefix) = result.unwrap();
8939 assert_eq!(prefix, "-- ");
8940 }
8941
8942 #[test]
8943 fn detect_non_comment_is_none() {
8944 assert!(detect_comment_on_line("rust", "let x = 1;").is_none());
8945 assert!(detect_comment_on_line("python", "x = 1").is_none());
8946 }
8947
8948 #[test]
8949 fn detect_bare_double_slash_still_matches() {
8950 assert!(detect_comment_on_line("rust", "//").is_some());
8952 }
8953
8954 #[test]
8955 fn rust_doc_before_plain() {
8956 let result = detect_comment_on_line("rust", "/// outer doc");
8958 let (_, prefix) = result.unwrap();
8959 assert_eq!(prefix, "/// ", "/// must match before //");
8960 }
8961
8962 #[test]
8963 fn continue_comment_returns_prefix_for_comment_row() {
8964 let ed = make_editor_with_lang("rust", "/// hello\n");
8965 let cont = continue_comment(&ed.buffer, &ed.settings, 0);
8966 assert_eq!(cont, Some("/// ".to_string()));
8967 }
8968
8969 #[test]
8970 fn continue_comment_returns_none_for_non_comment() {
8971 let ed = make_editor_with_lang("rust", "let x = 1;\n");
8972 let cont = continue_comment(&ed.buffer, &ed.settings, 0);
8973 assert!(cont.is_none());
8974 }
8975
8976 #[test]
8977 fn continue_comment_returns_none_when_filetype_empty() {
8978 let buf = Buffer::from_str("// hello\n");
8979 let host = DefaultHost::new();
8980 let ed = Editor::new(buf, host, Options::default());
8982 let cont = continue_comment(&ed.buffer, &ed.settings, 0);
8983 assert!(cont.is_none());
8984 }
8985}
8986
8987#[cfg(test)]
8988mod comment_toggle_tests {
8989 use super::*;
8990 use crate::{DefaultHost, Editor, Options};
8991 use hjkl_buffer::Buffer;
8992
8993 fn make_rust_editor(content: &str) -> Editor<Buffer, DefaultHost> {
8994 let buf = Buffer::from_str(content);
8995 let host = DefaultHost::new();
8996 let opts = Options {
8997 filetype: "rust".to_string(),
8998 ..Options::default()
8999 };
9000 Editor::new(buf, host, opts)
9001 }
9002
9003 fn line(ed: &Editor<Buffer, DefaultHost>, row: usize) -> String {
9004 buf_line(&ed.buffer, row).unwrap_or_default()
9005 }
9006
9007 #[test]
9010 fn gcc_comments_rust_line() {
9011 let mut ed = make_rust_editor("let x = 1;");
9012 ed.toggle_comment_range(0, 0);
9013 assert_eq!(line(&ed, 0), "// let x = 1;");
9014 }
9015
9016 #[test]
9017 fn gcc_uncomments_rust_line() {
9018 let mut ed = make_rust_editor("// let x = 1;");
9019 ed.toggle_comment_range(0, 0);
9020 assert_eq!(line(&ed, 0), "let x = 1;");
9021 }
9022
9023 #[test]
9024 fn gcc_indent_preserving() {
9025 let mut ed = make_rust_editor(" let x = 1;");
9027 ed.toggle_comment_range(0, 0);
9028 assert_eq!(line(&ed, 0), " // let x = 1;");
9029 }
9030
9031 #[test]
9032 fn gcc_indent_preserving_uncomment() {
9033 let mut ed = make_rust_editor(" // let x = 1;");
9034 ed.toggle_comment_range(0, 0);
9035 assert_eq!(line(&ed, 0), " let x = 1;");
9036 }
9037
9038 #[test]
9041 fn toggle_multi_line_all_uncommented() {
9042 let content = "let a = 1;\nlet b = 2;\nlet c = 3;";
9043 let mut ed = make_rust_editor(content);
9044 ed.toggle_comment_range(0, 2);
9045 assert_eq!(line(&ed, 0), "// let a = 1;");
9046 assert_eq!(line(&ed, 1), "// let b = 2;");
9047 assert_eq!(line(&ed, 2), "// let c = 3;");
9048 }
9049
9050 #[test]
9051 fn toggle_multi_line_all_commented() {
9052 let content = "// let a = 1;\n// let b = 2;\n// let c = 3;";
9053 let mut ed = make_rust_editor(content);
9054 ed.toggle_comment_range(0, 2);
9055 assert_eq!(line(&ed, 0), "let a = 1;");
9056 assert_eq!(line(&ed, 1), "let b = 2;");
9057 assert_eq!(line(&ed, 2), "let c = 3;");
9058 }
9059
9060 #[test]
9063 fn toggle_mixed_state_comments_all() {
9064 let content = "let a = 1;\n// let b = 2;\nlet c = 3;\n// let d = 4;\nlet e = 5;";
9066 let mut ed = make_rust_editor(content);
9067 ed.toggle_comment_range(0, 4);
9068 for r in 0..5 {
9069 assert!(
9070 line(&ed, r).trim_start().starts_with("//"),
9071 "row {r} not commented: {:?}",
9072 line(&ed, r)
9073 );
9074 }
9075 }
9076
9077 #[test]
9080 fn blank_lines_not_commented() {
9081 let content = "let a = 1;\n\nlet b = 2;";
9082 let mut ed = make_rust_editor(content);
9083 ed.toggle_comment_range(0, 2);
9084 assert_eq!(line(&ed, 0), "// let a = 1;");
9085 assert_eq!(line(&ed, 1), ""); assert_eq!(line(&ed, 2), "// let b = 2;");
9087 }
9088
9089 #[test]
9092 fn python_comment_toggle() {
9093 let buf = Buffer::from_str("x = 1\ny = 2");
9094 let host = DefaultHost::new();
9095 let opts = Options {
9096 filetype: "python".to_string(),
9097 ..Options::default()
9098 };
9099 let mut ed = Editor::new(buf, host, opts);
9100 ed.toggle_comment_range(0, 1);
9101 assert_eq!(line(&ed, 0), "# x = 1");
9102 assert_eq!(line(&ed, 1), "# y = 2");
9103 ed.toggle_comment_range(0, 1);
9105 assert_eq!(line(&ed, 0), "x = 1");
9106 assert_eq!(line(&ed, 1), "y = 2");
9107 }
9108
9109 #[test]
9112 fn commentstring_override_via_setting() {
9113 let buf = Buffer::from_str("hello world");
9114 let host = DefaultHost::new();
9115 let opts = Options {
9116 filetype: "rust".to_string(),
9117 ..Options::default()
9118 };
9119 let mut ed = Editor::new(buf, host, opts);
9120 ed.settings_mut().commentstring = "# %s".to_string();
9122 ed.toggle_comment_range(0, 0);
9123 assert_eq!(line(&ed, 0), "# hello world");
9124 }
9125
9126 #[test]
9129 fn unknown_lang_no_op() {
9130 let buf = Buffer::from_str("hello");
9131 let host = DefaultHost::new();
9132 let opts = Options::default(); let mut ed = Editor::new(buf, host, opts);
9134 ed.toggle_comment_range(0, 0);
9135 assert_eq!(line(&ed, 0), "hello");
9137 }
9138}
9139
9140#[cfg(test)]
9143mod g_ampersand_tests {
9144 use super::*;
9145 use crate::{DefaultHost, Editor, Options};
9146 use hjkl_buffer::{Buffer, rope_line_str};
9147
9148 fn make_editor(content: &str) -> Editor<Buffer, DefaultHost> {
9149 let buf = Buffer::from_str(content);
9150 let host = DefaultHost::new();
9151 Editor::new(buf, host, Options::default())
9152 }
9153
9154 fn buf_line(ed: &Editor<Buffer, DefaultHost>, row: usize) -> String {
9155 let rope = ed.buffer().rope();
9156 rope_line_str(&rope, row).trim_end_matches('\n').to_string()
9157 }
9158
9159 #[test]
9162 fn g_ampersand_repeats_last_substitute_on_whole_buffer() {
9163 let mut ed = make_editor("foo\nfoo bar foo\nbaz");
9164 let cmd = crate::substitute::parse_substitute("/foo/bar/").unwrap();
9166 ed.set_last_substitute(cmd);
9167 apply_after_g(&mut ed, '&', 1);
9169 assert_eq!(buf_line(&ed, 0), "bar");
9170 assert_eq!(buf_line(&ed, 1), "bar bar foo");
9172 assert_eq!(buf_line(&ed, 2), "baz");
9173 }
9174
9175 #[test]
9177 fn g_ampersand_with_g_flag_replaces_all_per_line() {
9178 let mut ed = make_editor("foo foo\nfoo");
9179 let cmd = crate::substitute::parse_substitute("/foo/bar/g").unwrap();
9180 ed.set_last_substitute(cmd);
9181 apply_after_g(&mut ed, '&', 1);
9182 assert_eq!(buf_line(&ed, 0), "bar bar");
9183 assert_eq!(buf_line(&ed, 1), "bar");
9184 }
9185
9186 #[test]
9188 fn g_ampersand_noop_when_no_prior_substitute() {
9189 let mut ed = make_editor("foo\nbar");
9190 apply_after_g(&mut ed, '&', 1);
9192 assert_eq!(buf_line(&ed, 0), "foo");
9193 assert_eq!(buf_line(&ed, 1), "bar");
9194 }
9195}
9196
9197#[cfg(test)]
9200mod sneak_tests {
9201 use super::*;
9202 use crate::{DefaultHost, Editor, Options};
9203 use hjkl_buffer::Buffer;
9204
9205 fn make_editor(content: &str) -> Editor<Buffer, DefaultHost> {
9206 let buf = Buffer::from_str(content);
9207 let host = DefaultHost::new();
9208 Editor::new(buf, host, Options::default())
9209 }
9210
9211 #[test]
9213 fn sneak_forward_jumps_to_two_char_digraph() {
9214 let mut ed = make_editor("foo bar baz qux\n");
9215 ed.jump_cursor(0, 0);
9216 ed.sneak('b', 'a', true, 1);
9217 assert_eq!(ed.cursor(), (0, 4), "cursor should land on 'ba' in 'bar'");
9218 }
9219
9220 #[test]
9222 fn sneak_backward_jumps_to_prior_match() {
9223 let mut ed = make_editor("foo bar baz qux\n");
9224 ed.jump_cursor(0, 12);
9225 ed.sneak('b', 'a', false, 1);
9226 assert_eq!(
9227 ed.cursor(),
9228 (0, 8),
9229 "backward sneak should find 'ba' in 'baz'"
9230 );
9231 }
9232
9233 #[test]
9235 fn sneak_repeat_semicolon_next_match() {
9236 let mut ed = make_editor("foo bar baz qux\n");
9237 ed.jump_cursor(0, 0);
9238 ed.sneak('b', 'a', true, 1);
9240 assert_eq!(ed.cursor(), (0, 4));
9241 execute_motion(&mut ed, Motion::FindRepeat { reverse: false }, 1);
9243 assert_eq!(ed.cursor(), (0, 8), "semicolon should jump to next 'ba'");
9244 }
9245
9246 #[test]
9248 fn sneak_repeat_comma_prev_match() {
9249 let mut ed = make_editor("foo bar baz qux\n");
9250 ed.jump_cursor(0, 0);
9251 ed.sneak('b', 'a', true, 1);
9252 assert_eq!(ed.cursor(), (0, 4));
9253 let pre = ed.cursor();
9255 execute_motion(&mut ed, Motion::FindRepeat { reverse: true }, 1);
9256 assert_eq!(
9257 ed.cursor(),
9258 pre,
9259 "comma with no prior match should leave cursor unchanged"
9260 );
9261 }
9262
9263 #[test]
9265 fn sneak_s_searches_backward() {
9266 let mut ed = make_editor("foo bar baz qux\n");
9267 ed.jump_cursor(0, 12);
9268 ed.sneak('b', 'a', false, 1);
9269 assert_eq!(ed.cursor(), (0, 8));
9270 }
9271
9272 #[test]
9274 fn sneak_with_count_jumps_to_nth() {
9275 let mut ed = make_editor("foo bar baz qux\n");
9276 ed.jump_cursor(0, 0);
9277 ed.sneak('b', 'a', true, 2);
9278 assert_eq!(ed.cursor(), (0, 8), "count=2 should jump to 2nd 'ba'");
9279 }
9280
9281 #[test]
9283 fn sneak_no_match_cursor_stays() {
9284 let mut ed = make_editor("foo bar baz qux\n");
9285 ed.jump_cursor(0, 0);
9286 let pre = ed.cursor();
9287 ed.sneak('x', 'x', true, 1);
9288 assert_eq!(ed.cursor(), pre, "no match should leave cursor unchanged");
9289 }
9290
9291 #[test]
9293 fn operator_pending_dsab_deletes_to_digraph() {
9294 let mut ed = make_editor("hello ab world\n");
9295 ed.jump_cursor(0, 0);
9296 ed.apply_op_sneak(Operator::Delete, 'a', 'b', true, 1);
9297 let content = ed.content();
9299 assert!(
9300 content.starts_with("ab world"),
9301 "dsab should delete 'hello ' leaving 'ab world'; got: {content:?}"
9302 );
9303 }
9304
9305 #[test]
9307 fn sneak_cross_line_match() {
9308 let mut ed = make_editor("foo\nbar baz\n");
9309 ed.jump_cursor(0, 0);
9310 ed.sneak('b', 'a', true, 1);
9311 assert_eq!(ed.cursor(), (1, 0), "sneak should cross line boundary");
9312 }
9313
9314 #[test]
9316 fn sneak_updates_last_sneak_state() {
9317 let mut ed = make_editor("foo bar baz\n");
9318 ed.jump_cursor(0, 0);
9319 ed.sneak('b', 'a', true, 1);
9320 let ls = ed.last_sneak();
9321 assert_eq!(
9322 ls,
9323 Some((('b', 'a'), true)),
9324 "last_sneak should record the digraph and direction"
9325 );
9326 }
9327}
9328
9329#[cfg(test)]
9338mod indent_count_tests {
9339 use super::*;
9340 use crate::{DefaultHost, Editor, Options};
9341 use hjkl_buffer::Buffer;
9342
9343 fn make_editor(content: &str) -> Editor<Buffer, DefaultHost> {
9344 let buf = Buffer::from_str(content);
9345 let mut ed = Editor::new(buf, DefaultHost::new(), Options::default());
9346 ed.settings_mut().expandtab = true;
9347 ed.settings_mut().shiftwidth = 4;
9348 ed
9349 }
9350
9351 fn content(ed: &Editor<Buffer, DefaultHost>) -> String {
9352 (*ed.buffer().content_joined()).clone()
9353 }
9354
9355 #[test]
9356 fn count_indent_operates_on_n_lines() {
9357 let mut ed = make_editor("a\nb\nc\nd\ne\nf\n");
9358 ed.jump_cursor(0, 0);
9359 execute_line_op(&mut ed, Operator::Indent, 3);
9360 assert_eq!(content(&ed), " a\n b\n c\nd\ne\nf\n");
9361 }
9362
9363 #[test]
9365 fn indent_noexpandtab_inserts_tab() {
9366 let mut ed = make_editor("hello\n");
9367 ed.settings_mut().expandtab = false;
9368 ed.settings_mut().shiftwidth = 4;
9369 ed.settings_mut().tabstop = 4;
9370 ed.jump_cursor(0, 0);
9371 execute_line_op(&mut ed, Operator::Indent, 1);
9372 assert_eq!(content(&ed), "\thello\n");
9373 }
9374
9375 #[test]
9378 fn indent_noexpandtab_subtab_remainder_is_spaces() {
9379 let mut ed = make_editor("hello\n");
9380 ed.settings_mut().expandtab = false;
9381 ed.settings_mut().shiftwidth = 2;
9382 ed.settings_mut().tabstop = 4;
9383 ed.jump_cursor(0, 0);
9384 execute_line_op(&mut ed, Operator::Indent, 1);
9385 assert_eq!(content(&ed), " hello\n");
9386 }
9387
9388 #[test]
9389 fn count_indent_clamps_to_buffer_end() {
9390 let mut ed = make_editor("a\nb\nc\nd\ne\nf\n");
9391 ed.jump_cursor(0, 0);
9392 execute_line_op(&mut ed, Operator::Indent, 10);
9393 assert_eq!(content(&ed), " a\n b\n c\n d\n e\n f\n");
9394 }
9395
9396 #[test]
9397 fn count_outdent_clamps_to_buffer_end() {
9398 let mut ed = make_editor(" a\n b\n c\n");
9399 ed.jump_cursor(0, 0);
9400 execute_line_op(&mut ed, Operator::Outdent, 10);
9401 assert_eq!(content(&ed), "a\nb\nc\n");
9402 }
9403
9404 #[test]
9405 fn count_indent_on_last_line_is_noop() {
9406 let mut ed = make_editor("a\nb\nc\n");
9407 ed.jump_cursor(2, 0); execute_line_op(&mut ed, Operator::Indent, 5);
9409 assert_eq!(
9410 content(&ed),
9411 "a\nb\nc\n",
9412 "5>> on last line must abort (E16)"
9413 );
9414 }
9415
9416 #[test]
9417 fn count_indent_on_single_line_is_noop() {
9418 let mut ed = make_editor("x\n");
9419 ed.jump_cursor(0, 0);
9420 execute_line_op(&mut ed, Operator::Indent, 5);
9421 assert_eq!(content(&ed), "x\n", "5>> on the only line must abort (E16)");
9422 }
9423
9424 #[test]
9425 fn count_outdent_on_last_line_is_noop() {
9426 let mut ed = make_editor(" a\n b\n c\n");
9427 ed.jump_cursor(2, 0);
9428 execute_line_op(&mut ed, Operator::Outdent, 5);
9429 assert_eq!(content(&ed), " a\n b\n c\n");
9430 }
9431
9432 #[test]
9433 fn single_indent_on_last_line_still_works() {
9434 let mut ed = make_editor("a\nb\nc\n");
9436 ed.jump_cursor(2, 0);
9437 execute_line_op(&mut ed, Operator::Indent, 1);
9438 assert_eq!(content(&ed), "a\nb\n c\n");
9439 }
9440}
9441
9442#[cfg(test)]
9445mod abbrev_tests {
9446 use super::{Abbrev, AbbrevKind, AbbrevTrigger, abbrev_kind, try_abbrev_expand};
9447 use AbbrevKind::{End, Full, NonKw};
9448
9449 const ISK: &str = "@,48-57,_,192-255"; fn make_abbrev(lhs: &str, rhs: &str) -> Abbrev {
9452 Abbrev {
9453 lhs: lhs.to_string(),
9454 rhs: rhs.to_string(),
9455 insert: true,
9456 cmdline: false,
9457 noremap: false,
9458 }
9459 }
9460
9461 fn expand(
9462 abbrevs: &[Abbrev],
9463 before: &str,
9464 mincol: usize,
9465 trig: AbbrevTrigger,
9466 ) -> Option<(usize, String)> {
9467 try_abbrev_expand(abbrevs, before, mincol, trig, ISK)
9468 }
9469
9470 #[test]
9473 fn fullid_all_keyword_chars() {
9474 assert_eq!(abbrev_kind("teh", ISK), Full);
9475 assert_eq!(abbrev_kind("abc123", ISK), Full);
9476 assert_eq!(abbrev_kind("_foo", ISK), Full);
9477 }
9478
9479 #[test]
9480 fn endid_ends_with_kw_has_nonkw() {
9481 assert_eq!(abbrev_kind("#i", ISK), End);
9482 assert_eq!(abbrev_kind("#include", ISK), End);
9483 }
9484
9485 #[test]
9486 fn nonid_ends_with_nonkw() {
9487 assert_eq!(abbrev_kind(";;", ISK), NonKw);
9488 assert_eq!(abbrev_kind("->", ISK), NonKw);
9489 }
9490
9491 #[test]
9494 fn fullid_expands_on_space_trigger() {
9495 let abbrevs = [make_abbrev("teh", "the")];
9496 let r = expand(&abbrevs, "teh", 0, AbbrevTrigger::NonKeyword(' '));
9497 assert_eq!(r, Some((3, "the".to_string())));
9498 }
9499
9500 #[test]
9501 fn fullid_expands_on_esc_trigger() {
9502 let abbrevs = [make_abbrev("teh", "the")];
9503 let r = expand(&abbrevs, "teh", 0, AbbrevTrigger::Esc);
9504 assert_eq!(r, Some((3, "the".to_string())));
9505 }
9506
9507 #[test]
9508 fn fullid_expands_on_cr_trigger() {
9509 let abbrevs = [make_abbrev("teh", "the")];
9510 let r = expand(&abbrevs, "teh", 0, AbbrevTrigger::Cr);
9511 assert_eq!(r, Some((3, "the".to_string())));
9512 }
9513
9514 #[test]
9515 fn fullid_expands_on_ctrl_bracket() {
9516 let abbrevs = [make_abbrev("teh", "the")];
9517 let r = expand(&abbrevs, "teh", 0, AbbrevTrigger::CtrlBracket);
9518 assert_eq!(r, Some((3, "the".to_string())));
9519 }
9520
9521 #[test]
9522 fn fullid_does_not_expand_on_keyword_trigger() {
9523 let abbrevs = [make_abbrev("teh", "the")];
9525 let r = expand(&abbrevs, "teh", 0, AbbrevTrigger::NonKeyword('a'));
9526 assert_eq!(r, None);
9528 }
9529
9530 #[test]
9531 fn fullid_no_expand_when_lhs_not_at_end() {
9532 let abbrevs = [make_abbrev("teh", "the")];
9533 let r = expand(&abbrevs, "ateh", 0, AbbrevTrigger::NonKeyword(' '));
9535 assert_eq!(r, None);
9536 }
9537
9538 #[test]
9539 fn fullid_expands_after_nonkw_prefix() {
9540 let abbrevs = [make_abbrev("teh", "the")];
9541 let r = expand(&abbrevs, "!teh", 0, AbbrevTrigger::NonKeyword(' '));
9543 assert_eq!(r, Some((3, "the".to_string())));
9544 }
9545
9546 #[test]
9547 fn fullid_single_char_no_expand_after_nonblank_nonkw() {
9548 let abbrevs = [make_abbrev("a", "b")];
9549 let r = expand(&abbrevs, "!a", 0, AbbrevTrigger::NonKeyword(' '));
9551 assert_eq!(r, None);
9552 }
9553
9554 #[test]
9555 fn fullid_single_char_expands_after_space() {
9556 let abbrevs = [make_abbrev("a", "b")];
9557 let r = expand(&abbrevs, " a", 0, AbbrevTrigger::NonKeyword(' '));
9559 assert_eq!(r, Some((1, "b".to_string())));
9560 }
9561
9562 #[test]
9565 fn mincol_blocks_consuming_preexisting_text() {
9566 let abbrevs = [make_abbrev("teh", "the")];
9567 let r = expand(&abbrevs, "teh", 3, AbbrevTrigger::NonKeyword(' '));
9569 assert_eq!(r, None);
9570 }
9571
9572 #[test]
9573 fn mincol_allows_match_starting_at_mincol() {
9574 let abbrevs = [make_abbrev("teh", "the")];
9575 let r = expand(&abbrevs, "!! teh", 3, AbbrevTrigger::NonKeyword(' '));
9578 assert_eq!(r, Some((3, "the".to_string())));
9579 }
9580
9581 #[test]
9584 fn endid_expands_on_space_trigger() {
9585 let abbrevs = [make_abbrev("#i", "#include")];
9586 let r = expand(&abbrevs, "#i", 0, AbbrevTrigger::NonKeyword(' '));
9587 assert_eq!(r, Some((2, "#include".to_string())));
9588 }
9589
9590 #[test]
9591 fn endid_expands_on_esc_trigger() {
9592 let abbrevs = [make_abbrev("#i", "#include")];
9593 let r = expand(&abbrevs, "#i", 0, AbbrevTrigger::Esc);
9594 assert_eq!(r, Some((2, "#include".to_string())));
9595 }
9596
9597 #[test]
9600 fn nonid_expands_on_esc_trigger() {
9601 let abbrevs = [make_abbrev(";;", "std::endl;")];
9602 let r = expand(&abbrevs, ";;", 0, AbbrevTrigger::Esc);
9603 assert_eq!(r, Some((2, "std::endl;".to_string())));
9604 }
9605
9606 #[test]
9607 fn nonid_expands_on_cr_trigger() {
9608 let abbrevs = [make_abbrev(";;", "std::endl;")];
9609 let r = expand(&abbrevs, ";;", 0, AbbrevTrigger::Cr);
9610 assert_eq!(r, Some((2, "std::endl;".to_string())));
9611 }
9612
9613 #[test]
9614 fn nonid_does_not_expand_on_nonkw_trigger() {
9615 let abbrevs = [make_abbrev(";;", "std::endl;")];
9617 let r = expand(&abbrevs, ";;", 0, AbbrevTrigger::NonKeyword(' '));
9618 assert_eq!(r, None);
9619 }
9620
9621 #[test]
9622 fn nonid_expands_on_ctrl_bracket() {
9623 let abbrevs = [make_abbrev(";;", "std::endl;")];
9624 let r = expand(&abbrevs, ";;", 0, AbbrevTrigger::CtrlBracket);
9625 assert_eq!(r, Some((2, "std::endl;".to_string())));
9626 }
9627
9628 #[test]
9631 fn multiword_rhs_expansion() {
9632 let abbrevs = [make_abbrev("hw", "hello world")];
9633 let r = expand(&abbrevs, "hw", 0, AbbrevTrigger::NonKeyword(' '));
9634 assert_eq!(r, Some((2, "hello world".to_string())));
9635 }
9636
9637 #[test]
9640 fn no_match_returns_none() {
9641 let abbrevs = [make_abbrev("teh", "the")];
9642 let r = expand(&abbrevs, "xyz", 0, AbbrevTrigger::NonKeyword(' '));
9643 assert_eq!(r, None);
9644 }
9645
9646 #[test]
9647 fn empty_abbrevs_returns_none() {
9648 let r = expand(&[], "teh", 0, AbbrevTrigger::NonKeyword(' '));
9649 assert_eq!(r, None);
9650 }
9651
9652 #[test]
9653 fn empty_before_text_returns_none() {
9654 let abbrevs = [make_abbrev("teh", "the")];
9655 let r = expand(&abbrevs, "", 0, AbbrevTrigger::NonKeyword(' '));
9656 assert_eq!(r, None);
9657 }
9658}
9659
9660#[cfg(test)]
9663mod scan_tag_opener_multibyte_tests {
9664 use crate::types::Options;
9665 use crate::{DefaultHost, Editor};
9666 use hjkl_buffer::Buffer;
9667
9668 fn html_editor(content: &str) -> Editor<Buffer, DefaultHost> {
9669 let buf = Buffer::from_str(content);
9670 let host = DefaultHost::new();
9671 let mut ed = Editor::new(buf, host, Options::default());
9672 ed.settings.filetype = "html".to_string();
9673 ed.settings.autoclose_tag = true;
9674 ed.settings.autopair = true;
9675 ed
9676 }
9677
9678 #[test]
9685 fn autoclose_gt_after_multibyte_no_panic() {
9686 let mut ed = html_editor("ñ");
9687 ed.enter_insert_i(1);
9689 ed.jump_cursor(0, 1);
9691 ed.insert_char('>');
9693 let rope = ed.buffer().rope();
9695 let line = hjkl_buffer::rope_line_str(&rope, 0);
9696 assert!(line.contains('>'), "inserted > must appear in buffer");
9697 }
9698
9699 #[test]
9716 fn autoclose_gt_direct_after_multibyte_no_panic() {
9717 let mut ed = html_editor("ñ");
9720 ed.enter_insert_i(1);
9721 ed.jump_cursor(0, 1); ed.insert_char('>');
9724 let rope = ed.buffer().rope();
9725 let line = hjkl_buffer::rope_line_str(&rope, 0);
9726 assert!(
9727 line.contains('>'),
9728 "inserted > must appear in buffer, got: {line:?}"
9729 );
9730 }
9731}