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:
+131
-51
@@ -3942,6 +3942,31 @@ fn diff_pane_panel(
|
||||
return area;
|
||||
}
|
||||
let focus = if before { Focus::Before } else { Focus::After };
|
||||
if let Some(resolver) = resolver {
|
||||
let file = resolver.files.get(resolver.current_file);
|
||||
let title = if before {
|
||||
" Resolution "
|
||||
} else {
|
||||
" Other version "
|
||||
};
|
||||
let state = if before {
|
||||
format!(
|
||||
" editable · {} ",
|
||||
file.map_or("loading", |file| resolver_status(&file.status).0)
|
||||
)
|
||||
} else {
|
||||
" read-only ".to_owned()
|
||||
};
|
||||
let source = if before {
|
||||
String::new()
|
||||
} else {
|
||||
file.and_then(|file| file.sources.get(resolver.selected_source))
|
||||
.map_or_else(String::new, |source| {
|
||||
format!(" {} ", display_text(&source.label))
|
||||
})
|
||||
};
|
||||
return diff_panel(frame, area, title, &state, &source, diff.focus == focus);
|
||||
}
|
||||
let label = diff.side_label(focus);
|
||||
let title = file.map_or_else(
|
||||
|| format!(" Diff · {} ", diff.comparison_mode.label()),
|
||||
@@ -3964,60 +3989,33 @@ fn diff_pane_panel(
|
||||
)
|
||||
},
|
||||
);
|
||||
let revision = if let Some(resolver) = resolver {
|
||||
let source = resolver
|
||||
.files
|
||||
.get(resolver.current_file)
|
||||
.and_then(|file| {
|
||||
file.sources.get(if before {
|
||||
file.seed_source
|
||||
} else {
|
||||
resolver.selected_source
|
||||
})
|
||||
})
|
||||
.map_or("", |source| source.label.as_str());
|
||||
if before {
|
||||
format!(" seeded from {} ", display_text(source))
|
||||
} else {
|
||||
format!(" {} ", display_text(source))
|
||||
}
|
||||
} else {
|
||||
diff.comparison
|
||||
.as_ref()
|
||||
.map_or_else(String::new, |comparison| {
|
||||
if diff.view_mode == ViewMode::Split && diff.pane_layout() == PaneLayout::Split {
|
||||
format!(
|
||||
" {} ",
|
||||
display_text(if before {
|
||||
&comparison.before
|
||||
} else {
|
||||
&comparison.after
|
||||
})
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
" {} → {} ",
|
||||
display_text(&comparison.before),
|
||||
display_text(&comparison.after)
|
||||
)
|
||||
}
|
||||
})
|
||||
};
|
||||
let revision = diff
|
||||
.comparison
|
||||
.as_ref()
|
||||
.map_or_else(String::new, |comparison| {
|
||||
if diff.view_mode == ViewMode::Split && diff.pane_layout() == PaneLayout::Split {
|
||||
format!(
|
||||
" {} ",
|
||||
display_text(if before {
|
||||
&comparison.before
|
||||
} else {
|
||||
&comparison.after
|
||||
})
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
" {} → {} ",
|
||||
display_text(&comparison.before),
|
||||
display_text(&comparison.after)
|
||||
)
|
||||
}
|
||||
});
|
||||
let access = if diff.side_editable(focus) {
|
||||
" editable "
|
||||
} else {
|
||||
" read-only "
|
||||
};
|
||||
let state = if let Some(resolver) = resolver {
|
||||
let status = resolver
|
||||
.files
|
||||
.get(resolver.current_file)
|
||||
.map_or("loading", |file| resolver_status(&file.status).0);
|
||||
Cow::Owned(format!("{access}· {status} "))
|
||||
} else {
|
||||
Cow::Borrowed(access)
|
||||
};
|
||||
diff_panel(frame, area, &title, &state, &revision, diff.focus == focus)
|
||||
diff_panel(frame, area, &title, access, &revision, diff.focus == focus)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -5465,7 +5463,10 @@ fn draw_file_list(
|
||||
));
|
||||
spans.push(Span::raw(" "));
|
||||
}
|
||||
if let Some(status) = status {
|
||||
if picker.is_conflicted(entry.path) {
|
||||
spans.push(Span::styled("C", Style::default().fg(Color::Yellow)));
|
||||
spans.push(Span::raw(" "));
|
||||
} else if let Some(status) = status {
|
||||
let color = match status {
|
||||
FileStatus::Added => Color::Green,
|
||||
FileStatus::Removed => Color::Red,
|
||||
@@ -6994,7 +6995,6 @@ mod tests {
|
||||
assert!(draft.height > 20);
|
||||
assert!(views.panels.files.is_none());
|
||||
let screen = screen_text(&terminal);
|
||||
assert!(screen.contains("Draft") && screen.contains("Alternative"));
|
||||
assert!(screen.contains("unchanged lines hidden"));
|
||||
assert!(!screen.contains("context 30"));
|
||||
assert!(!screen.contains("<<<<<<<") && !screen.contains("%%%%%%%"));
|
||||
@@ -7003,6 +7003,27 @@ mod tests {
|
||||
.map(|x| terminal.backend().buffer()[(x, y)].symbol())
|
||||
.collect::<String>()
|
||||
};
|
||||
let left_heading = line_at(&terminal, draft, draft.y);
|
||||
let right_heading = line_at(&terminal, alternative, alternative.y);
|
||||
assert!(left_heading.contains("Resolution") && left_heading.contains("editable"));
|
||||
assert!(right_heading.contains("Other version") && right_heading.contains("read-only"));
|
||||
for heading in [&left_heading, &right_heading] {
|
||||
assert!(!heading.contains("Branch changes"));
|
||||
assert!(
|
||||
!heading.contains(resolver.files[current_file].path.to_string_lossy().as_ref())
|
||||
);
|
||||
assert!(!heading.split_whitespace().any(|word| word == "M"));
|
||||
}
|
||||
assert!(
|
||||
line_at(&terminal, alternative, alternative.bottom() - 1)
|
||||
.contains(&resolver.files[current_file].sources[alternative_index].label)
|
||||
);
|
||||
assert!(
|
||||
line_at(&terminal, draft, draft.bottom() - 1)
|
||||
.chars()
|
||||
.all(|ch| !ch.is_alphanumeric()),
|
||||
"The output must not repeat its seed source metadata"
|
||||
);
|
||||
for (side, area) in [draft, alternative].into_iter().enumerate() {
|
||||
let content = diff_inner(area);
|
||||
let x = content.right() - geometry.text_widths[side];
|
||||
@@ -7233,6 +7254,11 @@ mod tests {
|
||||
let geometry = views.panels.diff.as_ref().unwrap();
|
||||
let left = diff_inner(geometry.regions[1].0);
|
||||
let right = diff_inner(geometry.regions[2].0);
|
||||
let left_heading = line_at(&terminal, geometry.regions[1].0, geometry.regions[1].0.y);
|
||||
let right_heading = line_at(&terminal, geometry.regions[2].0, geometry.regions[2].0.y);
|
||||
assert!(left_heading.contains("unsaved"));
|
||||
assert!(!right_heading.contains("unsaved"));
|
||||
assert!(right_heading.contains("read-only"));
|
||||
assert!(line_at(&terminal, left, left.y + 1).contains("manual opening context 00"));
|
||||
assert!(line_at(&terminal, right, right.y + 1).contains("opening context 00"));
|
||||
assert!(!line_at(&terminal, right, right.y + 1).contains("manual"));
|
||||
@@ -9175,6 +9201,60 @@ mod tests {
|
||||
assert!(!text.contains('✓'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_explorer_marks_only_confirmed_conflicts() {
|
||||
let directory = Directory::new();
|
||||
directory.file("file.txt", "");
|
||||
let mut picker = FilePicker::scan(
|
||||
directory.0.clone(),
|
||||
FilePresentation::Floating,
|
||||
FileView::List,
|
||||
)
|
||||
.unwrap();
|
||||
picker.set_repository_changes(vec![crate::repository::WorkingFileChange {
|
||||
path: "file.txt".into(),
|
||||
status: FileStatus::Modified,
|
||||
old_path: None,
|
||||
}]);
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(80, 12)).unwrap();
|
||||
for view in [FileView::List, FileView::Tree] {
|
||||
picker.set_view(view);
|
||||
for conflicted in [true, false] {
|
||||
picker.set_conflicted(std::path::Path::new("file.txt"), conflicted);
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
draw_file_list(
|
||||
frame,
|
||||
&picker,
|
||||
frame.area(),
|
||||
"",
|
||||
&mut WheelPanels::default(),
|
||||
None,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
let text = screen_text(&terminal);
|
||||
let row = text.lines().find(|row| row.contains("file.txt")).unwrap();
|
||||
assert!(row.contains(if conflicted {
|
||||
"C file.txt"
|
||||
} else {
|
||||
"M file.txt"
|
||||
}));
|
||||
if conflicted {
|
||||
let buffer = terminal.backend().buffer();
|
||||
let (x, y) = (0..buffer.area.height)
|
||||
.find_map(|y| {
|
||||
(0..buffer.area.width)
|
||||
.find(|&x| buffer[(x, y)].symbol() == "C")
|
||||
.map(|x| (x, y))
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(buffer[(x, y)].fg, Color::Yellow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_file_names_stay_put_when_status_arrives_or_clears() {
|
||||
let directory = Directory::new();
|
||||
|
||||
Reference in New Issue
Block a user