feat: show line change counts in diff tabs

This commit is contained in:
Valdemar Lorenzen
2026-10-16 12:02:42 +02:00
parent c689d9e782
commit a664b807b3
5 changed files with 122 additions and 49 deletions
+5 -3
View File
@@ -3381,9 +3381,11 @@ impl Editor {
}
pub fn tab_is_diff(&self, index: usize) -> bool {
self.tab_order
.get(index)
.is_some_and(|id| self.tab_diff(*id).is_some())
self.tab_diff_at(index).is_some()
}
pub(crate) fn tab_diff_at(&self, index: usize) -> Option<&DiffState> {
self.tab_diff(*self.tab_order.get(index)?)
}
pub(crate) fn tab_diff(&self, id: usize) -> Option<&DiffState> {
+113 -26
View File
@@ -1626,7 +1626,7 @@ fn chrome_title(
index: usize,
path: Option<&std::path::Path>,
dirty: bool,
is_diff: bool,
diff: Option<&DiffState>,
) -> String {
let name = path
.map(|path| {
@@ -1645,7 +1645,23 @@ fn chrome_title(
};
match field {
"index" => write!(title, "{index}").unwrap(),
"view" if is_diff => title.push_str("Diff · "),
"view" if diff.is_some() => {
title.push_str("Diff · ");
if let Some(diff) = diff
&& diff.phase == Phase::View
&& let Some(file) = diff
.comparison
.as_ref()
.and_then(|comparison| comparison.files.get(diff.file))
&& file.loaded
{
let (added, removed) =
file.hunks.iter().fold((0, 0), |(added, removed), hunk| {
(added + hunk.after_len, removed + hunk.before_len)
});
write!(title, "+{added} -{removed} · ").unwrap();
}
}
"name" => title.push_str(&name),
"path" => title.push_str(&path),
"modified" if dirty => title.push_str(" [+]"),
@@ -1680,7 +1696,7 @@ fn chrome_items<'a>(
index + 1,
buffer.as_ref().and_then(|buffer| buffer.path),
buffer.is_some_and(|buffer| buffer.dirty),
editor.tab_is_diff(index),
editor.tab_diff_at(index),
)),
config,
tab: Some(index),
@@ -1705,7 +1721,7 @@ fn chrome_items<'a>(
editor.tab_index() + 1,
editor.path(),
editor.dirty(),
editor.tab_is_diff(editor.tab_index()),
editor.tab_diff_at(editor.tab_index()),
)),
"position" => Cow::Owned(format!(
"{}:{}",
@@ -1780,7 +1796,7 @@ fn chrome_items<'a>(
editor.tab_index() + 1,
editor.path(),
editor.dirty(),
editor.tab_is_diff(editor.tab_index()),
editor.tab_diff_at(editor.tab_index()),
));
} else if text.chars().any(char::is_control) {
text = Cow::Owned(display_text(&text));
@@ -10728,7 +10744,7 @@ mod tests {
let path = directory.file("λ\t{index}.rs", "source");
let editor = Editor::open(Some(path.clone()), vec![]).unwrap();
let groups = chrome_groups(&editor, editor.bottom_bar_config(), &SyntaxCache::new());
let title = chrome_title("{path}", 2, Some(&path), false, false);
let title = chrome_title("{path}", 2, Some(&path), false, None);
assert_eq!(title, display_text(&display_path(&path)));
assert!(groups.iter().flatten().any(|item| item.text == title));
assert_eq!(editor.path(), Some(path.as_path()));
@@ -11411,10 +11427,10 @@ end
}
#[test]
fn comparison_titles_distinguish_same_named_tabs_and_follow_current_context() {
fn comparison_titles_follow_loaded_counts_and_each_tabs_context() {
let directory = Directory::new();
let path = directory.file("source.rs", "fn main() {}");
for phase in ["loading", "before", "after", "view"] {
for phase in ["loading", "before", "after", "unloaded", "view"] {
let mut editor = Editor::open(Some(path.clone()), vec![]).unwrap();
editor.execute("tab.new");
editor.execute("diff");
@@ -11422,14 +11438,26 @@ end
let (id, _) = editor.take_diff_request().unwrap();
editor.apply_diff_result(id, Ok(crate::diff::Outcome::Catalog(diff_catalog())));
}
if matches!(phase, "after" | "view") {
if matches!(phase, "after" | "unloaded" | "view") {
editor.handle(Input::Down);
editor.handle(Input::Enter);
}
if phase == "view" {
if matches!(phase, "unloaded" | "view") {
editor.handle(Input::Down);
editor.handle(Input::Enter);
let (id, _) = editor.take_diff_request().unwrap();
let mut file = syntax_diff_file(
"source.rs",
"old\ncontext\nremove",
"new\ncontext\nextra\nmore",
);
file.hunks = crate::diff::line_hunks(&file.before, false, &file.after, false);
if phase == "unloaded" {
file.loaded = false;
file.before.clear();
file.after.clear();
file.hunks.clear();
}
editor.apply_diff_result(
id,
Ok(crate::diff::Outcome::Comparison(crate::diff::Comparison {
@@ -11437,24 +11465,24 @@ end
editable_paths: Default::default(),
before: "baseline".into(),
after: "historic".into(),
files: vec![syntax_diff_file("source.rs", "fn old() {}", "fn main() {}")],
files: vec![file],
})),
);
}
let view = if phase == "view" {
"Diff · +3 -2 · "
} else {
"Diff · "
};
let default_tabs = editor.top_bar_config().segment("tabs").clone();
for (template, ordinary, comparison, marker_count) in [
(None, "1 source.rs", "2 Diff · source.rs", 1),
(
Some("{index} {name}{modified}"),
"1 source.rs",
"2 source.rs",
0,
),
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}"),
"T1:source.rs",
"T2:Diff · source.rs",
1,
"T2:",
true,
),
] {
configure_chrome(&mut editor, |top, bottom| {
@@ -11486,19 +11514,21 @@ end
.unwrap();
let titles = pane_screen(&terminal, Rect::new(0, 0, 160, 1));
assert!(titles.contains(ordinary), "{phase}: {titles}");
assert!(titles.contains(comparison), "{phase}: {titles}");
let comparison =
format!("{prefix}{}source.rs", if uses_view { view } else { "" });
assert!(titles.contains(&comparison), "{phase}: {titles}");
assert_eq!(
titles.matches("Diff · ").count(),
marker_count,
usize::from(uses_view),
"{phase}: {titles}"
);
let current = pane_screen(&terminal, Rect::new(0, 1, 160, 1));
let expected = if comparison_active {
"Current:Diff · source.rs"
format!("Current:{view}source.rs")
} else {
"Current:source.rs"
"Current:source.rs".into()
};
assert!(current.contains(expected), "{phase}: {current}");
assert!(current.contains(&expected), "{phase}: {current}");
editor.execute(if comparison_active {
"tab.previous"
} else {
@@ -11509,6 +11539,63 @@ end
}
}
#[test]
fn comparison_title_counts_follow_live_edits_and_undo_in_hidden_tabs() {
let directory = Directory::new();
let path = directory.file("live.rs", "new");
let mut editor = Editor::open(Some(path), vec![]).unwrap();
editor.execute("diff");
let (id, _) = editor.take_diff_request().unwrap();
editor.apply_diff_result(id, Ok(crate::diff::Outcome::Catalog(diff_catalog())));
editor.handle(Input::Down);
editor.handle(Input::Enter);
editor.handle(Input::Enter);
let (id, _) = editor.take_diff_request().unwrap();
editor.apply_diff_result(
id,
Ok(crate::diff::Outcome::Comparison(crate::diff::Comparison {
root: directory.0.clone(),
editable_paths: ["live.rs".into()].into(),
before: "baseline".into(),
after: "Working checkout".into(),
files: vec![syntax_diff_file("live.rs", "old", "new")],
})),
);
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"),
(
vec![
Input::Char('i'),
Input::Paste("extra\n".into()),
Input::Escape,
],
"+2 -1",
),
(vec![Input::Char('u')], "+1 -1"),
(vec![Input::Char('U')], "+2 -1"),
] {
for input in inputs {
editor.handle(input);
}
editor.refresh_live_diff();
terminal
.draw(|frame| views.draw(frame, &mut editor))
.unwrap();
let titles = pane_screen(&terminal, Rect::new(0, 0, 120, 1));
assert!(
titles.contains(&format!("1 Diff · {counts} · live.rs")),
"{titles}"
);
assert!(titles.contains("2 live.rs"), "{titles}");
}
}
#[test]
fn tab_titles_update_templates_without_reinterpreting_file_names() {
let directory = Directory::new();