feat: add counted word change actions

Add cw/cb and configurable core actions using the same line-local spans as dw/db. Enter Insert with grouped replacement undo and native dot repeat.
This commit is contained in:
2026-10-13 17:36:15 +02:00
parent 983f43e710
commit e84d70463f
6 changed files with 140 additions and 15 deletions
+94 -3
View File
@@ -191,7 +191,7 @@ fn substitute_deletes_counted_whole_graphemes_and_groups_replacement_with_undo()
}
#[test]
fn word_delete_counts_before_and_after_operator_multiply_in_both_directions() {
fn word_cut_counts_before_and_after_operator_multiply_in_both_directions() {
let source = "one two three four five six seven eight nine\n";
for (position, command, expected) in [
("", "dw", "two three four five six seven eight nine\n"),
@@ -202,18 +202,66 @@ fn word_delete_counts_before_and_after_operator_multiply_in_both_directions() {
("4w", "d2b", "one two five six seven eight nine\n"),
("4w", "2db", "one two five six seven eight nine\n"),
("6w", "2d3b", "seven eight nine\n"),
] {
for operator in ["d", "c"] {
let fixture = Fixture::new();
let mut editor = fixture.editor(source);
keys(&mut editor, position);
let caret = editor.cursor();
let command = command.replacen('d', operator, 1);
keys(&mut editor, &command);
assert_eq!(editor.context().text, expected, "{position} {command}");
assert_eq!(
editor.mode(),
if operator == "c" {
Mode::Insert
} else {
Mode::Normal
}
);
if operator == "c" {
keys(&mut editor, "Q");
editor.handle(Input::Escape);
}
keys(&mut editor, "u");
assert_eq!(editor.context().text, source);
assert_eq!(editor.cursor(), caret);
assert!(!editor.dirty());
}
}
}
#[test]
fn word_changes_preserve_line_boundaries_and_group_replacement_with_undo() {
for (source, position, command, after_cut, expected) in [
("one two\nnext\n", "", "cw", "two\nnext\n", "Qtwo\nnext\n"),
("one two\nnext\n", "w", "cb", "two\nnext\n", "Qtwo\nnext\n"),
("one\ntwo\n", "", "c9w", "\ntwo\n", "Q\ntwo\n"),
("one\ntwo\n", "j0", "c9b", "one\ntwo\n", "one\nQtwo\n"),
("one\n\ntwo\n", "j", "cw", "one\n\ntwo\n", "one\nQ\ntwo\n"),
("one\n\ntwo\n", "j", "cb", "one\n\ntwo\n", "one\nQ\ntwo\n"),
("one\n \ntwo\n", "j", "cw", "one\n\ntwo\n", "one\nQ\ntwo\n"),
("one \nnext\n", "", "cw", "\nnext\n", "Q\nnext\n"),
("e\u{301}lan 世界 fin\n", "", "c2w", "fin\n", "Qfin\n"),
("e\u{301}lan 世界", "$", "c9b", "界", "Q界"),
("👩‍🚀!!! next\n", "", "cw", "next\n", "Qnext\n"),
] {
let fixture = Fixture::new();
let mut editor = fixture.editor(source);
keys(&mut editor, position);
let caret = editor.cursor();
keys(&mut editor, command);
assert_eq!(editor.context().text, after_cut, "{position} {command}");
assert_eq!(editor.mode(), Mode::Insert);
keys(&mut editor, "Q");
editor.handle(Input::Escape);
assert_eq!(editor.context().text, expected, "{position} {command}");
assert_eq!(editor.mode(), Mode::Normal);
keys(&mut editor, "u");
assert_eq!(editor.context().text, source);
assert_eq!(editor.cursor(), caret);
assert!(!editor.dirty());
keys(&mut editor, "U");
assert_eq!(editor.context().text, expected);
}
}
@@ -306,7 +354,7 @@ fn word_delete_handles_eof_and_saturating_counts_without_splitting_graphemes() {
#[test]
fn pending_operator_and_replace_cancellation_clear_counts_without_editing() {
for prefix in ["3d", "d2", "2d3", "3c", "c2", "3r"] {
for prefix in ["3d", "d2", "2d3", "3c", "c2", "2c3", "3r"] {
let fixture = Fixture::new();
let source = "abcd\nnext\n";
let mut editor = fixture.editor(source);
@@ -387,6 +435,34 @@ fn new_edit_recipes_repeat_against_the_current_text_and_undo_as_single_changes()
"j02w",
"keep\nstay\n",
),
(
"one two keep\nthree four stay\n",
"c2wQ",
"Qkeep\nthree four stay\n",
"j0",
"Qkeep\nQstay\n",
),
(
"one two keep\nthree four stay\n",
"2wc2bQ",
"Qkeep\nthree four stay\n",
"j02w",
"Qkeep\nQstay\n",
),
(
"one\nthree four\n",
"c9wQ",
"Q\nthree four\n",
"j0",
"Q\nQ\n",
),
(
"one keep\nthree four stay\n",
"wc9bQ",
"Qkeep\nthree four stay\n",
"j02w",
"Qkeep\nQstay\n",
),
] {
let fixture = Fixture::new();
let mut editor = fixture.editor(source);
@@ -474,12 +550,19 @@ fn native_multicarets_edit_whole_graphemes_and_yank_matching_suffix_fragments()
("2wd2b", "keep\nstay\n"),
("d9w", "\n\n"),
("2wd9b", "keep\nstay\n"),
("c2wQ", "Qkeep\nQstay\n"),
("2wc2bQ", "Qkeep\nQstay\n"),
("c9wQ", "Q\nQ\n"),
("2wc9bQ", "Qkeep\nQstay\n"),
] {
let fixture = Fixture::new();
let source = "one two keep\nthree four stay\n";
let mut editor = fixture.editor(source);
editor.handle(Input::Ctrl('e'));
keys(&mut editor, command);
if editor.mode() == Mode::Insert {
editor.handle(Input::Escape);
}
assert_eq!(editor.context().text, expected, "{command}");
assert_eq!(editor.selection_count(), 2);
keys(&mut editor, "u");
@@ -516,6 +599,14 @@ fn remapped_core_edit_actions_keep_counts_pending_input_and_clipboard_semantics(
),
("core.delete_word_forward", "d", "", "2q", "ghi\nnext\n"),
("core.delete_word_backward", "d", "2w", "2q", "ghi\nnext\n"),
("core.change_word_forward", "c", "", "2qQ", "Qghi\nnext\n"),
(
"core.change_word_backward",
"c",
"2w",
"2qQ",
"Qghi\nnext\n",
),
] {
let fixture = Fixture::new();
let mut editor = fixture.editor("abc def ghi\nnext\n");