feat: add normal-mode change yank replace and word delete actions
This commit is contained in:
@@ -0,0 +1,471 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user