improve: show whole-diff totals alongside file stats

This commit is contained in:
Valdemar Lorenzen
2026-10-20 12:29:06 +02:00
parent a5a246db77
commit 759947736e
15 changed files with 394 additions and 48 deletions
+8 -3
View File
@@ -77,6 +77,9 @@ fn omitted_options_inherit_and_explicit_groups_replace_defaults() {
#[test]
fn both_positions_accept_all_builtins_and_custom_styles() {
let defaults = ChromeLine::top();
assert_eq!(defaults.right, ["diff", "shell", "battery"]);
assert_eq!(defaults.segment("diff"), defaults.segment("shell"));
let names = [
"tabs",
"mode",
@@ -90,6 +93,7 @@ fn both_positions_accept_all_builtins_and_custom_styles() {
"lsp",
"position",
"battery",
"diff",
"workspace",
];
let options = serde_json::json!({
@@ -119,7 +123,7 @@ fn both_positions_accept_all_builtins_and_custom_styles() {
}
#[test]
fn view_placeholder_is_valid_in_every_segment_and_bar() {
fn comparison_placeholders_are_valid_in_every_segment_and_bar() {
for name in [
"tabs",
"mode",
@@ -133,11 +137,12 @@ fn view_placeholder_is_valid_in_every_segment_and_bar() {
"lsp",
"position",
"battery",
"diff",
"workspace",
] {
let options = serde_json::json!({
"left": [name],
(name): {"title": "{index}:{view}{name}:{path}{modified}"}
(name): {"title": "{index}:{view}{name}{changes}:{path}{modified}"}
});
let lines: Lines = serde_json::from_value(serde_json::json!({
"top": options,
@@ -147,7 +152,7 @@ fn view_placeholder_is_valid_in_every_segment_and_bar() {
for line in [&lines.top, &lines.bottom] {
assert_eq!(
line.segment(name).title,
"{index}:{view}{name}:{path}{modified}"
"{index}:{view}{name}{changes}:{path}{modified}"
);
}
}
+20
View File
@@ -789,6 +789,7 @@ fn comparison() -> Comparison {
files: vec![
ChangedFile {
review_revisions: None,
stats: None,
path: "renamed.txt".into(),
old_path: Some("old.txt".into()),
status: FileStatus::Renamed,
@@ -827,6 +828,7 @@ fn comparison() -> Comparison {
},
ChangedFile {
review_revisions: None,
stats: None,
path: "added.txt".into(),
old_path: None,
status: FileStatus::Added,
@@ -845,6 +847,7 @@ fn comparison() -> Comparison {
},
ChangedFile {
review_revisions: None,
stats: None,
path: "removed.bin".into(),
old_path: None,
status: FileStatus::Removed,
@@ -860,6 +863,19 @@ fn comparison() -> Comparison {
}
}
#[test]
fn line_counts_use_frozen_metadata_only_until_hunks_are_loaded() {
let mut file = comparison().files.remove(0);
file.loaded = false;
assert_eq!(file.line_counts(), None);
file.stats = Some((99, 98));
assert_eq!(file.line_counts(), Some((99, 98)));
file.loaded = true;
assert_eq!(file.line_counts(), Some((2, 2)));
file.hunks.clear();
assert_eq!(file.line_counts(), Some((0, 0)));
}
#[test]
fn status_layout_cycles_only_present_panes_and_normalizes_selection() {
let mut state = live_state(vec!["old".into()], vec!["new".into()], true);
@@ -1566,6 +1582,7 @@ fn closing_diff_rejects_pending_file_details_even_after_reopening() {
catalog.files.clear();
catalog.files.push(ChangedFile {
review_revisions: None,
stats: None,
path: detail.path.clone(),
old_path: detail.old_path.clone(),
status: detail.status,
@@ -1816,6 +1833,7 @@ fn live_state(before: Vec<String>, after: Vec<String>, eof: bool) -> DiffState {
editable_paths: BTreeSet::from([PathBuf::from("file")]),
files: vec![ChangedFile {
review_revisions: None,
stats: None,
path: "file".into(),
old_path: None,
status: FileStatus::Modified,
@@ -2503,6 +2521,7 @@ fn reverting_loaded_file_keeps_unloaded_changes_selectable() {
state.pending = None;
state.comparison.as_mut().unwrap().files.push(ChangedFile {
review_revisions: None,
stats: None,
path: "unloaded.txt".into(),
old_path: None,
status: FileStatus::Modified,
@@ -2731,6 +2750,7 @@ fn lazy_selection_waits_for_details_rejects_stale_files_and_reuses_loaded_cache(
for detail in &details {
catalog.files.push(ChangedFile {
review_revisions: None,
stats: None,
path: detail.path.clone(),
old_path: detail.old_path.clone(),
status: detail.status,
+7
View File
@@ -1887,6 +1887,7 @@ fn hidden_tab_diff_results_restore_visible_insert_interaction_and_route_request_
editable_paths: Default::default(),
files: vec![ChangedFile {
review_revisions: None,
stats: None,
loaded: true,
path: "file.txt".into(),
old_path: None,
@@ -3053,6 +3054,7 @@ fn working_diff(editor: &mut Editor, root: &Path, paths: &[&str]) -> Box<[(PathB
let lines: Vec<String> = text.lines().map(str::to_owned).collect();
ChangedFile {
review_revisions: None,
stats: None,
loaded: true,
path: PathBuf::from(*path),
old_path: None,
@@ -3115,6 +3117,7 @@ fn historical_diff(editor: &mut Editor, root: &Path, before: Vec<String>, after:
editable_paths: Default::default(),
files: vec![ChangedFile {
review_revisions: None,
stats: None,
loaded: true,
path: "file.txt".into(),
old_path: None,
@@ -4078,6 +4081,7 @@ fn bookmark_refresh_reloads_paths_and_preserves_view_until_lazy_content_arrives(
let cursor = editor.cursor();
let file = |path: &str, loaded| ChangedFile {
review_revisions: None,
stats: None,
path: path.into(),
old_path: None,
status: FileStatus::Modified,
@@ -4288,6 +4292,7 @@ fn completed_working_refresh_serializes_lazy_selection_and_invalidates_pending_n
if changed {
let file = crate::diff::ChangedFile {
review_revisions: None,
stats: None,
path: "nested/new.txt".into(),
old_path: Some("nested/old.txt".into()),
status: crate::diff::FileStatus::Renamed,
@@ -4329,6 +4334,7 @@ fn lazy_lsp_target(editor: &mut Editor) -> crate::diff::ChangedFile {
let comparison = diff.comparison.as_mut().unwrap();
comparison.files.push(ChangedFile {
review_revisions: None,
stats: None,
path: "nested/new.txt".into(),
old_path: Some("nested/old.txt".into()),
status: FileStatus::Renamed,
@@ -4350,6 +4356,7 @@ fn lazy_lsp_target(editor: &mut Editor) -> crate::diff::ChangedFile {
));
ChangedFile {
review_revisions: None,
stats: None,
path: "nested/new.txt".into(),
old_path: Some("nested/old.txt".into()),
status: FileStatus::Renamed,
+1
View File
@@ -3730,6 +3730,7 @@ fn async_responses_edit_only_current_bound_working_after_and_refresh_projection(
editable_paths: [PathBuf::from("buffer.txt")].into_iter().collect(),
files: vec![ChangedFile {
review_revisions: None,
stats: None,
path: PathBuf::from("buffer.txt"),
old_path: None,
status: FileStatus::Modified,
+65 -17
View File
@@ -1985,6 +1985,7 @@ fn syntax_diff_file(path: &str, before: &str, after: &str) -> ChangedFile {
let after: Vec<String> = after.split('\n').map(str::to_owned).collect();
ChangedFile {
review_revisions: None,
stats: None,
loaded: true,
before_eof: false,
after_eof: false,
@@ -2608,6 +2609,7 @@ fn diff_comparison(files: Vec<ChangedFile>) -> DiffState {
fn deletion_file() -> ChangedFile {
ChangedFile {
review_revisions: None,
stats: None,
loaded: true,
before_eof: false,
after_eof: false,
@@ -3559,6 +3561,7 @@ fn diff_excerpts_render_original_lines_and_expand_without_losing_changes() {
after[60] = "let second_change = 901;".into();
let mut diff = diff_comparison(vec![ChangedFile {
review_revisions: None,
stats: None,
loaded: true,
before_eof: false,
after_eof: false,
@@ -3699,6 +3702,7 @@ fn diff_row_tint_covers_blank_lines_and_wrapped_gutters() {
fn diff_added_rows_hatch_missing_before_but_tint_real_empty_after_lines() {
let mut diff = diff_comparison(vec![ChangedFile {
review_revisions: None,
stats: None,
loaded: true,
before_eof: false,
after_eof: false,
@@ -3757,6 +3761,7 @@ fn diff_added_rows_hatch_missing_before_but_tint_real_empty_after_lines() {
fn diff_changed_backgrounds_preserve_syntax_and_clipped_source_cells() {
let mut diff = diff_comparison(vec![ChangedFile {
review_revisions: None,
stats: None,
loaded: true,
before_eof: false,
after_eof: false,
@@ -3887,6 +3892,7 @@ fn diff_changed_backgrounds_preserve_syntax_and_clipped_source_cells() {
fn diff_view_changes_files_focus_and_keeps_horizontal_offsets_independent() {
let added = ChangedFile {
review_revisions: None,
stats: None,
loaded: true,
before_eof: false,
after_eof: false,
@@ -4649,6 +4655,7 @@ fn large_generated_additions_render_first_scroll_and_final_rows_without_reparsin
let hunks = crate::diff::line_hunks(&[], false, &after, true);
let mut diff = diff_comparison(vec![ChangedFile {
review_revisions: None,
stats: None,
path: "parser.c".into(),
old_path: None,
status: FileStatus::Added,
@@ -4699,6 +4706,7 @@ fn diff_notices_empty_comparisons_and_tiny_layouts_remain_renderable() {
assert!(screen_text(&terminal).contains("Explorer · Tree"));
let binary = diff_comparison(vec![ChangedFile {
review_revisions: None,
stats: None,
loaded: true,
before_eof: false,
after_eof: false,
@@ -4714,6 +4722,7 @@ fn diff_notices_empty_comparisons_and_tiny_layouts_remain_renderable() {
assert!(screen_text(&terminal).contains("Binary file"));
let blank = diff_comparison(vec![ChangedFile {
review_revisions: None,
stats: None,
loaded: true,
before_eof: false,
after_eof: false,
@@ -5664,17 +5673,13 @@ fn comparison_titles_follow_loaded_counts_and_each_tabs_context() {
})),
);
}
let view = if phase == "view" {
"Diff · +3 -2 · "
} else {
"Diff · "
};
let changes = if phase == "view" { " +3 -2" } else { "" };
let default_tabs = editor.top_bar_config().segment("tabs").clone();
for (template, ordinary, prefix, uses_view) in [
(None, "1 source.rs", "2 ", true),
(Some("{index} {name}{modified}"), "1 source.rs", "2 ", false),
(
Some("T{index}:{view}{name}{modified}"),
Some("T{index}:{view}{name}{changes}{modified}"),
"T1:source.rs",
"T2:",
true,
@@ -5688,7 +5693,8 @@ fn comparison_titles_follow_loaded_counts_and_each_tabs_context() {
bottom.left = vec!["mode".into()];
bottom.center.clear();
bottom.right.clear();
bottom.segments.get_mut("mode").unwrap().title = "Current:{view}{name}".into();
bottom.segments.get_mut("mode").unwrap().title =
"Current:{view}{name}{changes}".into();
});
for comparison_active in [true, false] {
let mut terminal =
@@ -5709,16 +5715,33 @@ fn comparison_titles_follow_loaded_counts_and_each_tabs_context() {
.unwrap();
let titles = pane_screen(&terminal, Rect::new(0, 0, 160, 1));
assert!(titles.contains(ordinary), "{phase}: {titles}");
let comparison = format!("{prefix}{}source.rs", if uses_view { view } else { "" });
let comparison = if uses_view {
format!("{prefix}Diff · source.rs{changes}")
} else {
format!("{prefix}source.rs")
};
assert!(titles.contains(&comparison), "{phase}: {titles}");
assert_eq!(
titles.matches("Diff · ").count(),
usize::from(uses_view),
"{phase}: {titles}"
);
if matches!(phase, "view" | "unloaded") && comparison_active {
let total = if phase == "view" {
"Diff +3 -2"
} else {
"Diff …"
};
assert!(titles.contains(total), "{phase}: {titles}");
} else {
assert!(
!titles.contains("Diff +") && !titles.contains("Diff …"),
"{phase}: {titles}"
);
}
let current = pane_screen(&terminal, Rect::new(0, 1, 160, 1));
let expected = if comparison_active {
format!("Current:{view}source.rs")
format!("Current:Diff · source.rs{changes}")
} else {
"Current:source.rs".into()
};
@@ -5745,6 +5768,12 @@ fn comparison_title_counts_follow_live_edits_and_undo_in_hidden_tabs() {
editor.handle(Input::Enter);
editor.handle(Input::Enter);
let (id, _) = editor.take_diff_request().unwrap();
let mut unopened = syntax_diff_file("unopened.rs", "", "");
unopened.loaded = false;
unopened.before.clear();
unopened.after.clear();
unopened.hunks.clear();
unopened.stats = Some((7, 4));
editor.apply_diff_result(
id,
Ok(crate::diff::Outcome::Comparison(crate::diff::Comparison {
@@ -5754,17 +5783,25 @@ fn comparison_title_counts_follow_live_edits_and_undo_in_hidden_tabs() {
before: "baseline".into(),
after: "Working checkout".into(),
revisions: ["before-snapshot".into(), "after-snapshot".into()],
files: vec![syntax_diff_file("live.rs", "old", "new")],
files: vec![syntax_diff_file("live.rs", "old", "new"), unopened],
})),
);
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(120, 14)).unwrap();
let mut views = BufferViewports::default();
for input in [Input::Down, Input::Char('l'), Input::Ctrl('u'), Input::Home] {
editor.handle(input);
terminal
.draw(|frame| views.draw(frame, &mut editor))
.unwrap();
let titles = pane_screen(&terminal, Rect::new(0, 0, 120, 1));
assert!(titles.contains("Diff +8 -5"), "{titles}");
}
editor.handle(Input::Char('\t'));
editor.handle(Input::Char('\t'));
assert!(editor.diff_editing());
editor.execute("tab.new");
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(120, 14)).unwrap();
let mut views = BufferViewports::default();
for (inputs, counts) in [
(vec![], "+1 -1"),
for (inputs, counts, total) in [
(vec![], "+1 -1", "+8 -5"),
(
vec![
Input::Char('i'),
@@ -5772,9 +5809,10 @@ fn comparison_title_counts_follow_live_edits_and_undo_in_hidden_tabs() {
Input::Escape,
],
"+2 -1",
"+9 -5",
),
(vec![Input::Char('u')], "+1 -1"),
(vec![Input::Char('U')], "+2 -1"),
(vec![Input::Char('u')], "+1 -1", "+8 -5"),
(vec![Input::Char('U')], "+2 -1", "+9 -5"),
] {
for input in inputs {
editor.handle(input);
@@ -5785,10 +5823,18 @@ fn comparison_title_counts_follow_live_edits_and_undo_in_hidden_tabs() {
.unwrap();
let titles = pane_screen(&terminal, Rect::new(0, 0, 120, 1));
assert!(
titles.contains(&format!("1 Diff · {counts} · live.rs")),
titles.contains(&format!("1 Diff · live.rs {counts}")),
"{titles}"
);
assert!(titles.contains("2 live.rs"), "{titles}");
assert!(!titles.contains("Diff +"), "{titles}");
editor.execute("tab.previous");
terminal
.draw(|frame| views.draw(frame, &mut editor))
.unwrap();
let titles = pane_screen(&terminal, Rect::new(0, 0, 120, 1));
assert!(titles.contains(&format!("Diff {total}")), "{titles}");
editor.execute("tab.next");
}
}
@@ -7049,6 +7095,7 @@ fn custom_value_stays_above_split_overlays_and_diff_geometry() {
assert_eq!(pane_screen(&terminal, Rect::new(0, 0, 120, 1)), title);
let mut editor = readonly_diff_editor(deletion_file());
configure_chrome(&mut editor, |top, _| top.right = vec!["battery".into()]);
terminal
.draw(|frame| views.draw(frame, &mut editor))
.unwrap();
@@ -7234,6 +7281,7 @@ fn native_diff_dock_and_panes_fill_the_workspace_below_only_configured_chrome()
for (width, height) in [(120, 30), (32, 9)] {
for chrome in [false, true] {
let mut editor = readonly_diff_editor(deletion_file());
configure_chrome(&mut editor, |top, _| top.right = vec!["battery".into()]);
if chrome {
editor.set_label("battery".into(), "live".into()).unwrap();
}