markdown: Move `open_url` to the `MarkdownElement` as `on_url_click` (#28269)

Marshall Bowers and Antonio Scandurra created

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>

Change summary

crates/agent/src/active_thread.rs       | 318 +++++++++++++-------------
crates/agent/src/agent_diff.rs          |   8 
crates/editor/src/code_context_menus.rs |   9 
crates/editor/src/hover_popover.rs      |  20 +
crates/markdown/src/markdown.rs         |  34 +-
crates/zed/src/zed.rs                   |   2 
6 files changed, 199 insertions(+), 192 deletions(-)

Detailed changes

crates/agent/src/active_thread.rs ๐Ÿ”—

@@ -76,7 +76,6 @@ impl RenderedMessage {
     fn from_segments(
         segments: &[MessageSegment],
         language_registry: Arc<LanguageRegistry>,
-        workspace: WeakEntity<Workspace>,
         cx: &mut App,
     ) -> Self {
         let mut this = Self {
@@ -84,12 +83,12 @@ impl RenderedMessage {
             segments: Vec::with_capacity(segments.len()),
         };
         for segment in segments {
-            this.push_segment(segment, workspace.clone(), cx);
+            this.push_segment(segment, cx);
         }
         this
     }
 
-    fn append_thinking(&mut self, text: &String, workspace: WeakEntity<Workspace>, cx: &mut App) {
+    fn append_thinking(&mut self, text: &String, cx: &mut App) {
         if let Some(RenderedMessageSegment::Thinking {
             content,
             scroll_handle,
@@ -101,18 +100,13 @@ impl RenderedMessage {
             scroll_handle.scroll_to_bottom();
         } else {
             self.segments.push(RenderedMessageSegment::Thinking {
-                content: render_markdown(
-                    text.into(),
-                    self.language_registry.clone(),
-                    workspace,
-                    cx,
-                ),
+                content: render_markdown(text.into(), self.language_registry.clone(), cx),
                 scroll_handle: ScrollHandle::default(),
             });
         }
     }
 
-    fn append_text(&mut self, text: &String, workspace: WeakEntity<Workspace>, cx: &mut App) {
+    fn append_text(&mut self, text: &String, cx: &mut App) {
         if let Some(RenderedMessageSegment::Text(markdown)) = self.segments.last_mut() {
             markdown.update(cx, |markdown, cx| markdown.append(text, cx));
         } else {
@@ -120,32 +114,20 @@ impl RenderedMessage {
                 .push(RenderedMessageSegment::Text(render_markdown(
                     SharedString::from(text),
                     self.language_registry.clone(),
-                    workspace,
                     cx,
                 )));
         }
     }
 
-    fn push_segment(
-        &mut self,
-        segment: &MessageSegment,
-        workspace: WeakEntity<Workspace>,
-        cx: &mut App,
-    ) {
+    fn push_segment(&mut self, segment: &MessageSegment, cx: &mut App) {
         let rendered_segment = match segment {
             MessageSegment::Thinking(text) => RenderedMessageSegment::Thinking {
-                content: render_markdown(
-                    text.into(),
-                    self.language_registry.clone(),
-                    workspace,
-                    cx,
-                ),
+                content: render_markdown(text.into(), self.language_registry.clone(), cx),
                 scroll_handle: ScrollHandle::default(),
             },
             MessageSegment::Text(text) => RenderedMessageSegment::Text(render_markdown(
                 text.into(),
                 self.language_registry.clone(),
-                workspace,
                 cx,
             )),
         };
@@ -164,14 +146,9 @@ enum RenderedMessageSegment {
 fn render_markdown(
     text: SharedString,
     language_registry: Arc<LanguageRegistry>,
-    workspace: WeakEntity<Workspace>,
     cx: &mut App,
 ) -> Entity<Markdown> {
-    cx.new(|cx| {
-        Markdown::new(text, Some(language_registry), None, cx).open_url(move |text, window, cx| {
-            open_markdown_link(text, workspace.clone(), window, cx);
-        })
-    })
+    cx.new(|cx| Markdown::new(text, Some(language_registry), None, cx))
 }
 
 fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
@@ -261,14 +238,9 @@ fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
 fn render_tool_use_markdown(
     text: SharedString,
     language_registry: Arc<LanguageRegistry>,
-    workspace: WeakEntity<Workspace>,
     cx: &mut App,
 ) -> Entity<Markdown> {
-    cx.new(|cx| {
-        Markdown::new(text, Some(language_registry), None, cx).open_url(move |text, window, cx| {
-            open_markdown_link(text, workspace.clone(), window, cx);
-        })
-    })
+    cx.new(|cx| Markdown::new(text, Some(language_registry), None, cx))
 }
 
 fn tool_use_markdown_style(window: &Window, cx: &mut App) -> MarkdownStyle {
@@ -502,12 +474,8 @@ impl ActiveThread {
         self.messages.push(*id);
         self.list_state.splice(old_len..old_len, 1);
 
-        let rendered_message = RenderedMessage::from_segments(
-            segments,
-            self.language_registry.clone(),
-            self.workspace.clone(),
-            cx,
-        );
+        let rendered_message =
+            RenderedMessage::from_segments(segments, self.language_registry.clone(), cx);
         self.rendered_messages_by_id.insert(*id, rendered_message);
     }
 
@@ -522,12 +490,8 @@ impl ActiveThread {
             return;
         };
         self.list_state.splice(index..index + 1, 1);
-        let rendered_message = RenderedMessage::from_segments(
-            segments,
-            self.language_registry.clone(),
-            self.workspace.clone(),
-            cx,
-        );
+        let rendered_message =
+            RenderedMessage::from_segments(segments, self.language_registry.clone(), cx);
         self.rendered_messages_by_id.insert(*id, rendered_message);
     }
 
@@ -549,12 +513,7 @@ impl ActiveThread {
         cx: &mut Context<Self>,
     ) {
         let rendered = RenderedToolUse {
-            label: render_tool_use_markdown(
-                tool_label.into(),
-                self.language_registry.clone(),
-                self.workspace.clone(),
-                cx,
-            ),
+            label: render_tool_use_markdown(tool_label.into(), self.language_registry.clone(), cx),
             input: render_tool_use_markdown(
                 format!(
                     "```json\n{}\n```",
@@ -562,15 +521,9 @@ impl ActiveThread {
                 )
                 .into(),
                 self.language_registry.clone(),
-                self.workspace.clone(),
-                cx,
-            ),
-            output: render_tool_use_markdown(
-                tool_output,
-                self.language_registry.clone(),
-                self.workspace.clone(),
                 cx,
             ),
+            output: render_tool_use_markdown(tool_output, self.language_registry.clone(), cx),
         };
         self.rendered_tool_uses
             .insert(tool_use_id.clone(), rendered);
@@ -613,12 +566,12 @@ impl ActiveThread {
             }
             ThreadEvent::StreamedAssistantText(message_id, text) => {
                 if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) {
-                    rendered_message.append_text(text, self.workspace.clone(), cx);
+                    rendered_message.append_text(text, cx);
                 }
             }
             ThreadEvent::StreamedAssistantThinking(message_id, text) => {
                 if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) {
-                    rendered_message.append_thinking(text, self.workspace.clone(), cx);
+                    rendered_message.append_thinking(text, cx);
                 }
             }
             ThreadEvent::MessageAdded(message_id) => {
@@ -1550,10 +1503,18 @@ impl ActiveThread {
                             )
                             .into_any_element(),
                         RenderedMessageSegment::Text(markdown) => div()
-                            .child(MarkdownElement::new(
-                                markdown.clone(),
-                                default_markdown_style(window, cx),
-                            ))
+                            .child(
+                                MarkdownElement::new(
+                                    markdown.clone(),
+                                    default_markdown_style(window, cx),
+                                )
+                                .on_url_click({
+                                    let workspace = self.workspace.clone();
+                                    move |text, window, cx| {
+                                        open_markdown_link(text, workspace.clone(), window, cx);
+                                    }
+                                }),
+                            )
                             .into_any_element(),
                     },
                 ),
@@ -1712,10 +1673,23 @@ impl ActiveThread {
                                     .h_20()
                                     .track_scroll(scroll_handle)
                                     .text_ui_sm(cx)
-                                    .child(MarkdownElement::new(
-                                        markdown.clone(),
-                                        default_markdown_style(window, cx),
-                                    ))
+                                    .child(
+                                        MarkdownElement::new(
+                                            markdown.clone(),
+                                            default_markdown_style(window, cx),
+                                        )
+                                        .on_url_click({
+                                            let workspace = self.workspace.clone();
+                                            move |text, window, cx| {
+                                                open_markdown_link(
+                                                    text,
+                                                    workspace.clone(),
+                                                    window,
+                                                    cx,
+                                                );
+                                            }
+                                        }),
+                                    )
                                     .overflow_hidden(),
                             )
                             .child(gradient_overlay),
@@ -1730,10 +1704,18 @@ impl ActiveThread {
                             .rounded_b_lg()
                             .bg(editor_bg)
                             .text_ui_sm(cx)
-                            .child(MarkdownElement::new(
-                                markdown.clone(),
-                                default_markdown_style(window, cx),
-                            )),
+                            .child(
+                                MarkdownElement::new(
+                                    markdown.clone(),
+                                    default_markdown_style(window, cx),
+                                )
+                                .on_url_click({
+                                    let workspace = self.workspace.clone();
+                                    move |text, window, cx| {
+                                        open_markdown_link(text, workspace.clone(), window, cx);
+                                    }
+                                }),
+                            ),
                     )
                 }),
         )
@@ -1789,13 +1771,41 @@ impl ActiveThread {
         let rendered_tool_use = self.rendered_tool_uses.get(&tool_use.id).cloned();
         let results_content_container = || v_flex().p_2().gap_0p5();
 
-        let results_content =
-            v_flex()
-                .gap_1()
-                .child(
+        let results_content = v_flex()
+            .gap_1()
+            .child(
+                results_content_container()
+                    .child(
+                        Label::new("Input")
+                            .size(LabelSize::XSmall)
+                            .color(Color::Muted)
+                            .buffer_font(cx),
+                    )
+                    .child(
+                        div()
+                            .w_full()
+                            .text_ui_sm(cx)
+                            .children(rendered_tool_use.as_ref().map(|rendered| {
+                                MarkdownElement::new(
+                                    rendered.input.clone(),
+                                    tool_use_markdown_style(window, cx),
+                                )
+                                .on_url_click({
+                                    let workspace = self.workspace.clone();
+                                    move |text, window, cx| {
+                                        open_markdown_link(text, workspace.clone(), window, cx);
+                                    }
+                                })
+                            })),
+                    ),
+            )
+            .map(|container| match tool_use.status {
+                ToolUseStatus::Finished(_) => container.child(
                     results_content_container()
+                        .border_t_1()
+                        .border_color(self.tool_card_border_color(cx))
                         .child(
-                            Label::new("Input")
+                            Label::new("Result")
                                 .size(LabelSize::XSmall)
                                 .color(Color::Muted)
                                 .buffer_font(cx),
@@ -1803,95 +1813,87 @@ impl ActiveThread {
                         .child(div().w_full().text_ui_sm(cx).children(
                             rendered_tool_use.as_ref().map(|rendered| {
                                 MarkdownElement::new(
-                                    rendered.input.clone(),
+                                    rendered.output.clone(),
                                     tool_use_markdown_style(window, cx),
                                 )
+                                .on_url_click({
+                                    let workspace = self.workspace.clone();
+                                    move |text, window, cx| {
+                                        open_markdown_link(text, workspace.clone(), window, cx);
+                                    }
+                                })
                             }),
                         )),
-                )
-                .map(|container| match tool_use.status {
-                    ToolUseStatus::Finished(_) => container.child(
-                        results_content_container()
+                ),
+                ToolUseStatus::Running => container.child(
+                    results_content_container().child(
+                        h_flex()
+                            .gap_1()
+                            .pb_1()
                             .border_t_1()
                             .border_color(self.tool_card_border_color(cx))
                             .child(
-                                Label::new("Result")
+                                Icon::new(IconName::ArrowCircle)
+                                    .size(IconSize::Small)
+                                    .color(Color::Accent)
+                                    .with_animation(
+                                        "arrow-circle",
+                                        Animation::new(Duration::from_secs(2)).repeat(),
+                                        |icon, delta| {
+                                            icon.transform(Transformation::rotate(percentage(
+                                                delta,
+                                            )))
+                                        },
+                                    ),
+                            )
+                            .child(
+                                Label::new("Runningโ€ฆ")
                                     .size(LabelSize::XSmall)
                                     .color(Color::Muted)
                                     .buffer_font(cx),
-                            )
-                            .child(div().w_full().text_ui_sm(cx).children(
-                                rendered_tool_use.as_ref().map(|rendered| {
+                            ),
+                    ),
+                ),
+                ToolUseStatus::Error(_) => container.child(
+                    results_content_container()
+                        .border_t_1()
+                        .border_color(self.tool_card_border_color(cx))
+                        .child(
+                            Label::new("Error")
+                                .size(LabelSize::XSmall)
+                                .color(Color::Muted)
+                                .buffer_font(cx),
+                        )
+                        .child(
+                            div()
+                                .text_ui_sm(cx)
+                                .children(rendered_tool_use.as_ref().map(|rendered| {
                                     MarkdownElement::new(
                                         rendered.output.clone(),
                                         tool_use_markdown_style(window, cx),
                                     )
-                                }),
-                            )),
-                    ),
-                    ToolUseStatus::Running => container.child(
-                        results_content_container().child(
-                            h_flex()
-                                .gap_1()
-                                .pb_1()
-                                .border_t_1()
-                                .border_color(self.tool_card_border_color(cx))
-                                .child(
-                                    Icon::new(IconName::ArrowCircle)
-                                        .size(IconSize::Small)
-                                        .color(Color::Accent)
-                                        .with_animation(
-                                            "arrow-circle",
-                                            Animation::new(Duration::from_secs(2)).repeat(),
-                                            |icon, delta| {
-                                                icon.transform(Transformation::rotate(percentage(
-                                                    delta,
-                                                )))
-                                            },
-                                        ),
-                                )
-                                .child(
-                                    Label::new("Runningโ€ฆ")
-                                        .size(LabelSize::XSmall)
-                                        .color(Color::Muted)
-                                        .buffer_font(cx),
-                                ),
+                                    .on_url_click({
+                                        let workspace = self.workspace.clone();
+                                        move |text, window, cx| {
+                                            open_markdown_link(text, workspace.clone(), window, cx);
+                                        }
+                                    })
+                                })),
                         ),
-                    ),
-                    ToolUseStatus::Error(_) => {
-                        container.child(
-                            results_content_container()
-                                .border_t_1()
-                                .border_color(self.tool_card_border_color(cx))
-                                .child(
-                                    Label::new("Error")
-                                        .size(LabelSize::XSmall)
-                                        .color(Color::Muted)
-                                        .buffer_font(cx),
-                                )
-                                .child(div().text_ui_sm(cx).children(
-                                    rendered_tool_use.as_ref().map(|rendered| {
-                                        MarkdownElement::new(
-                                            rendered.output.clone(),
-                                            tool_use_markdown_style(window, cx),
-                                        )
-                                    }),
-                                )),
-                        )
-                    }
-                    ToolUseStatus::Pending => container,
-                    ToolUseStatus::NeedsConfirmation => container.child(
-                        results_content_container()
-                            .border_t_1()
-                            .border_color(self.tool_card_border_color(cx))
-                            .child(
-                                Label::new("Asking Permission")
-                                    .size(LabelSize::Small)
-                                    .color(Color::Muted)
-                                    .buffer_font(cx),
-                            ),
-                    ),
-                });
+                ),
+                ToolUseStatus::Pending => container,
+                ToolUseStatus::NeedsConfirmation => container.child(
+                    results_content_container()
+                        .border_t_1()
+                        .border_color(self.tool_card_border_color(cx))
+                        .child(
+                            Label::new("Asking Permission")
+                                .size(LabelSize::Small)
+                                .color(Color::Muted)
+                                .buffer_font(cx),
+                        ),
+                ),
+            });
 
         let gradient_overlay = |color: Hsla| {
             div()
@@ -1939,7 +1941,9 @@ impl ActiveThread {
                                         )
                                         .child(
                                             h_flex().pr_8().text_ui_sm(cx).children(
-                                                rendered_tool_use.map(|rendered| MarkdownElement::new(rendered.label, tool_use_markdown_style(window, cx)))
+                                                rendered_tool_use.map(|rendered| MarkdownElement::new(rendered.label, tool_use_markdown_style(window, cx)).on_url_click({let workspace = self.workspace.clone(); move |text, window, cx| {
+                                                    open_markdown_link(text, workspace.clone(), window, cx);
+                                                }}))
                                             ),
                                         ),
                                 )
@@ -2027,7 +2031,9 @@ impl ActiveThread {
                                     )
                                     .child(
                                         h_flex().pr_8().text_ui_sm(cx).children(
-                                            rendered_tool_use.map(|rendered| MarkdownElement::new(rendered.label, tool_use_markdown_style(window, cx)))
+                                            rendered_tool_use.map(|rendered| MarkdownElement::new(rendered.label, tool_use_markdown_style(window, cx)).on_url_click({let workspace = self.workspace.clone(); move |text, window, cx| {
+                                                open_markdown_link(text, workspace.clone(), window, cx);
+                                            }}))
                                         ),
                                     ),
                             )

crates/agent/src/agent_diff.rs ๐Ÿ”—

@@ -792,15 +792,11 @@ impl editor::Addon for AgentDiffAddon {
 
 pub struct AgentDiffToolbar {
     agent_diff: Option<WeakEntity<AgentDiff>>,
-    _workspace: WeakEntity<Workspace>,
 }
 
 impl AgentDiffToolbar {
-    pub fn new(workspace: &Workspace, _: &mut Context<Self>) -> Self {
-        Self {
-            agent_diff: None,
-            _workspace: workspace.weak_handle(),
-        }
+    pub fn new() -> Self {
+        Self { agent_diff: None }
     }
 
     fn agent_diff(&self, _: &App) -> Option<Entity<AgentDiff>> {

crates/editor/src/code_context_menus.rs ๐Ÿ”—

@@ -624,16 +624,15 @@ impl CompletionsMenu {
                             .map(|l| l.name().to_proto());
                         Markdown::new(SharedString::default(), languages, language, cx)
                             .copy_code_block_buttons(false)
-                            .open_url(open_markdown_url)
                     })
                 });
                 markdown.update(cx, |markdown, cx| {
                     markdown.reset(parsed.clone(), cx);
                 });
-                div().child(MarkdownElement::new(
-                    markdown.clone(),
-                    hover_markdown_style(window, cx),
-                ))
+                div().child(
+                    MarkdownElement::new(markdown.clone(), hover_markdown_style(window, cx))
+                        .on_url_click(open_markdown_url),
+                )
             }
             CompletionDocumentation::MultiLineMarkdown(_) => return None,
             CompletionDocumentation::SingleLine(_) => return None,

crates/editor/src/hover_popover.rs ๐Ÿ”—

@@ -336,7 +336,7 @@ fn show_hover(
                             }
                         };
 
-                        Markdown::new_text(SharedString::new(text), cx).open_url(open_markdown_url)
+                        Markdown::new_text(SharedString::new(text), cx)
                     })
                     .ok();
 
@@ -547,7 +547,6 @@ async fn parse_blocks(
                 cx,
             )
             .copy_code_block_buttons(false)
-            .open_url(open_markdown_url)
         })
         .ok();
 
@@ -783,10 +782,13 @@ impl InfoPopover {
                         .max_h(max_size.height)
                         .p_2()
                         .track_scroll(&self.scroll_handle)
-                        .child(MarkdownElement::new(
-                            markdown.clone(),
-                            hover_markdown_style(window, cx),
-                        )),
+                        .child(
+                            MarkdownElement::new(
+                                markdown.clone(),
+                                hover_markdown_style(window, cx),
+                            )
+                            .on_url_click(open_markdown_url),
+                        ),
                 )
                 .child(self.render_vertical_scrollbar(cx));
         }
@@ -881,8 +883,10 @@ impl DiagnosticPopover {
                 ..Default::default()
             };
 
-            markdown_div =
-                markdown_div.child(MarkdownElement::new(markdown.clone(), markdown_style));
+            markdown_div = markdown_div.child(
+                MarkdownElement::new(markdown.clone(), markdown_style)
+                    .on_url_click(open_markdown_url),
+            );
         }
 
         if let Some(background_color) = &self.background_color {

crates/markdown/src/markdown.rs ๐Ÿ”—

@@ -80,7 +80,6 @@ pub struct Markdown {
     focus_handle: FocusHandle,
     language_registry: Option<Arc<LanguageRegistry>>,
     fallback_code_block_language: Option<String>,
-    open_url: Option<Box<dyn Fn(SharedString, &mut Window, &mut App)>>,
     options: Options,
     copied_code_blocks: HashSet<ElementId>,
 }
@@ -116,23 +115,12 @@ impl Markdown {
                 parse_links_only: false,
                 copy_code_block_buttons: true,
             },
-            open_url: None,
             copied_code_blocks: HashSet::new(),
         };
         this.parse(cx);
         this
     }
 
-    pub fn open_url(
-        self,
-        open_url: impl Fn(SharedString, &mut Window, &mut App) + 'static,
-    ) -> Self {
-        Self {
-            open_url: Some(Box::new(open_url)),
-            ..self
-        }
-    }
-
     pub fn new_text(source: SharedString, cx: &mut Context<Self>) -> Self {
         let focus_handle = cx.focus_handle();
         let mut this = Self {
@@ -150,7 +138,6 @@ impl Markdown {
                 parse_links_only: true,
                 copy_code_block_buttons: true,
             },
-            open_url: None,
             copied_code_blocks: HashSet::new(),
         };
         this.parse(cx);
@@ -328,11 +315,24 @@ impl ParsedMarkdown {
 pub struct MarkdownElement {
     markdown: Entity<Markdown>,
     style: MarkdownStyle,
+    on_url_click: Option<Box<dyn Fn(SharedString, &mut Window, &mut App)>>,
 }
 
 impl MarkdownElement {
     pub fn new(markdown: Entity<Markdown>, style: MarkdownStyle) -> Self {
-        Self { markdown, style }
+        Self {
+            markdown,
+            style,
+            on_url_click: None,
+        }
+    }
+
+    pub fn on_url_click(
+        mut self,
+        handler: impl Fn(SharedString, &mut Window, &mut App) + 'static,
+    ) -> Self {
+        self.on_url_click = Some(Box::new(handler));
+        self
     }
 
     fn paint_selection(
@@ -404,7 +404,7 @@ impl MarkdownElement {
     }
 
     fn paint_mouse_listeners(
-        &self,
+        &mut self,
         hitbox: &Hitbox,
         rendered_text: &RenderedText,
         window: &mut Window,
@@ -422,6 +422,8 @@ impl MarkdownElement {
             window.set_cursor_style(CursorStyle::IBeam, Some(hitbox));
         }
 
+        let on_open_url = self.on_url_click.take();
+
         self.on_mouse_event(window, cx, {
             let rendered_text = rendered_text.clone();
             let hitbox = hitbox.clone();
@@ -493,7 +495,7 @@ impl MarkdownElement {
                 if phase.bubble() {
                     if let Some(pressed_link) = markdown.pressed_link.take() {
                         if Some(&pressed_link) == rendered_text.link_for_position(event.position) {
-                            if let Some(open_url) = markdown.open_url.as_mut() {
+                            if let Some(open_url) = on_open_url.as_ref() {
                                 open_url(pressed_link.destination_url, window, cx);
                             } else {
                                 cx.open_url(&pressed_link.destination_url);

crates/zed/src/zed.rs ๐Ÿ”—

@@ -938,7 +938,7 @@ fn initialize_pane(
             toolbar.add_item(migration_banner, window, cx);
             let project_diff_toolbar = cx.new(|cx| ProjectDiffToolbar::new(workspace, cx));
             toolbar.add_item(project_diff_toolbar, window, cx);
-            let agent_diff_toolbar = cx.new(|cx| AgentDiffToolbar::new(workspace, cx));
+            let agent_diff_toolbar = cx.new(|_cx| AgentDiffToolbar::new());
             toolbar.add_item(agent_diff_toolbar, window, cx);
         })
     });