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()
1123                                        .min_w(px(16.))
1124                                        .text_color(cx.theme().colors().text)
1125                                        .child(format!("{}.", bullet_index))
1126                                        .into_any_element()
1127                                } else {
1128                                    div()
1129                                        .min_w(px(16.))
1130                                        .text_color(cx.theme().colors().text)
1131                                        .child("•")
1132                                        .into_any_element()
1133                                };
1134                            self.push_markdown_list_item(&mut builder, bullet, range, markdown_end);
1135                        }
1136                        MarkdownTag::Emphasis => builder.push_text_style(TextStyleRefinement {
1137                            font_style: Some(FontStyle::Italic),
1138                            ..Default::default()
1139                        }),
1140                        MarkdownTag::Strong => builder.push_text_style(TextStyleRefinement {
1141                            font_weight: Some(FontWeight::BOLD),
1142                            color: Some(cx.theme().colors().text),
1143                            ..Default::default()
1144                        }),
1145                        MarkdownTag::Strikethrough => {
1146                            builder.push_text_style(TextStyleRefinement {
1147                                strikethrough: Some(StrikethroughStyle {
1148                                    thickness: px(1.),
1149                                    color: None,
1150                                }),
1151                                ..Default::default()
1152                            })
1153                        }
1154                        MarkdownTag::Link { dest_url, .. } => {
1155                            if builder.code_block_stack.is_empty() {
1156                                builder.link_depth += 1;
1157                                builder.push_link(dest_url.clone(), range.clone());
1158                                let style = self
1159                                    .style
1160                                    .link_callback
1161                                    .as_ref()
1162                                    .and_then(|callback| callback(dest_url, cx))
1163                                    .unwrap_or_else(|| self.style.link.clone());
1164                                builder.push_text_style(style)
1165                            }
1166                        }
1167                        MarkdownTag::FootnoteDefinition(label) => {
1168                            if !builder.rendered_footnote_separator {
1169                                builder.rendered_footnote_separator = true;
1170                                builder.push_div(
1171                                    div()
1172                                        .border_t_1()
1173                                        .mt_2()
1174                                        .border_color(self.style.rule_color),
1175                                    range,
1176                                    markdown_end,
1177                                );
1178                                builder.pop_div();
1179                            }
1180                            builder.push_div(
1181                                div()
1182                                    .pt_1()
1183                                    .mb_1()
1184                                    .line_height(rems(1.3))
1185                                    .text_size(rems(0.85))
1186                                    .flex()
1187                                    .flex_row()
1188                                    .items_start()
1189                                    .gap_2()
1190                                    .child(
1191                                        div().text_size(rems(0.85)).child(format!("{}.", label)),
1192                                    ),
1193                                range,
1194                                markdown_end,
1195                            );
1196                            builder.push_div(div().flex_1().w_0(), range, markdown_end);
1197                        }
1198                        MarkdownTag::MetadataBlock(_) => {
1199                            if let Some(metadata_block) =
1200                                parsed_markdown.metadata_blocks.get(&range.start)
1201                            {
1202                                self.push_metadata_block(
1203                                    &mut builder,
1204                                    &parsed_markdown.source,
1205                                    metadata_block,
1206                                    markdown_end,
1207                                    cx,
1208                                );
1209                                rendered_metadata_block = true;
1210                            }
1211                        }
1212                        MarkdownTag::Table(alignments) => {
1213                            builder.table.start(alignments.clone());
1214
1215                            let column_count = alignments.len();
1216                            builder.push_div(
1217                                div()
1218                                    .id(("table", range.start))
1219                                    .grid()
1220                                    .grid_cols(column_count as u16)
1221                                    .when(self.style.table_columns_min_size, |this| {
1222                                        this.grid_cols_min_content(column_count as u16)
1223                                    })
1224                                    .when(!self.style.table_columns_min_size, |this| {
1225                                        this.grid_cols(column_count as u16)
1226                                    })
1227                                    .w_full()
1228                                    .mb_2()
1229                                    .border(px(1.5))
1230                                    .border_color(cx.theme().colors().border)
1231                                    .rounded_sm()
1232                                    .overflow_hidden(),
1233                                range,
1234                                markdown_end,
1235                            );
1236                        }
1237                        MarkdownTag::TableHead => {
1238                            builder.table.start_head();
1239                            builder.push_text_style(TextStyleRefinement {
1240                                font_weight: Some(FontWeight::SEMIBOLD),
1241                                ..Default::default()
1242                            });
1243                        }
1244                        MarkdownTag::TableRow => {
1245                            builder.table.start_row();
1246                        }
1247                        MarkdownTag::TableCell => {
1248                            let is_header = builder.table.in_head;
1249                            let row_index = builder.table.row_index;
1250                            let col_index = builder.table.col_index;
1251                            let alignment = builder.table.current_cell_alignment();
1252                            let text_align = alignment
1253                                .and_then(alignment_to_text_align)
1254                                .unwrap_or(self.style.base_text_style.text_align);
1255
1256                            let mut cell_div = div()
1257                                .flex()
1258                                .flex_col()
1259                                .h_full()
1260                                .when(col_index > 0, |this| this.border_l_1())
1261                                .when(row_index > 0, |this| this.border_t_1())
1262                                .border_color(cx.theme().colors().border)
1263                                .px_1()
1264                                .py_0p5()
1265                                .when(is_header, |this| {
1266                                    this.bg(cx.theme().colors().title_bar_background)
1267                                })
1268                                .when(!is_header && row_index % 2 == 1, |this| {
1269                                    this.bg(cx.theme().colors().panel_background)
1270                                });
1271
1272                            cell_div = match alignment {
1273                                Some(Alignment::Center) => cell_div.items_center(),
1274                                Some(Alignment::Right) => cell_div.items_end(),
1275                                _ => cell_div,
1276                            };
1277
1278                            builder.push_text_style(TextStyleRefinement {
1279                                text_align: Some(text_align),
1280                                ..Default::default()
1281                            });
1282                            builder.push_div(cell_div, range, markdown_end);
1283                            builder.push_div(
1284                                div()
1285                                    .flex()
1286                                    .flex_col()
1287                                    .flex_1()
1288                                    .w_full()
1289                                    .justify_center()
1290                                    .text_align(text_align),
1291                                range,
1292                                markdown_end,
1293                            );
1294                        }
1295                        _ => log::debug!("unsupported markdown tag {:?}", tag),
1296                    }
1297                }
1298                MarkdownEvent::End(tag) => match tag {
1299                    MarkdownTagEnd::Image => {
1300                        current_img_block_range.take();
1301                    }
1302                    MarkdownTagEnd::Paragraph => {
1303                        self.pop_markdown_paragraph(&mut builder);
1304                    }
1305                    MarkdownTagEnd::Heading(_) => {
1306                        self.pop_markdown_heading(&mut builder);
1307                    }
1308                    MarkdownTagEnd::BlockQuote(_kind) => {
1309                        self.pop_markdown_block_quote(&mut builder);
1310                    }
1311                    MarkdownTagEnd::CodeBlock => {
1312                        builder.trim_trailing_newline();
1313
1314                        builder.pop_div();
1315                        builder.pop_code_block();
1316                        builder.pop_text_style();
1317
1318                        if let CodeBlockRenderer::Default {
1319                            copy_button_visibility,
1320                            wrap_button_visibility,
1321                            ..
1322                        } = &self.code_block_renderer
1323                            && (*copy_button_visibility != CopyButtonVisibility::Hidden
1324                                || *wrap_button_visibility != WrapButtonVisibility::Hidden)
1325                        {
1326                            let copy_button_visibility = *copy_button_visibility;
1327                            let wrap_button_visibility = *wrap_button_visibility;
1328                            builder.modify_current_div(|el| {
1329                                let content_range = crate::parser::extract_code_block_content_range(
1330                                    &parsed_markdown.source()[range.clone()],
1331                                );
1332                                let content_range = content_range.start + range.start
1333                                    ..content_range.end + range.start;
1334
1335                                let code = parsed_markdown.source()[content_range].to_string();
1336
1337                                let any_hover = copy_button_visibility
1338                                    == CopyButtonVisibility::VisibleOnHover
1339                                    || wrap_button_visibility
1340                                        == WrapButtonVisibility::VisibleOnHover;
1341                                let any_always = copy_button_visibility
1342                                    == CopyButtonVisibility::AlwaysVisible
1343                                    || wrap_button_visibility
1344                                        == WrapButtonVisibility::AlwaysVisible;
1345                                let use_hover = any_hover && !any_always;
1346
1347                                let button_row = h_flex()
1348                                    .gap_0p5()
1349                                    .absolute()
1350                                    .bg(cx.theme().colors().editor_background)
1351                                    .when_else(
1352                                        use_hover,
1353                                        |this| {
1354                                            this.top_1()
1355                                                .right_1()
1356                                                .opacity(0.)
1357                                                .group_hover("code_block", |el| el.opacity(1.))
1358                                        },
1359                                        |this| this.top_1p5().right_1p5(),
1360                                    )
1361                                    .when(
1362                                        wrap_button_visibility != WrapButtonVisibility::Hidden,
1363                                        |this| {
1364                                            let is_wrapped = self
1365                                                .markdown
1366                                                .read(cx)
1367                                                .is_code_block_wrapped(range.start);
1368
1369                                            this.child(render_wrap_code_block_button(
1370                                                range.start,
1371                                                is_wrapped,
1372                                                self.markdown.clone(),
1373                                            ))
1374                                        },
1375                                    )
1376                                    .when(
1377                                        copy_button_visibility != CopyButtonVisibility::Hidden,
1378                                        |this| {
1379                                            this.child(render_copy_code_block_button(
1380                                                range.end,
1381                                                code,
1382                                                self.markdown.clone(),
1383                                            ))
1384                                        },
1385                                    );
1386
1387                                el.child(button_row)
1388                            });
1389                        }
1390
1391                        // Pop the parent container.
1392                        builder.pop_div();
1393                    }
1394                    MarkdownTagEnd::HtmlBlock => builder.pop_div(),
1395                    MarkdownTagEnd::List(_) => {
1396                        builder.pop_list();
1397                        builder.pop_div();
1398                    }
1399                    MarkdownTagEnd::Item => {
1400                        self.pop_markdown_list_item(&mut builder);
1401                    }
1402                    MarkdownTagEnd::Emphasis => builder.pop_text_style(),
1403                    MarkdownTagEnd::Strong => builder.pop_text_style(),
1404                    MarkdownTagEnd::Strikethrough => builder.pop_text_style(),
1405                    MarkdownTagEnd::Link => {
1406                        if builder.code_block_stack.is_empty() {
1407                            builder.link_depth = builder.link_depth.saturating_sub(1);
1408                            builder.pop_text_style()
1409                        }
1410                    }
1411                    MarkdownTagEnd::Table => {
1412                        builder.pop_div();
1413                        builder.table.end();
1414                    }
1415                    MarkdownTagEnd::TableHead => {
1416                        builder.pop_text_style();
1417                        builder.table.end_head();
1418                    }
1419                    MarkdownTagEnd::TableRow => {
1420                        builder.table.end_row();
1421                    }
1422                    MarkdownTagEnd::TableCell => {
1423                        builder.replace_pending_checkbox(self.on_checkbox_toggle.clone());
1424                        builder.pop_div();
1425                        builder.pop_div();
1426                        builder.pop_text_style();
1427                        builder.table.end_cell();
1428                    }
1429                    MarkdownTagEnd::FootnoteDefinition => {
1430                        builder.pop_div();
1431                        builder.pop_div();
1432                    }
1433                    MarkdownTagEnd::MetadataBlock(_) => {}
1434                    _ => log::debug!("unsupported markdown tag end: {:?}", tag),
1435                },
1436                MarkdownEvent::Text => {
1437                    builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
1438                }
1439                MarkdownEvent::SubstitutedText(text) => {
1440                    builder.push_text(text, range.clone());
1441                }
1442                MarkdownEvent::Code => {
1443                    self.push_markdown_code_span(
1444                        &mut builder,
1445                        &parsed_markdown.source[range.clone()],
1446                        range.clone(),
1447                        cx,
1448                    );
1449                }
1450                MarkdownEvent::SubstitutedCode(text) => {
1451                    self.push_markdown_code_span(&mut builder, text, range.clone(), cx);
1452                }
1453                MarkdownEvent::Html => {
1454                    let html = &parsed_markdown.source[range.clone()];
1455                    if html.starts_with("<!--") {
1456                        builder.html_comment = true;
1457                    }
1458                    if html.trim_end().ends_with("-->") {
1459                        builder.html_comment = false;
1460                        continue;
1461                    }
1462                    if builder.html_comment {
1463                        continue;
1464                    }
1465                    builder.push_text(html, range.clone());
1466                }
1467                MarkdownEvent::InlineHtml => {
1468                    let html = &parsed_markdown.source[range.clone()];
1469                    if let Some(code) = html
1470                        .strip_prefix("<code>")
1471                        .and_then(|html| html.strip_suffix("</code>"))
1472                    {
1473                        let code_start = range.start + "<code>".len();
1474                        self.push_markdown_code_span(
1475                            &mut builder,
1476                            code,
1477                            code_start..code_start + code.len(),
1478                            cx,
1479                        );
1480                        continue;
1481                    }
1482                    if html.starts_with("<code>") {
1483                        builder.push_text_style(self.style.inline_code.clone());
1484                        continue;
1485                    }
1486                    if html.trim_end().starts_with("</code>") {
1487                        builder.pop_text_style();
1488                        continue;
1489                    }
1490                    builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
1491                }
1492                MarkdownEvent::Rule => {
1493                    builder.push_div(
1494                        div()
1495                            .border_b_1()
1496                            .my_2()
1497                            .border_color(self.style.rule_color),
1498                        range,
1499                        markdown_end,
1500                    );
1501                    builder.pop_div()
1502                }
1503                MarkdownEvent::SoftBreak if !self.style.soft_break_as_hard_break => {
1504                    builder.push_soft_break(range.clone());
1505                }
1506                MarkdownEvent::SoftBreak | MarkdownEvent::HardBreak => {
1507                    builder.push_line_break(range.clone());
1508                }
1509                MarkdownEvent::TaskListMarker(_) => {
1510                    // handled inside the `MarkdownTag::Item` case
1511                }
1512                MarkdownEvent::FootnoteReference(label) => {
1513                    builder.push_footnote_ref(label.clone(), range.clone());
1514                    builder.push_text_style(self.style.link.clone());
1515                    builder.push_text(&format!("[{label}]"), range.clone());
1516                    builder.pop_text_style();
1517                }
1518            }
1519        }
1520        if self.style.code_block_overflow_x_scroll {
1521            let code_block_ids = code_block_ids;
1522            self.markdown.update(cx, move |markdown, _| {
1523                markdown.retain_code_block_scroll_handles(&code_block_ids);
1524            });
1525        } else {
1526            self.markdown
1527                .update(cx, |markdown, _| markdown.clear_code_block_scroll_handles());
1528        }
1529        let mut rendered_markdown = builder.build();
1530        let child_layout_id = rendered_markdown.element.request_layout(window, cx);
1531        let layout_id = window.request_layout(gpui::Style::default(), [child_layout_id], cx);
1532        (layout_id, rendered_markdown)
1533    }
1534
1535    fn prepaint(
1536        &mut self,
1537        _id: Option<&GlobalElementId>,
1538        _inspector_id: Option<&gpui::InspectorElementId>,
1539        bounds: Bounds<Pixels>,
1540        rendered_markdown: &mut Self::RequestLayoutState,
1541        window: &mut Window,
1542        cx: &mut App,
1543    ) -> Self::PrepaintState {
1544        let focus_handle = self.markdown.read(cx).focus_handle.clone();
1545        window.set_focus_handle(&focus_handle, cx);
1546        window.set_view_id(self.markdown.entity_id());
1547
1548        let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
1549        rendered_markdown.element.prepaint(window, cx);
1550        self.autoscroll(&rendered_markdown.text, window, cx);
1551        hitbox
1552    }
1553
1554    fn paint(
1555        &mut self,
1556        _id: Option<&GlobalElementId>,
1557        _inspector_id: Option<&gpui::InspectorElementId>,
1558        _bounds: Bounds<Pixels>,
1559        rendered_markdown: &mut Self::RequestLayoutState,
1560        hitbox: &mut Self::PrepaintState,
1561        window: &mut Window,
1562        cx: &mut App,
1563    ) {
1564        let mut context = KeyContext::default();
1565        context.add("Markdown");
1566        window.set_key_context(context);
1567        window.on_action(std::any::TypeId::of::<crate::entity::Copy>(), {
1568            let entity = self.markdown.clone();
1569            let text = rendered_markdown.text.clone();
1570            move |_, phase, window, cx| {
1571                let text = text.clone();
1572                if phase == DispatchPhase::Bubble {
1573                    entity.update(cx, move |this, cx| this.copy(&text, window, cx))
1574                }
1575            }
1576        });
1577        window.on_action(std::any::TypeId::of::<crate::entity::CopyAsMarkdown>(), {
1578            let entity = self.markdown.clone();
1579            move |_, phase, window, cx| {
1580                if phase == DispatchPhase::Bubble {
1581                    entity.update(cx, move |this, cx| this.copy_as_markdown(window, cx))
1582                }
1583            }
1584        });
1585
1586        self.paint_mouse_listeners(hitbox, &rendered_markdown.text, window, cx);
1587        rendered_markdown.element.paint(window, cx);
1588        self.paint_search_highlights(&rendered_markdown.text, window, cx);
1589        self.paint_selection(&rendered_markdown.text, window, cx);
1590    }
1591}
1592
1593fn collect_image_alt_text(
1594    events_from_image_start: &[(Range<usize>, MarkdownEvent)],
1595    source: &str,
1596) -> Option<SharedString> {
1597    let mut alt_text = String::new();
1598    for (range, event) in events_from_image_start.iter().skip(1) {
1599        match event {
1600            MarkdownEvent::End(MarkdownTagEnd::Image) => break,
1601            MarkdownEvent::Text => alt_text.push_str(&source[range.clone()]),
1602            _ => {}
1603        }
1604    }
1605    if alt_text.is_empty() {
1606        None
1607    } else {
1608        Some(alt_text.into())
1609    }
1610}
1611
1612fn image_fallback_element(
1613    dest_url: SharedString,
1614    alt_text: Option<SharedString>,
1615    open_image_url_on_click: bool,
1616) -> AnyElement {
1617    let link_label = alt_text
1618        .filter(|alt| !alt.is_empty())
1619        .unwrap_or_else(|| dest_url.clone());
1620
1621    let label = format!("Failed to Load: {link_label}");
1622
1623    div()
1624        .id("image-fallback")
1625        .min_w_0()
1626        .text_color(gpui::red())
1627        .child(label)
1628        .tooltip(simple_tooltip(
1629            "Image failed to load. Check the logs for more details.",
1630        ))
1631        .when(open_image_url_on_click, |this| {
1632            this.cursor_pointer()
1633                .on_click(move |_, _, cx| cx.open_url(&dest_url))
1634        })
1635        .into_any_element()
1636}
1637
1638fn apply_heading_style(
1639    mut heading: Div,
1640    level: HeadingLevel,
1641    custom_styles: Option<&HeadingLevelStyles>,
1642    border_color: Option<Hsla>,
1643) -> Div {
1644    heading = match level {
1645        HeadingLevel::H1 => heading.text_3xl(),
1646        HeadingLevel::H2 => heading.text_2xl(),
1647        HeadingLevel::H3 => heading.text_xl(),
1648        HeadingLevel::H4 => heading.text_lg(),
1649        HeadingLevel::H5 => heading.text_base(),
1650        HeadingLevel::H6 => heading.text_sm(),
1651    };
1652
1653    heading = match level {
1654        HeadingLevel::H1 => heading,
1655        _ => heading.mt_6(),
1656    };
1657
1658    if let Some(border_color) = border_color
1659        && matches!(
1660            level,
1661            HeadingLevel::H1 | HeadingLevel::H2 | HeadingLevel::H3
1662        )
1663    {
1664        heading = heading.pb_1().border_b_1().border_color(border_color);
1665    }
1666
1667    if let Some(styles) = custom_styles {
1668        let style_opt = match level {
1669            HeadingLevel::H1 => &styles.h1,
1670            HeadingLevel::H2 => &styles.h2,
1671            HeadingLevel::H3 => &styles.h3,
1672            HeadingLevel::H4 => &styles.h4,
1673            HeadingLevel::H5 => &styles.h5,
1674            HeadingLevel::H6 => &styles.h6,
1675        };
1676
1677        if let Some(style) = style_opt {
1678            heading.style().text = style.clone();
1679        }
1680    }
1681
1682    heading
1683}
1684
1685fn render_wrap_code_block_button(
1686    id: usize,
1687    is_wrapped: bool,
1688    markdown: Entity<Markdown>,
1689) -> impl IntoElement {
1690    let tooltip = if is_wrapped {
1691        "Unwrap Content"
1692    } else {
1693        "Wrap Content"
1694    };
1695    let button_id = ElementId::NamedChild(
1696        Arc::new(ElementId::from(("wrap-code-block", markdown.entity_id()))),
1697        id.to_string().into(),
1698    );
1699
1700    div()
1701        .id(button_id)
1702        .flex()
1703        .items_center()
1704        .justify_center()
1705        .size(px(20.))
1706        .rounded_xs()
1707        .cursor_pointer()
1708        .child(icon_svg(
1709            IconName::ArrowRightLeft,
1710            px(14.),
1711            gpui::rgb(0xA0AEC0),
1712        ))
1713        .tooltip(simple_tooltip(tooltip))
1714        .on_click(move |_event, _window, cx| {
1715            markdown.update(cx, |markdown, cx| {
1716                markdown.toggle_code_block_wrap(id);
1717                cx.notify();
1718            });
1719        })
1720}
1721
1722fn render_copy_code_block_button(
1723    id: usize,
1724    code: String,
1725    markdown: Entity<Markdown>,
1726) -> impl IntoElement {
1727    let id = ElementId::NamedChild(
1728        Arc::new(ElementId::from((
1729            "copy-markdown-code",
1730            markdown.entity_id(),
1731        ))),
1732        id.to_string().into(),
1733    );
1734
1735    CopyButton::new(id, code)
1736}
1737
1738impl IntoElement for MarkdownElement {
1739    type Element = Self;
1740
1741    fn into_element(self) -> Self::Element {
1742        self
1743    }
1744}
1745
1746pub enum AnyDiv {
1747    Div(Div),
1748    Stateful(Stateful<Div>),
1749}
1750
1751impl AnyDiv {
1752    pub(crate) fn into_any_element(self) -> AnyElement {
1753        match self {
1754            Self::Div(div) => div.into_any_element(),
1755            Self::Stateful(div) => div.into_any_element(),
1756        }
1757    }
1758}
1759
1760impl From<Div> for AnyDiv {
1761    fn from(value: Div) -> Self {
1762        Self::Div(value)
1763    }
1764}
1765
1766impl From<Stateful<Div>> for AnyDiv {
1767    fn from(value: Stateful<Div>) -> Self {
1768        Self::Stateful(value)
1769    }
1770}
1771
1772impl Styled for AnyDiv {
1773    fn style(&mut self) -> &mut StyleRefinement {
1774        match self {
1775            Self::Div(div) => div.style(),
1776            Self::Stateful(div) => div.style(),
1777        }
1778    }
1779}
1780
1781impl ParentElement for AnyDiv {
1782    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
1783        match self {
1784            Self::Div(div) => div.extend(elements),
1785            Self::Stateful(div) => div.extend(elements),
1786        }
1787    }
1788}