1use hjkl_vim_types::{LastChange, Mode, RangeKind};
6
7use hjkl_engine::rope_util::{rope_line_to_str, rope_row_range_str};
8
9use super::*;
10use crate::vim_state::{vim, vim_mut};
11use hjkl_engine::Editor;
12use hjkl_engine::buf_helpers::{
13 buf_cursor_pos, buf_line, buf_line_chars, buf_row_count, buf_set_cursor_pos, buf_set_cursor_rc,
14};
15
16pub fn read_vim_range<H: hjkl_engine::types::Host>(
20 ed: &mut Editor<hjkl_buffer::View, H>,
21 start: (usize, usize),
22 end: (usize, usize),
23 kind: RangeKind,
24) -> String {
25 let (top, bot) = order(start, end);
26 ed.sync_buffer_content_from_textarea();
27 let rope = hjkl_engine::types::Query::rope(ed.buffer());
28 let n_lines = rope.len_lines();
29 match kind {
30 RangeKind::Linewise => {
31 let lo = top.0;
32 let hi = bot.0.min(n_lines.saturating_sub(1));
33 let mut text = rope_row_range_str(&rope, lo, hi);
34 text.push('\n');
35 text
36 }
37 RangeKind::Inclusive | RangeKind::Exclusive => {
38 let inclusive = matches!(kind, RangeKind::Inclusive);
39 let mut out = String::new();
41 for row in top.0..=bot.0 {
42 if row >= n_lines {
43 break;
44 }
45 let line = rope_line_to_str(&rope, row);
46 let lo = if row == top.0 { top.1 } else { 0 };
47 let hi_unclamped = if row == bot.0 {
48 if inclusive { bot.1 + 1 } else { bot.1 }
49 } else {
50 line.chars().count()
51 };
52 let row_chars: Vec<char> = line.chars().collect();
53 let hi = hi_unclamped.min(row_chars.len());
54 if lo < hi {
55 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
56 }
57 if row < bot.0 {
58 out.push('\n');
59 }
60 }
61 out
62 }
63 }
64}
65pub fn cut_vim_range<H: hjkl_engine::types::Host>(
74 ed: &mut Editor<hjkl_buffer::View, H>,
75 start: (usize, usize),
76 end: (usize, usize),
77 kind: RangeKind,
78) -> String {
79 cut_vim_range_inner(ed, start, end, kind, true)
80}
81pub(super) fn cut_vim_range_inner<H: hjkl_engine::types::Host>(
90 ed: &mut Editor<hjkl_buffer::View, H>,
91 start: (usize, usize),
92 end: (usize, usize),
93 kind: RangeKind,
94 record: bool,
95) -> String {
96 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
97 let (top, bot) = order(start, end);
98 ed.sync_buffer_content_from_textarea();
99 let (buf_start, buf_end, buf_kind) = match kind {
100 RangeKind::Linewise => (
101 Position::new(top.0, 0),
102 Position::new(bot.0, 0),
103 BufKind::Line,
104 ),
105 RangeKind::Inclusive => {
106 let line_chars = buf_line_chars(ed.buffer(), bot.0);
107 let next = Position::new(bot.0, (bot.1 + 1).min(line_chars));
115 (Position::new(top.0, top.1), next, BufKind::Char)
116 }
117 RangeKind::Exclusive => (
118 Position::new(top.0, top.1),
119 Position::new(bot.0, bot.1),
120 BufKind::Char,
121 ),
122 };
123 let inverse = ed.mutate_edit(Edit::DeleteRange {
124 start: buf_start,
125 end: buf_end,
126 kind: buf_kind,
127 });
128 let raw_text = match inverse {
129 Edit::InsertStr { text, .. } => text,
130 _ => String::new(),
131 };
132 let text = if matches!(kind, RangeKind::Linewise) {
137 if raw_text.ends_with('\n') {
138 raw_text
140 } else if top.0 > 0
141 && let Some(rows) = raw_text.strip_prefix('\n')
142 {
143 format!("{rows}\n")
148 } else if raw_text.is_empty() {
149 return String::new();
152 } else {
153 format!("{raw_text}\n")
155 }
156 } else {
157 raw_text
158 };
159 if text.is_empty() {
160 return text;
162 }
163 if record {
164 ed.record_yank_to_host(text.clone());
165 let target = vim_mut(ed).pending_register.take();
166 ed.record_delete(text.clone(), matches!(kind, RangeKind::Linewise), target);
167 }
168 text
169}
170fn linewise_payload(text: &str) -> &str {
175 text.strip_suffix('\n').unwrap_or(text)
176}
177pub fn delete_to_eol<H: hjkl_engine::types::Host>(ed: &mut Editor<hjkl_buffer::View, H>) {
183 use hjkl_buffer::{Edit, MotionKind, Position};
184 ed.sync_buffer_content_from_textarea();
185 let cursor = buf_cursor_pos(ed.buffer());
186 let line_chars = buf_line_chars(ed.buffer(), cursor.row);
187 if cursor.col >= line_chars {
188 return;
189 }
190 let inverse = ed.mutate_edit(Edit::DeleteRange {
191 start: cursor,
192 end: Position::new(cursor.row, line_chars),
193 kind: MotionKind::Char,
194 });
195 if let Edit::InsertStr { text, .. } = inverse
196 && !text.is_empty()
197 {
198 ed.record_yank_to_host(text.clone());
199 ed.set_yank_linewise(false);
200 let target = vim_mut(ed).pending_register.take();
204 ed.record_delete(text, false, target);
205 }
206 buf_set_cursor_pos(ed.buffer_mut(), cursor);
207}
208pub fn do_char_delete<H: hjkl_engine::types::Host>(
209 ed: &mut Editor<hjkl_buffer::View, H>,
210 forward: bool,
211 count: usize,
212) {
213 use hjkl_buffer::{Edit, MotionKind, Position};
214 if forward {
221 let cursor_row = ed.cursor().0;
222 let (fold_start, fold_end) =
223 expand_linewise_over_closed_folds(ed.buffer(), cursor_row, cursor_row);
224 if (fold_start, fold_end) != (cursor_row, cursor_row) {
225 run_operator_over_range(
226 ed,
227 Operator::Delete,
228 (fold_start, 0),
229 (fold_end, 0),
230 RangeKind::Linewise,
231 );
232 return;
233 }
234 }
235 ed.push_undo();
236 ed.sync_buffer_content_from_textarea();
237 let mut deleted = String::new();
240 for _ in 0..count {
241 let cursor = buf_cursor_pos(ed.buffer());
242 let line_chars = buf_line_chars(ed.buffer(), cursor.row);
243 if forward {
244 if cursor.col >= line_chars {
250 break;
251 }
252 let inverse = ed.mutate_edit(Edit::DeleteRange {
253 start: cursor,
254 end: Position::new(cursor.row, cursor.col + 1),
255 kind: MotionKind::Char,
256 });
257 if let Edit::InsertStr { text, .. } = inverse {
258 deleted.push_str(&text);
259 }
260 } else {
261 if cursor.col == 0 {
264 break;
265 }
266 let inverse = ed.mutate_edit(Edit::DeleteRange {
267 start: Position::new(cursor.row, cursor.col - 1),
268 end: cursor,
269 kind: MotionKind::Char,
270 });
271 if let Edit::InsertStr { text, .. } = inverse {
272 deleted = text + &deleted;
275 }
276 }
277 }
278 if !deleted.is_empty() {
279 ed.record_yank_to_host(deleted.clone());
280 let target = vim_mut(ed).pending_register.take();
281 ed.record_delete(deleted, false, target);
282 }
283 let cursor = buf_cursor_pos(ed.buffer());
286 let line_chars = buf_line_chars(ed.buffer(), cursor.row);
287 if line_chars > 0 && cursor.col >= line_chars {
288 buf_set_cursor_pos(ed.buffer_mut(), Position::new(cursor.row, line_chars - 1));
289 }
290 remember_operator_column(ed);
294}
295struct AdjustedNumber {
300 start: usize,
301 end: usize,
302 text: String,
303}
304
305fn adjusted_number_at(chars: &[char], from: usize, delta: i64) -> Option<AdjustedNumber> {
316 let len = chars.len();
317 let is_hex_prefix = |i: usize| {
318 chars[i] == '0'
319 && matches!(chars.get(i + 1), Some('x' | 'X'))
320 && chars.get(i + 2).is_some_and(|c| c.is_ascii_hexdigit())
321 };
322 let start = (from.min(len)..len).find(|&i| is_hex_prefix(i) || chars[i].is_ascii_digit())?;
323
324 if is_hex_prefix(start) {
325 let digits_start = start + 2;
327 let mut digits_end = digits_start;
328 while digits_end < len && chars[digits_end].is_ascii_hexdigit() {
329 digits_end += 1;
330 }
331 let digits: String = chars[digits_start..digits_end].iter().collect();
332 let n = u64::from_str_radix(&digits, 16).ok()?;
333 let new_val = (n as i128 + delta as i128).max(0) as u64;
334 let width = digits_end - digits_start;
335 let prefix: String = chars[start..digits_start].iter().collect();
336 let upper = digits
341 .chars()
342 .rev()
343 .find(|c| c.is_ascii_alphabetic())
344 .map_or(chars[start + 1] == 'X', |c| c.is_ascii_uppercase());
345 let text = if upper {
346 format!("{prefix}{new_val:0width$X}")
347 } else {
348 format!("{prefix}{new_val:0width$x}")
349 };
350 return Some(AdjustedNumber {
351 start,
352 end: digits_end,
353 text,
354 });
355 }
356
357 let span_start = if start > 0 && chars[start - 1] == '-' {
360 start - 1
361 } else {
362 start
363 };
364 let mut span_end = start;
365 while span_end < len && chars[span_end].is_ascii_digit() {
366 span_end += 1;
367 }
368 let s: String = chars[span_start..span_end].iter().collect();
369 let n = s.parse::<i64>().ok()?;
370 let new_val = n as i128 + delta as i128;
371 let digits: String = chars[start..span_end].iter().collect();
377 let width = digits.len();
378 let text = if width > 1 && digits.starts_with('0') {
379 if new_val < 0 {
380 let mag = new_val.unsigned_abs();
381 format!("-{mag:0width$}")
382 } else {
383 format!("{new_val:0width$}")
384 }
385 } else {
386 new_val.to_string()
387 };
388 Some(AdjustedNumber {
389 start: span_start,
390 end: span_end,
391 text,
392 })
393}
394
395pub fn adjust_number<H: hjkl_engine::types::Host>(
401 ed: &mut Editor<hjkl_buffer::View, H>,
402 delta: i64,
403) -> bool {
404 use hjkl_buffer::{Edit, MotionKind, Position};
405 ed.sync_buffer_content_from_textarea();
406 let cursor = buf_cursor_pos(ed.buffer());
407 let row = cursor.row;
408 let chars: Vec<char> = match buf_line(ed.buffer(), row) {
409 Some(l) => l.chars().collect(),
410 None => return false,
411 };
412 let Some(AdjustedNumber {
413 start: span_start,
414 end: span_end,
415 text: new_s,
416 }) = adjusted_number_at(&chars, cursor.col, delta)
417 else {
418 return false;
419 };
420
421 ed.push_undo();
422 let span_start_pos = Position::new(row, span_start);
423 let span_end_pos = Position::new(row, span_end);
424 ed.mutate_edit(Edit::DeleteRange {
425 start: span_start_pos,
426 end: span_end_pos,
427 kind: MotionKind::Char,
428 });
429 ed.mutate_edit(Edit::InsertStr {
430 at: span_start_pos,
431 text: new_s.clone(),
432 });
433 let new_len = new_s.chars().count();
434 buf_set_cursor_rc(ed.buffer_mut(), row, span_start + new_len.saturating_sub(1));
435 true
436}
437pub fn replace_char<H: hjkl_engine::types::Host>(
438 ed: &mut Editor<hjkl_buffer::View, H>,
439 ch: char,
440 count: usize,
441) {
442 use hjkl_buffer::{Edit, MotionKind, Position};
443 ed.sync_buffer_content_from_textarea();
444 let start = buf_cursor_pos(ed.buffer());
448 let start_line_chars = buf_line_chars(ed.buffer(), start.row);
449 if count == 0 || start.col + count > start_line_chars {
450 return;
451 }
452 ed.push_undo();
453 for _ in 0..count {
454 let cursor = buf_cursor_pos(ed.buffer());
455 let line_chars = buf_line_chars(ed.buffer(), cursor.row);
456 if cursor.col >= line_chars {
457 break;
458 }
459 ed.mutate_edit(Edit::DeleteRange {
460 start: cursor,
461 end: Position::new(cursor.row, cursor.col + 1),
462 kind: MotionKind::Char,
463 });
464 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
465 }
466 hjkl_engine::motions::move_left(ed.buffer_mut(), 1);
468}
469pub fn toggle_case_at_cursor<H: hjkl_engine::types::Host>(
473 ed: &mut Editor<hjkl_buffer::View, H>,
474) -> bool {
475 use hjkl_buffer::{Edit, MotionKind, Position};
476 ed.sync_buffer_content_from_textarea();
477 let cursor = buf_cursor_pos(ed.buffer());
478 let Some(c) = buf_line(ed.buffer(), cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
479 return false;
480 };
481 let toggled = if c.is_uppercase() {
482 c.to_lowercase().collect::<String>()
483 } else {
484 c.to_uppercase().collect::<String>()
485 };
486 ed.mutate_edit(Edit::DeleteRange {
487 start: cursor,
488 end: Position::new(cursor.row, cursor.col + 1),
489 kind: MotionKind::Char,
490 });
491 ed.mutate_edit(Edit::InsertStr {
492 at: cursor,
493 text: toggled,
494 });
495 true
496}
497fn has_line_to_join<H: hjkl_engine::types::Host>(
501 ed: &Editor<hjkl_buffer::View, H>,
502 row: usize,
503) -> bool {
504 let n_rows = buf_row_count(ed.buffer());
505 row + 1 < n_rows
506 && !(row + 2 >= n_rows && buf_line(ed.buffer(), row + 1).is_some_and(|s| s.is_empty()))
507}
508pub fn reset_column_for_counted_join_on_last_line<H: hjkl_engine::types::Host>(
513 ed: &mut Editor<hjkl_buffer::View, H>,
514 count: usize,
515) {
516 let row = buf_cursor_pos(ed.buffer()).row;
517 if count >= 3 && !has_line_to_join(ed, row) {
518 buf_set_cursor_rc(ed.buffer_mut(), row, 0);
519 }
520}
521pub fn join_line<H: hjkl_engine::types::Host>(ed: &mut Editor<hjkl_buffer::View, H>) -> bool {
524 use hjkl_buffer::{Edit, Position};
525 ed.sync_buffer_content_from_textarea();
526 let row = buf_cursor_pos(ed.buffer()).row;
527 if !has_line_to_join(ed, row) {
528 return false;
529 }
530 let cur_line = buf_line(ed.buffer(), row).unwrap_or_default();
531 let next_raw = buf_line(ed.buffer(), row + 1).unwrap_or_default();
532 let next_trimmed = next_raw.trim_start();
533 let cur_chars = cur_line.chars().count();
534 let next_chars = next_raw.chars().count();
535 let separator = if !cur_line.is_empty()
538 && !next_trimmed.is_empty()
539 && !cur_line.ends_with([' ', '\t'])
540 && !next_trimmed.starts_with(')')
541 {
542 " "
543 } else {
544 ""
545 };
546 let joined = format!("{cur_line}{separator}{next_trimmed}");
547 let joined_chars = joined.chars().count();
548 ed.mutate_edit(Edit::Replace {
549 start: Position::new(row, 0),
550 end: Position::new(row + 1, next_chars),
551 with: joined,
552 });
553 buf_set_cursor_rc(
559 ed.buffer_mut(),
560 row,
561 cur_chars.min(joined_chars.saturating_sub(1)),
562 );
563 true
564}
565pub fn join_line_raw<H: hjkl_engine::types::Host>(ed: &mut Editor<hjkl_buffer::View, H>) -> bool {
569 use hjkl_buffer::Edit;
570 ed.sync_buffer_content_from_textarea();
571 let row = buf_cursor_pos(ed.buffer()).row;
572 if !has_line_to_join(ed, row) {
573 return false;
574 }
575 let join_col = buf_line_chars(ed.buffer(), row);
576 ed.mutate_edit(Edit::JoinLines {
577 row,
578 count: 1,
579 with_space: false,
580 });
581 let last_col = buf_line_chars(ed.buffer(), row).saturating_sub(1);
584 buf_set_cursor_rc(ed.buffer_mut(), row, join_col.min(last_col));
585 true
586}
587pub fn visual_join<H: hjkl_engine::types::Host>(
591 ed: &mut Editor<hjkl_buffer::View, H>,
592 with_space: bool,
593) {
594 let (cursor_row, cursor_col) = ed.cursor();
595 let (top, bot, start_col) = match vim(ed).mode {
598 Mode::VisualLine => (
599 cursor_row.min(vim(ed).visual_line_anchor),
600 cursor_row.max(vim(ed).visual_line_anchor),
601 0,
602 ),
603 Mode::VisualBlock => {
604 let a = vim(ed).block_anchor;
605 (
606 a.0.min(cursor_row),
607 a.0.max(cursor_row),
608 a.1.min(cursor_col),
609 )
610 }
611 Mode::Visual => {
612 let a = vim(ed).visual_anchor;
613 let start = a.min((cursor_row, cursor_col));
614 (a.0.min(cursor_row), a.0.max(cursor_row), start.1)
615 }
616 _ => return,
617 };
618 let joins = (bot - top).max(1);
621 ed.push_undo();
622 buf_set_cursor_rc(ed.buffer_mut(), top, 0);
623 let mut joined_any = false;
624 for _ in 0..joins {
625 let joined = if with_space {
626 join_line(ed)
627 } else {
628 join_line_raw(ed)
629 };
630 if !joined {
631 break;
632 }
633 joined_any = true;
634 }
635 if !joined_any {
636 buf_set_cursor_rc(ed.buffer_mut(), top, start_col);
639 }
640 if !vim(ed).replaying {
646 vim_mut(ed).last_change = Some(LastChange::JoinLine { count: joins });
647 }
648 vim_mut(ed).mode = Mode::Normal;
649 ed.set_sticky_col(Some(buf_cursor_pos(ed.buffer()).col));
650}
651pub fn goto_percent<H: hjkl_engine::types::Host>(
654 ed: &mut Editor<hjkl_buffer::View, H>,
655 count: usize,
656) {
657 let rows = buf_row_count(ed.buffer());
658 if rows == 0 {
659 return;
660 }
661 let total = if rows >= 2 && buf_line(ed.buffer(), rows - 1).is_some_and(|s| s.is_empty()) {
664 rows - 1
665 } else {
666 rows
667 };
668 let line = count.saturating_mul(total).div_ceil(100).clamp(1, total);
672 let pre = ed.cursor();
673 ed.jump_cursor(line - 1, 0);
674 move_first_non_whitespace(ed);
675 ed.set_sticky_col(Some(ed.cursor().1));
676 if ed.cursor() != pre {
677 ed.push_jump(pre);
678 }
679}
680pub fn indent_width(s: &str, tabstop: usize) -> usize {
683 let ts = tabstop.max(1);
684 let mut w = 0usize;
685 for c in s.chars() {
686 match c {
687 ' ' => w += 1,
688 '\t' => w += ts - (w % ts),
689 _ => break,
690 }
691 }
692 w
693}
694pub fn build_indent(width: usize, expandtab: bool, tabstop: usize) -> String {
697 if expandtab {
698 return " ".repeat(width);
699 }
700 let ts = tabstop.max(1);
701 let tabs = width / ts;
702 let spaces = width % ts;
703 format!("{}{}", "\t".repeat(tabs), " ".repeat(spaces))
704}
705pub fn reindent_block(text: &str, target_width: usize, settings: &hjkl_engine::Settings) -> String {
708 let ts = settings.tabstop.max(1);
709 let lines: Vec<&str> = text.split('\n').collect();
710 let first_width = lines.first().map_or(0, |l| indent_width(l, ts));
711 let delta = target_width as isize - first_width as isize;
712 lines
713 .iter()
714 .map(|line| {
715 let trimmed = line.trim_start_matches([' ', '\t']);
716 if trimmed.is_empty() {
717 return String::new();
719 }
720 let old_w = indent_width(line, ts) as isize;
721 let new_w = (old_w + delta).max(0) as usize;
722 format!(
723 "{}{}",
724 build_indent(new_w, settings.expandtab, settings.tabstop),
725 trimmed
726 )
727 })
728 .collect::<Vec<_>>()
729 .join("\n")
730}
731pub const MAX_PASTE_BYTES: usize = 64 * 1024 * 1024;
748
749fn reject_oversized_paste<H: hjkl_engine::types::Host>(
752 ed: &mut Editor<hjkl_buffer::View, H>,
753 bytes: usize,
754) {
755 ed.push_error(format!("E342: Out of memory! (allocating {bytes} bytes)"));
756}
757
758pub fn do_paste<H: hjkl_engine::types::Host>(
759 ed: &mut Editor<hjkl_buffer::View, H>,
760 before: bool,
761 count: usize,
762 cursor_after: bool,
763 reindent: bool,
764) -> bool {
765 use hjkl_buffer::{Edit, Position};
766 let selector = vim_mut(ed).pending_register.take();
771 sync_clipboard_register_for(ed, selector);
775 let (yank, linewise, blockwise, block_width) =
776 ed.with_registers(|regs| match selector.and_then(|c| regs.read(c)) {
777 Some(slot) => (
778 slot.text.clone(),
779 slot.linewise,
780 slot.blockwise,
781 slot.block_width,
782 ),
783 None => (
789 regs.unnamed.text.clone(),
790 regs.unnamed.linewise,
791 regs.unnamed.blockwise,
792 regs.unnamed.block_width,
793 ),
794 });
795 let original_row_for_linewise_after = if linewise && !before {
804 let r = buf_cursor_pos(ed.buffer()).row;
807 let (_, fold_end) = expand_linewise_over_closed_folds(ed.buffer(), r, r);
808 Some(fold_end)
809 } else {
810 None
811 };
812 if yank.is_empty() {
815 return false;
816 }
817 let Some(requested_bytes) = yank.len().checked_mul(count) else {
825 reject_oversized_paste(ed, yank.len().saturating_mul(count));
826 return false;
827 };
828 if requested_bytes > MAX_PASTE_BYTES {
829 reject_oversized_paste(ed, requested_bytes);
830 return false;
831 }
832 if blockwise {
833 let Some(block_bytes) = block_width
834 .checked_mul(yank.split('\n').count())
835 .and_then(|bytes| bytes.checked_mul(count))
836 else {
837 reject_oversized_paste(ed, usize::MAX);
838 return false;
839 };
840 let Some(total_bytes) = requested_bytes.checked_add(block_bytes) else {
841 reject_oversized_paste(ed, usize::MAX);
842 return false;
843 };
844 if total_bytes > MAX_PASTE_BYTES {
845 reject_oversized_paste(ed, total_bytes);
846 return false;
847 }
848 }
849 ed.push_undo();
850 if blockwise {
854 do_block_paste(ed, before, count, block_width, &yank);
855 return true;
856 }
857 let paste_mark = if linewise {
861 ed.sync_buffer_content_from_textarea();
862 let row = buf_cursor_pos(ed.buffer()).row;
863 let mut text = linewise_payload(&yank).to_string();
864 if reindent {
865 let cur_line = buf_line(ed.buffer(), row).unwrap_or_default();
866 let target_w = indent_width(&cur_line, ed.settings().tabstop.max(1));
867 text = reindent_block(&text, target_w, ed.settings());
868 }
869 let (fold_start, fold_end) = expand_linewise_over_closed_folds(ed.buffer(), row, row);
870 let payload = std::iter::repeat_n(text.as_str(), count)
871 .collect::<Vec<_>>()
872 .join("\n");
873 let target_row = if before {
874 ed.mutate_edit(Edit::InsertStr {
875 at: Position::new(fold_start, 0),
876 text: format!("{payload}\n"),
877 });
878 fold_start
879 } else {
880 let line_chars = buf_line_chars(ed.buffer(), fold_end);
881 ed.mutate_edit(Edit::InsertStr {
882 at: Position::new(fold_end, line_chars),
883 text: format!("\n{payload}"),
884 });
885 fold_end + 1
886 };
887 buf_set_cursor_rc(ed.buffer_mut(), target_row, 0);
888 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
889 let payload_lines = payload.split('\n').count().max(1);
892 let bot_row = target_row + payload_lines - 1;
893 let bot_last_col = buf_line_chars(ed.buffer(), bot_row).saturating_sub(1);
894 ((target_row, 0), (bot_row, bot_last_col))
895 } else {
896 ed.sync_buffer_content_from_textarea();
897 let cursor = buf_cursor_pos(ed.buffer());
901 let at = if before {
902 cursor
903 } else {
904 let line_chars = buf_line_chars(ed.buffer(), cursor.row);
905 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
906 };
907 let repeated = yank.repeat(count);
908 ed.mutate_edit(Edit::InsertStr { at, text: repeated });
909 if !cursor_after && ed.cursor().1 > 0 {
914 hjkl_engine::motions::move_left(ed.buffer_mut(), 1);
915 }
916 let lo = (at.row, at.col);
918 let hi = if cursor_after {
919 let c = ed.cursor();
920 (c.0, c.1.saturating_sub(1))
921 } else {
922 ed.cursor()
923 };
924 (lo, hi)
925 };
926 ed.set_mark('[', paste_mark.0);
927 ed.set_mark(']', paste_mark.1);
928 if cursor_after && linewise {
931 let bot_row = paste_mark.1.0;
932 let last_row = buf_row_count(ed.buffer()).saturating_sub(1);
933 let target = (bot_row + 1).min(last_row);
934 buf_set_cursor_rc(ed.buffer_mut(), target, 0);
935 } else if let Some(orig_row) = original_row_for_linewise_after {
936 let first_target = orig_row.saturating_add(1);
941 buf_set_cursor_rc(ed.buffer_mut(), first_target, 0);
942 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
943 }
944 ed.set_sticky_col(Some(buf_cursor_pos(ed.buffer()).col));
946 true
947}
948fn do_block_paste<H: hjkl_engine::types::Host>(
966 ed: &mut Editor<hjkl_buffer::View, H>,
967 before: bool,
968 count: usize,
969 width: usize,
970 yank: &str,
971) {
972 use hjkl_buffer::{Edit, Position};
973 ed.sync_buffer_content_from_textarea();
974 let cursor = buf_cursor_pos(ed.buffer());
975 let start_row = cursor.row;
976 let cur_len = buf_line_chars(ed.buffer(), start_row);
979 let insert_col = if before {
980 cursor.col
981 } else if cur_len == 0 {
982 0
983 } else {
984 cursor.col + 1
985 };
986 let segments: Vec<&str> = yank.split('\n').collect();
987 let last_row = start_row + segments.len().saturating_sub(1);
993 let raw_rows = buf_row_count(ed.buffer());
994 let trailing_nl = raw_rows > 1 && buf_line_chars(ed.buffer(), raw_rows - 1) == 0;
1000 let content_rows = if trailing_nl { raw_rows - 1 } else { raw_rows };
1001 if last_row >= content_rows {
1002 let anchor = content_rows.saturating_sub(1);
1003 let at = Position::new(anchor, buf_line_chars(ed.buffer(), anchor));
1004 ed.mutate_edit(Edit::InsertStr {
1005 at,
1006 text: "\n".repeat(last_row + 1 - content_rows),
1007 });
1008 }
1009 let chunks: Vec<String> = segments
1013 .iter()
1014 .enumerate()
1015 .map(|(i, seg)| {
1016 let tail_is_empty = buf_line_chars(ed.buffer(), start_row + i) <= insert_col;
1019 if tail_is_empty {
1020 return seg.repeat(count);
1021 }
1022 let seg_len = seg.chars().count();
1023 let mut padded = String::with_capacity(seg.len() + width.saturating_sub(seg_len));
1024 padded.push_str(seg);
1025 if seg_len < width {
1026 padded.extend(std::iter::repeat_n(' ', width - seg_len));
1027 }
1028 padded.repeat(count)
1029 })
1030 .collect();
1031 ed.mutate_edit(Edit::InsertBlock {
1032 at: Position::new(start_row, insert_col),
1033 chunks,
1034 });
1035 ed.jump_cursor(start_row, insert_col);
1037 let bot_row = start_row + segments.len().saturating_sub(1);
1039 ed.set_mark('[', (start_row, insert_col));
1040 ed.set_mark(']', (bot_row, insert_col));
1041 ed.set_sticky_col(Some(insert_col));
1042}
1043pub fn visual_paste<H: hjkl_engine::types::Host>(
1048 ed: &mut Editor<hjkl_buffer::View, H>,
1049 before: bool,
1050) {
1051 use hjkl_buffer::{Edit, Position};
1052 ed.sync_buffer_content_from_textarea();
1053
1054 let selector = vim_mut(ed).pending_register.take();
1057 sync_clipboard_register_for(ed, selector);
1060 let (reg_text, reg_linewise, reg_blockwise, reg_block_width) =
1061 ed.with_registers(|regs| match selector.and_then(|c| regs.read(c)) {
1062 Some(slot) => (
1063 slot.text.clone(),
1064 slot.linewise,
1065 slot.blockwise,
1066 slot.block_width,
1067 ),
1068 None => (
1069 regs.unnamed.text.clone(),
1070 regs.unnamed.linewise,
1071 regs.unnamed.blockwise,
1072 regs.unnamed.block_width,
1073 ),
1074 });
1075 let saved_unnamed = before.then(|| ed.with_registers(|regs| regs.unnamed.clone()));
1077
1078 let mode = vim(ed).mode;
1079 ed.push_undo();
1080
1081 match mode {
1082 Mode::VisualLine => {
1083 let cursor_row = buf_cursor_pos(ed.buffer()).row;
1084 let top = cursor_row.min(vim(ed).visual_line_anchor);
1085 let bot = cursor_row.max(vim(ed).visual_line_anchor);
1086 let deleted_through_last = bot >= last_content_row(ed);
1090 let ends_in_newline = buf_row_count(ed.buffer()) > last_content_row(ed) + 1;
1091 cut_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
1093 let text = linewise_payload(®_text).to_string();
1095 let line_count = buf_row_count(ed.buffer());
1096 if reg_text.is_empty() {
1097 land_after_linewise_delete(ed, top, deleted_through_last);
1100 } else if top == 0 && deleted_through_last {
1101 let newline = if ends_in_newline { "\n" } else { "" };
1105 ed.mutate_edit(Edit::InsertStr {
1106 at: Position::new(0, 0),
1107 text: format!("{text}{newline}"),
1108 });
1109 buf_set_cursor_rc(ed.buffer_mut(), 0, 0);
1110 } else if top >= line_count {
1111 let last = line_count.saturating_sub(1);
1114 let lc = buf_line_chars(ed.buffer(), last);
1115 ed.mutate_edit(Edit::InsertStr {
1116 at: Position::new(last, lc),
1117 text: format!("\n{text}"),
1118 });
1119 buf_set_cursor_rc(ed.buffer_mut(), last + 1, 0);
1120 } else {
1121 ed.mutate_edit(Edit::InsertStr {
1122 at: Position::new(top, 0),
1123 text: format!("{text}\n"),
1124 });
1125 buf_set_cursor_rc(ed.buffer_mut(), top, 0);
1126 }
1127 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
1128 }
1129 Mode::Visual => {
1130 let anchor = vim(ed).visual_anchor;
1131 let cursor = ed.cursor();
1132 let (top, bot) = order(anchor, cursor);
1133 let (from, to, kind) = charwise_visual_range(ed, top, bot);
1136 cut_vim_range(ed, from, to, kind);
1137 if reg_linewise {
1139 let text = linewise_payload(®_text).to_string();
1143 ed.mutate_edit(Edit::InsertStr {
1144 at: Position::new(top.0, top.1),
1145 text: format!("\n{text}\n"),
1146 });
1147 buf_set_cursor_rc(ed.buffer_mut(), top.0 + 1, 0);
1148 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
1149 } else {
1150 ed.mutate_edit(Edit::InsertStr {
1151 at: Position::new(top.0, top.1),
1152 text: reg_text.clone(),
1153 });
1154 let inserted_len = reg_text.chars().count();
1158 let last_col = top.1 + inserted_len.saturating_sub(1);
1159 buf_set_cursor_rc(ed.buffer_mut(), top.0, last_col);
1160 clamp_cursor_to_normal_mode(ed);
1161 }
1162 }
1163 Mode::VisualBlock => {
1164 let (top, bot, left, right) = block_bounds(ed);
1176 let to_eol = vim(ed).block_to_eol;
1177 let deleted = block_yank(ed, top, bot, left, right, to_eol);
1179 let del_width = if to_eol {
1180 deleted
1181 .split('\n')
1182 .map(|s| s.chars().count())
1183 .max()
1184 .unwrap_or(0)
1185 } else {
1186 right + 1 - left
1187 };
1188 delete_block_contents(ed, top, bot, left, right, to_eol);
1189 if !before && !deleted.is_empty() {
1193 ed.record_yank_to_host(deleted.clone());
1194 ed.record_delete_block(deleted, del_width, None);
1195 }
1196 if reg_blockwise {
1197 ed.jump_cursor(top, left);
1198 do_block_paste(ed, true, 1, reg_block_width, ®_text);
1201 } else if reg_linewise {
1202 let text = linewise_payload(®_text).to_string();
1203 let lc = buf_line_chars(ed.buffer(), bot);
1204 ed.mutate_edit(Edit::InsertStr {
1205 at: Position::new(bot, lc),
1206 text: format!("\n{text}"),
1207 });
1208 buf_set_cursor_rc(ed.buffer_mut(), bot + 1, 0);
1209 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
1210 } else if reg_text.contains('\n') {
1211 ed.mutate_edit(Edit::InsertStr {
1212 at: Position::new(top, left),
1213 text: reg_text.clone(),
1214 });
1215 buf_set_cursor_rc(ed.buffer_mut(), top, left);
1216 } else {
1217 for r in top..=bot {
1223 let line_len = buf_line_chars(ed.buffer(), r);
1224 if left > line_len {
1225 continue;
1226 }
1227 ed.mutate_edit(Edit::InsertStr {
1228 at: Position::new(r, left),
1229 text: reg_text.clone(),
1230 });
1231 }
1232 let last_col = left + reg_text.chars().count().saturating_sub(1);
1233 buf_set_cursor_rc(ed.buffer_mut(), top, last_col);
1234 }
1235 }
1236 _ => {}
1237 }
1238
1239 if let Some(slot) = saved_unnamed {
1241 ed.with_registers_mut(|regs| regs.unnamed = slot);
1242 }
1243 vim_mut(ed).mode = Mode::Normal;
1244 ed.set_sticky_col(Some(buf_cursor_pos(ed.buffer()).col));
1245}
1246pub fn adjust_number_visual<H: hjkl_engine::types::Host>(
1251 ed: &mut Editor<hjkl_buffer::View, H>,
1252 delta: i64,
1253 sequential: bool,
1254) {
1255 use hjkl_buffer::{Edit, MotionKind, Position};
1256 ed.sync_buffer_content_from_textarea();
1257 let mode = vim(ed).mode;
1258 let cursor = buf_cursor_pos(ed.buffer());
1259
1260 let (top, bot, mut scan_col_first, block_left) = match mode {
1262 Mode::VisualLine => {
1263 let t = cursor.row.min(vim(ed).visual_line_anchor);
1264 let b = cursor.row.max(vim(ed).visual_line_anchor);
1265 (t, b, 0usize, None)
1266 }
1267 Mode::Visual => {
1268 let (a, c) = order(vim(ed).visual_anchor, (cursor.row, cursor.col));
1269 (a.0, c.0, a.1, None)
1270 }
1271 Mode::VisualBlock => {
1272 let (a, c) = order(vim(ed).block_anchor, (cursor.row, cursor.col));
1273 let left = a.1.min(c.1);
1274 (a.0, c.0, left, Some(left))
1275 }
1276 _ => return,
1277 };
1278
1279 ed.push_undo();
1280 let mut found_count: i64 = 0;
1281 for row in top..=bot {
1282 let start_col = match block_left {
1283 Some(left) => left,
1284 None => {
1285 let c = if row == top { scan_col_first } else { 0 };
1288 scan_col_first = 0;
1289 c
1290 }
1291 };
1292 let chars: Vec<char> = match buf_line(ed.buffer(), row) {
1293 Some(l) => l.chars().collect(),
1294 None => continue,
1295 };
1296 let this_delta = if sequential {
1302 delta.saturating_mul(found_count.saturating_add(1))
1303 } else {
1304 delta
1305 };
1306 let Some(AdjustedNumber {
1307 start: span_start,
1308 end: span_end,
1309 text: new_s,
1310 }) = adjusted_number_at(&chars, start_col, this_delta)
1311 else {
1312 continue;
1313 };
1314 found_count += 1;
1315 let span_start_pos = Position::new(row, span_start);
1316 let span_end_pos = Position::new(row, span_end);
1317 ed.mutate_edit(Edit::DeleteRange {
1318 start: span_start_pos,
1319 end: span_end_pos,
1320 kind: MotionKind::Char,
1321 });
1322 ed.mutate_edit(Edit::InsertStr {
1323 at: span_start_pos,
1324 text: new_s,
1325 });
1326 }
1327 buf_set_cursor_rc(ed.buffer_mut(), top, block_left.unwrap_or(0));
1329 vim_mut(ed).mode = Mode::Normal;
1330 ed.set_sticky_col(Some(buf_cursor_pos(ed.buffer()).col));
1331}
1332#[cfg(test)]
1333mod replace_char_tests {
1334 use hjkl_buffer::{View, rope_line_str};
1335 use hjkl_engine::{DefaultHost, Editor, Options};
1336
1337 fn line(ed: &Editor<View, DefaultHost>, row: usize) -> String {
1338 rope_line_str(&ed.buffer().rope(), row)
1339 }
1340
1341 #[test]
1342 fn replace_char_count_exceeding_line_replaces_nothing() {
1343 let buf = View::from_str("ab\ncd");
1344 let mut ed = crate::vim::vim_editor(buf, DefaultHost::new(), Options::default());
1345 super::replace_char(&mut ed, 'x', 3);
1348 assert_eq!(line(&ed, 0), "ab", "partial replace must not happen");
1349 assert_eq!(line(&ed, 1), "cd", "must not spill onto the next line");
1350 }
1351
1352 #[test]
1353 fn replace_char_count_fitting_replaces_run() {
1354 let buf = View::from_str("abc");
1355 let mut ed = crate::vim::vim_editor(buf, DefaultHost::new(), Options::default());
1356 super::replace_char(&mut ed, 'x', 2);
1357 assert_eq!(line(&ed, 0), "xxc");
1358 }
1359}
1360#[cfg(test)]
1361mod g_ampersand_tests {
1362 use super::*;
1363 use hjkl_buffer::{View, rope_line_str};
1364 use hjkl_engine::{DefaultHost, Editor, Options};
1365
1366 fn make_editor(content: &str) -> Editor<View, DefaultHost> {
1367 let buf = View::from_str(content);
1368 let host = DefaultHost::new();
1369 crate::vim::vim_editor(buf, host, Options::default())
1370 }
1371
1372 fn buf_line(ed: &Editor<View, DefaultHost>, row: usize) -> String {
1373 let rope = ed.buffer().rope();
1374 rope_line_str(&rope, row).trim_end_matches('\n').to_string()
1375 }
1376
1377 #[test]
1380 fn g_ampersand_repeats_last_substitute_on_whole_buffer() {
1381 let mut ed = make_editor("foo\nfoo bar foo\nbaz");
1382 let cmd = hjkl_engine::substitute::parse_substitute("/foo/bar/").unwrap();
1384 ed.set_last_substitute(cmd);
1385 apply_after_g(&mut ed, '&', 1);
1387 assert_eq!(buf_line(&ed, 0), "bar");
1388 assert_eq!(buf_line(&ed, 1), "bar bar foo");
1390 assert_eq!(buf_line(&ed, 2), "baz");
1391 }
1392
1393 #[test]
1395 fn g_ampersand_with_g_flag_replaces_all_per_line() {
1396 let mut ed = make_editor("foo foo\nfoo");
1397 let cmd = hjkl_engine::substitute::parse_substitute("/foo/bar/g").unwrap();
1398 ed.set_last_substitute(cmd);
1399 apply_after_g(&mut ed, '&', 1);
1400 assert_eq!(buf_line(&ed, 0), "bar bar");
1401 assert_eq!(buf_line(&ed, 1), "bar");
1402 }
1403
1404 #[test]
1406 fn g_ampersand_noop_when_no_prior_substitute() {
1407 let mut ed = make_editor("foo\nbar");
1408 apply_after_g(&mut ed, '&', 1);
1410 assert_eq!(buf_line(&ed, 0), "foo");
1411 assert_eq!(buf_line(&ed, 1), "bar");
1412 }
1413}
1414
1415#[cfg(test)]
1416mod fold_char_delete_tests {
1417 use hjkl_buffer::{View, rope_line_str};
1418 use hjkl_engine::types::FoldOp;
1419 use hjkl_engine::{DefaultHost, Editor, Options};
1420
1421 use super::do_char_delete;
1422
1423 fn make_editor(content: &str, fold: Option<(usize, usize)>) -> Editor<View, DefaultHost> {
1424 let mut ed = crate::vim::vim_editor(
1425 View::from_str(content),
1426 DefaultHost::new(),
1427 Options::default(),
1428 );
1429 ed.jump_cursor(0, 0);
1430 if let Some((start, end)) = fold {
1431 ed.apply_fold_op(FoldOp::Add {
1432 start_row: start,
1433 end_row: end,
1434 closed: true,
1435 });
1436 }
1437 ed
1438 }
1439
1440 fn full_buffer(ed: &Editor<View, DefaultHost>) -> String {
1441 let rope = ed.buffer().rope();
1442 (0..rope.len_lines())
1443 .map(|i| rope_line_str(&rope, i).to_string())
1444 .collect::<Vec<_>>()
1445 .join("\n")
1446 }
1447
1448 #[test]
1449 fn x_on_closed_fold_deletes_whole_fold_linewise() {
1450 let mut ed = make_editor("abc\ndef\nghi\n", Some((0, 1)));
1451 do_char_delete(&mut ed, true, 1);
1452 assert_eq!(full_buffer(&ed), "ghi\n");
1453 assert_eq!(ed.yank(), "abc\ndef\n");
1454 assert_eq!(ed.cursor(), (0, 0));
1455 }
1456
1457 #[test]
1458 fn backward_char_delete_on_closed_fold_is_not_promoted() {
1459 let mut ed = make_editor("abc\ndef\nghi\n", Some((0, 1)));
1460 do_char_delete(&mut ed, false, 1);
1461 assert_eq!(full_buffer(&ed), "abc\ndef\nghi\n");
1462 assert_eq!(ed.yank(), "");
1463 }
1464
1465 #[test]
1466 fn x_on_single_line_fold_stays_charwise() {
1467 let mut ed = make_editor("abc\ndef\nghi\n", Some((0, 0)));
1468 do_char_delete(&mut ed, true, 1);
1469 assert_eq!(full_buffer(&ed), "bc\ndef\nghi\n");
1470 assert_eq!(ed.yank(), "a");
1471 }
1472
1473 #[test]
1474 fn x_on_plain_buffer_deletes_one_char() {
1475 let mut ed = make_editor("abc\ndef\nghi\n", None);
1476 do_char_delete(&mut ed, true, 1);
1477 assert_eq!(full_buffer(&ed), "bc\ndef\nghi\n");
1478 assert_eq!(ed.yank(), "a");
1479 }
1480
1481 #[test]
1482 fn x_at_col1_on_closed_fold_stays_charwise() {
1483 let mut ed = make_editor("abc\ndef\nghi\n", Some((0, 1)));
1484 crate::vim::execute_motion(&mut ed, hjkl_vim_types::Motion::Right, 1);
1487 do_char_delete(&mut ed, true, 1);
1488 assert_eq!(full_buffer(&ed), "ac\ndef\nghi\n");
1489 assert_eq!(ed.yank(), "b");
1490 }
1491}