git_ui: Clean up the commit view UI (#44162)

Danilo Leal created

Change summary

crates/editor/src/editor.rs      |   5 
crates/editor/src/element.rs     |  22 ++-
crates/git_ui/src/commit_view.rs | 205 ++++++++++++++++++---------------
3 files changed, 132 insertions(+), 100 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -20090,6 +20090,11 @@ impl Editor {
         self.show_indent_guides
     }
 
+    pub fn disable_indent_guides(&mut self) -> Option<bool> {
+        self.show_indent_guides = Some(false);
+        self.show_indent_guides
+    }
+
     pub fn toggle_line_numbers(
         &mut self,
         _: &ToggleLineNumbers,

crates/editor/src/element.rs 🔗

@@ -3915,6 +3915,8 @@ impl EditorElement {
     ) -> impl IntoElement {
         let editor = self.editor.read(cx);
         let multi_buffer = editor.buffer.read(cx);
+        let is_read_only = self.editor.read(cx).read_only(cx);
+
         let file_status = multi_buffer
             .all_diff_hunks_expanded()
             .then(|| editor.status_for_buffer_id(for_excerpt.buffer_id, cx))
@@ -3967,7 +3969,7 @@ impl EditorElement {
                     .gap_1p5()
                     .when(is_sticky, |el| el.shadow_md())
                     .border_1()
-                    .map(|div| {
+                    .map(|border| {
                         let border_color = if is_selected
                             && is_folded
                             && focus_handle.contains_focused(window, cx)
@@ -3976,7 +3978,7 @@ impl EditorElement {
                         } else {
                             colors.border
                         };
-                        div.border_color(border_color)
+                        border.border_color(border_color)
                     })
                     .bg(colors.editor_subheader_background)
                     .hover(|style| style.bg(colors.element_hover))
@@ -4056,13 +4058,15 @@ impl EditorElement {
                             })
                             .take(1),
                     )
-                    .child(
-                        h_flex()
-                            .size_3()
-                            .justify_center()
-                            .flex_shrink_0()
-                            .children(indicator),
-                    )
+                    .when(!is_read_only, |this| {
+                        this.child(
+                            h_flex()
+                                .size_3()
+                                .justify_center()
+                                .flex_shrink_0()
+                                .children(indicator),
+                        )
+                    })
                     .child(
                         h_flex()
                             .cursor_pointer()

crates/git_ui/src/commit_view.rs 🔗

@@ -1,7 +1,7 @@
 use anyhow::{Context as _, Result};
 use buffer_diff::{BufferDiff, BufferDiffSnapshot};
 use editor::display_map::{BlockPlacement, BlockProperties, BlockStyle};
-use editor::{Addon, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer};
+use editor::{Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer};
 use git::repository::{CommitDetails, CommitDiff, RepoPath};
 use git::{GitHostingProviderRegistry, GitRemote, parse_git_remote_url};
 use gpui::{
@@ -11,9 +11,8 @@ use gpui::{
 };
 use language::{
     Anchor, Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, ReplicaId, Rope,
-    TextBuffer, ToPoint,
+    TextBuffer,
 };
-use multi_buffer::ExcerptInfo;
 use multi_buffer::PathKey;
 use project::{Project, WorktreeId, git_store::Repository};
 use std::{
@@ -22,11 +21,9 @@ use std::{
     sync::Arc,
 };
 use theme::ActiveTheme;
-use ui::{
-    Avatar, Button, ButtonCommon, Clickable, Color, Icon, IconName, IconSize, Label,
-    LabelCommon as _, LabelSize, SharedString, div, h_flex, v_flex,
-};
+use ui::{Avatar, DiffStat, Tooltip, prelude::*};
 use util::{ResultExt, paths::PathStyle, rel_path::RelPath, truncate_and_trailoff};
+use workspace::item::TabTooltipContent;
 use workspace::{
     Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
     Workspace,
@@ -151,11 +148,11 @@ impl CommitView {
         let editor = cx.new(|cx| {
             let mut editor =
                 Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
+
             editor.disable_inline_diagnostics();
+            editor.disable_indent_guides();
             editor.set_expand_all_diff_hunks(cx);
-            editor.register_addon(CommitViewAddon {
-                multibuffer: multibuffer.downgrade(),
-            });
+
             editor
         });
         let commit_sha = Arc::<str>::from(commit.sha.as_ref());
@@ -357,6 +354,41 @@ impl CommitView {
             .into_any()
     }
 
+    fn calculate_changed_lines(&self, cx: &App) -> (u32, u32) {
+        let snapshot = self.multibuffer.read(cx).snapshot(cx);
+        let mut total_additions = 0u32;
+        let mut total_deletions = 0u32;
+
+        let mut seen_buffers = std::collections::HashSet::new();
+        for (_, buffer, _) in snapshot.excerpts() {
+            let buffer_id = buffer.remote_id();
+            if !seen_buffers.insert(buffer_id) {
+                continue;
+            }
+
+            let Some(diff) = snapshot.diff_for_buffer_id(buffer_id) else {
+                continue;
+            };
+
+            let base_text = diff.base_text();
+
+            for hunk in diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer) {
+                let added_rows = hunk.range.end.row.saturating_sub(hunk.range.start.row);
+                total_additions += added_rows;
+
+                let base_start = base_text
+                    .offset_to_point(hunk.diff_base_byte_range.start)
+                    .row;
+                let base_end = base_text.offset_to_point(hunk.diff_base_byte_range.end).row;
+                let deleted_rows = base_end.saturating_sub(base_start);
+
+                total_deletions += deleted_rows;
+            }
+        }
+
+        (total_additions, total_deletions)
+    }
+
     fn render_header(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let commit = &self.commit;
         let author_name = commit.author_name.clone();
@@ -380,46 +412,72 @@ impl CommitView {
             )
         });
 
-        v_flex()
-            .p_4()
-            .pl_0()
-            .gap_4()
+        let (additions, deletions) = self.calculate_changed_lines(cx);
+
+        let commit_diff_stat = if additions > 0 || deletions > 0 {
+            Some(DiffStat::new(
+                "commit-diff-stat",
+                additions as usize,
+                deletions as usize,
+            ))
+        } else {
+            None
+        };
+
+        h_flex()
             .border_b_1()
-            .border_color(cx.theme().colors().border)
+            .border_color(cx.theme().colors().border_variant)
+            .child(
+                h_flex()
+                    .w(self.editor.read(cx).last_gutter_dimensions().full_width())
+                    .justify_center()
+                    .child(self.render_commit_avatar(&commit.sha, rems_from_px(48.), window, cx)),
+            )
             .child(
                 h_flex()
+                    .py_4()
+                    .pl_1()
+                    .pr_4()
+                    .w_full()
                     .items_start()
-                    .child(
-                        h_flex()
-                            .w(self.editor.read(cx).last_gutter_dimensions().full_width())
-                            .justify_center()
-                            .child(self.render_commit_avatar(
-                                &commit.sha,
-                                gpui::rems(3.0),
-                                window,
-                                cx,
-                            )),
-                    )
+                    .justify_between()
+                    .flex_wrap()
                     .child(
                         v_flex()
-                            .gap_1()
                             .child(
                                 h_flex()
-                                    .gap_3()
-                                    .items_baseline()
+                                    .gap_1()
                                     .child(Label::new(author_name).color(Color::Default))
                                     .child(
-                                        Label::new(format!("commit {}", commit.sha))
-                                            .color(Color::Muted),
+                                        Label::new(format!("Commit:{}", commit.sha))
+                                            .color(Color::Muted)
+                                            .size(LabelSize::Small)
+                                            .truncate()
+                                            .buffer_font(cx),
                                     ),
                             )
-                            .child(Label::new(date_string).color(Color::Muted)),
+                            .child(
+                                h_flex()
+                                    .gap_1p5()
+                                    .child(
+                                        Label::new(date_string)
+                                            .color(Color::Muted)
+                                            .size(LabelSize::Small),
+                                    )
+                                    .child(
+                                        Label::new("•")
+                                            .color(Color::Ignored)
+                                            .size(LabelSize::Small),
+                                    )
+                                    .children(commit_diff_stat),
+                            ),
                     )
-                    .child(div().flex_grow())
                     .children(github_url.map(|url| {
                         Button::new("view_on_github", "View on GitHub")
                             .icon(IconName::Github)
-                            .style(ui::ButtonStyle::Subtle)
+                            .icon_color(Color::Muted)
+                            .icon_size(IconSize::Small)
+                            .icon_position(IconPosition::Start)
                             .on_click(move |_, _, cx| cx.open_url(&url))
                     })),
             )
@@ -714,55 +772,6 @@ impl language::File for GitBlob {
 //     }
 // }
 
-struct CommitViewAddon {
-    multibuffer: WeakEntity<MultiBuffer>,
-}
-
-impl Addon for CommitViewAddon {
-    fn render_buffer_header_controls(
-        &self,
-        excerpt: &ExcerptInfo,
-        _window: &Window,
-        cx: &App,
-    ) -> Option<AnyElement> {
-        let multibuffer = self.multibuffer.upgrade()?;
-        let snapshot = multibuffer.read(cx).snapshot(cx);
-        let excerpts = snapshot.excerpts().collect::<Vec<_>>();
-        let current_idx = excerpts.iter().position(|(id, _, _)| *id == excerpt.id)?;
-        let (_, _, current_range) = &excerpts[current_idx];
-
-        let start_row = current_range.context.start.to_point(&excerpt.buffer).row;
-
-        let prev_end_row = if current_idx > 0 {
-            let (_, prev_buffer, prev_range) = &excerpts[current_idx - 1];
-            if prev_buffer.remote_id() == excerpt.buffer_id {
-                prev_range.context.end.to_point(&excerpt.buffer).row
-            } else {
-                0
-            }
-        } else {
-            0
-        };
-
-        let skipped_lines = start_row.saturating_sub(prev_end_row);
-
-        if skipped_lines > 0 {
-            Some(
-                Label::new(format!("{} unchanged lines", skipped_lines))
-                    .color(Color::Muted)
-                    .size(LabelSize::Small)
-                    .into_any_element(),
-            )
-        } else {
-            None
-        }
-    }
-
-    fn to_any(&self) -> &dyn Any {
-        self
-    }
-}
-
 async fn build_buffer(
     mut text: String,
     blob: Arc<dyn File>,
@@ -865,13 +874,28 @@ impl Item for CommitView {
     fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
         let short_sha = self.commit.sha.get(0..7).unwrap_or(&*self.commit.sha);
         let subject = truncate_and_trailoff(self.commit.message.split('\n').next().unwrap(), 20);
-        format!("{short_sha} - {subject}").into()
+        format!("{short_sha} — {subject}").into()
     }
 
-    fn tab_tooltip_text(&self, _: &App) -> Option<ui::SharedString> {
+    fn tab_tooltip_content(&self, _: &App) -> Option<TabTooltipContent> {
         let short_sha = self.commit.sha.get(0..16).unwrap_or(&*self.commit.sha);
         let subject = self.commit.message.split('\n').next().unwrap();
-        Some(format!("{short_sha} - {subject}").into())
+
+        Some(TabTooltipContent::Custom(Box::new(Tooltip::element({
+            let subject = subject.to_string();
+            let short_sha = short_sha.to_string();
+
+            move |_, _| {
+                v_flex()
+                    .child(Label::new(subject.clone()))
+                    .child(
+                        Label::new(short_sha.clone())
+                            .color(Color::Muted)
+                            .size(LabelSize::Small),
+                    )
+                    .into_any_element()
+            }
+        }))))
     }
 
     fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
@@ -988,12 +1012,11 @@ impl Item for CommitView {
 impl Render for CommitView {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let is_stash = self.stash.is_some();
-        div()
+
+        v_flex()
             .key_context(if is_stash { "StashDiff" } else { "CommitDiff" })
-            .bg(cx.theme().colors().editor_background)
-            .flex()
-            .flex_col()
             .size_full()
+            .bg(cx.theme().colors().editor_background)
             .child(self.render_header(window, cx))
             .child(div().flex_grow().child(self.editor.clone()))
     }
@@ -1013,7 +1036,7 @@ impl EventEmitter<ToolbarItemEvent> for CommitViewToolbar {}
 
 impl Render for CommitViewToolbar {
     fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
-        div()
+        div().hidden()
     }
 }