Add a button to copy diagnostic messages from the hover popover to the clipboard (#45625)

Rocky Shi and Danilo Leal created

Closes https://github.com/zed-industries/zed/issues/45346

Release Notes:

- Added a button to copy diagnostic messages from the hove popover

Screenshot:

<img width="842" height="360" alt="image"
src="https://github.com/user-attachments/assets/9c00fba5-82aa-4179-95b1-afd5c1173889"
/>

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>

Change summary

crates/editor/src/hover_popover.rs | 36 ++++++++++++++++++++++++++-----
1 file changed, 30 insertions(+), 6 deletions(-)

Detailed changes

crates/editor/src/hover_popover.rs 🔗

@@ -8,8 +8,8 @@ use crate::{
 };
 use anyhow::Context as _;
 use gpui::{
-    AnyElement, AsyncWindowContext, Context, Entity, Focusable as _, FontWeight, Hsla,
-    InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, ScrollHandle, Size,
+    AnyElement, AsyncWindowContext, ClipboardItem, Context, Entity, Focusable as _, FontWeight,
+    Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, ScrollHandle, Size,
     StatefulInteractiveElement, StyleRefinement, Styled, Subscription, Task, TextStyleRefinement,
     Window, div, px,
 };
@@ -24,7 +24,7 @@ use std::{borrow::Cow, cell::RefCell};
 use std::{ops::Range, sync::Arc, time::Duration};
 use std::{path::PathBuf, rc::Rc};
 use theme::ThemeSettings;
-use ui::{Scrollbars, WithScrollbar, prelude::*, theme_is_transparent};
+use ui::{Scrollbars, Tooltip, WithScrollbar, prelude::*, theme_is_transparent};
 use url::Url;
 use util::TryFutureExt;
 use workspace::{OpenOptions, OpenVisible, Workspace};
@@ -994,11 +994,13 @@ impl DiagnosticPopover {
                     .border_color(self.border_color)
                     .rounded_lg()
                     .child(
-                        div()
+                        h_flex()
                             .id("diagnostic-content-container")
-                            .overflow_y_scroll()
+                            .gap_1()
+                            .items_start()
                             .max_w(max_size.width)
                             .max_h(max_size.height)
+                            .overflow_y_scroll()
                             .track_scroll(&self.scroll_handle)
                             .child(
                                 MarkdownElement::new(
@@ -1021,7 +1023,29 @@ impl DiagnosticPopover {
                                         }
                                     },
                                 ),
-                            ),
+                            )
+                            .child({
+                                let message = self.local_diagnostic.diagnostic.message.clone();
+                                let copied = cx
+                                    .read_from_clipboard()
+                                    .map(|item| item.text().as_ref() == Some(&message))
+                                    .unwrap_or(false);
+                                let (icon, color) = if copied {
+                                    (IconName::Check, Color::Success)
+                                } else {
+                                    (IconName::Copy, Color::Muted)
+                                };
+
+                                IconButton::new("copy-diagnostic", icon)
+                                    .icon_color(color)
+                                    .icon_size(IconSize::Small)
+                                    .tooltip(Tooltip::text("Copy Diagnostic"))
+                                    .on_click(move |_, _, cx| {
+                                        cx.write_to_clipboard(ClipboardItem::new_string(
+                                            message.clone(),
+                                        ));
+                                    })
+                            }),
                     )
                     .custom_scrollbars(
                         Scrollbars::for_settings::<EditorSettings>()