git_ui: Render avatars in git blame gutter (#39168)

Lukas Wirth created

Release Notes:

- Added setting to render avatar in blame gutter

Change summary

assets/settings/default.json                          |   3 
crates/agent_ui/src/ui/burn_mode_tooltip.rs           |   2 
crates/agent_ui/src/ui/context_pill.rs                |   2 
crates/agent_ui/src/ui/unavailable_editing_tooltip.rs |   4 
crates/collab_ui/src/collab_panel.rs                  |   4 
crates/editor/src/editor.rs                           |   4 
crates/editor/src/element.rs                          |  32 
crates/editor/src/git/blame.rs                        |   6 
crates/git/src/hosting_provider.rs                    |   4 
crates/git_ui/src/blame_ui.rs                         | 314 ++++++------
crates/git_ui/src/commit_tooltip.rs                   |  28 
crates/gpui/src/app/context.rs                        |  19 
crates/markdown_preview/src/markdown_renderer.rs      |   4 
crates/onboarding/src/ai_setup_page.rs                |   4 
crates/project/src/project_settings.rs                |  16 
crates/recent_projects/src/recent_projects.rs         |   4 
crates/settings/src/settings_content/project.rs       |  12 
crates/terminal_view/src/terminal_tab_tooltip.rs      |   4 
crates/ui/src/components/tooltip.rs                   |  31 
19 files changed, 267 insertions(+), 230 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -1242,6 +1242,9 @@
       // The minimum column number to show the inline blame information at
       "min_column": 0
     },
