Files
vamp/tests/paste_shapes.rs
2026-10-14 13:13:01 +02:00

286 lines
9.7 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-paste-shapes-{}-{}",
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_paste_replaces_selection_and_preserves_clipboard_and_undo() {
for paste in ['p', 'P'] {
for (yank, select, expected) in [
("vly", "j0vl", "XY\nXYc\nend"),
("vly", "j0lvh", "XY\nXYc\nend"),
("yy", "j0vl", "XY\nXY\nc\nend"),
("yy", "jV", "XY\nXY\nend"),
("vly", "jV", "XY\nXYend"),
] {
let fixture = Fixture::new();
let source = "XY\nabc\nend";
let mut editor = fixture.editor(source);
keys(&mut editor, yank);
keys(&mut editor, select);
editor.handle(Input::Char(paste));
assert_eq!(editor.context().text, expected, "{yank} {select} {paste}");
assert_eq!(editor.mode(), Mode::Normal);
keys(&mut editor, "u");
assert_eq!(editor.context().text, source);
keys(&mut editor, "U");
assert_eq!(editor.context().text, expected);
keys(
&mut editor,
if select.ends_with("vh") {
"u0l."
} else {
"u0."
},
);
assert_eq!(
editor.context().text,
expected,
"repeat: {yank} {select} {paste}"
);
keys(&mut editor, "u");
keys(&mut editor, select.trim_start_matches('j'));
editor.handle(Input::Char(paste));
assert_eq!(editor.context().text, expected, "clipboard stays unchanged");
}
}
}
#[test]
fn visual_paste_replaces_every_selected_range_in_one_undo_group() {
let fixture = Fixture::new();
let source = "XY\n界e\u{301}\n👩‍🚀z";
let mut editor = fixture.editor(source);
keys(&mut editor, "vlyj0vl");
editor.handle(Input::Ctrl('e'));
keys(&mut editor, "p");
assert_eq!(editor.context().text, "XY\nXY\nXY");
keys(&mut editor, "u");
assert_eq!(editor.context().text, source);
}
#[test]
fn whole_line_yank_pastes_above_or_below_the_target_and_keeps_indentation() {
let source = " \t界e\u{301}👩‍🚀\nother\nlast\n";
for (paste, expected, row) in [
('p', " \t界e\u{301}👩‍🚀\nother\n \t界e\u{301}👩‍🚀\nlast\n", 2),
('P', " \t界e\u{301}👩‍🚀\n \t界e\u{301}👩‍🚀\nother\nlast\n", 1),
] {
let fixture = Fixture::new();
let mut editor = fixture.editor(source);
keys(&mut editor, "3l");
let yank_caret = editor.cursor();
keys(&mut editor, "yy");
assert_eq!(editor.context().text, source);
assert_eq!(editor.cursor(), yank_caret);
assert_eq!(editor.mode(), Mode::Normal);
assert!(!editor.dirty());
keys(&mut editor, "j2l");
let paste_caret = editor.cursor();
editor.handle(Input::Char(paste));
assert_eq!(editor.context().text, expected, "{paste}");
assert_eq!(editor.cursor(), Cursor { row, column: 2 });
assert_eq!(editor.mode(), Mode::Normal);
keys(&mut editor, "u");
assert_eq!(editor.context().text, source);
assert_eq!(editor.cursor(), paste_caret);
assert!(!editor.dirty());
}
}
#[test]
fn whole_line_paste_handles_unterminated_rows_empty_rows_and_the_final_empty_row() {
for (source, position, paste, expected, row) in [
("top\n last", "j2l", 'p', "top\n last\n last", 2),
("top\n last", "j2l", 'P', "top\n last\n last", 1),
("top\n last\n", "j2l", 'p', "top\n last\n last\n", 2),
("top\n last\n", "j2l", 'P', "top\n last\n last\n", 1),
("top\n\nend", "j", 'p', "top\n\n\nend", 2),
("top\n\nend", "j", 'P', "top\n\n\nend", 1),
("top\n", "j", 'p', "top\n\n", 2),
("top\n", "j", 'P', "top\n\n", 1),
("", "", 'p', "\n", 1),
("", "", 'P', "\n", 0),
] {
let fixture = Fixture::new();
let mut editor = fixture.editor(source);
keys(&mut editor, position);
let caret = editor.cursor();
keys(&mut editor, "yy");
editor.handle(Input::Char(paste));
assert_eq!(
editor.context().text,
expected,
"{source:?} {position}yy{paste}"
);
assert_eq!(
editor.cursor(),
Cursor {
row,
column: caret.column
}
);
keys(&mut editor, "u");
assert_eq!(editor.context().text, source);
assert_eq!(editor.cursor(), caret);
assert!(!editor.dirty());
}
}
#[test]
fn counted_line_yanks_multiply_prefixes_and_clamp_at_the_last_row() {
let source = "one\ntwo\nthree\nfour\nfive\nsix\nseven";
for (command, expected) in [
("2yy", "one\none\ntwo\ntwo\nthree\nfour\nfive\nsix\nseven"),
("y2y", "one\none\ntwo\ntwo\nthree\nfour\nfive\nsix\nseven"),
(
"2y3y",
"one\none\ntwo\nthree\nfour\nfive\nsix\ntwo\nthree\nfour\nfive\nsix\nseven",
),
(
"9yy",
"one\none\ntwo\nthree\nfour\nfive\nsix\nseven\ntwo\nthree\nfour\nfive\nsix\nseven",
),
] {
let fixture = Fixture::new();
let mut editor = fixture.editor(source);
keys(&mut editor, command);
assert_eq!(editor.context().text, source, "{command}");
keys(&mut editor, "p");
assert_eq!(editor.context().text, expected, "{command}p");
keys(&mut editor, "u");
assert_eq!(editor.context().text, source);
assert!(!editor.dirty());
}
}
#[test]
fn visual_character_yank_pastes_before_or_after_whole_extended_graphemes() {
let source = "ae\u{301}👩‍🚀z\nx👨‍👩‍👧‍👦q\n";
for (paste, expected, column) in [
('p', "ae\u{301}👩‍🚀z\nx👨‍👩‍👧‍👦e\u{301}👩‍🚀q\n", 3),
('P', "ae\u{301}👩‍🚀z\nxe\u{301}👩‍🚀👨‍👩‍👧‍👦q\n", 2),
] {
let fixture = Fixture::new();
let mut editor = fixture.editor(source);
keys(&mut editor, "lvly");
assert_eq!(editor.context().text, source);
assert!(!editor.dirty());
keys(&mut editor, "j0l");
let caret = editor.cursor();
editor.handle(Input::Char(paste));
assert_eq!(editor.context().text, expected, "{paste}");
assert_eq!(editor.cursor(), Cursor { row: 1, column });
assert_eq!(editor.mode(), Mode::Normal);
keys(&mut editor, "u");
assert_eq!(editor.context().text, source);
assert_eq!(editor.cursor(), caret);
editor.handle(Input::Char(paste));
assert_eq!(
editor.context().text,
expected,
"register survives undo and paste"
);
}
}
#[test]
fn dot_keeps_the_pasted_line_shape_without_replacing_the_new_yank_register() {
let fixture = Fixture::new();
let source = " a\nb\nc";
let mut editor = fixture.editor(source);
keys(&mut editor, "yyp");
assert_eq!(editor.context().text, " a\n a\nb\nc");
keys(&mut editor, "jyy.");
assert_eq!(editor.context().text, " a\n a\nb\n a\nc");
keys(&mut editor, "u");
assert_eq!(editor.context().text, " a\n a\nb\nc");
keys(&mut editor, "p");
assert_eq!(editor.context().text, " a\n a\nb\nb\nc");
keys(&mut editor, "u");
assert_eq!(editor.context().text, " a\n a\nb\nc");
keys(&mut editor, "u");
assert_eq!(editor.context().text, source);
assert!(!editor.dirty());
}
#[test]
fn native_multicarets_yank_matching_whole_lines_and_paste_in_one_undo_step() {
let fixture = Fixture::new();
let source = " a\n\tb\nc";
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, "yy");
assert_eq!(editor.carets(), carets);
keys(&mut editor, "p");
assert_eq!(editor.context().text, " a\n a\n\tb\n\tb\nc");
assert_eq!(
editor.carets(),
vec![Cursor { row: 1, column: 1 }, Cursor { row: 3, column: 1 }],
);
keys(&mut editor, "u");
assert_eq!(editor.context().text, source);
assert_eq!(editor.carets(), carets);
assert!(!editor.dirty());
keys(&mut editor, "P");
assert_eq!(editor.context().text, " a\n a\n\tb\n\tb\nc");
}
#[test]
fn a_visual_yank_ending_in_a_newline_stays_characterwise() {
let fixture = Fixture::new();
let source = "\nxy";
let mut editor = fixture.editor(source);
keys(&mut editor, "vyj0p");
assert_eq!(editor.context().text, "\nx\ny");
keys(&mut editor, "u");
assert_eq!(editor.context().text, source);
keys(&mut editor, "P");
assert_eq!(editor.context().text, "\n\nxy");
keys(&mut editor, "u");
assert_eq!(editor.context().text, source);
assert!(!editor.dirty());
}