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 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
80 let (top, bot) = order(start, end);
81 ed.sync_buffer_content_from_textarea();
82 let (buf_start, buf_end, buf_kind) = match kind {
83 RangeKind::Linewise => (
84 Position::new(top.0, 0),
85 Position::new(bot.0, 0),
86 BufKind::Line,
87 ),
88 RangeKind::Inclusive => {
89 let line_chars = buf_line_chars(ed.buffer(), bot.0);
90 let next = if bot.1 < line_chars {
94 Position::new(bot.0, bot.1 + 1)
95 } else if bot.0 + 1 < buf_row_count(ed.buffer()) {
96 Position::new(bot.0 + 1, 0)
97 } else {
98 Position::new(bot.0, line_chars)
99 };
100 (Position::new(top.0, top.1), next, BufKind::Char)
101 }
102 RangeKind::Exclusive => (
103 Position::new(top.0, top.1),
104 Position::new(bot.0, bot.1),
105 BufKind::Char,
106 ),
107 };
108 let inverse = ed.mutate_edit(Edit::DeleteRange {
109 start: buf_start,
110 end: buf_end,
111 kind: buf_kind,
112 });
113 let raw_text = match inverse {
114 Edit::InsertStr { text, .. } => text,
115 _ => String::new(),
116 };
117 let text = if matches!(kind, RangeKind::Linewise) {
122 if raw_text.ends_with('\n') {
123 raw_text
125 } else if raw_text.starts_with('\n') {
126 format!("{}\n", raw_text.strip_prefix('\n').unwrap())
129 } else if raw_text.is_empty() {
130 return String::new();
133 } else {
134 format!("{raw_text}\n")
136 }
137 } else {
138 raw_text
139 };
140 if text.is_empty() {
141 return text;
143 }
144 ed.record_yank_to_host(text.clone());
145 let target = vim_mut(ed).pending_register.take();
146 ed.record_delete(text.clone(), matches!(kind, RangeKind::Linewise), target);
147 text
148}
149pub fn delete_to_eol<H: hjkl_engine::types::Host>(ed: &mut Editor<hjkl_buffer::View, H>) {
155 use hjkl_buffer::{Edit, MotionKind, Position};
156 ed.sync_buffer_content_from_textarea();
157 let cursor = buf_cursor_pos(ed.buffer());
158 let line_chars = buf_line_chars(ed.buffer(), cursor.row);
159 if cursor.col >= line_chars {
160 return;
161 }
162 let inverse = ed.mutate_edit(Edit::DeleteRange {
163 start: cursor,
164 end: Position::new(cursor.row, line_chars),
165 kind: MotionKind::Char,
166 });
167 if let Edit::InsertStr { text, .. } = inverse
168 && !text.is_empty()
169 {
170 ed.record_yank_to_host(text.clone());
171 ed.set_yank_linewise(false);
172 let target = vim_mut(ed).pending_register.take();
176 ed.record_delete(text, false, target);
177 }
178 buf_set_cursor_pos(ed.buffer_mut(), cursor);
179}
180pub fn do_char_delete<H: hjkl_engine::types::Host>(
181 ed: &mut Editor<hjkl_buffer::View, H>,
182 forward: bool,
183 count: usize,
184) {
185 use hjkl_buffer::{Edit, MotionKind, Position};
186 ed.push_undo();
187 ed.sync_buffer_content_from_textarea();
188 let mut deleted = String::new();
191 for _ in 0..count {
192 let cursor = buf_cursor_pos(ed.buffer());
193 let line_chars = buf_line_chars(ed.buffer(), cursor.row);
194 if forward {
195 if cursor.col >= line_chars {
201 break;
202 }
203 let inverse = ed.mutate_edit(Edit::DeleteRange {
204 start: cursor,
205 end: Position::new(cursor.row, cursor.col + 1),
206 kind: MotionKind::Char,
207 });
208 if let Edit::InsertStr { text, .. } = inverse {
209 deleted.push_str(&text);
210 }
211 } else {
212 if cursor.col == 0 {
215 break;
216 }
217 let inverse = ed.mutate_edit(Edit::DeleteRange {
218 start: Position::new(cursor.row, cursor.col - 1),
219 end: cursor,
220 kind: MotionKind::Char,
221 });
222 if let Edit::InsertStr { text, .. } = inverse {
223 deleted = text + &deleted;
226 }
227 }
228 }
229 if !deleted.is_empty() {
230 ed.record_yank_to_host(deleted.clone());
231 let target = vim_mut(ed).pending_register.take();
232 ed.record_delete(deleted, false, target);
233 }
234 let cursor = buf_cursor_pos(ed.buffer());
237 let line_chars = buf_line_chars(ed.buffer(), cursor.row);
238 if line_chars > 0 && cursor.col >= line_chars {
239 buf_set_cursor_pos(ed.buffer_mut(), Position::new(cursor.row, line_chars - 1));
240 }
241}
242struct AdjustedNumber {
247 start: usize,
248 end: usize,
249 text: String,
250}
251
252fn adjusted_number_at(chars: &[char], from: usize, delta: i64) -> Option<AdjustedNumber> {
263 let len = chars.len();
264 let is_hex_prefix = |i: usize| {
265 chars[i] == '0'
266 && matches!(chars.get(i + 1), Some('x' | 'X'))
267 && chars.get(i + 2).is_some_and(|c| c.is_ascii_hexdigit())
268 };
269 let start = (from.min(len)..len).find(|&i| is_hex_prefix(i) || chars[i].is_ascii_digit())?;
270
271 if is_hex_prefix(start) {
272 let digits_start = start + 2;
274 let mut digits_end = digits_start;
275 while digits_end < len && chars[digits_end].is_ascii_hexdigit() {
276 digits_end += 1;
277 }
278 let digits: String = chars[digits_start..digits_end].iter().collect();
279 let n = u64::from_str_radix(&digits, 16).ok()?;
280 let new_val = (n as i128 + delta as i128).max(0) as u64;
281 let width = digits_end - digits_start;
282 let prefix: String = chars[start..digits_start].iter().collect();
283 let upper = digits
288 .chars()
289 .rev()
290 .find(|c| c.is_ascii_alphabetic())
291 .map_or(chars[start + 1] == 'X', |c| c.is_ascii_uppercase());
292 let text = if upper {
293 format!("{prefix}{new_val:0width$X}")
294 } else {
295 format!("{prefix}{new_val:0width$x}")
296 };
297 return Some(AdjustedNumber {
298 start,
299 end: digits_end,
300 text,
301 });
302 }
303
304 let span_start = if start > 0 && chars[start - 1] == '-' {
307 start - 1
308 } else {
309 start
310 };
311 let mut span_end = start;
312 while span_end < len && chars[span_end].is_ascii_digit() {
313 span_end += 1;
314 }
315 let s: String = chars[span_start..span_end].iter().collect();
316 let n = s.parse::<i64>().ok()?;
317 let new_val = n as i128 + delta as i128;
318 let digits: String = chars[start..span_end].iter().collect();
324 let width = digits.len();
325 let text = if width > 1 && digits.starts_with('0') {
326 if new_val < 0 {
327 let mag = new_val.unsigned_abs();
328 format!("-{mag:0width$}")
329 } else {
330 format!("{new_val:0width$}")
331 }
332 } else {
333 new_val.to_string()
334 };
335 Some(AdjustedNumber {
336 start: span_start,
337 end: span_end,
338 text,
339 })
340}
341
342pub fn adjust_number<H: hjkl_engine::types::Host>(
348 ed: &mut Editor<hjkl_buffer::View, H>,
349 delta: i64,
350) -> bool {
351 use hjkl_buffer::{Edit, MotionKind, Position};
352 ed.sync_buffer_content_from_textarea();
353 let cursor = buf_cursor_pos(ed.buffer());
354 let row = cursor.row;
355 let chars: Vec<char> = match buf_line(ed.buffer(), row) {
356 Some(l) => l.chars().collect(),
357 None => return false,
358 };
359 let Some(AdjustedNumber {
360 start: span_start,
361 end: span_end,
362 text: new_s,
363 }) = adjusted_number_at(&chars, cursor.col, delta)
364 else {
365 return false;
366 };
367
368 ed.push_undo();
369 let span_start_pos = Position::new(row, span_start);
370 let span_end_pos = Position::new(row, span_end);
371 ed.mutate_edit(Edit::DeleteRange {
372 start: span_start_pos,
373 end: span_end_pos,
374 kind: MotionKind::Char,
375 });
376 ed.mutate_edit(Edit::InsertStr {
377 at: span_start_pos,
378 text: new_s.clone(),
379 });
380 let new_len = new_s.chars().count();
381 buf_set_cursor_rc(ed.buffer_mut(), row, span_start + new_len.saturating_sub(1));
382 true
383}
384pub fn replace_char<H: hjkl_engine::types::Host>(
385 ed: &mut Editor<hjkl_buffer::View, H>,
386 ch: char,
387 count: usize,
388) {
389 use hjkl_buffer::{Edit, MotionKind, Position};
390 ed.sync_buffer_content_from_textarea();
391 let start = buf_cursor_pos(ed.buffer());
395 let start_line_chars = buf_line_chars(ed.buffer(), start.row);
396 if count == 0 || start.col + count > start_line_chars {
397 return;
398 }
399 ed.push_undo();
400 for _ in 0..count {
401 let cursor = buf_cursor_pos(ed.buffer());
402 let line_chars = buf_line_chars(ed.buffer(), cursor.row);
403 if cursor.col >= line_chars {
404 break;
405 }
406 ed.mutate_edit(Edit::DeleteRange {
407 start: cursor,
408 end: Position::new(cursor.row, cursor.col + 1),
409 kind: MotionKind::Char,
410 });
411 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
412 }
413 hjkl_engine::motions::move_left(ed.buffer_mut(), 1);
415}
416pub fn toggle_case_at_cursor<H: hjkl_engine::types::Host>(
420 ed: &mut Editor<hjkl_buffer::View, H>,
421) -> bool {
422 use hjkl_buffer::{Edit, MotionKind, Position};
423 ed.sync_buffer_content_from_textarea();
424 let cursor = buf_cursor_pos(ed.buffer());
425 let Some(c) = buf_line(ed.buffer(), cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
426 return false;
427 };
428 let toggled = if c.is_uppercase() {
429 c.to_lowercase().collect::<String>()
430 } else {
431 c.to_uppercase().collect::<String>()
432 };
433 ed.mutate_edit(Edit::DeleteRange {
434 start: cursor,
435 end: Position::new(cursor.row, cursor.col + 1),
436 kind: MotionKind::Char,
437 });
438 ed.mutate_edit(Edit::InsertStr {
439 at: cursor,
440 text: toggled,
441 });
442 true
443}
444pub fn join_line<H: hjkl_engine::types::Host>(ed: &mut Editor<hjkl_buffer::View, H>) -> bool {
447 use hjkl_buffer::{Edit, Position};
448 ed.sync_buffer_content_from_textarea();
449 let row = buf_cursor_pos(ed.buffer()).row;
450 let n_rows = buf_row_count(ed.buffer());
451 if row + 1 >= n_rows {
452 return false;
453 }
454 if row + 2 >= n_rows && buf_line(ed.buffer(), row + 1).is_some_and(|s| s.is_empty()) {
458 return false;
459 }
460 let cur_line = buf_line(ed.buffer(), row).unwrap_or_default();
461 let next_raw = buf_line(ed.buffer(), row + 1).unwrap_or_default();
462 let next_trimmed = next_raw.trim_start();
463 let cur_chars = cur_line.chars().count();
464 let next_chars = next_raw.chars().count();
465 let separator = if !cur_line.is_empty()
468 && !next_trimmed.is_empty()
469 && !cur_line.ends_with([' ', '\t'])
470 && !next_trimmed.starts_with(')')
471 {
472 " "
473 } else {
474 ""
475 };
476 let joined = format!("{cur_line}{separator}{next_trimmed}");
477 ed.mutate_edit(Edit::Replace {
478 start: Position::new(row, 0),
479 end: Position::new(row + 1, next_chars),
480 with: joined,
481 });
482 buf_set_cursor_rc(ed.buffer_mut(), row, cur_chars);
486 true
487}
488pub fn join_line_raw<H: hjkl_engine::types::Host>(ed: &mut Editor<hjkl_buffer::View, H>) -> bool {
492 use hjkl_buffer::Edit;
493 ed.sync_buffer_content_from_textarea();
494 let row = buf_cursor_pos(ed.buffer()).row;
495 let n_rows = buf_row_count(ed.buffer());
496 if row + 1 >= n_rows {
497 return false;
498 }
499 if row + 2 >= n_rows && buf_line(ed.buffer(), row + 1).is_some_and(|s| s.is_empty()) {
501 return false;
502 }
503 let join_col = buf_line_chars(ed.buffer(), row);
504 ed.mutate_edit(Edit::JoinLines {
505 row,
506 count: 1,
507 with_space: false,
508 });
509 buf_set_cursor_rc(ed.buffer_mut(), row, join_col);
511 true
512}
513pub fn visual_join<H: hjkl_engine::types::Host>(
517 ed: &mut Editor<hjkl_buffer::View, H>,
518 with_space: bool,
519) {
520 let cursor_row = buf_cursor_pos(ed.buffer()).row;
521 let (top, bot) = match vim(ed).mode {
522 Mode::VisualLine => (
523 cursor_row.min(vim(ed).visual_line_anchor),
524 cursor_row.max(vim(ed).visual_line_anchor),
525 ),
526 Mode::VisualBlock => {
527 let a = vim(ed).block_anchor.0;
528 (a.min(cursor_row), a.max(cursor_row))
529 }
530 Mode::Visual => {
531 let a = vim(ed).visual_anchor.0;
532 (a.min(cursor_row), a.max(cursor_row))
533 }
534 _ => return,
535 };
536 let joins = (bot - top).max(1);
539 ed.push_undo();
540 buf_set_cursor_rc(ed.buffer_mut(), top, 0);
541 for _ in 0..joins {
542 let joined = if with_space {
543 join_line(ed)
544 } else {
545 join_line_raw(ed)
546 };
547 if !joined {
548 break;
549 }
550 }
551 if !vim(ed).replaying {
557 vim_mut(ed).last_change = Some(LastChange::JoinLine { count: joins });
558 }
559 vim_mut(ed).mode = Mode::Normal;
560 ed.set_sticky_col(Some(buf_cursor_pos(ed.buffer()).col));
561}
562pub fn goto_percent<H: hjkl_engine::types::Host>(
565 ed: &mut Editor<hjkl_buffer::View, H>,
566 count: usize,
567) {
568 let rows = buf_row_count(ed.buffer());
569 if rows == 0 {
570 return;
571 }
572 let total = if rows >= 2 && buf_line(ed.buffer(), rows - 1).is_some_and(|s| s.is_empty()) {
575 rows - 1
576 } else {
577 rows
578 };
579 let line = count.saturating_mul(total).div_ceil(100).clamp(1, total);
583 let pre = ed.cursor();
584 ed.jump_cursor(line - 1, 0);
585 move_first_non_whitespace(ed);
586 ed.set_sticky_col(Some(ed.cursor().1));
587 if ed.cursor() != pre {
588 ed.push_jump(pre);
589 }
590}
591pub fn indent_width(s: &str, tabstop: usize) -> usize {
594 let ts = tabstop.max(1);
595 let mut w = 0usize;
596 for c in s.chars() {
597 match c {
598 ' ' => w += 1,
599 '\t' => w += ts - (w % ts),
600 _ => break,
601 }
602 }
603 w
604}
605pub fn build_indent(width: usize, settings: &hjkl_engine::Settings) -> String {
608 if settings.expandtab {
609 return " ".repeat(width);
610 }
611 let ts = settings.tabstop.max(1);
612 let tabs = width / ts;
613 let spaces = width % ts;
614 format!("{}{}", "\t".repeat(tabs), " ".repeat(spaces))
615}
616pub fn reindent_block(text: &str, target_width: usize, settings: &hjkl_engine::Settings) -> String {
619 let ts = settings.tabstop.max(1);
620 let lines: Vec<&str> = text.split('\n').collect();
621 let first_width = lines.first().map_or(0, |l| indent_width(l, ts));
622 let delta = target_width as isize - first_width as isize;
623 lines
624 .iter()
625 .map(|line| {
626 let trimmed = line.trim_start_matches([' ', '\t']);
627 if trimmed.is_empty() {
628 return String::new();
630 }
631 let old_w = indent_width(line, ts) as isize;
632 let new_w = (old_w + delta).max(0) as usize;
633 format!("{}{}", build_indent(new_w, settings), trimmed)
634 })
635 .collect::<Vec<_>>()
636 .join("\n")
637}
638pub const MAX_PASTE_BYTES: usize = 64 * 1024 * 1024;
655
656fn reject_oversized_paste<H: hjkl_engine::types::Host>(
659 ed: &mut Editor<hjkl_buffer::View, H>,
660 bytes: usize,
661) {
662 ed.push_error(format!("E342: Out of memory! (allocating {bytes} bytes)"));
663}
664
665pub fn do_paste<H: hjkl_engine::types::Host>(
666 ed: &mut Editor<hjkl_buffer::View, H>,
667 before: bool,
668 count: usize,
669 cursor_after: bool,
670 reindent: bool,
671) -> bool {
672 use hjkl_buffer::{Edit, Position};
673 let selector = vim_mut(ed).pending_register.take();
678 sync_clipboard_register_for(ed, selector);
682 let (yank, linewise, blockwise, block_width) =
683 ed.with_registers(|regs| match selector.and_then(|c| regs.read(c)) {
684 Some(slot) => (
685 slot.text.clone(),
686 slot.linewise,
687 slot.blockwise,
688 slot.block_width,
689 ),
690 None => (
696 regs.unnamed.text.clone(),
697 regs.unnamed.linewise,
698 regs.unnamed.blockwise,
699 regs.unnamed.block_width,
700 ),
701 });
702 let original_row_for_linewise_after = if linewise && !before {
711 let r = buf_cursor_pos(ed.buffer()).row;
714 let (_, fold_end) = expand_linewise_over_closed_folds(ed.buffer(), r, r);
715 Some(fold_end)
716 } else {
717 None
718 };
719 if yank.is_empty() {
722 return false;
723 }
724 let Some(requested_bytes) = yank.len().checked_mul(count) else {
732 reject_oversized_paste(ed, yank.len().saturating_mul(count));
733 return false;
734 };
735 if requested_bytes > MAX_PASTE_BYTES {
736 reject_oversized_paste(ed, requested_bytes);
737 return false;
738 }
739 if blockwise {
740 let Some(block_bytes) = block_width
741 .checked_mul(yank.split('\n').count())
742 .and_then(|bytes| bytes.checked_mul(count))
743 else {
744 reject_oversized_paste(ed, usize::MAX);
745 return false;
746 };
747 let Some(total_bytes) = requested_bytes.checked_add(block_bytes) else {
748 reject_oversized_paste(ed, usize::MAX);
749 return false;
750 };
751 if total_bytes > MAX_PASTE_BYTES {
752 reject_oversized_paste(ed, total_bytes);
753 return false;
754 }
755 }
756 ed.push_undo();
757 if blockwise {
761 do_block_paste(ed, before, count, block_width, &yank);
762 return true;
763 }
764 let paste_mark = if linewise {
768 ed.sync_buffer_content_from_textarea();
769 let row = buf_cursor_pos(ed.buffer()).row;
770 let mut text = yank.trim_matches('\n').to_string();
771 if reindent {
772 let cur_line = buf_line(ed.buffer(), row).unwrap_or_default();
773 let target_w = indent_width(&cur_line, ed.settings().tabstop.max(1));
774 text = reindent_block(&text, target_w, ed.settings());
775 }
776 let (fold_start, fold_end) = expand_linewise_over_closed_folds(ed.buffer(), row, row);
777 let payload = std::iter::repeat_n(text.as_str(), count)
778 .collect::<Vec<_>>()
779 .join("\n");
780 let target_row = if before {
781 ed.mutate_edit(Edit::InsertStr {
782 at: Position::new(fold_start, 0),
783 text: format!("{payload}\n"),
784 });
785 fold_start
786 } else {
787 let line_chars = buf_line_chars(ed.buffer(), fold_end);
788 ed.mutate_edit(Edit::InsertStr {
789 at: Position::new(fold_end, line_chars),
790 text: format!("\n{payload}"),
791 });
792 fold_end + 1
793 };
794 buf_set_cursor_rc(ed.buffer_mut(), target_row, 0);
795 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
796 let payload_lines = payload.lines().count().max(1);
797 let bot_row = target_row + payload_lines - 1;
798 let bot_last_col = buf_line_chars(ed.buffer(), bot_row).saturating_sub(1);
799 ((target_row, 0), (bot_row, bot_last_col))
800 } else {
801 ed.sync_buffer_content_from_textarea();
802 let cursor = buf_cursor_pos(ed.buffer());
806 let at = if before {
807 cursor
808 } else {
809 let line_chars = buf_line_chars(ed.buffer(), cursor.row);
810 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
811 };
812 let repeated = yank.repeat(count);
813 ed.mutate_edit(Edit::InsertStr { at, text: repeated });
814 if !cursor_after && ed.cursor().1 > 0 {
819 hjkl_engine::motions::move_left(ed.buffer_mut(), 1);
820 }
821 let lo = (at.row, at.col);
823 let hi = if cursor_after {
824 let c = ed.cursor();
825 (c.0, c.1.saturating_sub(1))
826 } else {
827 ed.cursor()
828 };
829 (lo, hi)
830 };
831 ed.set_mark('[', paste_mark.0);
832 ed.set_mark(']', paste_mark.1);
833 if cursor_after && linewise {
836 let bot_row = paste_mark.1.0;
837 let last_row = buf_row_count(ed.buffer()).saturating_sub(1);
838 let target = (bot_row + 1).min(last_row);
839 buf_set_cursor_rc(ed.buffer_mut(), target, 0);
840 } else if let Some(orig_row) = original_row_for_linewise_after {
841 let first_target = orig_row.saturating_add(1);
846 buf_set_cursor_rc(ed.buffer_mut(), first_target, 0);
847 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
848 }
849 ed.set_sticky_col(Some(buf_cursor_pos(ed.buffer()).col));
851 true
852}
853fn do_block_paste<H: hjkl_engine::types::Host>(
871 ed: &mut Editor<hjkl_buffer::View, H>,
872 before: bool,
873 count: usize,
874 width: usize,
875 yank: &str,
876) {
877 use hjkl_buffer::{Edit, Position};
878 ed.sync_buffer_content_from_textarea();
879 let cursor = buf_cursor_pos(ed.buffer());
880 let start_row = cursor.row;
881 let cur_len = buf_line_chars(ed.buffer(), start_row);
884 let insert_col = if before {
885 cursor.col
886 } else if cur_len == 0 {
887 0
888 } else {
889 cursor.col + 1
890 };
891 let segments: Vec<&str> = yank.split('\n').collect();
892 let last_row = start_row + segments.len().saturating_sub(1);
898 let raw_rows = buf_row_count(ed.buffer());
899 let trailing_nl = raw_rows > 1 && buf_line_chars(ed.buffer(), raw_rows - 1) == 0;
905 let content_rows = if trailing_nl { raw_rows - 1 } else { raw_rows };
906 if last_row >= content_rows {
907 let anchor = content_rows.saturating_sub(1);
908 let at = Position::new(anchor, buf_line_chars(ed.buffer(), anchor));
909 ed.mutate_edit(Edit::InsertStr {
910 at,
911 text: "\n".repeat(last_row + 1 - content_rows),
912 });
913 }
914 let chunks: Vec<String> = segments
918 .iter()
919 .enumerate()
920 .map(|(i, seg)| {
921 let tail_is_empty = buf_line_chars(ed.buffer(), start_row + i) <= insert_col;
924 if tail_is_empty {
925 return seg.repeat(count);
926 }
927 let seg_len = seg.chars().count();
928 let mut padded = String::with_capacity(seg.len() + width.saturating_sub(seg_len));
929 padded.push_str(seg);
930 if seg_len < width {
931 padded.extend(std::iter::repeat_n(' ', width - seg_len));
932 }
933 padded.repeat(count)
934 })
935 .collect();
936 ed.mutate_edit(Edit::InsertBlock {
937 at: Position::new(start_row, insert_col),
938 chunks,
939 });
940 ed.jump_cursor(start_row, insert_col);
942 let bot_row = start_row + segments.len().saturating_sub(1);
944 ed.set_mark('[', (start_row, insert_col));
945 ed.set_mark(']', (bot_row, insert_col));
946 ed.set_sticky_col(Some(insert_col));
947}
948pub fn visual_paste<H: hjkl_engine::types::Host>(
953 ed: &mut Editor<hjkl_buffer::View, H>,
954 before: bool,
955) {
956 use hjkl_buffer::{Edit, Position};
957 ed.sync_buffer_content_from_textarea();
958
959 let selector = vim_mut(ed).pending_register.take();
962 sync_clipboard_register_for(ed, selector);
965 let (reg_text, reg_linewise, reg_blockwise, reg_block_width) =
966 ed.with_registers(|regs| match selector.and_then(|c| regs.read(c)) {
967 Some(slot) => (
968 slot.text.clone(),
969 slot.linewise,
970 slot.blockwise,
971 slot.block_width,
972 ),
973 None => (
974 regs.unnamed.text.clone(),
975 regs.unnamed.linewise,
976 regs.unnamed.blockwise,
977 regs.unnamed.block_width,
978 ),
979 });
980 let saved_unnamed = before.then(|| ed.with_registers(|regs| regs.unnamed.clone()));
982
983 let mode = vim(ed).mode;
984 ed.push_undo();
985
986 match mode {
987 Mode::VisualLine => {
988 let cursor_row = buf_cursor_pos(ed.buffer()).row;
989 let top = cursor_row.min(vim(ed).visual_line_anchor);
990 let bot = cursor_row.max(vim(ed).visual_line_anchor);
991 cut_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
993 let text = reg_text.trim_matches('\n').to_string();
995 let line_count = buf_row_count(ed.buffer());
996 if top >= line_count {
997 let last = line_count.saturating_sub(1);
1000 let lc = buf_line_chars(ed.buffer(), last);
1001 ed.mutate_edit(Edit::InsertStr {
1002 at: Position::new(last, lc),
1003 text: format!("\n{text}"),
1004 });
1005 buf_set_cursor_rc(ed.buffer_mut(), last + 1, 0);
1006 } else {
1007 ed.mutate_edit(Edit::InsertStr {
1008 at: Position::new(top, 0),
1009 text: format!("{text}\n"),
1010 });
1011 buf_set_cursor_rc(ed.buffer_mut(), top, 0);
1012 }
1013 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
1014 }
1015 Mode::Visual => {
1016 let anchor = vim(ed).visual_anchor;
1017 let cursor = ed.cursor();
1018 let (top, bot) = order(anchor, cursor);
1019 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
1021 if reg_linewise {
1023 let text = reg_text.trim_matches('\n').to_string();
1025 let lc = buf_line_chars(ed.buffer(), top.0);
1026 ed.mutate_edit(Edit::InsertStr {
1027 at: Position::new(top.0, lc),
1028 text: format!("\n{text}"),
1029 });
1030 buf_set_cursor_rc(ed.buffer_mut(), top.0 + 1, 0);
1031 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
1032 } else {
1033 ed.mutate_edit(Edit::InsertStr {
1034 at: Position::new(top.0, top.1),
1035 text: reg_text.clone(),
1036 });
1037 let inserted_len = reg_text.chars().count();
1039 let last_col = top.1 + inserted_len.saturating_sub(1);
1040 buf_set_cursor_rc(ed.buffer_mut(), top.0, last_col);
1041 }
1042 }
1043 Mode::VisualBlock => {
1044 let (top, bot, left, right) = block_bounds(ed);
1056 let to_eol = vim(ed).block_to_eol;
1057 let deleted = block_yank(ed, top, bot, left, right, to_eol);
1059 let del_width = if to_eol {
1060 deleted
1061 .split('\n')
1062 .map(|s| s.chars().count())
1063 .max()
1064 .unwrap_or(0)
1065 } else {
1066 right + 1 - left
1067 };
1068 delete_block_contents(ed, top, bot, left, right, to_eol);
1069 if !before && !deleted.is_empty() {
1073 ed.record_yank_to_host(deleted.clone());
1074 ed.record_delete_block(deleted, del_width, None);
1075 }
1076 if reg_blockwise {
1077 ed.jump_cursor(top, left);
1078 do_block_paste(ed, true, 1, reg_block_width, ®_text);
1081 } else if reg_linewise {
1082 let text = reg_text.trim_matches('\n').to_string();
1083 let lc = buf_line_chars(ed.buffer(), bot);
1084 ed.mutate_edit(Edit::InsertStr {
1085 at: Position::new(bot, lc),
1086 text: format!("\n{text}"),
1087 });
1088 buf_set_cursor_rc(ed.buffer_mut(), bot + 1, 0);
1089 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
1090 } else if reg_text.contains('\n') {
1091 ed.mutate_edit(Edit::InsertStr {
1092 at: Position::new(top, left),
1093 text: reg_text.clone(),
1094 });
1095 buf_set_cursor_rc(ed.buffer_mut(), top, left);
1096 } else {
1097 for r in top..=bot {
1103 let line_len = buf_line_chars(ed.buffer(), r);
1104 if left > line_len {
1105 continue;
1106 }
1107 ed.mutate_edit(Edit::InsertStr {
1108 at: Position::new(r, left),
1109 text: reg_text.clone(),
1110 });
1111 }
1112 let last_col = left + reg_text.chars().count().saturating_sub(1);
1113 buf_set_cursor_rc(ed.buffer_mut(), top, last_col);
1114 }
1115 }
1116 _ => {}
1117 }
1118
1119 if let Some(slot) = saved_unnamed {
1121 ed.with_registers_mut(|regs| regs.unnamed = slot);
1122 }
1123 vim_mut(ed).mode = Mode::Normal;
1124 ed.set_sticky_col(Some(buf_cursor_pos(ed.buffer()).col));
1125}
1126pub fn adjust_number_visual<H: hjkl_engine::types::Host>(
1131 ed: &mut Editor<hjkl_buffer::View, H>,
1132 delta: i64,
1133 sequential: bool,
1134) {
1135 use hjkl_buffer::{Edit, MotionKind, Position};
1136 ed.sync_buffer_content_from_textarea();
1137 let mode = vim(ed).mode;
1138 let cursor = buf_cursor_pos(ed.buffer());
1139
1140 let (top, bot, mut scan_col_first, block_left) = match mode {
1142 Mode::VisualLine => {
1143 let t = cursor.row.min(vim(ed).visual_line_anchor);
1144 let b = cursor.row.max(vim(ed).visual_line_anchor);
1145 (t, b, 0usize, None)
1146 }
1147 Mode::Visual => {
1148 let (a, c) = order(vim(ed).visual_anchor, (cursor.row, cursor.col));
1149 (a.0, c.0, a.1, None)
1150 }
1151 Mode::VisualBlock => {
1152 let (a, c) = order(vim(ed).block_anchor, (cursor.row, cursor.col));
1153 let left = a.1.min(c.1);
1154 (a.0, c.0, left, Some(left))
1155 }
1156 _ => return,
1157 };
1158
1159 ed.push_undo();
1160 let mut found_count: i64 = 0;
1161 for row in top..=bot {
1162 let start_col = match block_left {
1163 Some(left) => left,
1164 None => {
1165 let c = if row == top { scan_col_first } else { 0 };
1168 scan_col_first = 0;
1169 c
1170 }
1171 };
1172 let chars: Vec<char> = match buf_line(ed.buffer(), row) {
1173 Some(l) => l.chars().collect(),
1174 None => continue,
1175 };
1176 let this_delta = if sequential {
1182 delta.saturating_mul(found_count.saturating_add(1))
1183 } else {
1184 delta
1185 };
1186 let Some(AdjustedNumber {
1187 start: span_start,
1188 end: span_end,
1189 text: new_s,
1190 }) = adjusted_number_at(&chars, start_col, this_delta)
1191 else {
1192 continue;
1193 };
1194 found_count += 1;
1195 let span_start_pos = Position::new(row, span_start);
1196 let span_end_pos = Position::new(row, span_end);
1197 ed.mutate_edit(Edit::DeleteRange {
1198 start: span_start_pos,
1199 end: span_end_pos,
1200 kind: MotionKind::Char,
1201 });
1202 ed.mutate_edit(Edit::InsertStr {
1203 at: span_start_pos,
1204 text: new_s,
1205 });
1206 }
1207 buf_set_cursor_rc(ed.buffer_mut(), top, block_left.unwrap_or(0));
1209 vim_mut(ed).mode = Mode::Normal;
1210 ed.set_sticky_col(Some(buf_cursor_pos(ed.buffer()).col));
1211}
1212#[cfg(test)]
1213mod replace_char_tests {
1214 use hjkl_buffer::{View, rope_line_str};
1215 use hjkl_engine::{DefaultHost, Editor, Options};
1216
1217 fn line(ed: &Editor<View, DefaultHost>, row: usize) -> String {
1218 rope_line_str(&ed.buffer().rope(), row)
1219 }
1220
1221 #[test]
1222 fn replace_char_count_exceeding_line_replaces_nothing() {
1223 let buf = View::from_str("ab\ncd");
1224 let mut ed = crate::vim::vim_editor(buf, DefaultHost::new(), Options::default());
1225 super::replace_char(&mut ed, 'x', 3);
1228 assert_eq!(line(&ed, 0), "ab", "partial replace must not happen");
1229 assert_eq!(line(&ed, 1), "cd", "must not spill onto the next line");
1230 }
1231
1232 #[test]
1233 fn replace_char_count_fitting_replaces_run() {
1234 let buf = View::from_str("abc");
1235 let mut ed = crate::vim::vim_editor(buf, DefaultHost::new(), Options::default());
1236 super::replace_char(&mut ed, 'x', 2);
1237 assert_eq!(line(&ed, 0), "xxc");
1238 }
1239}
1240#[cfg(test)]
1241mod g_ampersand_tests {
1242 use super::*;
1243 use hjkl_buffer::{View, rope_line_str};
1244 use hjkl_engine::{DefaultHost, Editor, Options};
1245
1246 fn make_editor(content: &str) -> Editor<View, DefaultHost> {
1247 let buf = View::from_str(content);
1248 let host = DefaultHost::new();
1249 crate::vim::vim_editor(buf, host, Options::default())
1250 }
1251
1252 fn buf_line(ed: &Editor<View, DefaultHost>, row: usize) -> String {
1253 let rope = ed.buffer().rope();
1254 rope_line_str(&rope, row).trim_end_matches('\n').to_string()
1255 }
1256
1257 #[test]
1260 fn g_ampersand_repeats_last_substitute_on_whole_buffer() {
1261 let mut ed = make_editor("foo\nfoo bar foo\nbaz");
1262 let cmd = hjkl_engine::substitute::parse_substitute("/foo/bar/").unwrap();
1264 ed.set_last_substitute(cmd);
1265 apply_after_g(&mut ed, '&', 1);
1267 assert_eq!(buf_line(&ed, 0), "bar");
1268 assert_eq!(buf_line(&ed, 1), "bar bar foo");
1270 assert_eq!(buf_line(&ed, 2), "baz");
1271 }
1272
1273 #[test]
1275 fn g_ampersand_with_g_flag_replaces_all_per_line() {
1276 let mut ed = make_editor("foo foo\nfoo");
1277 let cmd = hjkl_engine::substitute::parse_substitute("/foo/bar/g").unwrap();
1278 ed.set_last_substitute(cmd);
1279 apply_after_g(&mut ed, '&', 1);
1280 assert_eq!(buf_line(&ed, 0), "bar bar");
1281 assert_eq!(buf_line(&ed, 1), "bar");
1282 }
1283
1284 #[test]
1286 fn g_ampersand_noop_when_no_prior_substitute() {
1287 let mut ed = make_editor("foo\nbar");
1288 apply_after_g(&mut ed, '&', 1);
1290 assert_eq!(buf_line(&ed, 0), "foo");
1291 assert_eq!(buf_line(&ed, 1), "bar");
1292 }
1293}