use hjkl_vim_types::{InsertReason, LastChange, Mode, Motion, Operator, RangeKind, TextObject};
use super::*;
use crate::vim_state::{vim, vim_mut};
use hjkl_engine::Editor;
use hjkl_engine::buf_helpers::{buf_line, buf_line_chars, buf_row_count, buf_set_cursor_rc};
pub fn apply_op_with_motion<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
op: Operator,
motion: &Motion,
count: usize,
) {
let start = ed.cursor();
let find_repeat = vim(ed).find_repeat_skip;
let mut sentence_inclusive = false;
if matches!(motion, Motion::SentenceNext) {
let Some((pos, inclusive)) =
crate::vim::text_object::sentence_forward_for_operator(ed, count)
else {
return;
};
ed.jump_cursor(pos.0, pos.1);
sentence_inclusive = inclusive;
} else {
apply_motion_cursor_ctx(ed, motion, count, true);
}
let mut end = ed.cursor();
if start.0 == end.0
&& matches!(
motion,
Motion::Up
| Motion::Down
| Motion::ScreenUp
| Motion::ScreenDown
| Motion::FirstNonBlankNextLine
| Motion::FirstNonBlankPrevLine
)
{
return;
}
let mut kind = motion_kind(motion);
let last_row = last_content_row(ed);
let last_len = buf_line_chars(ed.buffer(), last_row);
let lands_past_the_end = match motion {
Motion::ParagraphNext => end.0 == last_row && end.1 + 1 == last_len,
Motion::SentenceNext => sentence_inclusive,
_ => false,
};
if lands_past_the_end && kind == RangeKind::Exclusive && last_len > 0 {
if start == end {
end = (end.0, end.1 + 1);
} else {
kind = RangeKind::Inclusive;
}
}
if start == end
&& end.1 < buf_line_chars(ed.buffer(), end.0)
&& succeeds_in_place(ed, motion, count, find_repeat)
{
end = (end.0, end.1 + 1);
kind = RangeKind::Exclusive;
}
if op == Operator::Change
&& start == end
&& kind != RangeKind::Linewise
&& change_survives_in_place(motion, count)
{
run_operator_over_range(ed, op, start, end, RangeKind::Exclusive);
return;
}
let word_fwd = matches!(motion, Motion::WordFwd | Motion::BigWordFwd);
operate_over_motion_range(ed, op, start, end, kind, word_fwd);
}
fn change_survives_in_place(motion: &Motion, count: usize) -> bool {
match motion {
Motion::SentencePrev
| Motion::ParagraphPrev
| Motion::SentenceNext
| Motion::ParagraphNext
| Motion::LineEnd
| Motion::ScreenLineEnd => count <= 1,
_ => matches!(
motion,
Motion::Left
| Motion::Right
| Motion::BackspaceBack
| Motion::SpaceFwd
| Motion::LineStart
| Motion::FirstNonBlank
| Motion::ScreenLineStart
| Motion::ScreenLineFirstNonBlank
| Motion::GotoColumn
| Motion::WordFwd
| Motion::BigWordFwd
| Motion::WordEnd
| Motion::BigWordEnd
| Motion::ChangeWordEnd { .. }
),
}
}
fn succeeds_in_place<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
motion: &Motion,
count: usize,
find_repeat: bool,
) -> bool {
match *motion {
Motion::LineEnd | Motion::LastNonBlank | Motion::ScreenLineEnd | Motion::MatchBracket => {
count <= 1
}
Motion::WordEnd | Motion::BigWordEnd | Motion::ChangeWordEnd { .. } => true,
Motion::Find {
ch,
forward,
till: true,
} if count <= 1 && !find_repeat => {
let (row, col) = ed.cursor();
let line: Vec<char> = buf_line(ed.buffer(), row)
.unwrap_or_default()
.chars()
.collect();
let adjacent = if forward {
line.get(col + 1)
} else {
col.checked_sub(1).and_then(|c| line.get(c))
};
adjacent == Some(&ch)
}
_ => false,
}
}
pub fn apply_op_mark<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
op: Operator,
ch: char,
linewise: bool,
) {
let target = if ch.is_ascii_uppercase() {
same_buffer_global_mark_pos(ed, ch)
} else {
mark_pos(ed, ch)
};
let Some(target) = target else {
return;
};
let register = vim(ed).pending_register;
if !vim(ed).replaying && op_is_change(op) {
vim_mut(ed).last_change = Some(LastChange::OpMark {
op,
ch,
linewise,
inserted: None,
register,
});
}
let start = ed.cursor();
let (row, col) = clamp_pos(ed, target);
let (end, kind) = if linewise {
buf_set_cursor_rc(ed.buffer_mut(), row, 0);
move_first_non_whitespace(ed);
(ed.cursor(), RangeKind::Linewise)
} else {
((row, col), RangeKind::Exclusive)
};
operate_over_motion_range(ed, op, start, end, kind, false);
}
fn operate_over_motion_range<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
op: Operator,
mut start: (usize, usize),
mut end: (usize, usize),
mut kind: RangeKind,
word_fwd: bool,
) {
let cursor_restore = if matches!(op, Operator::Reflow | Operator::ReflowKeepCursor) {
start
} else {
start.min(end)
};
if start == end && !matches!(kind, RangeKind::Linewise) {
return;
}
if word_fwd
&& kind == RangeKind::Exclusive
&& end.0 > start.0
&& let Some(word_end) = last_word_end_before(ed, start, end)
&& word_end.0 < end.0
{
end = (word_end.0, buf_line_chars(ed.buffer(), word_end.0));
}
let mut end_adjusted = false;
if kind == RangeKind::Exclusive {
let forward = end.0 >= start.0;
let (top, bot) = if forward { (start, end) } else { (end, start) };
if bot.1 == 0 && bot.0 > top.0 {
let prev = bot.0 - 1;
let prev_len = buf_line_chars(ed.buffer(), prev);
let indent = buf_line(ed.buffer(), top.0)
.unwrap_or_default()
.chars()
.take_while(|c| *c == ' ' || *c == '\t')
.count();
let new_bot = if top.1 <= indent {
kind = RangeKind::Linewise;
(prev, 0)
} else if prev_len > 0 {
kind = RangeKind::Inclusive;
(prev, prev_len - 1)
} else {
(prev, 0)
};
if forward {
end = new_bot;
} else {
start = new_bot;
}
end_adjusted = true;
}
}
if op != Operator::Comment {
let (top, bot) = order(start, end);
if kind == RangeKind::Linewise {
let (fold_top, fold_bot) = expand_linewise_over_closed_folds(ed.buffer(), top.0, bot.0);
let (new_top, new_bot) = ((fold_top, top.1), (fold_bot, bot.1));
if start == top {
(start, end) = (new_top, new_bot);
} else {
(start, end) = (new_bot, new_top);
}
} else if start < end {
let start_fold = expand_linewise_over_closed_folds(ed.buffer(), start.0, start.0);
if start_fold != (start.0, start.0) {
start = (start_fold.0, 0);
}
let (_, fold_end) = expand_linewise_over_closed_folds(ed.buffer(), end.0, end.0);
if fold_end > end.0 && (end.1 > 0 || kind == RangeKind::Inclusive) {
let last_col = buf_line_chars(ed.buffer(), fold_end).saturating_sub(1);
kind = RangeKind::Inclusive;
end = (fold_end, last_col);
}
}
}
if delete_goes_linewise(ed, op, start, end, kind) {
kind = RangeKind::Linewise;
}
ed.jump_cursor(cursor_restore.0, cursor_restore.1);
if op == Operator::Comment {
let top = start.0.min(end.0);
let bot = start.0.max(end.0);
ed.toggle_comment_range(top, bot);
vim_mut(ed).mode = Mode::Normal;
return;
}
run_operator_over_range(ed, op, start, end, kind);
if op == Operator::Reflow && end_adjusted {
let row = ed.cursor().0;
if row < last_content_row(ed) {
buf_set_cursor_rc(ed.buffer_mut(), row + 1, 0);
move_first_non_whitespace(ed);
}
}
}
pub fn last_word_end_before<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
start: (usize, usize),
end: (usize, usize),
) -> Option<(usize, usize)> {
for r in (start.0..=end.0).rev() {
let line = buf_line(ed.buffer(), r).unwrap_or_default();
let lo = if r == start.0 { start.1 } else { 0 };
let hi = if r == end.0 {
end.1
} else {
line.chars().count()
};
let last = line
.chars()
.enumerate()
.filter(|(i, ch)| *i >= lo && *i < hi && *ch != ' ' && *ch != '\t')
.map(|(i, _)| i)
.last();
if let Some(col) = last {
return Some((r, col));
}
}
None
}
fn delete_goes_linewise<H: hjkl_engine::types::Host>(
ed: &Editor<hjkl_buffer::View, H>,
op: Operator,
start: (usize, usize),
end: (usize, usize),
kind: RangeKind,
) -> bool {
if op != Operator::Delete || kind == RangeKind::Linewise {
return false;
}
let (top, bot) = order(start, end);
if bot.0 <= top.0 {
return false;
}
let bot_line = buf_line(ed.buffer(), bot.0).unwrap_or_default();
let bot_chars: Vec<char> = bot_line.chars().collect();
let mut idx = bot.1;
if idx < bot_chars.len() && kind == RangeKind::Inclusive {
idx += 1;
}
let tail_blank = bot_chars[idx.min(bot_chars.len())..]
.iter()
.all(|c| *c == ' ' || *c == '\t');
let indent = buf_line(ed.buffer(), top.0)
.unwrap_or_default()
.chars()
.take_while(|c| *c == ' ' || *c == '\t')
.count();
tail_blank && top.1 <= indent
}
pub fn apply_op_with_text_object<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
op: Operator,
obj: TextObject,
inner: bool,
count: usize,
) {
let count = count.min(MAX_COUNT);
let Some((mut start, mut end, mut kind)) = text_object_range(ed, obj, inner, count) else {
if let TextObject::Word { big } = obj
&& let Some((row, col)) = word_object_stop(ed, inner, big, count)
{
ed.jump_cursor(row, col);
clamp_cursor_to_normal_mode(ed);
}
return;
};
if inner
&& matches!(obj, TextObject::Bracket(_))
&& kind == RangeKind::Exclusive
&& end.0 > start.0
&& end.1 == 0
{
let prev = end.0 - 1;
let prev_len = buf_line_chars(ed.buffer(), prev);
let fnb = buf_line(ed.buffer(), start.0)
.unwrap_or_default()
.chars()
.take_while(|c| *c == ' ' || *c == '\t')
.count();
if start.1 <= fnb {
start = (start.0, 0);
end = (prev, prev_len);
kind = RangeKind::Linewise;
} else {
end = (prev, prev_len.saturating_sub(1));
kind = RangeKind::Inclusive;
}
}
let (judged_end, judged_kind) = if kind == RangeKind::Exclusive && end.1 == 0 && end.0 > start.0
{
let prev = end.0 - 1;
let last = buf_line_chars(ed.buffer(), prev).saturating_sub(1);
((prev, last), RangeKind::Inclusive)
} else {
(end, kind)
};
if delete_goes_linewise(ed, op, start, judged_end, judged_kind) {
end = judged_end;
kind = RangeKind::Linewise;
}
ed.jump_cursor(start.0, start.1);
run_operator_over_range(ed, op, start, end, kind);
}
pub fn motion_kind(motion: &Motion) -> RangeKind {
match motion {
Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => RangeKind::Linewise,
Motion::FileTop | Motion::FileBottom => RangeKind::Linewise,
Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
RangeKind::Linewise
}
Motion::WordEnd
| Motion::BigWordEnd
| Motion::ChangeWordEnd { .. }
| Motion::WordEndBack
| Motion::BigWordEndBack => RangeKind::Inclusive,
Motion::Find { .. } => RangeKind::Inclusive,
Motion::MatchBracket => RangeKind::Inclusive,
Motion::UnmatchedBracket { .. } => RangeKind::Exclusive,
Motion::LineEnd => RangeKind::Inclusive,
Motion::LastNonBlank | Motion::ScreenLineEnd => RangeKind::Inclusive,
Motion::FirstNonBlankNextLine
| Motion::FirstNonBlankPrevLine
| Motion::FirstNonBlankLine => RangeKind::Linewise,
Motion::SectionBackward
| Motion::SectionForward
| Motion::SectionEndBackward
| Motion::SectionEndForward => RangeKind::Exclusive,
_ => RangeKind::Exclusive,
}
}
pub fn change_linewise_rows<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
top_row: usize,
end_row: usize,
) {
use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
vim_mut(ed).change_mark_start = Some((top_row, 0));
ed.push_undo();
ed.sync_buffer_content_from_textarea();
let payload = read_vim_range(ed, (top_row, 0), (end_row, 0), RangeKind::Linewise);
if end_row > top_row {
ed.mutate_edit(Edit::DeleteRange {
start: Position::new(top_row + 1, 0),
end: Position::new(end_row, 0),
kind: BufKind::Line,
});
}
let indent_chars = if ed.settings().autoindent {
let line =
hjkl_buffer::rope_line_str(&hjkl_engine::types::Query::rope(ed.buffer()), top_row);
line.chars().take_while(|c| *c == ' ' || *c == '\t').count()
} else {
0
};
let line_chars = buf_line_chars(ed.buffer(), top_row);
if line_chars > indent_chars {
ed.mutate_edit(Edit::DeleteRange {
start: Position::new(top_row, indent_chars),
end: Position::new(top_row, line_chars),
kind: BufKind::Char,
});
}
if !payload.is_empty() {
ed.record_yank_to_host(payload.clone());
let target = vim_mut(ed).pending_register.take();
ed.record_delete(payload, true, target);
}
buf_set_cursor_rc(ed.buffer_mut(), top_row, indent_chars);
begin_insert_noundo(ed, 1, InsertReason::AfterChange);
}
pub fn run_operator_over_range<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
op: Operator,
start: (usize, usize),
end: (usize, usize),
kind: RangeKind,
) {
operator_over_range_body(ed, op, start, end, kind);
if op != Operator::Fold {
remember_operator_column(ed);
}
}
fn operator_over_range_body<H: hjkl_engine::types::Host>(
ed: &mut Editor<hjkl_buffer::View, H>,
op: Operator,
start: (usize, usize),
end: (usize, usize),
kind: RangeKind,
) {
let (top, bot) = order(start, end);
if top == bot && kind == RangeKind::Exclusive {
if op == Operator::Change {
vim_mut(ed).change_mark_start = Some(top);
ed.push_undo();
begin_insert_noundo(ed, 1, InsertReason::AfterChange);
}
return;
}
match op {
Operator::Yank => {
let text = read_vim_range(ed, top, bot, kind);
if !text.is_empty() {
ed.record_yank_to_host(text.clone());
let target = vim_mut(ed).pending_register.take();
ed.record_yank(text, matches!(kind, RangeKind::Linewise), target);
}
let rbr = match kind {
RangeKind::Linewise => {
let last_col = buf_line_chars(ed.buffer(), bot.0).saturating_sub(1);
(bot.0, last_col)
}
RangeKind::Inclusive => (bot.0, bot.1),
RangeKind::Exclusive => (bot.0, bot.1.saturating_sub(1)),
};
ed.set_mark('[', top);
ed.set_mark(']', rbr);
buf_set_cursor_rc(ed.buffer_mut(), top.0, top.1);
}
Operator::Delete => {
buf_set_cursor_rc(ed.buffer_mut(), top.0, top.1);
ed.push_undo();
let total = buf_row_count(ed.buffer());
let deleted_through_last = matches!(kind, RangeKind::Linewise) && bot.0 + 1 >= total;
cut_vim_range(ed, top, bot, kind);
if !matches!(kind, RangeKind::Linewise) {
clamp_cursor_to_normal_mode(ed);
} else {
land_after_linewise_delete(ed, top.0, deleted_through_last);
}
vim_mut(ed).mode = Mode::Normal;
let pos = ed.cursor();
ed.set_mark('[', pos);
ed.set_mark(']', pos);
}
Operator::Change => {
if matches!(kind, RangeKind::Linewise) {
change_linewise_rows(ed, top.0, bot.0);
} else {
vim_mut(ed).change_mark_start = Some(top);
ed.push_undo();
cut_vim_range(ed, top, bot, kind);
begin_insert_noundo(ed, 1, InsertReason::AfterChange);
}
}
Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase | Operator::Rot13 => {
apply_case_op_to_selection(ed, op, top, bot, kind);
}
Operator::Indent | Operator::Outdent => {
ed.push_undo();
if op == Operator::Indent {
indent_rows(ed, top.0, bot.0, 1);
} else {
outdent_rows(ed, top.0, bot.0, 1);
}
vim_mut(ed).mode = Mode::Normal;
}
Operator::Fold => {
if bot.0 >= top.0 {
ed.apply_fold_op(hjkl_engine::types::FoldOp::Add {
start_row: top.0,
end_row: bot.0,
closed: true,
});
}
buf_set_cursor_rc(ed.buffer_mut(), top.0, top.1);
vim_mut(ed).mode = Mode::Normal;
}
Operator::Reflow => {
ed.push_undo();
reflow_rows(ed, top.0, bot.0);
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.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);
ed.set_sticky_col(Some(new_col));
vim_mut(ed).mode = Mode::Normal;
}
Operator::AutoIndent => {
ed.push_undo();
auto_indent_rows(ed, top.0, bot.0);
vim_mut(ed).mode = Mode::Normal;
}
Operator::Filter => {
}
Operator::Comment => {
}
}
}
#[cfg(test)]
mod fold_charwise_tests {
use hjkl_buffer::{View, rope_line_str};
use hjkl_engine::types::FoldOp;
use hjkl_engine::{DefaultHost, Editor, Options};
use hjkl_vim_types::{Mode, Motion, Operator};
use super::apply_op_with_motion;
fn make_editor(content: &str, fold: Option<(usize, usize)>) -> Editor<View, DefaultHost> {
let mut ed = crate::vim::vim_editor(
View::from_str(content),
DefaultHost::new(),
Options::default(),
);
ed.jump_cursor(0, 0);
if let Some((start, end)) = fold {
ed.apply_fold_op(FoldOp::Add {
start_row: start,
end_row: end,
closed: true,
});
}
ed
}
fn full_buffer(ed: &Editor<View, DefaultHost>) -> String {
let rope = ed.buffer().rope();
(0..rope.len_lines())
.map(|i| rope_line_str(&rope, i).to_string())
.collect::<Vec<_>>()
.join("\n")
}
#[test]
fn dw_on_closed_fold_deletes_whole_fold_linewise() {
let mut ed = make_editor("abc\ndef\nghi\n", Some((0, 1)));
apply_op_with_motion(&mut ed, Operator::Delete, &Motion::WordFwd, 1);
assert_eq!(full_buffer(&ed), "ghi\n");
assert_eq!(ed.yank(), "abc\ndef\n");
assert_eq!(ed.cursor(), (0, 0));
}
#[test]
fn yw_on_closed_fold_yanks_whole_fold_charwise() {
let mut ed = make_editor("abc\ndef\nghi\n", Some((0, 1)));
apply_op_with_motion(&mut ed, Operator::Yank, &Motion::WordFwd, 1);
assert_eq!(full_buffer(&ed), "abc\ndef\nghi\n");
assert_eq!(ed.yank(), "abc\ndef");
assert_eq!(ed.cursor(), (0, 0));
}
#[test]
fn g_upper_w_on_closed_fold_uppercases_whole_fold_linewise() {
let mut ed = make_editor("abc\ndef\nghi\n", Some((0, 1)));
apply_op_with_motion(&mut ed, Operator::Uppercase, &Motion::WordFwd, 1);
assert_eq!(full_buffer(&ed), "ABC\nDEF\nghi\n");
assert_eq!(ed.yank(), "");
}
#[test]
fn cw_on_closed_fold_changes_whole_fold_with_charwise_register() {
let mut ed = make_editor("abc\ndef\nghi\n", Some((0, 1)));
apply_op_with_motion(
&mut ed,
Operator::Change,
&Motion::ChangeWordEnd { big: false },
1,
);
assert_eq!(full_buffer(&ed), "\nghi\n");
assert_eq!(ed.yank(), "abc\ndef");
assert_eq!(ed.cursor(), (0, 0));
assert_eq!(crate::vim_state::vim(&ed).mode, Mode::Insert);
}
#[test]
fn counted_cw_on_closed_fold_walks_past_the_fold() {
let mut ed = make_editor("abc\ndef\nghi jkl\n", Some((0, 1)));
apply_op_with_motion(
&mut ed,
Operator::Change,
&Motion::ChangeWordEnd { big: false },
2,
);
assert_eq!(full_buffer(&ed), " jkl\n");
assert_eq!(ed.yank(), "abc\ndef\nghi");
}
#[test]
fn ce_on_closed_fold_runs_past_the_fold() {
let mut ed = make_editor("abc\ndef\nghi jkl\n", Some((0, 1)));
apply_op_with_motion(&mut ed, Operator::Change, &Motion::WordEnd, 1);
assert_eq!(full_buffer(&ed), " jkl\n");
assert_eq!(ed.yank(), "abc\ndef\nghi");
}
#[test]
fn de_on_closed_fold_runs_past_the_fold() {
let mut ed = make_editor("abc\ndef\nghi jkl\n", Some((0, 1)));
apply_op_with_motion(&mut ed, Operator::Delete, &Motion::WordEnd, 1);
assert_eq!(full_buffer(&ed), " jkl\n");
assert_eq!(ed.yank(), "abc\ndef\nghi");
}
#[test]
fn counted_dw_on_closed_fold_walks_past_the_fold() {
let mut ed = make_editor("abc\ndef\nghi jkl\n", Some((0, 1)));
apply_op_with_motion(&mut ed, Operator::Delete, &Motion::WordFwd, 2);
assert_eq!(full_buffer(&ed), "jkl\n");
assert_eq!(ed.yank(), "abc\ndef\nghi ");
}
#[test]
fn counted_yw_on_closed_fold_walks_past_the_fold() {
let mut ed = make_editor("abc\ndef\nghi jkl\n", Some((0, 1)));
apply_op_with_motion(&mut ed, Operator::Yank, &Motion::WordFwd, 2);
assert_eq!(full_buffer(&ed), "abc\ndef\nghi jkl\n");
assert_eq!(ed.yank(), "abc\ndef\nghi ");
}
#[test]
fn dj_ending_in_a_later_fold_takes_all_of_it() {
let mut ed = make_editor("aa\nBB\nCC\ndd\n", Some((1, 2)));
apply_op_with_motion(&mut ed, Operator::Delete, &Motion::Down, 1);
assert_eq!(full_buffer(&ed), "dd\n");
assert_eq!(ed.yank(), "aa\nBB\nCC\n");
}
#[test]
fn db_on_closed_fold_start_is_a_noop() {
let mut ed = make_editor("abc\ndef\nghi\n", Some((0, 1)));
apply_op_with_motion(&mut ed, Operator::Delete, &Motion::WordBack, 1);
assert_eq!(full_buffer(&ed), "abc\ndef\nghi\n");
assert_eq!(ed.yank(), "");
}
#[test]
fn dw_on_plain_buffer_is_unchanged_charwise() {
let mut ed = make_editor("abc\ndef\nghi\n", None);
apply_op_with_motion(&mut ed, Operator::Delete, &Motion::WordFwd, 1);
assert_eq!(full_buffer(&ed), "\ndef\nghi\n");
assert_eq!(ed.yank(), "abc");
}
#[test]
fn dw_on_single_line_fold_stays_charwise() {
let mut ed = make_editor("abc\ndef\nghi\n", Some((0, 0)));
apply_op_with_motion(&mut ed, Operator::Delete, &Motion::WordFwd, 1);
assert_eq!(full_buffer(&ed), "\ndef\nghi\n");
assert_eq!(ed.yank(), "abc");
}
#[test]
fn dw_at_col1_on_closed_fold_stays_charwise() {
let mut ed = make_editor("abc\ndef\nghi\n", Some((0, 1)));
crate::vim::execute_motion(&mut ed, Motion::Right, 1);
apply_op_with_motion(&mut ed, Operator::Delete, &Motion::WordFwd, 1);
assert_eq!(full_buffer(&ed), "a\ndef\nghi\n");
assert_eq!(ed.yank(), "bc");
}
}