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

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
This commit is contained in:
Marshall Bowers 2025-04-07 16:43:00 -04:00 committed by GitHub
parent d3abc61728
commit 0414908c4a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 201 additions and 194 deletions

View file

@ -76,7 +76,6 @@ impl RenderedMessage {
fn from_segments( fn from_segments(
segments: &[MessageSegment], segments: &[MessageSegment],
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
workspace: WeakEntity<Workspace>,
cx: &mut App, cx: &mut App,
) -> Self { ) -> Self {
let mut this = Self { let mut this = Self {
@ -84,12 +83,12 @@ impl RenderedMessage {
segments: Vec::with_capacity(segments.len()), segments: Vec::with_capacity(segments.len()),
}; };
for segment in segments { for segment in segments {
this.push_segment(segment, workspace.clone(), cx); this.push_segment(segment, cx);
} }
this 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 { if let Some(RenderedMessageSegment::Thinking {
content, content,
scroll_handle, scroll_handle,
@ -101,18 +100,13 @@ impl RenderedMessage {
scroll_handle.scroll_to_bottom(); scroll_handle.scroll_to_bottom();
} else { } else {
self.segments.push(RenderedMessageSegment::Thinking { self.segments.push(RenderedMessageSegment::Thinking {
content: render_markdown( content: render_markdown(text.into(), self.language_registry.clone(), cx),
text.into(),
self.language_registry.clone(),
workspace,
cx,
),
scroll_handle: ScrollHandle::default(), 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() { if let Some(RenderedMessageSegment::Text(markdown)) = self.segments.last_mut() {
markdown.update(cx, |markdown, cx| markdown.append(text, cx)); markdown.update(cx, |markdown, cx| markdown.append(text, cx));
} else { } else {
@ -120,32 +114,20 @@ impl RenderedMessage {
.push(RenderedMessageSegment::Text(render_markdown( .push(RenderedMessageSegment::Text(render_markdown(
SharedString::from(text), SharedString::from(text),
self.language_registry.clone(), self.language_registry.clone(),
workspace,
cx, cx,
))); )));
} }
} }
fn push_segment( fn push_segment(&mut self, segment: &MessageSegment, cx: &mut App) {
&mut self,
segment: &MessageSegment,
workspace: WeakEntity<Workspace>,
cx: &mut App,
) {
let rendered_segment = match segment { let rendered_segment = match segment {
MessageSegment::Thinking(text) => RenderedMessageSegment::Thinking { MessageSegment::Thinking(text) => RenderedMessageSegment::Thinking {
content: render_markdown( content: render_markdown(text.into(), self.language_registry.clone(), cx),
text.into(),
self.language_registry.clone(),
workspace,
cx,
),
scroll_handle: ScrollHandle::default(), scroll_handle: ScrollHandle::default(),
}, },
MessageSegment::Text(text) => RenderedMessageSegment::Text(render_markdown( MessageSegment::Text(text) => RenderedMessageSegment::Text(render_markdown(
text.into(), text.into(),
self.language_registry.clone(), self.language_registry.clone(),
workspace,
cx, cx,
)), )),
}; };
@ -164,14 +146,9 @@ enum RenderedMessageSegment {
fn render_markdown( fn render_markdown(
text: SharedString, text: SharedString,
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
workspace: WeakEntity<Workspace>,
cx: &mut App, cx: &mut App,
) -> Entity<Markdown> { ) -> Entity<Markdown> {
cx.new(|cx| { cx.new(|cx| Markdown::new(text, Some(language_registry), None, cx))
Markdown::new(text, Some(language_registry), None, cx).open_url(move |text, window, cx| {
open_markdown_link(text, workspace.clone(), window, cx);
})
})
} }
fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { 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( fn render_tool_use_markdown(
text: SharedString, text: SharedString,
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
workspace: WeakEntity<Workspace>,
cx: &mut App, cx: &mut App,
) -> Entity<Markdown> { ) -> Entity<Markdown> {
cx.new(|cx| { cx.new(|cx| Markdown::new(text, Some(language_registry), None, cx))
Markdown::new(text, Some(language_registry), None, cx).open_url(move |text, window, cx| {
open_markdown_link(text, workspace.clone(), window, cx);
})
})
} }
fn tool_use_markdown_style(window: &Window, cx: &mut App) -> MarkdownStyle { fn tool_use_markdown_style(window: &Window, cx: &mut App) -> MarkdownStyle {
@ -502,12 +474,8 @@ impl ActiveThread {
self.messages.push(*id); self.messages.push(*id);
self.list_state.splice(old_len..old_len, 1); self.list_state.splice(old_len..old_len, 1);
let rendered_message = RenderedMessage::from_segments( let rendered_message =
segments, RenderedMessage::from_segments(segments, self.language_registry.clone(), cx);
self.language_registry.clone(),
self.workspace.clone(),
cx,
);
self.rendered_messages_by_id.insert(*id, rendered_message); self.rendered_messages_by_id.insert(*id, rendered_message);
} }
@ -522,12 +490,8 @@ impl ActiveThread {
return; return;
}; };
self.list_state.splice(index..index + 1, 1); self.list_state.splice(index..index + 1, 1);
let rendered_message = RenderedMessage::from_segments( let rendered_message =
segments, RenderedMessage::from_segments(segments, self.language_registry.clone(), cx);
self.language_registry.clone(),
self.workspace.clone(),
cx,
);
self.rendered_messages_by_id.insert(*id, rendered_message); self.rendered_messages_by_id.insert(*id, rendered_message);
} }
@ -549,12 +513,7 @@ impl ActiveThread {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let rendered = RenderedToolUse { let rendered = RenderedToolUse {
label: render_tool_use_markdown( label: render_tool_use_markdown(tool_label.into(), self.language_registry.clone(), cx),
tool_label.into(),
self.language_registry.clone(),
self.workspace.clone(),
cx,
),
input: render_tool_use_markdown( input: render_tool_use_markdown(
format!( format!(
"```json\n{}\n```", "```json\n{}\n```",
@ -562,15 +521,9 @@ impl ActiveThread {
) )
.into(), .into(),
self.language_registry.clone(), self.language_registry.clone(),
self.workspace.clone(),
cx,
),
output: render_tool_use_markdown(
tool_output,
self.language_registry.clone(),
self.workspace.clone(),
cx, cx,
), ),
output: render_tool_use_markdown(tool_output, self.language_registry.clone(), cx),
}; };
self.rendered_tool_uses self.rendered_tool_uses
.insert(tool_use_id.clone(), rendered); .insert(tool_use_id.clone(), rendered);
@ -613,12 +566,12 @@ impl ActiveThread {
} }
ThreadEvent::StreamedAssistantText(message_id, text) => { ThreadEvent::StreamedAssistantText(message_id, text) => {
if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) { 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) => { ThreadEvent::StreamedAssistantThinking(message_id, text) => {
if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) { 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) => { ThreadEvent::MessageAdded(message_id) => {
@ -1550,10 +1503,18 @@ impl ActiveThread {
) )
.into_any_element(), .into_any_element(),
RenderedMessageSegment::Text(markdown) => div() RenderedMessageSegment::Text(markdown) => div()
.child(MarkdownElement::new( .child(
MarkdownElement::new(
markdown.clone(), markdown.clone(),
default_markdown_style(window, cx), 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(), .into_any_element(),
}, },
), ),
@ -1712,10 +1673,23 @@ impl ActiveThread {
.h_20() .h_20()
.track_scroll(scroll_handle) .track_scroll(scroll_handle)
.text_ui_sm(cx) .text_ui_sm(cx)
.child(MarkdownElement::new( .child(
MarkdownElement::new(
markdown.clone(), markdown.clone(),
default_markdown_style(window, cx), 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(), .overflow_hidden(),
) )
.child(gradient_overlay), .child(gradient_overlay),
@ -1730,10 +1704,18 @@ impl ActiveThread {
.rounded_b_lg() .rounded_b_lg()
.bg(editor_bg) .bg(editor_bg)
.text_ui_sm(cx) .text_ui_sm(cx)
.child(MarkdownElement::new( .child(
MarkdownElement::new(
markdown.clone(), markdown.clone(),
default_markdown_style(window, cx), 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,8 +1771,7 @@ impl ActiveThread {
let rendered_tool_use = self.rendered_tool_uses.get(&tool_use.id).cloned(); 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_container = || v_flex().p_2().gap_0p5();
let results_content = let results_content = v_flex()
v_flex()
.gap_1() .gap_1()
.child( .child(
results_content_container() results_content_container()
@ -1800,14 +1781,23 @@ impl ActiveThread {
.color(Color::Muted) .color(Color::Muted)
.buffer_font(cx), .buffer_font(cx),
) )
.child(div().w_full().text_ui_sm(cx).children( .child(
rendered_tool_use.as_ref().map(|rendered| { div()
.w_full()
.text_ui_sm(cx)
.children(rendered_tool_use.as_ref().map(|rendered| {
MarkdownElement::new( MarkdownElement::new(
rendered.input.clone(), rendered.input.clone(),
tool_use_markdown_style(window, cx), 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 { .map(|container| match tool_use.status {
ToolUseStatus::Finished(_) => container.child( ToolUseStatus::Finished(_) => container.child(
@ -1826,6 +1816,12 @@ impl ActiveThread {
rendered.output.clone(), rendered.output.clone(),
tool_use_markdown_style(window, cx), 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);
}
})
}), }),
)), )),
), ),
@ -1858,8 +1854,7 @@ impl ActiveThread {
), ),
), ),
), ),
ToolUseStatus::Error(_) => { ToolUseStatus::Error(_) => container.child(
container.child(
results_content_container() results_content_container()
.border_t_1() .border_t_1()
.border_color(self.tool_card_border_color(cx)) .border_color(self.tool_card_border_color(cx))
@ -1869,16 +1864,23 @@ impl ActiveThread {
.color(Color::Muted) .color(Color::Muted)
.buffer_font(cx), .buffer_font(cx),
) )
.child(div().text_ui_sm(cx).children( .child(
rendered_tool_use.as_ref().map(|rendered| { div()
.text_ui_sm(cx)
.children(rendered_tool_use.as_ref().map(|rendered| {
MarkdownElement::new( MarkdownElement::new(
rendered.output.clone(), rendered.output.clone(),
tool_use_markdown_style(window, cx), 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);
} }
})
})),
),
),
ToolUseStatus::Pending => container, ToolUseStatus::Pending => container,
ToolUseStatus::NeedsConfirmation => container.child( ToolUseStatus::NeedsConfirmation => container.child(
results_content_container() results_content_container()
@ -1939,7 +1941,9 @@ impl ActiveThread {
) )
.child( .child(
h_flex().pr_8().text_ui_sm(cx).children( 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( .child(
h_flex().pr_8().text_ui_sm(cx).children( 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);
}}))
), ),
), ),
) )

View file

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

View file

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

View file

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

View file

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

View file

@ -938,7 +938,7 @@ fn initialize_pane(
toolbar.add_item(migration_banner, window, cx); toolbar.add_item(migration_banner, window, cx);
let project_diff_toolbar = cx.new(|cx| ProjectDiffToolbar::new(workspace, cx)); let project_diff_toolbar = cx.new(|cx| ProjectDiffToolbar::new(workspace, cx));
toolbar.add_item(project_diff_toolbar, window, 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); toolbar.add_item(agent_diff_toolbar, window, cx);
}) })
}); });