Files
vamp/tests/normal_editing.rs

707 lines
24 KiB
Rust

use std::{
fs,
path::PathBuf,
sync::atomic::{AtomicU64, Ordering},
};
use vamp::editor::{Cursor, Editor, Input, Mode};
static NEXT: AtomicU64 = AtomicU64::new(0);
struct Fixture(PathBuf);
impl Fixture {
fn new() -> Self {
let root = std::env::temp_dir().join(format!(
"vamp-normal-editing-{}-{}",
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed),
));
fs::create_dir_all(&root).unwrap();
Self(root)
}
fn editor(&self, text: &str) -> Editor {
let path = self.0.join("buffer.txt");
fs::write(&path, text).unwrap();
let mut editor = Editor::open(Some(path), vec!["text".into()]).unwrap();
editor.set_clipboard_mode("separate").unwrap();
editor
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
fn keys(editor: &mut Editor, text: &str) {
for ch in text.chars() {
editor.handle(Input::Char(ch));
}
}
#[test]
fn visual_r_replaces_selected_graphemes_as_one_undo_edit() {
let fixture = Fixture::new();
let source = "ae\u{301}👩‍🚀z\nrest\n";
let mut editor = fixture.editor(source);
keys(&mut editor, "lv2l");
assert_eq!(editor.mode(), Mode::Visual);
keys(&mut editor, "r界");
assert_eq!(editor.context().text, "a界界界\nrest\n");
assert_eq!(editor.mode(), Mode::Normal);
keys(&mut editor, "u");
assert_eq!(editor.context().text, source);
assert!(!editor.dirty());
keys(&mut editor, "U");
assert_eq!(editor.context().text, "a界界界\nrest\n");
}
#[test]
fn visual_r_preserves_selected_newlines_in_both_directions_and_shapes() {
for (source, selection, expected) in [
("ae\u{301}👩‍🚀z\n\nbc\nlast\n", "v2j", "QQQQ\n\nQc\nlast\n"),
("ae\u{301}👩‍🚀z\n\nbc\nlast\n", "2jv2k", "QQQQ\n\nQc\nlast\n"),
("ae\u{301}👩‍🚀z\n\nbc\nlast\n", "V2j", "QQQQ\n\nQQ\nlast\n"),
("ab\ncd", "Vj", "QQ\nQQ"),
("\n\n", "Vj", "\n\n"),
] {
let fixture = Fixture::new();
let mut editor = fixture.editor(source);
keys(&mut editor, selection);
keys(&mut editor, "rQ");
assert_eq!(editor.context().text, expected, "{selection}");
assert_eq!(editor.mode(), Mode::Normal);
assert_eq!(editor.dirty(), source != expected);
keys(&mut editor, "u");
assert_eq!(editor.context().text, source);
}
}
#[test]
fn visual_r_replaces_every_selection_and_restores_carets_on_undo() {
let fixture = Fixture::new();
let source = "abcd\nwxyz\n";
let mut editor = fixture.editor(source);
keys(&mut editor, "vl");
editor.handle(Input::Ctrl('e'));
let carets = editor.carets();
assert_eq!(editor.selection_count(), 2);
keys(&mut editor, "r界");
assert_eq!(editor.context().text, "界界cd\n界界yz\n");
assert_eq!(editor.selection_count(), 2);
keys(&mut editor, "u");
assert_eq!(editor.context().text, source);
assert_eq!(editor.carets(), carets);
}
#[test]
fn visual_r_repeats_selection_intent_without_overwriting_the_clipboard() {
let fixture = Fixture::new();
let mut editor = fixture.editor("abc\nuvwxyz\n");
keys(&mut editor, "yyvlrQj0.");
assert_eq!(editor.context().text, "QQc\nQQwxyz\n");
keys(&mut editor, "4lv.");
assert_eq!(editor.context().text, "QQc\nQQwxQz\n");
keys(&mut editor, "P");
assert_eq!(editor.context().text, "QQc\nabc\nQQwxQz\n");
}
#[test]
fn cancelled_and_identical_visual_replacements_preserve_redo() {
let fixture = Fixture::new();
let mut editor = fixture.editor("abc\n");
keys(&mut editor, "rQulvr");
editor.handle(Input::Escape);
assert_eq!(editor.mode(), Mode::Visual);
assert_eq!(editor.line_operator_pending(), None);
keys(&mut editor, "rb");
assert_eq!(editor.context().text, "abc\n");
assert_eq!(editor.mode(), Mode::Normal);
assert!(!editor.dirty());
keys(&mut editor, "U");
assert_eq!(editor.context().text, "Qbc\n");
}
#[test]
fn change_and_delete_line_suffix_preserve_newlines_and_group_insert_undo() {
for operation in ["C", "D", "9C", "9D"] {
let fixture = Fixture::new();
let source = "ae\u{301}👩‍🚀tail\nnext\n";
let mut editor = fixture.editor(source);
keys(&mut editor, "2l");
let caret = editor.cursor();
keys(&mut editor, operation);
assert_eq!(editor.context().text, "ae\u{301}\nnext\n", "{operation}");
if operation.ends_with('C') {
assert_eq!(editor.mode(), Mode::Insert);
keys(&mut editor, "replacement");
editor.handle(Input::Escape);
assert_eq!(editor.context().text, "ae\u{301}replacement\nnext\n");
} else {
assert_eq!(editor.mode(), Mode::Normal);
}
keys(&mut editor, "u");
assert_eq!(editor.context().text, source);
assert_eq!(editor.cursor(), caret);
assert!(!editor.dirty());
}
}
#[test]
fn suffix_commands_at_last_grapheme_and_empty_lines_never_remove_newlines() {
for operation in ["C", "D"] {
for source in ["a👩‍🚀\nnext\n", "a👩‍🚀\nnext"] {
let fixture = Fixture::new();
let mut editor = fixture.editor(source);
keys(&mut editor, "$");
keys(&mut editor, operation);
assert_eq!(editor.context().text, source.replacen("a👩‍🚀", "a", 1));
if editor.mode() == Mode::Insert {
editor.handle(Input::Escape);
}
keys(&mut editor, "u");
assert_eq!(editor.context().text, source);
}
let fixture = Fixture::new();
let mut editor = fixture.editor("\nnext\n");
keys(&mut editor, operation);
assert_eq!(editor.context().text, "\nnext\n");
assert_eq!(
editor.mode(),
if operation == "C" {
Mode::Insert
} else {
Mode::Normal
}
);
assert!(!editor.dirty());
}
}
#[test]
fn yank_line_suffix_is_characterwise_and_does_not_create_an_undo_entry() {
for operation in ["Y", "8Y"] {
let fixture = Fixture::new();
let source = "a👩‍🚀e\u{301}tail\nnext\n";
let mut editor = fixture.editor(source);
keys(&mut editor, "l");
let caret = editor.cursor();
keys(&mut editor, operation);
assert_eq!(editor.context().text, source);
assert_eq!(editor.cursor(), caret);
assert_eq!(editor.mode(), Mode::Normal);
assert!(!editor.dirty());
keys(&mut editor, "j0P");
assert_eq!(
editor.context().text,
"a👩‍🚀e\u{301}tail\n👩‍🚀e\u{301}tailnext\n"
);
keys(&mut editor, "u");
assert_eq!(editor.context().text, source);
assert!(!editor.dirty());
}
}
#[test]
fn yank_and_cancelled_replace_preserve_the_previous_repeatable_edit() {
let fixture = Fixture::new();
let mut editor = fixture.editor("abc\nxyz\n");
keys(&mut editor, "xY3r");
editor.handle(Input::Escape);
keys(&mut editor, "j0.");
assert_eq!(editor.context().text, "bc\nyz\n");
keys(&mut editor, "u");
assert_eq!(editor.context().text, "bc\nxyz\n");
keys(&mut editor, "u");
assert_eq!(editor.context().text, "abc\nxyz\n");
}
#[test]
fn replace_consumes_a_literal_character_and_replaces_whole_counted_graphemes() {
for (command, expected) in [
("rx", "ax👩‍🚀z\nnext\n"),
("r2", "a2👩‍🚀z\nnext\n"),
("r界", "a界👩‍🚀z\nnext\n"),
("r\t", "a\t👩‍🚀z\nnext\n"),
("2rQ", "aQQz\nnext\n"),
("9rQ", "aQQQ\nnext\n"),
] {
let fixture = Fixture::new();
let source = "ae\u{301}👩‍🚀z\nnext\n";
let mut editor = fixture.editor(source);
keys(&mut editor, "l");
keys(&mut editor, command);
assert_eq!(editor.context().text, expected, "{command:?}");
assert_eq!(editor.mode(), Mode::Normal);
keys(&mut editor, "u");
assert_eq!(editor.context().text, source);
assert_eq!(editor.cursor(), Cursor { row: 0, column: 1 });
assert!(!editor.dirty());
}
let fixture = Fixture::new();
let mut editor = fixture.editor("\nnext\n");
keys(&mut editor, "rQ");
assert_eq!(editor.context().text, "\nnext\n");
assert_eq!(editor.mode(), Mode::Normal);
assert!(!editor.dirty());
}
#[test]
fn substitute_deletes_counted_whole_graphemes_and_groups_replacement_with_undo() {
for (command, after_delete, expected) in [
("s", "a👩‍🚀z\nnext\n", "aQ👩‍🚀z\nnext\n"),
("2s", "az\nnext\n", "aQz\nnext\n"),
("9s", "a\nnext\n", "aQ\nnext\n"),
] {
let fixture = Fixture::new();
let source = "ae\u{301}👩‍🚀z\nnext\n";
let mut editor = fixture.editor(source);
keys(&mut editor, "l");
keys(&mut editor, command);
assert_eq!(editor.context().text, after_delete, "{command}");
assert_eq!(editor.mode(), Mode::Insert);
keys(&mut editor, "Q");
editor.handle(Input::Escape);
assert_eq!(editor.context().text, expected);
keys(&mut editor, "u");
assert_eq!(editor.context().text, source);
assert_eq!(editor.cursor(), Cursor { row: 0, column: 1 });
assert!(!editor.dirty());
}
}
#[test]
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"),
("", "d2w", "three four five six seven eight nine\n"),
("", "2dw", "three four five six seven eight nine\n"),
("", "2d3w", "seven eight nine\n"),
("2w", "db", "one three four five six seven eight nine\n"),
("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}");
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);
}
}
#[test]
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"),
] {
let fixture = Fixture::new();
let mut editor = fixture.editor(source);
keys(&mut editor, command);
assert_eq!(editor.context().text, expected, "{command} on {source:?}");
keys(&mut editor, "u");
assert_eq!(editor.context().text, source);
}
}
#[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 世界";
for (command, expected) in [
("d2w".to_owned(), ""),
(format!("{}d2w", usize::MAX), ""),
("$d3b".to_owned(), "界"),
] {
let fixture = Fixture::new();
let mut editor = fixture.editor(source);
keys(&mut editor, &command);
assert_eq!(editor.context().text, expected, "{command}");
assert_eq!(editor.mode(), Mode::Normal);
keys(&mut editor, "u");
assert_eq!(editor.context().text, source);
assert!(!editor.dirty());
}
}
#[test]
fn pending_operator_and_replace_cancellation_clear_counts_without_editing() {
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);
keys(&mut editor, prefix);
editor.handle(Input::Escape);
assert_eq!(editor.context().text, source, "{prefix}");
assert_eq!(editor.cursor(), Cursor { row: 0, column: 0 });
assert_eq!(editor.mode(), Mode::Normal);
assert!(!editor.dirty());
keys(&mut editor, "x");
assert_eq!(editor.context().text, "bcd\nnext\n", "{prefix}");
}
for input in [
Input::Enter,
Input::Char('\n'),
Input::Paste("ignored".into()),
] {
let fixture = Fixture::new();
let mut editor = fixture.editor("abcd\nnext\n");
keys(&mut editor, "3r");
editor.handle(input);
keys(&mut editor, "lx");
assert_eq!(editor.context().text, "acd\nnext\n");
assert_eq!(editor.mode(), Mode::Normal);
}
}
#[test]
fn new_edit_recipes_repeat_against_the_current_text_and_undo_as_single_changes() {
for (source, first, after_first, position, expected) in [
(
"prefix one\nlongprefix two\n",
"7lCnew",
"prefix new\nlongprefix two\n",
"j011l",
"prefix new\nlongprefix new\n",
),
(
"prefix one\nlongprefix two\n",
"7lD",
"prefix \nlongprefix two\n",
"j011l",
"prefix \nlongprefix \n",
),
(
"e\u{301}abc\n👩‍🚀xyz\n",
"rQ",
"Qabc\n👩‍🚀xyz\n",
"j0",
"Qabc\nQxyz\n",
),
(
"e\u{301}abc\n👩‍🚀xyz\n",
"sQ",
"Qabc\n👩‍🚀xyz\n",
"j0",
"Qabc\nQxyz\n",
),
(
"one two keep\nthree four stay\n",
"d2w",
"keep\nthree four stay\n",
"j0",
"keep\nstay\n",
),
(
"one two keep\nthree four stay\n",
"2wd2b",
"keep\nthree four stay\n",
"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",
),
(
"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);
keys(&mut editor, first);
if editor.mode() == Mode::Insert {
editor.handle(Input::Escape);
}
assert_eq!(editor.context().text, after_first, "{first}");
keys(&mut editor, position);
keys(&mut editor, ".");
assert_eq!(editor.context().text, expected, "{first} then {position}.");
assert_eq!(editor.mode(), Mode::Normal);
keys(&mut editor, "u");
assert_eq!(editor.context().text, after_first);
keys(&mut editor, "u");
assert_eq!(editor.context().text, source);
assert!(!editor.dirty());
}
}
#[test]
fn dd_and_cc_keep_linewise_behavior_with_counts_after_the_operator() {
let source = "1\n2\n3\n4\n5\n6\n7\n8\n9\n";
for (command, remaining) in [
("dd", "2\n3\n4\n5\n6\n7\n8\n9\n"),
("2dd", "3\n4\n5\n6\n7\n8\n9\n"),
("d2d", "3\n4\n5\n6\n7\n8\n9\n"),
("2d3d", "7\n8\n9\n"),
] {
let fixture = Fixture::new();
let mut editor = fixture.editor(source);
keys(&mut editor, command);
assert_eq!(editor.context().text, remaining, "{command}");
keys(&mut editor, "P");
assert_eq!(editor.context().text, source, "{command} linewise paste");
}
for (command, remaining) in [
("cc", "2\n3\n4\n5\n6\n7\n8\n9\n"),
("2cc", "3\n4\n5\n6\n7\n8\n9\n"),
("c2c", "3\n4\n5\n6\n7\n8\n9\n"),
("2c3c", "7\n8\n9\n"),
] {
let fixture = Fixture::new();
let mut editor = fixture.editor(source);
keys(&mut editor, command);
assert_eq!(editor.mode(), Mode::Insert);
assert_eq!(editor.context().text, format!("\n{remaining}"), "{command}");
keys(&mut editor, "Q");
editor.handle(Input::Escape);
assert_eq!(editor.context().text, format!("Q\n{remaining}"));
keys(&mut editor, "u");
assert_eq!(editor.context().text, source);
assert!(!editor.dirty());
}
}
#[test]
fn native_multicarets_edit_whole_graphemes_and_yank_matching_suffix_fragments() {
let source = "a👩‍🚀tail\nbe\u{301}rest\n";
for (command, expected) in [
("D", "a\nb\n"),
("CQ", "aQ\nbQ\n"),
("rQ", "aQtail\nbQrest\n"),
("sQ", "aQtail\nbQrest\n"),
("YP", "a👩‍🚀tail👩‍🚀tail\nbe\u{301}reste\u{301}rest\n"),
] {
let fixture = Fixture::new();
let mut editor = fixture.editor(source);
keys(&mut editor, "l");
editor.handle(Input::Ctrl('e'));
let carets = editor.carets();
assert_eq!(carets.len(), 2);
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");
assert_eq!(editor.context().text, source);
assert_eq!(editor.carets(), carets);
}
for (command, expected) in [
("d2w", "keep\nstay\n"),
("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");
assert_eq!(editor.context().text, source);
assert_eq!(editor.selection_count(), 2);
}
}
#[test]
fn remapped_core_edit_actions_keep_counts_pending_input_and_clipboard_semantics() {
for (action, original_key, position, command, expected) in [
("core.change_to_line_end", "C", "l", "qX", "aX\nnext\n"),
("core.delete_to_line_end", "D", "l", "q", "a\nnext\n"),
(
"core.copy_to_line_end",
"Y",
"l",
"qj0P",
"abc def ghi\nbc def ghinext\n",
),
(
"core.replace_character",
"r",
"l",
"qZ",
"aZc def ghi\nnext\n",
),
(
"core.substitute_character",
"s",
"l",
"qZ",
"aZc def ghi\nnext\n",
),
("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");
editor.bind("normal", original_key, None).unwrap();
editor.bind("normal", "q", Some(action.into())).unwrap();
keys(&mut editor, position);
keys(&mut editor, command);
if editor.mode() == Mode::Insert {
editor.handle(Input::Escape);
}
assert_eq!(editor.context().text, expected, "{action}");
assert_eq!(editor.mode(), Mode::Normal);
}
}