323 lines
11 KiB
Rust
323 lines
11 KiB
Rust
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_edits_unsaved_working_after_and_preserves_undo_on_explicit_close() {
|
|
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, ["uafter", ""]);
|
|
session.handle(Input::Char('\t'));
|
|
assert_eq!(
|
|
session.editor().diff().unwrap().focus,
|
|
vamp::diff::Focus::Before
|
|
);
|
|
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_some());
|
|
assert_eq!(session.editor().context().text, "uafter\n");
|
|
assert_eq!(session.editor().cursor(), cursor);
|
|
assert_eq!(fs::read_to_string(&source).unwrap(), "after\n");
|
|
|
|
session.handle(Input::Char('\t'));
|
|
assert_eq!(
|
|
session.editor().diff().unwrap().focus,
|
|
vamp::diff::Focus::After
|
|
);
|
|
for input in [Input::Char('i'), Input::Char('x'), Input::Escape] {
|
|
session.handle(input);
|
|
}
|
|
assert_eq!(session.editor().context().text, "xuafter\n");
|
|
assert!(session.editor().diff().is_some());
|
|
session.handle(Input::Char('u'));
|
|
assert_eq!(session.editor().context().text, "uafter\n");
|
|
session.handle(Input::Char('U'));
|
|
assert_eq!(session.editor().context().text, "xuafter\n");
|
|
session.execute("w");
|
|
assert_eq!(fs::read_to_string(&source).unwrap(), "xuafter\n");
|
|
assert!(!session.editor().dirty());
|
|
session.handle(Input::Char('u'));
|
|
assert_eq!(session.editor().context().text, "uafter\n");
|
|
assert!(session.editor().dirty());
|
|
|
|
session.execute("diff.close");
|
|
assert!(session.editor().diff().is_none());
|
|
assert_eq!(session.editor().context().text, "uafter\n");
|
|
session.handle(Input::Char('u'));
|
|
assert_eq!(session.editor().context().text, "after\n");
|
|
assert_eq!(fs::read_to_string(&source).unwrap(), "xuafter\n");
|
|
session.execute(":diff");
|
|
assert!(session.editor().diff().is_some());
|
|
session.execute("diff.close");
|
|
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");
|
|
}
|
|
|
|
#[test]
|
|
fn editable_diff_navigation_skips_omitted_lines_without_expanding_excerpts() {
|
|
use vamp::diff::DisplayRow;
|
|
|
|
let fixture = Fixture::new();
|
|
fixture.git(&["init", "-q"]);
|
|
let source = fixture.0.join("repo/context.txt");
|
|
let mut lines = (1..=100)
|
|
.map(|line| format!("line_{line:03} words"))
|
|
.collect::<Vec<_>>();
|
|
lines[49].clear(); // Hidden short lines must not clamp the visible caret column.
|
|
fs::write(&source, format!("{}\n", lines.join("\n"))).unwrap();
|
|
fixture.git(&["add", "context.txt"]);
|
|
fixture.git(&["commit", "-qm", "base"]);
|
|
let base = fixture.git(&["rev-parse", "HEAD"]);
|
|
lines[19] = "changed_020 words".into();
|
|
lines[79] = "changed_080 words".into();
|
|
let text = format!("{}\n", lines.join("\n"));
|
|
fs::write(&source, &text).unwrap();
|
|
let config = fixture.0.join("config.rb");
|
|
fs::write(&config, "Vamp.configure do |config|\n config.clipboard_mode = 'separate'\n config.diff_context_lines = 1\n config.diff_expand_lines = 2\n config.bind 'ctrl+f', config.core.down\n config.bind 'ctrl+b', config.core.up\nend\n").unwrap();
|
|
let mut session =
|
|
EditorSession::open(Some(source.clone()), vec!["text".into()], Some(&config)).unwrap();
|
|
session.execute("diff");
|
|
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);
|
|
session.handle(Input::Enter);
|
|
wait_until(&mut session, |session| {
|
|
session
|
|
.editor()
|
|
.diff()
|
|
.is_some_and(|diff| diff.phase == Phase::View)
|
|
});
|
|
session.handle(Input::BackTab);
|
|
let shown = |session: &EditorSession| {
|
|
let diff = session.editor().diff().unwrap();
|
|
diff.display_rows
|
|
.iter()
|
|
.filter_map(|row| match *row {
|
|
DisplayRow::Line(index) => diff.rows[index].after,
|
|
DisplayRow::Fold { .. } => None,
|
|
})
|
|
.collect::<Vec<_>>()
|
|
};
|
|
let excerpts = vec![18, 19, 20, 78, 79, 80];
|
|
assert_eq!(shown(&session), excerpts);
|
|
assert_eq!(session.editor().cursor().row, 18);
|
|
type_query(&mut session, "5l");
|
|
for (keys, row) in [("2j", 20), ("j", 78), ("k", 20), ("3j", 80), ("5k", 18)] {
|
|
type_query(&mut session, keys);
|
|
assert_eq!(session.editor().cursor().row, row, "{keys}");
|
|
assert_eq!(session.editor().cursor().column, 5, "{keys}");
|
|
assert_eq!(shown(&session), excerpts, "{keys}");
|
|
}
|
|
for (keys, row) in [
|
|
("gb", 80),
|
|
("gt", 18),
|
|
("2j$w", 78),
|
|
("b", 20),
|
|
("$2w", 78),
|
|
("0B", 20),
|
|
] {
|
|
type_query(&mut session, keys);
|
|
assert_eq!(session.editor().cursor().row, row, "{keys}");
|
|
assert_eq!(shown(&session), excerpts, "{keys}");
|
|
}
|
|
session.handle(Input::Ctrl('f'));
|
|
assert_eq!(session.editor().cursor().row, 78);
|
|
session.handle(Input::Ctrl('b'));
|
|
assert_eq!(session.editor().cursor().row, 20);
|
|
type_query(&mut session, "Vj");
|
|
assert_eq!(session.editor().cursor().row, 78);
|
|
session.handle(Input::Escape);
|
|
session.handle(Input::Ctrl('k'));
|
|
assert_eq!(
|
|
session
|
|
.editor()
|
|
.carets()
|
|
.iter()
|
|
.map(|caret| caret.row)
|
|
.collect::<Vec<_>>(),
|
|
[20, 78]
|
|
);
|
|
type_query(&mut session, "j");
|
|
assert_eq!(
|
|
session
|
|
.editor()
|
|
.carets()
|
|
.iter()
|
|
.map(|caret| caret.row)
|
|
.collect::<Vec<_>>(),
|
|
[78, 79]
|
|
);
|
|
session.handle(Input::Escape);
|
|
type_query(&mut session, "gt2j$v");
|
|
session.handle(Input::Ctrl('a'));
|
|
assert_eq!(
|
|
session
|
|
.editor()
|
|
.carets()
|
|
.iter()
|
|
.map(|caret| caret.row)
|
|
.collect::<Vec<_>>(),
|
|
excerpts
|
|
);
|
|
assert_eq!(shown(&session), excerpts);
|
|
session.handle(Input::Escape);
|
|
session.handle(Input::Escape);
|
|
type_query(&mut session, "/line_040");
|
|
session.handle(Input::Enter);
|
|
assert_eq!(session.editor().cursor().row, 20);
|
|
type_query(&mut session, "/line_079");
|
|
session.handle(Input::Enter);
|
|
assert_eq!(session.editor().cursor().row, 78);
|
|
assert_eq!(shown(&session), excerpts);
|
|
type_query(&mut session, "gt2ji");
|
|
session.handle(Input::Down);
|
|
assert_eq!(session.editor().cursor().row, 78);
|
|
session.handle(Input::Up);
|
|
assert_eq!(session.editor().cursor().row, 20);
|
|
session.handle(Input::Escape);
|
|
assert_eq!(shown(&session), excerpts);
|
|
assert_eq!(session.editor().context().text, text);
|
|
assert!(!session.editor().dirty());
|
|
assert_eq!(fs::read_to_string(&source).unwrap(), text);
|
|
type_query(&mut session, "[");
|
|
assert_eq!(shown(&session), [16, 17, 18, 19, 20, 78, 79, 80]);
|
|
type_query(&mut session, "]");
|
|
assert_eq!(shown(&session), [16, 17, 18, 19, 20, 21, 22, 78, 79, 80]);
|
|
}
|