use hjkl_vim_types::{
InsertEntry, InsertReason, LastChange, LastHorizontalMotion, Mode, Motion, Operator, Pending,
RangeKind,
};
use hjkl_engine::input::{Input, Key};
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(crate) fn apply_op_motion_key<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
op: Operator,
motion_key: char,
total_count: usize,
) {
let input = Input {
key: Key::Char(motion_key),
ctrl: false,
alt: false,
shift: false,
};
let Some(motion) = parse_motion(&input) else {
return;
};
let cursor_on_nonblank = {
let (r, c) = ed.cursor();
buf_line(ed.buffer(), r)
.and_then(|l| l.chars().nth(c))
.map(|ch| !ch.is_whitespace())
.unwrap_or(false)
};
let motion = match motion {
Motion::FindRepeat { reverse } => match vim(ed).last_find {
Some((ch, forward, till)) => Motion::Find {
ch,
forward: if reverse { !forward } else { forward },
till,
},
None => return,
},
Motion::WordFwd if op == Operator::Change && cursor_on_nonblank => Motion::WordEnd,
Motion::BigWordFwd if op == Operator::Change && cursor_on_nonblank => Motion::BigWordEnd,
m => m,
};
apply_op_with_motion(ed, op, &motion, total_count);
if let Motion::Find { ch, forward, till } = &motion {
vim_mut(ed).last_find = Some((*ch, *forward, *till));
}
if !vim(ed).replaying && op_is_change(op) {
vim_mut(ed).last_change = Some(LastChange::OpMotion {
op,
motion,
count: total_count,
inserted: None,
});
}
}
pub(crate) fn apply_op_double<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
op: Operator,
total_count: usize,
) {
if op == Operator::Comment {
let row = buf_cursor_pos(ed.buffer()).row;
let end_row = (row + total_count.max(1) - 1).min(ed.buffer().row_count().saturating_sub(1));
ed.toggle_comment_range(row, end_row);
vim_mut(ed).mode = Mode::Normal;
if !vim(ed).replaying {
vim_mut(ed).last_change = Some(LastChange::LineOp {
op,
count: total_count,
inserted: None,
register: None,
});
}
return;
}
let register = vim(ed).pending_register;
execute_line_op(ed, op, total_count);
if !vim(ed).replaying {
vim_mut(ed).last_change = Some(LastChange::LineOp {
op,
count: total_count,
inserted: None,
register,
});
}
}
pub(crate) fn gn_find_range<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
re: ®ex::Regex,
forward: bool,
) -> Option<(hjkl_engine::types::Pos, hjkl_engine::types::Pos)> {
use hjkl_engine::types::{Cursor, Pos, Search};
let cursor = Cursor::cursor(ed.buffer());
let contains =
Search::find_prev(ed.buffer(), cursor, re).filter(|m| m.start <= cursor && cursor < m.end);
let range = if let Some(m) = contains {
m
} else if forward {
Search::find_next(ed.buffer(), cursor, re)?
} else {
Search::find_prev(ed.buffer(), cursor, re)?
};
let end_incl = if range.end.col > 0 {
Pos::new(range.end.line, range.end.col - 1)
} else {
range.end
};
Some((range.start, end_incl))
}
pub(crate) fn gn_operate<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
op: Option<Operator>,
forward: bool,
count: usize,
) {
use hjkl_engine::types::{Cursor, Pos};
if let Some(p) = ed.last_search_pattern() {
ed.push_search_pattern(&p);
}
let Some(re) = ed.search_state().pattern.clone() else {
return;
};
ed.sync_buffer_content_from_textarea();
let Some(mut range) = gn_find_range(ed, &re, forward) else {
return;
};
for _ in 1..count.max(1) {
let past = Pos::new(range.1.line, range.1.col + 1);
Cursor::set_cursor(ed.buffer_mut(), past);
match gn_find_range(ed, &re, forward) {
Some(r) => range = r,
None => break,
}
}
let start_t = (range.0.line as usize, range.0.col as usize);
let end_t = (range.1.line as usize, range.1.col as usize);
match op {
None => {
vim_mut(ed).visual_anchor = start_t;
buf_set_cursor_rc(ed.buffer_mut(), end_t.0, end_t.1);
vim_mut(ed).mode = Mode::Visual;
vim_mut(ed).current_mode = hjkl_engine::VimMode::Visual;
}
Some(Operator::Delete) => {
ed.push_undo();
cut_vim_range(ed, start_t, end_t, RangeKind::Inclusive);
clamp_cursor_to_normal_mode(ed);
if !vim(ed).replaying {
vim_mut(ed).last_change = Some(LastChange::GnOp {
op: Operator::Delete,
forward,
inserted: None,
});
}
}
Some(Operator::Change) => {
ed.push_undo();
vim_mut(ed).change_mark_start = Some(start_t);
cut_vim_range(ed, start_t, end_t, RangeKind::Inclusive);
if !vim(ed).replaying {
vim_mut(ed).last_change = Some(LastChange::GnOp {
op: Operator::Change,
forward,
inserted: None,
});
}
begin_insert_noundo(ed, 1, InsertReason::AfterChange);
}
Some(Operator::Yank) => {
let text = read_vim_range(ed, start_t, end_t, 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(), start_t.0, start_t.1);
}
Some(other @ (Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase)) => {
ed.push_undo();
apply_case_op_to_selection(ed, other, start_t, end_t, RangeKind::Inclusive);
}
Some(_) => {}
}
}
pub(crate) fn apply_op_g_inner<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
op: Operator,
ch: char,
total_count: usize,
) {
if matches!(
op,
Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase | Operator::Rot13
) {
let op_char = match op {
Operator::Uppercase => 'U',
Operator::Lowercase => 'u',
Operator::ToggleCase => '~',
Operator::Rot13 => '?',
_ => unreachable!(),
};
if ch == op_char {
execute_line_op(ed, op, total_count);
if !vim(ed).replaying {
vim_mut(ed).last_change = Some(LastChange::LineOp {
op,
count: total_count,
inserted: None,
register: None,
});
}
return;
}
}
if ch == 'n' || ch == 'N' {
gn_operate(ed, Some(op), ch == 'n', total_count);
return;
}
let motion = match ch {
'g' => Motion::FileTop,
'e' => Motion::WordEndBack,
'E' => Motion::BigWordEndBack,
'j' => Motion::ScreenDown,
'k' => Motion::ScreenUp,
_ => return, };
apply_op_with_motion(ed, op, &motion, total_count);
if !vim(ed).replaying && op_is_change(op) {
vim_mut(ed).last_change = Some(LastChange::OpMotion {
op,
motion,
count: total_count,
inserted: None,
});
}
}
pub(crate) fn apply_after_g<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
ch: char,
count: usize,
) {
match ch {
'g' => {
let pre = ed.cursor();
if count > 1 {
ed.jump_cursor(count - 1, 0);
} else {
ed.jump_cursor(0, 0);
}
move_first_non_whitespace(ed);
ed.set_sticky_col(Some(ed.cursor().1));
if ed.cursor() != pre {
ed.push_jump(pre);
}
}
'e' => execute_motion(ed, Motion::WordEndBack, count),
'E' => execute_motion(ed, Motion::BigWordEndBack, count),
'_' => execute_motion(ed, Motion::LastNonBlank, count),
'M' => execute_motion(ed, Motion::LineMiddle, count),
'm' => execute_motion(ed, Motion::ScreenLineMiddle, count),
'v' => reenter_last_visual_bridge(ed),
'j' => execute_motion(ed, Motion::ScreenDown, count),
'k' => execute_motion(ed, Motion::ScreenUp, count),
'U' => {
vim_mut(ed).pending = Pending::Op {
op: Operator::Uppercase,
count1: count,
};
}
'u' => {
vim_mut(ed).pending = Pending::Op {
op: Operator::Lowercase,
count1: count,
};
}
'~' => {
vim_mut(ed).pending = Pending::Op {
op: Operator::ToggleCase,
count1: count,
};
}
'?' => {
vim_mut(ed).pending = Pending::Op {
op: Operator::Rot13,
count1: count,
};
}
'q' => {
vim_mut(ed).pending = Pending::Op {
op: Operator::Reflow,
count1: count,
};
}
'w' => {
vim_mut(ed).pending = Pending::Op {
op: Operator::ReflowKeepCursor,
count1: count,
};
}
'J' => {
let joins = count.max(2) - 1;
for _ in 0..joins {
ed.push_undo();
if !join_line_raw(ed) {
break;
}
}
if !vim(ed).replaying {
vim_mut(ed).last_change = Some(LastChange::JoinLine { count: joins });
}
}
'd' => {
ed.set_pending_lsp(Some(hjkl_engine::LspIntent::GotoDefinition));
}
'i' => {
if let Some((row, col)) = vim(ed).last_insert_pos {
ed.jump_cursor(row, col);
}
begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
}
'c' => {
vim_mut(ed).pending = Pending::Op {
op: Operator::Comment,
count1: count,
};
}
'p' => paste_bridge(ed, false, count.max(1), true, false),
'P' => paste_bridge(ed, true, count.max(1), true, false),
'n' => gn_operate(ed, None, true, count.max(1)),
'N' => gn_operate(ed, None, false, count.max(1)),
';' => walk_change_list(ed, -1, count.max(1)),
',' => walk_change_list(ed, 1, count.max(1)),
'*' => execute_motion(
ed,
Motion::WordAtCursor {
forward: true,
whole_word: false,
},
count,
),
'#' => execute_motion(
ed,
Motion::WordAtCursor {
forward: false,
whole_word: false,
},
count,
),
'-' => {
ed.earlier_by_steps(count.max(1));
}
'+' => {
ed.later_by_steps(count.max(1));
}
'&' => {
let cmd = match ed.last_substitute() {
Some(c) => c,
None => {
return;
}
};
let last_row = buf_row_count(ed.buffer()).saturating_sub(1) as u32;
let r = 0u32..=last_row;
let _ = hjkl_engine::substitute::apply_substitute(ed, &cmd, r);
ed.set_last_substitute(cmd);
}
_ => {}
}
}
pub(crate) fn ampersand_repeat<H: hjkl_engine::types::Host>(ed: &mut Editor<hjkl_buffer::View, H>) {
let Some(mut cmd) = ed.last_substitute() else {
return;
};
cmd.flags = hjkl_engine::substitute::SubstFlags::default();
let row = buf_cursor_pos(ed.buffer()).row as u32;
let _ = hjkl_engine::substitute::apply_substitute(ed, &cmd, row..=row);
}
pub(crate) fn apply_after_z<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
ch: char,
count: usize,
) {
use hjkl_engine::CursorScrollTarget;
let row = ed.cursor().0;
match ch {
'z' | 't' | 'b' => {
if count > 1 {
let total = buf_row_count(ed.buffer());
let target_row = (count - 1).min(total.saturating_sub(1));
ed.buffer_mut().reveal_row(target_row);
let cur_col = ed.cursor().1;
let line_chars = buf_line_chars(ed.buffer(), target_row);
let new_col = if line_chars == 0 {
0
} else {
cur_col.min(line_chars - 1)
};
buf_set_cursor_rc(ed.buffer_mut(), target_row, new_col);
}
let target = match ch {
'z' => CursorScrollTarget::Center,
't' => CursorScrollTarget::Top,
_ => CursorScrollTarget::Bottom,
};
ed.scroll_cursor_to(target);
ed.set_viewport_pinned(true);
ed.set_scroll_anim_hint(true);
}
'h' | 'l' | 'H' | 'L' if ed.host().viewport().wrap == hjkl_buffer::Wrap::None => {
let width = ed.host().viewport().width as i16;
let cols = match ch {
'h' | 'l' => count.max(1) as i16,
_ => width / 2,
};
match ch {
'h' | 'H' => ed.scroll_left(cols),
_ => ed.scroll_right(cols),
}
}
'h' | 'l' | 'H' | 'L' => {}
'o' => {
ed.apply_fold_op(hjkl_engine::types::FoldOp::OpenAt(row));
}
'c' => {
ed.apply_fold_op(hjkl_engine::types::FoldOp::CloseAt(row));
}
'a' => {
ed.apply_fold_op(hjkl_engine::types::FoldOp::ToggleAt(row));
}
'R' => {
ed.apply_fold_op(hjkl_engine::types::FoldOp::OpenAll);
}
'M' => {
ed.apply_fold_op(hjkl_engine::types::FoldOp::CloseAll);
}
'E' => {
ed.apply_fold_op(hjkl_engine::types::FoldOp::ClearAll);
}
'd' => {
ed.apply_fold_op(hjkl_engine::types::FoldOp::RemoveAt(row));
}
'f' => {
if matches!(
vim(ed).mode,
Mode::Visual | Mode::VisualLine | Mode::VisualBlock
) {
let anchor_row = match vim(ed).mode {
Mode::VisualLine => vim(ed).visual_line_anchor,
Mode::VisualBlock => vim(ed).block_anchor.0,
_ => vim(ed).visual_anchor.0,
};
let cur = ed.cursor().0;
let top = anchor_row.min(cur);
let bot = anchor_row.max(cur);
ed.apply_fold_op(hjkl_engine::types::FoldOp::Add {
start_row: top,
end_row: bot,
closed: true,
});
vim_mut(ed).mode = Mode::Normal;
} else {
vim_mut(ed).pending = Pending::Op {
op: Operator::Fold,
count1: count,
};
}
}
_ => {}
}
}
pub(crate) fn apply_find_char<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
ch: char,
forward: bool,
till: bool,
count: usize,
) {
execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
vim_mut(ed).last_find = Some((ch, forward, till));
vim_mut(ed).last_horizontal_motion = LastHorizontalMotion::FindChar;
}
#[cfg(test)]
mod indent_count_tests {
use super::*;
use hjkl_buffer::View;
use hjkl_engine::{DefaultHost, Editor, Options};
fn make_editor(content: &str) -> Editor<View, DefaultHost> {
let buf = View::from_str(content);
let mut ed = crate::vim::vim_editor(buf, DefaultHost::new(), Options::default());
ed.settings_mut().expandtab = true;
ed.settings_mut().shiftwidth = 4;
ed
}
fn content(ed: &Editor<View, DefaultHost>) -> String {
(*ed.buffer().content_joined()).clone()
}
#[test]
fn count_indent_operates_on_n_lines() {
let mut ed = make_editor("a\nb\nc\nd\ne\nf\n");
ed.jump_cursor(0, 0);
execute_line_op(&mut ed, Operator::Indent, 3);
assert_eq!(content(&ed), " a\n b\n c\nd\ne\nf\n");
}
#[test]
fn indent_noexpandtab_inserts_tab() {
let mut ed = make_editor("hello\n");
ed.settings_mut().expandtab = false;
ed.settings_mut().shiftwidth = 4;
ed.settings_mut().tabstop = 4;
ed.jump_cursor(0, 0);
execute_line_op(&mut ed, Operator::Indent, 1);
assert_eq!(content(&ed), "\thello\n");
}
#[test]
fn indent_noexpandtab_subtab_remainder_is_spaces() {
let mut ed = make_editor("hello\n");
ed.settings_mut().expandtab = false;
ed.settings_mut().shiftwidth = 2;
ed.settings_mut().tabstop = 4;
ed.jump_cursor(0, 0);
execute_line_op(&mut ed, Operator::Indent, 1);
assert_eq!(content(&ed), " hello\n");
}
#[test]
fn count_indent_clamps_to_buffer_end() {
let mut ed = make_editor("a\nb\nc\nd\ne\nf\n");
ed.jump_cursor(0, 0);
execute_line_op(&mut ed, Operator::Indent, 10);
assert_eq!(content(&ed), " a\n b\n c\n d\n e\n f\n");
}
#[test]
fn count_outdent_clamps_to_buffer_end() {
let mut ed = make_editor(" a\n b\n c\n");
ed.jump_cursor(0, 0);
execute_line_op(&mut ed, Operator::Outdent, 10);
assert_eq!(content(&ed), "a\nb\nc\n");
}
#[test]
fn count_indent_on_last_line_is_noop() {
let mut ed = make_editor("a\nb\nc\n");
ed.jump_cursor(2, 0); execute_line_op(&mut ed, Operator::Indent, 5);
assert_eq!(
content(&ed),
"a\nb\nc\n",
"5>> on last line must abort (E16)"
);
}
#[test]
fn count_indent_on_single_line_is_noop() {
let mut ed = make_editor("x\n");
ed.jump_cursor(0, 0);
execute_line_op(&mut ed, Operator::Indent, 5);
assert_eq!(content(&ed), "x\n", "5>> on the only line must abort (E16)");
}
#[test]
fn count_outdent_on_last_line_is_noop() {
let mut ed = make_editor(" a\n b\n c\n");
ed.jump_cursor(2, 0);
execute_line_op(&mut ed, Operator::Outdent, 5);
assert_eq!(content(&ed), " a\n b\n c\n");
}
#[test]
fn single_indent_on_last_line_still_works() {
let mut ed = make_editor("a\nb\nc\n");
ed.jump_cursor(2, 0);
execute_line_op(&mut ed, Operator::Indent, 1);
assert_eq!(content(&ed), "a\nb\n c\n");
}
}