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 if forward {
211 let (cursor_row, cursor_col) = ed.cursor();
212 let (fold_start, fold_end) =
213 expand_linewise_over_closed_folds(ed.buffer(), cursor_row, cursor_row);
214 if cursor_col == 0 && (fold_start, fold_end) != (cursor_row, cursor_row) {
215 run_operator_over_range(
216 ed,
217 Operator::Delete,
218 (fold_start, 0),
219 (fold_end, 0),
220 RangeKind::Linewise,
221 );
222 return;
223 }
224 }
225 ed.push_undo();
226 ed.sync_buffer_content_from_textarea();
227 let mut deleted = String::new();
230 for _ in 0..count {
231 let cursor = buf_cursor_pos(ed.buffer());
232 let line_chars = buf_line_chars(ed.buffer(), cursor.row);
233 if forward {
234 if cursor.col >= line_chars {
240 break;
241 }
242 let inverse = ed.mutate_edit(Edit::DeleteRange {
243 start: cursor,
244 end: Position::new(cursor.row, cursor.col + 1),
245 kind: MotionKind::Char,
246 });
247 if let Edit::InsertStr { text, .. } = inverse {
248 deleted.push_str(&text);
249 }
250 } else {
251 if cursor.col == 0 {
254 break;
255 }
256 let inverse = ed.mutate_edit(Edit::DeleteRange {
257 start: Position::new(cursor.row, cursor.col - 1),
258 end: cursor,
259 kind: MotionKind::Char,
260 });
261 if let Edit::InsertStr { text, .. } = inverse {
262 deleted = text + &deleted;
265 }
266 }
267 }
268 if !deleted.is_empty() {
269 ed.record_yank_to_host(deleted.clone());
270 let target = vim_mut(ed).pending_register.take();
271 ed.record_delete(deleted, false, target);
272 }
273 let cursor = buf_cursor_pos(ed.buffer());
276 let line_chars = buf_line_chars(ed.buffer(), cursor.row);
277 if line_chars > 0 && cursor.col >= line_chars {
278 buf_set_cursor_pos(ed.buffer_mut(), Position::new(cursor.row, line_chars - 1));
279 }
280}
281struct AdjustedNumber {
286 start: usize,
287 end: usize,
288 text: String,
289}
290
291fn adjusted_number_at(chars: &[char], from: usize, delta: i64) -> Option<AdjustedNumber> {
302 let len = chars.len();
303 let is_hex_prefix = |i: usize| {
304 chars[i] == '0'
305 && matches!(chars.get(i + 1), Some('x' | 'X'))
306 && chars.get(i + 2).is_some_and(|c| c.is_ascii_hexdigit())
307 };
308 let start = (from.min(len)..len).find(|&i| is_hex_prefix(i) || chars[i].is_ascii_digit())?;
309
310 if is_hex_prefix(start) {
311 let digits_start = start + 2;
313 let mut digits_end = digits_start;
314 while digits_end < len && chars[digits_end].is_ascii_hexdigit() {
315 digits_end += 1;
316 }
317 let digits: String = chars[digits_start..digits_end].iter().collect();
318 let n = u64::from_str_radix(&digits, 16).ok()?;
319 let new_val = (n as i128 + delta as i128).max(0) as u64;
320 let width = digits_end - digits_start;
321 let prefix: String = chars[start..digits_start].iter().collect();
322 let upper = digits
327 .chars()
328 .rev()
329 .find(|c| c.is_ascii_alphabetic())
330 .map_or(chars[start + 1] == 'X', |c| c.is_ascii_uppercase());
331 let text = if upper {
332 format!("{prefix}{new_val:0width$X}")
333 } else {
334 format!("{prefix}{new_val:0width$x}")
335 };
336 return Some(AdjustedNumber {
337 start,
338 end: digits_end,
339 text,
340 });
341 }
342
343 let span_start = if start > 0 && chars[start - 1] == '-' {
346 start - 1
347 } else {
348 start
349 };
350 let mut span_end = start;
351 while span_end < len && chars[span_end].is_ascii_digit() {
352 span_end += 1;
353 }
354 let s: String = chars[span_start..span_end].iter().collect();
355 let n = s.parse::<i64>().ok()?;
356 let new_val = n as i128 + delta as i128;
357 let digits: String = chars[start..span_end].iter().collect();
363 let width = digits.len();
364 let text = if width > 1 && digits.starts_with('0') {
365 if new_val < 0 {
366 let mag = new_val.unsigned_abs();
367 format!("-{mag:0width$}")
368 } else {
369 format!("{new_val:0width$}")
370 }
371 } else {
372 new_val.to_string()
373 };
374 Some(AdjustedNumber {
375 start: span_start,
376 end: span_end,
377 text,
378 })
379}
380
381pub fn adjust_number<H: hjkl_engine::types::Host>(
387 ed: &mut Editor<hjkl_buffer::View, H>,
388 delta: i64,
389) -> bool {
390 use hjkl_buffer::{Edit, MotionKind, Position};
391 ed.sync_buffer_content_from_textarea();
392 let cursor = buf_cursor_pos(ed.buffer());
393 let row = cursor.row;
394 let chars: Vec<char> = match buf_line(ed.buffer(), row) {
395 Some(l) => l.chars().collect(),
396 None => return false,
397 };
398 let Some(AdjustedNumber {
399 start: span_start,
400 end: span_end,
401 text: new_s,
402 }) = adjusted_number_at(&chars, cursor.col, delta)
403 else {
404 return false;
405 };
406
407 ed.push_undo();
408 let span_start_pos = Position::new(row, span_start);
409 let span_end_pos = Position::new(row, span_end);
410 ed.mutate_edit(Edit::DeleteRange {
411 start: span_start_pos,
412 end: span_end_pos,
413 kind: MotionKind::Char,
414 });
415 ed.mutate_edit(Edit::InsertStr {
416 at: span_start_pos,
417 text: new_s.clone(),
418 });
419 let new_len = new_s.chars().count();
420 buf_set_cursor_rc(ed.buffer_mut(), row, span_start + new_len.saturating_sub(1));
421 true
422}
423pub fn replace_char<H: hjkl_engine::types::Host>(
424 ed: &mut Editor<hjkl_buffer::View, H>,
425 ch: char,
426 count: usize,
427) {
428 use hjkl_buffer::{Edit, MotionKind, Position};
429 ed.sync_buffer_content_from_textarea();
430 let start = buf_cursor_pos(ed.buffer());
434 let start_line_chars = buf_line_chars(ed.buffer(), start.row);
435 if count == 0 || start.col + count > start_line_chars {
436 return;
437 }
438 ed.push_undo();
439 for _ in 0..count {
440 let cursor = buf_cursor_pos(ed.buffer());
441 let line_chars = buf_line_chars(ed.buffer(), cursor.row);
442 if cursor.col >= line_chars {
443 break;
444 }
445 ed.mutate_edit(Edit::DeleteRange {
446 start: cursor,
447 end: Position::new(cursor.row, cursor.col + 1),
448 kind: MotionKind::Char,
449 });
450 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
451 }
452 hjkl_engine::motions::move_left(ed.buffer_mut(), 1);
454}
455pub fn toggle_case_at_cursor<H: hjkl_engine::types::Host>(
459 ed: &mut Editor<hjkl_buffer::View, H>,
460) -> bool {
461 use hjkl_buffer::{Edit, MotionKind, Position};
462 ed.sync_buffer_content_from_textarea();
463 let cursor = buf_cursor_pos(ed.buffer());
464 let Some(c) = buf_line(ed.buffer(), cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
465 return false;
466 };
467 let toggled = if c.is_uppercase() {
468 c.to_lowercase().collect::<String>()
469 } else {
470 c.to_uppercase().collect::<String>()
471 };
472 ed.mutate_edit(Edit::DeleteRange {
473 start: cursor,
474 end: Position::new(cursor.row, cursor.col + 1),
475 kind: MotionKind::Char,
476 });
477 ed.mutate_edit(Edit::InsertStr {
478 at: cursor,
479 text: toggled,
480 });
481 true
482}
483pub fn join_line<H: hjkl_engine::types::Host>(ed: &mut Editor<hjkl_buffer::View, H>) -> bool {
486 use hjkl_buffer::{Edit, Position};
487 ed.sync_buffer_content_from_textarea();
488 let row = buf_cursor_pos(ed.buffer()).row;
489 let n_rows = buf_row_count(ed.buffer());
490 if row + 1 >= n_rows {
491 return false;
492 }
493 if row + 2 >= n_rows && buf_line(ed.buffer(), row + 1).is_some_and(|s| s.is_empty()) {
497 return false;
498 }
499 let cur_line = buf_line(ed.buffer(), row).unwrap_or_default();
500 let next_raw = buf_line(ed.buffer(), row + 1).unwrap_or_default();
501 let next_trimmed = next_raw.trim_start();
502 let cur_chars = cur_line.chars().count();
503 let next_chars = next_raw.chars().count();
504 let separator = if !cur_line.is_empty()
507 && !next_trimmed.is_empty()
508 && !cur_line.ends_with([' ', '\t'])
509 && !next_trimmed.starts_with(')')
510 {
511 " "
512 } else {
513 ""
514 };
515 let joined = format!("{cur_line}{separator}{next_trimmed}");
516 ed.mutate_edit(Edit::Replace {
517 start: Position::new(row, 0),
518 end: Position::new(row + 1, next_chars),
519 with: joined,
520 });
521 buf_set_cursor_rc(ed.buffer_mut(), row, cur_chars);
525 true
526}
527pub fn join_line_raw<H: hjkl_engine::types::Host>(ed: &mut Editor<hjkl_buffer::View, H>) -> bool {
531 use hjkl_buffer::Edit;
532 ed.sync_buffer_content_from_textarea();
533 let row = buf_cursor_pos(ed.buffer()).row;
534 let n_rows = buf_row_count(ed.buffer());
535 if row + 1 >= n_rows {
536 return false;
537 }
538 if row + 2 >= n_rows && buf_line(ed.buffer(), row + 1).is_some_and(|s| s.is_empty()) {
540 return false;
541 }
542 let join_col = buf_line_chars(ed.buffer(), row);
543 ed.mutate_edit(Edit::JoinLines {
544 row,
545 count: 1,
546 with_space: false,
547 });
548 buf_set_cursor_rc(ed.buffer_mut(), row, join_col);
550 true
551}
552pub fn visual_join<H: hjkl_engine::types::Host>(
556 ed: &mut Editor<hjkl_buffer::View, H>,
557 with_space: bool,
558) {
559 let cursor_row = buf_cursor_pos(ed.buffer()).row;
560 let (top, bot) = match vim(ed).mode {
561 Mode::VisualLine => (
562 cursor_row.min(vim(ed).visual_line_anchor),
563 cursor_row.max(vim(ed).visual_line_anchor),
564 ),
565 Mode::VisualBlock => {
566 let a = vim(ed).block_anchor.0;
567 (a.min(cursor_row), a.max(cursor_row))
568 }
569 Mode::Visual => {
570 let a = vim(ed).visual_anchor.0;
571 (a.min(cursor_row), a.max(cursor_row))
572 }
573 _ => return,
574 };
575 let joins = (bot - top).max(1);
578 ed.push_undo();
579 buf_set_cursor_rc(ed.buffer_mut(), top, 0);
580 for _ in 0..joins {
581 let joined = if with_space {
582 join_line(ed)
583 } else {
584 join_line_raw(ed)
585 };
586 if !joined {
587 break;
588 }
589 }
590 if !vim(ed).replaying {
596 vim_mut(ed).last_change = Some(LastChange::JoinLine { count: joins });
597 }
598 vim_mut(ed).mode = Mode::Normal;
599 ed.set_sticky_col(Some(buf_cursor_pos(ed.buffer()).col));
600}
601pub fn goto_percent<H: hjkl_engine::types::Host>(
604 ed: &mut Editor<hjkl_buffer::View, H>,
605 count: usize,
606) {
607 let rows = buf_row_count(ed.buffer());
608 if rows == 0 {
609 return;
610 }
611 let total = if rows >= 2 && buf_line(ed.buffer(), rows - 1).is_some_and(|s| s.is_empty()) {
614 rows - 1
615 } else {
616 rows
617 };
618 let line = count.saturating_mul(total).div_ceil(100).clamp(1, total);
622 let pre = ed.cursor();
623 ed.jump_cursor(line - 1, 0);
624 move_first_non_whitespace(ed);
625 ed.set_sticky_col(Some(ed.cursor().1));
626 if ed.cursor() != pre {
627 ed.push_jump(pre);
628 }
629}
630pub fn indent_width(s: &str, tabstop: usize) -> usize {
633 let ts = tabstop.max(1);
634 let mut w = 0usize;
635 for c in s.chars() {
636 match c {
637 ' ' => w += 1,
638 '\t' => w += ts - (w % ts),
639 _ => break,
640 }
641 }
642 w
643}
644pub fn build_indent(width: usize, settings: &hjkl_engine::Settings) -> String {
647 if settings.expandtab {
648 return " ".repeat(width);
649 }
650 let ts = settings.tabstop.max(1);
651 let tabs = width / ts;
652 let spaces = width % ts;
653 format!("{}{}", "\t".repeat(tabs), " ".repeat(spaces))
654}
655pub fn reindent_block(text: &str, target_width: usize, settings: &hjkl_engine::Settings) -> String {
658 let ts = settings.tabstop.max(1);
659 let lines: Vec<&str> = text.split('\n').collect();
660 let first_width = lines.first().map_or(0, |l| indent_width(l, ts));
661 let delta = target_width as isize - first_width as isize;
662 lines
663 .iter()
664 .map(|line| {
665 let trimmed = line.trim_start_matches([' ', '\t']);
666 if trimmed.is_empty() {
667 return String::new();
669 }
670 let old_w = indent_width(line, ts) as isize;
671 let new_w = (old_w + delta).max(0) as usize;
672 format!("{}{}", build_indent(new_w, settings), trimmed)
673 })
674 .collect::<Vec<_>>()
675 .join("\n")
676}
677pub const MAX_PASTE_BYTES: usize = 64 * 1024 * 1024;
694
695fn reject_oversized_paste<H: hjkl_engine::types::Host>(
698 ed: &mut Editor<hjkl_buffer::View, H>,
699 bytes: usize,
700) {
701 ed.push_error(format!("E342: Out of memory! (allocating {bytes} bytes)"));
702}
703
704pub fn do_paste<H: hjkl_engine::types::Host>(
705 ed: &mut Editor<hjkl_buffer::View, H>,
706 before: bool,
707 count: usize,
708 cursor_after: bool,
709 reindent: bool,
710) -> bool {
711 use hjkl_buffer::{Edit, Position};
712 let selector = vim_mut(ed).pending_register.take();
717 sync_clipboard_register_for(ed, selector);
721 let (yank, linewise, blockwise, block_width) =
722 ed.with_registers(|regs| match selector.and_then(|c| regs.read(c)) {
723 Some(slot) => (
724 slot.text.clone(),
725 slot.linewise,
726 slot.blockwise,
727 slot.block_width,
728 ),
729 None => (
735 regs.unnamed.text.clone(),
736 regs.unnamed.linewise,
737 regs.unnamed.blockwise,
738 regs.unnamed.block_width,
739 ),
740 });
741 let original_row_for_linewise_after = if linewise && !before {
750 let r = buf_cursor_pos(ed.buffer()).row;
753 let (_, fold_end) = expand_linewise_over_closed_folds(ed.buffer(), r, r);
754 Some(fold_end)
755 } else {
756 None
757 };
758 if yank.is_empty() {
761 return false;
762 }
763 let Some(requested_bytes) = yank.len().checked_mul(count) else {
771 reject_oversized_paste(ed, yank.len().saturating_mul(count));
772 return false;
773 };
774 if requested_bytes > MAX_PASTE_BYTES {
775 reject_oversized_paste(ed, requested_bytes);
776 return false;
777 }
778 if blockwise {
779 let Some(block_bytes) = block_width
780 .checked_mul(yank.split('\n').count())
781 .and_then(|bytes| bytes.checked_mul(count))
782 else {
783 reject_oversized_paste(ed, usize::MAX);
784 return false;
785 };
786 let Some(total_bytes) = requested_bytes.checked_add(block_bytes) else {
787 reject_oversized_paste(ed, usize::MAX);
788 return false;
789 };
790 if total_bytes > MAX_PASTE_BYTES {
791 reject_oversized_paste(ed, total_bytes);
792 return false;
793 }
794 }
795 ed.push_undo();
796 if blockwise {
800 do_block_paste(ed, before, count, block_width, &yank);
801 return true;
802 }
803 let paste_mark = if linewise {
807 ed.sync_buffer_content_from_textarea();
808 let row = buf_cursor_pos(ed.buffer()).row;
809 let mut text = yank.trim_matches('\n').to_string();
810 if reindent {
811 let cur_line = buf_line(ed.buffer(), row).unwrap_or_default();
812 let target_w = indent_width(&cur_line, ed.settings().tabstop.max(1));
813 text = reindent_block(&text, target_w, ed.settings());
814 }
815 let (fold_start, fold_end) = expand_linewise_over_closed_folds(ed.buffer(), row, row);
816 let payload = std::iter::repeat_n(text.as_str(), count)
817 .collect::<Vec<_>>()
818 .join("\n");
819 let target_row = if before {
820 ed.mutate_edit(Edit::InsertStr {
821 at: Position::new(fold_start, 0),
822 text: format!("{payload}\n"),
823 });
824 fold_start
825 } else {
826 let line_chars = buf_line_chars(ed.buffer(), fold_end);
827 ed.mutate_edit(Edit::InsertStr {
828 at: Position::new(fold_end, line_chars),
829 text: format!("\n{payload}"),
830 });
831 fold_end + 1
832 };
833 buf_set_cursor_rc(ed.buffer_mut(), target_row, 0);
834 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
835 let payload_lines = payload.lines().count().max(1);
836 let bot_row = target_row + payload_lines - 1;
837 let bot_last_col = buf_line_chars(ed.buffer(), bot_row).saturating_sub(1);
838 ((target_row, 0), (bot_row, bot_last_col))
839 } else {
840 ed.sync_buffer_content_from_textarea();
841 let cursor = buf_cursor_pos(ed.buffer());
845 let at = if before {
846 cursor
847 } else {
848 let line_chars = buf_line_chars(ed.buffer(), cursor.row);
849 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
850 };
851 let repeated = yank.repeat(count);
852 ed.mutate_edit(Edit::InsertStr { at, text: repeated });
853 if !cursor_after && ed.cursor().1 > 0 {
858 hjkl_engine::motions::move_left(ed.buffer_mut(), 1);
859 }
860 let lo = (at.row, at.col);
862 let hi = if cursor_after {
863 let c = ed.cursor();
864 (c.0, c.1.saturating_sub(1))
865 } else {
866 ed.cursor()
867 };
868 (lo, hi)
869 };
870 ed.set_mark('[', paste_mark.0);
871 ed.set_mark(']', paste_mark.1);
872 if cursor_after && linewise {
875 let bot_row = paste_mark.1.0;
876 let last_row = buf_row_count(ed.buffer()).saturating_sub(1);
877 let target = (bot_row + 1).min(last_row);
878 buf_set_cursor_rc(ed.buffer_mut(), target, 0);
879 } else if let Some(orig_row) = original_row_for_linewise_after {
880 let first_target = orig_row.saturating_add(1);
885 buf_set_cursor_rc(ed.buffer_mut(), first_target, 0);
886 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
887 }
888 ed.set_sticky_col(Some(buf_cursor_pos(ed.buffer()).col));
890 true
891}
892fn do_block_paste<H: hjkl_engine::types::Host>(
910 ed: &mut Editor<hjkl_buffer::View, H>,
911 before: bool,
912 count: usize,
913 width: usize,
914 yank: &str,
915) {
916 use hjkl_buffer::{Edit, Position};
917 ed.sync_buffer_content_from_textarea();
918 let cursor = buf_cursor_pos(ed.buffer());
919 let start_row = cursor.row;
920 let cur_len = buf_line_chars(ed.buffer(), start_row);
923 let insert_col = if before {
924 cursor.col
925 } else if cur_len == 0 {
926 0
927 } else {
928 cursor.col + 1
929 };
930 let segments: Vec<&str> = yank.split('\n').collect();
931 let last_row = start_row + segments.len().saturating_sub(1);
937 let raw_rows = buf_row_count(ed.buffer());
938 let trailing_nl = raw_rows > 1 && buf_line_chars(ed.buffer(), raw_rows - 1) == 0;
944 let content_rows = if trailing_nl { raw_rows - 1 } else { raw_rows };
945 if last_row >= content_rows {
946 let anchor = content_rows.saturating_sub(1);
947 let at = Position::new(anchor, buf_line_chars(ed.buffer(), anchor));
948 ed.mutate_edit(Edit::InsertStr {
949 at,
950 text: "\n".repeat(last_row + 1 - content_rows),
951 });
952 }
953 let chunks: Vec<String> = segments
957 .iter()
958 .enumerate()
959 .map(|(i, seg)| {
960 let tail_is_empty = buf_line_chars(ed.buffer(), start_row + i) <= insert_col;
963 if tail_is_empty {
964 return seg.repeat(count);
965 }
966 let seg_len = seg.chars().count();
967 let mut padded = String::with_capacity(seg.len() + width.saturating_sub(seg_len));
968 padded.push_str(seg);
969 if seg_len < width {
970 padded.extend(std::iter::repeat_n(' ', width - seg_len));
971 }
972 padded.repeat(count)
973 })
974 .collect();
975 ed.mutate_edit(Edit::InsertBlock {
976 at: Position::new(start_row, insert_col),
977 chunks,
978 });
979 ed.jump_cursor(start_row, insert_col);
981 let bot_row = start_row + segments.len().saturating_sub(1);
983 ed.set_mark('[', (start_row, insert_col));
984 ed.set_mark(']', (bot_row, insert_col));
985 ed.set_sticky_col(Some(insert_col));
986}
987pub fn visual_paste<H: hjkl_engine::types::Host>(
992 ed: &mut Editor<hjkl_buffer::View, H>,
993 before: bool,
994) {
995 use hjkl_buffer::{Edit, Position};
996 ed.sync_buffer_content_from_textarea();
997
998 let selector = vim_mut(ed).pending_register.take();
1001 sync_clipboard_register_for(ed, selector);
1004 let (reg_text, reg_linewise, reg_blockwise, reg_block_width) =
1005 ed.with_registers(|regs| match selector.and_then(|c| regs.read(c)) {
1006 Some(slot) => (
1007 slot.text.clone(),
1008 slot.linewise,
1009 slot.blockwise,
1010 slot.block_width,
1011 ),
1012 None => (
1013 regs.unnamed.text.clone(),
1014 regs.unnamed.linewise,
1015 regs.unnamed.blockwise,
1016 regs.unnamed.block_width,
1017 ),
1018 });
1019 let saved_unnamed = before.then(|| ed.with_registers(|regs| regs.unnamed.clone()));
1021
1022 let mode = vim(ed).mode;
1023 ed.push_undo();
1024
1025 match mode {
1026 Mode::VisualLine => {
1027 let cursor_row = buf_cursor_pos(ed.buffer()).row;
1028 let top = cursor_row.min(vim(ed).visual_line_anchor);
1029 let bot = cursor_row.max(vim(ed).visual_line_anchor);
1030 cut_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
1032 let text = reg_text.trim_matches('\n').to_string();
1034 let line_count = buf_row_count(ed.buffer());
1035 if top >= line_count {
1036 let last = line_count.saturating_sub(1);
1039 let lc = buf_line_chars(ed.buffer(), last);
1040 ed.mutate_edit(Edit::InsertStr {
1041 at: Position::new(last, lc),
1042 text: format!("\n{text}"),
1043 });
1044 buf_set_cursor_rc(ed.buffer_mut(), last + 1, 0);
1045 } else {
1046 ed.mutate_edit(Edit::InsertStr {
1047 at: Position::new(top, 0),
1048 text: format!("{text}\n"),
1049 });
1050 buf_set_cursor_rc(ed.buffer_mut(), top, 0);
1051 }
1052 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
1053 }
1054 Mode::Visual => {
1055 let anchor = vim(ed).visual_anchor;
1056 let cursor = ed.cursor();
1057 let (top, bot) = order(anchor, cursor);
1058 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
1060 if reg_linewise {
1062 let text = reg_text.trim_matches('\n').to_string();
1064 let lc = buf_line_chars(ed.buffer(), top.0);
1065 ed.mutate_edit(Edit::InsertStr {
1066 at: Position::new(top.0, lc),
1067 text: format!("\n{text}"),
1068 });
1069 buf_set_cursor_rc(ed.buffer_mut(), top.0 + 1, 0);
1070 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
1071 } else {
1072 ed.mutate_edit(Edit::InsertStr {
1073 at: Position::new(top.0, top.1),
1074 text: reg_text.clone(),
1075 });
1076 let inserted_len = reg_text.chars().count();
1078 let last_col = top.1 + inserted_len.saturating_sub(1);
1079 buf_set_cursor_rc(ed.buffer_mut(), top.0, last_col);
1080 }
1081 }
1082 Mode::VisualBlock => {
1083 let (top, bot, left, right) = block_bounds(ed);
1095 let to_eol = vim(ed).block_to_eol;
1096 let deleted = block_yank(ed, top, bot, left, right, to_eol);
1098 let del_width = if to_eol {
1099 deleted
1100 .split('\n')
1101 .map(|s| s.chars().count())
1102 .max()
1103 .unwrap_or(0)
1104 } else {
1105 right + 1 - left
1106 };
1107 delete_block_contents(ed, top, bot, left, right, to_eol);
1108 if !before && !deleted.is_empty() {
1112 ed.record_yank_to_host(deleted.clone());
1113 ed.record_delete_block(deleted, del_width, None);
1114 }
1115 if reg_blockwise {
1116 ed.jump_cursor(top, left);
1117 do_block_paste(ed, true, 1, reg_block_width, ®_text);
1120 } else if reg_linewise {
1121 let text = reg_text.trim_matches('\n').to_string();
1122 let lc = buf_line_chars(ed.buffer(), bot);
1123 ed.mutate_edit(Edit::InsertStr {
1124 at: Position::new(bot, lc),
1125 text: format!("\n{text}"),
1126 });
1127 buf_set_cursor_rc(ed.buffer_mut(), bot + 1, 0);
1128 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
1129 } else if reg_text.contains('\n') {
1130 ed.mutate_edit(Edit::InsertStr {
1131 at: Position::new(top, left),
1132 text: reg_text.clone(),
1133 });
1134 buf_set_cursor_rc(ed.buffer_mut(), top, left);
1135 } else {
1136 for r in top..=bot {
1142 let line_len = buf_line_chars(ed.buffer(), r);
1143 if left > line_len {
1144 continue;
1145 }
1146 ed.mutate_edit(Edit::InsertStr {
1147 at: Position::new(r, left),
1148 text: reg_text.clone(),
1149 });
1150 }
1151 let last_col = left + reg_text.chars().count().saturating_sub(1);
1152 buf_set_cursor_rc(ed.buffer_mut(), top, last_col);
1153 }
1154 }
1155 _ => {}
1156 }
1157
1158 if let Some(slot) = saved_unnamed {
1160 ed.with_registers_mut(|regs| regs.unnamed = slot);
1161 }
1162 vim_mut(ed).mode = Mode::Normal;
1163 ed.set_sticky_col(Some(buf_cursor_pos(ed.buffer()).col));
1164}
1165pub fn adjust_number_visual<H: hjkl_engine::types::Host>(
1170 ed: &mut Editor<hjkl_buffer::View, H>,
1171 delta: i64,
1172 sequential: bool,
1173) {
1174 use hjkl_buffer::{Edit, MotionKind, Position};
1175 ed.sync_buffer_content_from_textarea();
1176 let mode = vim(ed).mode;
1177 let cursor = buf_cursor_pos(ed.buffer());
1178
1179 let (top, bot, mut scan_col_first, block_left) = match mode {
1181 Mode::VisualLine => {
1182 let t = cursor.row.min(vim(ed).visual_line_anchor);
1183 let b = cursor.row.max(vim(ed).visual_line_anchor);
1184 (t, b, 0usize, None)
1185 }
1186 Mode::Visual => {
1187 let (a, c) = order(vim(ed).visual_anchor, (cursor.row, cursor.col));
1188 (a.0, c.0, a.1, None)
1189 }
1190 Mode::VisualBlock => {
1191 let (a, c) = order(vim(ed).block_anchor, (cursor.row, cursor.col));
1192 let left = a.1.min(c.1);
1193 (a.0, c.0, left, Some(left))
1194 }
1195 _ => return,
1196 };
1197
1198 ed.push_undo();
1199 let mut found_count: i64 = 0;
1200 for row in top..=bot {
1201 let start_col = match block_left {
1202 Some(left) => left,
1203 None => {
1204 let c = if row == top { scan_col_first } else { 0 };
1207 scan_col_first = 0;
1208 c
1209 }
1210 };
1211 let chars: Vec<char> = match buf_line(ed.buffer(), row) {
1212 Some(l) => l.chars().collect(),
1213 None => continue,
1214 };
1215 let this_delta = if sequential {
1221 delta.saturating_mul(found_count.saturating_add(1))
1222 } else {
1223 delta
1224 };
1225 let Some(AdjustedNumber {
1226 start: span_start,
1227 end: span_end,
1228 text: new_s,
1229 }) = adjusted_number_at(&chars, start_col, this_delta)
1230 else {
1231 continue;
1232 };
1233 found_count += 1;
1234 let span_start_pos = Position::new(row, span_start);
1235 let span_end_pos = Position::new(row, span_end);
1236 ed.mutate_edit(Edit::DeleteRange {
1237 start: span_start_pos,
1238 end: span_end_pos,
1239 kind: MotionKind::Char,
1240 });
1241 ed.mutate_edit(Edit::InsertStr {
1242 at: span_start_pos,
1243 text: new_s,
1244 });
1245 }
1246 buf_set_cursor_rc(ed.buffer_mut(), top, block_left.unwrap_or(0));
1248 vim_mut(ed).mode = Mode::Normal;
1249 ed.set_sticky_col(Some(buf_cursor_pos(ed.buffer()).col));
1250}
1251#[cfg(test)]
1252mod replace_char_tests {
1253 use hjkl_buffer::{View, rope_line_str};
1254 use hjkl_engine::{DefaultHost, Editor, Options};
1255
1256 fn line(ed: &Editor<View, DefaultHost>, row: usize) -> String {
1257 rope_line_str(&ed.buffer().rope(), row)
1258 }
1259
1260 #[test]
1261 fn replace_char_count_exceeding_line_replaces_nothing() {
1262 let buf = View::from_str("ab\ncd");
1263 let mut ed = crate::vim::vim_editor(buf, DefaultHost::new(), Options::default());
1264 super::replace_char(&mut ed, 'x', 3);
1267 assert_eq!(line(&ed, 0), "ab", "partial replace must not happen");
1268 assert_eq!(line(&ed, 1), "cd", "must not spill onto the next line");
1269 }
1270
1271 #[test]
1272 fn replace_char_count_fitting_replaces_run() {
1273 let buf = View::from_str("abc");
1274 let mut ed = crate::vim::vim_editor(buf, DefaultHost::new(), Options::default());
1275 super::replace_char(&mut ed, 'x', 2);
1276 assert_eq!(line(&ed, 0), "xxc");
1277 }
1278}
1279#[cfg(test)]
1280mod g_ampersand_tests {
1281 use super::*;
1282 use hjkl_buffer::{View, rope_line_str};
1283 use hjkl_engine::{DefaultHost, Editor, Options};
1284
1285 fn make_editor(content: &str) -> Editor<View, DefaultHost> {
1286 let buf = View::from_str(content);
1287 let host = DefaultHost::new();
1288 crate::vim::vim_editor(buf, host, Options::default())
1289 }
1290
1291 fn buf_line(ed: &Editor<View, DefaultHost>, row: usize) -> String {
1292 let rope = ed.buffer().rope();
1293 rope_line_str(&rope, row).trim_end_matches('\n').to_string()
1294 }
1295
1296 #[test]
1299 fn g_ampersand_repeats_last_substitute_on_whole_buffer() {
1300 let mut ed = make_editor("foo\nfoo bar foo\nbaz");
1301 let cmd = hjkl_engine::substitute::parse_substitute("/foo/bar/").unwrap();
1303 ed.set_last_substitute(cmd);
1304 apply_after_g(&mut ed, '&', 1);
1306 assert_eq!(buf_line(&ed, 0), "bar");
1307 assert_eq!(buf_line(&ed, 1), "bar bar foo");
1309 assert_eq!(buf_line(&ed, 2), "baz");
1310 }
1311
1312 #[test]
1314 fn g_ampersand_with_g_flag_replaces_all_per_line() {
1315 let mut ed = make_editor("foo foo\nfoo");
1316 let cmd = hjkl_engine::substitute::parse_substitute("/foo/bar/g").unwrap();
1317 ed.set_last_substitute(cmd);
1318 apply_after_g(&mut ed, '&', 1);
1319 assert_eq!(buf_line(&ed, 0), "bar bar");
1320 assert_eq!(buf_line(&ed, 1), "bar");
1321 }
1322
1323 #[test]
1325 fn g_ampersand_noop_when_no_prior_substitute() {
1326 let mut ed = make_editor("foo\nbar");
1327 apply_after_g(&mut ed, '&', 1);
1329 assert_eq!(buf_line(&ed, 0), "foo");
1330 assert_eq!(buf_line(&ed, 1), "bar");
1331 }
1332}
1333
1334#[cfg(test)]
1335mod fold_char_delete_tests {
1336 use hjkl_buffer::{View, rope_line_str};
1337 use hjkl_engine::types::FoldOp;
1338 use hjkl_engine::{DefaultHost, Editor, Options};
1339
1340 use super::do_char_delete;
1341
1342 fn make_editor(content: &str, fold: Option<(usize, usize)>) -> Editor<View, DefaultHost> {
1343 let mut ed = crate::vim::vim_editor(
1344 View::from_str(content),
1345 DefaultHost::new(),
1346 Options::default(),
1347 );
1348 ed.jump_cursor(0, 0);
1349 if let Some((start, end)) = fold {
1350 ed.apply_fold_op(FoldOp::Add {
1351 start_row: start,
1352 end_row: end,
1353 closed: true,
1354 });
1355 }
1356 ed
1357 }
1358
1359 fn full_buffer(ed: &Editor<View, DefaultHost>) -> String {
1360 let rope = ed.buffer().rope();
1361 (0..rope.len_lines())
1362 .map(|i| rope_line_str(&rope, i).to_string())
1363 .collect::<Vec<_>>()
1364 .join("\n")
1365 }
1366
1367 #[test]
1368 fn x_on_closed_fold_deletes_whole_fold_linewise() {
1369 let mut ed = make_editor("abc\ndef\nghi\n", Some((0, 1)));
1370 do_char_delete(&mut ed, true, 1);
1371 assert_eq!(full_buffer(&ed), "ghi\n");
1372 assert_eq!(ed.yank(), "abc\ndef\n");
1373 assert_eq!(ed.cursor(), (0, 0));
1374 }
1375
1376 #[test]
1377 fn backward_char_delete_on_closed_fold_is_not_promoted() {
1378 let mut ed = make_editor("abc\ndef\nghi\n", Some((0, 1)));
1379 do_char_delete(&mut ed, false, 1);
1380 assert_eq!(full_buffer(&ed), "abc\ndef\nghi\n");
1381 assert_eq!(ed.yank(), "");
1382 }
1383
1384 #[test]
1385 fn x_on_single_line_fold_stays_charwise() {
1386 let mut ed = make_editor("abc\ndef\nghi\n", Some((0, 0)));
1387 do_char_delete(&mut ed, true, 1);
1388 assert_eq!(full_buffer(&ed), "bc\ndef\nghi\n");
1389 assert_eq!(ed.yank(), "a");
1390 }
1391
1392 #[test]
1393 fn x_on_plain_buffer_deletes_one_char() {
1394 let mut ed = make_editor("abc\ndef\nghi\n", None);
1395 do_char_delete(&mut ed, true, 1);
1396 assert_eq!(full_buffer(&ed), "bc\ndef\nghi\n");
1397 assert_eq!(ed.yank(), "a");
1398 }
1399
1400 #[test]
1401 fn x_at_col1_on_closed_fold_stays_charwise() {
1402 let mut ed = make_editor("abc\ndef\nghi\n", Some((0, 1)));
1403 ed.jump_cursor(0, 1);
1404 do_char_delete(&mut ed, true, 1);
1405 assert_eq!(full_buffer(&ed), "ac\ndef\nghi\n");
1406 assert_eq!(ed.yank(), "b");
1407 }
1408}