use std::{ fs, path::PathBuf, sync::atomic::{AtomicU64, Ordering}, }; use vamp::editor::{Editor, Feature, Input}; static NEXT: AtomicU64 = AtomicU64::new(0); struct Fixture(PathBuf); impl Fixture { fn new() -> Self { let root = std::env::temp_dir().join(format!( "vamp-commands-{}-{}", std::process::id(), NEXT.fetch_add(1, Ordering::Relaxed), )); fs::create_dir_all(&root).unwrap(); Self(root) } fn editor(&self, name: &str, text: &str) -> Editor { let path = self.0.join(name); fs::write(&path, text).unwrap(); Editor::open(Some(path), vec!["text".into()]).unwrap() } } impl Drop for Fixture { fn drop(&mut self) { let _ = fs::remove_dir_all(&self.0); } } fn insert(editor: &mut Editor, text: &str) { for input in [Input::Char('i'), Input::Paste(text.into()), Input::Escape] { editor.handle(input); } } #[test] fn save_and_w_preserve_spaced_path_arguments_and_write_current_contents() { for command in ["save", "w"] { let fixture = Fixture::new(); let mut editor = fixture.editor("original file.txt", "original\n"); insert(&mut editor, "changed "); let destination = fixture.0.join("saved file with spaces.txt"); editor.execute(&format!(":{command} {}", destination.display())); assert_eq!(editor.path(), Some(destination.as_path()), "{command}"); assert_eq!( fs::read_to_string(&destination).unwrap(), "changed original\n" ); assert_eq!( fs::read_to_string(fixture.0.join("original file.txt")).unwrap(), "original\n" ); assert!(!editor.dirty(), "{command}"); assert!(!editor.should_quit(), "{command}"); } } #[test] fn quit_and_q_refuse_dirty_buffers_then_exit_when_clean() { for command in ["quit", "q"] { let fixture = Fixture::new(); let mut editor = fixture.editor("file.txt", "original\n"); insert(&mut editor, "changed "); editor.execute(command); assert!(!editor.should_quit(), "{command}"); assert!(editor.dirty(), "{command}"); assert_eq!(editor.context().text, "changed original\n"); assert_eq!( fs::read_to_string(fixture.0.join("file.txt")).unwrap(), "original\n" ); editor.handle(Input::Char('u')); assert!(!editor.dirty(), "{command}"); editor.execute(command); assert!(editor.should_quit(), "{command}"); } } #[test] fn force_quit_and_q_bang_validate_arguments_before_discarding_changes() { for command in ["force_quit", "q!"] { let fixture = Fixture::new(); let mut editor = fixture.editor("file.txt", "original\n"); insert(&mut editor, "changed "); editor.execute(&format!("{command} unexpected argument")); assert!(!editor.should_quit(), "{command}"); assert!(editor.dirty(), "{command}"); assert_eq!(editor.context().text, "changed original\n"); editor.execute(command); assert!(editor.should_quit(), "{command}"); assert_eq!( fs::read_to_string(fixture.0.join("file.txt")).unwrap(), "original\n" ); } } #[test] fn buffer_next_and_bnext_preserve_each_buffers_dirty_text_and_undo() { for command in ["buffer_next", "bnext"] { let fixture = Fixture::new(); let mut editor = fixture.editor("first.txt", "first\n"); let first_id = editor.buffer_id(); insert(&mut editor, "changed "); let second_path = fixture.0.join("second file.txt"); fs::write(&second_path, "second\n").unwrap(); editor.execute(&format!("edit {}", second_path.display())); let second_id = editor.buffer_id(); assert_ne!(first_id, second_id); insert(&mut editor, "edited "); editor.execute(command); assert_eq!(editor.buffer_id(), first_id, "{command}"); assert_eq!(editor.context().text, "changed first\n"); assert!(editor.dirty(), "{command}"); editor.handle(Input::Char('u')); assert_eq!(editor.context().text, "first\n"); assert!(!editor.dirty(), "{command}"); editor.execute(command); assert_eq!(editor.buffer_id(), second_id, "{command}"); assert_eq!(editor.context().text, "edited second\n"); assert!(editor.dirty(), "{command}"); editor.handle(Input::Char('u')); assert_eq!(editor.context().text, "second\n"); assert!(!editor.dirty(), "{command}"); assert_eq!(editor.buffer_count(), 2); assert_eq!( fs::read_to_string(fixture.0.join("first.txt")).unwrap(), "first\n" ); assert_eq!(fs::read_to_string(&second_path).unwrap(), "second\n"); } } #[test] fn builtin_canonical_and_alias_collisions_reject_the_entire_feature() { for reserved in ["save", "w", "quit", "q"] { let mut editor = Editor::open(None, vec!["text".into()]).unwrap(); let feature = |commands: Vec| Feature { name: "example".into(), required_capabilities: vec!["text".into()], commands, core: false, }; assert!( editor .register(feature(vec!["example.run".into(), reserved.into()])) .is_err(), "{reserved}" ); assert!(editor.execute("example.run").is_none(), "{reserved}"); editor .register(feature(vec!["example.run".into()])) .unwrap(); let invocation = editor.execute("example.run remaining args").unwrap(); assert_eq!(invocation.plugin, "example"); } }