Files
vamp/tests/commands.rs
T

317 lines
11 KiB
Rust

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 restart_closes_clean_buffers_across_tabs_and_panes_but_preserves_dirty_history() {
let fixture = Fixture::new();
let mut editor = fixture.editor("first.txt", "first\n");
let first_id = editor.buffer_id();
insert(&mut editor, "changed ");
editor.handle(Input::Char('0'));
insert(&mut editor, "extra ");
let first_redo_text = editor.context().text;
editor.handle(Input::Char('u'));
assert_eq!(editor.context().text, "changed first\n");
editor.execute("pane.split_vertical");
let clean_path = fixture.0.join("clean.txt");
fs::write(&clean_path, "clean\n").unwrap();
editor.execute(&format!("edit {}", clean_path.display()));
let clean_id = editor.buffer_id();
editor.execute("tab.new");
let second_path = fixture.0.join("second.txt");
fs::write(&second_path, "second\n").unwrap();
editor.execute(&format!("edit {}", second_path.display()));
let second_id = editor.buffer_id();
insert(&mut editor, "edited ");
editor.execute("pane.split_horizontal");
let active_clean_path = fixture.0.join("active-clean.txt");
fs::write(&active_clean_path, "active clean\n").unwrap();
editor.execute(&format!("edit {}", active_clean_path.display()));
let active_clean_id = editor.buffer_id();
assert_eq!(editor.tab_count(), 2);
assert_eq!(editor.pane_layout().len(), 2);
editor.execute(":restart");
assert!(!editor.should_restart());
assert!(!editor.should_quit());
assert!(editor.message().to_lowercase().contains("unsaved"));
assert_eq!(editor.buffer_count(), 2);
assert!(!editor.has_buffer(clean_id));
assert!(!editor.has_buffer(active_clean_id));
assert!(editor.has_buffer(first_id));
assert!(editor.has_buffer(second_id));
editor.execute(&format!("buffer {first_id}"));
assert_eq!(editor.buffer_id(), first_id);
assert_eq!(editor.context().text, "changed first\n");
assert!(editor.dirty());
editor.handle(Input::Char('U'));
assert_eq!(editor.context().text, first_redo_text);
editor.handle(Input::Char('u'));
assert_eq!(editor.context().text, "changed first\n");
editor.handle(Input::Char('u'));
assert_eq!(editor.context().text, "first\n");
editor.handle(Input::Char('U'));
assert_eq!(editor.context().text, "changed first\n");
editor.execute("save");
editor.execute(&format!("buffer {second_id}"));
assert_eq!(editor.buffer_id(), second_id);
assert_eq!(editor.context().text, "edited second\n");
assert!(editor.dirty());
editor.handle(Input::Char('u'));
assert_eq!(editor.context().text, "second\n");
editor.handle(Input::Char('U'));
assert_eq!(editor.context().text, "edited second\n");
editor.execute("save");
editor.execute(":restart");
assert!(editor.should_restart());
assert!(editor.should_quit());
assert_eq!(
fs::read_to_string(fixture.0.join("first.txt")).unwrap(),
"changed first\n"
);
assert_eq!(fs::read_to_string(second_path).unwrap(), "edited second\n");
}
#[test]
fn restart_commands_validate_arguments_before_closing_or_discarding_buffers() {
for command in ["restart", "force_restart", "restart!"] {
let fixture = Fixture::new();
let mut editor = fixture.editor("dirty.txt", "original\n");
let dirty_id = editor.buffer_id();
insert(&mut editor, "changed ");
let clean_path = fixture.0.join("clean.txt");
fs::write(&clean_path, "clean\n").unwrap();
editor.execute(&format!("edit {}", clean_path.display()));
let clean_id = editor.buffer_id();
editor.execute(&format!(":{command} unexpected argument"));
assert!(!editor.should_restart(), "{command}");
assert!(!editor.should_quit(), "{command}");
assert_eq!(editor.buffer_count(), 2, "{command}");
assert_eq!(editor.buffer_id(), clean_id, "{command}");
assert!(editor.has_buffer(dirty_id), "{command}");
editor.execute(&format!("buffer {dirty_id}"));
assert_eq!(editor.context().text, "changed original\n", "{command}");
assert!(editor.dirty(), "{command}");
editor.handle(Input::Char('u'));
assert_eq!(editor.context().text, "original\n", "{command}");
editor.handle(Input::Char('U'));
assert_eq!(editor.context().text, "changed original\n", "{command}");
if command != "restart" {
editor.execute(&format!(":{command}"));
assert!(editor.should_restart(), "{command}");
assert!(editor.should_quit(), "{command}");
}
assert_eq!(
fs::read_to_string(fixture.0.join("dirty.txt")).unwrap(),
"original\n",
"{command}"
);
assert_eq!(fs::read_to_string(clean_path).unwrap(), "clean\n");
}
}
#[test]
fn configured_restart_bindings_execute_directly_without_force_confirmation() {
for action in ["core.restart", "core.force_restart"] {
let fixture = Fixture::new();
let mut editor = fixture.editor("file.txt", "original\n");
editor.bind("normal", "x", Some(action.into())).unwrap();
if action == "core.force_restart" {
insert(&mut editor, "changed ");
}
editor.handle(Input::Char('x'));
assert!(editor.should_restart(), "{action}");
assert!(editor.should_quit(), "{action}");
assert_eq!(
fs::read_to_string(fixture.0.join("file.txt")).unwrap(),
"original\n",
"{action}"
);
}
}
#[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<String>| 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");
}
}