use hjkl_vim_types::{LastChange, Mode, RangeKind};
use hjkl_engine::rope_util::{rope_line_to_str, rope_row_range_str};
use super::*;
use crate::vim_state::{vim, vim_mut};
use hjkl_engine::Editor;
use hjkl_engine::buf_helpers::{
buf_cursor_pos, buf_line, buf_line_chars, buf_row_count, buf_set_cursor_pos, buf_set_cursor_rc,
};
pub fn read_vim_range<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
start: (usize, usize),
end: (usize, usize),
kind: RangeKind,
) -> String {
let (top, bot) = order(start, end);
ed.sync_buffer_content_from_textarea();
let rope = hjkl_engine::types::Query::rope(ed.buffer());
let n_lines = rope.len_lines();
match kind {
RangeKind::Linewise => {
let lo = top.0;
let hi = bot.0.min(n_lines.saturating_sub(1));
let mut text = rope_row_range_str(&rope, lo, hi);
text.push('\n');
text
}
RangeKind::Inclusive | RangeKind::Exclusive => {
let inclusive = matches!(kind, RangeKind::Inclusive);
let mut out = String::new();
for row in top.0..=bot.0 {
if row >= n_lines {
break;
}
let line = rope_line_to_str(&rope, row);
let lo = if row == top.0 { top.1 } else { 0 };
let hi_unclamped = if row == bot.0 {
if inclusive { bot.1 + 1 } else { bot.1 }
} else {
line.chars().count()
};
let row_chars: Vec<char> = line.chars().collect();
let hi = hi_unclamped.min(row_chars.len());
if lo < hi {
out.push_str(&row_chars[lo..hi].iter().collect::<String>());
}
if row < bot.0 {
out.push('\n');
}
}
out
}
}
}
pub fn cut_vim_range<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
start: (usize, usize),
end: (usize, usize),
kind: RangeKind,
) -> String {
use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
let (top, bot) = order(start, end);
ed.sync_buffer_content_from_textarea();
let (buf_start, buf_end, buf_kind) = match kind {
RangeKind::Linewise => (
Position::new(top.0, 0),
Position::new(bot.0, 0),
BufKind::Line,
),
RangeKind::Inclusive => {
let line_chars = buf_line_chars(ed.buffer(), bot.0);
let next = if bot.1 < line_chars {
Position::new(bot.0, bot.1 + 1)
} else if bot.0 + 1 < buf_row_count(ed.buffer()) {
Position::new(bot.0 + 1, 0)
} else {
Position::new(bot.0, line_chars)
};
(Position::new(top.0, top.1), next, BufKind::Char)
}
RangeKind::Exclusive => (
Position::new(top.0, top.1),
Position::new(bot.0, bot.1),
BufKind::Char,
),
};
let inverse = ed.mutate_edit(Edit::DeleteRange {
start: buf_start,
end: buf_end,
kind: buf_kind,
});
let raw_text = match inverse {
Edit::InsertStr { text, .. } => text,
_ => String::new(),
};
let text = if matches!(kind, RangeKind::Linewise) {
if raw_text.ends_with('\n') {
raw_text
} else if raw_text.starts_with('\n') {
format!("{}\n", raw_text.strip_prefix('\n').unwrap())
} else if raw_text.is_empty() {
return String::new();
} else {
format!("{raw_text}\n")
}
} else {
raw_text
};
if text.is_empty() {
return text;
}
ed.record_yank_to_host(text.clone());
let target = vim_mut(ed).pending_register.take();
ed.record_delete(text.clone(), matches!(kind, RangeKind::Linewise), target);
text
}
pub fn delete_to_eol<H: hjkl_engine::types::Host>(ed: &mut Editor<hjkl_buffer::View, H>) {
use hjkl_buffer::{Edit, MotionKind, Position};
ed.sync_buffer_content_from_textarea();
let cursor = buf_cursor_pos(ed.buffer());
let line_chars = buf_line_chars(ed.buffer(), cursor.row);
if cursor.col >= line_chars {
return;
}
let inverse = ed.mutate_edit(Edit::DeleteRange {
start: cursor,
end: Position::new(cursor.row, line_chars),
kind: MotionKind::Char,
});
if let Edit::InsertStr { text, .. } = inverse
&& !text.is_empty()
{
ed.record_yank_to_host(text.clone());
ed.set_yank_linewise(false);
ed.set_yank(text);
}
buf_set_cursor_pos(ed.buffer_mut(), cursor);
}
pub fn do_char_delete<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
forward: bool,
count: usize,
) {
use hjkl_buffer::{Edit, MotionKind, Position};
ed.push_undo();
ed.sync_buffer_content_from_textarea();
let mut deleted = String::new();
for _ in 0..count {
let cursor = buf_cursor_pos(ed.buffer());
let line_chars = buf_line_chars(ed.buffer(), cursor.row);
if forward {
if cursor.col >= line_chars {
break;
}
let inverse = ed.mutate_edit(Edit::DeleteRange {
start: cursor,
end: Position::new(cursor.row, cursor.col + 1),
kind: MotionKind::Char,
});
if let Edit::InsertStr { text, .. } = inverse {
deleted.push_str(&text);
}
} else {
if cursor.col == 0 {
break;
}
let inverse = ed.mutate_edit(Edit::DeleteRange {
start: Position::new(cursor.row, cursor.col - 1),
end: cursor,
kind: MotionKind::Char,
});
if let Edit::InsertStr { text, .. } = inverse {
deleted = text + &deleted;
}
}
}
if !deleted.is_empty() {
ed.record_yank_to_host(deleted.clone());
let target = vim_mut(ed).pending_register.take();
ed.record_delete(deleted, false, target);
}
let cursor = buf_cursor_pos(ed.buffer());
let line_chars = buf_line_chars(ed.buffer(), cursor.row);
if line_chars > 0 && cursor.col >= line_chars {
buf_set_cursor_pos(ed.buffer_mut(), Position::new(cursor.row, line_chars - 1));
}
}
struct AdjustedNumber {
start: usize,
end: usize,
text: String,
}
fn adjusted_number_at(chars: &[char], from: usize, delta: i64) -> Option<AdjustedNumber> {
let len = chars.len();
let is_hex_prefix = |i: usize| {
chars[i] == '0'
&& matches!(chars.get(i + 1), Some('x' | 'X'))
&& chars.get(i + 2).is_some_and(|c| c.is_ascii_hexdigit())
};
let start = (from.min(len)..len).find(|&i| is_hex_prefix(i) || chars[i].is_ascii_digit())?;
if is_hex_prefix(start) {
let digits_start = start + 2;
let mut digits_end = digits_start;
while digits_end < len && chars[digits_end].is_ascii_hexdigit() {
digits_end += 1;
}
let digits: String = chars[digits_start..digits_end].iter().collect();
let n = u64::from_str_radix(&digits, 16).ok()?;
let new_val = (n as i128 + delta as i128).max(0) as u64;
let width = digits_end - digits_start;
let prefix: String = chars[start..digits_start].iter().collect();
let upper = digits
.chars()
.rev()
.find(|c| c.is_ascii_alphabetic())
.map_or(chars[start + 1] == 'X', |c| c.is_ascii_uppercase());
let text = if upper {
format!("{prefix}{new_val:0width$X}")
} else {
format!("{prefix}{new_val:0width$x}")
};
return Some(AdjustedNumber {
start,
end: digits_end,
text,
});
}
let span_start = if start > 0 && chars[start - 1] == '-' {
start - 1
} else {
start
};
let mut span_end = start;
while span_end < len && chars[span_end].is_ascii_digit() {
span_end += 1;
}
let s: String = chars[span_start..span_end].iter().collect();
let n = s.parse::<i64>().ok()?;
let new_val = n as i128 + delta as i128;
let digits: String = chars[start..span_end].iter().collect();
let width = digits.len();
let text = if width > 1 && digits.starts_with('0') {
if new_val < 0 {
let mag = new_val.unsigned_abs();
format!("-{mag:0width$}")
} else {
format!("{new_val:0width$}")
}
} else {
new_val.to_string()
};
Some(AdjustedNumber {
start: span_start,
end: span_end,
text,
})
}
pub fn adjust_number<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
delta: i64,
) -> bool {
use hjkl_buffer::{Edit, MotionKind, Position};
ed.sync_buffer_content_from_textarea();
let cursor = buf_cursor_pos(ed.buffer());
let row = cursor.row;
let chars: Vec<char> = match buf_line(ed.buffer(), row) {
Some(l) => l.chars().collect(),
None => return false,
};
let Some(AdjustedNumber {
start: span_start,
end: span_end,
text: new_s,
}) = adjusted_number_at(&chars, cursor.col, delta)
else {
return false;
};
ed.push_undo();
let span_start_pos = Position::new(row, span_start);
let span_end_pos = Position::new(row, span_end);
ed.mutate_edit(Edit::DeleteRange {
start: span_start_pos,
end: span_end_pos,
kind: MotionKind::Char,
});
ed.mutate_edit(Edit::InsertStr {
at: span_start_pos,
text: new_s.clone(),
});
let new_len = new_s.chars().count();
buf_set_cursor_rc(ed.buffer_mut(), row, span_start + new_len.saturating_sub(1));
true
}
pub fn replace_char<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
ch: char,
count: usize,
) {
use hjkl_buffer::{Edit, MotionKind, Position};
ed.sync_buffer_content_from_textarea();
let start = buf_cursor_pos(ed.buffer());
let start_line_chars = buf_line_chars(ed.buffer(), start.row);
if count == 0 || start.col + count > start_line_chars {
return;
}
ed.push_undo();
for _ in 0..count {
let cursor = buf_cursor_pos(ed.buffer());
let line_chars = buf_line_chars(ed.buffer(), cursor.row);
if cursor.col >= line_chars {
break;
}
ed.mutate_edit(Edit::DeleteRange {
start: cursor,
end: Position::new(cursor.row, cursor.col + 1),
kind: MotionKind::Char,
});
ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
}
hjkl_engine::motions::move_left(ed.buffer_mut(), 1);
}
pub fn toggle_case_at_cursor<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
) -> bool {
use hjkl_buffer::{Edit, MotionKind, Position};
ed.sync_buffer_content_from_textarea();
let cursor = buf_cursor_pos(ed.buffer());
let Some(c) = buf_line(ed.buffer(), cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
return false;
};
let toggled = if c.is_uppercase() {
c.to_lowercase().collect::<String>()
} else {
c.to_uppercase().collect::<String>()
};
ed.mutate_edit(Edit::DeleteRange {
start: cursor,
end: Position::new(cursor.row, cursor.col + 1),
kind: MotionKind::Char,
});
ed.mutate_edit(Edit::InsertStr {
at: cursor,
text: toggled,
});
true
}
pub fn join_line<H: hjkl_engine::types::Host>(ed: &mut Editor<hjkl_buffer::View, H>) -> bool {
use hjkl_buffer::{Edit, Position};
ed.sync_buffer_content_from_textarea();
let row = buf_cursor_pos(ed.buffer()).row;
let n_rows = buf_row_count(ed.buffer());
if row + 1 >= n_rows {
return false;
}
if row + 2 >= n_rows && buf_line(ed.buffer(), row + 1).is_some_and(|s| s.is_empty()) {
return false;
}
let cur_line = buf_line(ed.buffer(), row).unwrap_or_default();
let next_raw = buf_line(ed.buffer(), row + 1).unwrap_or_default();
let next_trimmed = next_raw.trim_start();
let cur_chars = cur_line.chars().count();
let next_chars = next_raw.chars().count();
let separator = if !cur_line.is_empty()
&& !next_trimmed.is_empty()
&& !cur_line.ends_with([' ', '\t'])
&& !next_trimmed.starts_with(')')
{
" "
} else {
""
};
let joined = format!("{cur_line}{separator}{next_trimmed}");
ed.mutate_edit(Edit::Replace {
start: Position::new(row, 0),
end: Position::new(row + 1, next_chars),
with: joined,
});
buf_set_cursor_rc(ed.buffer_mut(), row, cur_chars);
true
}
pub fn join_line_raw<H: hjkl_engine::types::Host>(ed: &mut Editor<hjkl_buffer::View, H>) -> bool {
use hjkl_buffer::Edit;
ed.sync_buffer_content_from_textarea();
let row = buf_cursor_pos(ed.buffer()).row;
let n_rows = buf_row_count(ed.buffer());
if row + 1 >= n_rows {
return false;
}
if row + 2 >= n_rows && buf_line(ed.buffer(), row + 1).is_some_and(|s| s.is_empty()) {
return false;
}
let join_col = buf_line_chars(ed.buffer(), row);
ed.mutate_edit(Edit::JoinLines {
row,
count: 1,
with_space: false,
});
buf_set_cursor_rc(ed.buffer_mut(), row, join_col);
true
}
pub fn visual_join<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
with_space: bool,
) {
let cursor_row = buf_cursor_pos(ed.buffer()).row;
let (top, bot) = match vim(ed).mode {
Mode::VisualLine => (
cursor_row.min(vim(ed).visual_line_anchor),
cursor_row.max(vim(ed).visual_line_anchor),
),
Mode::VisualBlock => {
let a = vim(ed).block_anchor.0;
(a.min(cursor_row), a.max(cursor_row))
}
Mode::Visual => {
let a = vim(ed).visual_anchor.0;
(a.min(cursor_row), a.max(cursor_row))
}
_ => return,
};
let joins = (bot - top).max(1);
ed.push_undo();
buf_set_cursor_rc(ed.buffer_mut(), top, 0);
for _ in 0..joins {
let joined = if with_space {
join_line(ed)
} else {
join_line_raw(ed)
};
if !joined {
break;
}
}
if !vim(ed).replaying {
vim_mut(ed).last_change = Some(LastChange::JoinLine { count: joins });
}
vim_mut(ed).mode = Mode::Normal;
ed.set_sticky_col(Some(buf_cursor_pos(ed.buffer()).col));
}
pub fn goto_percent<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
count: usize,
) {
let rows = buf_row_count(ed.buffer());
if rows == 0 {
return;
}
let total = if rows >= 2 && buf_line(ed.buffer(), rows - 1).is_some_and(|s| s.is_empty()) {
rows - 1
} else {
rows
};
let line = count.saturating_mul(total).div_ceil(100).clamp(1, total);
let pre = ed.cursor();
ed.jump_cursor(line - 1, 0);
move_first_non_whitespace(ed);
ed.set_sticky_col(Some(ed.cursor().1));
if ed.cursor() != pre {
ed.push_jump(pre);
}
}
pub fn indent_width(s: &str, tabstop: usize) -> usize {
let ts = tabstop.max(1);
let mut w = 0usize;
for c in s.chars() {
match c {
' ' => w += 1,
'\t' => w += ts - (w % ts),
_ => break,
}
}
w
}
pub fn build_indent(width: usize, settings: &hjkl_engine::Settings) -> String {
if settings.expandtab {
return " ".repeat(width);
}
let ts = settings.tabstop.max(1);
let tabs = width / ts;
let spaces = width % ts;
format!("{}{}", "\t".repeat(tabs), " ".repeat(spaces))
}
pub fn reindent_block(text: &str, target_width: usize, settings: &hjkl_engine::Settings) -> String {
let ts = settings.tabstop.max(1);
let lines: Vec<&str> = text.split('\n').collect();
let first_width = lines.first().map_or(0, |l| indent_width(l, ts));
let delta = target_width as isize - first_width as isize;
lines
.iter()
.map(|line| {
let trimmed = line.trim_start_matches([' ', '\t']);
if trimmed.is_empty() {
return String::new();
}
let old_w = indent_width(line, ts) as isize;
let new_w = (old_w + delta).max(0) as usize;
format!("{}{}", build_indent(new_w, settings), trimmed)
})
.collect::<Vec<_>>()
.join("\n")
}
pub fn do_paste<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
before: bool,
count: usize,
cursor_after: bool,
reindent: bool,
) -> bool {
use hjkl_buffer::{Edit, Position};
let selector = vim_mut(ed).pending_register.take();
sync_clipboard_register_for(ed, selector);
let (yank, linewise, blockwise, block_width) =
ed.with_registers(|regs| match selector.and_then(|c| regs.read(c)) {
Some(slot) => (
slot.text.clone(),
slot.linewise,
slot.blockwise,
slot.block_width,
),
None => (
regs.unnamed.text.clone(),
regs.unnamed.linewise,
regs.unnamed.blockwise,
regs.unnamed.block_width,
),
});
let original_row_for_linewise_after = if linewise && !before {
let r = buf_cursor_pos(ed.buffer()).row;
let (_, fold_end) = expand_linewise_over_closed_folds(ed.buffer(), r, r);
Some(fold_end)
} else {
None
};
if yank.is_empty() {
return false;
}
const MAX_PASTE_BYTES: usize = 1024 * 1024;
let Some(requested_bytes) = yank.len().checked_mul(count) else {
return false;
};
if requested_bytes > MAX_PASTE_BYTES {
return false;
}
if blockwise {
let Some(block_bytes) = block_width
.checked_mul(yank.split('\n').count())
.and_then(|bytes| bytes.checked_mul(count))
else {
return false;
};
let Some(total_bytes) = requested_bytes.checked_add(block_bytes) else {
return false;
};
if total_bytes > MAX_PASTE_BYTES {
return false;
}
}
ed.push_undo();
if blockwise {
do_block_paste(ed, before, count, block_width, &yank);
return true;
}
let paste_mark = if linewise {
ed.sync_buffer_content_from_textarea();
let row = buf_cursor_pos(ed.buffer()).row;
let mut text = yank.trim_matches('\n').to_string();
if reindent {
let cur_line = buf_line(ed.buffer(), row).unwrap_or_default();
let target_w = indent_width(&cur_line, ed.settings().tabstop.max(1));
text = reindent_block(&text, target_w, ed.settings());
}
let (fold_start, fold_end) = expand_linewise_over_closed_folds(ed.buffer(), row, row);
let payload = std::iter::repeat_n(text.as_str(), count)
.collect::<Vec<_>>()
.join("\n");
let target_row = if before {
ed.mutate_edit(Edit::InsertStr {
at: Position::new(fold_start, 0),
text: format!("{payload}\n"),
});
fold_start
} else {
let line_chars = buf_line_chars(ed.buffer(), fold_end);
ed.mutate_edit(Edit::InsertStr {
at: Position::new(fold_end, line_chars),
text: format!("\n{payload}"),
});
fold_end + 1
};
buf_set_cursor_rc(ed.buffer_mut(), target_row, 0);
hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
let payload_lines = payload.lines().count().max(1);
let bot_row = target_row + payload_lines - 1;
let bot_last_col = buf_line_chars(ed.buffer(), bot_row).saturating_sub(1);
((target_row, 0), (bot_row, bot_last_col))
} else {
ed.sync_buffer_content_from_textarea();
let cursor = buf_cursor_pos(ed.buffer());
let at = if before {
cursor
} else {
let line_chars = buf_line_chars(ed.buffer(), cursor.row);
Position::new(cursor.row, (cursor.col + 1).min(line_chars))
};
let repeated = yank.repeat(count);
ed.mutate_edit(Edit::InsertStr { at, text: repeated });
if !cursor_after && ed.cursor().1 > 0 {
hjkl_engine::motions::move_left(ed.buffer_mut(), 1);
}
let lo = (at.row, at.col);
let hi = if cursor_after {
let c = ed.cursor();
(c.0, c.1.saturating_sub(1))
} else {
ed.cursor()
};
(lo, hi)
};
ed.set_mark('[', paste_mark.0);
ed.set_mark(']', paste_mark.1);
if cursor_after && linewise {
let bot_row = paste_mark.1.0;
let last_row = buf_row_count(ed.buffer()).saturating_sub(1);
let target = (bot_row + 1).min(last_row);
buf_set_cursor_rc(ed.buffer_mut(), target, 0);
} else if let Some(orig_row) = original_row_for_linewise_after {
let first_target = orig_row.saturating_add(1);
buf_set_cursor_rc(ed.buffer_mut(), first_target, 0);
hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
}
ed.set_sticky_col(Some(buf_cursor_pos(ed.buffer()).col));
true
}
fn do_block_paste<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
before: bool,
count: usize,
width: usize,
yank: &str,
) {
ed.sync_buffer_content_from_textarea();
let mut lines: Vec<String> =
hjkl_engine::rope_util::rope_to_lines_vec(&hjkl_engine::types::Query::rope(ed.buffer()));
let trailing_nl = lines.len() > 1 && lines.last().is_some_and(String::is_empty);
if trailing_nl {
lines.pop();
}
let cursor = buf_cursor_pos(ed.buffer());
let start_row = cursor.row;
let cur_len = lines.get(start_row).map_or(0, |l| l.chars().count());
let insert_col = if before {
cursor.col
} else if cur_len == 0 {
0
} else {
cursor.col + 1
};
let segments: Vec<&str> = yank.split('\n').collect();
for (i, seg) in segments.iter().enumerate() {
let row = start_row + i;
while row >= lines.len() {
lines.push(String::new());
}
let mut chars: Vec<char> = lines[row].chars().collect();
if chars.len() < insert_col {
chars.resize(insert_col, ' ');
}
let head: String = chars[..insert_col].iter().collect();
let tail: String = chars[insert_col..].iter().collect();
let piece = if tail.is_empty() {
seg.repeat(count)
} else {
let seg_len = seg.chars().count();
let mut padded = seg.to_string();
if seg_len < width {
padded.extend(std::iter::repeat_n(' ', width - seg_len));
}
padded.repeat(count)
};
lines[row] = format!("{head}{piece}{tail}");
}
let mut joined = lines.join("\n");
if trailing_nl {
joined.push('\n');
}
hjkl_engine::types::BufferEdit::replace_all(ed.buffer_mut(), &joined);
ed.mark_content_dirty();
ed.jump_cursor(start_row, insert_col);
let bot_row = start_row + segments.len().saturating_sub(1);
ed.set_mark('[', (start_row, insert_col));
ed.set_mark(']', (bot_row, insert_col));
ed.set_sticky_col(Some(insert_col));
}
pub fn visual_paste<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
before: bool,
) {
use hjkl_buffer::{Edit, Position};
ed.sync_buffer_content_from_textarea();
let selector = vim_mut(ed).pending_register.take();
sync_clipboard_register_for(ed, selector);
let (reg_text, reg_linewise, reg_blockwise, reg_block_width) =
ed.with_registers(|regs| match selector.and_then(|c| regs.read(c)) {
Some(slot) => (
slot.text.clone(),
slot.linewise,
slot.blockwise,
slot.block_width,
),
None => (
regs.unnamed.text.clone(),
regs.unnamed.linewise,
regs.unnamed.blockwise,
regs.unnamed.block_width,
),
});
let saved_unnamed = before.then(|| ed.with_registers(|regs| regs.unnamed.clone()));
let mode = vim(ed).mode;
ed.push_undo();
match mode {
Mode::VisualLine => {
let cursor_row = buf_cursor_pos(ed.buffer()).row;
let top = cursor_row.min(vim(ed).visual_line_anchor);
let bot = cursor_row.max(vim(ed).visual_line_anchor);
cut_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
let text = reg_text.trim_matches('\n').to_string();
let line_count = buf_row_count(ed.buffer());
if top >= line_count {
let last = line_count.saturating_sub(1);
let lc = buf_line_chars(ed.buffer(), last);
ed.mutate_edit(Edit::InsertStr {
at: Position::new(last, lc),
text: format!("\n{text}"),
});
buf_set_cursor_rc(ed.buffer_mut(), last + 1, 0);
} else {
ed.mutate_edit(Edit::InsertStr {
at: Position::new(top, 0),
text: format!("{text}\n"),
});
buf_set_cursor_rc(ed.buffer_mut(), top, 0);
}
hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
}
Mode::Visual => {
let anchor = vim(ed).visual_anchor;
let cursor = ed.cursor();
let (top, bot) = order(anchor, cursor);
cut_vim_range(ed, top, bot, RangeKind::Inclusive);
if reg_linewise {
let text = reg_text.trim_matches('\n').to_string();
let lc = buf_line_chars(ed.buffer(), top.0);
ed.mutate_edit(Edit::InsertStr {
at: Position::new(top.0, lc),
text: format!("\n{text}"),
});
buf_set_cursor_rc(ed.buffer_mut(), top.0 + 1, 0);
hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
} else {
ed.mutate_edit(Edit::InsertStr {
at: Position::new(top.0, top.1),
text: reg_text.clone(),
});
let inserted_len = reg_text.chars().count();
let last_col = top.1 + inserted_len.saturating_sub(1);
buf_set_cursor_rc(ed.buffer_mut(), top.0, last_col);
}
}
Mode::VisualBlock => {
let (top, bot, left, right) = block_bounds(ed);
let to_eol = vim(ed).block_to_eol;
let deleted = block_yank(ed, top, bot, left, right, to_eol);
let del_width = if to_eol {
deleted
.split('\n')
.map(|s| s.chars().count())
.max()
.unwrap_or(0)
} else {
right + 1 - left
};
delete_block_contents(ed, top, bot, left, right, to_eol);
if !before && !deleted.is_empty() {
ed.record_yank_to_host(deleted.clone());
ed.record_delete_block(deleted, del_width, None);
}
if reg_blockwise {
ed.jump_cursor(top, left);
do_block_paste(ed, true, 1, reg_block_width, ®_text);
} else if reg_linewise {
let text = reg_text.trim_matches('\n').to_string();
let lc = buf_line_chars(ed.buffer(), bot);
ed.mutate_edit(Edit::InsertStr {
at: Position::new(bot, lc),
text: format!("\n{text}"),
});
buf_set_cursor_rc(ed.buffer_mut(), bot + 1, 0);
hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
} else if reg_text.contains('\n') {
ed.mutate_edit(Edit::InsertStr {
at: Position::new(top, left),
text: reg_text.clone(),
});
buf_set_cursor_rc(ed.buffer_mut(), top, left);
} else {
for r in top..=bot {
let line_len = buf_line_chars(ed.buffer(), r);
if left > line_len {
continue;
}
ed.mutate_edit(Edit::InsertStr {
at: Position::new(r, left),
text: reg_text.clone(),
});
}
let last_col = left + reg_text.chars().count().saturating_sub(1);
buf_set_cursor_rc(ed.buffer_mut(), top, last_col);
}
}
_ => {}
}
if let Some(slot) = saved_unnamed {
ed.with_registers_mut(|regs| regs.unnamed = slot);
}
vim_mut(ed).mode = Mode::Normal;
ed.set_sticky_col(Some(buf_cursor_pos(ed.buffer()).col));
}
pub fn adjust_number_visual<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
delta: i64,
sequential: bool,
) {
use hjkl_buffer::{Edit, MotionKind, Position};
ed.sync_buffer_content_from_textarea();
let mode = vim(ed).mode;
let cursor = buf_cursor_pos(ed.buffer());
let (top, bot, mut scan_col_first, block_left) = match mode {
Mode::VisualLine => {
let t = cursor.row.min(vim(ed).visual_line_anchor);
let b = cursor.row.max(vim(ed).visual_line_anchor);
(t, b, 0usize, None)
}
Mode::Visual => {
let (a, c) = order(vim(ed).visual_anchor, (cursor.row, cursor.col));
(a.0, c.0, a.1, None)
}
Mode::VisualBlock => {
let (a, c) = order(vim(ed).block_anchor, (cursor.row, cursor.col));
let left = a.1.min(c.1);
(a.0, c.0, left, Some(left))
}
_ => return,
};
ed.push_undo();
let mut found_count: i64 = 0;
for row in top..=bot {
let start_col = match block_left {
Some(left) => left,
None => {
let c = if row == top { scan_col_first } else { 0 };
scan_col_first = 0;
c
}
};
let chars: Vec<char> = match buf_line(ed.buffer(), row) {
Some(l) => l.chars().collect(),
None => continue,
};
let this_delta = if sequential {
delta.saturating_mul(found_count.saturating_add(1))
} else {
delta
};
let Some(AdjustedNumber {
start: span_start,
end: span_end,
text: new_s,
}) = adjusted_number_at(&chars, start_col, this_delta)
else {
continue;
};
found_count += 1;
let span_start_pos = Position::new(row, span_start);
let span_end_pos = Position::new(row, span_end);
ed.mutate_edit(Edit::DeleteRange {
start: span_start_pos,
end: span_end_pos,
kind: MotionKind::Char,
});
ed.mutate_edit(Edit::InsertStr {
at: span_start_pos,
text: new_s,
});
}
buf_set_cursor_rc(ed.buffer_mut(), top, block_left.unwrap_or(0));
vim_mut(ed).mode = Mode::Normal;
ed.set_sticky_col(Some(buf_cursor_pos(ed.buffer()).col));
}
#[cfg(test)]
mod replace_char_tests {
use hjkl_buffer::{View, rope_line_str};
use hjkl_engine::{DefaultHost, Editor, Options};
fn line(ed: &Editor<View, DefaultHost>, row: usize) -> String {
rope_line_str(&ed.buffer().rope(), row)
}
#[test]
fn replace_char_count_exceeding_line_replaces_nothing() {
let buf = View::from_str("ab\ncd");
let mut ed = crate::vim::vim_editor(buf, DefaultHost::new(), Options::default());
super::replace_char(&mut ed, 'x', 3);
assert_eq!(line(&ed, 0), "ab", "partial replace must not happen");
assert_eq!(line(&ed, 1), "cd", "must not spill onto the next line");
}
#[test]
fn replace_char_count_fitting_replaces_run() {
let buf = View::from_str("abc");
let mut ed = crate::vim::vim_editor(buf, DefaultHost::new(), Options::default());
super::replace_char(&mut ed, 'x', 2);
assert_eq!(line(&ed, 0), "xxc");
}
}
#[cfg(test)]
mod g_ampersand_tests {
use super::*;
use hjkl_buffer::{View, rope_line_str};
use hjkl_engine::{DefaultHost, Editor, Options};
fn make_editor(content: &str) -> Editor<View, DefaultHost> {
let buf = View::from_str(content);
let host = DefaultHost::new();
crate::vim::vim_editor(buf, host, Options::default())
}
fn buf_line(ed: &Editor<View, DefaultHost>, row: usize) -> String {
let rope = ed.buffer().rope();
rope_line_str(&rope, row).trim_end_matches('\n').to_string()
}
#[test]
fn g_ampersand_repeats_last_substitute_on_whole_buffer() {
let mut ed = make_editor("foo\nfoo bar foo\nbaz");
let cmd = hjkl_engine::substitute::parse_substitute("/foo/bar/").unwrap();
ed.set_last_substitute(cmd);
apply_after_g(&mut ed, '&', 1);
assert_eq!(buf_line(&ed, 0), "bar");
assert_eq!(buf_line(&ed, 1), "bar bar foo");
assert_eq!(buf_line(&ed, 2), "baz");
}
#[test]
fn g_ampersand_with_g_flag_replaces_all_per_line() {
let mut ed = make_editor("foo foo\nfoo");
let cmd = hjkl_engine::substitute::parse_substitute("/foo/bar/g").unwrap();
ed.set_last_substitute(cmd);
apply_after_g(&mut ed, '&', 1);
assert_eq!(buf_line(&ed, 0), "bar bar");
assert_eq!(buf_line(&ed, 1), "bar");
}
#[test]
fn g_ampersand_noop_when_no_prior_substitute() {
let mut ed = make_editor("foo\nbar");
apply_after_g(&mut ed, '&', 1);
assert_eq!(buf_line(&ed, 0), "foo");
assert_eq!(buf_line(&ed, 1), "bar");
}
}