Files
vamp/tests/normal_editing.rs
T

472 lines
16 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 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_delete_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"),
] {
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}");
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 word_delete_uses_unicode_word_motion_boundaries_across_punctuation_and_rows() {
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);
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_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", "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",
),
] {
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 in ["d2w", "2wd2b"] {
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.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"),
] {
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);
}
}