Add copy-on-click to diagnostic messages (#2634)

Mikayla Maki created

I finally got fed up with being unable to copy error messages. This adds
a click target and tooltip to f8-style diagnostics that copies their
text on click.

Release Notes:

- Added the ability to copy under-line diagnostic errors on click

Change summary

crates/diagnostics/src/diagnostics.rs      |  4 +
crates/editor/src/display_map/block_map.rs |  1 
crates/editor/src/editor.rs                | 48 +++++++++++++++++-------
crates/editor/src/element.rs               | 10 +++-
crates/workspace/src/workspace.rs          |  6 ++-
5 files changed, 49 insertions(+), 20 deletions(-)

Detailed changes

crates/diagnostics/src/diagnostics.rs 🔗

@@ -1509,7 +1509,8 @@ mod tests {
             let snapshot = editor.snapshot(cx);
             snapshot
                 .blocks_in_range(0..snapshot.max_point().row())
-                .filter_map(|(row, block)| {
+                .enumerate()
+                .filter_map(|(ix, (row, block))| {
                     let name = match block {
                         TransformBlock::Custom(block) => block
                             .render(&mut BlockContext {
@@ -1520,6 +1521,7 @@ mod tests {
                                 gutter_width: 0.,
                                 line_height: 0.,
                                 em_width: 0.,
+                                block_id: ix,
                             })
                             .name()?
                             .to_string(),

crates/editor/src/display_map/block_map.rs 🔗

@@ -88,6 +88,7 @@ pub struct BlockContext<'a, 'b, 'c> {
     pub gutter_padding: f32,
     pub em_width: f32,
     pub line_height: f32,
+    pub block_id: usize,
 }
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]

crates/editor/src/editor.rs 🔗

@@ -7949,6 +7949,7 @@ impl Deref for EditorStyle {
 
 pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> RenderBlock {
     let mut highlighted_lines = Vec::new();
+
     for (index, line) in diagnostic.message.lines().enumerate() {
         let line = match &diagnostic.source {
             Some(source) if index == 0 => {
@@ -7960,25 +7961,44 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
         };
         highlighted_lines.push(line);
     }
-
+    let message = diagnostic.message;
     Arc::new(move |cx: &mut BlockContext| {
+        let message = message.clone();
         let settings = settings::get::<ThemeSettings>(cx);
+        let tooltip_style = settings.theme.tooltip.clone();
         let theme = &settings.theme.editor;
         let style = diagnostic_style(diagnostic.severity, is_valid, theme);
         let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round();
-        Flex::column()
-            .with_children(highlighted_lines.iter().map(|(line, highlights)| {
-                Label::new(
-                    line.clone(),
-                    style.message.clone().with_font_size(font_size),
-                )
-                .with_highlights(highlights.clone())
-                .contained()
-                .with_margin_left(cx.anchor_x)
-            }))
-            .aligned()
-            .left()
-            .into_any()
+        let anchor_x = cx.anchor_x;
+        enum BlockContextToolip {}
+        MouseEventHandler::<BlockContext, _>::new(cx.block_id, cx, |_, _| {
+            Flex::column()
+                .with_children(highlighted_lines.iter().map(|(line, highlights)| {
+                    Label::new(
+                        line.clone(),
+                        style.message.clone().with_font_size(font_size),
+                    )
+                    .with_highlights(highlights.clone())
+                    .contained()
+                    .with_margin_left(anchor_x)
+                }))
+                .aligned()
+                .left()
+                .into_any()
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, move |_, _, cx| {
+            cx.write_to_clipboard(ClipboardItem::new(message.clone()));
+        })
+        // We really need to rethink this ID system...
+        .with_tooltip::<BlockContextToolip>(
+            cx.block_id,
+            "Copy diagnostic message".to_string(),
+            None,
+            tooltip_style,
+            cx,
+        )
+        .into_any()
     })
 }
 

crates/editor/src/element.rs 🔗

@@ -1467,6 +1467,7 @@ impl EditorElement {
         editor: &mut Editor,
         cx: &mut LayoutContext<Editor>,
     ) -> (f32, Vec<BlockLayout>) {
+        let mut block_id = 0;
         let scroll_x = snapshot.scroll_anchor.offset.x();
         let (fixed_blocks, non_fixed_blocks) = snapshot
             .blocks_in_range(rows.clone())
@@ -1474,7 +1475,7 @@ impl EditorElement {
                 TransformBlock::ExcerptHeader { .. } => false,
                 TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed,
             });
-        let mut render_block = |block: &TransformBlock, width: f32| {
+        let mut render_block = |block: &TransformBlock, width: f32, block_id: usize| {
             let mut element = match block {
                 TransformBlock::Custom(block) => {
                     let align_to = block
@@ -1499,6 +1500,7 @@ impl EditorElement {
                         scroll_x,
                         gutter_width,
                         em_width,
+                        block_id,
                     })
                 }
                 TransformBlock::ExcerptHeader {
@@ -1634,7 +1636,8 @@ impl EditorElement {
         let mut fixed_block_max_width = 0f32;
         let mut blocks = Vec::new();
         for (row, block) in fixed_blocks {
-            let element = render_block(block, f32::INFINITY);
+            let element = render_block(block, f32::INFINITY, block_id);
+            block_id += 1;
             fixed_block_max_width = fixed_block_max_width.max(element.size().x() + em_width);
             blocks.push(BlockLayout {
                 row,
@@ -1654,7 +1657,8 @@ impl EditorElement {
                     .max(gutter_width + scroll_width),
                 BlockStyle::Fixed => unreachable!(),
             };
-            let element = render_block(block, width);
+            let element = render_block(block, width, block_id);
+            block_id += 1;
             blocks.push(BlockLayout {
                 row,
                 element,

crates/workspace/src/workspace.rs 🔗

@@ -140,9 +140,11 @@ pub struct OpenPaths {
 #[derive(Clone, Deserialize, PartialEq)]
 pub struct ActivatePane(pub usize);
 
+#[derive(Deserialize)]
 pub struct Toast {
     id: usize,
     msg: Cow<'static, str>,
+    #[serde(skip)]
     on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
 }
 
@@ -183,9 +185,9 @@ impl Clone for Toast {
     }
 }
 
-pub type WorkspaceId = i64;
+impl_actions!(workspace, [ActivatePane, Toast]);
 
-impl_actions!(workspace, [ActivatePane]);
+pub type WorkspaceId = i64;
 
 pub fn init_settings(cx: &mut AppContext) {
     settings::register::<WorkspaceSettings>(cx);