168e12160a
Use the active DiffState, native FilePicker and existing diff input, layout, focus, source navigation and scrolling paths for Resolution. Delete separate resolver filtering, file-list rendering and scroll APIs. Keep projections, source choices, History and guarded jj saves distinct. Verify native parity for picker remaps, current-file selection, excerpt initialization, folded search, wheel focus, layouts, buffer transitions and per-file viewports. All 932 tests pass; actual terminal smoke covers Space-e explorer, Down/Enter file selection and guarded confirmed save. Clippy completes with existing warnings only. Size exception: 1197 implementation added/deleted lines excluding tests and docs; input routing, native buffer lifecycle and rendering are kept as one coherent cutover, without a parallel legacy resolver path.
276 lines
8.5 KiB
Rust
Executable File
276 lines
8.5 KiB
Rust
Executable File
#![cfg(unix)]
|
|
|
|
use std::{
|
|
fs,
|
|
path::PathBuf,
|
|
process::Command,
|
|
sync::atomic::{AtomicU64, Ordering},
|
|
thread,
|
|
time::{Duration, Instant},
|
|
};
|
|
use vamp::{
|
|
editor::{Input, ResolverStatus},
|
|
session::EditorSession,
|
|
};
|
|
|
|
static NEXT: AtomicU64 = AtomicU64::new(0);
|
|
struct Fixture {
|
|
root: PathBuf,
|
|
base: String,
|
|
}
|
|
impl Fixture {
|
|
fn jj(&self, args: &[&str]) -> String {
|
|
let result = Command::new("jj")
|
|
.args(["--no-pager", "--color=never"])
|
|
.args(args)
|
|
.current_dir(&self.root)
|
|
.env("JJ_CONFIG", "/dev/null")
|
|
.env("JJ_USER", "Resolver Test")
|
|
.env("JJ_EMAIL", "resolver@example.invalid")
|
|
.env("JJ_TIMESTAMP", "2000-01-01T00:00:00Z")
|
|
.env("GIT_CONFIG_NOSYSTEM", "1")
|
|
.env("GIT_CONFIG_GLOBAL", "/dev/null")
|
|
.output()
|
|
.unwrap();
|
|
assert!(
|
|
result.status.success(),
|
|
"{}",
|
|
String::from_utf8_lossy(&result.stderr)
|
|
);
|
|
String::from_utf8(result.stdout).unwrap().trim().into()
|
|
}
|
|
fn new() -> Self {
|
|
let root = std::env::temp_dir().join(format!(
|
|
"vamp-resolve-session-{}-{}",
|
|
std::process::id(),
|
|
NEXT.fetch_add(1, Ordering::Relaxed)
|
|
));
|
|
fs::create_dir(&root).unwrap();
|
|
let mut fixture = Self {
|
|
root,
|
|
base: String::new(),
|
|
};
|
|
fixture.jj(&["git", "init", "--no-colocate", "."]);
|
|
for name in ["first.txt", "second.txt"] {
|
|
fs::write(fixture.root.join(name), Self::text("base")).unwrap();
|
|
}
|
|
fixture.jj(&["describe", "-m", "base"]);
|
|
fixture.base = fixture.jj(&["log", "--no-graph", "-r", "@", "-T", "commit_id"]);
|
|
fixture.jj(&["new", &fixture.base]);
|
|
for name in ["first.txt", "second.txt"] {
|
|
fs::write(fixture.root.join(name), Self::text("left")).unwrap();
|
|
}
|
|
fixture.jj(&["describe", "-m", "left"]);
|
|
let left = fixture.jj(&["log", "--no-graph", "-r", "@", "-T", "commit_id"]);
|
|
fixture.jj(&["new", &fixture.base]);
|
|
for name in ["first.txt", "second.txt"] {
|
|
fs::write(fixture.root.join(name), Self::text("right")).unwrap();
|
|
}
|
|
fixture.jj(&["describe", "-m", "right"]);
|
|
let right = fixture.jj(&["log", "--no-graph", "-r", "@", "-T", "commit_id"]);
|
|
fixture.jj(&["new", &left, &right, "-m", "conflicted result"]);
|
|
fixture
|
|
}
|
|
fn text(side: &str) -> String {
|
|
format!(
|
|
"{side} first\n{}{side} last\n",
|
|
(0..30)
|
|
.map(|n| format!("context_{n}\n"))
|
|
.collect::<String>()
|
|
)
|
|
}
|
|
fn open(&self) -> EditorSession {
|
|
let config = self.root.join("test-config.rb");
|
|
fs::write(
|
|
&config,
|
|
"Vamp.configure do |config|\n config.clipboard_mode = 'separate'\nend\n",
|
|
)
|
|
.unwrap();
|
|
EditorSession::open(
|
|
Some(self.root.join("first.txt")),
|
|
vec!["text".into()],
|
|
Some(&config),
|
|
)
|
|
.unwrap()
|
|
}
|
|
}
|
|
impl Drop for Fixture {
|
|
fn drop(&mut self) {
|
|
let _ = fs::remove_dir_all(&self.root);
|
|
}
|
|
}
|
|
fn ready(session: &mut EditorSession) {
|
|
let deadline = Instant::now() + Duration::from_secs(15);
|
|
loop {
|
|
session.poll();
|
|
if session.editor().resolver().is_some_and(|resolver| {
|
|
!resolver.loading
|
|
&& resolver
|
|
.files
|
|
.iter()
|
|
.all(|file| file.status != ResolverStatus::Checking)
|
|
}) {
|
|
return;
|
|
}
|
|
assert!(
|
|
Instant::now() < deadline,
|
|
"resolver did not settle: {}",
|
|
session.editor().message()
|
|
);
|
|
thread::sleep(Duration::from_millis(1));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn resolver_keeps_unsaved_result_and_undo_then_saves_and_retains_resolved_file() {
|
|
let fixture = Fixture::new();
|
|
let original_disk = fs::read_to_string(fixture.root.join("first.txt")).unwrap();
|
|
let mut session = fixture.open();
|
|
for input in [
|
|
Input::Char('i'),
|
|
Input::Paste("draft\n".into()),
|
|
Input::Escape,
|
|
] {
|
|
session.handle(input);
|
|
}
|
|
let draft = session.editor().context().text;
|
|
let operation = fixture.jj(&[
|
|
"--ignore-working-copy",
|
|
"op",
|
|
"log",
|
|
"--no-graph",
|
|
"--limit",
|
|
"1",
|
|
"-T",
|
|
"id",
|
|
]);
|
|
session.execute("resolve");
|
|
ready(&mut session);
|
|
let seeded = format!("draft\n{}", Fixture::text("left"));
|
|
assert_eq!(session.editor().context().text, seeded);
|
|
assert_ne!(seeded, draft);
|
|
assert!(session.editor().dirty());
|
|
assert_eq!(
|
|
fs::read_to_string(fixture.root.join("first.txt")).unwrap(),
|
|
original_disk
|
|
);
|
|
assert_eq!(
|
|
fixture.jj(&[
|
|
"--ignore-working-copy",
|
|
"op",
|
|
"log",
|
|
"--no-graph",
|
|
"--limit",
|
|
"1",
|
|
"-T",
|
|
"id"
|
|
]),
|
|
operation
|
|
);
|
|
session.handle(Input::Char('u'));
|
|
assert_eq!(session.editor().context().text, draft);
|
|
session.handle(Input::Char('U'));
|
|
assert_eq!(session.editor().context().text, seeded);
|
|
let source = session.editor().resolver().unwrap().files[0]
|
|
.sources
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(_, source)| !source.base)
|
|
.nth(1)
|
|
.unwrap()
|
|
.0
|
|
+ 1;
|
|
session.execute(&format!("resolve.use_source {source}"));
|
|
let chosen = session.editor().context().text;
|
|
assert_ne!(chosen, seeded);
|
|
assert!(chosen.starts_with("draft\n"));
|
|
assert!(chosen.contains("context_15\n"));
|
|
session.handle(Input::Char('u'));
|
|
assert_eq!(session.editor().context().text, seeded);
|
|
session.execute(&format!("resolve.use_source {source}"));
|
|
let cursor = session.editor().cursor();
|
|
let resolved_text = session.editor().context().text;
|
|
session.execute("resolve.view.toggle");
|
|
session.execute("resolve.view.toggle");
|
|
assert_eq!(session.editor().context().text, resolved_text);
|
|
assert_eq!(session.editor().cursor(), cursor);
|
|
session.execute("w");
|
|
ready(&mut session);
|
|
let resolver = session.editor().resolver().unwrap();
|
|
assert_eq!(resolver.files.len(), 2);
|
|
assert_eq!(
|
|
resolver.files[resolver.current_file].status,
|
|
ResolverStatus::Resolved
|
|
);
|
|
assert_eq!(
|
|
fs::read_to_string(fixture.root.join("first.txt")).unwrap(),
|
|
resolved_text
|
|
);
|
|
assert!(
|
|
!fixture
|
|
.jj(&["--ignore-working-copy", "resolve", "--list"])
|
|
.contains("first.txt")
|
|
);
|
|
assert!(
|
|
fixture
|
|
.jj(&["--ignore-working-copy", "resolve", "--list"])
|
|
.contains("second.txt")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn resolver_refuses_save_after_checkout_change_even_when_forced_and_preserves_draft() {
|
|
let fixture = Fixture::new();
|
|
let mut session = fixture.open();
|
|
session.execute("resolve");
|
|
ready(&mut session);
|
|
for input in [
|
|
Input::Char('i'),
|
|
Input::Paste("keep my draft\n".into()),
|
|
Input::Escape,
|
|
] {
|
|
session.handle(input);
|
|
}
|
|
let draft = session.editor().context().text;
|
|
fixture.jj(&["new", &fixture.base]);
|
|
let checkout_disk = fs::read_to_string(fixture.root.join("first.txt")).unwrap();
|
|
for save in ["w", "w!"] {
|
|
session.execute(save);
|
|
ready(&mut session);
|
|
assert_eq!(
|
|
fs::read_to_string(fixture.root.join("first.txt")).unwrap(),
|
|
checkout_disk
|
|
);
|
|
assert_eq!(session.editor().context().text, draft);
|
|
assert!(session.editor().dirty());
|
|
}
|
|
session.execute("resolve.close");
|
|
assert_eq!(session.editor().context().text, draft);
|
|
assert!(session.editor().dirty());
|
|
}
|
|
|
|
#[test]
|
|
fn resolver_save_preserves_external_writes_and_keeps_the_unsaved_result() {
|
|
let fixture = Fixture::new();
|
|
let mut session = fixture.open();
|
|
session.execute("resolve");
|
|
ready(&mut session);
|
|
for input in [
|
|
Input::Char('i'),
|
|
Input::Paste("local draft\n".into()),
|
|
Input::Escape,
|
|
] {
|
|
session.handle(input);
|
|
}
|
|
let draft = session.editor().context().text;
|
|
fs::write(fixture.root.join("first.txt"), "external edit\n").unwrap();
|
|
session.execute("w");
|
|
ready(&mut session);
|
|
assert_eq!(
|
|
fs::read_to_string(fixture.root.join("first.txt")).unwrap(),
|
|
"external edit\n"
|
|
);
|
|
assert_eq!(session.editor().context().text, draft);
|
|
assert!(session.editor().dirty());
|
|
}
|