use std::{ fs, path::{Path, PathBuf}, process::Command, sync::atomic::{AtomicU64, Ordering}, thread, time::{Duration, Instant}, }; use vamp::{diff::Phase, editor::Input, session::EditorSession}; static NEXT: AtomicU64 = AtomicU64::new(0); struct Fixture(PathBuf); impl Fixture { fn new() -> Self { let root = std::env::temp_dir().join(format!( "vamp-diff-session-{}-{}", std::process::id(), NEXT.fetch_add(1, Ordering::Relaxed), )); fs::create_dir_all(root.join("repo")).unwrap(); Self(root) } fn git(&self, args: &[&str]) -> String { let output = Command::new("git") .args(args) .current_dir(self.0.join("repo")) .env("GIT_CONFIG_NOSYSTEM", "1") .env("GIT_CONFIG_GLOBAL", "/dev/null") .env("GIT_AUTHOR_NAME", "Diff Fixture") .env("GIT_AUTHOR_EMAIL", "fixture@example.invalid") .env("GIT_COMMITTER_NAME", "Diff Fixture") .env("GIT_COMMITTER_EMAIL", "fixture@example.invalid") .env("GIT_AUTHOR_DATE", "2000-01-01T00:00:00Z") .env("GIT_COMMITTER_DATE", "2000-01-01T00:00:00Z") .output() .unwrap(); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); String::from_utf8(output.stdout).unwrap().trim().to_owned() } } impl Drop for Fixture { fn drop(&mut self) { let _ = fs::remove_dir_all(&self.0); } } fn wait_until(session: &mut EditorSession, ready: impl Fn(&EditorSession) -> bool) { let deadline = Instant::now() + Duration::from_secs(10); while !ready(session) { session.poll(); assert!( Instant::now() < deadline, "diff state: {:?}", session.editor().diff() ); if let Some(diff) = session.editor().diff() { assert!(diff.error.is_none(), "{:?}", diff.error); } thread::sleep(Duration::from_millis(1)); } } fn type_query(session: &mut EditorSession, text: &str) { for ch in text.chars() { session.handle(Input::Char(ch)); } } #[test] fn typed_diff_compares_disk_not_unsaved_buffers_and_preserves_undo() { let fixture = Fixture::new(); fixture.git(&["init", "-q"]); let source = fixture.0.join("repo/buffer.rs"); fs::write(&source, "before\n").unwrap(); fixture.git(&["add", "buffer.rs"]); fixture.git(&["commit", "-qm", "base"]); let base = fixture.git(&["rev-parse", "HEAD"]); fs::write(&source, "after\n").unwrap(); let config = fixture.0.join("config.rb"); fs::write( &config, "Vamp.configure do |config|\n config.clipboard_mode = 'separate'\n config.bind 'leader d', config.core.diff\nend\n", ) .unwrap(); let mut session = EditorSession::open(Some(source.clone()), vec!["text".into()], Some(&config)).unwrap(); assert!(session.editor().startup_diagnostic().is_none()); for input in [Input::Char('i'), Input::Char('u'), Input::Escape] { session.handle(input); } let cursor = session.editor().cursor(); session.handle(Input::Char(' ')); session.handle(Input::Char('d')); wait_until(&mut session, |session| { session .editor() .diff() .is_some_and(|diff| diff.catalog.is_some() && !diff.loading) }); type_query(&mut session, &base); session.handle(Input::Down); session.handle(Input::Enter); assert_eq!(session.editor().diff().unwrap().phase, Phase::After); session.handle(Input::Enter); wait_until(&mut session, |session| { session .editor() .diff() .is_some_and(|diff| diff.phase == Phase::View) }); let comparison = session .editor() .diff() .unwrap() .comparison .as_ref() .unwrap(); assert_eq!(comparison.files.len(), 1); let file = &comparison.files[0]; assert_eq!(file.path, Path::new("buffer.rs")); assert_eq!(file.before, ["before"]); assert_eq!(file.after, ["after"]); for input in [ Input::Char('i'), Input::Char('x'), Input::Paste("overwrite".into()), ] { session.handle(input); } session.handle(Input::Escape); assert!(session.editor().diff().is_none()); assert_eq!(session.editor().context().text, "uafter\n"); assert!(session.editor().dirty()); assert_eq!(session.editor().cursor(), cursor); assert_eq!(fs::read_to_string(&source).unwrap(), "after\n"); session.handle(Input::Char('u')); assert_eq!(session.editor().context().text, "after\n"); session.execute(":diff"); assert!(session.editor().diff().is_some()); session.handle(Input::Escape); session.poll(); assert!(session.editor().diff().is_none()); assert_eq!(fixture.git(&["rev-parse", "HEAD"]), base); assert_eq!(fixture.git(&["status", "--porcelain"]), "M buffer.rs"); }