Skip to main content

markdown/
element.rs

1//! `MarkdownElement` + its render helpers (`push_markdown_*`) + `impl Styled`
2//! + `impl Element for MarkdownElement` + `impl IntoElement` + `AnyDiv`.
3//!
4//! Design notes (this crate cannot depend on `boltz-ui`; see `controls.rs`'s
5//! module docs):
6//! - Mermaid diagram rendering is dropped entirely; fenced ` ```mermaid `
7//!   code blocks render as plain code.
8//! - `ui::{Icon, IconButton, IconSize, Label, Tooltip, Color, ToggleState}`
9//!   have no replacement here beyond small inline helpers built on
10//!   `crate::controls::icon_svg`/`simple_tooltip` + plain `div()` text/colors
11//!   — see `push_markdown_block_quote`, `image_fallback_element`,
12//!   `render_wrap_code_block_button`. `Checkbox` takes a plain `bool` instead
13//!   of `ui::ToggleState`.
14//! - The code-block copy/wrap button row has no `visible_on_hover` helper (a
15//!   `boltz-ui` convenience over `group_hover`); it's reproduced here as an
16//!   explicit `opacity(0.)` + `group_hover(.., |el| el.opacity(1.))` pair.
17//! - Fenced code blocks never wrap in a visible custom scrollbar thumb/track;
18//!   only the inner code content div gets native `overflow_x_scroll` +
19//!   `track_scroll` (see `crate::controls::WithScrollbar`'s module docs for
20//!   why this crate has no visible-scrollbar primitive).
21//! - `CodeBlockRenderer::Custom { render, transform }` is stored but never
22//!   invoked here; `render`/`transform` remain dead fields until a caller
23//!   needs custom code-block rendering.
24
25use std::ops::Range;
26use std::rc::Rc;
27use std::sync::Arc;
28
29use gpui::{
30    AnyElement, App, BorderStyle, Bounds, CursorStyle, DefiniteLength, DispatchPhase, Div, Edges,
31    ElementId, Entity, FontStyle, FontWeight, GlobalElementId, Hitbox, HitboxBehavior, Hsla,
32    ImageSource, InteractiveElement, IntoElement, KeyContext, MouseButton, MouseDownEvent,
33    MouseEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Refineable as _, ScrollHandle,
34    SharedString, Stateful, StatefulInteractiveElement as _, StrikethroughStyle, StyleRefinement,
35    Styled, StyledImage as _, TextAlign, TextStyleRefinement, Window, div, img, point,
36    prelude::FluentBuilder as _, px, quad, rems,
37};
38use icons::IconName;
39use pulldown_cmark::{Alignment, BlockQuoteKind, HeadingLevel};
40use theme::ActiveTheme as _;
41use util::ResultExt as _;
42
43use crate::builder::{MarkdownElementBuilder, MetadataCellStyle, alignment_to_text_align, h_flex};
44use crate::controls::{Checkbox, CopyButton, icon_svg, simple_tooltip};
45use crate::entity::{
46    AutoscrollBehavior, CodeBlockRenderer, Markdown, SelectMode, Selection,
47    resolve_code_block_language,
48};
49use crate::parser::{
50    CodeBlockKind, MarkdownEvent, MarkdownTag, MarkdownTagEnd, ParsedMetadataBlock,
51};
52use crate::rendered::{RenderedMarkdown, RenderedText};
53use crate::style::{CopyButtonVisibility, HeadingLevelStyles, MarkdownStyle, WrapButtonVisibility};
54
55/// A callback that can turn inline code span text into a link destination.
56pub type CodeSpanLinkCallback = Arc<dyn Fn(&str, &App) -> Option<SharedString> + 'static>;
57type SourceClickCallback = Box<dyn Fn(usize, usize, &mut Window, &mut App) -> bool>;
58pub(crate) type CheckboxToggleCallback = Rc<dyn Fn(Range<usize>, bool, &mut Window, &mut App)>;
59
60pub struct MarkdownElement {
61    pub(crate) markdown: Entity<Markdown>,
62    pub(crate) style: MarkdownStyle,
63    code_block_renderer: CodeBlockRenderer,
64    on_url_click: Option<Rc<dyn Fn(SharedString, &mut Window, &mut App)>>,
65    code_span_link: Option<CodeSpanLinkCallback>,
66    on_source_click: Option<SourceClickCallback>,
67    on_checkbox_toggle: Option<CheckboxToggleCallback>,
68    pub(crate) image_resolver: Option<Box<dyn Fn(&str) -> Option<ImageSource>>>,
69    show_root_block_markers: bool,
70    autoscroll: AutoscrollBehavior,
71}
72
73impl MarkdownElement {
74    pub fn new(markdown: Entity<Markdown>, style: MarkdownStyle) -> Self {
75        Self {
76            markdown,
77            style,
78            code_block_renderer: CodeBlockRenderer::Default {
79                copy_button_visibility: CopyButtonVisibility::VisibleOnHover,
80                wrap_button_visibility: WrapButtonVisibility::Hidden,
81                border: false,
82            },
83            on_url_click: None,
84            code_span_link: None,
85            on_source_click: None,
86            on_checkbox_toggle: None,
87            image_resolver: None,
88            show_root_block_markers: false,
89            autoscroll: AutoscrollBehavior::Propagate,
90        }
91    }
92
93    #[cfg(any(test, feature = "test-support"))]
94    pub fn rendered_text(
95        markdown: Entity<Markdown>,
96        cx: &mut gpui::VisualTestContext,
97        style: impl FnOnce(&Window, &App) -> MarkdownStyle,
98    ) -> String {
99        use gpui::size;
100
101        let (text, _) = cx.draw(
102            Default::default(),
103            size(px(600.0), px(600.0)),
104            |window, cx| Self::new(markdown, style(window, cx)),
105        );
106        text.text
107            .lines
108            .iter()
109            .map(|line| line.layout.wrapped_text())
110            .collect::<Vec<_>>()
111            .join("\n")
112    }
113
114    pub fn code_block_renderer(mut self, variant: CodeBlockRenderer) -> Self {
115        self.code_block_renderer = variant;
116        self
117    }
118
119    pub fn on_url_click(
120        mut self,
121        handler: impl Fn(SharedString, &mut Window, &mut App) + 'static,
122    ) -> Self {
123        self.on_url_click = Some(Rc::new(handler));
124        self
125    }
126
127    pub fn on_code_span_link(
128        mut self,
129        callback: impl Fn(&str, &App) -> Option<SharedString> + 'static,
130    ) -> Self {
131        self.code_span_link = Some(Arc::new(callback));
132        self
133    }
134
135    pub fn on_source_click(
136        mut self,
137        handler: impl Fn(usize, usize, &mut Window, &mut App) -> bool + 'static,
138    ) -> Self {
139        self.on_source_click = Some(Box::new(handler));
140        self
141    }
142
143    pub fn on_checkbox_toggle(
144        mut self,
145        handler: impl Fn(Range<usize>, bool, &mut Window, &mut App) + 'static,
146    ) -> Self {
147        self.on_checkbox_toggle = Some(Rc::new(handler));
148        self
149    }
150
151    pub fn image_resolver(
152        mut self,
153        resolver: impl Fn(&str) -> Option<ImageSource> + 'static,
154    ) -> Self {
155        self.image_resolver = Some(Box::new(resolver));
156        self
157    }
158
159    pub fn show_root_block_markers(mut self) -> Self {
160        self.show_root_block_markers = true;
161        self
162    }
163
164    pub fn scroll_handle(mut self, scroll_handle: ScrollHandle) -> Self {
165        self.autoscroll = AutoscrollBehavior::Controlled(scroll_handle);
166        self
167    }
168
169    fn push_markdown_code_span(
170        &self,
171        builder: &mut MarkdownElementBuilder,
172        text: &str,
173        range: Range<usize>,
174        cx: &App,
175    ) {
176        let link_url = if builder.code_block_stack.is_empty()
177            && builder.link_depth == 0
178            && !self.style.prevent_mouse_interaction
179        {
180            self.code_span_link
181                .as_ref()
182                .and_then(|callback| callback(text, cx))
183        } else {
184            None
185        };
186
187        if let Some(url) = link_url {
188            builder.push_link(url.clone(), range.clone());
189            let link_style = self
190                .style
191                .link_callback
192                .as_ref()
193                .and_then(|callback| callback(url.as_ref(), cx))
194                .unwrap_or_else(|| self.style.link.clone());
195            builder.push_text_style(self.style.inline_code.clone());
196            builder.push_text_style(link_style);
197            builder.push_text(text, range);
198            builder.pop_text_style();
199            builder.pop_text_style();
200        } else {
201            let mut code_style = self.style.inline_code.clone();
202            if builder.link_depth > 0 {
203                code_style.color = self.style.link.color.or(code_style.color);
204            }
205            builder.push_text_style(code_style);
206            builder.push_text(text, range);
207            builder.pop_text_style();
208        }
209    }
210
211    pub(crate) fn push_markdown_image(
212        &self,
213        builder: &mut MarkdownElementBuilder,
214        range: &Range<usize>,
215        source: ImageSource,
216        dest_url: SharedString,
217        alt_text: Option<SharedString>,
218        width: Option<DefiniteLength>,
219        height: Option<DefiniteLength>,
220    ) {
221        let enclosing_link_url = (builder.link_depth > 0)
222            .then(|| builder.rendered_links.last())
223            .flatten()
224            .map(|link| link.destination_url.clone());
225        let fallback_opens_image_url = enclosing_link_url.is_none();
226
227        let image_element = {
228            let wrapper = div().id(("markdown-image-link", range.start)).min_w_0();
229            let wrapper = if !self.style.prevent_mouse_interaction
230                && let Some(url) = enclosing_link_url
231            {
232                let click_url = url.clone();
233                let markdown = self.markdown.clone();
234                let url_click = self.on_url_click.clone();
235                wrapper
236                    .cursor_pointer()
237                    .on_click(move |_, window, cx| {
238                        if let Some(ref on_url_click) = url_click {
239                            on_url_click(click_url.clone(), window, cx);
240                        } else {
241                            cx.open_url(&click_url);
242                        }
243                    })
244                    .capture_any_mouse_down(move |event, _window, cx| {
245                        if event.button == MouseButton::Right {
246                            markdown.update(cx, |md, _| {
247                                md.capture_for_context_menu(Some(url.clone()), None)
248                            });
249                        }
250                    })
251            } else {
252                wrapper
253            };
254            wrapper.child(
255                img(source)
256                    .id(("markdown-image", range.start))
257                    .min_w_0()
258                    .max_w_full()
259                    .rounded_md()
260                    .mr_1()
261                    .mb_1()
262                    .when_some(height, |this, height| this.h(height))
263                    .when_some(width, |this, width| this.w(width))
264                    .with_fallback(move || {
265                        image_fallback_element(
266                            dest_url.clone(),
267                            alt_text.clone(),
268                            fallback_opens_image_url,
269                        )
270                    }),
271            )
272        };
273
274        builder.push_image_child(image_element);
275    }
276
277    pub(crate) fn push_markdown_paragraph(
278        &self,
279        builder: &mut MarkdownElementBuilder,
280        range: &Range<usize>,
281        markdown_end: usize,
282        text_align_override: Option<TextAlign>,
283    ) {
284        let align = text_align_override.unwrap_or(self.style.base_text_style.text_align);
285        let mut paragraph = div().when(!self.style.height_is_multiple_of_line_height, |el| {
286            el.mb_2().line_height(rems(1.3))
287        });
288
289        paragraph = match align {
290            TextAlign::Center => paragraph.text_center(),
291            TextAlign::Left => paragraph.text_left(),
292            TextAlign::Right => paragraph.text_right(),
293        };
294
295        builder.push_text_style(TextStyleRefinement {
296            text_align: Some(align),
297            ..Default::default()
298        });
299        builder.push_div(paragraph, range, markdown_end);
300    }
301
302    pub(crate) fn pop_markdown_paragraph(&self, builder: &mut MarkdownElementBuilder) {
303        builder.pop_div();
304        builder.pop_text_style();
305    }
306
307    pub(crate) fn push_markdown_heading(
308        &self,
309        builder: &mut MarkdownElementBuilder,
310        level: HeadingLevel,
311        range: &Range<usize>,
312        markdown_end: usize,
313        text_align_override: Option<TextAlign>,
314    ) {
315        let align = text_align_override.unwrap_or(self.style.base_text_style.text_align);
316        let mut heading = div().mt_4().mb_2();
317        heading = apply_heading_style(
318            heading,
319            level,
320            self.style.heading_level_styles.as_ref(),
321            self.style.heading_border_color,
322        );
323
324        heading = match align {
325            TextAlign::Center => heading.text_center(),
326            TextAlign::Left => heading.text_left(),
327            TextAlign::Right => heading.text_right(),
328        };
329
330        let heading_style = self.style.heading.clone();
331        let heading_text_style = heading_style.text.clone();
332        heading.style().refine(&heading_style);
333
334        builder.push_text_style(TextStyleRefinement {
335            text_align: Some(align),
336            ..heading_text_style
337        });
338        builder.push_div(heading, range, markdown_end);
339    }
340
341    pub(crate) fn pop_markdown_heading(&self, builder: &mut MarkdownElementBuilder) {
342        builder.pop_div();
343        builder.pop_text_style();
344    }
345
346    pub(crate) fn push_markdown_block_quote(
347        &self,
348        builder: &mut MarkdownElementBuilder,
349        kind: Option<BlockQuoteKind>,
350        range: &Range<usize>,
351        markdown_end: usize,
352    ) {
353        let border_color = self
354            .style
355            .block_quote_kind_colors
356            .for_kind(kind, self.style.block_quote_border_color);
357
358        let header = kind.map(|kind| {
359            let (icon_name, label) = match kind {
360                BlockQuoteKind::Note => (IconName::Info, "Note"),
361                BlockQuoteKind::Tip => (IconName::Sparkle, "Tip"),
362                BlockQuoteKind::Important => (IconName::Chat, "Important"),
363                BlockQuoteKind::Warning => (IconName::Warning, "Warning"),
364                BlockQuoteKind::Caution => (IconName::Stop, "Caution"),
365            };
366            h_flex()
367                .gap_1()
368                .mb_1()
369                .child(icon_svg(icon_name, px(14.), border_color))
370                .child(
371                    div()
372                        .text_color(border_color)
373                        .font_weight(FontWeight::BOLD)
374                        .child(label),
375                )
376                .into_any_element()
377        });
378
379        let block_div = div().pl_4().mb_2().border_l_4().border_color(border_color);
380        let block_div = match header {
381            Some(header) => block_div.child(header),
382            None => block_div,
383        };
384
385        builder.push_text_style(self.style.block_quote.clone());
386        builder.push_div(block_div, range, markdown_end);
387    }
388
389    pub(crate) fn pop_markdown_block_quote(&self, builder: &mut MarkdownElementBuilder) {
390        builder.pop_div();
391        builder.pop_text_style();
392    }
393
394    fn push_metadata_block(
395        &self,
396        builder: &mut MarkdownElementBuilder,
397        source: &str,
398        metadata_block: &ParsedMetadataBlock,
399        markdown_end: usize,
400        cx: &App,
401    ) {
402        let content_range = &metadata_block.content_range;
403        if let Some(rows) = metadata_block.rows.as_deref() {
404            builder.push_div(
405                div()
406                    .grid()
407                    .grid_cols(2)
408                    .w_full()
409                    .mb_2()
410                    .border_1()
411                    .border_color(cx.theme().colors().border)
412                    .rounded_sm()
413                    .overflow_hidden(),
414                content_range,
415                markdown_end,
416            );
417
418            for (row_index, row) in rows.iter().enumerate() {
419                self.push_metadata_cell(
420                    builder,
421                    source,
422                    row.key.clone(),
423                    content_range,
424                    markdown_end,
425                    MetadataCellStyle {
426                        row_index,
427                        is_key: true,
428                    },
429                    cx,
430                );
431                self.push_metadata_cell(
432                    builder,
433                    source,
434                    row.value.clone(),
435                    content_range,
436                    markdown_end,
437                    MetadataCellStyle {
438                        row_index,
439                        is_key: false,
440                    },
441                    cx,
442                );
443            }
444
445            builder.pop_div();
446        } else {
447            let mut metadata_block = div().w_full().rounded_md();
448            metadata_block.style().refine(&self.style.code_block);
449            builder.push_text_style(self.style.code_block.text.to_owned());
450            builder.push_code_block(None);
451            builder.push_div(metadata_block, content_range, markdown_end);
452            builder.push_text(&source[content_range.clone()], content_range.clone());
453            builder.trim_trailing_newline();
454            builder.pop_div();
455            builder.pop_code_block();
456            builder.pop_text_style();
457        }
458    }
459
460    fn push_metadata_cell(
461        &self,
462        builder: &mut MarkdownElementBuilder,
463        source: &str,
464        text_range: Range<usize>,
465        block_range: &Range<usize>,
466        markdown_end: usize,
467        cell_style: MetadataCellStyle,
468        cx: &App,
469    ) {
470        builder.push_div(
471            div()
472                .flex()
473                .flex_col()
474                .min_w_0()
475                .px_2()
476                .py_1()
477                .border_color(cx.theme().colors().border)
478                .when(cell_style.row_index > 0, |this| this.border_t_1())
479                .when(!cell_style.is_key, |this| this.border_l_1())
480                .when(cell_style.is_key, |this| {
481                    this.bg(cx.theme().colors().panel_background)
482                }),
483            block_range,
484            markdown_end,
485        );
486
487        let text_style = if cell_style.is_key {
488            TextStyleRefinement {
489                color: Some(cx.theme().colors().text_muted),
490                font_weight: Some(FontWeight::SEMIBOLD),
491                ..Default::default()
492            }
493        } else {
494            TextStyleRefinement::default()
495        };
496        builder.push_text_style(text_style);
497        builder.push_text(&source[text_range.clone()], text_range);
498        builder.pop_text_style();
499        builder.pop_div();
500    }
501
502    pub(crate) fn push_markdown_list_item(
503        &self,
504        builder: &mut MarkdownElementBuilder,
505        bullet: AnyElement,
506        range: &Range<usize>,
507        markdown_end: usize,
508    ) {
509        builder.push_div(
510            div()
511                .when(!self.style.height_is_multiple_of_line_height, |el| {
512                    el.mb_1().gap_1().line_height(rems(1.3))
513                })
514                .flex()
515                .flex_row()
516                .items_start()
517                .child(bullet),
518            range,
519            markdown_end,
520        );
521        // Without `w_0`, text doesn't wrap to the width of the container.
522        builder.push_div(div().flex_1().w_0(), range, markdown_end);
523    }
524
525    pub(crate) fn pop_markdown_list_item(&self, builder: &mut MarkdownElementBuilder) {
526        builder.pop_div();
527        builder.pop_div();
528    }
529
530    fn paint_highlight_range(
531        start: usize,
532        end: usize,
533        color: Hsla,
534        rendered_text: &RenderedText,
535        window: &mut Window,
536    ) {
537        for bounds in rendered_text.bounds_for_source_range(start..end) {
538            window.paint_quad(quad(
539                bounds,
540                Pixels::ZERO,
541                color,
542                Edges::default(),
543                Hsla::transparent_black(),
544                BorderStyle::default(),
545            ));
546        }
547    }
548
549    fn paint_selection(&self, rendered_text: &RenderedText, window: &mut Window, cx: &mut App) {
550        let selection = self.markdown.read(cx).selection.clone();
551        Self::paint_highlight_range(
552            selection.start,
553            selection.end,
554            self.style.selection_background_color,
555            rendered_text,
556            window,
557        );
558    }
559
560    fn paint_search_highlights(
561        &self,
562        rendered_text: &RenderedText,
563        window: &mut Window,
564        cx: &mut App,
565    ) {
566        let markdown = self.markdown.read(cx);
567        let active_index = markdown.active_search_highlight;
568        let colors = cx.theme().colors();
569
570        let highlight_bounds = rendered_text.bounds_for_sorted_source_ranges(
571            markdown
572                .search_highlights
573                .iter()
574                .enumerate()
575                .map(|(ix, range)| (ix, range.clone())),
576        );
577        for (highlight_ix, bounds) in highlight_bounds {
578            let color = if Some(highlight_ix) == active_index {
579                colors.search_active_match_background
580            } else {
581                colors.search_match_background
582            };
583            window.paint_quad(quad(
584                bounds,
585                Pixels::ZERO,
586                color,
587                Edges::default(),
588                Hsla::transparent_black(),
589                BorderStyle::default(),
590            ));
591        }
592    }
593
594    fn paint_mouse_listeners(
595        &mut self,
596        hitbox: &Hitbox,
597        rendered_text: &RenderedText,
598        window: &mut Window,
599        cx: &mut App,
600    ) {
601        if self.style.prevent_mouse_interaction {
602            return;
603        }
604
605        let is_hovering_clickable = hitbox.is_hovered(window)
606            && !self.markdown.read(cx).selection.pending
607            && rendered_text
608                .source_index_for_position(window.mouse_position())
609                .ok()
610                .is_some_and(|source_index| {
611                    rendered_text.link_for_source_index(source_index).is_some()
612                        || rendered_text
613                            .footnote_ref_for_source_index(source_index)
614                            .is_some()
615                });
616
617        if is_hovering_clickable {
618            window.set_cursor_style(CursorStyle::PointingHand, hitbox);
619        } else {
620            window.set_cursor_style(CursorStyle::IBeam, hitbox);
621        }
622
623        let on_open_url = self.on_url_click.take();
624        let on_source_click = self.on_source_click.take();
625
626        self.on_mouse_event(window, cx, {
627            let hitbox = hitbox.clone();
628            let rendered_text = rendered_text.clone();
629            move |markdown, event: &MouseDownEvent, phase, window, _cx| {
630                if phase.capture()
631                    && event.button == MouseButton::Right
632                    && hitbox.is_hovered(window)
633                {
634                    let link = rendered_text
635                        .source_index_for_position(event.position)
636                        .ok()
637                        .and_then(|ix| rendered_text.link_for_source_index(ix))
638                        .map(|link| link.destination_url.clone());
639                    markdown.capture_for_context_menu(link, Some(&rendered_text));
640                }
641            }
642        });
643
644        self.on_mouse_event(window, cx, {
645            let rendered_text = rendered_text.clone();
646            let hitbox = hitbox.clone();
647            move |markdown, event: &MouseDownEvent, phase, window, cx| {
648                if hitbox.is_hovered(window) {
649                    if phase.bubble() && event.button != MouseButton::Right {
650                        let position_result =
651                            rendered_text.source_index_for_position(event.position);
652
653                        if let Ok(source_index) = position_result {
654                            if let Some(footnote_ref) =
655                                rendered_text.footnote_ref_for_source_index(source_index)
656                            {
657                                markdown.pressed_footnote_ref = Some(footnote_ref.clone());
658                            } else if let Some(link) =
659                                rendered_text.link_for_source_index(source_index)
660                            {
661                                markdown.pressed_link = Some(link.clone());
662                            }
663                        }
664
665                        if markdown.pressed_footnote_ref.is_none()
666                            && markdown.pressed_link.is_none()
667                        {
668                            let source_index = match position_result {
669                                Ok(ix) | Err(ix) => ix,
670                            };
671                            if let Some(handler) = on_source_click.as_ref() {
672                                let blocked = handler(source_index, event.click_count, window, cx);
673                                if blocked {
674                                    markdown.selection = Selection::default();
675                                    markdown.pressed_link = None;
676                                    window.prevent_default();
677                                    cx.notify();
678                                    return;
679                                }
680                            }
681                            let (range, mode, reversed) = match event.click_count {
682                                1 if event.modifiers.shift => {
683                                    let tail = markdown.selection.tail();
684                                    let reversed = source_index < tail;
685                                    let range = if reversed {
686                                        source_index..tail
687                                    } else {
688                                        tail..source_index
689                                    };
690                                    (range, SelectMode::Character, reversed)
691                                }
692                                1 => {
693                                    let range = source_index..source_index;
694                                    (range, SelectMode::Character, false)
695                                }
696                                2 => {
697                                    let range = rendered_text.surrounding_word_range(source_index);
698                                    (range.clone(), SelectMode::Word(range), false)
699                                }
700                                3 => {
701                                    let range = rendered_text.surrounding_line_range(source_index);
702                                    (range.clone(), SelectMode::Line(range), false)
703                                }
704                                _ => {
705                                    let range = 0..rendered_text
706                                        .lines
707                                        .last()
708                                        .map(|line| line.source_end)
709                                        .unwrap_or(0);
710                                    (range, SelectMode::All, false)
711                                }
712                            };
713                            markdown.selection = Selection {
714                                start: range.start,
715                                end: range.end,
716                                reversed,
717                                pending: true,
718                                mode,
719                            };
720                            window.focus(&markdown.focus_handle, cx);
721                        }
722
723                        window.prevent_default();
724                        cx.notify();
725                    }
726                } else if phase.capture() && event.button == MouseButton::Left {
727                    markdown.selection = Selection::default();
728                    markdown.pressed_link = None;
729                    cx.notify();
730                }
731            }
732        });
733        self.on_mouse_event(window, cx, {
734            let rendered_text = rendered_text.clone();
735            let hitbox = hitbox.clone();
736            let was_hovering_clickable = is_hovering_clickable;
737            move |markdown, event: &MouseMoveEvent, phase, window, cx| {
738                if phase.capture() {
739                    return;
740                }
741
742                if markdown.selection.pending {
743                    let source_index = match rendered_text.source_index_for_position(event.position)
744                    {
745                        Ok(ix) | Err(ix) => ix,
746                    };
747                    markdown.selection.set_head(source_index, &rendered_text);
748                    markdown.autoscroll_code_block(source_index, event.position);
749                    markdown.autoscroll_request = Some(source_index);
750                    cx.notify();
751                } else {
752                    let is_hovering_clickable = hitbox.is_hovered(window)
753                        && rendered_text
754                            .source_index_for_position(event.position)
755                            .ok()
756                            .is_some_and(|source_index| {
757                                rendered_text.link_for_source_index(source_index).is_some()
758                                    || rendered_text
759                                        .footnote_ref_for_source_index(source_index)
760                                        .is_some()
761                            });
762                    if is_hovering_clickable != was_hovering_clickable {
763                        cx.notify();
764                    }
765                }
766            }
767        });
768        self.on_mouse_event(window, cx, {
769            let rendered_text = rendered_text.clone();
770            move |markdown, event: &MouseUpEvent, phase, window, cx| {
771                if phase.bubble() {
772                    let source_index = rendered_text.source_index_for_position(event.position).ok();
773                    if let Some(pressed_footnote_ref) = markdown.pressed_footnote_ref.take()
774                        && source_index
775                            .and_then(|ix| rendered_text.footnote_ref_for_source_index(ix))
776                            == Some(&pressed_footnote_ref)
777                    {
778                        if let Some(source_index) =
779                            markdown.footnote_definition_content_start(&pressed_footnote_ref.label)
780                        {
781                            markdown.autoscroll_request = Some(source_index);
782                            cx.notify();
783                        }
784                    } else if let Some(pressed_link) = markdown.pressed_link.take()
785                        && source_index.and_then(|ix| rendered_text.link_for_source_index(ix))
786                            == Some(&pressed_link)
787                    {
788                        if let Some(open_url) = on_open_url.as_ref() {
789                            open_url(pressed_link.destination_url, window, cx);
790                        } else {
791                            cx.open_url(&pressed_link.destination_url);
792                        }
793                    }
794                } else if markdown.selection.pending {
795                    markdown.selection.pending = false;
796                    #[cfg(any(target_os = "linux", target_os = "freebsd"))]
797                    {
798                        let text = rendered_text
799                            .text_for_range(markdown.selection.start..markdown.selection.end);
800                        cx.write_to_primary(gpui::ClipboardItem::new_string(text))
801                    }
802                    cx.notify();
803                }
804            }
805        });
806    }
807
808    fn autoscroll(
809        &self,
810        rendered_text: &RenderedText,
811        window: &mut Window,
812        cx: &mut App,
813    ) -> Option<()> {
814        let autoscroll_index = self
815            .markdown
816            .update(cx, |markdown, _| markdown.autoscroll_request.take())?;
817        let (position, line_height) = rendered_text.position_for_source_index(autoscroll_index)?;
818
819        match &self.autoscroll {
820            AutoscrollBehavior::Controlled(scroll_handle) => {
821                let viewport = scroll_handle.bounds();
822                let margin = line_height * 3.;
823                let top_goal = viewport.top() + margin;
824                let bottom_goal = viewport.bottom() - margin;
825                let current_offset = scroll_handle.offset();
826
827                let new_offset_y = if position.y < top_goal {
828                    current_offset.y + (top_goal - position.y)
829                } else if position.y + line_height > bottom_goal {
830                    current_offset.y + (bottom_goal - (position.y + line_height))
831                } else {
832                    current_offset.y
833                };
834
835                scroll_handle.set_offset(point(
836                    current_offset.x,
837                    new_offset_y.clamp(-scroll_handle.max_offset().y, Pixels::ZERO),
838                ));
839            }
840            AutoscrollBehavior::Propagate => {
841                let text_style = self.style.base_text_style.clone();
842                let font_id = window.text_system().resolve_font(&text_style.font());
843                let font_size = text_style.font_size.to_pixels(window.rem_size());
844                let em_width = window.text_system().em_width(font_id, font_size).unwrap();
845                window.request_autoscroll(Bounds::from_corners(
846                    point(position.x - 3. * em_width, position.y - 3. * line_height),
847                    point(position.x + 3. * em_width, position.y + 3. * line_height),
848                ));
849            }
850        }
851        Some(())
852    }
853
854    fn on_mouse_event<T: MouseEvent>(
855        &self,
856        window: &mut Window,
857        _cx: &mut App,
858        mut f: impl 'static
859        + FnMut(&mut Markdown, &T, DispatchPhase, &mut Window, &mut gpui::Context<Markdown>),
860    ) {
861        window.on_mouse_event({
862            let markdown = self.markdown.downgrade();
863            move |event, phase, window, cx| {
864                markdown
865                    .update(cx, |markdown, cx| f(markdown, event, phase, window, cx))
866                    .log_err();
867            }
868        });
869    }
870}
871
872impl Styled for MarkdownElement {
873    fn style(&mut self) -> &mut StyleRefinement {
874        &mut self.style.container_style
875    }
876}
877
878impl gpui::Element for MarkdownElement {
879    type RequestLayoutState = RenderedMarkdown;
880    type PrepaintState = Hitbox;
881
882    fn id(&self) -> Option<ElementId> {
883        None
884    }
885
886    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
887        None
888    }
889
890    fn request_layout(
891        &mut self,
892        _id: Option<&GlobalElementId>,
893        _inspector_id: Option<&gpui::InspectorElementId>,
894        window: &mut Window,
895        cx: &mut App,
896    ) -> (gpui::LayoutId, Self::RequestLayoutState) {
897        let mut builder = MarkdownElementBuilder::new(
898            &self.style.container_style,
899            self.style.base_text_style.clone(),
900            self.style.syntax.clone(),
901        );
902        let (parsed_markdown, images, active_root_block) = {
903            let markdown = self.markdown.read(cx);
904            (
905                markdown.parsed_markdown.clone(),
906                markdown.images_by_source_offset.clone(),
907                markdown.active_root_block(),
908            )
909        };
910        let markdown_end = if let Some(last) = parsed_markdown.events.last() {
911            last.0.end
912        } else {
913            0
914        };
915        let mut code_block_ids = collections::HashSet::default();
916
917        let mut current_img_block_range: Option<Range<usize>> = None;
918        let mut handled_html_block = false;
919        let mut rendered_metadata_block = false;
920        for (index, (range, event)) in parsed_markdown.events.iter().enumerate() {
921            // Skip alt text for images that rendered
922            if let Some(current_img_block_range) = &current_img_block_range
923                && current_img_block_range.end > range.end
924            {
925                continue;
926            }
927
928            if handled_html_block {
929                if let MarkdownEvent::End(MarkdownTagEnd::HtmlBlock) = event {
930                    handled_html_block = false;
931                } else {
932                    continue;
933                }
934            }
935
936            if rendered_metadata_block {
937                if matches!(event, MarkdownEvent::End(MarkdownTagEnd::MetadataBlock(_))) {
938                    rendered_metadata_block = false;
939                }
940                continue;
941            }
942
943            match event {
944                MarkdownEvent::RootStart => {
945                    if self.show_root_block_markers {
946                        builder.push_root_block(range, markdown_end);
947                    }
948                }
949                MarkdownEvent::RootEnd(root_block_index) => {
950                    if self.show_root_block_markers {
951                        builder.pop_root_block(
952                            active_root_block == Some(*root_block_index),
953                            cx.theme().colors().border,
954                            cx.theme().colors().border_variant,
955                        );
956                    }
957                }
958                MarkdownEvent::Start(tag) => {
959                    match tag {
960                        MarkdownTag::Image { dest_url, .. } => {
961                            let alt_text = collect_image_alt_text(
962                                &parsed_markdown.events[index..],
963                                &parsed_markdown.source,
964                            );
965                            if let Some(image) = images.get(&range.start) {
966                                current_img_block_range = Some(range.clone());
967                                self.push_markdown_image(
968                                    &mut builder,
969                                    range,
970                                    image.clone().into(),
971                                    dest_url.clone(),
972                                    alt_text,
973                                    None,
974                                    None,
975                                );
976                            } else if let Some(source) = self
977                                .image_resolver
978                                .as_ref()
979                                .and_then(|resolve| resolve(dest_url.as_ref()))
980                            {
981                                current_img_block_range = Some(range.clone());
982                                self.push_markdown_image(
983                                    &mut builder,
984                                    range,
985                                    source,
986                                    dest_url.clone(),
987                                    alt_text,
988                                    None,
989                                    None,
990                                );
991                            }
992                        }
993                        MarkdownTag::Paragraph => {
994                            let text_align_override = builder
995                                .table
996                                .current_cell_alignment()
997                                .and_then(alignment_to_text_align);
998                            self.push_markdown_paragraph(
999                                &mut builder,
1000                                range,
1001                                markdown_end,
1002                                text_align_override,
1003                            );
1004                        }
1005                        MarkdownTag::Heading { level, .. } => {
1006                            let text_align_override = builder
1007                                .table
1008                                .current_cell_alignment()
1009                                .and_then(alignment_to_text_align);
1010                            self.push_markdown_heading(
1011                                &mut builder,
1012                                *level,
1013                                range,
1014                                markdown_end,
1015                                text_align_override,
1016                            );
1017                        }
1018                        MarkdownTag::BlockQuote(kind) => {
1019                            self.push_markdown_block_quote(
1020                                &mut builder,
1021                                *kind,
1022                                range,
1023                                markdown_end,
1024                            );
1025                        }
1026                        MarkdownTag::CodeBlock { kind, .. } => {
1027                            let language = resolve_code_block_language(kind);
1028
1029                            let is_indented = matches!(kind, CodeBlockKind::Indented);
1030                            let scroll_handle = if self.style.code_block_overflow_x_scroll {
1031                                self.markdown.update(cx, |markdown, _| {
1032                                    markdown.code_block_scroll_handle(range.start)
1033                                })
1034                            } else {
1035                                None
1036                            };
1037                            if scroll_handle.is_some() {
1038                                code_block_ids.insert(range.start);
1039                            }
1040
1041                            match (&self.code_block_renderer, is_indented) {
1042                                (CodeBlockRenderer::Default { .. }, _) | (_, true) => {
1043                                    // This is a parent container that we can position the copy button inside.
1044                                    let mut parent_container: AnyDiv =
1045                                        div().group("code_block").relative().w_full().into();
1046
1047                                    if let CodeBlockRenderer::Default { border: true, .. } =
1048                                        &self.code_block_renderer
1049                                    {
1050                                        parent_container = parent_container
1051                                            .rounded_md()
1052                                            .border_1()
1053                                            .border_color(cx.theme().colors().border_variant);
1054                                    }
1055
1056                                    parent_container.style().refine(&self.style.code_block);
1057                                    builder.push_div(parent_container, range, markdown_end);
1058
1059                                    let code_block = div()
1060                                        .id(("code-block", range.start))
1061                                        .rounded_lg()
1062                                        .map(|mut code_block| {
1063                                            if let Some(scroll_handle) = scroll_handle.as_ref() {
1064                                                code_block.style().restrict_scroll_to_axis =
1065                                                    Some(true);
1066                                                code_block
1067                                                    .flex()
1068                                                    .overflow_x_scroll()
1069                                                    .track_scroll(scroll_handle)
1070                                            } else {
1071                                                code_block.w_full()
1072                                            }
1073                                        });
1074
1075                                    builder.push_text_style(self.style.code_block.text.to_owned());
1076                                    builder.push_code_block(language);
1077                                    builder.push_div(code_block, range, markdown_end);
1078                                }
1079                                (CodeBlockRenderer::Custom { .. }, _) => {}
1080                            }
1081                        }
1082                        MarkdownTag::HtmlBlock => {
1083                            builder.push_div(div(), range, markdown_end);
1084                            if let Some(block) = parsed_markdown.html_blocks.get(&range.start) {
1085                                self.render_html_block(block, &mut builder, markdown_end, cx);
1086                                handled_html_block = true;
1087                            }
1088                        }
1089                        MarkdownTag::List(bullet_index) => {
1090                            builder.push_list(*bullet_index);
1091                            builder.push_div(div().pl_2p5(), range, markdown_end);
1092                        }
1093                        MarkdownTag::Item => {
1094                            let bullet =
1095                                if let Some((task_range, MarkdownEvent::TaskListMarker(checked))) =
1096                                    parsed_markdown.events.get(index.saturating_add(1))
1097                                {
1098                                    let source = &parsed_markdown.source()[range.clone()];
1099                                    let checked = *checked;
1100
1101                                    let checkbox = Checkbox::new(
1102                                        ElementId::Name(source.to_string().into()),
1103                                        checked,
1104                                    );
1105
1106                                    if let Some(on_toggle) = self.on_checkbox_toggle.clone() {
1107                                        let task_source_range = task_range.clone();
1108                                        checkbox
1109                                            .on_click(move |_state, window, cx| {
1110                                                on_toggle(
1111                                                    task_source_range.clone(),
1112                                                    !checked,
1113                                                    window,
1114                                                    cx,
1115                                                );
1116                                            })
1117                                            .into_any_element()
1118                                    } else {
1119                                        checkbox.into_any_element()
1120                                    }
1121                                } else if let Some(bullet_index) = builder.next_bullet_index() {
1122                                    div().child(format!("{}.", bullet_index)).into_any_element()
1123                                } else {
1124                                    div().child("•").into_any_element()
1125                                };
1126                            self.push_markdown_list_item(&mut builder, bullet, range, markdown_end);
1127                        }
1128                        MarkdownTag::Emphasis => builder.push_text_style(TextStyleRefinement {
1129                            font_style: Some(FontStyle::Italic),
1130                            ..Default::default()
1131                        }),
1132                        MarkdownTag::Strong => builder.push_text_style(TextStyleRefinement {
1133                            font_weight: Some(FontWeight::BOLD),
1134                            color: Some(cx.theme().colors().text),
1135                            ..Default::default()
1136                        }),
1137                        MarkdownTag::Strikethrough => {
1138                            builder.push_text_style(TextStyleRefinement {
1139                                strikethrough: Some(StrikethroughStyle {
1140                                    thickness: px(1.),
1141                                    color: None,
1142                                }),
1143                                ..Default::default()
1144                            })
1145                        }
1146                        MarkdownTag::Link { dest_url, .. } => {
1147                            if builder.code_block_stack.is_empty() {
1148                                builder.link_depth += 1;
1149                                builder.push_link(dest_url.clone(), range.clone());
1150                                let style = self
1151                                    .style
1152                                    .link_callback
1153                                    .as_ref()
1154                                    .and_then(|callback| callback(dest_url, cx))
1155                                    .unwrap_or_else(|| self.style.link.clone());
1156                                builder.push_text_style(style)
1157                            }
1158                        }
1159                        MarkdownTag::FootnoteDefinition(label) => {
1160                            if !builder.rendered_footnote_separator {
1161                                builder.rendered_footnote_separator = true;
1162                                builder.push_div(
1163                                    div()
1164                                        .border_t_1()
1165                                        .mt_2()
1166                                        .border_color(self.style.rule_color),
1167                                    range,
1168                                    markdown_end,
1169                                );
1170                                builder.pop_div();
1171                            }
1172                            builder.push_div(
1173                                div()
1174                                    .pt_1()
1175                                    .mb_1()
1176                                    .line_height(rems(1.3))
1177                                    .text_size(rems(0.85))
1178                                    .flex()
1179                                    .flex_row()
1180                                    .items_start()
1181                                    .gap_2()
1182                                    .child(
1183                                        div().text_size(rems(0.85)).child(format!("{}.", label)),
1184                                    ),
1185                                range,
1186                                markdown_end,
1187                            );
1188                            builder.push_div(div().flex_1().w_0(), range, markdown_end);
1189                        }
1190                        MarkdownTag::MetadataBlock(_) => {
1191                            if let Some(metadata_block) =
1192                                parsed_markdown.metadata_blocks.get(&range.start)
1193                            {
1194                                self.push_metadata_block(
1195                                    &mut builder,
1196                                    &parsed_markdown.source,
1197                                    metadata_block,
1198                                    markdown_end,
1199                                    cx,
1200                                );
1201                                rendered_metadata_block = true;
1202                            }
1203                        }
1204                        MarkdownTag::Table(alignments) => {
1205                            builder.table.start(alignments.clone());
1206
1207                            let column_count = alignments.len();
1208                            builder.push_div(
1209                                div()
1210                                    .id(("table", range.start))
1211                                    .grid()
1212                                    .grid_cols(column_count as u16)
1213                                    .when(self.style.table_columns_min_size, |this| {
1214                                        this.grid_cols_min_content(column_count as u16)
1215                                    })
1216                                    .when(!self.style.table_columns_min_size, |this| {
1217                                        this.grid_cols(column_count as u16)
1218                                    })
1219                                    .w_full()
1220                                    .mb_2()
1221                                    .border(px(1.5))
1222                                    .border_color(cx.theme().colors().border)
1223                                    .rounded_sm()
1224                                    .overflow_hidden(),
1225                                range,
1226                                markdown_end,
1227                            );
1228                        }
1229                        MarkdownTag::TableHead => {
1230                            builder.table.start_head();
1231                            builder.push_text_style(TextStyleRefinement {
1232                                font_weight: Some(FontWeight::SEMIBOLD),
1233                                ..Default::default()
1234                            });
1235                        }
1236                        MarkdownTag::TableRow => {
1237                            builder.table.start_row();
1238                        }
1239                        MarkdownTag::TableCell => {
1240                            let is_header = builder.table.in_head;
1241                            let row_index = builder.table.row_index;
1242                            let col_index = builder.table.col_index;
1243                            let alignment = builder.table.current_cell_alignment();
1244                            let text_align = alignment
1245                                .and_then(alignment_to_text_align)
1246                                .unwrap_or(self.style.base_text_style.text_align);
1247
1248                            let mut cell_div = div()
1249                                .flex()
1250                                .flex_col()
1251                                .h_full()
1252                                .when(col_index > 0, |this| this.border_l_1())
1253                                .when(row_index > 0, |this| this.border_t_1())
1254                                .border_color(cx.theme().colors().border)
1255                                .px_1()
1256                                .py_0p5()
1257                                .when(is_header, |this| {
1258                                    this.bg(cx.theme().colors().title_bar_background)
1259                                })
1260                                .when(!is_header && row_index % 2 == 1, |this| {
1261                                    this.bg(cx.theme().colors().panel_background)
1262                                });
1263
1264                            cell_div = match alignment {
1265                                Some(Alignment::Center) => cell_div.items_center(),
1266                                Some(Alignment::Right) => cell_div.items_end(),
1267                                _ => cell_div,
1268                            };
1269
1270                            builder.push_text_style(TextStyleRefinement {
1271                                text_align: Some(text_align),
1272                                ..Default::default()
1273                            });
1274                            builder.push_div(cell_div, range, markdown_end);
1275                            builder.push_div(
1276                                div()
1277                                    .flex()
1278                                    .flex_col()
1279                                    .flex_1()
1280                                    .w_full()
1281                                    .justify_center()
1282                                    .text_align(text_align),
1283                                range,
1284                                markdown_end,
1285                            );
1286                        }
1287                        _ => log::debug!("unsupported markdown tag {:?}", tag),
1288                    }
1289                }
1290                MarkdownEvent::End(tag) => match tag {
1291                    MarkdownTagEnd::Image => {
1292                        current_img_block_range.take();
1293                    }
1294                    MarkdownTagEnd::Paragraph => {
1295                        self.pop_markdown_paragraph(&mut builder);
1296                    }
1297                    MarkdownTagEnd::Heading(_) => {
1298                        self.pop_markdown_heading(&mut builder);
1299                    }
1300                    MarkdownTagEnd::BlockQuote(_kind) => {
1301                        self.pop_markdown_block_quote(&mut builder);
1302                    }
1303                    MarkdownTagEnd::CodeBlock => {
1304                        builder.trim_trailing_newline();
1305
1306                        builder.pop_div();
1307                        builder.pop_code_block();
1308                        builder.pop_text_style();
1309
1310                        if let CodeBlockRenderer::Default {
1311                            copy_button_visibility,
1312                            wrap_button_visibility,
1313                            ..
1314                        } = &self.code_block_renderer
1315                            && (*copy_button_visibility != CopyButtonVisibility::Hidden
1316                                || *wrap_button_visibility != WrapButtonVisibility::Hidden)
1317                        {
1318                            let copy_button_visibility = *copy_button_visibility;
1319                            let wrap_button_visibility = *wrap_button_visibility;
1320                            builder.modify_current_div(|el| {
1321                                let content_range = crate::parser::extract_code_block_content_range(
1322                                    &parsed_markdown.source()[range.clone()],
1323                                );
1324                                let content_range = content_range.start + range.start
1325                                    ..content_range.end + range.start;
1326
1327                                let code = parsed_markdown.source()[content_range].to_string();
1328
1329                                let any_hover = copy_button_visibility
1330                                    == CopyButtonVisibility::VisibleOnHover
1331                                    || wrap_button_visibility
1332                                        == WrapButtonVisibility::VisibleOnHover;
1333                                let any_always = copy_button_visibility
1334                                    == CopyButtonVisibility::AlwaysVisible
1335                                    || wrap_button_visibility
1336                                        == WrapButtonVisibility::AlwaysVisible;
1337                                let use_hover = any_hover && !any_always;
1338
1339                                let button_row = h_flex()
1340                                    .gap_0p5()
1341                                    .absolute()
1342                                    .bg(cx.theme().colors().editor_background)
1343                                    .when_else(
1344                                        use_hover,
1345                                        |this| {
1346                                            this.top_1()
1347                                                .right_1()
1348                                                .opacity(0.)
1349                                                .group_hover("code_block", |el| el.opacity(1.))
1350                                        },
1351                                        |this| this.top_1p5().right_1p5(),
1352                                    )
1353                                    .when(
1354                                        wrap_button_visibility != WrapButtonVisibility::Hidden,
1355                                        |this| {
1356                                            let is_wrapped = self
1357                                                .markdown
1358                                                .read(cx)
1359                                                .is_code_block_wrapped(range.start);
1360
1361                                            this.child(render_wrap_code_block_button(
1362                                                range.start,
1363                                                is_wrapped,
1364                                                self.markdown.clone(),
1365                                            ))
1366                                        },
1367                                    )
1368                                    .when(
1369                                        copy_button_visibility != CopyButtonVisibility::Hidden,
1370                                        |this| {
1371                                            this.child(render_copy_code_block_button(
1372                                                range.end,
1373                                                code,
1374                                                self.markdown.clone(),
1375                                            ))
1376                                        },
1377                                    );
1378
1379                                el.child(button_row)
1380                            });
1381                        }
1382
1383                        // Pop the parent container.
1384                        builder.pop_div();
1385                    }
1386                    MarkdownTagEnd::HtmlBlock => builder.pop_div(),
1387                    MarkdownTagEnd::List(_) => {
1388                        builder.pop_list();
1389                        builder.pop_div();
1390                    }
1391                    MarkdownTagEnd::Item => {
1392                        self.pop_markdown_list_item(&mut builder);
1393                    }
1394                    MarkdownTagEnd::Emphasis => builder.pop_text_style(),
1395                    MarkdownTagEnd::Strong => builder.pop_text_style(),
1396                    MarkdownTagEnd::Strikethrough => builder.pop_text_style(),
1397                    MarkdownTagEnd::Link => {
1398                        if builder.code_block_stack.is_empty() {
1399                            builder.link_depth = builder.link_depth.saturating_sub(1);
1400                            builder.pop_text_style()
1401                        }
1402                    }
1403                    MarkdownTagEnd::Table => {
1404                        builder.pop_div();
1405                        builder.table.end();
1406                    }
1407                    MarkdownTagEnd::TableHead => {
1408                        builder.pop_text_style();
1409                        builder.table.end_head();
1410                    }
1411                    MarkdownTagEnd::TableRow => {
1412                        builder.table.end_row();
1413                    }
1414                    MarkdownTagEnd::TableCell => {
1415                        builder.replace_pending_checkbox(self.on_checkbox_toggle.clone());
1416                        builder.pop_div();
1417                        builder.pop_div();
1418                        builder.pop_text_style();
1419                        builder.table.end_cell();
1420                    }
1421                    MarkdownTagEnd::FootnoteDefinition => {
1422                        builder.pop_div();
1423                        builder.pop_div();
1424                    }
1425                    MarkdownTagEnd::MetadataBlock(_) => {}
1426                    _ => log::debug!("unsupported markdown tag end: {:?}", tag),
1427                },
1428                MarkdownEvent::Text => {
1429                    builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
1430                }
1431                MarkdownEvent::SubstitutedText(text) => {
1432                    builder.push_text(text, range.clone());
1433                }
1434                MarkdownEvent::Code => {
1435                    self.push_markdown_code_span(
1436                        &mut builder,
1437                        &parsed_markdown.source[range.clone()],
1438                        range.clone(),
1439                        cx,
1440                    );
1441                }
1442                MarkdownEvent::SubstitutedCode(text) => {
1443                    self.push_markdown_code_span(&mut builder, text, range.clone(), cx);
1444                }
1445                MarkdownEvent::Html => {
1446                    let html = &parsed_markdown.source[range.clone()];
1447                    if html.starts_with("<!--") {
1448                        builder.html_comment = true;
1449                    }
1450                    if html.trim_end().ends_with("-->") {
1451                        builder.html_comment = false;
1452                        continue;
1453                    }
1454                    if builder.html_comment {
1455                        continue;
1456                    }
1457                    builder.push_text(html, range.clone());
1458                }
1459                MarkdownEvent::InlineHtml => {
1460                    let html = &parsed_markdown.source[range.clone()];
1461                    if let Some(code) = html
1462                        .strip_prefix("<code>")
1463                        .and_then(|html| html.strip_suffix("</code>"))
1464                    {
1465                        let code_start = range.start + "<code>".len();
1466                        self.push_markdown_code_span(
1467                            &mut builder,
1468                            code,
1469                            code_start..code_start + code.len(),
1470                            cx,
1471                        );
1472                        continue;
1473                    }
1474                    if html.starts_with("<code>") {
1475                        builder.push_text_style(self.style.inline_code.clone());
1476                        continue;
1477                    }
1478                    if html.trim_end().starts_with("</code>") {
1479                        builder.pop_text_style();
1480                        continue;
1481                    }
1482                    builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
1483                }
1484                MarkdownEvent::Rule => {
1485                    builder.push_div(
1486                        div()
1487                            .border_b_1()
1488                            .my_2()
1489                            .border_color(self.style.rule_color),
1490                        range,
1491                        markdown_end,
1492                    );
1493                    builder.pop_div()
1494                }
1495                MarkdownEvent::SoftBreak if !self.style.soft_break_as_hard_break => {
1496                    builder.push_soft_break(range.clone());
1497                }
1498                MarkdownEvent::SoftBreak | MarkdownEvent::HardBreak => {
1499                    builder.push_line_break(range.clone());
1500                }
1501                MarkdownEvent::TaskListMarker(_) => {
1502                    // handled inside the `MarkdownTag::Item` case
1503                }
1504                MarkdownEvent::FootnoteReference(label) => {
1505                    builder.push_footnote_ref(label.clone(), range.clone());
1506                    builder.push_text_style(self.style.link.clone());
1507                    builder.push_text(&format!("[{label}]"), range.clone());
1508                    builder.pop_text_style();
1509                }
1510            }
1511        }
1512        if self.style.code_block_overflow_x_scroll {
1513            let code_block_ids = code_block_ids;
1514            self.markdown.update(cx, move |markdown, _| {
1515                markdown.retain_code_block_scroll_handles(&code_block_ids);
1516            });
1517        } else {
1518            self.markdown
1519                .update(cx, |markdown, _| markdown.clear_code_block_scroll_handles());
1520        }
1521        let mut rendered_markdown = builder.build();
1522        let child_layout_id = rendered_markdown.element.request_layout(window, cx);
1523        let layout_id = window.request_layout(gpui::Style::default(), [child_layout_id], cx);
1524        (layout_id, rendered_markdown)
1525    }
1526
1527    fn prepaint(
1528        &mut self,
1529        _id: Option<&GlobalElementId>,
1530        _inspector_id: Option<&gpui::InspectorElementId>,
1531        bounds: Bounds<Pixels>,
1532        rendered_markdown: &mut Self::RequestLayoutState,
1533        window: &mut Window,
1534        cx: &mut App,
1535    ) -> Self::PrepaintState {
1536        let focus_handle = self.markdown.read(cx).focus_handle.clone();
1537        window.set_focus_handle(&focus_handle, cx);
1538        window.set_view_id(self.markdown.entity_id());
1539
1540        let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
1541        rendered_markdown.element.prepaint(window, cx);
1542        self.autoscroll(&rendered_markdown.text, window, cx);
1543        hitbox
1544    }
1545
1546    fn paint(
1547        &mut self,
1548        _id: Option<&GlobalElementId>,
1549        _inspector_id: Option<&gpui::InspectorElementId>,
1550        _bounds: Bounds<Pixels>,
1551        rendered_markdown: &mut Self::RequestLayoutState,
1552        hitbox: &mut Self::PrepaintState,
1553        window: &mut Window,
1554        cx: &mut App,
1555    ) {
1556        let mut context = KeyContext::default();
1557        context.add("Markdown");
1558        window.set_key_context(context);
1559        window.on_action(std::any::TypeId::of::<crate::entity::Copy>(), {
1560            let entity = self.markdown.clone();
1561            let text = rendered_markdown.text.clone();
1562            move |_, phase, window, cx| {
1563                let text = text.clone();
1564                if phase == DispatchPhase::Bubble {
1565                    entity.update(cx, move |this, cx| this.copy(&text, window, cx))
1566                }
1567            }
1568        });
1569        window.on_action(std::any::TypeId::of::<crate::entity::CopyAsMarkdown>(), {
1570            let entity = self.markdown.clone();
1571            move |_, phase, window, cx| {
1572                if phase == DispatchPhase::Bubble {
1573                    entity.update(cx, move |this, cx| this.copy_as_markdown(window, cx))
1574                }
1575            }
1576        });
1577
1578        self.paint_mouse_listeners(hitbox, &rendered_markdown.text, window, cx);
1579        rendered_markdown.element.paint(window, cx);
1580        self.paint_search_highlights(&rendered_markdown.text, window, cx);
1581        self.paint_selection(&rendered_markdown.text, window, cx);
1582    }
1583}
1584
1585fn collect_image_alt_text(
1586    events_from_image_start: &[(Range<usize>, MarkdownEvent)],
1587    source: &str,
1588) -> Option<SharedString> {
1589    let mut alt_text = String::new();
1590    for (range, event) in events_from_image_start.iter().skip(1) {
1591        match event {
1592            MarkdownEvent::End(MarkdownTagEnd::Image) => break,
1593            MarkdownEvent::Text => alt_text.push_str(&source[range.clone()]),
1594            _ => {}
1595        }
1596    }
1597    if alt_text.is_empty() {
1598        None
1599    } else {
1600        Some(alt_text.into())
1601    }
1602}
1603
1604fn image_fallback_element(
1605    dest_url: SharedString,
1606    alt_text: Option<SharedString>,
1607    open_image_url_on_click: bool,
1608) -> AnyElement {
1609    let link_label = alt_text
1610        .filter(|alt| !alt.is_empty())
1611        .unwrap_or_else(|| dest_url.clone());
1612
1613    let label = format!("Failed to Load: {link_label}");
1614
1615    div()
1616        .id("image-fallback")
1617        .min_w_0()
1618        .text_color(gpui::red())
1619        .child(label)
1620        .tooltip(simple_tooltip(
1621            "Image failed to load. Check the logs for more details.",
1622        ))
1623        .when(open_image_url_on_click, |this| {
1624            this.cursor_pointer()
1625                .on_click(move |_, _, cx| cx.open_url(&dest_url))
1626        })
1627        .into_any_element()
1628}
1629
1630fn apply_heading_style(
1631    mut heading: Div,
1632    level: HeadingLevel,
1633    custom_styles: Option<&HeadingLevelStyles>,
1634    border_color: Option<Hsla>,
1635) -> Div {
1636    heading = match level {
1637        HeadingLevel::H1 => heading.text_3xl(),
1638        HeadingLevel::H2 => heading.text_2xl(),
1639        HeadingLevel::H3 => heading.text_xl(),
1640        HeadingLevel::H4 => heading.text_lg(),
1641        HeadingLevel::H5 => heading.text_base(),
1642        HeadingLevel::H6 => heading.text_sm(),
1643    };
1644
1645    heading = match level {
1646        HeadingLevel::H1 => heading,
1647        _ => heading.mt_6(),
1648    };
1649
1650    if let Some(border_color) = border_color
1651        && matches!(
1652            level,
1653            HeadingLevel::H1 | HeadingLevel::H2 | HeadingLevel::H3
1654        )
1655    {
1656        heading = heading.pb_1().border_b_1().border_color(border_color);
1657    }
1658
1659    if let Some(styles) = custom_styles {
1660        let style_opt = match level {
1661            HeadingLevel::H1 => &styles.h1,
1662            HeadingLevel::H2 => &styles.h2,
1663            HeadingLevel::H3 => &styles.h3,
1664            HeadingLevel::H4 => &styles.h4,
1665            HeadingLevel::H5 => &styles.h5,
1666            HeadingLevel::H6 => &styles.h6,
1667        };
1668
1669        if let Some(style) = style_opt {
1670            heading.style().text = style.clone();
1671        }
1672    }
1673
1674    heading
1675}
1676
1677fn render_wrap_code_block_button(
1678    id: usize,
1679    is_wrapped: bool,
1680    markdown: Entity<Markdown>,
1681) -> impl IntoElement {
1682    let tooltip = if is_wrapped {
1683        "Unwrap Content"
1684    } else {
1685        "Wrap Content"
1686    };
1687    let button_id = ElementId::NamedChild(
1688        Arc::new(ElementId::from(("wrap-code-block", markdown.entity_id()))),
1689        id.to_string().into(),
1690    );
1691
1692    div()
1693        .id(button_id)
1694        .flex()
1695        .items_center()
1696        .justify_center()
1697        .size(px(20.))
1698        .rounded_xs()
1699        .cursor_pointer()
1700        .child(icon_svg(
1701            IconName::ArrowRightLeft,
1702            px(14.),
1703            gpui::rgb(0xA0AEC0),
1704        ))
1705        .tooltip(simple_tooltip(tooltip))
1706        .on_click(move |_event, _window, cx| {
1707            markdown.update(cx, |markdown, cx| {
1708                markdown.toggle_code_block_wrap(id);
1709                cx.notify();
1710            });
1711        })
1712}
1713
1714fn render_copy_code_block_button(
1715    id: usize,
1716    code: String,
1717    markdown: Entity<Markdown>,
1718) -> impl IntoElement {
1719    let id = ElementId::NamedChild(
1720        Arc::new(ElementId::from((
1721            "copy-markdown-code",
1722            markdown.entity_id(),
1723        ))),
1724        id.to_string().into(),
1725    );
1726
1727    CopyButton::new(id, code)
1728}
1729
1730impl IntoElement for MarkdownElement {
1731    type Element = Self;
1732
1733    fn into_element(self) -> Self::Element {
1734        self
1735    }
1736}
1737
1738pub enum AnyDiv {
1739    Div(Div),
1740    Stateful(Stateful<Div>),
1741}
1742
1743impl AnyDiv {
1744    pub(crate) fn into_any_element(self) -> AnyElement {
1745        match self {
1746            Self::Div(div) => div.into_any_element(),
1747            Self::Stateful(div) => div.into_any_element(),
1748        }
1749    }
1750}
1751
1752impl From<Div> for AnyDiv {
1753    fn from(value: Div) -> Self {
1754        Self::Div(value)
1755    }
1756}
1757
1758impl From<Stateful<Div>> for AnyDiv {
1759    fn from(value: Stateful<Div>) -> Self {
1760        Self::Stateful(value)
1761    }
1762}
1763
1764impl Styled for AnyDiv {
1765    fn style(&mut self) -> &mut StyleRefinement {
1766        match self {
1767            Self::Div(div) => div.style(),
1768            Self::Stateful(div) => div.style(),
1769        }
1770    }
1771}
1772
1773impl ParentElement for AnyDiv {
1774    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
1775        match self {
1776            Self::Div(div) => div.extend(elements),
1777            Self::Stateful(div) => div.extend(elements),
1778        }
1779    }
1780}