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 = if bot.1 < line_chars {
111 Position::new(bot.0, bot.1 + 1)
112 } else if bot.0 + 1 < buf_row_count(ed.buffer()) {
113 Position::new(bot.0 + 1, 0)
114 } else {
115 Position::new(bot.0, line_chars)
116 };
117 (Position::new(top.0, top.1), next, BufKind::Char)
118 }
119 RangeKind::Exclusive => (
120 Position::new(top.0, top.1),
121 Position::new(bot.0, bot.1),
122 BufKind::Char,
123 ),
124 };
125 let inverse = ed.mutate_edit(Edit::DeleteRange {
126 start: buf_start,
127 end: buf_end,
128 kind: buf_kind,
129 });
130 let raw_text = match inverse {
131 Edit::InsertStr { text, .. } => text,
132 _ => String::new(),
133 };
134 let text = if matches!(kind, RangeKind::Linewise) {
139 if raw_text.ends_with('\n') {
140 raw_text
142 } else if raw_text.starts_with('\n') {
143 format!("{}\n", raw_text.strip_prefix('\n').unwrap())
146 } else if raw_text.is_empty() {
147 return String::new();
150 } else {
151 format!("{raw_text}\n")
153 }
154 } else {
155 raw_text
156 };
157 if text.is_empty() {
158 return text;
160 }
161 if record {
162 ed.record_yank_to_host(text.clone());
163 let target = vim_mut(ed).pending_register.take();
164 ed.record_delete(text.clone(), matches!(kind, RangeKind::Linewise), target);
165 }
166 text
167}
168pub fn delete_to_eol<H: hjkl_engine::types::Host>(ed: &mut Editor<hjkl_buffer::View, H>) {
174 use hjkl_buffer::{Edit, MotionKind, Position};
175 ed.sync_buffer_content_from_textarea();
176 let cursor = buf_cursor_pos(ed.buffer());
177 let line_chars = buf_line_chars(ed.buffer(), cursor.row);
178 if cursor.col >= line_chars {
179 return;
180 }
181 let inverse = ed.mutate_edit(Edit::DeleteRange {
182 start: cursor,
183 end: Position::new(cursor.row, line_chars),
184 kind: MotionKind::Char,
185 });
186 if let Edit::InsertStr { text, .. } = inverse
187 && !text.is_empty()
188 {
189 ed.record_yank_to_host(text.clone());
190 ed.set_yank_linewise(false);
191 let target = vim_mut(ed).pending_register.take();
195 ed.record_delete(text, false, target);
196 }
197 buf_set_cursor_pos(ed.buffer_mut(), cursor);
198}
199pub fn do_char_delete<H: hjkl_engine::types::Host>(
200 ed: &mut Editor<hjkl_buffer::View, H>,
201 forward: bool,
202 count: usize,
203) {
204 use hjkl_buffer::{Edit, MotionKind, Position};
205 ed.push_undo();
206 ed.sync_buffer_content_from_textarea();
207 let mut deleted = String::new();
210 for _ in 0..count {
211 let cursor = buf_cursor_pos(ed.buffer());
212 let line_chars = buf_line_chars(ed.buffer(), cursor.row);
213 if forward {
214 if cursor.col >= line_chars {
220 break;
221 }
222 let inverse = ed.mutate_edit(Edit::DeleteRange {
223 start: cursor,
224 end: Position::new(cursor.row, cursor.col + 1),
225 kind: MotionKind::Char,
226 });
227 if let Edit::InsertStr { text, .. } = inverse {
228 deleted.push_str(&text);
229 }
230 } else {
231 if cursor.col == 0 {
234 break;
235 }
236 let inverse = ed.mutate_edit(Edit::DeleteRange {
237 start: Position::new(cursor.row, cursor.col - 1),
238 end: cursor,
239 kind: MotionKind::Char,
240 });
241 if let Edit::InsertStr { text, .. } = inverse {
242 deleted = text + &deleted;
245 }
246 }
247 }
248 if !deleted.is_empty() {
249 ed.record_yank_to_host(deleted.clone());
250 let target = vim_mut(ed).pending_register.take();
251 ed.record_delete(deleted, false, target);
252 }
253 let cursor = buf_cursor_pos(ed.buffer());
256 let line_chars = buf_line_chars(ed.buffer(), cursor.row);
257 if line_chars > 0 && cursor.col >= line_chars {
258 buf_set_cursor_pos(ed.buffer_mut(), Position::new(cursor.row, line_chars - 1));
259 }
260}
261struct AdjustedNumber {
266 start: usize,
267 end: usize,
268 text: String,
269}
270
271fn adjusted_number_at(chars: &[char], from: usize, delta: i64) -> Option<AdjustedNumber> {
282 let len = chars.len();
283 let is_hex_prefix = |i: usize| {
284 chars[i] == '0'
285 && matches!(chars.get(i + 1), Some('x' | 'X'))
286 && chars.get(i + 2).is_some_and(|c| c.is_ascii_hexdigit())
287 };
288 let start = (from.min(len)..len).find(|&i| is_hex_prefix(i) || chars[i].is_ascii_digit())?;
289
290 if is_hex_prefix(start) {
291 let digits_start = start + 2;
293 let mut digits_end = digits_start;
294 while digits_end < len && chars[digits_end].is_ascii_hexdigit() {
295 digits_end += 1;
296 }
297 let digits: String = chars[digits_start..digits_end].iter().collect();
298 let n = u64::from_str_radix(&digits, 16).ok()?;
299 let new_val = (n as i128 + delta as i128).max(0) as u64;
300 let width = digits_end - digits_start;
301 let prefix: String = chars[start..digits_start].iter().collect();
302 let upper = digits
307 .chars()
308 .rev()
309 .find(|c| c.is_ascii_alphabetic())
310 .map_or(chars[start + 1] == 'X', |c| c.is_ascii_uppercase());
311 let text = if upper {
312 format!("{prefix}{new_val:0width$X}")
313 } else {
314 format!("{prefix}{new_val:0width$x}")
315 };
316 return Some(AdjustedNumber {
317 start,
318 end: digits_end,
319 text,
320 });
321 }
322
323 let span_start = if start > 0 && chars[start - 1] == '-' {
326 start - 1
327 } else {
328 start
329 };
330 let mut span_end = start;
331 while span_end < len && chars[span_end].is_ascii_digit() {
332 span_end += 1;
333 }
334 let s: String = chars[span_start..span_end].iter().collect();
335 let n = s.parse::<i64>().ok()?;
336 let new_val = n as i128 + delta as i128;
337 let digits: String = chars[start..span_end].iter().collect();
343 let width = digits.len();
344 let text = if width > 1 && digits.starts_with('0') {
345 if new_val < 0 {
346 let mag = new_val.unsigned_abs();
347 format!("-{mag:0width$}")
348 } else {
349 format!("{new_val:0width$}")
350 }
351 } else {
352 new_val.to_string()
353 };
354 Some(AdjustedNumber {
355 start: span_start,
356 end: span_end,
357 text,
358 })
359}
360
361pub fn adjust_number<H: hjkl_engine::types::Host>(
367 ed: &mut Editor<hjkl_buffer::View, H>,
368 delta: i64,
369) -> bool {
370 use hjkl_buffer::{Edit, MotionKind, Position};
371 ed.sync_buffer_content_from_textarea();
372 let cursor = buf_cursor_pos(ed.buffer());
373 let row = cursor.row;
374 let chars: Vec<char> = match buf_line(ed.buffer(), row) {
375 Some(l) => l.chars().collect(),
376 None => return false,
377 };
378 let Some(AdjustedNumber {
379 start: span_start,
380 end: span_end,
381 text: new_s,
382 }) = adjusted_number_at(&chars, cursor.col, delta)
383 else {
384 return false;
385 };
386
387 ed.push_undo();
388 let span_start_pos = Position::new(row, span_start);
389 let span_end_pos = Position::new(row, span_end);
390 ed.mutate_edit(Edit::DeleteRange {
391 start: span_start_pos,
392 end: span_end_pos,
393 kind: MotionKind::Char,
394 });
395 ed.mutate_edit(Edit::InsertStr {
396 at: span_start_pos,
397 text: new_s.clone(),
398 });
399 let new_len = new_s.chars().count();
400 buf_set_cursor_rc(ed.buffer_mut(), row, span_start + new_len.saturating_sub(1));
401 true
402}
403pub fn replace_char<H: hjkl_engine::types::Host>(
404 ed: &mut Editor<hjkl_buffer::View, H>,
405 ch: char,
406 count: usize,
407) {
408 use hjkl_buffer::{Edit, MotionKind, Position};
409 ed.sync_buffer_content_from_textarea();
410 let start = buf_cursor_pos(ed.buffer());
414 let start_line_chars = buf_line_chars(ed.buffer(), start.row);
415 if count == 0 || start.col + count > start_line_chars {
416 return;
417 }
418 ed.push_undo();
419 for _ in 0..count {
420 let cursor = buf_cursor_pos(ed.buffer());
421 let line_chars = buf_line_chars(ed.buffer(), cursor.row);
422 if cursor.col >= line_chars {
423 break;
424 }
425 ed.mutate_edit(Edit::DeleteRange {
426 start: cursor,
427 end: Position::new(cursor.row, cursor.col + 1),
428 kind: MotionKind::Char,
429 });
430 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
431 }
432 hjkl_engine::motions::move_left(ed.buffer_mut(), 1);
434}
435pub fn toggle_case_at_cursor<H: hjkl_engine::types::Host>(
439 ed: &mut Editor<hjkl_buffer::View, H>,
440) -> bool {
441 use hjkl_buffer::{Edit, MotionKind, Position};
442 ed.sync_buffer_content_from_textarea();
443 let cursor = buf_cursor_pos(ed.buffer());
444 let Some(c) = buf_line(ed.buffer(), cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
445 return false;
446 };
447 let toggled = if c.is_uppercase() {
448 c.to_lowercase().collect::<String>()
449 } else {
450 c.to_uppercase().collect::<String>()
451 };
452 ed.mutate_edit(Edit::DeleteRange {
453 start: cursor,
454 end: Position::new(cursor.row, cursor.col + 1),
455 kind: MotionKind::Char,
456 });
457 ed.mutate_edit(Edit::InsertStr {
458 at: cursor,
459 text: toggled,
460 });
461 true
462}
463pub fn join_line<H: hjkl_engine::types::Host>(ed: &mut Editor<hjkl_buffer::View, H>) -> bool {
466 use hjkl_buffer::{Edit, Position};
467 ed.sync_buffer_content_from_textarea();
468 let row = buf_cursor_pos(ed.buffer()).row;
469 let n_rows = buf_row_count(ed.buffer());
470 if row + 1 >= n_rows {
471 return false;
472 }
473 if row + 2 >= n_rows && buf_line(ed.buffer(), row + 1).is_some_and(|s| s.is_empty()) {
477 return false;
478 }
479 let cur_line = buf_line(ed.buffer(), row).unwrap_or_default();
480 let next_raw = buf_line(ed.buffer(), row + 1).unwrap_or_default();
481 let next_trimmed = next_raw.trim_start();
482 let cur_chars = cur_line.chars().count();
483 let next_chars = next_raw.chars().count();
484 let separator = if !cur_line.is_empty()
487 && !next_trimmed.is_empty()
488 && !cur_line.ends_with([' ', '\t'])
489 && !next_trimmed.starts_with(')')
490 {
491 " "
492 } else {
493 ""
494 };
495 let joined = format!("{cur_line}{separator}{next_trimmed}");
496 ed.mutate_edit(Edit::Replace {
497 start: Position::new(row, 0),
498 end: Position::new(row + 1, next_chars),
499 with: joined,
500 });
501 buf_set_cursor_rc(ed.buffer_mut(), row, cur_chars);
505 true
506}
507pub fn join_line_raw<H: hjkl_engine::types::Host>(ed: &mut Editor<hjkl_buffer::View, H>) -> bool {
511 use hjkl_buffer::Edit;
512 ed.sync_buffer_content_from_textarea();
513 let row = buf_cursor_pos(ed.buffer()).row;
514 let n_rows = buf_row_count(ed.buffer());
515 if row + 1 >= n_rows {
516 return false;
517 }
518 if row + 2 >= n_rows && buf_line(ed.buffer(), row + 1).is_some_and(|s| s.is_empty()) {
520 return false;
521 }
522 let join_col = buf_line_chars(ed.buffer(), row);
523 ed.mutate_edit(Edit::JoinLines {
524 row,
525 count: 1,
526 with_space: false,
527 });
528 buf_set_cursor_rc(ed.buffer_mut(), row, join_col);
530 true
531}
532pub fn visual_join<H: hjkl_engine::types::Host>(
536 ed: &mut Editor<hjkl_buffer::View, H>,
537 with_space: bool,
538) {
539 let cursor_row = buf_cursor_pos(ed.buffer()).row;
540 let (top, bot) = match vim(ed).mode {
541 Mode::VisualLine => (
542 cursor_row.min(vim(ed).visual_line_anchor),
543 cursor_row.max(vim(ed).visual_line_anchor),
544 ),
545 Mode::VisualBlock => {
546 let a = vim(ed).block_anchor.0;
547 (a.min(cursor_row), a.max(cursor_row))
548 }
549 Mode::Visual => {
550 let a = vim(ed).visual_anchor.0;
551 (a.min(cursor_row), a.max(cursor_row))
552 }
553 _ => return,
554 };
555 let joins = (bot - top).max(1);
558 ed.push_undo();
559 buf_set_cursor_rc(ed.buffer_mut(), top, 0);
560 for _ in 0..joins {
561 let joined = if with_space {
562 join_line(ed)
563 } else {
564 join_line_raw(ed)
565 };
566 if !joined {
567 break;
568 }
569 }
570 if !vim(ed).replaying {
576 vim_mut(ed).last_change = Some(LastChange::JoinLine { count: joins });
577 }
578 vim_mut(ed).mode = Mode::Normal;
579 ed.set_sticky_col(Some(buf_cursor_pos(ed.buffer()).col));
580}
581pub fn goto_percent<H: hjkl_engine::types::Host>(
584 ed: &mut Editor<hjkl_buffer::View, H>,
585 count: usize,
586) {
587 let rows = buf_row_count(ed.buffer());
588 if rows == 0 {
589 return;
590 }
591 let total = if rows >= 2 && buf_line(ed.buffer(), rows - 1).is_some_and(|s| s.is_empty()) {
594 rows - 1
595 } else {
596 rows
597 };
598 let line = count.saturating_mul(total).div_ceil(100).clamp(1, total);
602 let pre = ed.cursor();
603 ed.jump_cursor(line - 1, 0);
604 move_first_non_whitespace(ed);
605 ed.set_sticky_col(Some(ed.cursor().1));
606 if ed.cursor() != pre {
607 ed.push_jump(pre);
608 }
609}
610pub fn indent_width(s: &str, tabstop: usize) -> usize {
613 let ts = tabstop.max(1);
614 let mut w = 0usize;
615 for c in s.chars() {
616 match c {
617 ' ' => w += 1,
618 '\t' => w += ts - (w % ts),
619 _ => break,
620 }
621 }
622 w
623}
624pub fn build_indent(width: usize, settings: &hjkl_engine::Settings) -> String {
627 if settings.expandtab {
628 return " ".repeat(width);
629 }
630 let ts = settings.tabstop.max(1);
631 let tabs = width / ts;
632 let spaces = width % ts;
633 format!("{}{}", "\t".repeat(tabs), " ".repeat(spaces))
634}
635pub fn reindent_block(text: &str, target_width: usize, settings: &hjkl_engine::Settings) -> String {
638 let ts = settings.tabstop.max(1);
639 let lines: Vec<&str> = text.split('\n').collect();
640 let first_width = lines.first().map_or(0, |l| indent_width(l, ts));
641 let delta = target_width as isize - first_width as isize;
642 lines
643 .iter()
644 .map(|line| {
645 let trimmed = line.trim_start_matches([' ', '\t']);
646 if trimmed.is_empty() {
647 return String::new();
649 }
650 let old_w = indent_width(line, ts) as isize;
651 let new_w = (old_w + delta).max(0) as usize;
652 format!("{}{}", build_indent(new_w, settings), trimmed)
653 })
654 .collect::<Vec<_>>()
655 .join("\n")
656}
657pub const MAX_PASTE_BYTES: usize = 64 * 1024 * 1024;
674
675fn reject_oversized_paste<H: hjkl_engine::types::Host>(
678 ed: &mut Editor<hjkl_buffer::View, H>,
679 bytes: usize,
680) {
681 ed.push_error(format!("E342: Out of memory! (allocating {bytes} bytes)"));
682}
683
684pub fn do_paste<H: hjkl_engine::types::Host>(
685 ed: &mut Editor<hjkl_buffer::View, H>,
686 before: bool,
687 count: usize,
688 cursor_after: bool,
689 reindent: bool,
690) -> bool {
691 use hjkl_buffer::{Edit, Position};
692 let selector = vim_mut(ed).pending_register.take();
697 sync_clipboard_register_for(ed, selector);
701 let (yank, linewise, blockwise, block_width) =
702 ed.with_registers(|regs| match selector.and_then(|c| regs.read(c)) {
703 Some(slot) => (
704 slot.text.clone(),
705 slot.linewise,
706 slot.blockwise,
707 slot.block_width,
708 ),
709 None => (
715 regs.unnamed.text.clone(),
716 regs.unnamed.linewise,
717 regs.unnamed.blockwise,
718 regs.unnamed.block_width,
719 ),
720 });
721 let original_row_for_linewise_after = if linewise && !before {
730 let r = buf_cursor_pos(ed.buffer()).row;
733 let (_, fold_end) = expand_linewise_over_closed_folds(ed.buffer(), r, r);
734 Some(fold_end)
735 } else {
736 None
737 };
738 if yank.is_empty() {
741 return false;
742 }
743 let Some(requested_bytes) = yank.len().checked_mul(count) else {
751 reject_oversized_paste(ed, yank.len().saturating_mul(count));
752 return false;
753 };
754 if requested_bytes > MAX_PASTE_BYTES {
755 reject_oversized_paste(ed, requested_bytes);
756 return false;
757 }
758 if blockwise {
759 let Some(block_bytes) = block_width
760 .checked_mul(yank.split('\n').count())
761 .and_then(|bytes| bytes.checked_mul(count))
762 else {
763 reject_oversized_paste(ed, usize::MAX);
764 return false;
765 };
766 let Some(total_bytes) = requested_bytes.checked_add(block_bytes) else {
767 reject_oversized_paste(ed, usize::MAX);
768 return false;
769 };
770 if total_bytes > MAX_PASTE_BYTES {
771 reject_oversized_paste(ed, total_bytes);
772 return false;
773 }
774 }
775 ed.push_undo();
776 if blockwise {
780 do_block_paste(ed, before, count, block_width, &yank);
781 return true;
782 }
783 let paste_mark = if linewise {
787 ed.sync_buffer_content_from_textarea();
788 let row = buf_cursor_pos(ed.buffer()).row;
789 let mut text = yank.trim_matches('\n').to_string();
790 if reindent {
791 let cur_line = buf_line(ed.buffer(), row).unwrap_or_default();
792 let target_w = indent_width(&cur_line, ed.settings().tabstop.max(1));
793 text = reindent_block(&text, target_w, ed.settings());
794 }
795 let (fold_start, fold_end) = expand_linewise_over_closed_folds(ed.buffer(), row, row);
796 let payload = std::iter::repeat_n(text.as_str(), count)
797 .collect::<Vec<_>>()
798 .join("\n");
799 let target_row = if before {
800 ed.mutate_edit(Edit::InsertStr {
801 at: Position::new(fold_start, 0),
802 text: format!("{payload}\n"),
803 });
804 fold_start
805 } else {
806 let line_chars = buf_line_chars(ed.buffer(), fold_end);
807 ed.mutate_edit(Edit::InsertStr {
808 at: Position::new(fold_end, line_chars),
809 text: format!("\n{payload}"),
810 });
811 fold_end + 1
812 };
813 buf_set_cursor_rc(ed.buffer_mut(), target_row, 0);
814 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
815 let payload_lines = payload.lines().count().max(1);
816 let bot_row = target_row + payload_lines - 1;
817 let bot_last_col = buf_line_chars(ed.buffer(), bot_row).saturating_sub(1);
818 ((target_row, 0), (bot_row, bot_last_col))
819 } else {
820 ed.sync_buffer_content_from_textarea();
821 let cursor = buf_cursor_pos(ed.buffer());
825 let at = if before {
826 cursor
827 } else {
828 let line_chars = buf_line_chars(ed.buffer(), cursor.row);
829 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
830 };
831 let repeated = yank.repeat(count);
832 ed.mutate_edit(Edit::InsertStr { at, text: repeated });
833 if !cursor_after && ed.cursor().1 > 0 {
838 hjkl_engine::motions::move_left(ed.buffer_mut(), 1);
839 }
840 let lo = (at.row, at.col);
842 let hi = if cursor_after {
843 let c = ed.cursor();
844 (c.0, c.1.saturating_sub(1))
845 } else {
846 ed.cursor()
847 };
848 (lo, hi)
849 };
850 ed.set_mark('[', paste_mark.0);
851 ed.set_mark(']', paste_mark.1);
852 if cursor_after && linewise {
855 let bot_row = paste_mark.1.0;
856 let last_row = buf_row_count(ed.buffer()).saturating_sub(1);
857 let target = (bot_row + 1).min(last_row);
858 buf_set_cursor_rc(ed.buffer_mut(), target, 0);
859 } else if let Some(orig_row) = original_row_for_linewise_after {
860 let first_target = orig_row.saturating_add(1);
865 buf_set_cursor_rc(ed.buffer_mut(), first_target, 0);
866 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
867 }
868 ed.set_sticky_col(Some(buf_cursor_pos(ed.buffer()).col));
870 true
871}
872fn do_block_paste<H: hjkl_engine::types::Host>(
890 ed: &mut Editor<hjkl_buffer::View, H>,
891 before: bool,
892 count: usize,
893 width: usize,
894 yank: &str,
895) {
896 use hjkl_buffer::{Edit, Position};
897 ed.sync_buffer_content_from_textarea();
898 let cursor = buf_cursor_pos(ed.buffer());
899 let start_row = cursor.row;
900 let cur_len = buf_line_chars(ed.buffer(), start_row);
903 let insert_col = if before {
904 cursor.col
905 } else if cur_len == 0 {
906 0
907 } else {
908 cursor.col + 1
909 };
910 let segments: Vec<&str> = yank.split('\n').collect();
911 let last_row = start_row + segments.len().saturating_sub(1);
917 let raw_rows = buf_row_count(ed.buffer());
918 let trailing_nl = raw_rows > 1 && buf_line_chars(ed.buffer(), raw_rows - 1) == 0;
924 let content_rows = if trailing_nl { raw_rows - 1 } else { raw_rows };
925 if last_row >= content_rows {
926 let anchor = content_rows.saturating_sub(1);
927 let at = Position::new(anchor, buf_line_chars(ed.buffer(), anchor));
928 ed.mutate_edit(Edit::InsertStr {
929 at,
930 text: "\n".repeat(last_row + 1 - content_rows),
931 });
932 }
933 let chunks: Vec<String> = segments
937 .iter()
938 .enumerate()
939 .map(|(i, seg)| {
940 let tail_is_empty = buf_line_chars(ed.buffer(), start_row + i) <= insert_col;
943 if tail_is_empty {
944 return seg.repeat(count);
945 }
946 let seg_len = seg.chars().count();
947 let mut padded = String::with_capacity(seg.len() + width.saturating_sub(seg_len));
948 padded.push_str(seg);
949 if seg_len < width {
950 padded.extend(std::iter::repeat_n(' ', width - seg_len));
951 }
952 padded.repeat(count)
953 })
954 .collect();
955 ed.mutate_edit(Edit::InsertBlock {
956 at: Position::new(start_row, insert_col),
957 chunks,
958 });
959 ed.jump_cursor(start_row, insert_col);
961 let bot_row = start_row + segments.len().saturating_sub(1);
963 ed.set_mark('[', (start_row, insert_col));
964 ed.set_mark(']', (bot_row, insert_col));
965 ed.set_sticky_col(Some(insert_col));
966}
967pub fn visual_paste<H: hjkl_engine::types::Host>(
972 ed: &mut Editor<hjkl_buffer::View, H>,
973 before: bool,
974) {
975 use hjkl_buffer::{Edit, Position};
976 ed.sync_buffer_content_from_textarea();
977
978 let selector = vim_mut(ed).pending_register.take();
981 sync_clipboard_register_for(ed, selector);
984 let (reg_text, reg_linewise, reg_blockwise, reg_block_width) =
985 ed.with_registers(|regs| match selector.and_then(|c| regs.read(c)) {
986 Some(slot) => (
987 slot.text.clone(),
988 slot.linewise,
989 slot.blockwise,
990 slot.block_width,
991 ),
992 None => (
993 regs.unnamed.text.clone(),
994 regs.unnamed.linewise,
995 regs.unnamed.blockwise,
996 regs.unnamed.block_width,
997 ),
998 });
999 let saved_unnamed = before.then(|| ed.with_registers(|regs| regs.unnamed.clone()));
1001
1002 let mode = vim(ed).mode;
1003 ed.push_undo();
1004
1005 match mode {
1006 Mode::VisualLine => {
1007 let cursor_row = buf_cursor_pos(ed.buffer()).row;
1008 let top = cursor_row.min(vim(ed).visual_line_anchor);
1009 let bot = cursor_row.max(vim(ed).visual_line_anchor);
1010 cut_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
1012 let text = reg_text.trim_matches('\n').to_string();
1014 let line_count = buf_row_count(ed.buffer());
1015 if top >= line_count {
1016 let last = line_count.saturating_sub(1);
1019 let lc = buf_line_chars(ed.buffer(), last);
1020 ed.mutate_edit(Edit::InsertStr {
1021 at: Position::new(last, lc),
1022 text: format!("\n{text}"),
1023 });
1024 buf_set_cursor_rc(ed.buffer_mut(), last + 1, 0);
1025 } else {
1026 ed.mutate_edit(Edit::InsertStr {
1027 at: Position::new(top, 0),
1028 text: format!("{text}\n"),
1029 });
1030 buf_set_cursor_rc(ed.buffer_mut(), top, 0);
1031 }
1032 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
1033 }
1034 Mode::Visual => {
1035 let anchor = vim(ed).visual_anchor;
1036 let cursor = ed.cursor();
1037 let (top, bot) = order(anchor, cursor);
1038 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
1040 if reg_linewise {
1042 let text = reg_text.trim_matches('\n').to_string();
1044 let lc = buf_line_chars(ed.buffer(), top.0);
1045 ed.mutate_edit(Edit::InsertStr {
1046 at: Position::new(top.0, lc),
1047 text: format!("\n{text}"),
1048 });
1049 buf_set_cursor_rc(ed.buffer_mut(), top.0 + 1, 0);
1050 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
1051 } else {
1052 ed.mutate_edit(Edit::InsertStr {
1053 at: Position::new(top.0, top.1),
1054 text: reg_text.clone(),
1055 });
1056 let inserted_len = reg_text.chars().count();
1058 let last_col = top.1 + inserted_len.saturating_sub(1);
1059 buf_set_cursor_rc(ed.buffer_mut(), top.0, last_col);
1060 }
1061 }
1062 Mode::VisualBlock => {
1063 let (top, bot, left, right) = block_bounds(ed);
1075 let to_eol = vim(ed).block_to_eol;
1076 let deleted = block_yank(ed, top, bot, left, right, to_eol);
1078 let del_width = if to_eol {
1079 deleted
1080 .split('\n')
1081 .map(|s| s.chars().count())
1082 .max()
1083 .unwrap_or(0)
1084 } else {
1085 right + 1 - left
1086 };
1087 delete_block_contents(ed, top, bot, left, right, to_eol);
1088 if !before && !deleted.is_empty() {
1092 ed.record_yank_to_host(deleted.clone());
1093 ed.record_delete_block(deleted, del_width, None);
1094 }
1095 if reg_blockwise {
1096 ed.jump_cursor(top, left);
1097 do_block_paste(ed, true, 1, reg_block_width, ®_text);
1100 } else if reg_linewise {
1101 let text = reg_text.trim_matches('\n').to_string();
1102 let lc = buf_line_chars(ed.buffer(), bot);
1103 ed.mutate_edit(Edit::InsertStr {
1104 at: Position::new(bot, lc),
1105 text: format!("\n{text}"),
1106 });
1107 buf_set_cursor_rc(ed.buffer_mut(), bot + 1, 0);
1108 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
1109 } else if reg_text.contains('\n') {
1110 ed.mutate_edit(Edit::InsertStr {
1111 at: Position::new(top, left),
1112 text: reg_text.clone(),
1113 });
1114 buf_set_cursor_rc(ed.buffer_mut(), top, left);
1115 } else {
1116 for r in top..=bot {
1122 let line_len = buf_line_chars(ed.buffer(), r);
1123 if left > line_len {
1124 continue;
1125 }
1126 ed.mutate_edit(Edit::InsertStr {
1127 at: Position::new(r, left),
1128 text: reg_text.clone(),
1129 });
1130 }
1131 let last_col = left + reg_text.chars().count().saturating_sub(1);
1132 buf_set_cursor_rc(ed.buffer_mut(), top, last_col);
1133 }
1134 }
1135 _ => {}
1136 }
1137
1138 if let Some(slot) = saved_unnamed {
1140 ed.with_registers_mut(|regs| regs.unnamed = slot);
1141 }
1142 vim_mut(ed).mode = Mode::Normal;
1143 ed.set_sticky_col(Some(buf_cursor_pos(ed.buffer()).col));
1144}
1145pub fn adjust_number_visual<H: hjkl_engine::types::Host>(
1150 ed: &mut Editor<hjkl_buffer::View, H>,
1151 delta: i64,
1152 sequential: bool,
1153) {
1154 use hjkl_buffer::{Edit, MotionKind, Position};
1155 ed.sync_buffer_content_from_textarea();
1156 let mode = vim(ed).mode;
1157 let cursor = buf_cursor_pos(ed.buffer());
1158
1159 let (top, bot, mut scan_col_first, block_left) = match mode {
1161 Mode::VisualLine => {
1162 let t = cursor.row.min(vim(ed).visual_line_anchor);
1163 let b = cursor.row.max(vim(ed).visual_line_anchor);
1164 (t, b, 0usize, None)
1165 }
1166 Mode::Visual => {
1167 let (a, c) = order(vim(ed).visual_anchor, (cursor.row, cursor.col));
1168 (a.0, c.0, a.1, None)
1169 }
1170 Mode::VisualBlock => {
1171 let (a, c) = order(vim(ed).block_anchor, (cursor.row, cursor.col));
1172 let left = a.1.min(c.1);
1173 (a.0, c.0, left, Some(left))
1174 }
1175 _ => return,
1176 };
1177
1178 ed.push_undo();
1179 let mut found_count: i64 = 0;
1180 for row in top..=bot {
1181 let start_col = match block_left {
1182 Some(left) => left,
1183 None => {
1184 let c = if row == top { scan_col_first } else { 0 };
1187 scan_col_first = 0;
1188 c
1189 }
1190 };
1191 let chars: Vec<char> = match buf_line(ed.buffer(), row) {
1192 Some(l) => l.chars().collect(),
1193 None => continue,
1194 };
1195 let this_delta = if sequential {
1201 delta.saturating_mul(found_count.saturating_add(1))
1202 } else {
1203 delta
1204 };
1205 let Some(AdjustedNumber {
1206 start: span_start,
1207 end: span_end,
1208 text: new_s,
1209 }) = adjusted_number_at(&chars, start_col, this_delta)
1210 else {
1211 continue;
1212 };
1213 found_count += 1;
1214 let span_start_pos = Position::new(row, span_start);
1215 let span_end_pos = Position::new(row, span_end);
1216 ed.mutate_edit(Edit::DeleteRange {
1217 start: span_start_pos,
1218 end: span_end_pos,
1219 kind: MotionKind::Char,
1220 });
1221 ed.mutate_edit(Edit::InsertStr {
1222 at: span_start_pos,
1223 text: new_s,
1224 });
1225 }
1226 buf_set_cursor_rc(ed.buffer_mut(), top, block_left.unwrap_or(0));
1228 vim_mut(ed).mode = Mode::Normal;
1229 ed.set_sticky_col(Some(buf_cursor_pos(ed.buffer()).col));
1230}
1231#[cfg(test)]
1232mod replace_char_tests {
1233 use hjkl_buffer::{View, rope_line_str};
1234 use hjkl_engine::{DefaultHost, Editor, Options};
1235
1236 fn line(ed: &Editor<View, DefaultHost>, row: usize) -> String {
1237 rope_line_str(&ed.buffer().rope(), row)
1238 }
1239
1240 #[test]
1241 fn replace_char_count_exceeding_line_replaces_nothing() {
1242 let buf = View::from_str("ab\ncd");
1243 let mut ed = crate::vim::vim_editor(buf, DefaultHost::new(), Options::default());
1244 super::replace_char(&mut ed, 'x', 3);
1247 assert_eq!(line(&ed, 0), "ab", "partial replace must not happen");
1248 assert_eq!(line(&ed, 1), "cd", "must not spill onto the next line");
1249 }
1250
1251 #[test]
1252 fn replace_char_count_fitting_replaces_run() {
1253 let buf = View::from_str("abc");
1254 let mut ed = crate::vim::vim_editor(buf, DefaultHost::new(), Options::default());
1255 super::replace_char(&mut ed, 'x', 2);
1256 assert_eq!(line(&ed, 0), "xxc");
1257 }
1258}
1259#[cfg(test)]
1260mod g_ampersand_tests {
1261 use super::*;
1262 use hjkl_buffer::{View, rope_line_str};
1263 use hjkl_engine::{DefaultHost, Editor, Options};
1264
1265 fn make_editor(content: &str) -> Editor<View, DefaultHost> {
1266 let buf = View::from_str(content);
1267 let host = DefaultHost::new();
1268 crate::vim::vim_editor(buf, host, Options::default())
1269 }
1270
1271 fn buf_line(ed: &Editor<View, DefaultHost>, row: usize) -> String {
1272 let rope = ed.buffer().rope();
1273 rope_line_str(&rope, row).trim_end_matches('\n').to_string()
1274 }
1275
1276 #[test]
1279 fn g_ampersand_repeats_last_substitute_on_whole_buffer() {
1280 let mut ed = make_editor("foo\nfoo bar foo\nbaz");
1281 let cmd = hjkl_engine::substitute::parse_substitute("/foo/bar/").unwrap();
1283 ed.set_last_substitute(cmd);
1284 apply_after_g(&mut ed, '&', 1);
1286 assert_eq!(buf_line(&ed, 0), "bar");
1287 assert_eq!(buf_line(&ed, 1), "bar bar foo");
1289 assert_eq!(buf_line(&ed, 2), "baz");
1290 }
1291
1292 #[test]
1294 fn g_ampersand_with_g_flag_replaces_all_per_line() {
1295 let mut ed = make_editor("foo foo\nfoo");
1296 let cmd = hjkl_engine::substitute::parse_substitute("/foo/bar/g").unwrap();
1297 ed.set_last_substitute(cmd);
1298 apply_after_g(&mut ed, '&', 1);
1299 assert_eq!(buf_line(&ed, 0), "bar bar");
1300 assert_eq!(buf_line(&ed, 1), "bar");
1301 }
1302
1303 #[test]
1305 fn g_ampersand_noop_when_no_prior_substitute() {
1306 let mut ed = make_editor("foo\nbar");
1307 apply_after_g(&mut ed, '&', 1);
1309 assert_eq!(buf_line(&ed, 0), "foo");
1310 assert_eq!(buf_line(&ed, 1), "bar");
1311 }
1312}