707 lines
24 KiB
Rust
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);
|
|
}
|
|
}
|