use hjkl_vim_types::{InsertReason, LastChange, Mode, Motion, Operator, RangeKind, VisualExtent};
use hjkl_engine::rope_util::rope_line_to_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_rc,
};
pub fn apply_visual_operator<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
op: Operator,
count: usize,
) {
include_closed_folds_in_selection(ed);
visual_operator_body(ed, op, count);
remember_operator_column(ed);
}
fn include_closed_folds_in_selection<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
) {
let mode = vim(ed).mode;
let anchor = match mode {
Mode::Visual => vim(ed).visual_anchor,
Mode::VisualLine => (vim(ed).visual_line_anchor, 0),
Mode::VisualBlock => vim(ed).block_anchor,
_ => return,
};
let cursor = ed.cursor();
let fold_edge = |ed: &Editor<hjkl_buffer::View, H>, pos: (usize, usize), last: bool| {
let (first, end) = expand_linewise_over_closed_folds(ed.buffer(), pos.0, pos.0);
if first == end {
pos
} else if last {
(end, buf_line_chars(ed.buffer(), end))
} else {
(first, 0)
}
};
let anchor_first = anchor < cursor;
let new_anchor = fold_edge(ed, anchor, !anchor_first);
let new_cursor = fold_edge(ed, cursor, anchor_first);
if (new_anchor, new_cursor) == (anchor, cursor) {
return;
}
match mode {
Mode::Visual => vim_mut(ed).visual_anchor = new_anchor,
Mode::VisualLine => vim_mut(ed).visual_line_anchor = new_anchor.0,
_ => {
vim_mut(ed).block_anchor = new_anchor;
vim_mut(ed).block_vcol = new_cursor.1;
}
}
buf_set_cursor_rc(ed.buffer_mut(), new_cursor.0, new_cursor.1);
}
pub fn remember_operator_column<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
) {
if vim(ed).mode == Mode::Normal {
remember_cursor_column(ed);
}
}
pub fn remember_cursor_column<H: hjkl_engine::types::Host>(ed: &mut Editor<hjkl_buffer::View, H>) {
let (row, col) = ed.cursor();
let want = ed.cursor_want(row, col);
ed.set_sticky_col(Some(want));
}
pub fn visual_op_start<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
) -> (usize, usize) {
let cursor = ed.cursor();
match vim(ed).mode {
Mode::Visual => order(vim(ed).visual_anchor, cursor).0,
Mode::VisualLine if cursor.0 < vim(ed).visual_line_anchor => cursor,
Mode::VisualLine => (vim(ed).visual_line_anchor, 0),
Mode::VisualBlock => {
let (top, _, left, _) = block_bounds(ed);
(top, left)
}
_ => cursor,
}
}
fn visual_operator_body<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
op: Operator,
count: usize,
) {
let levels = count.max(1);
let register = vim(ed).pending_register;
if matches!(op, Operator::Delete | Operator::Yank | Operator::Change)
&& refuse_read_only_register(ed)
{
let start = visual_op_start(ed);
refused_operator(ed, op, start, RangeKind::Exclusive);
return;
}
match vim(ed).mode {
Mode::VisualLine => {
let (cursor_row, cursor_col) = ed.cursor();
let anchor_row = vim(ed).visual_line_anchor;
let top = cursor_row.min(anchor_row);
let bot = cursor_row.max(anchor_row);
ed.set_yank_linewise(true);
let start_col = if cursor_row < anchor_row {
cursor_col
} else {
0
};
if !matches!(op, Operator::Reflow | Operator::ReflowKeepCursor) {
buf_set_cursor_rc(ed.buffer_mut(), top, start_col);
}
match op {
Operator::Yank => {
let text = read_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
if !text.is_empty() {
ed.record_yank_to_host(text.clone());
let target = vim_mut(ed).pending_register.take();
ed.record_yank(text, true, target);
}
vim_mut(ed).mode = Mode::Normal;
}
Operator::Delete => {
ed.push_undo();
let total = buf_row_count(ed.buffer());
let deleted_through_last = bot + 1 >= total;
cut_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
land_after_linewise_delete(ed, top, deleted_through_last);
record_visual_last_change(
ed,
op,
VisualExtent::Line {
lines: bot - top + 1,
},
register,
1,
);
vim_mut(ed).mode = Mode::Normal;
}
Operator::Change => {
record_visual_last_change(
ed,
op,
VisualExtent::Line {
lines: bot - top + 1,
},
register,
1,
);
change_linewise_rows(ed, top, bot, ed.cursor().1);
}
Operator::Uppercase
| Operator::Lowercase
| Operator::ToggleCase
| Operator::Rot13 => {
apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), RangeKind::Linewise);
buf_set_cursor_rc(ed.buffer_mut(), top, 0);
record_visual_last_change(
ed,
op,
VisualExtent::Line {
lines: bot - top + 1,
},
register,
1,
);
}
Operator::Indent | Operator::Outdent => {
ed.push_undo();
if op == Operator::Indent {
indent_rows(ed, top, bot, levels);
} else {
outdent_rows(ed, top, bot, levels);
}
record_visual_last_change(
ed,
op,
VisualExtent::Line {
lines: bot - top + 1,
},
register,
levels,
);
vim_mut(ed).mode = Mode::Normal;
}
Operator::Reflow => {
ed.push_undo();
let (cursor_row, _) = ed.cursor();
let bot = cursor_row.max(vim(ed).visual_line_anchor);
reflow_rows(ed, top, bot);
vim_mut(ed).mode = Mode::Normal;
}
Operator::ReflowKeepCursor => {
let saved = ed.cursor();
ed.push_undo();
let (cursor_row, _) = ed.cursor();
let bot = cursor_row.max(vim(ed).visual_line_anchor);
let (before, after, opts) = reflow_rows_keep_cursor(ed, top, bot);
let (new_row, new_col) =
reflow_keep_cursor(top, saved.0, saved.1, &before, &after, opts);
buf_set_cursor_rc(ed.buffer_mut(), new_row, new_col);
vim_mut(ed).mode = Mode::Normal;
}
Operator::AutoIndent => {
ed.push_undo();
auto_indent_rows(ed, top, bot);
vim_mut(ed).mode = Mode::Normal;
}
Operator::Filter => {}
Operator::Comment => {}
Operator::Fold => unreachable!("Visual zf takes its own path"),
}
}
Mode::Visual => {
ed.set_yank_linewise(false);
let anchor = vim(ed).visual_anchor;
let cursor = ed.cursor();
let (top, bot) = order(anchor, cursor);
if !matches!(op, Operator::Reflow | Operator::ReflowKeepCursor) {
buf_set_cursor_rc(ed.buffer_mut(), top.0, top.1);
}
match op {
Operator::Yank => {
let (from, to, kind) = charwise_visual_range(ed, top, bot);
let text = read_vim_range(ed, from, to, kind);
if !text.is_empty() {
ed.record_yank_to_host(text.clone());
let target = vim_mut(ed).pending_register.take();
ed.record_yank(text, false, target);
}
buf_set_cursor_rc(ed.buffer_mut(), top.0, top.1);
vim_mut(ed).mode = Mode::Normal;
}
Operator::Delete => {
ed.push_undo();
let (from, to, kind) = charwise_visual_range(ed, top, bot);
cut_vim_range(ed, from, to, kind);
clamp_cursor_to_normal_mode(ed);
record_visual_last_change(ed, op, charwise_extent(top, bot), register, 1);
vim_mut(ed).mode = Mode::Normal;
}
Operator::Change => {
ed.push_undo();
record_visual_last_change(ed, op, charwise_extent(top, bot), register, 1);
let (from, to, kind) = charwise_visual_range(ed, top, bot);
cut_vim_range(ed, from, to, kind);
begin_insert_noundo(ed, 1, InsertReason::AfterChange);
}
Operator::Uppercase
| Operator::Lowercase
| Operator::ToggleCase
| Operator::Rot13 => {
let (from, to, kind) = charwise_visual_range(ed, top, bot);
apply_case_op_to_selection(ed, op, from, to, kind);
record_visual_last_change(ed, op, charwise_extent(top, bot), register, 1);
}
Operator::Indent | Operator::Outdent => {
ed.push_undo();
if op == Operator::Indent {
indent_rows(ed, top.0, bot.0, levels);
} else {
outdent_rows(ed, top.0, bot.0, levels);
}
record_visual_last_change(
ed,
op,
VisualExtent::Line {
lines: bot.0 - top.0 + 1,
},
register,
levels,
);
vim_mut(ed).mode = Mode::Normal;
}
Operator::Reflow => {
ed.push_undo();
let anchor = vim(ed).visual_anchor;
let cursor = ed.cursor();
let (top, bot) = order(anchor, cursor);
reflow_rows(ed, top.0, bot.0);
vim_mut(ed).mode = Mode::Normal;
}
Operator::ReflowKeepCursor => {
let saved = ed.cursor();
ed.push_undo();
let anchor = vim(ed).visual_anchor;
let cursor = ed.cursor();
let (top, bot) = order(anchor, cursor);
let (before, after, opts) = reflow_rows_keep_cursor(ed, top.0, bot.0);
let (new_row, new_col) =
reflow_keep_cursor(top.0, saved.0, saved.1, &before, &after, opts);
buf_set_cursor_rc(ed.buffer_mut(), new_row, new_col);
vim_mut(ed).mode = Mode::Normal;
}
Operator::AutoIndent => {
ed.push_undo();
let anchor = vim(ed).visual_anchor;
let cursor = ed.cursor();
let (top, bot) = order(anchor, cursor);
auto_indent_rows(ed, top.0, bot.0);
vim_mut(ed).mode = Mode::Normal;
}
Operator::Filter => {}
Operator::Comment => {}
Operator::Fold => unreachable!("Visual zf takes its own path"),
}
}
Mode::VisualBlock => apply_block_operator(ed, op, levels),
_ => {}
}
}
fn record_visual_last_change<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
op: Operator,
extent: VisualExtent,
register: Option<char>,
levels: usize,
) {
if !vim(ed).replaying {
vim_mut(ed).last_change = Some(LastChange::VisualOp {
op,
extent,
inserted: None,
register,
levels,
});
}
}
pub fn charwise_visual_range<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
top: (usize, usize),
bot: (usize, usize),
) -> ((usize, usize), (usize, usize), RangeKind) {
let rows = buf_row_count(ed.buffer());
let next_is_line =
bot.0 + 1 < rows && !(bot.0 + 2 == rows && buf_line_chars(ed.buffer(), bot.0 + 1) == 0);
if bot.1 >= buf_line_chars(ed.buffer(), bot.0) && next_is_line {
(top, (bot.0 + 1, 0), RangeKind::Exclusive)
} else {
(top, bot, RangeKind::Inclusive)
}
}
fn charwise_extent(top: (usize, usize), bot: (usize, usize)) -> VisualExtent {
if top.0 == bot.0 {
VisualExtent::Char {
lines: 1,
width: bot.1 - top.1 + 1,
}
} else {
VisualExtent::Char {
lines: bot.0 - top.0 + 1,
width: bot.1 + 1,
}
}
}
fn block_extent(top: usize, bot: usize, cols: BlockCols) -> VisualExtent {
let to_eol = cols.right == usize::MAX;
VisualExtent::Block {
rows: bot - top + 1,
cols: if to_eol {
1
} else {
cols.right + 1 - cols.left
},
to_eol,
}
}
pub fn block_bounds<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
) -> (usize, usize, usize, usize) {
let (ar, ac) = vim(ed).block_anchor;
let (cr, _) = ed.cursor();
let cc = vim(ed).block_vcol;
let top = ar.min(cr);
let bot = ar.max(cr);
let left = ac.min(cc);
let right = ac.max(cc);
(top, bot, left, right)
}
pub fn update_block_vcol<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
motion: &Motion,
) {
match motion {
Motion::LineEnd => {
vim_mut(ed).block_vcol = ed.cursor().1;
vim_mut(ed).block_to_eol = true;
}
Motion::Left
| Motion::Right
| Motion::SpaceFwd
| Motion::BackspaceBack
| Motion::WordFwd
| Motion::BigWordFwd
| Motion::WordBack
| Motion::BigWordBack
| Motion::WordEnd
| Motion::BigWordEnd
| Motion::WordEndBack
| Motion::BigWordEndBack
| Motion::LineStart
| Motion::Home
| Motion::FirstNonBlank
| Motion::LastNonBlank
| Motion::LineMiddle
| Motion::ScreenLineMiddle
| Motion::ScreenLineStart
| Motion::ScreenLineFirstNonBlank
| Motion::ScreenLineEnd
| Motion::WordAtCursor { .. }
| Motion::Find { .. }
| Motion::FindRepeat { .. }
| Motion::MatchBracket
| Motion::FileTop
| Motion::FileBottom
| Motion::ParagraphPrev
| Motion::ParagraphNext
| Motion::SentencePrev
| Motion::SentenceNext
| Motion::SectionBackward
| Motion::SectionForward
| Motion::SectionEndBackward
| Motion::SectionEndForward
| Motion::UnmatchedBracket { .. }
| Motion::FirstNonBlankNextLine
| Motion::FirstNonBlankPrevLine
| Motion::FirstNonBlankLine
| Motion::GotoColumn
| Motion::SearchNext { .. } => {
vim_mut(ed).block_vcol = ed.cursor().1;
vim_mut(ed).block_to_eol = false;
}
Motion::Up | Motion::Down => {
vim_mut(ed).block_vcol = ed.cursor().1;
}
_ => {}
}
}
pub fn visual_selection_for_search<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
) -> ((usize, usize), String) {
match vim(ed).mode {
Mode::Visual => {
let (top, bot) = order(vim(ed).visual_anchor, ed.cursor());
(top, read_vim_range(ed, top, bot, RangeKind::Inclusive))
}
Mode::VisualLine => {
let top = vim(ed).visual_line_anchor.min(ed.cursor().0);
let bot = vim(ed).visual_line_anchor.max(ed.cursor().0);
let text = read_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
(
(top, 0),
text.strip_suffix('\n').unwrap_or(&text).to_owned(),
)
}
Mode::VisualBlock => {
let (top, bot, cols) = block_rect(ed);
(
(top, block_start_col(ed, top, cols)),
block_yank(ed, top, bot, cols),
)
}
Mode::Normal | Mode::Insert => (ed.cursor(), String::new()),
}
}
pub fn apply_block_operator<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
op: Operator,
count: usize,
) {
let register = vim(ed).pending_register;
let (top, bot, cols) = block_rect(ed);
let left_col =
block_start_col(ed, top, cols).min(buf_line_chars(ed.buffer(), top).saturating_sub(1));
if matches!(op, Operator::Delete | Operator::Yank | Operator::Change)
&& refuse_read_only_register(ed)
{
refused_operator(ed, op, (top, left_col), RangeKind::Exclusive);
return;
}
let yank = block_yank(ed, top, bot, cols);
let block_width = block_register_width(&yank, cols);
ed.jump_cursor(top, left_col);
match op {
Operator::Yank => {
if !yank.is_empty() {
ed.record_yank_to_host(yank.clone());
let target = vim_mut(ed).pending_register.take();
ed.record_yank_block(yank, block_width, target);
}
vim_mut(ed).mode = Mode::Normal;
ed.jump_cursor(top, left_col);
}
Operator::Delete => {
ed.push_undo();
delete_block_contents(ed, top, bot, cols);
if !yank.is_empty() {
ed.record_yank_to_host(yank.clone());
let target = vim_mut(ed).pending_register.take();
ed.record_delete_block(yank, block_width, target);
}
vim_mut(ed).mode = Mode::Normal;
let line_len = buf_line_chars(ed.buffer(), top);
ed.jump_cursor(top, left_col.min(line_len.saturating_sub(1)));
record_visual_last_change(ed, op, block_extent(top, bot, cols), register, 1);
}
Operator::Change => {
ed.push_undo();
record_visual_last_change(ed, op, block_extent(top, bot, cols), register, 1);
let top_line = buf_line(ed.buffer(), top).unwrap_or_default();
let top_span = block_row(&top_line, cols, ed.settings().tabstop, true);
let insert_col = top_span.start + top_span.startspaces;
delete_block_contents(ed, top, bot, cols);
if !yank.is_empty() {
ed.record_yank_to_host(yank.clone());
let target = vim_mut(ed).pending_register.take();
ed.record_delete_block(yank, block_width, target);
}
ed.jump_cursor(top, insert_col);
begin_insert_noundo(
ed,
1,
InsertReason::BlockChange {
top,
bot,
col: insert_col,
},
);
}
Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase | Operator::Rot13 => {
ed.push_undo();
transform_block_case(ed, op, top, bot, cols);
vim_mut(ed).mode = Mode::Normal;
ed.jump_cursor(top, left_col);
record_visual_last_change(ed, op, block_extent(top, bot, cols), register, 1);
}
Operator::Indent | Operator::Outdent => {
ed.push_undo();
if op == Operator::Indent {
indent_block(ed, top, bot, cols, count.max(1));
} else {
outdent_block(ed, top, bot, cols, count.max(1));
}
vim_mut(ed).mode = Mode::Normal;
record_visual_last_change(ed, op, block_extent(top, bot, cols), register, count.max(1));
}
Operator::Fold => unreachable!("Visual zf takes its own path"),
Operator::Reflow => {
ed.push_undo();
reflow_rows(ed, top, bot);
vim_mut(ed).mode = Mode::Normal;
}
Operator::ReflowKeepCursor => {
let saved = ed.cursor();
ed.push_undo();
let (before, after, opts) = reflow_rows_keep_cursor(ed, top, bot);
let (new_row, new_col) =
reflow_keep_cursor(top, saved.0, saved.1, &before, &after, opts);
buf_set_cursor_rc(ed.buffer_mut(), new_row, new_col);
vim_mut(ed).mode = Mode::Normal;
}
Operator::AutoIndent => {
ed.push_undo();
auto_indent_rows(ed, top, bot);
vim_mut(ed).mode = Mode::Normal;
}
Operator::Filter => {}
Operator::Comment => {}
}
}
pub fn transform_block_case<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
op: Operator,
top: usize,
bot: usize,
cols: BlockCols,
) {
use hjkl_buffer::{Edit, Position};
let tabstop = ed.settings().tabstop;
let rope = hjkl_engine::types::Query::rope(ed.buffer());
for r in top..=bot.min(rope.len_lines().saturating_sub(1)) {
let line = rope_line_to_str(&rope, r);
let span = block_row(&line, cols, tabstop, false);
if span.start >= span.end {
continue;
}
let mid: String = line
.chars()
.skip(span.start)
.take(span.end - span.start)
.collect();
let transformed = match op {
Operator::Uppercase => mid.to_uppercase(),
Operator::Lowercase => mid.to_lowercase(),
Operator::ToggleCase => toggle_case_str(&mid),
Operator::Rot13 => rot13_str(&mid),
_ => mid,
};
ed.mutate_edit(Edit::Replace {
start: Position::new(r, span.start),
end: Position::new(r, span.end),
with: transformed,
});
}
let saved_yank = ed.yank();
let saved_linewise = ed.yank_linewise();
let start =
block_start_col(ed, top, cols).min(buf_line_chars(ed.buffer(), top).saturating_sub(1));
buf_set_cursor_rc(ed.buffer_mut(), top, start);
ed.set_yank(saved_yank);
ed.set_yank_linewise(saved_linewise);
}
pub fn block_start_col<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
row: usize,
cols: BlockCols,
) -> usize {
let line = buf_line(ed.buffer(), row).unwrap_or_default();
block_row(&line, cols, ed.settings().tabstop, true).start
}
pub fn block_cols_at<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
row: usize,
col: usize,
width: usize,
to_eol: bool,
) -> BlockCols {
let line = buf_line(ed.buffer(), row).unwrap_or_default();
let left = hjkl_buffer::char_col_to_visual_col(&line, col, ed.settings().tabstop);
BlockCols {
left,
right: if to_eol {
usize::MAX
} else {
left + width.max(1) - 1
},
}
}
pub fn block_yank<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
top: usize,
bot: usize,
cols: BlockCols,
) -> String {
let tabstop = ed.settings().tabstop;
let rope = hjkl_engine::types::Query::rope(ed.buffer());
let n = rope.len_lines();
let mut rows: Vec<String> = Vec::new();
for r in top..=bot {
if r >= n {
break;
}
let line = rope_line_to_str(&rope, r);
let span = block_row(&line, cols, tabstop, false);
let mut text = " ".repeat(span.startspaces);
text.extend(line.chars().skip(span.start).take(span.end - span.start));
text.push_str(&" ".repeat(span.endspaces));
rows.push(text);
}
rows.join("\n")
}
pub fn block_register_width(yank: &str, cols: BlockCols) -> usize {
if cols.right == usize::MAX {
yank.split('\n')
.map(|s| s.chars().count())
.max()
.unwrap_or(0)
} else {
cols.right + 1 - cols.left
}
}
pub fn delete_block_contents<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
top: usize,
bot: usize,
cols: BlockCols,
) {
use hjkl_buffer::{Edit, Position};
ed.sync_buffer_content_from_textarea();
let tabstop = ed.settings().tabstop;
let last_row = bot.min(buf_row_count(ed.buffer()).saturating_sub(1));
for row in top..=last_row {
let line = buf_line(ed.buffer(), row).unwrap_or_default();
let span = block_row(&line, cols, tabstop, true);
let spaces = span.startspaces + span.endspaces;
if span.start == span.end && spaces == 0 {
continue;
}
ed.mutate_edit(Edit::Replace {
start: Position::new(row, span.start),
end: Position::new(row, span.end),
with: " ".repeat(spaces),
});
}
}
pub fn block_replace<H: hjkl_engine::types::Host>(ed: &mut Editor<hjkl_buffer::View, H>, ch: char) {
let (top, bot, cols) = block_rect(ed);
let to_eol = vim(ed).block_to_eol;
if !vim(ed).replaying {
vim_mut(ed).last_change = Some(LastChange::VisualBlockReplace {
ch,
rows: bot - top + 1,
cols: if to_eol {
1
} else {
cols.right + 1 - cols.left
},
to_eol,
});
}
block_replace_bounds(ed, ch, top, bot, cols);
}
pub fn block_replace_bounds<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
ch: char,
top: usize,
bot: usize,
cols: BlockCols,
) {
ed.push_undo();
ed.sync_buffer_content_from_textarea();
use hjkl_buffer::{Edit, Position};
let tabstop = ed.settings().tabstop;
let rope = hjkl_engine::types::Query::rope(ed.buffer());
for r in top..=bot.min(rope.len_lines().saturating_sub(1)) {
let line = rope_line_to_str(&rope, r);
let span = block_row(&line, cols, tabstop, true);
let end_vcol = hjkl_buffer::char_col_to_visual_col(&line, span.end, tabstop);
let short = cols.right == usize::MAX || end_vcol <= cols.right;
let cells = if short {
end_vcol.saturating_sub(cols.left)
} else {
cols.right + 1 - cols.left
};
if span.start == span.end || cells == 0 {
continue;
}
let mut with = " ".repeat(span.startspaces);
with.extend(std::iter::repeat_n(ch, cells));
if !short {
with.push_str(&" ".repeat(span.endspaces));
}
ed.mutate_edit(Edit::Replace {
start: Position::new(r, span.start),
end: Position::new(r, span.end),
with,
});
}
vim_mut(ed).mode = Mode::Normal;
let start =
block_start_col(ed, top, cols).min(buf_line_chars(ed.buffer(), top).saturating_sub(1));
ed.jump_cursor(top, start);
}
pub fn replicate_block_text<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
inserted: &str,
top: usize,
bot: usize,
col: usize,
pad: bool,
to_eol: bool,
) {
use hjkl_buffer::{Edit, Position};
let tabstop = ed.settings().tabstop;
let top_line = buf_line(ed.buffer(), top).unwrap_or_default();
let vcol = hjkl_buffer::char_col_to_visual_col(&top_line, col, tabstop);
for r in (top + 1)..=bot {
if !to_eol {
let line = buf_line(ed.buffer(), r).unwrap_or_default();
if let Some((tab, start, width)) = tab_cut_at(&line, vcol, tabstop) {
ed.mutate_edit(Edit::Replace {
start: Position::new(r, tab),
end: Position::new(r, tab + 1),
with: " ".repeat(width),
});
ed.mutate_edit(Edit::InsertStr {
at: Position::new(r, tab + vcol - start),
text: inserted.to_string(),
});
continue;
}
}
let line_len = buf_line_chars(ed.buffer(), r);
let line = buf_line(ed.buffer(), r).unwrap_or_default();
let row_col = if to_eol {
line_len
} else {
match char_at_vcol(&line, vcol, tabstop) {
Ok(c) => c,
Err(short) => {
if !pad {
continue;
}
ed.mutate_edit(Edit::InsertStr {
at: Position::new(r, line_len),
text: " ".repeat(short),
});
line_len + short
}
}
};
ed.mutate_edit(Edit::InsertStr {
at: Position::new(r, row_col),
text: inserted.to_string(),
});
}
}
pub fn replay_block_visual_op<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
op: Operator,
rows: usize,
cols: usize,
to_eol: bool,
inserted: Option<String>,
levels: usize,
) {
let (top, left) = ed.cursor();
let bot = top + rows.saturating_sub(1);
let cols = block_cols_at(ed, top, left, cols, to_eol);
let record_delete = |ed: &mut Editor<hjkl_buffer::View, H>| {
let yank = block_yank(ed, top, bot, cols);
if !yank.is_empty() {
let block_width = block_register_width(&yank, cols);
ed.record_yank_to_host(yank.clone());
let target = vim_mut(ed).pending_register.take();
ed.record_delete_block(yank, block_width, target);
}
};
match op {
Operator::Delete => {
ed.push_undo();
record_delete(ed);
delete_block_contents(ed, top, bot, cols);
let line_len = buf_line_chars(ed.buffer(), top);
ed.jump_cursor(top, left.min(line_len.saturating_sub(1)));
}
Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase | Operator::Rot13 => {
ed.push_undo();
transform_block_case(ed, op, top, bot, cols);
ed.jump_cursor(top, left);
}
Operator::Indent => {
ed.push_undo();
indent_block(ed, top, bot, cols, levels);
}
Operator::Outdent => {
ed.push_undo();
outdent_block(ed, top, bot, cols, levels);
}
Operator::Change => {
ed.push_undo();
record_delete(ed);
let top_line = buf_line(ed.buffer(), top).unwrap_or_default();
let top_span = block_row(&top_line, cols, ed.settings().tabstop, true);
let left = top_span.start + top_span.startspaces;
delete_block_contents(ed, top, bot, cols);
let text = inserted.unwrap_or_default();
if text.is_empty() {
ed.jump_cursor(top, left);
} else {
use hjkl_buffer::{Edit, Position};
ed.mutate_edit(Edit::InsertStr {
at: Position::new(top, left),
text: text.clone(),
});
replicate_block_text(ed, &text, top, bot, left, false, false);
let ins_chars = text.chars().count();
let line_len = buf_line_chars(ed.buffer(), top);
ed.jump_cursor(
top,
(left + ins_chars)
.saturating_sub(1)
.min(line_len.saturating_sub(1)),
);
}
}
_ => {}
}
vim_mut(ed).mode = Mode::Normal;
}
pub fn replay_block_replace<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
ch: char,
rows: usize,
cols: usize,
to_eol: bool,
) {
let (top, left) = ed.cursor();
let bot = top + rows.saturating_sub(1);
let cols = block_cols_at(ed, top, left, cols, to_eol);
block_replace_bounds(ed, ch, top, bot, cols);
}
pub fn replay_block_insert<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
text: &str,
rows: usize,
cols: usize,
to_eol: bool,
append: bool,
) {
use hjkl_buffer::{Edit, Position};
let (top, left) = ed.cursor();
let bot = top + rows.saturating_sub(1);
ed.push_undo();
ed.sync_buffer_content_from_textarea();
let line_len = buf_line_chars(ed.buffer(), top);
let top_col = if append {
if to_eol {
line_len
} else {
let line = buf_line(ed.buffer(), top).unwrap_or_default();
let tabstop = ed.settings().tabstop;
let target = hjkl_buffer::char_col_to_visual_col(&line, left, tabstop) + cols;
match char_at_vcol(&line, target, tabstop) {
Ok(c) => c,
Err(short) => {
ed.mutate_edit(Edit::InsertStr {
at: Position::new(top, line_len),
text: " ".repeat(short),
});
line_len + short
}
}
}
} else {
left
};
ed.mutate_edit(Edit::InsertStr {
at: Position::new(top, top_col),
text: text.to_string(),
});
replicate_block_text(ed, text, top, bot, top_col, append, to_eol);
ed.jump_cursor(top, left);
vim_mut(ed).mode = Mode::Normal;
}
pub fn visual_replace_char<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
ch: char,
) {
let linewise = matches!(vim(ed).mode, Mode::VisualLine);
let (top, bot) = if linewise {
let cursor_row = buf_cursor_pos(ed.buffer()).row;
let anchor_row = vim(ed).visual_line_anchor;
(
(cursor_row.min(anchor_row), 0),
(cursor_row.max(anchor_row), 0),
)
} else {
let anchor = vim(ed).visual_anchor;
let cursor = ed.cursor();
order(anchor, cursor)
};
if !vim(ed).replaying {
let extent = if linewise {
VisualExtent::Line {
lines: bot.0 - top.0 + 1,
}
} else {
charwise_extent(top, bot)
};
vim_mut(ed).last_change = Some(LastChange::VisualReplace { ch, extent });
}
let start = if !linewise {
top
} else if ed.cursor().0 < vim(ed).visual_line_anchor {
ed.cursor()
} else {
top
};
buf_set_cursor_rc(ed.buffer_mut(), start.0, start.1);
replace_range_with_char(ed, linewise, top, bot, ch);
}
fn replace_range_with_char<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
linewise: bool,
top: (usize, usize),
bot: (usize, usize),
ch: char,
) {
ed.push_undo();
ed.sync_buffer_content_from_textarea();
use hjkl_buffer::{Edit, Position};
let rope = hjkl_engine::types::Query::rope(ed.buffer());
let last_row = bot.0.min(rope.len_lines().saturating_sub(1));
for row in top.0..=last_row {
let chars: Vec<char> = rope_line_to_str(&rope, row).chars().collect();
let lo = if !linewise && row == top.0 { top.1 } else { 0 };
let hi = if !linewise && row == bot.0 {
(bot.1 + 1).min(chars.len())
} else {
chars.len()
};
if lo >= hi {
continue;
}
let middle: String = std::iter::repeat_n(ch, hi - lo).collect();
ed.mutate_edit(Edit::Replace {
start: Position::new(row, lo),
end: Position::new(row, hi),
with: middle,
});
}
vim_mut(ed).mode = Mode::Normal;
ed.jump_cursor(top.0, if linewise { 0 } else { top.1 });
}
pub fn replay_visual_replace<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
ch: char,
extent: VisualExtent,
) {
let (r, c) = ed.cursor();
match extent {
VisualExtent::Line { lines } => {
let top = (r, 0);
let bot = (r + lines.saturating_sub(1), 0);
replace_range_with_char(ed, true, top, bot, ch);
}
VisualExtent::Char { lines, width } => {
let top = (r, c);
let bot = if lines <= 1 {
(r, c + width.saturating_sub(1))
} else {
(r + lines - 1, width.saturating_sub(1))
};
replace_range_with_char(ed, false, top, bot, ch);
}
VisualExtent::Block { .. } => {}
}
}