agent: Add headers for code blocks (#28253)

Bennet Bo Fenner , Danilo Leal , and Antonio Scandurra created

<img width="639" alt="image"
src="https://github.com/user-attachments/assets/1fd51387-cbdc-474d-b1a3-3d0201f3735a"
/>


Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Antonio Scandurra <me@as-cii.com>

Change summary

Cargo.lock                              |   3 
crates/agent/src/active_thread.rs       | 249 +++++++++++++++++-
crates/editor/src/code_context_menus.rs |   4 
crates/editor/src/hover_popover.rs      |   7 
crates/markdown/Cargo.toml              |   3 
crates/markdown/src/markdown.rs         | 342 ++++++++++++--------------
crates/markdown/src/parser.rs           |  13 
crates/markdown/src/path_range.rs       |  76 +++---
8 files changed, 437 insertions(+), 260 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -8354,7 +8354,6 @@ dependencies = [
  "anyhow",
  "assets",
  "env_logger 0.11.8",
- "file_icons",
  "gpui",
  "language",
  "languages",
@@ -8363,10 +8362,10 @@ dependencies = [
  "node_runtime",
  "pulldown-cmark 0.12.2",
  "settings",
+ "sum_tree",
  "theme",
  "ui",
  "util",
- "workspace",
  "workspace-hack",
 ]
 

crates/agent/src/active_thread.rs 🔗

@@ -10,21 +10,24 @@ use crate::tool_use::{PendingToolUseStatus, ToolUse, ToolUseStatus};
 use crate::ui::{AddedContext, AgentNotification, AgentNotificationEvent, ContextPill};
 use anyhow::Context as _;
 use assistant_settings::{AssistantSettings, NotifyWhenAgentWaiting};
-use collections::HashMap;
+use collections::{HashMap, HashSet};
 use editor::scroll::Autoscroll;
 use editor::{Editor, MultiBuffer};
 use gpui::{
-    AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent, DefiniteLength,
-    EdgesRefinement, Empty, Entity, Focusable, Hsla, Length, ListAlignment, ListState, MouseButton,
-    PlatformDisplay, ScrollHandle, Stateful, StyleRefinement, Subscription, Task,
+    AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent, ClipboardItem,
+    DefiniteLength, EdgesRefinement, Empty, Entity, Focusable, Hsla, ListAlignment, ListState,
+    MouseButton, PlatformDisplay, ScrollHandle, Stateful, StyleRefinement, Subscription, Task,
     TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, WindowHandle,
     linear_color_stop, linear_gradient, list, percentage, pulsating_between,
 };
 use language::{Buffer, LanguageRegistry};
 use language_model::{ConfiguredModel, LanguageModelRegistry, LanguageModelToolUseId, Role};
-use markdown::{Markdown, MarkdownElement, MarkdownStyle};
+use markdown::parser::CodeBlockKind;
+use markdown::{Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown, without_fences};
 use project::ProjectItem as _;
 use settings::{Settings as _, update_settings_file};
+use std::ops::Range;
+use std::path::Path;
 use std::rc::Rc;
 use std::sync::Arc;
 use std::time::Duration;
@@ -55,6 +58,7 @@ pub struct ActiveThread {
     expanded_thinking_segments: HashMap<(MessageId, usize), bool>,
     last_error: Option<ThreadError>,
     notifications: Vec<WindowHandle<AgentNotification>>,
+    copied_code_block_ids: HashSet<usize>,
     _subscriptions: Vec<Subscription>,
     notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
     feedback_message_editor: Option<Entity<Editor>>,
@@ -100,7 +104,7 @@ impl RenderedMessage {
             scroll_handle.scroll_to_bottom();
         } else {
             self.segments.push(RenderedMessageSegment::Thinking {
-                content: render_markdown(text.into(), self.language_registry.clone(), cx),
+                content: parse_markdown(text.into(), self.language_registry.clone(), cx),
                 scroll_handle: ScrollHandle::default(),
             });
         }
@@ -111,7 +115,7 @@ impl RenderedMessage {
             markdown.update(cx, |markdown, cx| markdown.append(text, cx));
         } else {
             self.segments
-                .push(RenderedMessageSegment::Text(render_markdown(
+                .push(RenderedMessageSegment::Text(parse_markdown(
                     SharedString::from(text),
                     self.language_registry.clone(),
                     cx,
@@ -122,10 +126,10 @@ impl RenderedMessage {
     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(), cx),
+                content: parse_markdown(text.into(), self.language_registry.clone(), cx),
                 scroll_handle: ScrollHandle::default(),
             },
-            MessageSegment::Text(text) => RenderedMessageSegment::Text(render_markdown(
+            MessageSegment::Text(text) => RenderedMessageSegment::Text(parse_markdown(
                 text.into(),
                 self.language_registry.clone(),
                 cx,
@@ -143,7 +147,7 @@ enum RenderedMessageSegment {
     Text(Entity<Markdown>),
 }
 
-fn render_markdown(
+fn parse_markdown(
     text: SharedString,
     language_registry: Arc<LanguageRegistry>,
     cx: &mut App,
@@ -174,12 +178,6 @@ fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
         code_block_overflow_x_scroll: true,
         table_overflow_x_scroll: true,
         code_block: StyleRefinement {
-            margin: EdgesRefinement {
-                top: Some(Length::Definite(rems(0.).into())),
-                left: Some(Length::Definite(rems(0.).into())),
-                right: Some(Length::Definite(rems(0.).into())),
-                bottom: Some(Length::Definite(rems(0.5).into())),
-            },
             padding: EdgesRefinement {
                 top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
                 left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
@@ -187,13 +185,6 @@ fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
                 bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
             },
             background: Some(colors.editor_background.into()),
-            border_color: Some(colors.border_variant),
-            border_widths: EdgesRefinement {
-                top: Some(AbsoluteLength::Pixels(Pixels(1.))),
-                left: Some(AbsoluteLength::Pixels(Pixels(1.))),
-                right: Some(AbsoluteLength::Pixels(Pixels(1.))),
-                bottom: Some(AbsoluteLength::Pixels(Pixels(1.))),
-            },
             text: Some(TextStyleRefinement {
                 font_family: Some(theme_settings.buffer_font.family.clone()),
                 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
@@ -297,6 +288,197 @@ fn tool_use_markdown_style(window: &Window, cx: &mut App) -> MarkdownStyle {
     }
 }
 
+fn render_markdown_code_block(
+    id: usize,
+    kind: &CodeBlockKind,
+    parsed_markdown: &ParsedMarkdown,
+    codeblock_range: Range<usize>,
+    active_thread: Entity<ActiveThread>,
+    workspace: WeakEntity<Workspace>,
+    _window: &mut Window,
+    cx: &App,
+) -> Div {
+    let label = match kind {
+        CodeBlockKind::Indented => None,
+        CodeBlockKind::Fenced => Some(
+            h_flex()
+                .gap_1()
+                .child(
+                    Icon::new(IconName::Code)
+                        .color(Color::Muted)
+                        .size(IconSize::XSmall),
+                )
+                .child(Label::new("untitled").size(LabelSize::Small))
+                .into_any_element(),
+        ),
+        CodeBlockKind::FencedLang(raw_language_name) => Some(
+            h_flex()
+                .gap_1()
+                .children(
+                    parsed_markdown
+                        .languages_by_name
+                        .get(raw_language_name)
+                        .and_then(|language| {
+                            language
+                                .config()
+                                .matcher
+                                .path_suffixes
+                                .iter()
+                                .find_map(|extension| {
+                                    file_icons::FileIcons::get_icon(Path::new(extension), cx)
+                                })
+                                .map(Icon::from_path)
+                                .map(|icon| icon.color(Color::Muted).size(IconSize::Small))
+                        }),
+                )
+                .child(
+                    Label::new(
+                        parsed_markdown
+                            .languages_by_name
+                            .get(raw_language_name)
+                            .map(|language| language.name().into())
+                            .clone()
+                            .unwrap_or_else(|| raw_language_name.clone()),
+                    )
+                    .size(LabelSize::Small),
+                )
+                .into_any_element(),
+        ),
+        CodeBlockKind::FencedSrc(path_range) => path_range.path.file_name().map(|file_name| {
+            let content = if let Some(parent) = path_range.path.parent() {
+                h_flex()
+                    .ml_1()
+                    .gap_1()
+                    .child(
+                        Label::new(file_name.to_string_lossy().to_string()).size(LabelSize::Small),
+                    )
+                    .child(
+                        Label::new(parent.to_string_lossy().to_string())
+                            .color(Color::Muted)
+                            .size(LabelSize::Small),
+                    )
+                    .into_any_element()
+            } else {
+                Label::new(path_range.path.to_string_lossy().to_string())
+                    .size(LabelSize::Small)
+                    .ml_1()
+                    .into_any_element()
+            };
+
+            h_flex()
+                .id(("code-block-header-label", id))
+                .w_full()
+                .max_w_full()
+                .px_1()
+                .gap_0p5()
+                .cursor_pointer()
+                .rounded_sm()
+                .hover(|item| item.bg(cx.theme().colors().element_hover.opacity(0.5)))
+                .tooltip(Tooltip::text("Jump to file"))
+                .children(
+                    file_icons::FileIcons::get_icon(&path_range.path, cx)
+                        .map(Icon::from_path)
+                        .map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)),
+                )
+                .child(content)
+                .child(
+                    Icon::new(IconName::ArrowUpRight)
+                        .size(IconSize::XSmall)
+                        .color(Color::Ignored),
+                )
+                .on_click({
+                    let path_range = path_range.clone();
+                    move |_, window, cx| {
+                        workspace
+                            .update(cx, {
+                                |workspace, cx| {
+                                    if let Some(project_path) = workspace
+                                        .project()
+                                        .read(cx)
+                                        .find_project_path(&path_range.path, cx)
+                                    {
+                                        workspace
+                                            .open_path(project_path, None, true, window, cx)
+                                            .detach_and_log_err(cx);
+                                    }
+                                }
+                            })
+                            .ok();
+                    }
+                })
+                .into_any_element()
+        }),
+    };
+
+    let codeblock_header_bg = cx
+        .theme()
+        .colors()
+        .element_background
+        .blend(cx.theme().colors().editor_foreground.opacity(0.01));
+
+    let codeblock_was_copied = active_thread.read(cx).copied_code_block_ids.contains(&id);
+
+    let codeblock_header = h_flex()
+        .p_1()
+        .gap_1()
+        .justify_between()
+        .border_b_1()
+        .border_color(cx.theme().colors().border_variant)
+        .bg(codeblock_header_bg)
+        .rounded_t_md()
+        .children(label)
+        .child(
+            IconButton::new(
+                ("copy-markdown-code", id),
+                if codeblock_was_copied {
+                    IconName::Check
+                } else {
+                    IconName::Copy
+                },
+            )
+            .icon_color(Color::Muted)
+            .shape(ui::IconButtonShape::Square)
+            .tooltip(Tooltip::text("Copy Code"))
+            .on_click({
+                let active_thread = active_thread.clone();
+                let parsed_markdown = parsed_markdown.clone();
+                move |_event, _window, cx| {
+                    active_thread.update(cx, |this, cx| {
+                        this.copied_code_block_ids.insert(id);
+
+                        let code =
+                            without_fences(&parsed_markdown.source()[codeblock_range.clone()])
+                                .to_string();
+
+                        cx.write_to_clipboard(ClipboardItem::new_string(code.clone()));
+
+                        cx.spawn(async move |this, cx| {
+                            cx.background_executor().timer(Duration::from_secs(2)).await;
+
+                            cx.update(|cx| {
+                                this.update(cx, |this, cx| {
+                                    this.copied_code_block_ids.remove(&id);
+                                    cx.notify();
+                                })
+                            })
+                            .ok();
+                        })
+                        .detach();
+                    });
+                }
+            }),
+        );
+
+    v_flex()
+        .mb_2()
+        .relative()
+        .overflow_hidden()
+        .rounded_lg()
+        .border_1()
+        .border_color(cx.theme().colors().border_variant)
+        .child(codeblock_header)
+}
+
 fn open_markdown_link(
     text: SharedString,
     workspace: WeakEntity<Workspace>,
@@ -410,6 +592,7 @@ impl ActiveThread {
             hide_scrollbar_task: None,
             editing_message: None,
             last_error: None,
+            copied_code_block_ids: HashSet::default(),
             notifications: Vec::new(),
             _subscriptions: subscriptions,
             notification_subscriptions: HashMap::default(),
@@ -1128,6 +1311,7 @@ impl ActiveThread {
                                         message_id,
                                         rendered_message,
                                         has_tool_uses,
+                                        workspace.clone(),
                                         window,
                                         cx,
                                     ))
@@ -1465,6 +1649,7 @@ impl ActiveThread {
         message_id: MessageId,
         rendered_message: &RenderedMessage,
         has_tool_uses: bool,
+        workspace: WeakEntity<Workspace>,
         window: &Window,
         cx: &Context<Self>,
     ) -> impl IntoElement {
@@ -1508,6 +1693,24 @@ impl ActiveThread {
                                     markdown.clone(),
                                     default_markdown_style(window, cx),
                                 )
+                                .code_block_renderer(markdown::CodeBlockRenderer::Custom {
+                                    render: Arc::new({
+                                        let workspace = workspace.clone();
+                                        let active_thread = cx.entity();
+                                        move |id, kind, parsed_markdown, range, window, cx| {
+                                            render_markdown_code_block(
+                                                id,
+                                                kind,
+                                                parsed_markdown,
+                                                range,
+                                                active_thread.clone(),
+                                                workspace.clone(),
+                                                window,
+                                                cx,
+                                            )
+                                        }
+                                    }),
+                                })
                                 .on_url_click({
                                     let workspace = self.workspace.clone();
                                     move |text, window, cx| {

crates/editor/src/code_context_menus.rs 🔗

@@ -623,7 +623,6 @@ impl CompletionsMenu {
                             .language_at(self.initial_position, cx)
                             .map(|l| l.name().to_proto());
                         Markdown::new(SharedString::default(), languages, language, cx)
-                            .copy_code_block_buttons(false)
                     })
                 });
                 markdown.update(cx, |markdown, cx| {
@@ -631,6 +630,9 @@ impl CompletionsMenu {
                 });
                 div().child(
                     MarkdownElement::new(markdown.clone(), hover_markdown_style(window, cx))
+                        .code_block_renderer(markdown::CodeBlockRenderer::Default {
+                            copy_button: false,
+                        })
                         .on_url_click(open_markdown_url),
                 )
             }

crates/editor/src/hover_popover.rs 🔗

@@ -546,7 +546,6 @@ async fn parse_blocks(
                 fallback_language_name,
                 cx,
             )
-            .copy_code_block_buttons(false)
         })
         .ok();
 
@@ -787,6 +786,9 @@ impl InfoPopover {
                                 markdown.clone(),
                                 hover_markdown_style(window, cx),
                             )
+                            .code_block_renderer(markdown::CodeBlockRenderer::Default {
+                                copy_button: false,
+                            })
                             .on_url_click(open_markdown_url),
                         ),
                 )
@@ -885,6 +887,9 @@ impl DiagnosticPopover {
 
             markdown_div = markdown_div.child(
                 MarkdownElement::new(markdown.clone(), markdown_style)
+                    .code_block_renderer(markdown::CodeBlockRenderer::Default {
+                        copy_button: false,
+                    })
                     .on_url_click(open_markdown_url),
             );
         }

crates/markdown/Cargo.toml 🔗

@@ -20,16 +20,15 @@ test-support = [
 
 [dependencies]
 anyhow.workspace = true
-file_icons.workspace = true
 gpui.workspace = true
 language.workspace = true
 linkify.workspace = true
 log.workspace = true
 pulldown-cmark.workspace = true
+sum_tree.workspace = true
 theme.workspace = true
 ui.workspace = true
 util.workspace = true
-workspace.workspace = true
 workspace-hack.workspace = true
 
 [dev-dependencies]

crates/markdown/src/markdown.rs 🔗

@@ -1,12 +1,11 @@
 pub mod parser;
 mod path_range;
 
-use file_icons::FileIcons;
-use std::collections::{HashMap, HashSet};
+use std::collections::HashSet;
 use std::iter;
 use std::mem;
 use std::ops::Range;
-use std::path::PathBuf;
+use std::path::Path;
 use std::rc::Rc;
 use std::sync::Arc;
 use std::time::Duration;
@@ -21,10 +20,10 @@ use gpui::{
 use language::{Language, LanguageRegistry, Rope};
 use parser::{MarkdownEvent, MarkdownTag, MarkdownTagEnd, parse_links_only, parse_markdown};
 use pulldown_cmark::Alignment;
+use sum_tree::TreeMap;
 use theme::SyntaxTheme;
-use ui::{ButtonLike, Tooltip, prelude::*};
+use ui::{Tooltip, prelude::*};
 use util::{ResultExt, TryFutureExt};
-use workspace::Workspace;
 
 use crate::parser::CodeBlockKind;
 
@@ -84,12 +83,18 @@ pub struct Markdown {
     copied_code_blocks: HashSet<ElementId>,
 }
 
-#[derive(Debug)]
 struct Options {
     parse_links_only: bool,
-    copy_code_block_buttons: bool,
 }
 
+pub enum CodeBlockRenderer {
+    Default { copy_button: bool },
+    Custom { render: CodeBlockRenderFn },
+}
+
+pub type CodeBlockRenderFn =
+    Arc<dyn Fn(usize, &CodeBlockKind, &ParsedMarkdown, Range<usize>, &mut Window, &App) -> Div>;
+
 actions!(markdown, [Copy, CopyAsMarkdown]);
 
 impl Markdown {
@@ -113,7 +118,6 @@ impl Markdown {
             fallback_code_block_language,
             options: Options {
                 parse_links_only: false,
-                copy_code_block_buttons: true,
             },
             copied_code_blocks: HashSet::new(),
         };
@@ -136,7 +140,6 @@ impl Markdown {
             fallback_code_block_language: None,
             options: Options {
                 parse_links_only: true,
-                copy_code_block_buttons: true,
             },
             copied_code_blocks: HashSet::new(),
         };
@@ -205,19 +208,19 @@ impl Markdown {
                 return anyhow::Ok(ParsedMarkdown {
                     events: Arc::from(parse_links_only(source.as_ref())),
                     source,
-                    languages_by_name: HashMap::default(),
-                    languages_by_path: HashMap::default(),
+                    languages_by_name: TreeMap::default(),
+                    languages_by_path: TreeMap::default(),
                 });
             }
             let (events, language_names, paths) = parse_markdown(&source);
-            let mut languages_by_name = HashMap::with_capacity(language_names.len());
-            let mut languages_by_path = HashMap::with_capacity(paths.len());
+            let mut languages_by_name = TreeMap::default();
+            let mut languages_by_path = TreeMap::default();
             if let Some(registry) = language_registry.as_ref() {
                 for name in language_names {
                     let language = if !name.is_empty() {
-                        registry.language_for_name(&name)
+                        registry.language_for_name_or_extension(&name)
                     } else if let Some(fallback) = &fallback {
-                        registry.language_for_name(fallback)
+                        registry.language_for_name_or_extension(fallback)
                     } else {
                         continue;
                     };
@@ -259,11 +262,6 @@ impl Markdown {
             .await
         }));
     }
-
-    pub fn copy_code_block_buttons(mut self, should_copy: bool) -> Self {
-        self.options.copy_code_block_buttons = should_copy;
-        self
-    }
 }
 
 impl Focusable for Markdown {
@@ -302,12 +300,12 @@ impl Selection {
     }
 }
 
-#[derive(Default)]
+#[derive(Clone, Default)]
 pub struct ParsedMarkdown {
-    source: SharedString,
-    events: Arc<[(Range<usize>, MarkdownEvent)]>,
-    languages_by_name: HashMap<SharedString, Arc<Language>>,
-    languages_by_path: HashMap<PathBuf, Arc<Language>>,
+    pub source: SharedString,
+    pub events: Arc<[(Range<usize>, MarkdownEvent)]>,
+    pub languages_by_name: TreeMap<SharedString, Arc<Language>>,
+    pub languages_by_path: TreeMap<Arc<Path>, Arc<Language>>,
 }
 
 impl ParsedMarkdown {
@@ -323,6 +321,7 @@ impl ParsedMarkdown {
 pub struct MarkdownElement {
     markdown: Entity<Markdown>,
     style: MarkdownStyle,
+    code_block_renderer: CodeBlockRenderer,
     on_url_click: Option<Box<dyn Fn(SharedString, &mut Window, &mut App)>>,
 }
 
@@ -331,10 +330,16 @@ impl MarkdownElement {
         Self {
             markdown,
             style,
+            code_block_renderer: CodeBlockRenderer::Default { copy_button: true },
             on_url_click: None,
         }
     }
 
+    pub fn code_block_renderer(mut self, variant: CodeBlockRenderer) -> Self {
+        self.code_block_renderer = variant;
+        self
+    }
+
     pub fn on_url_click(
         mut self,
         handler: impl Fn(SharedString, &mut Window, &mut App) + 'static,
@@ -589,7 +594,6 @@ impl Element for MarkdownElement {
             0
         };
 
-        let code_citation_id = SharedString::from("code-citation-link");
         for (index, (range, event)) in parsed_markdown.events.iter().enumerate() {
             match event {
                 MarkdownEvent::Start(tag) => {
@@ -634,123 +638,80 @@ impl Element for MarkdownElement {
                                 CodeBlockKind::FencedLang(language) => {
                                     parsed_markdown.languages_by_name.get(language).cloned()
                                 }
-                                CodeBlockKind::FencedSrc(path_range) => {
-                                    // If the path actually exists in the project, render a link to it.
-                                    if let Some(project_path) =
-                                        window.root::<Workspace>().flatten().and_then(|workspace| {
-                                            if path_range.path.is_absolute() {
-                                                return None;
-                                            }
+                                CodeBlockKind::FencedSrc(path_range) => parsed_markdown
+                                    .languages_by_path
+                                    .get(&path_range.path)
+                                    .cloned(),
+                                _ => None,
+                            };
 
-                                            workspace
-                                                .read(cx)
-                                                .project()
-                                                .read(cx)
-                                                .find_project_path(&path_range.path, cx)
-                                        })
+                            let is_indented = matches!(kind, CodeBlockKind::Indented);
+
+                            match (&self.code_block_renderer, is_indented) {
+                                (CodeBlockRenderer::Default { .. }, _) | (_, true) => {
+                                    // This is a parent container that we can position the copy button inside.
+                                    builder.push_div(
+                                        div().relative().w_full(),
+                                        range,
+                                        markdown_end,
+                                    );
+
+                                    let mut code_block = div()
+                                        .id(("code-block", range.start))
+                                        .rounded_lg()
+                                        .map(|mut code_block| {
+                                            if self.style.code_block_overflow_x_scroll {
+                                                code_block.style().restrict_scroll_to_axis =
+                                                    Some(true);
+                                                code_block.flex().overflow_x_scroll()
+                                            } else {
+                                                code_block.w_full()
+                                            }
+                                        });
+                                    code_block.style().refine(&self.style.code_block);
+                                    if let Some(code_block_text_style) = &self.style.code_block.text
                                     {
-                                        builder.flush_text();
-
-                                        builder.push_div(
-                                            div().relative().w_full(),
-                                            range,
-                                            markdown_end,
-                                        );
-
-                                        builder.modify_current_div(|el| {
-                                            let file_icon =
-                                                FileIcons::get_icon(&project_path.path, cx)
-                                                    .map(|path| {
-                                                        Icon::from_path(path)
-                                                            .color(Color::Muted)
-                                                            .into_any_element()
-                                                    })
-                                                    .unwrap_or_else(|| {
-                                                        IconButton::new(
-                                                            "file-path-icon",
-                                                            IconName::File,
-                                                        )
-                                                        .shape(ui::IconButtonShape::Square)
-                                                        .into_any_element()
-                                                    });
-
-                                            el.child(
-                                                ButtonLike::new(ElementId::NamedInteger(
-                                                    code_citation_id.clone(),
-                                                    index,
-                                                ))
-                                                .child(
-                                                    div()
-                                                        .mb_1()
-                                                        .flex()
-                                                        .items_center()
-                                                        .gap_1()
-                                                        .child(file_icon)
-                                                        .child(
-                                                            Label::new(
-                                                                project_path
-                                                                    .path
-                                                                    .display()
-                                                                    .to_string(),
-                                                            )
-                                                            .color(Color::Muted)
-                                                            .underline(),
-                                                        ),
-                                                )
-                                                .on_click({
-                                                    let click_path = project_path.clone();
-                                                    move |_, window, cx| {
-                                                        if let Some(workspace) =
-                                                            window.root::<Workspace>().flatten()
-                                                        {
-                                                            workspace.update(cx, |workspace, cx| {
-                                                                workspace
-                                                                    .open_path(
-                                                                        click_path.clone(),
-                                                                        None,
-                                                                        true,
-                                                                        window,
-                                                                        cx,
-                                                                    )
-                                                                    .detach_and_log_err(cx);
-                                                            })
-                                                        }
-                                                    }
-                                                }),
-                                            )
+                                        builder.push_text_style(code_block_text_style.to_owned());
+                                    }
+                                    builder.push_code_block(language);
+                                    builder.push_div(code_block, range, markdown_end);
+                                }
+                                (CodeBlockRenderer::Custom { render }, _) => {
+                                    let parent_container = render(
+                                        index,
+                                        kind,
+                                        &parsed_markdown,
+                                        range.clone(),
+                                        window,
+                                        cx,
+                                    );
+
+                                    builder.push_div(parent_container, range, markdown_end);
+
+                                    let mut code_block = div()
+                                        .id(("code-block", range.start))
+                                        .rounded_b_lg()
+                                        .map(|mut code_block| {
+                                            if self.style.code_block_overflow_x_scroll {
+                                                code_block.style().restrict_scroll_to_axis =
+                                                    Some(true);
+                                                code_block.flex().overflow_x_scroll()
+                                            } else {
+                                                code_block.w_full()
+                                            }
                                         });
 
-                                        builder.pop_div();
+                                    code_block.style().refine(&self.style.code_block);
+
+                                    if let Some(code_block_text_style) = &self.style.code_block.text
+                                    {
+                                        builder.push_text_style(code_block_text_style.to_owned());
                                     }
 
-                                    parsed_markdown
-                                        .languages_by_path
-                                        .get(&path_range.path)
-                                        .cloned()
+                                    builder.push_code_block(language);
+                                    builder.push_div(code_block, range, markdown_end);
                                 }
-                                _ => None,
-                            };
-
-                            // This is a parent container that we can position the copy button inside.
-                            builder.push_div(div().relative().w_full(), range, markdown_end);
-
-                            let mut code_block = div()
-                                .id(("code-block", range.start))
-                                .rounded_lg()
-                                .map(|mut code_block| {
-                                    if self.style.code_block_overflow_x_scroll {
-                                        code_block.style().restrict_scroll_to_axis = Some(true);
-                                        code_block.flex().overflow_x_scroll()
-                                    } else {
-                                        code_block.w_full()
-                                    }
-                                });
-                            code_block.style().refine(&self.style.code_block);
-                            if let Some(code_block_text_style) = &self.style.code_block.text {
-                                builder.push_text_style(code_block_text_style.to_owned());
                             }
-                            builder.push_code_block(language);
-                            builder.push_div(code_block, range, markdown_end);
                         }
                         MarkdownTag::HtmlBlock => builder.push_div(div(), range, markdown_end),
                         MarkdownTag::List(bullet_index) => {
@@ -885,61 +846,22 @@ impl Element for MarkdownElement {
                             builder.pop_text_style();
                         }
 
-                        if self.markdown.read(cx).options.copy_code_block_buttons {
+                        if matches!(
+                            &self.code_block_renderer,
+                            CodeBlockRenderer::Default { copy_button: true }
+                        ) {
                             builder.flush_text();
                             builder.modify_current_div(|el| {
-                                let id =
-                                    ElementId::NamedInteger("copy-markdown-code".into(), range.end);
-                                let was_copied =
-                                    self.markdown.read(cx).copied_code_blocks.contains(&id);
-                                let copy_button = div().absolute().top_1().right_1().w_5().child(
-                                    IconButton::new(
-                                        id.clone(),
-                                        if was_copied {
-                                            IconName::Check
-                                        } else {
-                                            IconName::Copy
-                                        },
-                                    )
-                                    .icon_color(Color::Muted)
-                                    .shape(ui::IconButtonShape::Square)
-                                    .tooltip(Tooltip::text("Copy Code"))
-                                    .on_click({
-                                        let id = id.clone();
-                                        let markdown = self.markdown.clone();
-                                        let code = without_fences(
-                                            parsed_markdown.source()[range.clone()].trim(),
-                                        )
+                                let code =
+                                    without_fences(parsed_markdown.source()[range.clone()].trim())
                                         .to_string();
-                                        move |_event, _window, cx| {
-                                            let id = id.clone();
-                                            markdown.update(cx, |this, cx| {
-                                                this.copied_code_blocks.insert(id.clone());
-
-                                                cx.write_to_clipboard(ClipboardItem::new_string(
-                                                    code.clone(),
-                                                ));
-
-                                                cx.spawn(async move |this, cx| {
-                                                    cx.background_executor()
-                                                        .timer(Duration::from_secs(2))
-                                                        .await;
-
-                                                    cx.update(|cx| {
-                                                        this.update(cx, |this, cx| {
-                                                            this.copied_code_blocks.remove(&id);
-                                                            cx.notify();
-                                                        })
-                                                    })
-                                                    .ok();
-                                                })
-                                                .detach();
-                                            });
-                                        }
-                                    }),
+                                let codeblock = render_copy_code_block_button(
+                                    range.end,
+                                    code,
+                                    self.markdown.clone(),
+                                    cx,
                                 );
-
-                                el.child(copy_button)
+                                el.child(div().absolute().top_1().right_1().w_5().child(codeblock))
                             });
                         }
 
@@ -1073,6 +995,52 @@ impl Element for MarkdownElement {
     }
 }
 
+fn render_copy_code_block_button(
+    id: usize,
+    code: String,
+    markdown: Entity<Markdown>,
+    cx: &App,
+) -> impl IntoElement {
+    let id = ElementId::NamedInteger("copy-markdown-code".into(), id);
+    let was_copied = markdown.read(cx).copied_code_blocks.contains(&id);
+    IconButton::new(
+        id.clone(),
+        if was_copied {
+            IconName::Check
+        } else {
+            IconName::Copy
+        },
+    )
+    .icon_color(Color::Muted)
+    .shape(ui::IconButtonShape::Square)
+    .tooltip(Tooltip::text("Copy Code"))
+    .on_click({
+        let id = id.clone();
+        let markdown = markdown.clone();
+        move |_event, _window, cx| {
+            let id = id.clone();
+            markdown.update(cx, |this, cx| {
+                this.copied_code_blocks.insert(id.clone());
+
+                cx.write_to_clipboard(ClipboardItem::new_string(code.clone()));
+
+                cx.spawn(async move |this, cx| {
+                    cx.background_executor().timer(Duration::from_secs(2)).await;
+
+                    cx.update(|cx| {
+                        this.update(cx, |this, cx| {
+                            this.copied_code_blocks.remove(&id);
+                            cx.notify();
+                        })
+                    })
+                    .ok();
+                })
+                .detach();
+            });
+        }
+    })
+}
+
 impl IntoElement for MarkdownElement {
     type Element = Self;
 
@@ -1529,7 +1497,7 @@ impl RenderedText {
 /// Some markdown blocks are indented, and others have e.g. ```rust … ``` around them.
 /// If this block is fenced with backticks, strip them off (and the language name).
 /// We use this when copying code blocks to the clipboard.
-fn without_fences(mut markdown: &str) -> &str {
+pub fn without_fences(mut markdown: &str) -> &str {
     if let Some(opening_backticks) = markdown.find("```") {
         markdown = &markdown[opening_backticks..];
 

crates/markdown/src/parser.rs 🔗

@@ -7,10 +7,11 @@ use pulldown_cmark::{
 use std::{
     collections::HashSet,
     ops::{Deref, Range},
-    path::PathBuf,
+    path::Path,
+    sync::Arc,
 };
 
-use crate::path_range::PathRange;
+use crate::path_range::PathWithRange;
 
 const PARSE_OPTIONS: Options = Options::ENABLE_TABLES
     .union(Options::ENABLE_FOOTNOTES)
@@ -27,7 +28,7 @@ pub fn parse_markdown(
 ) -> (
     Vec<(Range<usize>, MarkdownEvent)>,
     HashSet<SharedString>,
-    HashSet<PathBuf>,
+    HashSet<Arc<Path>>,
 ) {
     let mut events = Vec::new();
     let mut language_names = HashSet::new();
@@ -73,7 +74,7 @@ pub fn parse_markdown(
                             // Languages should never contain a slash, and PathRanges always should.
                             // (Models are told to specify them relative to a workspace root.)
                         } else if info.contains('/') {
-                            let path_range = PathRange::new(info);
+                            let path_range = PathWithRange::new(info);
                             language_paths.insert(path_range.path.clone());
                             CodeBlockKind::FencedSrc(path_range)
                         } else {
@@ -332,7 +333,7 @@ pub enum CodeBlockKind {
     /// e.g. ```path/to/foo.rs#L123-456 instead of ```rust
     Fenced,
     FencedLang(SharedString),
-    FencedSrc(PathRange),
+    FencedSrc(PathWithRange),
 }
 
 impl From<pulldown_cmark::Tag<'_>> for MarkdownTag {
@@ -378,7 +379,7 @@ impl From<pulldown_cmark::Tag<'_>> for MarkdownTag {
                     } else if info.contains('/') {
                         // Languages should never contain a slash, and PathRanges always should.
                         // (Models are told to specify them relative to a workspace root.)
-                        CodeBlockKind::FencedSrc(PathRange::new(info))
+                        CodeBlockKind::FencedSrc(PathWithRange::new(info))
                     } else {
                         CodeBlockKind::FencedLang(SharedString::from(info.to_string()))
                     })

crates/markdown/src/path_range.rs 🔗

@@ -1,8 +1,8 @@
-use std::{ops::Range, path::PathBuf};
+use std::{ops::Range, path::Path, sync::Arc};
 
 #[derive(Debug, Clone, PartialEq)]
-pub struct PathRange {
-    pub path: PathBuf,
+pub struct PathWithRange {
+    pub path: Arc<Path>,
     pub range: Option<Range<LineCol>>,
 }
 
@@ -31,7 +31,7 @@ impl LineCol {
     }
 }
 
-impl PathRange {
+impl PathWithRange {
     pub fn new(str: impl AsRef<str>) -> Self {
         let str = str.as_ref();
         // Sometimes the model will include a language at the start,
@@ -55,12 +55,12 @@ impl PathRange {
                 };
 
                 Self {
-                    path: PathBuf::from(path),
+                    path: Path::new(path).into(),
                     range,
                 }
             }
             None => Self {
-                path: str.into(),
+                path: Path::new(str).into(),
                 range: None,
             },
         }
@@ -99,8 +99,8 @@ mod tests {
 
     #[test]
     fn test_pathrange_parsing() {
-        let path_range = PathRange::new("file.rs#L10-L20");
-        assert_eq!(path_range.path, PathBuf::from("file.rs"));
+        let path_range = PathWithRange::new("file.rs#L10-L20");
+        assert_eq!(path_range.path.as_ref(), Path::new("file.rs"));
         assert!(path_range.range.is_some());
         if let Some(range) = path_range.range {
             assert_eq!(range.start.line, 10);
@@ -109,78 +109,78 @@ mod tests {
             assert_eq!(range.end.col, None);
         }
 
-        let single_line = PathRange::new("file.rs#L15");
-        assert_eq!(single_line.path, PathBuf::from("file.rs"));
+        let single_line = PathWithRange::new("file.rs#L15");
+        assert_eq!(single_line.path.as_ref(), Path::new("file.rs"));
         assert!(single_line.range.is_some());
         if let Some(range) = single_line.range {
             assert_eq!(range.start.line, 15);
             assert_eq!(range.end.line, 15);
         }
 
-        let no_range = PathRange::new("file.rs");
-        assert_eq!(no_range.path, PathBuf::from("file.rs"));
+        let no_range = PathWithRange::new("file.rs");
+        assert_eq!(no_range.path.as_ref(), Path::new("file.rs"));
         assert!(no_range.range.is_none());
 
-        let lowercase = PathRange::new("file.rs#l5-l10");
-        assert_eq!(lowercase.path, PathBuf::from("file.rs"));
+        let lowercase = PathWithRange::new("file.rs#l5-l10");
+        assert_eq!(lowercase.path.as_ref(), Path::new("file.rs"));
         assert!(lowercase.range.is_some());
         if let Some(range) = lowercase.range {
             assert_eq!(range.start.line, 5);
             assert_eq!(range.end.line, 10);
         }
 
-        let complex = PathRange::new("src/path/to/file.rs#L100");
-        assert_eq!(complex.path, PathBuf::from("src/path/to/file.rs"));
+        let complex = PathWithRange::new("src/path/to/file.rs#L100");
+        assert_eq!(complex.path.as_ref(), Path::new("src/path/to/file.rs"));
         assert!(complex.range.is_some());
     }
 
     #[test]
     fn test_pathrange_from_str() {
-        let with_range = PathRange::new("file.rs#L10-L20");
+        let with_range = PathWithRange::new("file.rs#L10-L20");
         assert!(with_range.range.is_some());
-        assert_eq!(with_range.path, PathBuf::from("file.rs"));
+        assert_eq!(with_range.path.as_ref(), Path::new("file.rs"));
 
-        let without_range = PathRange::new("file.rs");
+        let without_range = PathWithRange::new("file.rs");
         assert!(without_range.range.is_none());
 
-        let single_line = PathRange::new("file.rs#L15");
+        let single_line = PathWithRange::new("file.rs#L15");
         assert!(single_line.range.is_some());
     }
 
     #[test]
     fn test_pathrange_leading_text_trimming() {
-        let with_language = PathRange::new("```rust file.rs#L10");
-        assert_eq!(with_language.path, PathBuf::from("file.rs"));
+        let with_language = PathWithRange::new("```rust file.rs#L10");
+        assert_eq!(with_language.path.as_ref(), Path::new("file.rs"));
         assert!(with_language.range.is_some());
         if let Some(range) = with_language.range {
             assert_eq!(range.start.line, 10);
         }
 
-        let with_spaces = PathRange::new("```    file.rs#L10-L20");
-        assert_eq!(with_spaces.path, PathBuf::from("file.rs"));
+        let with_spaces = PathWithRange::new("```    file.rs#L10-L20");
+        assert_eq!(with_spaces.path.as_ref(), Path::new("file.rs"));
         assert!(with_spaces.range.is_some());
 
-        let with_words = PathRange::new("```rust code example file.rs#L15:10");
-        assert_eq!(with_words.path, PathBuf::from("file.rs"));
+        let with_words = PathWithRange::new("```rust code example file.rs#L15:10");
+        assert_eq!(with_words.path.as_ref(), Path::new("file.rs"));
         assert!(with_words.range.is_some());
         if let Some(range) = with_words.range {
             assert_eq!(range.start.line, 15);
             assert_eq!(range.start.col, Some(10));
         }
 
-        let with_whitespace = PathRange::new("  file.rs#L5");
-        assert_eq!(with_whitespace.path, PathBuf::from("file.rs"));
+        let with_whitespace = PathWithRange::new("  file.rs#L5");
+        assert_eq!(with_whitespace.path.as_ref(), Path::new("file.rs"));
         assert!(with_whitespace.range.is_some());
 
-        let no_leading = PathRange::new("file.rs#L10");
-        assert_eq!(no_leading.path, PathBuf::from("file.rs"));
+        let no_leading = PathWithRange::new("file.rs#L10");
+        assert_eq!(no_leading.path.as_ref(), Path::new("file.rs"));
         assert!(no_leading.range.is_some());
     }
 
     #[test]
     fn test_pathrange_with_line_and_column() {
-        let line_and_col = PathRange::new("file.rs#L10:5");
-        assert_eq!(line_and_col.path, PathBuf::from("file.rs"));
+        let line_and_col = PathWithRange::new("file.rs#L10:5");
+        assert_eq!(line_and_col.path.as_ref(), Path::new("file.rs"));
         assert!(line_and_col.range.is_some());
         if let Some(range) = line_and_col.range {
             assert_eq!(range.start.line, 10);
@@ -189,8 +189,8 @@ mod tests {
             assert_eq!(range.end.col, Some(5));
         }
 
-        let full_range = PathRange::new("file.rs#L10:5-L20:15");
-        assert_eq!(full_range.path, PathBuf::from("file.rs"));
+        let full_range = PathWithRange::new("file.rs#L10:5-L20:15");
+        assert_eq!(full_range.path.as_ref(), Path::new("file.rs"));
         assert!(full_range.range.is_some());
         if let Some(range) = full_range.range {
             assert_eq!(range.start.line, 10);
@@ -199,8 +199,8 @@ mod tests {
             assert_eq!(range.end.col, Some(15));
         }
 
-        let mixed_range1 = PathRange::new("file.rs#L10:5-L20");
-        assert_eq!(mixed_range1.path, PathBuf::from("file.rs"));
+        let mixed_range1 = PathWithRange::new("file.rs#L10:5-L20");
+        assert_eq!(mixed_range1.path.as_ref(), Path::new("file.rs"));
         assert!(mixed_range1.range.is_some());
         if let Some(range) = mixed_range1.range {
             assert_eq!(range.start.line, 10);
@@ -209,8 +209,8 @@ mod tests {
             assert_eq!(range.end.col, None);
         }
 
-        let mixed_range2 = PathRange::new("file.rs#L10-L20:15");
-        assert_eq!(mixed_range2.path, PathBuf::from("file.rs"));
+        let mixed_range2 = PathWithRange::new("file.rs#L10-L20:15");
+        assert_eq!(mixed_range2.path.as_ref(), Path::new("file.rs"));
         assert!(mixed_range2.range.is_some());
         if let Some(range) = mixed_range2.range {
             assert_eq!(range.start.line, 10);