feat: resolve current jj working-copy conflicts
Add a dedicated native resolver workspace with all side/base inputs, shared editable Results, region choices, retained per-file status, responsive rendering, and explicit nontext file choices. Guard writes with jj working-copy locking, frozen checkout identity, exact disk baselines, native dirty-buffer protection, and asynchronous jj snapshot/status confirmation. Historical comparisons remain readonly. Verified all 916 tests and live isolated terminal workflows covering text regions, narrow layouts, binary bytes and symlink targets/types. Clippy completes with pre-existing warnings only. Size exception: 2614 implementation additions/deletions excluding tests and docs. Native workspace integration, explicit file choices and the safe save/check operation are retained as one complete behavioral slice.
This commit is contained in:
+172
-1
@@ -14,7 +14,10 @@ use std::{
|
||||
use crate::{
|
||||
blame::BlameWorker,
|
||||
diff::{Focus, Operation as DiffOperation, Outcome as DiffOutcome},
|
||||
editor::{BlameSource, Cursor, Editor, Effect, Input, Invocation, Mode},
|
||||
editor::{
|
||||
BlameSource, Cursor, Editor, Effect, Input, Invocation, Mode, ResolverOutcome,
|
||||
ResolverRequest,
|
||||
},
|
||||
lsp::Lsp,
|
||||
plugin::{
|
||||
DocumentEvent, DocumentEventKind, DocumentSnapshot, PluginEvent, PluginUpdate,
|
||||
@@ -26,6 +29,72 @@ use crate::{
|
||||
|
||||
type DiffResult = (u64, Result<DiffOutcome, String>);
|
||||
|
||||
struct ResolverWorker {
|
||||
requests: Option<Sender<ResolverRequest>>,
|
||||
results: Receiver<ResolverOutcome>,
|
||||
stopped: Arc<AtomicBool>,
|
||||
thread: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl ResolverWorker {
|
||||
fn start() -> Result<Self, String> {
|
||||
let (requests, incoming) = mpsc::channel::<ResolverRequest>();
|
||||
let (outgoing, results) = mpsc::channel();
|
||||
let stopped = Arc::new(AtomicBool::new(false));
|
||||
let stop = Arc::clone(&stopped);
|
||||
let thread = thread::Builder::new()
|
||||
.name("vamp-resolve".into())
|
||||
.spawn(move || {
|
||||
while let Ok(request) = incoming.recv() {
|
||||
if stop.load(Ordering::Acquire) {
|
||||
break;
|
||||
}
|
||||
let cancelled = || stop.load(Ordering::Acquire);
|
||||
let outcome = match request {
|
||||
ResolverRequest::Load { tab, root } => ResolverOutcome::Loaded {
|
||||
tab,
|
||||
result: crate::resolve::load(&root, &cancelled),
|
||||
},
|
||||
ResolverRequest::Save {
|
||||
tab,
|
||||
file,
|
||||
buffer,
|
||||
workspace,
|
||||
resolution,
|
||||
quit,
|
||||
} => ResolverOutcome::Saved {
|
||||
tab,
|
||||
file,
|
||||
buffer,
|
||||
quit,
|
||||
result: crate::resolve::save(&workspace, file, resolution, &cancelled),
|
||||
},
|
||||
};
|
||||
if outgoing.send(outcome).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.map_err(|error| format!("Cannot start resolver worker: {error}"))?;
|
||||
Ok(Self {
|
||||
requests: Some(requests),
|
||||
results,
|
||||
stopped,
|
||||
thread: Some(thread),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ResolverWorker {
|
||||
fn drop(&mut self) {
|
||||
self.stopped.store(true, Ordering::Release);
|
||||
self.requests.take();
|
||||
if let Some(thread) = self.thread.take() {
|
||||
let _ = thread.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct PullRequestJob {
|
||||
page: u32,
|
||||
@@ -367,6 +436,7 @@ pub struct EditorSession {
|
||||
pending_completion: bool,
|
||||
hover_owner: Option<String>,
|
||||
diff_workers: BTreeMap<usize, DiffWorker>,
|
||||
resolver_worker: Option<ResolverWorker>,
|
||||
blame_worker: Option<BlameWorker>,
|
||||
blame_source: Option<BlameSource>,
|
||||
blame_lines: Option<Vec<Option<Arc<crate::blame::LineBlame>>>>,
|
||||
@@ -487,6 +557,7 @@ impl EditorSession {
|
||||
pending_completion: false,
|
||||
hover_owner: None,
|
||||
diff_workers: BTreeMap::new(),
|
||||
resolver_worker: None,
|
||||
blame_worker: None,
|
||||
blame_source: None,
|
||||
blame_lines: None,
|
||||
@@ -557,6 +628,7 @@ impl EditorSession {
|
||||
self.synchronize_documents();
|
||||
}
|
||||
self.start_diff_request();
|
||||
self.start_resolver_requests();
|
||||
}
|
||||
|
||||
pub fn execute(&mut self, command: &str) {
|
||||
@@ -568,6 +640,67 @@ impl EditorSession {
|
||||
self.synchronize_documents();
|
||||
}
|
||||
self.start_diff_request();
|
||||
self.start_resolver_requests();
|
||||
}
|
||||
|
||||
fn start_resolver_requests(&mut self) {
|
||||
while let Some(request) = self.editor.take_resolver_request() {
|
||||
if self.resolver_worker.is_none() {
|
||||
match ResolverWorker::start() {
|
||||
Ok(worker) => self.resolver_worker = Some(worker),
|
||||
Err(error) => {
|
||||
self.fail_resolver_request(request, error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Err(error) = self
|
||||
.resolver_worker
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.requests
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.send(request)
|
||||
{
|
||||
self.fail_resolver_request(error.0, "Resolver worker stopped unexpectedly".into());
|
||||
self.resolver_worker = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fail_resolver_request(&mut self, request: ResolverRequest, error: String) {
|
||||
let outcome = match request {
|
||||
ResolverRequest::Load { tab, .. } => ResolverOutcome::Loaded {
|
||||
tab,
|
||||
result: Err(error),
|
||||
},
|
||||
ResolverRequest::Save {
|
||||
tab,
|
||||
file,
|
||||
buffer,
|
||||
quit,
|
||||
..
|
||||
} => ResolverOutcome::Saved {
|
||||
tab,
|
||||
file,
|
||||
buffer,
|
||||
quit,
|
||||
result: Err(error),
|
||||
},
|
||||
};
|
||||
self.editor.apply_resolver_outcome(outcome);
|
||||
}
|
||||
|
||||
fn poll_resolver(&mut self) -> bool {
|
||||
self.start_resolver_requests();
|
||||
let mut changed = false;
|
||||
if let Some(worker) = &self.resolver_worker {
|
||||
while let Ok(outcome) = worker.results.try_recv() {
|
||||
changed |= self.editor.apply_resolver_outcome(outcome);
|
||||
}
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
fn start_diff_request(&mut self) {
|
||||
@@ -1603,6 +1736,7 @@ impl EditorSession {
|
||||
changed |= self.poll_battery();
|
||||
self.refresh_checkout_diffs_at(now);
|
||||
changed |= self.poll_diff();
|
||||
changed |= self.poll_resolver();
|
||||
for update in self.lsp.poll() {
|
||||
match self.apply_update("core", update) {
|
||||
Ok(applied) => changed |= applied,
|
||||
@@ -1664,6 +1798,43 @@ mod tests {
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn resolver_load_errors_return_through_real_session_without_touching_the_draft() {
|
||||
let fixture = Fixture::new();
|
||||
let path = fixture.file("buffer.txt", "disk\n");
|
||||
let mut session = EditorSession::open(Some(path), vec![], None).unwrap();
|
||||
session
|
||||
.editor
|
||||
.apply(vec![Effect::Replace("draft\n".into())])
|
||||
.unwrap();
|
||||
let buffer = session.editor.buffer_id();
|
||||
session.execute("resolve");
|
||||
assert!(session.editor.resolver().unwrap().loading);
|
||||
let outcome = session
|
||||
.resolver_worker
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.results
|
||||
.recv_timeout(Duration::from_secs(5))
|
||||
.unwrap();
|
||||
session.editor.apply_resolver_outcome(outcome);
|
||||
assert!(!session.editor.resolver().unwrap().loading);
|
||||
assert!(session.editor.resolver().unwrap().error.is_some());
|
||||
session.execute("resolve.close");
|
||||
assert!(
|
||||
session
|
||||
.editor
|
||||
.documents()
|
||||
.any(|document| document.id == buffer
|
||||
&& document.dirty
|
||||
&& document.lines == ["draft", ""])
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read_to_string(fixture.0.join("buffer.txt")).unwrap(),
|
||||
"disk\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pull_request_pages_stream_concurrently_and_retry_only_the_failed_category() {
|
||||
use crate::diff::{PullRequestPage, RepoKind};
|
||||
|
||||
Reference in New Issue
Block a user