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()); }