fix: clarify conflict pane roles and status indicators

Use Resolution and Other version borders, omit branch/path metadata,
and show unsaved output state only on the writable pane. Track native
explorer conflict C from authoritative jj state across draft/check/error
transitions and clear it only after confirmed resolution.

Focused resolver and save tests pass; live terminal labels and explorer
verified without saving the mock conflicts. Full library run: 795 pass,
one unrelated async plugin fixture_barrier startup failure.
This commit is contained in:
Valdemar Lorenzen
2026-10-15 10:50:22 +02:00
parent 168e12160a
commit 5396968b08
5 changed files with 265 additions and 66 deletions
+102 -11
View File
@@ -1482,6 +1482,8 @@ pub struct ResolverFile {
pub path: PathBuf,
pub draft_buffer: usize,
pub status: ResolverStatus,
/// Retained through draft edits and failed/pending jj confirmation.
jj_conflicted: bool,
pub sources: Vec<ResolverSource>,
pub text: bool,
pub whole_file_choice: Option<usize>,
@@ -1492,6 +1494,22 @@ pub struct ResolverFile {
comparison_key: Option<(u64, usize)>,
}
impl ResolverFile {
fn apply_jj_status(&mut self, status: crate::resolve::Status) {
self.status = match status {
crate::resolve::Status::Unresolved => {
self.jj_conflicted = true;
ResolverStatus::Unresolved
}
crate::resolve::Status::Resolved => {
self.jj_conflicted = false;
ResolverStatus::Resolved
}
crate::resolve::Status::Unconfirmed => ResolverStatus::Unconfirmed,
};
}
}
pub struct ResolverWorkspace {
pub files: Vec<ResolverFile>,
pub current_file: usize,
@@ -2285,6 +2303,16 @@ impl Editor {
};
}
}
if let Some(mut picker) = resolver
.files
.get_mut(resolver.current_file)
.and_then(|file| file.diff.files.take())
{
for file in &resolver.files {
picker.set_conflicted(&file.path, file.jj_conflicted);
}
resolver.files[resolver.current_file].diff.files = Some(picker);
}
}
}
@@ -2394,6 +2422,7 @@ impl Editor {
files.push(ResolverFile {
path: file.path.clone(), draft_buffer,
status: ResolverStatus::Unresolved, text, whole_file_choice: None,
jj_conflicted: file.status != crate::resolve::Status::Resolved,
error: file.notice.clone(),
seed_source,
projection,
@@ -2478,12 +2507,8 @@ impl Editor {
.map(split_lines);
let was_text = resolver.files[file].text;
resolver.files[file].text = saved_text.is_some();
let status = match model.files[file].status {
crate::resolve::Status::Resolved => ResolverStatus::Resolved,
crate::resolve::Status::Unresolved => ResolverStatus::Unresolved,
crate::resolve::Status::Unconfirmed => ResolverStatus::Unconfirmed,
};
resolver.files[file].status = status;
resolver.files[file].apply_jj_status(model.files[file].status);
let status = resolver.files[file].status;
resolver.files[file].error = outcome.confirmation.clone();
resolver.files[file].whole_file_choice = None;
if let Some(buffer) = if self.active.id == buffer {
@@ -2639,6 +2664,7 @@ impl Editor {
resolver.files[file].comparison_key = None;
self.blur_file_picker();
self.refresh_resolver_comparison();
self.refresh_resolver_status();
if self.resolver().unwrap().view == ResolverView::History {
self.set_resolver_view(ResolverView::History);
}
@@ -12552,6 +12578,7 @@ mod tests {
path: "file.txt".into(),
draft_buffer: result,
status: ResolverStatus::Unsaved,
jj_conflicted: true,
text: true,
whole_file_choice: None,
error: None,
@@ -12688,11 +12715,6 @@ mod tests {
ordinary.diff().unwrap().display_rows
);
assert!(resolver.diff_editing());
assert_eq!(resolver.diff().unwrap().side_label(Focus::Before), "Draft");
assert_eq!(
resolver.diff().unwrap().side_label(Focus::After),
"Alternative"
);
assert!(resolver.diff().unwrap().side_editable(Focus::Before));
assert!(!resolver.diff().unwrap().side_editable(Focus::After));
let draft_buffer = resolver.buffer_id();
@@ -12791,6 +12813,7 @@ mod tests {
path: "conflict.txt".into(),
draft_buffer,
status: ResolverStatus::Unsaved,
jj_conflicted: true,
text: true,
whole_file_choice: None,
error: None,
@@ -12812,6 +12835,73 @@ mod tests {
editor
}
#[test]
fn resolver_explorer_retains_last_jj_conflict_state_until_confirmation() {
let mut editor = resolver_editor("draft", &[("draft", false), ("other", false)]);
let path = Path::new("conflict.txt");
let marker = |editor: &Editor| {
editor
.resolver_workspace_diff()
.unwrap()
.files
.as_ref()
.unwrap()
.is_conflicted(path)
};
assert_eq!(
editor.resolver().unwrap().files[0].status,
ResolverStatus::Unsaved
);
assert!(marker(&editor));
for confirmed in [
crate::resolve::Status::Unresolved,
crate::resolve::Status::Resolved,
crate::resolve::Status::Unresolved,
] {
editor.resolvers.get_mut(&editor.tab_id).unwrap().files[0].apply_jj_status(confirmed);
editor.refresh_resolver_status();
let conflicted = confirmed == crate::resolve::Status::Unresolved;
assert_eq!(marker(&editor), conflicted);
assert_eq!(
editor.resolver().unwrap().files[0].status,
ResolverStatus::Unsaved
);
editor.resolvers.get_mut(&editor.tab_id).unwrap().files[0].status =
ResolverStatus::Checking;
editor.refresh_resolver_status();
assert_eq!(
editor.resolver().unwrap().files[0].status,
ResolverStatus::Checking
);
assert_eq!(marker(&editor), conflicted);
editor.resolvers.get_mut(&editor.tab_id).unwrap().files[0]
.apply_jj_status(crate::resolve::Status::Unconfirmed);
assert_eq!(
editor.resolver().unwrap().files[0].status,
ResolverStatus::Unconfirmed
);
editor.active.saved_lines.clone_from(&editor.active.lines);
editor.refresh_resolver_status();
assert_eq!(
editor.resolver().unwrap().files[0].status,
ResolverStatus::Unconfirmed
);
assert_eq!(marker(&editor), conflicted);
editor.apply(vec![Effect::Insert(" edit".into())]).unwrap();
editor.refresh_resolver_status();
assert_eq!(
editor.resolver().unwrap().files[0].status,
ResolverStatus::Unsaved
);
assert_eq!(marker(&editor), conflicted);
editor.select_resolver_file(0);
assert_eq!(marker(&editor), conflicted);
}
}
#[test]
fn resolver_takes_only_current_diff_hunk_and_undo_preserves_native_newlines() {
for (draft, alternative, chosen) in [
@@ -12944,6 +13034,7 @@ mod tests {
path: "second.txt".into(),
draft_buffer: buffer,
status: ResolverStatus::Unresolved,
jj_conflicted: true,
text: true,
whole_file_choice: None,
error: None,