From 1281f4672c64cd44ea7f6b9f3d55a7b8ae4dc8e3 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 23 Dec 2025 12:09:10 -0300 Subject: [PATCH] markdown: Add support for right-click menu copy item (#45572) In https://github.com/zed-industries/zed/pull/45440, we're implementing the ability to right-click in the agent panel and copy the rendered markdown. However, that presented itself as not as straightforward as just making the menu item fire the `CopyAsMarkdown` action because any selection in markdown is cleared after a new mouse click, and for the right-click copy menu item to work, we need to persist that selection even after the menu itself is opened and the "Copy" menu item is clicked. This all demanded a bit of work in the markdown file itself, and given we may want to use this functionality for other non-agent thread view markdown use cases in the future, I felt like it'd be better breaking it down into a separate PR that we can more easily track in the future. The context menu still needs to be built in the place where the markdown is created and rendered, though. This PR only adds the infrastructure needed so that this menu can simply fire the `CopyAsMarkdown` and make the copying work. Release Notes: - N/A --- crates/markdown/src/markdown.rs | 42 +++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 0bc3b9eb726e1782bafb2a31229ea21f308adc6e..2e18055a19c81189adb9a967c3dfe0d1ff55e8ff 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -22,9 +22,9 @@ use collections::{HashMap, HashSet}; use gpui::{ AnyElement, App, BorderStyle, Bounds, ClipboardItem, CursorStyle, DispatchPhase, Edges, Entity, FocusHandle, Focusable, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla, Image, - ImageFormat, KeyContext, Length, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, - Point, ScrollHandle, Stateful, StrikethroughStyle, StyleRefinement, StyledText, Task, - TextLayout, TextRun, TextStyle, TextStyleRefinement, actions, img, point, quad, + ImageFormat, KeyContext, Length, MouseButton, MouseDownEvent, MouseEvent, MouseMoveEvent, + MouseUpEvent, Point, ScrollHandle, Stateful, StrikethroughStyle, StyleRefinement, StyledText, + Task, TextLayout, TextRun, TextStyle, TextStyleRefinement, actions, img, point, quad, }; use language::{Language, LanguageRegistry, Rope}; use parser::CodeBlockMetadata; @@ -112,6 +112,7 @@ pub struct Markdown { options: Options, copied_code_blocks: HashSet, code_block_scroll_handles: HashMap, + context_menu_selected_text: Option, } struct Options { @@ -181,6 +182,7 @@ impl Markdown { }, copied_code_blocks: HashSet::default(), code_block_scroll_handles: HashMap::default(), + context_menu_selected_text: None, }; this.parse(cx); this @@ -205,6 +207,7 @@ impl Markdown { }, copied_code_blocks: HashSet::default(), code_block_scroll_handles: HashMap::default(), + context_menu_selected_text: None, }; this.parse(cx); this @@ -289,6 +292,14 @@ impl Markdown { } } + pub fn selected_text(&self) -> Option { + if self.selection.end <= self.selection.start { + None + } else { + Some(self.source[self.selection.start..self.selection.end].to_string()) + } + } + fn copy(&self, text: &RenderedText, _: &mut Window, cx: &mut Context) { if self.selection.end <= self.selection.start { return; @@ -297,7 +308,11 @@ impl Markdown { cx.write_to_clipboard(ClipboardItem::new_string(text)); } - fn copy_as_markdown(&self, _: &mut Window, cx: &mut Context) { + fn copy_as_markdown(&mut self, _: &mut Window, cx: &mut Context) { + if let Some(text) = self.context_menu_selected_text.take() { + cx.write_to_clipboard(ClipboardItem::new_string(text)); + return; + } if self.selection.end <= self.selection.start { return; } @@ -305,6 +320,10 @@ impl Markdown { cx.write_to_clipboard(ClipboardItem::new_string(text)); } + fn capture_selection_for_context_menu(&mut self) { + self.context_menu_selected_text = self.selected_text(); + } + fn parse(&mut self, cx: &mut Context) { if self.source.is_empty() { return; @@ -665,6 +684,19 @@ impl MarkdownElement { let on_open_url = self.on_url_click.take(); + self.on_mouse_event(window, cx, { + let hitbox = hitbox.clone(); + move |markdown, event: &MouseDownEvent, phase, window, _| { + if phase.capture() + && event.button == MouseButton::Right + && hitbox.is_hovered(window) + { + // Capture selected text so it survives until menu item is clicked + markdown.capture_selection_for_context_menu(); + } + } + }); + self.on_mouse_event(window, cx, { let rendered_text = rendered_text.clone(); let hitbox = hitbox.clone(); @@ -713,7 +745,7 @@ impl MarkdownElement { window.prevent_default(); cx.notify(); } - } else if phase.capture() { + } else if phase.capture() && event.button == MouseButton::Left { markdown.selection = Selection::default(); markdown.pressed_link = None; cx.notify();