use hjkl_vim_types::{InsertReason, Mode, Motion, Operator, RangeKind};
use hjkl_engine::rope_util::{rope_line_to_str, rope_to_lines_vec};
use super::*;
use crate::vim_state::{vim, vim_mut};
use hjkl_engine::Editor;
use hjkl_engine::buf_helpers::{buf_cursor_pos, buf_row_count, buf_set_cursor_rc};
pub(crate) fn apply_visual_operator<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
op: Operator,
count: usize,
) {
let levels = count.max(1);
match vim(ed).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);
ed.set_yank_linewise(true);
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);
}
buf_set_cursor_rc(ed.buffer_mut(), top, 0);
ed.push_buffer_cursor_to_textarea();
vim_mut(ed).mode = Mode::Normal;
}
Operator::Delete => {
ed.push_undo();
cut_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
vim_mut(ed).mode = Mode::Normal;
}
Operator::Change => {
change_linewise_rows(ed, top, bot);
}
Operator::Uppercase
| Operator::Lowercase
| Operator::ToggleCase
| Operator::Rot13 => {
let bot = buf_cursor_pos(ed.buffer())
.row
.max(vim(ed).visual_line_anchor);
apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), RangeKind::Linewise);
move_first_non_whitespace(ed);
}
Operator::Indent | Operator::Outdent => {
ed.push_undo();
let (cursor_row, _) = ed.cursor();
let bot = cursor_row.max(vim(ed).visual_line_anchor);
if op == Operator::Indent {
indent_rows(ed, top, bot, levels);
} else {
outdent_rows(ed, top, bot, 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) = reflow_rows_keep_cursor(ed, top, bot);
let (new_row, new_col) =
reflow_keep_cursor(top, saved.0, saved.1, &before, &after);
buf_set_cursor_rc(ed.buffer_mut(), new_row, new_col);
ed.push_buffer_cursor_to_textarea();
vim_mut(ed).mode = Mode::Normal;
}
Operator::AutoIndent => {
ed.push_undo();
let (cursor_row, _) = ed.cursor();
let bot = cursor_row.max(vim(ed).visual_line_anchor);
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);
match op {
Operator::Yank => {
let text = read_vim_range(ed, top, bot, RangeKind::Inclusive);
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);
ed.push_buffer_cursor_to_textarea();
vim_mut(ed).mode = Mode::Normal;
}
Operator::Delete => {
ed.push_undo();
cut_vim_range(ed, top, bot, RangeKind::Inclusive);
vim_mut(ed).mode = Mode::Normal;
}
Operator::Change => {
ed.push_undo();
cut_vim_range(ed, top, bot, RangeKind::Inclusive);
begin_insert_noundo(ed, 1, InsertReason::AfterChange);
}
Operator::Uppercase
| Operator::Lowercase
| Operator::ToggleCase
| Operator::Rot13 => {
let anchor = vim(ed).visual_anchor;
let cursor = ed.cursor();
let (top, bot) = order(anchor, cursor);
apply_case_op_to_selection(ed, op, top, bot, RangeKind::Inclusive);
}
Operator::Indent | Operator::Outdent => {
ed.push_undo();
let anchor = vim(ed).visual_anchor;
let cursor = ed.cursor();
let (top, bot) = order(anchor, cursor);
if op == Operator::Indent {
indent_rows(ed, top.0, bot.0, levels);
} else {
outdent_rows(ed, top.0, bot.0, 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) = 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);
buf_set_cursor_rc(ed.buffer_mut(), new_row, new_col);
ed.push_buffer_cursor_to_textarea();
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),
_ => {}
}
}
pub(crate) 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(crate) fn update_block_vcol<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
motion: &Motion,
) {
match motion {
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::FirstNonBlank
| Motion::LineEnd
| Motion::Find { .. }
| Motion::FindRepeat { .. }
| Motion::MatchBracket => {
vim_mut(ed).block_vcol = ed.cursor().1;
}
_ => {}
}
}
pub(crate) fn apply_block_operator<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
op: Operator,
count: usize,
) {
let (top, bot, left, right) = block_bounds(ed);
let yank = block_yank(ed, top, bot, left, right);
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(yank, false, target);
}
vim_mut(ed).mode = Mode::Normal;
ed.jump_cursor(top, left);
}
Operator::Delete => {
ed.push_undo();
delete_block_contents(ed, top, bot, left, right);
if !yank.is_empty() {
ed.record_yank_to_host(yank.clone());
let target = vim_mut(ed).pending_register.take();
ed.record_delete(yank, false, target);
}
vim_mut(ed).mode = Mode::Normal;
ed.jump_cursor(top, left);
}
Operator::Change => {
ed.push_undo();
delete_block_contents(ed, top, bot, left, right);
if !yank.is_empty() {
ed.record_yank_to_host(yank.clone());
let target = vim_mut(ed).pending_register.take();
ed.record_delete(yank, false, target);
}
ed.jump_cursor(top, left);
begin_insert_noundo(
ed,
1,
InsertReason::BlockChange {
top,
bot,
col: left,
},
);
}
Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase | Operator::Rot13 => {
ed.push_undo();
transform_block_case(ed, op, top, bot, left, right);
vim_mut(ed).mode = Mode::Normal;
ed.jump_cursor(top, left);
}
Operator::Indent | Operator::Outdent => {
ed.push_undo();
if op == Operator::Indent {
indent_rows(ed, top, bot, count.max(1));
} else {
outdent_rows(ed, top, bot, count.max(1));
}
vim_mut(ed).mode = Mode::Normal;
}
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) = reflow_rows_keep_cursor(ed, top, bot);
let (new_row, new_col) = reflow_keep_cursor(top, saved.0, saved.1, &before, &after);
buf_set_cursor_rc(ed.buffer_mut(), new_row, new_col);
ed.push_buffer_cursor_to_textarea();
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(crate) fn transform_block_case<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
op: Operator,
top: usize,
bot: usize,
left: usize,
right: usize,
) {
let mut lines: Vec<String> = rope_to_lines_vec(&hjkl_engine::types::Query::rope(ed.buffer()));
for r in top..=bot.min(lines.len().saturating_sub(1)) {
let chars: Vec<char> = lines[r].chars().collect();
if left >= chars.len() {
continue;
}
let end = (right + 1).min(chars.len());
let head: String = chars[..left].iter().collect();
let mid: String = chars[left..end].iter().collect();
let tail: String = chars[end..].iter().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,
};
lines[r] = format!("{head}{transformed}{tail}");
}
let saved_yank = ed.yank().to_string();
let saved_linewise = ed.yank_linewise();
ed.restore(lines, (top, left));
ed.set_yank(saved_yank);
ed.set_yank_linewise(saved_linewise);
}
pub(crate) fn block_yank<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
top: usize,
bot: usize,
left: usize,
right: usize,
) -> String {
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 chars: Vec<char> = line.chars().collect();
let end = (right + 1).min(chars.len());
if left >= chars.len() {
rows.push(String::new());
} else {
rows.push(chars[left..end].iter().collect());
}
}
rows.join("\n")
}
pub(crate) fn delete_block_contents<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
top: usize,
bot: usize,
left: usize,
right: usize,
) {
use hjkl_buffer::{Edit, MotionKind, Position};
ed.sync_buffer_content_from_textarea();
let last_row = bot.min(buf_row_count(ed.buffer()).saturating_sub(1));
if last_row < top {
return;
}
ed.mutate_edit(Edit::DeleteRange {
start: Position::new(top, left),
end: Position::new(last_row, right),
kind: MotionKind::Block,
});
ed.push_buffer_cursor_to_textarea();
}
pub(crate) fn block_replace<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
ch: char,
) {
let (top, bot, left, right) = block_bounds(ed);
ed.push_undo();
ed.sync_buffer_content_from_textarea();
let mut lines: Vec<String> = rope_to_lines_vec(&hjkl_engine::types::Query::rope(ed.buffer()));
for r in top..=bot.min(lines.len().saturating_sub(1)) {
let chars: Vec<char> = lines[r].chars().collect();
if left >= chars.len() {
continue;
}
let end = (right + 1).min(chars.len());
let before: String = chars[..left].iter().collect();
let middle: String = std::iter::repeat_n(ch, end - left).collect();
let after: String = chars[end..].iter().collect();
lines[r] = format!("{before}{middle}{after}");
}
reset_textarea_lines(ed, lines);
vim_mut(ed).mode = Mode::Normal;
ed.jump_cursor(top, left);
}
pub(crate) fn reset_textarea_lines<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
lines: Vec<String>,
) {
let cursor = ed.cursor();
hjkl_engine::types::BufferEdit::replace_all(ed.buffer_mut(), &lines.join("\n"));
buf_set_cursor_rc(ed.buffer_mut(), cursor.0, cursor.1);
ed.mark_content_dirty();
}