+    "blame": {
+      "show_avatar": true
+    },
     // Control which information is shown in the branch picker.
     "branch_picker": {
       "show_author_name": true

crates/agent_ui/src/ui/burn_mode_tooltip.rs 🔗

@@ -48,7 +48,7 @@ impl Render for BurnModeTooltip {
         let keybinding = KeyBinding::for_action(&ToggleBurnMode, window, cx)
             .map(|kb| kb.size(rems_from_px(12.)));
 
-        tooltip_container(window, cx, |this, _, _| {
+        tooltip_container(cx, |this, _| {
             this
                 .child(
                     h_flex()

crates/agent_ui/src/ui/context_pill.rs 🔗

@@ -704,7 +704,7 @@ impl ContextPillHover {
 
 impl Render for ContextPillHover {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        tooltip_container(window, cx, move |this, window, cx| {
+        tooltip_container(cx, move |this, cx| {
             this.occlude()
                 .on_mouse_move(|_, _, cx| cx.stop_propagation())
                 .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())

crates/agent_ui/src/ui/unavailable_editing_tooltip.rs 🔗

@@ -12,8 +12,8 @@ impl UnavailableEditingTooltip {
 }
 
 impl Render for UnavailableEditingTooltip {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        tooltip_container(window, cx, |this, _, _| {
+    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        tooltip_container(cx, |this, _| {
             this.child(Label::new("Unavailable Editing")).child(
                 div().max_w_64().child(
                     Label::new(format!(

crates/collab_ui/src/collab_panel.rs 🔗

@@ -3170,8 +3170,8 @@ struct JoinChannelTooltip {
 }
 
 impl Render for JoinChannelTooltip {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        tooltip_container(window, cx, |container, _, cx| {
+    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        tooltip_container(cx, |container, cx| {
             let participants = self
                 .channel_store
                 .read(cx)

crates/editor/src/editor.rs 🔗

@@ -24448,8 +24448,8 @@ fn all_edits_insertions_or_deletions(
 struct MissingEditPredictionKeybindingTooltip;
 
 impl Render for MissingEditPredictionKeybindingTooltip {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        ui::tooltip_container(window, cx, |container, _, cx| {
+    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        ui::tooltip_container(cx, |container, cx| {
             container
                 .flex_shrink_0()
                 .max_w_80()

crates/editor/src/element.rs 🔗

@@ -2618,7 +2618,7 @@ impl EditorElement {
         let scroll_top = scroll_position.y * line_height;
         let start_x = em_width;
 
-        let mut last_used_color: Option<(PlayerColor, Oid)> = None;
+        let mut last_used_color: Option<(Hsla, Oid)> = None;
         let blame_renderer = cx.global::<GlobalBlameRenderer>().0.clone();
 
         let shaped_lines = blamed_rows
@@ -2635,7 +2635,8 @@ impl EditorElement {
                     self.editor.clone(),
                     workspace.clone(),
                     buffer_id,
-                    blame_renderer.clone(),
+                    &*blame_renderer,
+                    window,
                     cx,
                 )?;
 
@@ -7513,27 +7514,25 @@ fn render_blame_entry(
     blame: &Entity<GitBlame>,
     blame_entry: BlameEntry,
     style: &EditorStyle,
-    last_used_color: &mut Option<(PlayerColor, Oid)>,
+    last_used_color: &mut Option<(Hsla, Oid)>,
     editor: Entity<Editor>,
     workspace: Entity<Workspace>,
     buffer: BufferId,
-    renderer: Arc<dyn BlameRenderer>,
+    renderer: &dyn BlameRenderer,
+    window: &mut Window,
     cx: &mut App,
 ) -> Option<AnyElement> {
-    let mut sha_color = cx
-        .theme()
-        .players()
-        .color_for_participant(blame_entry.sha.into());
+    let index: u32 = blame_entry.sha.into();
+    let mut sha_color = cx.theme().players().color_for_participant(index).cursor;
 
     // If the last color we used is the same as the one we get for this line, but
     // the commit SHAs are different, then we try again to get a different color.
-    match *last_used_color {
-        Some((color, sha)) if sha != blame_entry.sha && color.cursor == sha_color.cursor => {
-            let index: u32 = blame_entry.sha.into();
-            sha_color = cx.theme().players().color_for_participant(index + 1);
-        }
-        _ => {}
-    };
+    if let Some((color, sha)) = *last_used_color
+        && sha != blame_entry.sha
+        && color == sha_color
+    {
+        sha_color = cx.theme().players().color_for_participant(index + 1).cursor;
+    }
     last_used_color.replace((sha_color, blame_entry.sha));
 
     let blame = blame.read(cx);
@@ -7547,7 +7546,8 @@ fn render_blame_entry(
         workspace.downgrade(),
         editor,
         ix,
-        sha_color.cursor,
+        sha_color,
+        window,
         cx,
     )
 }

crates/editor/src/git/blame.rs 🔗

@@ -95,6 +95,7 @@ pub trait BlameRenderer {
         _: Entity<Editor>,
         _: usize,
         _: Hsla,
+        window: &mut Window,
         _: &mut App,
     ) -> Option<AnyElement>;
 
@@ -142,6 +143,7 @@ impl BlameRenderer for () {
         _: Entity<Editor>,
         _: usize,
         _: Hsla,
+        _: &mut Window,
         _: &mut App,
     ) -> Option<AnyElement> {
         None
@@ -673,8 +675,8 @@ async fn parse_commit_messages(
             .as_ref()
             .map(|(provider, remote)| GitRemote {
                 host: provider.clone(),
-                owner: remote.owner.to_string(),
-                repo: remote.repo.to_string(),
+                owner: remote.owner.clone().into(),
+                repo: remote.repo.clone().into(),
             });
 
         let pull_request = parsed_remote_url

crates/git/src/hosting_provider.rs 🔗

@@ -17,8 +17,8 @@ pub struct PullRequest {
 #[derive(Clone)]
 pub struct GitRemote {
     pub host: Arc<dyn GitHostingProvider + Send + Sync + 'static>,
-    pub owner: String,
-    pub repo: String,
+    pub owner: SharedString,
+    pub repo: SharedString,
 }
 
 impl std::fmt::Debug for GitRemote {

crates/git_ui/src/blame_ui.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{
-    commit_tooltip::{CommitAvatar, CommitDetails, CommitTooltip},
+    commit_tooltip::{CommitAvatar, CommitTooltip},
     commit_view::CommitView,
 };
 use editor::{BlameRenderer, Editor, hover_markdown_style};
@@ -17,7 +17,7 @@ use settings::Settings as _;
 use theme::ThemeSettings;
 use time::OffsetDateTime;
 use time_format::format_local_timestamp;
-use ui::{ContextMenu, Divider, IconButtonShape, prelude::*};
+use ui::{ContextMenu, Divider, IconButtonShape, prelude::*, tooltip_container};
 use workspace::Workspace;
 
 const GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED: usize = 20;
@@ -39,6 +39,7 @@ impl BlameRenderer for GitBlameRenderer {
         editor: Entity<Editor>,
         ix: usize,
         sha_color: Hsla,
+        window: &mut Window,
         cx: &mut App,
     ) -> Option<AnyElement> {
         let relative_timestamp = blame_entry_relative_timestamp(&blame_entry);
@@ -46,6 +47,19 @@ impl BlameRenderer for GitBlameRenderer {
         let author_name = blame_entry.author.as_deref().unwrap_or("<no name>");
         let name = util::truncate_and_trailoff(author_name, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED);
 
+        let avatar = if ProjectSettings::get_global(cx)
+            .git
+            .inline_blame
+            .show_commit_summary
+        {
+            None
+        } else {
+            CommitAvatar::new(
+                &blame_entry.sha.to_string().into(),
+                details.as_ref().and_then(|it| it.remote.as_ref()),
+            )
+            .render(window, cx)
+        };
         Some(
             div()
                 .mr_2()
@@ -63,6 +77,7 @@ impl BlameRenderer for GitBlameRenderer {
                                 .items_center()
                                 .gap_2()
                                 .child(div().text_color(sha_color).child(short_commit_id))
+                                .children(avatar)
                                 .child(name),
                         )
                         .child(relative_timestamp)
@@ -179,31 +194,22 @@ impl BlameRenderer for GitBlameRenderer {
             .and_then(|t| OffsetDateTime::from_unix_timestamp(t).ok())
             .unwrap_or(OffsetDateTime::now_utc());
 
-        let commit_details = CommitDetails {
-            sha: blame.sha.to_string().into(),
-            commit_time,
-            author_name: blame
-                .author
-                .clone()
-                .unwrap_or("<no name>".to_string())
-                .into(),
-            author_email: blame.author_mail.unwrap_or("".to_string()).into(),
-            message: details,
-        };
-
-        let avatar = CommitAvatar::new(&commit_details).render(window, cx);
-
-        let author = commit_details.author_name.clone();
-        let author_email = commit_details.author_email.clone();
+        let sha = blame.sha.to_string().into();
+        let author: SharedString = blame
+            .author
+            .clone()
+            .unwrap_or("<no name>".to_string())
+            .into();
+        let author_email = blame.author_mail.as_deref().unwrap_or_default();
+        let avatar = CommitAvatar::new(&sha, details.as_ref().and_then(|it| it.remote.as_ref()))
+            .render(window, cx);
 
-        let short_commit_id = commit_details
-            .sha
-            .get(0..8)
+        let short_commit_id = sha
+            .get(..8)
             .map(|sha| sha.to_string().into())
-            .unwrap_or_else(|| commit_details.sha.clone());
-        let full_sha = commit_details.sha.to_string();
+            .unwrap_or_else(|| sha.clone());
         let absolute_timestamp = format_local_timestamp(
-            commit_details.commit_time,
+            commit_time,
             OffsetDateTime::now_utc(),
             time_format::TimestampFormat::MediumAbsolute,
         );
@@ -215,165 +221,143 @@ impl BlameRenderer for GitBlameRenderer {
             style
         };
 
-        let message = commit_details
-            .message
+        let message = details
             .as_ref()
             .map(|_| MarkdownElement::new(markdown.clone(), markdown_style).into_any())
             .unwrap_or("<no commit message>".into_any());
 
-        let pull_request = commit_details
-            .message
+        let pull_request = details
             .as_ref()
             .and_then(|details| details.pull_request.clone());
 
         let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
         let message_max_height = window.line_height() * 12 + (ui_font_size / 0.4);
         let commit_summary = CommitSummary {
-            sha: commit_details.sha.clone(),
-            subject: commit_details
-                .message
+            sha: sha.clone(),
+            subject: details
                 .as_ref()
-                .map_or(Default::default(), |message| {
-                    message
-                        .message
-                        .split('\n')
-                        .next()
-                        .unwrap()
-                        .trim_end()
-                        .to_string()
-                        .into()
-                }),
-            commit_timestamp: commit_details.commit_time.unix_timestamp(),
-            author_name: commit_details.author_name.clone(),
+                .and_then(|details| {
+                    Some(
+                        details
+                            .message
+                            .split('\n')
+                            .next()?
+                            .trim_end()
+                            .to_string()
+                            .into(),
+                    )
+                })
+                .unwrap_or_default(),
+            commit_timestamp: commit_time.unix_timestamp(),
+            author_name: author.clone(),
             has_parent: false,
         };
 
-        let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
-
-        // padding to avoid tooltip appearing right below the mouse cursor
-        // TODO: use tooltip_container here
         Some(
-            div()
-                .pl_2()
-                .pt_2p5()
-                .child(
-                    v_flex()
-                        .elevation_2(cx)
-                        .font(ui_font)
-                        .text_ui(cx)
-                        .text_color(cx.theme().colors().text)
-                        .py_1()
-                        .px_2()
-                        .map(|el| {
-                            el.occlude()
-                                .on_mouse_move(|_, _, cx| cx.stop_propagation())
-                                .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
-                                .child(
-                                    v_flex()
-                                        .w(gpui::rems(30.))
-                                        .gap_4()
-                                        .child(
-                                            h_flex()
-                                                .pb_1p5()
-                                                .gap_x_2()
-                                                .overflow_x_hidden()
-                                                .flex_wrap()
-                                                .children(avatar)
-                                                .child(author)
-                                                .when(!author_email.is_empty(), |this| {
-                                                    this.child(
-                                                        div()
-                                                            .text_color(
-                                                                cx.theme().colors().text_muted,
-                                                            )
-                                                            .child(author_email),
-                                                    )
-                                                })
-                                                .border_b_1()
-                                                .border_color(cx.theme().colors().border_variant),
-                                        )
-                                        .child(
+            tooltip_container(cx, |d, cx| {
+                d.occlude()
+                    .on_mouse_move(|_, _, cx| cx.stop_propagation())
+                    .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
+                    .child(
+                        v_flex()
+                            .w(gpui::rems(30.))
+                            .gap_4()
+                            .child(
+                                h_flex()
+                                    .pb_1p5()
+                                    .gap_x_2()
+                                    .overflow_x_hidden()
+                                    .flex_wrap()
+                                    .children(avatar)
+                                    .child(author)
+                                    .when(!author_email.is_empty(), |this| {
+                                        this.child(
                                             div()
-                                                .id("inline-blame-commit-message")
-                                                .child(message)
-                                                .max_h(message_max_height)
-                                                .overflow_y_scroll()
-                                                .track_scroll(&scroll_handle),
-                                        )
-                                        .child(
-                                            h_flex()
                                                 .text_color(cx.theme().colors().text_muted)
-                                                .w_full()
-                                                .justify_between()
-                                                .pt_1p5()
-                                                .border_t_1()
-                                                .border_color(cx.theme().colors().border_variant)
-                                                .child(absolute_timestamp)
-                                                .child(
-                                                    h_flex()
-                                                        .gap_1p5()
-                                                        .when_some(pull_request, |this, pr| {
-                                                            this.child(
-                                                                Button::new(
-                                                                    "pull-request-button",
-                                                                    format!("#{}", pr.number),
-                                                                )
-                                                                .color(Color::Muted)
-                                                                .icon(IconName::PullRequest)
-                                                                .icon_color(Color::Muted)
-                                                                .icon_position(IconPosition::Start)
-                                                                .style(ButtonStyle::Subtle)
-                                                                .on_click(move |_, _, cx| {
-                                                                    cx.stop_propagation();
-                                                                    cx.open_url(pr.url.as_str())
-                                                                }),
-                                                            )
-                                                        })
-                                                        .child(Divider::vertical())
-                                                        .child(
-                                                            Button::new(
-                                                                "commit-sha-button",
-                                                                short_commit_id.clone(),
-                                                            )
-                                                            .style(ButtonStyle::Subtle)
-                                                            .color(Color::Muted)
-                                                            .icon(IconName::FileGit)
-                                                            .icon_color(Color::Muted)
-                                                            .icon_position(IconPosition::Start)
-                                                            .on_click(move |_, window, cx| {
-                                                                CommitView::open(
-                                                                    commit_summary.clone(),
-                                                                    repository.downgrade(),
-                                                                    workspace.clone(),
-                                                                    window,
-                                                                    cx,
-                                                                );
-                                                                cx.stop_propagation();
-                                                            }),
+                                                .child(author_email.to_owned()),
+                                        )
+                                    })
+                                    .border_b_1()
+                                    .border_color(cx.theme().colors().border_variant),
+                            )
+                            .child(
+                                div()
+                                    .id("inline-blame-commit-message")
+                                    .child(message)
+                                    .max_h(message_max_height)
+                                    .overflow_y_scroll()
+                                    .track_scroll(&scroll_handle),
+                            )
+                            .child(
+                                h_flex()
+                                    .text_color(cx.theme().colors().text_muted)
+                                    .w_full()
+                                    .justify_between()
+                                    .pt_1p5()
+                                    .border_t_1()
+                                    .border_color(cx.theme().colors().border_variant)
+                                    .child(absolute_timestamp)
+                                    .child(
+                                        h_flex()
+                                            .gap_1p5()
+                                            .when_some(pull_request, |this, pr| {
+                                                this.child(
+                                                    Button::new(
+                                                        "pull-request-button",
+                                                        format!("#{}", pr.number),
+                                                    )
+                                                    .color(Color::Muted)
+                                                    .icon(IconName::PullRequest)
+                                                    .icon_color(Color::Muted)
+                                                    .icon_position(IconPosition::Start)
+                                                    .style(ButtonStyle::Subtle)
+                                                    .on_click(move |_, _, cx| {
+                                                        cx.stop_propagation();
+                                                        cx.open_url(pr.url.as_str())
+                                                    }),
+                                                )
+                                            })
+                                            .child(Divider::vertical())
+                                            .child(
+                                                Button::new(
+                                                    "commit-sha-button",
+                                                    short_commit_id.clone(),
+                                                )
+                                                .style(ButtonStyle::Subtle)
+                                                .color(Color::Muted)
+                                                .icon(IconName::FileGit)
+                                                .icon_color(Color::Muted)
+                                                .icon_position(IconPosition::Start)
+                                                .on_click(move |_, window, cx| {
+                                                    CommitView::open(
+                                                        commit_summary.clone(),
+                                                        repository.downgrade(),
+                                                        workspace.clone(),
+                                                        window,
+                                                        cx,
+                                                    );
+                                                    cx.stop_propagation();
+                                                }),
+                                            )
+                                            .child(
+                                                IconButton::new("copy-sha-button", IconName::Copy)
+                                                    .shape(IconButtonShape::Square)
+                                                    .icon_size(IconSize::Small)
+                                                    .icon_color(Color::Muted)
+                                                    .on_click(move |_, _, cx| {
+                                                        cx.stop_propagation();
+                                                        cx.write_to_clipboard(
+                                                            ClipboardItem::new_string(
+                                                                sha.to_string(),
+                                                            ),
                                                         )
-                                                        .child(
-                                                            IconButton::new(
-                                                                "copy-sha-button",
-                                                                IconName::Copy,
-                                                            )
-                                                            .shape(IconButtonShape::Square)
-                                                            .icon_size(IconSize::Small)
-                                                            .icon_color(Color::Muted)
-                                                            .on_click(move |_, _, cx| {
-                                                                cx.stop_propagation();
-                                                                cx.write_to_clipboard(
-                                                                    ClipboardItem::new_string(
-                                                                        full_sha.clone(),
-                                                                    ),
-                                                                )
-                                                            }),
-                                                        ),
-                                                ),
-                                        ),
-                                )
-                        }),
-                )
-                .into_any_element(),
+                                                    }),
+                                            ),
+                                    ),
+                            ),
+                    )
+            })
+            .into_any_element(),
         )
     }
 

crates/git_ui/src/commit_tooltip.rs 🔗

@@ -28,25 +28,33 @@ pub struct CommitDetails {
 }
 
 pub struct CommitAvatar<'a> {
-    commit: &'a CommitDetails,
+    sha: &'a SharedString,
+    remote: Option<&'a GitRemote>,
 }
 
 impl<'a> CommitAvatar<'a> {
-    pub fn new(details: &'a CommitDetails) -> Self {
-        Self { commit: details }
+    pub fn new(sha: &'a SharedString, remote: Option<&'a GitRemote>) -> Self {
+        Self { sha, remote }
+    }
+
+    pub fn from_commit_details(details: &'a CommitDetails) -> Self {
+        Self {
+            sha: &details.sha,
+            remote: details
+                .message
+                .as_ref()
+                .and_then(|details| details.remote.as_ref()),
+        }
     }
 }
 
 impl<'a> CommitAvatar<'a> {
     pub fn render(&'a self, window: &mut Window, cx: &mut App) -> Option<impl IntoElement + use<>> {
         let remote = self
-            .commit
-            .message
-            .as_ref()
-            .and_then(|details| details.remote.clone())
+            .remote
             .filter(|remote| remote.host_supports_avatars())?;
 
-        let avatar_url = CommitAvatarAsset::new(remote, self.commit.sha.clone());
+        let avatar_url = CommitAvatarAsset::new(remote.clone(), self.sha.clone());
 
         let element = match window.use_asset::<CommitAvatarAsset>(&avatar_url, cx) {
             // Loading or no avatar found
@@ -169,7 +177,7 @@ impl CommitTooltip {
 
 impl Render for CommitTooltip {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let avatar = CommitAvatar::new(&self.commit).render(window, cx);
+        let avatar = CommitAvatar::from_commit_details(&self.commit).render(window, cx);
 
         let author = self.commit.author_name.clone();
 
@@ -233,7 +241,7 @@ impl Render for CommitTooltip {
             has_parent: false,
         };
 
-        tooltip_container(window, cx, move |this, _, cx| {
+        tooltip_container(cx, move |this, cx| {
             this.occlude()
                 .on_mouse_move(|_, _, cx| cx.stop_propagation())
                 .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())

crates/gpui/src/app/context.rs 🔗

@@ -4,12 +4,12 @@ use crate::{
     Subscription, Task, WeakEntity, WeakFocusHandle, Window, WindowHandle,
 };
 use anyhow::Result;
-use derive_more::{Deref, DerefMut};
 use futures::FutureExt;
 use std::{
     any::{Any, TypeId},
     borrow::{Borrow, BorrowMut},
     future::Future,
+    ops,
     sync::Arc,
 };
 use util::Deferred;
@@ -17,14 +17,25 @@ use util::Deferred;
 use super::{App, AsyncWindowContext, Entity, KeystrokeEvent};
 
 /// The app context, with specialized behavior for the given entity.
-#[derive(Deref, DerefMut)]
 pub struct Context<'a, T> {
-    #[deref]
-    #[deref_mut]
     app: &'a mut App,
     entity_state: WeakEntity<T>,
 }
 
+impl<'a, T> ops::Deref for Context<'a, T> {
+    type Target = App;
+
+    fn deref(&self) -> &Self::Target {
+        self.app
+    }
+}
+
+impl<'a, T> ops::DerefMut for Context<'a, T> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        self.app
+    }
+}
+
 impl<'a, T: 'static> Context<'a, T> {
     pub(crate) fn new_context(app: &'a mut App, entity_state: WeakEntity<T>) -> Self {
         Self { app, entity_state }

crates/markdown_preview/src/markdown_renderer.rs 🔗

@@ -839,8 +839,8 @@ impl InteractiveMarkdownElementTooltip {
 }
 
 impl Render for InteractiveMarkdownElementTooltip {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        tooltip_container(window, cx, |el, _, _| {
+    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        tooltip_container(cx, |el, _| {
             let secondary_modifier = Keystroke {
                 modifiers: Modifiers::secondary_key(),
                 ..Default::default()

crates/onboarding/src/ai_setup_page.rs 🔗

@@ -401,10 +401,10 @@ impl AiPrivacyTooltip {
 }
 
 impl Render for AiPrivacyTooltip {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         const DESCRIPTION: &str = "We believe in opt-in data sharing as the default for building AI products, rather than opt-out. We'll only use or store your data if you affirmatively send it to us. ";
 
-        tooltip_container(window, cx, move |this, _, _| {
+        tooltip_container(cx, move |this, _| {
             this.child(
                 h_flex()
                     .gap_1()

crates/project/src/project_settings.rs 🔗

@@ -307,6 +307,8 @@ pub struct GitSettings {
     ///
     /// Default: on
     pub inline_blame: InlineBlameSettings,
+    /// Git blame settings.
+    pub blame: BlameSettings,
     /// Which information to show in the branch picker.
     ///
     /// Default: on
@@ -344,6 +346,14 @@ pub struct InlineBlameSettings {
     pub show_commit_summary: bool,
 }
 
+#[derive(Clone, Copy, Debug)]
+pub struct BlameSettings {
+    /// Whether to show the avatar of the author of the commit.
+    ///
+    /// Default: true
+    pub show_avatar: bool,
+}
+
 impl GitSettings {
     pub fn inline_blame_delay(&self) -> Option<Duration> {
         if self.inline_blame.delay_ms.as_millis() > 0 {
@@ -447,6 +457,12 @@ impl Settings for ProjectSettings {
                     show_commit_summary: inline.show_commit_summary.unwrap(),
                 }
             },
+            blame: {
+                let blame = git.blame.unwrap();
+                BlameSettings {
+                    show_avatar: blame.show_avatar.unwrap(),
+                }
+            },
             branch_picker: {
                 let branch_picker = git.branch_picker.unwrap();
                 BranchPickerSettings {

crates/recent_projects/src/recent_projects.rs 🔗

@@ -688,8 +688,8 @@ struct MatchTooltip {
 }
 
 impl Render for MatchTooltip {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        tooltip_container(window, cx, |div, _, _| {
+    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        tooltip_container(cx, |div, _| {
             self.highlighted_location.render_paths_children(div)
         })
     }

crates/settings/src/settings_content/project.rs 🔗

@@ -256,6 +256,8 @@ pub struct GitSettings {
     ///
     /// Default: on
     pub inline_blame: Option<InlineBlameSettings>,
+    /// Git blame settings.
+    pub blame: Option<BlameSettings>,
     /// Which information to show in the branch picker.
     ///
     /// Default: on
@@ -305,6 +307,16 @@ pub struct InlineBlameSettings {
     pub show_commit_summary: Option<bool>,
 }
 
+#[skip_serializing_none]
+#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
+#[serde(rename_all = "snake_case")]
+pub struct BlameSettings {
+    /// Whether to show the avatar of the author of the commit.
+    ///
+    /// Default: true
+    pub show_avatar: Option<bool>,
+}
+
 #[skip_serializing_none]
 #[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]

crates/terminal_view/src/terminal_tab_tooltip.rs 🔗

@@ -16,8 +16,8 @@ impl TerminalTooltip {
 }
 
 impl Render for TerminalTooltip {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        tooltip_container(window, cx, move |this, _window, _cx| {
+    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        tooltip_container(cx, move |this, _cx| {
             this.occlude()
                 .on_mouse_move(|_, _window, cx| cx.stop_propagation())
                 .child(

crates/ui/src/components/tooltip.rs 🔗

@@ -1,6 +1,7 @@
+use std::borrow::Borrow;
 use std::rc::Rc;
 
-use gpui::{Action, AnyElement, AnyView, AppContext as _, FocusHandle, IntoElement, Render};
+use gpui::{Action, AnyElement, AnyView, AppContext, FocusHandle, IntoElement, Render};
 use settings::Settings;
 use theme::ThemeSettings;
 
@@ -195,8 +196,8 @@ impl Tooltip {
 }
 
 impl Render for Tooltip {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        tooltip_container(window, cx, |el, _, _| {
+    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        tooltip_container(cx, |el, _| {
             el.child(
                 h_flex()
                     .gap_4()
@@ -216,23 +217,23 @@ impl Render for Tooltip {
     }
 }
 
-pub fn tooltip_container<V, ContentsBuilder: FnOnce(Div, &mut Window, &mut Context<V>) -> Div>(
-    window: &mut Window,
-    cx: &mut Context<V>,
-    f: ContentsBuilder,
-) -> impl IntoElement + use<V, ContentsBuilder> {
-    let ui_font = ThemeSettings::get_global(cx).ui_font.clone();
+pub fn tooltip_container<C>(cx: &mut C, f: impl FnOnce(Div, &mut C) -> Div) -> impl IntoElement
+where
+    C: AppContext + Borrow<App>,
+{
+    let app = (*cx).borrow();
+    let ui_font = ThemeSettings::get_global(app).ui_font.clone();
 
     // padding to avoid tooltip appearing right below the mouse cursor
     div().pl_2().pt_2p5().child(
         v_flex()
-            .elevation_2(cx)
+            .elevation_2(app)
             .font(ui_font)
-            .text_ui(cx)
-            .text_color(cx.theme().colors().text)
+            .text_ui(app)
+            .text_color(app.theme().colors().text)
             .py_1()
             .px_2()
-            .map(|el| f(el, window, cx)),
+            .map(|el| f(el, cx)),
     )
 }
 
@@ -261,8 +262,8 @@ impl LinkPreview {
 }
 
 impl Render for LinkPreview {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        tooltip_container(window, cx, |el, _, _| {
+    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        tooltip_container(cx, |el, _| {
             el.child(
                 Label::new(self.link.clone())
                     .size(LabelSize::XSmall)