diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 47adc19d43..b09adde8c0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -78,18 +78,19 @@ use futures::{ }; use fuzzy::StringMatchCandidate; -use ::git::Restore; +use ::git::blame::BlameEntry; +use ::git::{Restore, blame::ParsedCommitMessage}; use code_context_menus::{ AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu, CompletionsMenu, ContextMenuOrigin, }; use git::blame::{GitBlame, GlobalBlameRenderer}; use gpui::{ - Action, Animation, AnimationExt, AnyElement, AnyWeakEntity, App, AppContext, - AsyncWindowContext, AvailableSpace, Background, Bounds, ClickEvent, ClipboardEntry, - ClipboardItem, Context, DispatchPhase, Edges, Entity, EntityInputHandler, EventEmitter, - FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global, HighlightStyle, Hsla, - KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad, ParentElement, Pixels, Render, + Action, Animation, AnimationExt, AnyElement, App, AppContext, AsyncWindowContext, + AvailableSpace, Background, Bounds, ClickEvent, ClipboardEntry, ClipboardItem, Context, + DispatchPhase, Edges, Entity, EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent, + Focusable, FontId, FontWeight, Global, HighlightStyle, Hsla, KeyContext, Modifiers, + MouseButton, MouseDownEvent, PaintQuad, ParentElement, Pixels, Render, ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task, TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window, div, impl_actions, point, prelude::*, pulsating_between, px, relative, size, @@ -117,6 +118,7 @@ use language::{ }; use language::{BufferRow, CharClassifier, Runnable, RunnableRange, point_to_lsp}; use linked_editing_ranges::refresh_linked_ranges; +use markdown::Markdown; use mouse_context_menu::MouseContextMenu; use persistence::DB; use project::{ @@ -798,6 +800,21 @@ impl ChangeList { } } +#[derive(Clone)] +struct InlineBlamePopoverState { + scroll_handle: ScrollHandle, + commit_message: Option, + markdown: Entity, +} + +struct InlineBlamePopover { + position: gpui::Point, + show_task: Option>, + hide_task: Option>, + popover_bounds: Option>, + popover_state: InlineBlamePopoverState, +} + /// Zed's primary implementation of text input, allowing users to edit a [`MultiBuffer`]. /// /// See the [module level documentation](self) for more information. @@ -866,6 +883,7 @@ pub struct Editor { context_menu_options: Option, mouse_context_menu: Option, completion_tasks: Vec<(CompletionId, Task>)>, + inline_blame_popover: Option, signature_help_state: SignatureHelpState, auto_signature_help: Option, find_all_references_task_sources: Vec, @@ -922,7 +940,6 @@ pub struct Editor { show_git_blame_gutter: bool, show_git_blame_inline: bool, show_git_blame_inline_delay_task: Option>, - pub git_blame_inline_tooltip: Option, git_blame_inline_enabled: bool, render_diff_hunk_controls: RenderDiffHunkControlsFn, serialize_dirty_buffers: bool, @@ -1665,6 +1682,7 @@ impl Editor { context_menu_options: None, mouse_context_menu: None, completion_tasks: Default::default(), + inline_blame_popover: Default::default(), signature_help_state: SignatureHelpState::default(), auto_signature_help: None, find_all_references_task_sources: Vec::new(), @@ -1730,7 +1748,6 @@ impl Editor { show_git_blame_inline: false, show_selection_menu: None, show_git_blame_inline_delay_task: None, - git_blame_inline_tooltip: None, git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(), render_diff_hunk_controls: Arc::new(render_diff_hunk_controls), serialize_dirty_buffers: ProjectSettings::get_global(cx) @@ -1806,6 +1823,7 @@ impl Editor { ); }); editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape); + editor.inline_blame_popover.take(); } } EditorEvent::Edited { .. } => { @@ -2603,6 +2621,7 @@ impl Editor { self.update_visible_inline_completion(window, cx); self.edit_prediction_requires_modifier_in_indent_conflict = true; linked_editing_ranges::refresh_linked_ranges(self, window, cx); + self.inline_blame_popover.take(); if self.git_blame_inline_enabled { self.start_inline_blame_timer(window, cx); } @@ -5483,6 +5502,82 @@ impl Editor { } } + fn show_blame_popover( + &mut self, + blame_entry: &BlameEntry, + position: gpui::Point, + cx: &mut Context, + ) { + if let Some(state) = &mut self.inline_blame_popover { + state.hide_task.take(); + cx.notify(); + } else { + let delay = EditorSettings::get_global(cx).hover_popover_delay; + let show_task = cx.spawn(async move |editor, cx| { + cx.background_executor() + .timer(std::time::Duration::from_millis(delay)) + .await; + editor + .update(cx, |editor, cx| { + if let Some(state) = &mut editor.inline_blame_popover { + state.show_task = None; + cx.notify(); + } + }) + .ok(); + }); + let Some(blame) = self.blame.as_ref() else { + return; + }; + let blame = blame.read(cx); + let details = blame.details_for_entry(&blame_entry); + let markdown = cx.new(|cx| { + Markdown::new( + details + .as_ref() + .map(|message| message.message.clone()) + .unwrap_or_default(), + None, + None, + cx, + ) + }); + self.inline_blame_popover = Some(InlineBlamePopover { + position, + show_task: Some(show_task), + hide_task: None, + popover_bounds: None, + popover_state: InlineBlamePopoverState { + scroll_handle: ScrollHandle::new(), + commit_message: details, + markdown, + }, + }); + } + } + + fn hide_blame_popover(&mut self, cx: &mut Context) { + if let Some(state) = &mut self.inline_blame_popover { + if state.show_task.is_some() { + self.inline_blame_popover.take(); + cx.notify(); + } else { + let hide_task = cx.spawn(async move |editor, cx| { + cx.background_executor() + .timer(std::time::Duration::from_millis(100)) + .await; + editor + .update(cx, |editor, cx| { + editor.inline_blame_popover.take(); + cx.notify(); + }) + .ok(); + }); + state.hide_task = Some(hide_task); + } + } + } + fn refresh_document_highlights(&mut self, cx: &mut Context) -> Option<()> { if self.pending_rename.is_some() { return None; @@ -16657,12 +16752,7 @@ impl Editor { pub fn render_git_blame_inline(&self, window: &Window, cx: &App) -> bool { self.show_git_blame_inline - && (self.focus_handle.is_focused(window) - || self - .git_blame_inline_tooltip - .as_ref() - .and_then(|t| t.upgrade()) - .is_some()) + && (self.focus_handle.is_focused(window) || self.inline_blame_popover.is_some()) && !self.newest_selection_head_on_empty_line(cx) && self.has_blame_entries(cx) } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index b3adfebd78..abb7a48dce 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -32,15 +32,19 @@ use client::ParticipantIndex; use collections::{BTreeMap, HashMap}; use feature_flags::{Debugger, FeatureFlagAppExt}; use file_icons::FileIcons; -use git::{Oid, blame::BlameEntry, status::FileStatus}; +use git::{ + Oid, + blame::{BlameEntry, ParsedCommitMessage}, + status::FileStatus, +}; use gpui::{ - Action, Along, AnyElement, App, AvailableSpace, Axis as ScrollbarAxis, BorderStyle, Bounds, - ClickEvent, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase, Edges, Element, - ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox, Hsla, + Action, Along, AnyElement, App, AppContext, AvailableSpace, Axis as ScrollbarAxis, BorderStyle, + Bounds, ClickEvent, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase, Edges, + Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, - ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, - TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, + ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, + Style, Styled, TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background, transparent_black, }; @@ -49,6 +53,7 @@ use language::language_settings::{ IndentGuideBackgroundColoring, IndentGuideColoring, IndentGuideSettings, ShowWhitespaceSetting, }; use lsp::DiagnosticSeverity; +use markdown::Markdown; use multi_buffer::{ Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, ExpandInfo, MultiBufferPoint, MultiBufferRow, RowInfo, @@ -1749,6 +1754,7 @@ impl EditorElement { content_origin: gpui::Point, scroll_pixel_position: gpui::Point, line_height: Pixels, + text_hitbox: &Hitbox, window: &mut Window, cx: &mut App, ) -> Option { @@ -1780,21 +1786,13 @@ impl EditorElement { padding * em_width }; - let workspace = editor.workspace()?.downgrade(); let blame_entry = blame .update(cx, |blame, cx| { blame.blame_for_rows(&[*row_info], cx).next() }) .flatten()?; - let mut element = render_inline_blame_entry( - self.editor.clone(), - workspace, - &blame, - blame_entry, - &self.style, - cx, - )?; + let mut element = render_inline_blame_entry(blame_entry.clone(), &self.style, cx)?; let start_y = content_origin.y + line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height); @@ -1820,11 +1818,122 @@ impl EditorElement { }; let absolute_offset = point(start_x, start_y); + let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); + let bounds = Bounds::new(absolute_offset, size); + + self.layout_blame_entry_popover( + bounds, + blame_entry, + blame, + line_height, + text_hitbox, + window, + cx, + ); + element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), window, cx); Some(element) } + fn layout_blame_entry_popover( + &self, + parent_bounds: Bounds, + blame_entry: BlameEntry, + blame: Entity, + line_height: Pixels, + text_hitbox: &Hitbox, + window: &mut Window, + cx: &mut App, + ) { + let mouse_position = window.mouse_position(); + let mouse_over_inline_blame = parent_bounds.contains(&mouse_position); + let mouse_over_popover = self.editor.update(cx, |editor, _| { + editor + .inline_blame_popover + .as_ref() + .and_then(|state| state.popover_bounds) + .map_or(false, |bounds| bounds.contains(&mouse_position)) + }); + + self.editor.update(cx, |editor, cx| { + if mouse_over_inline_blame || mouse_over_popover { + editor.show_blame_popover(&blame_entry, mouse_position, cx); + } else { + editor.hide_blame_popover(cx); + } + }); + + let should_draw = self.editor.update(cx, |editor, _| { + editor + .inline_blame_popover + .as_ref() + .map_or(false, |state| state.show_task.is_none()) + }); + + if should_draw { + let maybe_element = self.editor.update(cx, |editor, cx| { + editor + .workspace() + .map(|workspace| workspace.downgrade()) + .zip( + editor + .inline_blame_popover + .as_ref() + .map(|p| p.popover_state.clone()), + ) + .and_then(|(workspace, popover_state)| { + render_blame_entry_popover( + blame_entry, + popover_state.scroll_handle, + popover_state.commit_message, + popover_state.markdown, + workspace, + &blame, + window, + cx, + ) + }) + }); + + if let Some(mut element) = maybe_element { + let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); + let origin = self.editor.update(cx, |editor, _| { + let target_point = editor + .inline_blame_popover + .as_ref() + .map_or(mouse_position, |state| state.position); + + let overall_height = size.height + HOVER_POPOVER_GAP; + let popover_origin = if target_point.y > overall_height { + point(target_point.x, target_point.y - size.height) + } else { + point( + target_point.x, + target_point.y + line_height + HOVER_POPOVER_GAP, + ) + }; + + let horizontal_offset = (text_hitbox.top_right().x + - POPOVER_RIGHT_OFFSET + - (popover_origin.x + size.width)) + .min(Pixels::ZERO); + + point(popover_origin.x + horizontal_offset, popover_origin.y) + }); + + let popover_bounds = Bounds::new(origin, size); + self.editor.update(cx, |editor, _| { + if let Some(state) = &mut editor.inline_blame_popover { + state.popover_bounds = Some(popover_bounds); + } + }); + + window.defer_draw(element, origin, 2); + } + } + } + fn layout_blame_entries( &self, buffer_rows: &[RowInfo], @@ -5851,24 +5960,35 @@ fn prepaint_gutter_button( } fn render_inline_blame_entry( - editor: Entity, - workspace: WeakEntity, - blame: &Entity, blame_entry: BlameEntry, style: &EditorStyle, cx: &mut App, +) -> Option { + let renderer = cx.global::().0.clone(); + renderer.render_inline_blame_entry(&style.text, blame_entry, cx) +} + +fn render_blame_entry_popover( + blame_entry: BlameEntry, + scroll_handle: ScrollHandle, + commit_message: Option, + markdown: Entity, + workspace: WeakEntity, + blame: &Entity, + window: &mut Window, + cx: &mut App, ) -> Option { let renderer = cx.global::().0.clone(); let blame = blame.read(cx); - let details = blame.details_for_entry(&blame_entry); let repository = blame.repository(cx)?.clone(); - renderer.render_inline_blame_entry( - &style.text, + renderer.render_blame_entry_popover( blame_entry, - details, + scroll_handle, + commit_message, + markdown, repository, workspace, - editor, + window, cx, ) } @@ -7046,14 +7166,7 @@ impl Element for EditorElement { blame.blame_for_rows(&[row_infos], cx).next() }) .flatten()?; - let mut element = render_inline_blame_entry( - self.editor.clone(), - editor.workspace()?.downgrade(), - blame, - blame_entry, - &style, - cx, - )?; + let mut element = render_inline_blame_entry(blame_entry, &style, cx)?; let inline_blame_padding = INLINE_BLAME_PADDING_EM_WIDTHS * em_advance; Some( element @@ -7262,6 +7375,7 @@ impl Element for EditorElement { content_origin, scroll_pixel_position, line_height, + &text_hitbox, window, cx, ); diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs index 84b1e8d317..fa68db7d7f 100644 --- a/crates/editor/src/git/blame.rs +++ b/crates/editor/src/git/blame.rs @@ -7,10 +7,11 @@ use git::{ parse_git_remote_url, }; use gpui::{ - AnyElement, App, AppContext as _, Context, Entity, Hsla, Subscription, Task, TextStyle, - WeakEntity, Window, + AnyElement, App, AppContext as _, Context, Entity, Hsla, ScrollHandle, Subscription, Task, + TextStyle, WeakEntity, Window, }; use language::{Bias, Buffer, BufferSnapshot, Edit}; +use markdown::Markdown; use multi_buffer::RowInfo; use project::{ Project, ProjectItem, @@ -98,10 +99,18 @@ pub trait BlameRenderer { &self, _: &TextStyle, _: BlameEntry, + _: &mut App, + ) -> Option; + + fn render_blame_entry_popover( + &self, + _: BlameEntry, + _: ScrollHandle, _: Option, + _: Entity, _: Entity, _: WeakEntity, - _: Entity, + _: &mut Window, _: &mut App, ) -> Option; @@ -139,10 +148,20 @@ impl BlameRenderer for () { &self, _: &TextStyle, _: BlameEntry, + _: &mut App, + ) -> Option { + None + } + + fn render_blame_entry_popover( + &self, + _: BlameEntry, + _: ScrollHandle, _: Option, + _: Entity, _: Entity, _: WeakEntity, - _: Entity, + _: &mut Window, _: &mut App, ) -> Option { None diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 7d3f618f52..ce3bc11bd4 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -957,6 +957,7 @@ impl Item for Editor { cx.subscribe(&workspace, |editor, _, event: &workspace::Event, _cx| { if matches!(event, workspace::Event::ModalOpened) { editor.mouse_context_menu.take(); + editor.inline_blame_popover.take(); } }) .detach(); diff --git a/crates/git_ui/src/blame_ui.rs b/crates/git_ui/src/blame_ui.rs index a86e4cab8c..f910de7bbe 100644 --- a/crates/git_ui/src/blame_ui.rs +++ b/crates/git_ui/src/blame_ui.rs @@ -1,19 +1,23 @@ -use crate::{commit_tooltip::CommitTooltip, commit_view::CommitView}; -use editor::{BlameRenderer, Editor}; +use crate::{ + commit_tooltip::{CommitAvatar, CommitDetails, CommitTooltip}, + commit_view::CommitView, +}; +use editor::{BlameRenderer, Editor, hover_markdown_style}; use git::{ blame::{BlameEntry, ParsedCommitMessage}, repository::CommitSummary, }; use gpui::{ - AnyElement, App, AppContext as _, ClipboardItem, Element as _, Entity, Hsla, - InteractiveElement as _, MouseButton, Pixels, StatefulInteractiveElement as _, Styled as _, - Subscription, TextStyle, WeakEntity, Window, div, + ClipboardItem, Entity, Hsla, MouseButton, ScrollHandle, Subscription, TextStyle, WeakEntity, + prelude::*, }; +use markdown::{Markdown, MarkdownElement}; use project::{git_store::Repository, project_settings::ProjectSettings}; use settings::Settings as _; -use ui::{ - ActiveTheme, Color, ContextMenu, FluentBuilder as _, Icon, IconName, ParentElement as _, h_flex, -}; +use theme::ThemeSettings; +use time::OffsetDateTime; +use time_format::format_local_timestamp; +use ui::{ContextMenu, Divider, IconButtonShape, prelude::*}; use workspace::Workspace; const GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED: usize = 20; @@ -115,10 +119,6 @@ impl BlameRenderer for GitBlameRenderer { &self, style: &TextStyle, blame_entry: BlameEntry, - details: Option, - repository: Entity, - workspace: WeakEntity, - editor: Entity, cx: &mut App, ) -> Option { let relative_timestamp = blame_entry_relative_timestamp(&blame_entry); @@ -144,25 +144,223 @@ impl BlameRenderer for GitBlameRenderer { .child(Icon::new(IconName::FileGit).color(Color::Hint)) .child(text) .gap_2() - .hoverable_tooltip(move |_window, cx| { - let tooltip = cx.new(|cx| { - CommitTooltip::blame_entry( - &blame_entry, - details.clone(), - repository.clone(), - workspace.clone(), - cx, - ) - }); - editor.update(cx, |editor, _| { - editor.git_blame_inline_tooltip = Some(tooltip.downgrade().into()) - }); - tooltip.into() - }) .into_any(), ) } + fn render_blame_entry_popover( + &self, + blame: BlameEntry, + scroll_handle: ScrollHandle, + details: Option, + markdown: Entity, + repository: Entity, + workspace: WeakEntity, + window: &mut Window, + cx: &mut App, + ) -> Option { + let commit_time = blame + .committer_time + .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("".to_string()) + .into(), + author_email: blame.author_mail.clone().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 short_commit_id = commit_details + .sha + .get(0..8) + .map(|sha| sha.to_string().into()) + .unwrap_or_else(|| commit_details.sha.clone()); + let full_sha = commit_details.sha.to_string().clone(); + let absolute_timestamp = format_local_timestamp( + commit_details.commit_time, + OffsetDateTime::now_utc(), + time_format::TimestampFormat::MediumAbsolute, + ); + let markdown_style = { + let mut style = hover_markdown_style(window, cx); + if let Some(code_block) = &style.code_block.text { + style.base_text_style.refine(code_block); + } + style + }; + + let message = commit_details + .message + .as_ref() + .map(|_| MarkdownElement::new(markdown.clone(), markdown_style).into_any()) + .unwrap_or("".into_any()); + + let pull_request = commit_details + .message + .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 + .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(), + 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( + 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( + full_sha.clone(), + ), + ) + }), + ), + ), + ), + ) + }), + ) + .into_any_element(), + ) + } + fn open_blame_commit( &self, blame_entry: BlameEntry, diff --git a/crates/git_ui/src/commit_tooltip.rs b/crates/git_ui/src/commit_tooltip.rs index a01c6bf1a9..00ab911610 100644 --- a/crates/git_ui/src/commit_tooltip.rs +++ b/crates/git_ui/src/commit_tooltip.rs @@ -27,22 +27,18 @@ pub struct CommitDetails { pub message: Option, } -struct CommitAvatar<'a> { +pub struct CommitAvatar<'a> { commit: &'a CommitDetails, } impl<'a> CommitAvatar<'a> { - fn new(details: &'a CommitDetails) -> Self { + pub fn new(details: &'a CommitDetails) -> Self { Self { commit: details } } } impl<'a> CommitAvatar<'a> { - fn render( - &'a self, - window: &mut Window, - cx: &mut Context, - ) -> Option> { + pub fn render(&'a self, window: &mut Window, cx: &mut App) -> Option> { let remote = self .commit .message