fix: keep counted word deletions within the current line

Word deletion reused cross-line word motion destinations. Constrain token traversal and boundary fallbacks to each caret’s current row while preserving cross-line navigation.
This commit is contained in:
2026-10-13 17:30:00 +02:00
parent 99fa5a2aff
commit 983f43e710
5 changed files with 89 additions and 29 deletions
+66 -5
View File
@@ -218,15 +218,13 @@ fn word_delete_counts_before_and_after_operator_multiply_in_both_directions() {
}
#[test]
fn word_delete_uses_unicode_word_motion_boundaries_across_punctuation_and_rows() {
fn word_delete_uses_unicode_word_motion_boundaries_across_punctuation() {
for (source, command, expected) in [
("e\u{301}lan 世界 fin\n", "d2w", "fin\n"),
("e\u{301}lan 世界 fin\n", "2wd2b", "fin\n"),
("alpha... beta gamma\n", "d2w", "beta gamma\n"),
("alpha... beta gamma\n", "2wd2b", "beta gamma\n"),
("👩‍🚀!!! next\n", "dw", "next\n"),
("one\ntwo three\n", "d2w", "three\n"),
("one\ntwo three\n", "2wd2b", "three\n"),
] {
let fixture = Fixture::new();
let mut editor = fixture.editor(source);
@@ -237,6 +235,56 @@ fn word_delete_uses_unicode_word_motion_boundaries_across_punctuation_and_rows()
}
}
#[test]
fn word_delete_stops_at_current_line_boundaries() {
for (source, position, command, expected) in [
("one\ntwo three\n", "", "dw", "\ntwo three\n"),
("one\ntwo three\n", "", "d2w", "\ntwo three\n"),
("one two\nthree four\n", "w", "2d3w", "one \nthree four\n"),
("one\ntwo three\n", "2w", "d2b", "one\nthree\n"),
("one\n two three\n", "jw", "2d3b", "one\ntwo three\n"),
("one \ntwo\n", "", "d9w", "\ntwo\n"),
("one\n \ntwo\n", "j", "dw", "one\n\ntwo\n"),
("one\n\ntwo\n", "j", "d9w", "one\n\ntwo\n"),
("one\n\ntwo\n", "j", "d9b", "one\n\ntwo\n"),
("one\ntwo\n", "j0", "db", "one\ntwo\n"),
("one\n two\n", "jl", "db", "one\n two\n"),
(
"one\ne\u{301}lan 世界\nnext\n",
"j0",
"d9w",
"one\n\nnext\n",
),
(
"one\ne\u{301}lan 世界\nnext\n",
"j$",
"d9b",
"one\n界\nnext\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,
expected,
"{position} {command} on {source:?}"
);
assert_eq!(editor.mode(), Mode::Normal);
if source == expected {
assert_eq!(editor.cursor(), caret);
assert!(!editor.dirty());
} else {
keys(&mut editor, "u");
assert_eq!(editor.context().text, source);
assert_eq!(editor.cursor(), caret);
assert!(!editor.dirty());
}
}
}
#[test]
fn word_delete_handles_eof_and_saturating_counts_without_splitting_graphemes() {
let source = "e\u{301}lan 世界";
@@ -331,6 +379,14 @@ fn new_edit_recipes_repeat_against_the_current_text_and_undo_as_single_changes()
"j02w",
"keep\nstay\n",
),
("one\nthree four\n", "d9w", "\nthree four\n", "j0", "\n\n"),
(
"one keep\nthree four stay\n",
"wd9b",
"keep\nthree four stay\n",
"j02w",
"keep\nstay\n",
),
] {
let fixture = Fixture::new();
let mut editor = fixture.editor(source);
@@ -413,13 +469,18 @@ fn native_multicarets_edit_whole_graphemes_and_yank_matching_suffix_fragments()
assert_eq!(editor.context().text, source);
assert_eq!(editor.carets(), carets);
}
for command in ["d2w", "2wd2b"] {
for (command, expected) in [
("d2w", "keep\nstay\n"),
("2wd2b", "keep\nstay\n"),
("d9w", "\n\n"),
("2wd9b", "keep\nstay\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);
assert_eq!(editor.context().text, "keep\nstay\n", "{command}");
assert_eq!(editor.context().text, expected, "{command}");
assert_eq!(editor.selection_count(), 2);
keys(&mut editor, "u");
assert_eq!(editor.context().text, source);