Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T01:51:14.789Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

markdown.rs

5799 lines · 216.9 KB · rust
1pub mod html;
2mod mermaid;
3pub mod parser;
4mod path_range;
5mod selection;
6
7use base64::Engine as _;
8use futures::FutureExt as _;
9use gpui::EdgesRefinement;
10use gpui::HitboxBehavior;
11use gpui::UnderlineStyle;
12use language::LanguageName;
13
14use log::Level;
15use mermaid::{
16    MermaidState, ParsedMarkdownMermaidDiagram, extract_mermaid_diagrams, render_mermaid_diagram,
17};
18pub use path_range::{LineCol, PathWithRange};
19use settings::Settings as _;
20use smallvec::SmallVec;
21use theme_settings::ThemeSettings;
22use util::maybe;
23
24use std::borrow::Cow;
25use std::cell::Cell;
26use std::collections::BTreeMap;
27use std::mem;
28use std::ops::Range;
29use std::path::Path;
30use std::rc::Rc;
31use std::sync::Arc;
32use std::time::Duration;
33
34use collections::{HashMap, HashSet};
35use gpui::{
36    AnyElement, App, BorderStyle, Bounds, ClipboardItem, CursorStyle, DispatchPhase, Edges, Entity,
37    FocusHandle, Focusable, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla, Image,
38    ImageFormat, ImageSource, KeyContext, Length, MouseButton, MouseDownEvent, MouseEvent,
39    MouseMoveEvent, MouseUpEvent, Point, ScrollHandle, Stateful, StrikethroughStyle,
40    StyleRefinement, StyledImage, StyledText, Subscription, Task, TextAlign, TextLayout, TextRun,
41    TextStyle, TextStyleRefinement, WrappedLineLayout, actions, canvas, img, point, quad,
42};
43use language::{CharClassifier, Language, LanguageRegistry, Rope};
44use parser::CodeBlockMetadata;
45use parser::{
46    MarkdownEvent, MarkdownTag, MarkdownTagEnd, ParsedMetadataBlock, parse_links_only,
47    parse_markdown_with_options,
48};
49use pulldown_cmark::{Alignment, BlockQuoteKind};
50use sum_tree::TreeMap;
51use theme::SyntaxTheme;
52use ui::{Checkbox, CopyButton, ScrollAxes, Scrollbars, Tooltip, WithScrollbar, prelude::*};
53use util::ResultExt;
54
55use crate::parser::CodeBlockKind;
56
57/// A callback function that can be used to customize the style of links based on the destination URL.
58/// If the callback returns `None`, the default link style will be used.
59type LinkStyleCallback = Rc<dyn Fn(&str, &App) -> Option<TextStyleRefinement>>;
60pub type CodeSpanLinkCallback = Arc<dyn Fn(&str, &App) -> Option<SharedString> + 'static>;
61type UrlHoverCallback = Rc<dyn Fn(Option<SharedString>, &mut Window, &mut App)>;
62type SourceClickCallback = Box<dyn Fn(usize, usize, &mut Window, &mut App) -> bool>;
63type CheckboxToggleCallback = Rc<dyn Fn(Range<usize>, bool, &mut Window, &mut App)>;
64
65#[derive(Clone, Copy, Default)]
66pub struct BlockQuoteKindColors {
67    pub note: Hsla,
68    pub tip: Hsla,
69    pub important: Hsla,
70    pub warning: Hsla,
71    pub caution: Hsla,
72}
73
74impl BlockQuoteKindColors {
75    fn for_kind(&self, kind: Option<BlockQuoteKind>, default: Hsla) -> Hsla {
76        match kind {
77            Some(BlockQuoteKind::Note) => self.note,
78            Some(BlockQuoteKind::Tip) => self.tip,
79            Some(BlockQuoteKind::Important) => self.important,
80            Some(BlockQuoteKind::Warning) => self.warning,
81            Some(BlockQuoteKind::Caution) => self.caution,
82            None => default,
83        }
84    }
85}
86
87#[derive(Clone, Default)]
88pub struct HeadingLevelStyles {
89    pub h1: Option<TextStyleRefinement>,
90    pub h2: Option<TextStyleRefinement>,
91    pub h3: Option<TextStyleRefinement>,
92    pub h4: Option<TextStyleRefinement>,
93    pub h5: Option<TextStyleRefinement>,
94    pub h6: Option<TextStyleRefinement>,
95}
96
97#[derive(Clone)]
98pub struct MarkdownStyle {
99    pub base_text_style: TextStyle,
100    pub container_style: StyleRefinement,
101    pub code_block: StyleRefinement,
102    pub code_block_overflow_x_scroll: bool,
103    pub inline_code: TextStyleRefinement,
104    pub block_quote: TextStyleRefinement,
105    pub link: TextStyleRefinement,
106    pub link_callback: Option<LinkStyleCallback>,
107    pub rule_color: Hsla,
108    pub block_quote_border_color: Hsla,
109    pub block_quote_kind_colors: BlockQuoteKindColors,
110    pub syntax: Arc<SyntaxTheme>,
111    pub selection_background_color: Hsla,
112    pub heading: StyleRefinement,
113    pub heading_level_styles: Option<HeadingLevelStyles>,
114    pub heading_border_color: Option<Hsla>,
115    pub height_is_multiple_of_line_height: bool,
116    pub prevent_mouse_interaction: bool,
117    pub table_columns_min_size: bool,
118    pub soft_break_as_hard_break: bool,
119}
120
121impl Default for MarkdownStyle {
122    fn default() -> Self {
123        Self {
124            base_text_style: Default::default(),
125            container_style: Default::default(),
126            code_block: Default::default(),
127            code_block_overflow_x_scroll: false,
128            inline_code: Default::default(),
129            block_quote: Default::default(),
130            link: Default::default(),
131            link_callback: None,
132            rule_color: Default::default(),
133            block_quote_border_color: Default::default(),
134            block_quote_kind_colors: Default::default(),
135            syntax: Arc::new(SyntaxTheme::default()),
136            selection_background_color: Default::default(),
137            heading: Default::default(),
138            heading_level_styles: None,
139            heading_border_color: None,
140            height_is_multiple_of_line_height: false,
141            prevent_mouse_interaction: false,
142            table_columns_min_size: false,
143            soft_break_as_hard_break: false,
144        }
145    }
146}
147
148#[derive(Clone, Copy)]
149pub enum MarkdownFont {
150    Agent,
151    Editor,
152    Preview,
153}
154
155impl MarkdownStyle {
156    pub fn themed(font: MarkdownFont, window: &Window, cx: &App) -> Self {
157        let colors = cx.theme().colors();
158        let syntax = cx.theme().syntax().clone();
159        Self::themed_with_overrides(font, colors, &syntax, window, cx)
160    }
161
162    /// Like [`Self::themed`], but takes explicit [`ThemeColors`] and
163    /// [`SyntaxTheme`] so callers (e.g. the markdown preview) can render the
164    /// markdown using a theme other than the active editor theme.
165    pub fn themed_with_overrides(
166        font: MarkdownFont,
167        colors: &theme::ThemeColors,
168        syntax: &Arc<SyntaxTheme>,
169        window: &Window,
170        cx: &App,
171    ) -> Self {
172        let theme_settings = ThemeSettings::get_global(cx);
173        let is_preview = matches!(font, MarkdownFont::Preview);
174
175        let buffer_font_weight = theme_settings.buffer_font.weight;
176        let (buffer_font_size, ui_font_size) = match font {
177            MarkdownFont::Agent => (
178                theme_settings.agent_buffer_font_size(cx),
179                theme_settings.agent_ui_font_size(cx),
180            ),
181            MarkdownFont::Editor => (
182                theme_settings.buffer_font_size(cx),
183                theme_settings.ui_font_size(cx),
184            ),
185            MarkdownFont::Preview => (
186                theme_settings.markdown_preview_font_size(cx),
187                theme_settings.ui_font_size(cx),
188            ),
189        };
190
191        let body_font_family = if is_preview {
192            theme_settings.markdown_preview_font_family().clone()
193        } else {
194            theme_settings.ui_font.family.clone()
195        };
196        let code_font_family = if is_preview {
197            theme_settings.markdown_preview_code_font_family().clone()
198        } else {
199            theme_settings.buffer_font.family.clone()
200        };
201
202        let mut text_style = window.text_style();
203        let line_height = buffer_font_size * 1.75;
204
205        text_style.refine(&TextStyleRefinement {
206            font_family: Some(body_font_family),
207            font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
208            font_features: Some(theme_settings.ui_font.features.clone()),
209            font_size: Some(if is_preview {
210                rems(1.0).into()
211            } else {
212                ui_font_size.into()
213            }),
214            line_height: Some(line_height.into()),
215            color: Some(colors.text),
216            ..Default::default()
217        });
218
219        let style = MarkdownStyle {
220            base_text_style: text_style.clone(),
221            syntax: syntax.clone(),
222            selection_background_color: colors.element_selection_background,
223            rule_color: colors.border,
224            block_quote_border_color: colors.border,
225            block_quote_kind_colors: {
226                let status = cx.theme().status();
227                BlockQuoteKindColors {
228                    note: status.info,
229                    tip: status.success,
230                    important: status.info,
231                    warning: status.warning,
232                    caution: status.error,
233                }
234            },
235            code_block_overflow_x_scroll: true,
236            code_block: StyleRefinement {
237                padding: EdgesRefinement {
238                    top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
239                    left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
240                    right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
241                    bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
242                },
243                margin: EdgesRefinement {
244                    top: Some(Length::Definite(px(8.).into())),
245                    left: Some(Length::Definite(px(0.).into())),
246                    right: Some(Length::Definite(px(0.).into())),
247                    bottom: Some(Length::Definite(px(12.).into())),
248                },
249                border_style: Some(BorderStyle::Solid),
250                border_widths: EdgesRefinement {
251                    top: Some(AbsoluteLength::Pixels(px(1.))),
252                    left: Some(AbsoluteLength::Pixels(px(1.))),
253                    right: Some(AbsoluteLength::Pixels(px(1.))),
254                    bottom: Some(AbsoluteLength::Pixels(px(1.))),
255                },
256                border_color: Some(colors.border_variant),
257                background: Some(colors.editor_background.into()),
258                text: TextStyleRefinement {
259                    font_family: Some(code_font_family.clone()),
260                    font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
261                    font_features: Some(theme_settings.buffer_font.features.clone()),
262                    font_size: Some(buffer_font_size.into()),
263                    font_weight: Some(buffer_font_weight),
264                    ..Default::default()
265                },
266                ..Default::default()
267            },
268            inline_code: TextStyleRefinement {
269                font_family: Some(code_font_family),
270                font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
271                font_features: Some(theme_settings.buffer_font.features.clone()),
272                font_size: Some(buffer_font_size.into()),
273                font_weight: Some(buffer_font_weight),
274                background_color: Some(colors.editor_foreground.opacity(0.08)),
275                ..Default::default()
276            },
277            link: TextStyleRefinement {
278                background_color: Some(colors.editor_foreground.opacity(0.025)),
279                color: Some(colors.text_accent),
280                underline: Some(UnderlineStyle {
281                    color: Some(colors.text_accent.opacity(0.5)),
282                    thickness: px(1.),
283                    ..Default::default()
284                }),
285                ..Default::default()
286            },
287            soft_break_as_hard_break: matches!(font, MarkdownFont::Agent),
288            heading_level_styles: matches!(font, MarkdownFont::Agent).then_some(
289                HeadingLevelStyles {
290                    h1: Some(TextStyleRefinement {
291                        font_size: Some(rems(1.15).into()),
292                        ..Default::default()
293                    }),
294                    h2: Some(TextStyleRefinement {
295                        font_size: Some(rems(1.1).into()),
296                        ..Default::default()
297                    }),
298                    h3: Some(TextStyleRefinement {
299                        font_size: Some(rems(1.05).into()),
300                        ..Default::default()
301                    }),
302                    h4: Some(TextStyleRefinement {
303                        font_size: Some(rems(1.).into()),
304                        ..Default::default()
305                    }),
306                    h5: Some(TextStyleRefinement {
307                        font_size: Some(rems(0.95).into()),
308                        ..Default::default()
309                    }),
310                    h6: Some(TextStyleRefinement {
311                        font_size: Some(rems(0.875).into()),
312                        ..Default::default()
313                    }),
314                },
315            ),
316            ..Default::default()
317        };
318
319        if is_preview {
320            style.with_preview_overrides(colors)
321        } else {
322            style
323        }
324    }
325
326    fn with_preview_overrides(mut self, colors: &theme::ThemeColors) -> Self {
327        let body_font_size = rems(0.92);
328        self.base_text_style.font_size = body_font_size.into();
329        self.container_style.text.font_size = Some(body_font_size.into());
330
331        self.base_text_style.color = colors.text_muted.blend(colors.text.opacity(0.25));
332        self.inline_code.color = Some(colors.text);
333        self.heading.text.color = Some(colors.text);
334
335        self.heading_level_styles = Some(HeadingLevelStyles {
336            h1: Some(TextStyleRefinement {
337                font_size: Some(rems(1.45).into()),
338                ..Default::default()
339            }),
340            h2: Some(TextStyleRefinement {
341                font_size: Some(rems(1.3).into()),
342                ..Default::default()
343            }),
344            h3: Some(TextStyleRefinement {
345                font_size: Some(rems(1.1).into()),
346                ..Default::default()
347            }),
348            h4: Some(TextStyleRefinement {
349                font_size: Some(rems(1.01).into()),
350                ..Default::default()
351            }),
352            h5: Some(TextStyleRefinement {
353                font_size: Some(rems(0.95).into()),
354                ..Default::default()
355            }),
356            h6: Some(TextStyleRefinement {
357                font_size: Some(rems(0.85).into()),
358                ..Default::default()
359            }),
360        });
361
362        self.heading_border_color = Some(colors.border_variant);
363
364        self
365    }
366
367    pub fn with_buffer_font(mut self, cx: &App) -> Self {
368        let theme_settings = ThemeSettings::get_global(cx);
369        self.base_text_style.font_family = theme_settings.buffer_font.family.clone();
370        self.base_text_style.font_fallbacks = theme_settings.buffer_font.fallbacks.clone();
371        self.base_text_style.font_features = theme_settings.buffer_font.features.clone();
372        self.base_text_style.font_weight = theme_settings.buffer_font.weight;
373        self
374    }
375
376    pub fn with_muted_text(mut self, cx: &App) -> Self {
377        let colors = cx.theme().colors();
378        self.base_text_style.color = colors.text_muted;
379        self
380    }
381}
382
383pub struct Markdown {
384    source: SharedString,
385    selection: Selection,
386    pressed_link: Option<RenderedLink>,
387    pressed_footnote_ref: Option<RenderedFootnoteRef>,
388    autoscroll_request: Option<usize>,
389    active_root_block: Option<usize>,
390    parsed_markdown: ParsedMarkdown,
391    images_by_source_offset: HashMap<usize, Arc<Image>>,
392    should_reparse: bool,
393    pending_parse: Option<Task<()>>,
394    focus_handle: FocusHandle,
395    language_registry: Option<Arc<LanguageRegistry>>,
396    fallback_code_block_language: Option<LanguageName>,
397    options: MarkdownOptions,
398    mermaid_state: MermaidState,
399    _mermaid_theme_subscription: Option<Subscription>,
400    mermaid_showing_code: HashSet<usize>,
401    copied_code_blocks: HashSet<ElementId>,
402    wrapped_code_blocks: HashSet<usize>,
403    code_block_scroll_handles: BTreeMap<usize, ScrollHandle>,
404    context_menu_link: Option<SharedString>,
405    context_menu_selected_text: Option<SharedString>,
406    context_menu_selected_markdown: Option<SharedString>,
407    search_highlights: Vec<Range<usize>>,
408    active_search_highlight: Option<usize>,
409}
410
411#[derive(Clone, Copy, Default)]
412pub struct MarkdownOptions {
413    pub parse_links_only: bool,
414    pub parse_html: bool,
415    pub render_mermaid_diagrams: bool,
416    pub parse_heading_slugs: bool,
417    pub render_metadata_blocks: bool,
418}
419
420#[derive(Clone, Copy, PartialEq, Eq)]
421pub enum CopyButtonVisibility {
422    Hidden,
423    AlwaysVisible,
424    VisibleOnHover,
425}
426
427#[derive(Debug, Clone, Copy, PartialEq, Eq)]
428pub enum WrapButtonVisibility {
429    Hidden,
430    AlwaysVisible,
431    VisibleOnHover,
432}
433
434pub enum CodeBlockRenderer {
435    Default {
436        copy_button_visibility: CopyButtonVisibility,
437        wrap_button_visibility: WrapButtonVisibility,
438        border: bool,
439    },
440    Custom {
441        render: CodeBlockRenderFn,
442        /// A function that can modify the parent container after the code block
443        /// content has been appended as a child element.
444        transform: Option<CodeBlockTransformFn>,
445    },
446}
447
448pub type CodeBlockRenderFn = Arc<
449    dyn Fn(
450        &CodeBlockKind,
451        &ParsedMarkdown,
452        Range<usize>,
453        CodeBlockMetadata,
454        &mut Window,
455        &App,
456    ) -> Div,
457>;
458
459pub type CodeBlockTransformFn =
460    Arc<dyn Fn(AnyDiv, Range<usize>, CodeBlockMetadata, &mut Window, &App) -> AnyDiv>;
461
462actions!(
463    markdown,
464    [
465        /// Copies the selected text to the clipboard.
466        Copy,
467        /// Copies the selected text as markdown to the clipboard.
468        CopyAsMarkdown
469    ]
470);
471
472enum EscapeAction {
473    PassThrough,
474    Nbsp(usize),
475    DoubleNewline,
476    PrefixBackslash,
477}
478
479impl EscapeAction {
480    fn output_len(&self, c: char) -> usize {
481        match self {
482            Self::PassThrough => c.len_utf8(),
483            Self::Nbsp(count) => count * '\u{00A0}'.len_utf8(),
484            Self::DoubleNewline => 2,
485            Self::PrefixBackslash => '\\'.len_utf8() + c.len_utf8(),
486        }
487    }
488
489    fn write_to(&self, c: char, output: &mut String) {
490        match self {
491            Self::PassThrough => output.push(c),
492            Self::Nbsp(count) => {
493                for _ in 0..*count {
494                    output.push('\u{00A0}');
495                }
496            }
497            Self::DoubleNewline => {
498                output.push('\n');
499                output.push('\n');
500            }
501            Self::PrefixBackslash => {
502                // '\\' is a single backslash in Rust, e.g. '|' -> '\|'
503                output.push('\\');
504                output.push(c);
505            }
506        }
507    }
508}
509
510struct MarkdownEscaper {
511    in_leading_whitespace: bool,
512}
513
514impl MarkdownEscaper {
515    const TAB_SIZE: usize = 4;
516
517    fn new() -> Self {
518        Self {
519            in_leading_whitespace: true,
520        }
521    }
522
523    fn next(&mut self, c: char) -> EscapeAction {
524        let action = if self.in_leading_whitespace && c == '\t' {
525            EscapeAction::Nbsp(Self::TAB_SIZE)
526        } else if self.in_leading_whitespace && c == ' ' {
527            EscapeAction::Nbsp(1)
528        } else if c == '\n' {
529            EscapeAction::DoubleNewline
530        } else if c.is_ascii_punctuation() {
531            EscapeAction::PrefixBackslash
532        } else {
533            EscapeAction::PassThrough
534        };
535
536        self.in_leading_whitespace =
537            c == '\n' || (self.in_leading_whitespace && (c == ' ' || c == '\t'));
538        action
539    }
540}
541
542impl Markdown {
543    pub fn new(
544        source: SharedString,
545        language_registry: Option<Arc<LanguageRegistry>>,
546        fallback_code_block_language: Option<LanguageName>,
547        cx: &mut Context<Self>,
548    ) -> Self {
549        Self::new_with_options(
550            source,
551            language_registry,
552            fallback_code_block_language,
553            MarkdownOptions::default(),
554            cx,
555        )
556    }
557
558    pub fn new_with_options(
559        source: SharedString,
560        language_registry: Option<Arc<LanguageRegistry>>,
561        fallback_code_block_language: Option<LanguageName>,
562        options: MarkdownOptions,
563        cx: &mut Context<Self>,
564    ) -> Self {
565        let focus_handle = cx.focus_handle();
566
567        let theme_subscription = if options.render_mermaid_diagrams {
568            Some(
569                cx.observe_global::<theme::GlobalTheme>(|this: &mut Self, cx| {
570                    this.invalidate_mermaid_cache(cx);
571                }),
572            )
573        } else {
574            None
575        };
576        let mut this = Self {
577            source,
578            selection: Selection::default(),
579            pressed_link: None,
580            pressed_footnote_ref: None,
581            autoscroll_request: None,
582            active_root_block: None,
583            should_reparse: false,
584            images_by_source_offset: Default::default(),
585            parsed_markdown: ParsedMarkdown::default(),
586            pending_parse: None,
587            focus_handle,
588            language_registry,
589            fallback_code_block_language,
590            options,
591            mermaid_state: MermaidState::default(),
592            _mermaid_theme_subscription: theme_subscription,
593            mermaid_showing_code: HashSet::default(),
594            copied_code_blocks: HashSet::default(),
595            wrapped_code_blocks: HashSet::default(),
596            code_block_scroll_handles: BTreeMap::default(),
597            context_menu_link: None,
598            context_menu_selected_text: None,
599            context_menu_selected_markdown: None,
600            search_highlights: Vec::new(),
601            active_search_highlight: None,
602        };
603        this.parse(cx);
604        this
605    }
606
607    pub fn new_text(source: SharedString, cx: &mut Context<Self>) -> Self {
608        Self::new_with_options(
609            source,
610            None,
611            None,
612            MarkdownOptions {
613                parse_links_only: true,
614                ..Default::default()
615            },
616            cx,
617        )
618    }
619
620    fn is_code_block_wrapped(&self, id: usize) -> bool {
621        self.wrapped_code_blocks.contains(&id)
622    }
623
624    fn toggle_code_block_wrap(&mut self, id: usize) {
625        if !self.wrapped_code_blocks.remove(&id) {
626            self.wrapped_code_blocks.insert(id);
627        }
628    }
629
630    fn code_block_scroll_handle(&mut self, id: usize) -> Option<ScrollHandle> {
631        (!self.is_code_block_wrapped(id)).then(|| {
632            self.code_block_scroll_handles
633                .entry(id)
634                .or_insert_with(ScrollHandle::new)
635                .clone()
636        })
637    }
638
639    fn retain_code_block_scroll_handles(&mut self, ids: &HashSet<usize>) {
640        self.code_block_scroll_handles
641            .retain(|id, _| ids.contains(id));
642    }
643
644    pub fn invalidate_mermaid_cache(&mut self, cx: &mut Context<Self>) {
645        if !self.options.render_mermaid_diagrams || self.parsed_markdown.mermaid_diagrams.is_empty()
646        {
647            return;
648        }
649
650        self.mermaid_state.clear();
651        self.mermaid_state.update(&self.parsed_markdown, cx);
652        cx.notify();
653    }
654
655    pub(crate) fn is_mermaid_showing_code(&self, source_offset: usize) -> bool {
656        self.mermaid_showing_code.contains(&source_offset)
657    }
658
659    pub(crate) fn toggle_mermaid_tab(&mut self, source_offset: usize) {
660        if !self.mermaid_showing_code.remove(&source_offset) {
661            self.mermaid_showing_code.insert(source_offset);
662        }
663    }
664
665    fn clear_code_block_scroll_handles(&mut self) {
666        self.code_block_scroll_handles.clear();
667    }
668
669    fn autoscroll_code_block(&self, source_index: usize, cursor_position: Point<Pixels>) {
670        let Some((_, scroll_handle)) = self
671            .code_block_scroll_handles
672            .range(..=source_index)
673            .next_back()
674        else {
675            return;
676        };
677
678        let bounds = scroll_handle.bounds();
679        if cursor_position.y < bounds.top() || cursor_position.y > bounds.bottom() {
680            return;
681        }
682
683        let horizontal_delta = if cursor_position.x < bounds.left() {
684            bounds.left() - cursor_position.x
685        } else if cursor_position.x > bounds.right() {
686            bounds.right() - cursor_position.x
687        } else {
688            return;
689        };
690
691        let offset = scroll_handle.offset();
692        scroll_handle.set_offset(point(offset.x + horizontal_delta, offset.y));
693    }
694
695    pub fn is_parsing(&self) -> bool {
696        self.pending_parse.is_some()
697    }
698
699    pub fn scroll_to_heading(&mut self, slug: &str, cx: &mut Context<Self>) -> Option<usize> {
700        if let Some(source_index) = self.parsed_markdown.heading_slugs.get(slug).copied() {
701            self.autoscroll_request = Some(source_index);
702            cx.notify();
703            Some(source_index)
704        } else {
705            None
706        }
707    }
708
709    pub fn source(&self) -> &SharedString {
710        &self.source
711    }
712
713    pub fn first_code_block_language(&self) -> Option<Arc<Language>> {
714        self.parsed_markdown.events.iter().find_map(|(_, event)| {
715            let MarkdownEvent::Start(MarkdownTag::CodeBlock { kind, .. }) = event else {
716                return None;
717            };
718
719            match kind {
720                CodeBlockKind::FencedLang(language) => self
721                    .parsed_markdown
722                    .languages_by_name
723                    .get(language)
724                    .cloned(),
725                CodeBlockKind::FencedSrc(path_range) => self
726                    .parsed_markdown
727                    .languages_by_path
728                    .get(&path_range.path)
729                    .cloned(),
730                CodeBlockKind::Fenced | CodeBlockKind::Indented => None,
731            }
732        })
733    }
734
735    pub fn append(&mut self, text: &str, cx: &mut Context<Self>) {
736        self.source = SharedString::new(self.source.to_string() + text);
737        self.parse(cx);
738    }
739
740    pub fn replace(&mut self, source: impl Into<SharedString>, cx: &mut Context<Self>) {
741        self.source = source.into();
742        self.parse(cx);
743    }
744
745    pub fn request_autoscroll_to_source_index(
746        &mut self,
747        source_index: usize,
748        cx: &mut Context<Self>,
749    ) {
750        self.autoscroll_request = Some(source_index);
751        cx.refresh_windows();
752    }
753
754    fn footnote_definition_content_start(&self, label: &SharedString) -> Option<usize> {
755        self.parsed_markdown
756            .footnote_definitions
757            .get(label)
758            .copied()
759    }
760
761    pub fn set_active_root_for_source_index(
762        &mut self,
763        source_index: Option<usize>,
764        cx: &mut Context<Self>,
765    ) {
766        let active_root_block =
767            source_index.and_then(|index| self.parsed_markdown.root_block_for_source_index(index));
768        if self.active_root_block == active_root_block {
769            return;
770        }
771
772        self.active_root_block = active_root_block;
773        cx.notify();
774    }
775
776    pub fn reset(&mut self, source: SharedString, cx: &mut Context<Self>) {
777        if &source == self.source() {
778            return;
779        }
780        self.source = source;
781        self.selection = Selection::default();
782        self.autoscroll_request = None;
783        self.pending_parse = None;
784        self.should_reparse = false;
785        self.search_highlights.clear();
786        self.active_search_highlight = None;
787        // Don't clear parsed_markdown here - keep existing content visible until new parse completes
788        self.parse(cx);
789    }
790
791    #[cfg(any(test, feature = "test-support"))]
792    pub fn parsed_markdown(&self) -> &ParsedMarkdown {
793        &self.parsed_markdown
794    }
795
796    pub fn escape(s: &str) -> Cow<'_, str> {
797        let output_len: usize = {
798            let mut escaper = MarkdownEscaper::new();
799            s.chars().map(|c| escaper.next(c).output_len(c)).sum()
800        };
801
802        if output_len == s.len() {
803            return s.into();
804        }
805
806        let mut escaper = MarkdownEscaper::new();
807        let mut output = String::with_capacity(output_len);
808        for c in s.chars() {
809            escaper.next(c).write_to(c, &mut output);
810        }
811        output.into()
812    }
813
814    pub fn has_selection(&self) -> bool {
815        self.selection.end > self.selection.start
816    }
817
818    pub fn selected_source(&self) -> Option<&str> {
819        if self.selection.end <= self.selection.start {
820            return None;
821        }
822        self.source.get(self.selection.start..self.selection.end)
823    }
824
825    pub fn set_search_highlights(
826        &mut self,
827        highlights: Vec<Range<usize>>,
828        active: Option<usize>,
829        cx: &mut Context<Self>,
830    ) {
831        debug_assert!(
832            highlights
833                .windows(2)
834                .all(|ranges| (ranges[0].start, ranges[0].end) <= (ranges[1].start, ranges[1].end))
835        );
836        self.search_highlights = highlights;
837        self.active_search_highlight =
838            active.filter(|active| *active < self.search_highlights.len());
839        cx.notify();
840    }
841
842    pub fn clear_search_highlights(&mut self, cx: &mut Context<Self>) {
843        if !self.search_highlights.is_empty() || self.active_search_highlight.is_some() {
844            self.search_highlights.clear();
845            self.active_search_highlight = None;
846            cx.notify();
847        }
848    }
849
850    pub fn set_active_search_highlight(&mut self, active: Option<usize>, cx: &mut Context<Self>) {
851        let active = active.filter(|active| *active < self.search_highlights.len());
852        if self.active_search_highlight != active {
853            self.active_search_highlight = active;
854            cx.notify();
855        }
856    }
857
858    pub fn search_highlights(&self) -> &[Range<usize>] {
859        &self.search_highlights
860    }
861
862    pub fn active_search_highlight(&self) -> Option<usize> {
863        self.active_search_highlight
864    }
865
866    fn copy(&self, text: &RenderedText, _: &mut Window, cx: &mut Context<Self>) {
867        if self.selection.end <= self.selection.start {
868            return;
869        }
870        let text = text.text_for_range(self.selection.start..self.selection.end);
871        cx.write_to_clipboard(ClipboardItem::new_string(text));
872    }
873
874    fn copy_as_markdown(&mut self, _: &mut Window, cx: &mut Context<Self>) {
875        if let Some(text) = self.context_menu_selected_markdown.take() {
876            cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
877            return;
878        }
879        if self.selection.end <= self.selection.start {
880            return;
881        }
882        let text = self
883            .parsed_markdown
884            .rebalanced_markdown_for_selection(self.selection.start..self.selection.end);
885        cx.write_to_clipboard(ClipboardItem::new_string(text));
886    }
887
888    fn capture_for_context_menu(
889        &mut self,
890        link: Option<SharedString>,
891        rendered_text: Option<&RenderedText>,
892    ) {
893        let range = self.selection.start..self.selection.end;
894        if range.end > range.start {
895            self.context_menu_selected_markdown = Some(SharedString::new(
896                self.parsed_markdown
897                    .rebalanced_markdown_for_selection(range.clone()),
898            ));
899            self.context_menu_selected_text = rendered_text
900                .map(|text| text.text_for_range(range))
901                .map(SharedString::new)
902                .or_else(|| self.context_menu_selected_markdown.clone());
903        } else {
904            self.context_menu_selected_markdown = None;
905            self.context_menu_selected_text = None;
906        }
907        self.context_menu_link = link;
908    }
909
910    /// Returns the URL of the link that was most recently right-clicked, if any.
911    /// This is set during a right-click mouse-down event and can be read by parent
912    /// views to include a "Copy Link" item in their context menus.
913    pub fn context_menu_link(&self) -> Option<&SharedString> {
914        self.context_menu_link.as_ref()
915    }
916
917    /// Returns the rendered (plain) text that was selected when the most recent
918    /// context menu invocation happened.
919    pub fn context_menu_selected_text(&self) -> Option<&SharedString> {
920        self.context_menu_selected_text.as_ref()
921    }
922
923    /// Returns the markdown that was selected when the most recent context
924    /// menu invocation happened, rebalanced via
925    /// [`ParsedMarkdown::rebalanced_markdown_for_selection`].
926    pub fn context_menu_selected_markdown(&self) -> Option<&SharedString> {
927        self.context_menu_selected_markdown.as_ref()
928    }
929
930    fn parse(&mut self, cx: &mut Context<Self>) {
931        if self.source.is_empty() {
932            self.should_reparse = false;
933            self.pending_parse.take();
934            self.parsed_markdown = ParsedMarkdown {
935                source: self.source.clone(),
936                ..Default::default()
937            };
938            self.active_root_block = None;
939            self.images_by_source_offset.clear();
940            self.mermaid_state.clear();
941            cx.notify();
942            cx.refresh_windows();
943            return;
944        }
945
946        if self.pending_parse.is_some() {
947            self.should_reparse = true;
948            return;
949        }
950        self.should_reparse = false;
951        self.pending_parse = Some(self.start_background_parse(cx));
952    }
953
954    fn start_background_parse(&self, cx: &Context<Self>) -> Task<()> {
955        let source = self.source.clone();
956        let should_parse_links_only = self.options.parse_links_only;
957        let should_parse_html = self.options.parse_html;
958        let should_render_mermaid_diagrams = self.options.render_mermaid_diagrams;
959        let should_parse_heading_slugs = self.options.parse_heading_slugs;
960        let should_parse_metadata_blocks = self.options.render_metadata_blocks;
961        let language_registry = self.language_registry.clone();
962        let fallback = self.fallback_code_block_language.clone();
963
964        let parsed = cx.background_spawn(async move {
965            if should_parse_links_only {
966                return (
967                    ParsedMarkdown {
968                        events: Arc::from(parse_links_only(source.as_ref())),
969                        source,
970                        languages_by_name: TreeMap::default(),
971                        languages_by_path: TreeMap::default(),
972                        root_block_starts: Arc::default(),
973                        html_blocks: BTreeMap::default(),
974                        metadata_blocks: BTreeMap::default(),
975                        mermaid_diagrams: BTreeMap::default(),
976                        heading_slugs: HashMap::default(),
977                        footnote_definitions: HashMap::default(),
978                    },
979                    Default::default(),
980                );
981            }
982
983            let parsed = parse_markdown_with_options(
984                &source,
985                should_parse_html,
986                should_parse_heading_slugs,
987                should_parse_metadata_blocks,
988            );
989            let events = parsed.events;
990            let language_names = parsed.language_names;
991            let paths = parsed.language_paths;
992            let root_block_starts = parsed.root_block_starts;
993            let html_blocks = parsed.html_blocks;
994            let metadata_blocks = parsed.metadata_blocks;
995            let heading_slugs = parsed.heading_slugs;
996            let footnote_definitions = parsed.footnote_definitions;
997            let mermaid_diagrams = if should_render_mermaid_diagrams {
998                extract_mermaid_diagrams(&source, &events)
999            } else {
1000                BTreeMap::default()
1001            };
1002            let mut images_by_source_offset = HashMap::default();
1003            let mut languages_by_name = TreeMap::default();
1004            let mut languages_by_path = TreeMap::default();
1005            if let Some(registry) = language_registry.as_ref() {
1006                for name in language_names {
1007                    let language = if !name.is_empty() {
1008                        registry.language_for_name_or_extension(&name).left_future()
1009                    } else if let Some(fallback) = &fallback {
1010                        registry.language_for_name(fallback.as_ref()).right_future()
1011                    } else {
1012                        continue;
1013                    };
1014                    if let Ok(language) = language.await {
1015                        languages_by_name.insert(name, language);
1016                    }
1017                }
1018
1019                for path in paths {
1020                    if let Ok(language) = registry
1021                        .load_language_for_file_path(Path::new(path.as_ref()))
1022                        .await
1023                    {
1024                        languages_by_path.insert(path, language);
1025                    }
1026                }
1027            }
1028
1029            for (range, event) in &events {
1030                if let MarkdownEvent::Start(MarkdownTag::Image { dest_url, .. }) = event
1031                    && let Some(data_url) = dest_url.strip_prefix("data:")
1032                {
1033                    let Some((mime_info, data)) = data_url.split_once(',') else {
1034                        continue;
1035                    };
1036                    let Some((mime_type, encoding)) = mime_info.split_once(';') else {
1037                        continue;
1038                    };
1039                    let Some(format) = ImageFormat::from_mime_type(mime_type) else {
1040                        continue;
1041                    };
1042                    let is_base64 = encoding == "base64";
1043                    if is_base64
1044                        && let Some(bytes) = base64::prelude::BASE64_STANDARD
1045                            .decode(data)
1046                            .log_with_level(Level::Debug)
1047                    {
1048                        let image = Arc::new(Image::from_bytes(format, bytes));
1049                        images_by_source_offset.insert(range.start, image);
1050                    }
1051                }
1052            }
1053
1054            (
1055                ParsedMarkdown {
1056                    source,
1057                    events: Arc::from(events),
1058                    languages_by_name,
1059                    languages_by_path,
1060                    root_block_starts: Arc::from(root_block_starts),
1061                    html_blocks,
1062                    metadata_blocks,
1063                    mermaid_diagrams,
1064                    heading_slugs,
1065                    footnote_definitions,
1066                },
1067                images_by_source_offset,
1068            )
1069        });
1070
1071        cx.spawn(async move |this, cx| {
1072            let (parsed, images_by_source_offset) = parsed.await;
1073
1074            this.update(cx, |this, cx| {
1075                this.parsed_markdown = parsed;
1076                this.images_by_source_offset = images_by_source_offset;
1077                if this.active_root_block.is_some_and(|block_index| {
1078                    block_index >= this.parsed_markdown.root_block_starts.len()
1079                }) {
1080                    this.active_root_block = None;
1081                }
1082                if this.options.render_mermaid_diagrams {
1083                    let parsed_markdown = this.parsed_markdown.clone();
1084                    this.mermaid_state.update(&parsed_markdown, cx);
1085                    this.mermaid_showing_code
1086                        .retain(|offset| parsed_markdown.mermaid_diagrams.contains_key(offset));
1087                } else {
1088                    this.mermaid_state.clear();
1089                    this.mermaid_showing_code.clear();
1090                }
1091                this.pending_parse.take();
1092                if this.should_reparse {
1093                    this.parse(cx);
1094                }
1095                cx.notify();
1096                cx.refresh_windows();
1097            })
1098            .ok();
1099        })
1100    }
1101}
1102
1103impl Focusable for Markdown {
1104    fn focus_handle(&self, _cx: &App) -> FocusHandle {
1105        self.focus_handle.clone()
1106    }
1107}
1108
1109#[derive(Debug, Default, Clone)]
1110enum SelectMode {
1111    #[default]
1112    Character,
1113    Word(Range<usize>),
1114    Line(Range<usize>),
1115    All,
1116}
1117
1118#[derive(Clone, Default)]
1119struct Selection {
1120    start: usize,
1121    end: usize,
1122    reversed: bool,
1123    pending: bool,
1124    mode: SelectMode,
1125}
1126
1127impl Selection {
1128    fn set_head(&mut self, head: usize, rendered_text: &RenderedText) {
1129        match &self.mode {
1130            SelectMode::Character => {
1131                if head < self.tail() {
1132                    if !self.reversed {
1133                        self.end = self.start;
1134                        self.reversed = true;
1135                    }
1136                    self.start = head;
1137                } else {
1138                    if self.reversed {
1139                        self.start = self.end;
1140                        self.reversed = false;
1141                    }
1142                    self.end = head;
1143                }
1144            }
1145            SelectMode::Word(original_range) | SelectMode::Line(original_range) => {
1146                let head_range = if matches!(self.mode, SelectMode::Word(_)) {
1147                    rendered_text.surrounding_word_range(head)
1148                } else {
1149                    rendered_text.surrounding_line_range(head)
1150                };
1151
1152                if head < original_range.start {
1153                    self.start = head_range.start;
1154                    self.end = original_range.end;
1155                    self.reversed = true;
1156                } else if head >= original_range.end {
1157                    self.start = original_range.start;
1158                    self.end = head_range.end;
1159                    self.reversed = false;
1160                } else {
1161                    self.start = original_range.start;
1162                    self.end = original_range.end;
1163                    self.reversed = false;
1164                }
1165            }
1166            SelectMode::All => {
1167                self.start = 0;
1168                self.end = rendered_text
1169                    .lines
1170                    .last()
1171                    .map(|line| line.source_end)
1172                    .unwrap_or(0);
1173                self.reversed = false;
1174            }
1175        }
1176    }
1177
1178    fn tail(&self) -> usize {
1179        if self.reversed { self.end } else { self.start }
1180    }
1181}
1182
1183#[derive(Debug, Clone, Default)]
1184pub struct ParsedMarkdown {
1185    pub source: SharedString,
1186    pub events: Arc<[(Range<usize>, MarkdownEvent)]>,
1187    pub languages_by_name: TreeMap<SharedString, Arc<Language>>,
1188    pub languages_by_path: TreeMap<Arc<str>, Arc<Language>>,
1189    pub root_block_starts: Arc<[usize]>,
1190    pub(crate) html_blocks: BTreeMap<usize, html::html_parser::ParsedHtmlBlock>,
1191    pub(crate) metadata_blocks: BTreeMap<usize, ParsedMetadataBlock>,
1192    pub(crate) mermaid_diagrams: BTreeMap<usize, ParsedMarkdownMermaidDiagram>,
1193    pub heading_slugs: HashMap<SharedString, usize>,
1194    pub footnote_definitions: HashMap<SharedString, usize>,
1195}
1196
1197impl ParsedMarkdown {
1198    pub fn source(&self) -> &SharedString {
1199        &self.source
1200    }
1201
1202    pub fn events(&self) -> &Arc<[(Range<usize>, MarkdownEvent)]> {
1203        &self.events
1204    }
1205
1206    pub fn root_block_starts(&self) -> &Arc<[usize]> {
1207        &self.root_block_starts
1208    }
1209
1210    pub fn root_block_for_source_index(&self, source_index: usize) -> Option<usize> {
1211        if self.root_block_starts.is_empty() {
1212            return None;
1213        }
1214
1215        let partition = self
1216            .root_block_starts
1217            .partition_point(|block_start| *block_start <= source_index);
1218
1219        Some(partition.saturating_sub(1))
1220    }
1221
1222    /// Extracts the markdown source for a selection, rebalancing inline
1223    /// delimiters (`**`, backticks, link syntax, etc.) so partial selections of
1224    /// styled spans stay well-formed.
1225    ///
1226    /// With an exception of a single inline code span, which is returned as plain
1227    /// text, since copying a command or identifier is the dominant use case there.
1228    pub fn rebalanced_markdown_for_selection(&self, selection: Range<usize>) -> String {
1229        selection::rebalanced_markdown_for_selection(
1230            &self.source,
1231            &self.events,
1232            &self.root_block_starts,
1233            selection,
1234        )
1235    }
1236}
1237
1238pub enum AutoscrollBehavior {
1239    /// Propagate the request up the element tree for the nearest
1240    /// scrollable ancestor (e.g. `List`) to handle.
1241    Propagate,
1242    /// Directly control a specific scroll handle.
1243    Controlled(ScrollHandle),
1244}
1245
1246pub struct MarkdownElement {
1247    markdown: Entity<Markdown>,
1248    style: MarkdownStyle,
1249    code_block_renderer: CodeBlockRenderer,
1250    on_url_click: Option<Rc<dyn Fn(SharedString, &mut Window, &mut App)>>,
1251    on_url_hover: Option<UrlHoverCallback>,
1252    code_span_link: Option<CodeSpanLinkCallback>,
1253    on_source_click: Option<SourceClickCallback>,
1254    on_checkbox_toggle: Option<CheckboxToggleCallback>,
1255    image_resolver: Option<Box<dyn Fn(&str) -> Option<ImageSource>>>,
1256    show_root_block_markers: bool,
1257    autoscroll: AutoscrollBehavior,
1258}
1259
1260impl MarkdownElement {
1261    pub fn new(markdown: Entity<Markdown>, style: MarkdownStyle) -> Self {
1262        Self {
1263            markdown,
1264            style,
1265            code_block_renderer: CodeBlockRenderer::Default {
1266                copy_button_visibility: CopyButtonVisibility::VisibleOnHover,
1267                wrap_button_visibility: WrapButtonVisibility::Hidden,
1268                border: false,
1269            },
1270            on_url_click: None,
1271            on_url_hover: None,
1272            code_span_link: None,
1273            on_source_click: None,
1274            on_checkbox_toggle: None,
1275            image_resolver: None,
1276            show_root_block_markers: false,
1277            autoscroll: AutoscrollBehavior::Propagate,
1278        }
1279    }
1280
1281    #[cfg(any(test, feature = "test-support"))]
1282    pub fn rendered_text(
1283        markdown: Entity<Markdown>,
1284        cx: &mut gpui::VisualTestContext,
1285        style: impl FnOnce(&Window, &App) -> MarkdownStyle,
1286    ) -> String {
1287        use gpui::size;
1288
1289        let (text, _) = cx.draw(
1290            Default::default(),
1291            size(px(600.0), px(600.0)),
1292            |window, cx| Self::new(markdown, style(window, cx)),
1293        );
1294        text.text
1295            .lines
1296            .iter()
1297            .map(|line| line.layout.wrapped_text())
1298            .collect::<Vec<_>>()
1299            .join("\n")
1300    }
1301
1302    pub fn code_block_renderer(mut self, variant: CodeBlockRenderer) -> Self {
1303        self.code_block_renderer = variant;
1304        self
1305    }
1306
1307    pub fn on_url_click(
1308        mut self,
1309        handler: impl Fn(SharedString, &mut Window, &mut App) + 'static,
1310    ) -> Self {
1311        self.on_url_click = Some(Rc::new(handler));
1312        self
1313    }
1314
1315    pub fn on_url_hover(
1316        mut self,
1317        handler: impl Fn(Option<SharedString>, &mut Window, &mut App) + 'static,
1318    ) -> Self {
1319        self.on_url_hover = Some(Rc::new(handler));
1320        self
1321    }
1322
1323    pub fn on_code_span_link(
1324        mut self,
1325        callback: impl Fn(&str, &App) -> Option<SharedString> + 'static,
1326    ) -> Self {
1327        self.code_span_link = Some(Arc::new(callback));
1328        self
1329    }
1330
1331    pub fn on_source_click(
1332        mut self,
1333        handler: impl Fn(usize, usize, &mut Window, &mut App) -> bool + 'static,
1334    ) -> Self {
1335        self.on_source_click = Some(Box::new(handler));
1336        self
1337    }
1338
1339    pub fn on_checkbox_toggle(
1340        mut self,
1341        handler: impl Fn(Range<usize>, bool, &mut Window, &mut App) + 'static,
1342    ) -> Self {
1343        self.on_checkbox_toggle = Some(Rc::new(handler));
1344        self
1345    }
1346
1347    pub fn image_resolver(
1348        mut self,
1349        resolver: impl Fn(&str) -> Option<ImageSource> + 'static,
1350    ) -> Self {
1351        self.image_resolver = Some(Box::new(resolver));
1352        self
1353    }
1354
1355    pub fn show_root_block_markers(mut self) -> Self {
1356        self.show_root_block_markers = true;
1357        self
1358    }
1359
1360    pub fn scroll_handle(mut self, scroll_handle: ScrollHandle) -> Self {
1361        self.autoscroll = AutoscrollBehavior::Controlled(scroll_handle);
1362        self
1363    }
1364
1365    fn push_markdown_code_span(
1366        &self,
1367        builder: &mut MarkdownElementBuilder,
1368        text: &str,
1369        range: Range<usize>,
1370        cx: &App,
1371    ) {
1372        let link_url = if builder.code_block_stack.is_empty()
1373            && builder.link_depth == 0
1374            && !self.style.prevent_mouse_interaction
1375        {
1376            self.code_span_link
1377                .as_ref()
1378                .and_then(|callback| callback(text, cx))
1379        } else {
1380            None
1381        };
1382
1383        if let Some(url) = link_url {
1384            builder.push_link(url.clone(), range.clone());
1385            let link_style = self
1386                .style
1387                .link_callback
1388                .as_ref()
1389                .and_then(|callback| callback(url.as_ref(), cx))
1390                .unwrap_or_else(|| self.style.link.clone());
1391            builder.push_text_style(self.style.inline_code.clone());
1392            builder.push_text_style(link_style);
1393            builder.push_text(text, range);
1394            builder.pop_text_style();
1395            builder.pop_text_style();
1396        } else {
1397            let mut code_style = self.style.inline_code.clone();
1398            if builder.link_depth > 0 {
1399                code_style.color = self.style.link.color.or(code_style.color);
1400            }
1401            builder.push_text_style(code_style);
1402            builder.push_text(text, range);
1403            builder.pop_text_style();
1404        }
1405    }
1406
1407    fn push_markdown_image(
1408        &self,
1409        builder: &mut MarkdownElementBuilder,
1410        range: &Range<usize>,
1411        source: ImageSource,
1412        dest_url: SharedString,
1413        alt_text: Option<SharedString>,
1414        width: Option<DefiniteLength>,
1415        height: Option<DefiniteLength>,
1416    ) {
1417        let enclosing_link_url = (builder.link_depth > 0)
1418            .then(|| builder.rendered_links.last())
1419            .flatten()
1420            .map(|link| link.destination_url.clone());
1421        let fallback_opens_image_url = enclosing_link_url.is_none();
1422
1423        let image_element = {
1424            let wrapper = div().id(("markdown-image-link", range.start)).min_w_0();
1425            let wrapper = if !self.style.prevent_mouse_interaction
1426                && let Some(url) = enclosing_link_url
1427            {
1428                let click_url = url.clone();
1429                let markdown = self.markdown.clone();
1430                let url_click = self.on_url_click.clone();
1431                let bounds = Rc::new(Cell::new(None));
1432                builder.push_image_link(url.clone(), bounds.clone());
1433                wrapper
1434                    .relative()
1435                    .cursor_pointer()
1436                    .child(
1437                        canvas(
1438                            move |image_bounds, _window, _cx| bounds.set(Some(image_bounds)),
1439                            |_, _, _, _| {},
1440                        )
1441                        .size_full()
1442                        .absolute()
1443                        .top_0()
1444                        .left_0(),
1445                    )
1446                    .on_click(move |_, window, cx| {
1447                        if let Some(ref on_url_click) = url_click {
1448                            on_url_click(click_url.clone(), window, cx);
1449                        } else {
1450                            cx.open_url(&click_url);
1451                        }
1452                    })
1453                    .capture_any_mouse_down(move |event, _window, cx| {
1454                        if event.button == MouseButton::Right {
1455                            markdown.update(cx, |md, _| {
1456                                md.capture_for_context_menu(Some(url.clone()), None)
1457                            });
1458                        }
1459                    })
1460            } else {
1461                wrapper
1462            };
1463            wrapper.child(
1464                img(source)
1465                    .id(("markdown-image", range.start))
1466                    .min_w_0()
1467                    .max_w_full()
1468                    .rounded_md()
1469                    .mr_1()
1470                    .mb_1()
1471                    .when_some(height, |this, height| this.h(height))
1472                    .when_some(width, |this, width| this.w(width))
1473                    .with_fallback(move || {
1474                        image_fallback_element(
1475                            dest_url.clone(),
1476                            alt_text.clone(),
1477                            fallback_opens_image_url,
1478                        )
1479                    }),
1480            )
1481        };
1482
1483        builder.push_image_child(image_element);
1484    }
1485
1486    fn push_markdown_paragraph(
1487        &self,
1488        builder: &mut MarkdownElementBuilder,
1489        range: &Range<usize>,
1490        markdown_end: usize,
1491        text_align_override: Option<TextAlign>,
1492    ) {
1493        let align = text_align_override.unwrap_or(self.style.base_text_style.text_align);
1494        let mut paragraph = div().when(!self.style.height_is_multiple_of_line_height, |el| {
1495            el.mb_2().line_height(rems(1.3))
1496        });
1497
1498        paragraph = match align {
1499            TextAlign::Center => paragraph.text_center(),
1500            TextAlign::Left => paragraph.text_left(),
1501            TextAlign::Right => paragraph.text_right(),
1502        };
1503
1504        builder.push_text_style(TextStyleRefinement {
1505            text_align: Some(align),
1506            ..Default::default()
1507        });
1508        builder.push_div(paragraph, range, markdown_end);
1509    }
1510
1511    fn pop_markdown_paragraph(&self, builder: &mut MarkdownElementBuilder) {
1512        builder.pop_div();
1513        builder.pop_text_style();
1514    }
1515
1516    fn push_markdown_heading(
1517        &self,
1518        builder: &mut MarkdownElementBuilder,
1519        level: pulldown_cmark::HeadingLevel,
1520        range: &Range<usize>,
1521        markdown_end: usize,
1522        text_align_override: Option<TextAlign>,
1523    ) {
1524        let align = text_align_override.unwrap_or(self.style.base_text_style.text_align);
1525        let mut heading = div().mt_4().mb_2();
1526        heading = apply_heading_style(
1527            heading,
1528            level,
1529            self.style.heading_level_styles.as_ref(),
1530            self.style.heading_border_color,
1531        );
1532
1533        heading = match align {
1534            TextAlign::Center => heading.text_center(),
1535            TextAlign::Left => heading.text_left(),
1536            TextAlign::Right => heading.text_right(),
1537        };
1538
1539        let mut heading_style = self.style.heading.clone();
1540        let heading_text_style = heading_style.text_style().clone();
1541        heading.style().refine(&heading_style);
1542
1543        builder.push_text_style(TextStyleRefinement {
1544            text_align: Some(align),
1545            ..heading_text_style
1546        });
1547        builder.push_div(heading, range, markdown_end);
1548    }
1549
1550    fn pop_markdown_heading(&self, builder: &mut MarkdownElementBuilder) {
1551        builder.pop_div();
1552        builder.pop_text_style();
1553    }
1554
1555    fn push_markdown_block_quote(
1556        &self,
1557        builder: &mut MarkdownElementBuilder,
1558        kind: Option<pulldown_cmark::BlockQuoteKind>,
1559        range: &Range<usize>,
1560        markdown_end: usize,
1561    ) {
1562        let border_color = self
1563            .style
1564            .block_quote_kind_colors
1565            .for_kind(kind, self.style.block_quote_border_color);
1566
1567        let header = kind.map(|kind| {
1568            let (icon_name, label) = match kind {
1569                BlockQuoteKind::Note => (IconName::Info, "Note"),
1570                BlockQuoteKind::Tip => (IconName::Sparkle, "Tip"),
1571                BlockQuoteKind::Important => (IconName::Chat, "Important"),
1572                BlockQuoteKind::Warning => (IconName::Warning, "Warning"),
1573                BlockQuoteKind::Caution => (IconName::Stop, "Caution"),
1574            };
1575            h_flex()
1576                .gap_1()
1577                .items_center()
1578                .mb_1()
1579                .child(
1580                    Icon::new(icon_name)
1581                        .size(IconSize::Small)
1582                        .color(Color::Custom(border_color)),
1583                )
1584                .child(
1585                    Label::new(label)
1586                        .color(Color::Custom(border_color))
1587                        .weight(FontWeight::BOLD),
1588                )
1589                .into_any_element()
1590        });
1591
1592        let block_div = div().pl_4().mb_2().border_l_4().border_color(border_color);
1593        let block_div = match header {
1594            Some(header) => block_div.child(header),
1595            None => block_div,
1596        };
1597
1598        builder.push_text_style(self.style.block_quote.clone());
1599        builder.push_div(block_div, range, markdown_end);
1600    }
1601
1602    fn pop_markdown_block_quote(&self, builder: &mut MarkdownElementBuilder) {
1603        builder.pop_div();
1604        builder.pop_text_style();
1605    }
1606
1607    fn push_metadata_block(
1608        &self,
1609        builder: &mut MarkdownElementBuilder,
1610        source: &str,
1611        metadata_block: &ParsedMetadataBlock,
1612        markdown_end: usize,
1613        cx: &App,
1614    ) {
1615        let content_range = &metadata_block.content_range;
1616        if let Some(rows) = metadata_block.rows.as_deref() {
1617            builder.push_div(
1618                div()
1619                    .grid()
1620                    .grid_cols(2)
1621                    .w_full()
1622                    .mb_2()
1623                    .border_1()
1624                    .border_color(cx.theme().colors().border)
1625                    .rounded_sm()
1626                    .overflow_hidden(),
1627                content_range,
1628                markdown_end,
1629            );
1630
1631            for (row_index, row) in rows.iter().enumerate() {
1632                self.push_metadata_cell(
1633                    builder,
1634                    source,
1635                    row.key.clone(),
1636                    content_range,
1637                    markdown_end,
1638                    MetadataCellStyle {
1639                        row_index,
1640                        is_key: true,
1641                    },
1642                    cx,
1643                );
1644                self.push_metadata_cell(
1645                    builder,
1646                    source,
1647                    row.value.clone(),
1648                    content_range,
1649                    markdown_end,
1650                    MetadataCellStyle {
1651                        row_index,
1652                        is_key: false,
1653                    },
1654                    cx,
1655                );
1656            }
1657
1658            builder.pop_div();
1659        } else {
1660            let mut metadata_block = div().w_full().rounded_md();
1661            metadata_block.style().refine(&self.style.code_block);
1662            builder.push_text_style(self.style.code_block.text.to_owned());
1663            builder.push_code_block(None);
1664            builder.push_div(metadata_block, content_range, markdown_end);
1665            builder.push_text(&source[content_range.clone()], content_range.clone());
1666            builder.trim_trailing_newline();
1667            builder.pop_div();
1668            builder.pop_code_block();
1669            builder.pop_text_style();
1670        }
1671    }
1672
1673    fn push_metadata_cell(
1674        &self,
1675        builder: &mut MarkdownElementBuilder,
1676        source: &str,
1677        text_range: Range<usize>,
1678        block_range: &Range<usize>,
1679        markdown_end: usize,
1680        cell_style: MetadataCellStyle,
1681        cx: &App,
1682    ) {
1683        builder.push_div(
1684            div()
1685                .flex()
1686                .flex_col()
1687                .min_w_0()
1688                .px_2()
1689                .py_1()
1690                .border_color(cx.theme().colors().border)
1691                .when(cell_style.row_index > 0, |this| this.border_t_1())
1692                .when(!cell_style.is_key, |this| this.border_l_1())
1693                .when(cell_style.is_key, |this| {
1694                    this.bg(cx.theme().colors().panel_background)
1695                }),
1696            block_range,
1697            markdown_end,
1698        );
1699
1700        let text_style = if cell_style.is_key {
1701            TextStyleRefinement {
1702                color: Some(cx.theme().colors().text_muted),
1703                font_weight: Some(FontWeight::SEMIBOLD),
1704                ..Default::default()
1705            }
1706        } else {
1707            TextStyleRefinement::default()
1708        };
1709        builder.push_text_style(text_style);
1710        builder.push_text(&source[text_range.clone()], text_range);
1711        builder.pop_text_style();
1712        builder.pop_div();
1713    }
1714
1715    fn push_markdown_list_item(
1716        &self,
1717        builder: &mut MarkdownElementBuilder,
1718        bullet: AnyElement,
1719        range: &Range<usize>,
1720        markdown_end: usize,
1721    ) {
1722        builder.push_div(
1723            div()
1724                .when(!self.style.height_is_multiple_of_line_height, |el| {
1725                    el.mb_1().gap_1().line_height(rems(1.3))
1726                })
1727                .h_flex()
1728                .items_start()
1729                .child(bullet),
1730            range,
1731            markdown_end,
1732        );
1733        // Without `w_0`, text doesn't wrap to the width of the container.
1734        builder.push_div(div().flex_1().w_0(), range, markdown_end);
1735    }
1736
1737    fn pop_markdown_list_item(&self, builder: &mut MarkdownElementBuilder) {
1738        builder.pop_div();
1739        builder.pop_div();
1740    }
1741
1742    fn paint_highlight_range(
1743        start: usize,
1744        end: usize,
1745        color: Hsla,
1746        rendered_text: &RenderedText,
1747        window: &mut Window,
1748    ) {
1749        for bounds in rendered_text.bounds_for_source_range(start..end) {
1750            window.paint_quad(quad(
1751                bounds,
1752                Pixels::ZERO,
1753                color,
1754                Edges::default(),
1755                Hsla::transparent_black(),
1756                BorderStyle::default(),
1757            ));
1758        }
1759    }
1760
1761    fn paint_selection(&self, rendered_text: &RenderedText, window: &mut Window, cx: &mut App) {
1762        let selection = self.markdown.read(cx).selection.clone();
1763        Self::paint_highlight_range(
1764            selection.start,
1765            selection.end,
1766            self.style.selection_background_color,
1767            rendered_text,
1768            window,
1769        );
1770    }
1771
1772    fn paint_search_highlights(
1773        &self,
1774        rendered_text: &RenderedText,
1775        window: &mut Window,
1776        cx: &mut App,
1777    ) {
1778        let markdown = self.markdown.read(cx);
1779        let active_index = markdown.active_search_highlight;
1780        let colors = cx.theme().colors();
1781
1782        let highlight_bounds = rendered_text.bounds_for_sorted_source_ranges(
1783            markdown
1784                .search_highlights
1785                .iter()
1786                .enumerate()
1787                .map(|(ix, range)| (ix, range.clone())),
1788        );
1789        for (highlight_ix, bounds) in highlight_bounds {
1790            let color = if Some(highlight_ix) == active_index {
1791                colors.search_active_match_background
1792            } else {
1793                colors.search_match_background
1794            };
1795            window.paint_quad(quad(
1796                bounds,
1797                Pixels::ZERO,
1798                color,
1799                Edges::default(),
1800                Hsla::transparent_black(),
1801                BorderStyle::default(),
1802            ));
1803        }
1804    }
1805
1806    fn paint_mouse_listeners(
1807        &mut self,
1808        hitbox: &Hitbox,
1809        rendered_text: &RenderedText,
1810        window: &mut Window,
1811        cx: &mut App,
1812    ) {
1813        if self.style.prevent_mouse_interaction {
1814            return;
1815        }
1816
1817        let is_hovering_clickable = hitbox.is_hovered(window)
1818            && !self.markdown.read(cx).selection.pending
1819            && (rendered_text
1820                .image_link_for_position(window.mouse_position())
1821                .is_some()
1822                || rendered_text
1823                    .source_index_for_position(window.mouse_position())
1824                    .ok()
1825                    .is_some_and(|source_index| {
1826                        rendered_text.link_for_source_index(source_index).is_some()
1827                            || rendered_text
1828                                .footnote_ref_for_source_index(source_index)
1829                                .is_some()
1830                    }));
1831
1832        if is_hovering_clickable {
1833            window.set_cursor_style(CursorStyle::PointingHand, hitbox);
1834        } else {
1835            window.set_cursor_style(CursorStyle::IBeam, hitbox);
1836        }
1837
1838        let on_open_url = self.on_url_click.take();
1839        let on_url_hover = self.on_url_hover.take();
1840        let on_source_click = self.on_source_click.take();
1841
1842        self.on_mouse_event(window, cx, {
1843            let hitbox = hitbox.clone();
1844            let rendered_text = rendered_text.clone();
1845            move |markdown, event: &MouseDownEvent, phase, window, _cx| {
1846                if phase.capture()
1847                    && event.button == MouseButton::Right
1848                    && hitbox.is_hovered(window)
1849                {
1850                    let link = rendered_text
1851                        .source_index_for_position(event.position)
1852                        .ok()
1853                        .and_then(|ix| rendered_text.link_for_source_index(ix))
1854                        .map(|link| link.destination_url.clone());
1855                    markdown.capture_for_context_menu(link, Some(&rendered_text));
1856                }
1857            }
1858        });
1859
1860        self.on_mouse_event(window, cx, {
1861            let rendered_text = rendered_text.clone();
1862            let hitbox = hitbox.clone();
1863            move |markdown, event: &MouseDownEvent, phase, window, cx| {
1864                if hitbox.is_hovered(window) {
1865                    if phase.bubble() && event.button != MouseButton::Right {
1866                        let position_result =
1867                            rendered_text.source_index_for_position(event.position);
1868
1869                        if let Ok(source_index) = position_result {
1870                            if let Some(footnote_ref) =
1871                                rendered_text.footnote_ref_for_source_index(source_index)
1872                            {
1873                                markdown.pressed_footnote_ref = Some(footnote_ref.clone());
1874                            } else if let Some(link) =
1875                                rendered_text.link_for_source_index(source_index)
1876                            {
1877                                markdown.pressed_link = Some(link.clone());
1878                            }
1879                        }
1880
1881                        if markdown.pressed_footnote_ref.is_none()
1882                            && markdown.pressed_link.is_none()
1883                        {
1884                            let source_index = match position_result {
1885                                Ok(ix) | Err(ix) => ix,
1886                            };
1887                            if let Some(handler) = on_source_click.as_ref() {
1888                                let blocked = handler(source_index, event.click_count, window, cx);
1889                                if blocked {
1890                                    markdown.selection = Selection::default();
1891                                    markdown.pressed_link = None;
1892                                    window.prevent_default();
1893                                    cx.notify();
1894                                    return;
1895                                }
1896                            }
1897                            let (range, mode, reversed) = match event.click_count {
1898                                1 if event.modifiers.shift => {
1899                                    let tail = markdown.selection.tail();
1900                                    let reversed = source_index < tail;
1901                                    let range = if reversed {
1902                                        source_index..tail
1903                                    } else {
1904                                        tail..source_index
1905                                    };
1906                                    (range, SelectMode::Character, reversed)
1907                                }
1908                                1 => {
1909                                    let range = source_index..source_index;
1910                                    (range, SelectMode::Character, false)
1911                                }
1912                                2 => {
1913                                    let range = rendered_text.surrounding_word_range(source_index);
1914                                    (range.clone(), SelectMode::Word(range), false)
1915                                }
1916                                3 => {
1917                                    let range = rendered_text.surrounding_line_range(source_index);
1918                                    (range.clone(), SelectMode::Line(range), false)
1919                                }
1920                                _ => {
1921                                    let range = 0..rendered_text
1922                                        .lines
1923                                        .last()
1924                                        .map(|line| line.source_end)
1925                                        .unwrap_or(0);
1926                                    (range, SelectMode::All, false)
1927                                }
1928                            };
1929                            markdown.selection = Selection {
1930                                start: range.start,
1931                                end: range.end,
1932                                reversed,
1933                                pending: true,
1934                                mode,
1935                            };
1936                            window.focus(&markdown.focus_handle, cx);
1937                        }
1938
1939                        window.prevent_default();
1940                        cx.notify();
1941                    }
1942                } else if phase.capture() && event.button == MouseButton::Left {
1943                    markdown.selection = Selection::default();
1944                    markdown.pressed_link = None;
1945                    cx.notify();
1946                }
1947            }
1948        });
1949        self.on_mouse_event(window, cx, {
1950            let rendered_text = rendered_text.clone();
1951            let hitbox = hitbox.clone();
1952            let was_hovering_clickable = is_hovering_clickable;
1953            move |markdown, event: &MouseMoveEvent, phase, window, cx| {
1954                if phase.capture() {
1955                    return;
1956                }
1957
1958                if markdown.selection.pending {
1959                    let source_index = match rendered_text.source_index_for_position(event.position)
1960                    {
1961                        Ok(ix) | Err(ix) => ix,
1962                    };
1963                    markdown.selection.set_head(source_index, &rendered_text);
1964                    markdown.autoscroll_code_block(source_index, event.position);
1965                    markdown.autoscroll_request = Some(source_index);
1966                    cx.notify();
1967                } else {
1968                    let is_hitbox_hovered = hitbox.is_hovered(window);
1969                    let source_index = is_hitbox_hovered
1970                        .then(|| rendered_text.source_index_for_position(event.position).ok())
1971                        .flatten();
1972                    let hovered_url = is_hitbox_hovered
1973                        .then(|| rendered_text.image_link_for_position(event.position))
1974                        .flatten()
1975                        .map(|image| image.destination_url.clone())
1976                        .or_else(|| {
1977                            source_index
1978                                .and_then(|source_index| {
1979                                    rendered_text.link_for_source_index(source_index)
1980                                })
1981                                .map(|link| link.destination_url.clone())
1982                        });
1983                    let is_hovering_clickable = hovered_url.is_some()
1984                        || source_index.is_some_and(|source_index| {
1985                            rendered_text
1986                                .footnote_ref_for_source_index(source_index)
1987                                .is_some()
1988                        });
1989                    if let Some(on_url_hover) = on_url_hover.as_ref() {
1990                        on_url_hover(hovered_url, window, cx);
1991                    }
1992                    if is_hovering_clickable != was_hovering_clickable {
1993                        cx.notify();
1994                    }
1995                }
1996            }
1997        });
1998        self.on_mouse_event(window, cx, {
1999            let rendered_text = rendered_text.clone();
2000            move |markdown, event: &MouseUpEvent, phase, window, cx| {
2001                if phase.bubble() {
2002                    let source_index = rendered_text.source_index_for_position(event.position).ok();
2003                    if let Some(pressed_footnote_ref) = markdown.pressed_footnote_ref.take()
2004                        && source_index
2005                            .and_then(|ix| rendered_text.footnote_ref_for_source_index(ix))
2006                            == Some(&pressed_footnote_ref)
2007                    {
2008                        if let Some(source_index) =
2009                            markdown.footnote_definition_content_start(&pressed_footnote_ref.label)
2010                        {
2011                            markdown.autoscroll_request = Some(source_index);
2012                            cx.notify();
2013                        }
2014                    } else if let Some(pressed_link) = markdown.pressed_link.take()
2015                        && source_index.and_then(|ix| rendered_text.link_for_source_index(ix))
2016                            == Some(&pressed_link)
2017                    {
2018                        if let Some(open_url) = on_open_url.as_ref() {
2019                            open_url(pressed_link.destination_url, window, cx);
2020                        } else {
2021                            cx.open_url(&pressed_link.destination_url);
2022                        }
2023                    }
2024                } else if markdown.selection.pending {
2025                    markdown.selection.pending = false;
2026                    #[cfg(any(target_os = "linux", target_os = "freebsd"))]
2027                    {
2028                        let text = rendered_text
2029                            .text_for_range(markdown.selection.start..markdown.selection.end);
2030                        cx.write_to_primary(ClipboardItem::new_string(text))
2031                    }
2032                    cx.notify();
2033                }
2034            }
2035        });
2036    }
2037
2038    fn autoscroll(
2039        &self,
2040        rendered_text: &RenderedText,
2041        window: &mut Window,
2042        cx: &mut App,
2043    ) -> Option<()> {
2044        let autoscroll_index = self
2045            .markdown
2046            .update(cx, |markdown, _| markdown.autoscroll_request.take())?;
2047        let (position, line_height) = rendered_text.position_for_source_index(autoscroll_index)?;
2048
2049        match &self.autoscroll {
2050            AutoscrollBehavior::Controlled(scroll_handle) => {
2051                let viewport = scroll_handle.bounds();
2052                let margin = line_height * 3.;
2053                let top_goal = viewport.top() + margin;
2054                let bottom_goal = viewport.bottom() - margin;
2055                let current_offset = scroll_handle.offset();
2056
2057                let new_offset_y = if position.y < top_goal {
2058                    current_offset.y + (top_goal - position.y)
2059                } else if position.y + line_height > bottom_goal {
2060                    current_offset.y + (bottom_goal - (position.y + line_height))
2061                } else {
2062                    current_offset.y
2063                };
2064
2065                scroll_handle.set_offset(point(
2066                    current_offset.x,
2067                    new_offset_y.clamp(-scroll_handle.max_offset().y, Pixels::ZERO),
2068                ));
2069            }
2070            AutoscrollBehavior::Propagate => {
2071                let text_style = self.style.base_text_style.clone();
2072                let font_id = window.text_system().resolve_font(&text_style.font());
2073                let font_size = text_style.font_size.to_pixels(window.rem_size());
2074                let em_width = window.text_system().em_width(font_id, font_size).unwrap();
2075                window.request_autoscroll(Bounds::from_corners(
2076                    point(position.x - 3. * em_width, position.y - 3. * line_height),
2077                    point(position.x + 3. * em_width, position.y + 3. * line_height),
2078                ));
2079            }
2080        }
2081        Some(())
2082    }
2083
2084    fn on_mouse_event<T: MouseEvent>(
2085        &self,
2086        window: &mut Window,
2087        _cx: &mut App,
2088        mut f: impl 'static
2089        + FnMut(&mut Markdown, &T, DispatchPhase, &mut Window, &mut Context<Markdown>),
2090    ) {
2091        window.on_mouse_event({
2092            let markdown = self.markdown.downgrade();
2093            move |event, phase, window, cx| {
2094                markdown
2095                    .update(cx, |markdown, cx| f(markdown, event, phase, window, cx))
2096                    .log_err();
2097            }
2098        });
2099    }
2100}
2101
2102impl Styled for MarkdownElement {
2103    fn style(&mut self) -> &mut StyleRefinement {
2104        &mut self.style.container_style
2105    }
2106}
2107
2108impl Element for MarkdownElement {
2109    type RequestLayoutState = RenderedMarkdown;
2110    type PrepaintState = Hitbox;
2111
2112    fn id(&self) -> Option<ElementId> {
2113        None
2114    }
2115
2116    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
2117        None
2118    }
2119
2120    fn request_layout(
2121        &mut self,
2122        _id: Option<&GlobalElementId>,
2123        _inspector_id: Option<&gpui::InspectorElementId>,
2124        window: &mut Window,
2125        cx: &mut App,
2126    ) -> (gpui::LayoutId, Self::RequestLayoutState) {
2127        let mut builder = MarkdownElementBuilder::new(
2128            &self.style.container_style,
2129            self.style.base_text_style.clone(),
2130            self.style.syntax.clone(),
2131        );
2132        let (parsed_markdown, images, active_root_block, render_mermaid_diagrams, mermaid_state) = {
2133            let markdown = self.markdown.read(cx);
2134            (
2135                markdown.parsed_markdown.clone(),
2136                markdown.images_by_source_offset.clone(),
2137                markdown.active_root_block,
2138                markdown.options.render_mermaid_diagrams,
2139                markdown.mermaid_state.clone(),
2140            )
2141        };
2142        let markdown_end = if let Some(last) = parsed_markdown.events.last() {
2143            last.0.end
2144        } else {
2145            0
2146        };
2147        let mut code_block_ids = HashSet::default();
2148
2149        let mut current_img_block_range: Option<Range<usize>> = None;
2150        let mut handled_html_block = false;
2151        let mut rendered_mermaid_block = false;
2152        let mut rendered_metadata_block = false;
2153        for (index, (range, event)) in parsed_markdown.events.iter().enumerate() {
2154            // Skip alt text for images that rendered
2155            if let Some(current_img_block_range) = &current_img_block_range
2156                && current_img_block_range.end > range.end
2157            {
2158                continue;
2159            }
2160
2161            if handled_html_block {
2162                if let MarkdownEvent::End(MarkdownTagEnd::HtmlBlock) = event {
2163                    handled_html_block = false;
2164                } else {
2165                    continue;
2166                }
2167            }
2168
2169            if rendered_mermaid_block {
2170                if matches!(event, MarkdownEvent::End(MarkdownTagEnd::CodeBlock)) {
2171                    rendered_mermaid_block = false;
2172                }
2173                continue;
2174            }
2175
2176            if rendered_metadata_block {
2177                if matches!(event, MarkdownEvent::End(MarkdownTagEnd::MetadataBlock(_))) {
2178                    rendered_metadata_block = false;
2179                }
2180                continue;
2181            }
2182
2183            match event {
2184                MarkdownEvent::RootStart => {
2185                    if self.show_root_block_markers {
2186                        builder.push_root_block(range, markdown_end);
2187                    }
2188                }
2189                MarkdownEvent::RootEnd(root_block_index) => {
2190                    if self.show_root_block_markers {
2191                        builder.pop_root_block(
2192                            active_root_block == Some(*root_block_index),
2193                            cx.theme().colors().border,
2194                            cx.theme().colors().border_variant,
2195                        );
2196                    }
2197                }
2198                MarkdownEvent::Start(tag) => {
2199                    match tag {
2200                        MarkdownTag::Image { dest_url, .. } => {
2201                            let alt_text = collect_image_alt_text(
2202                                &parsed_markdown.events[index..],
2203                                &parsed_markdown.source,
2204                            );
2205                            if let Some(image) = images.get(&range.start) {
2206                                current_img_block_range = Some(range.clone());
2207                                self.push_markdown_image(
2208                                    &mut builder,
2209                                    range,
2210                                    image.clone().into(),
2211                                    dest_url.clone(),
2212                                    alt_text,
2213                                    None,
2214                                    None,
2215                                );
2216                            } else if let Some(source) = self
2217                                .image_resolver
2218                                .as_ref()
2219                                .and_then(|resolve| resolve(dest_url.as_ref()))
2220                            {
2221                                current_img_block_range = Some(range.clone());
2222                                self.push_markdown_image(
2223                                    &mut builder,
2224                                    range,
2225                                    source,
2226                                    dest_url.clone(),
2227                                    alt_text,
2228                                    None,
2229                                    None,
2230                                );
2231                            }
2232                        }
2233                        MarkdownTag::Paragraph => {
2234                            let text_align_override = builder
2235                                .table
2236                                .current_cell_alignment()
2237                                .and_then(alignment_to_text_align);
2238                            self.push_markdown_paragraph(
2239                                &mut builder,
2240                                range,
2241                                markdown_end,
2242                                text_align_override,
2243                            );
2244                        }
2245                        MarkdownTag::Heading { level, .. } => {
2246                            let text_align_override = builder
2247                                .table
2248                                .current_cell_alignment()
2249                                .and_then(alignment_to_text_align);
2250                            self.push_markdown_heading(
2251                                &mut builder,
2252                                *level,
2253                                range,
2254                                markdown_end,
2255                                text_align_override,
2256                            );
2257                        }
2258                        MarkdownTag::BlockQuote(kind) => {
2259                            self.push_markdown_block_quote(
2260                                &mut builder,
2261                                *kind,
2262                                range,
2263                                markdown_end,
2264                            );
2265                        }
2266                        MarkdownTag::CodeBlock { kind, .. } => {
2267                            if render_mermaid_diagrams
2268                                && let Some(mermaid_diagram) =
2269                                    parsed_markdown.mermaid_diagrams.get(&range.start)
2270                            {
2271                                let showing_code =
2272                                    self.markdown.read(cx).is_mermaid_showing_code(range.start);
2273                                let copy_button_visibility = match &self.code_block_renderer {
2274                                    CodeBlockRenderer::Default {
2275                                        copy_button_visibility,
2276                                        ..
2277                                    } => *copy_button_visibility,
2278                                    _ => CopyButtonVisibility::VisibleOnHover,
2279                                };
2280                                builder.push_sourced_element(
2281                                    mermaid_diagram.content_range.clone(),
2282                                    render_mermaid_diagram(
2283                                        mermaid_diagram,
2284                                        &mermaid_state,
2285                                        &self.style,
2286                                        self.markdown.clone(),
2287                                        range.start,
2288                                        showing_code,
2289                                        copy_button_visibility,
2290                                    ),
2291                                );
2292                                rendered_mermaid_block = true;
2293                                continue;
2294                            }
2295
2296                            let language = match kind {
2297                                CodeBlockKind::Fenced => None,
2298                                CodeBlockKind::FencedLang(language) => {
2299                                    parsed_markdown.languages_by_name.get(language).cloned()
2300                                }
2301                                CodeBlockKind::FencedSrc(path_range) => parsed_markdown
2302                                    .languages_by_path
2303                                    .get(&path_range.path)
2304                                    .cloned(),
2305                                _ => None,
2306                            };
2307
2308                            let is_indented = matches!(kind, CodeBlockKind::Indented);
2309                            let scroll_handle = if self.style.code_block_overflow_x_scroll {
2310                                self.markdown.update(cx, |markdown, _| {
2311                                    markdown.code_block_scroll_handle(range.start)
2312                                })
2313                            } else {
2314                                None
2315                            };
2316                            if scroll_handle.is_some() {
2317                                code_block_ids.insert(range.start);
2318                            }
2319
2320                            match (&self.code_block_renderer, is_indented) {
2321                                (CodeBlockRenderer::Default { .. }, _) | (_, true) => {
2322                                    // This is a parent container that we can position the copy button inside.
2323                                    let parent_container =
2324                                        div().group("code_block").relative().w_full();
2325
2326                                    let mut parent_container: AnyDiv = if let Some(scroll_handle) =
2327                                        scroll_handle.as_ref()
2328                                    {
2329                                        let scrollbars = Scrollbars::new(ScrollAxes::Horizontal)
2330                                            .id(("markdown-code-block-scrollbar", range.start))
2331                                            .tracked_scroll_handle(scroll_handle)
2332                                            .with_track_along(
2333                                                ScrollAxes::Horizontal,
2334                                                cx.theme().colors().editor_background,
2335                                            )
2336                                            .notify_content();
2337
2338                                        parent_container
2339                                            .rounded_lg()
2340                                            .custom_scrollbars(scrollbars, window, cx)
2341                                            .into()
2342                                    } else {
2343                                        parent_container.into()
2344                                    };
2345
2346                                    if let CodeBlockRenderer::Default { border: true, .. } =
2347                                        &self.code_block_renderer
2348                                    {
2349                                        parent_container = parent_container
2350                                            .rounded_md()
2351                                            .border_1()
2352                                            .border_color(cx.theme().colors().border_variant);
2353                                    }
2354
2355                                    parent_container.style().refine(&self.style.code_block);
2356                                    builder.push_div(parent_container, range, markdown_end);
2357
2358                                    let code_block = div()
2359                                        .id(("code-block", range.start))
2360                                        .rounded_lg()
2361                                        .map(|mut code_block| {
2362                                            if let Some(scroll_handle) = scroll_handle.as_ref() {
2363                                                code_block.style().restrict_scroll_to_axis =
2364                                                    Some(true);
2365                                                code_block
2366                                                    .flex()
2367                                                    .overflow_x_scroll()
2368                                                    .track_scroll(scroll_handle)
2369                                            } else {
2370                                                code_block.w_full()
2371                                            }
2372                                        });
2373
2374                                    builder.push_text_style(self.style.code_block.text.to_owned());
2375                                    builder.push_code_block(language);
2376                                    builder.push_div(code_block, range, markdown_end);
2377                                }
2378                                (CodeBlockRenderer::Custom { .. }, _) => {}
2379                            }
2380                        }
2381                        MarkdownTag::HtmlBlock => {
2382                            builder.push_div(div(), range, markdown_end);
2383                            if let Some(block) = parsed_markdown.html_blocks.get(&range.start) {
2384                                self.render_html_block(block, &mut builder, markdown_end, cx);
2385                                handled_html_block = true;
2386                            }
2387                        }
2388                        MarkdownTag::List(bullet_index) => {
2389                            builder.push_list(*bullet_index);
2390                            builder.push_div(div().pl_2p5(), range, markdown_end);
2391                        }
2392                        MarkdownTag::Item => {
2393                            let bullet =
2394                                if let Some((task_range, MarkdownEvent::TaskListMarker(checked))) =
2395                                    parsed_markdown.events.get(index.saturating_add(1))
2396                                {
2397                                    let source = &parsed_markdown.source()[range.clone()];
2398                                    let checked = *checked;
2399                                    let toggle_state = if checked {
2400                                        ToggleState::Selected
2401                                    } else {
2402                                        ToggleState::Unselected
2403                                    };
2404
2405                                    let checkbox = Checkbox::new(
2406                                        ElementId::Name(source.to_string().into()),
2407                                        toggle_state,
2408                                    )
2409                                    .fill();
2410
2411                                    if let Some(on_toggle) = self.on_checkbox_toggle.clone() {
2412                                        let task_source_range = task_range.clone();
2413                                        checkbox
2414                                            .on_click(move |_state, window, cx| {
2415                                                on_toggle(
2416                                                    task_source_range.clone(),
2417                                                    !checked,
2418                                                    window,
2419                                                    cx,
2420                                                );
2421                                            })
2422                                            .into_any_element()
2423                                    } else {
2424                                        checkbox.visualization_only(true).into_any_element()
2425                                    }
2426                                } else if let Some(bullet_index) = builder.next_bullet_index() {
2427                                    div().child(format!("{}.", bullet_index)).into_any_element()
2428                                } else {
2429                                    div().child("•").into_any_element()
2430                                };
2431                            self.push_markdown_list_item(&mut builder, bullet, range, markdown_end);
2432                        }
2433                        MarkdownTag::Emphasis => builder.push_text_style(TextStyleRefinement {
2434                            font_style: Some(FontStyle::Italic),
2435                            ..Default::default()
2436                        }),
2437                        MarkdownTag::Strong => builder.push_text_style(TextStyleRefinement {
2438                            font_weight: Some(FontWeight::BOLD),
2439                            color: Some(cx.theme().colors().text),
2440                            ..Default::default()
2441                        }),
2442                        MarkdownTag::Strikethrough => {
2443                            builder.push_text_style(TextStyleRefinement {
2444                                strikethrough: Some(StrikethroughStyle {
2445                                    thickness: px(1.),
2446                                    color: None,
2447                                }),
2448                                ..Default::default()
2449                            })
2450                        }
2451                        MarkdownTag::Link { dest_url, .. } => {
2452                            if builder.code_block_stack.is_empty() {
2453                                builder.link_depth += 1;
2454                                builder.push_link(dest_url.clone(), range.clone());
2455                                let style = self
2456                                    .style
2457                                    .link_callback
2458                                    .as_ref()
2459                                    .and_then(|callback| callback(dest_url, cx))
2460                                    .unwrap_or_else(|| self.style.link.clone());
2461                                builder.push_text_style(style)
2462                            }
2463                        }
2464                        MarkdownTag::FootnoteDefinition(label) => {
2465                            if !builder.rendered_footnote_separator {
2466                                builder.rendered_footnote_separator = true;
2467                                builder.push_div(
2468                                    div()
2469                                        .border_t_1()
2470                                        .mt_2()
2471                                        .border_color(self.style.rule_color),
2472                                    range,
2473                                    markdown_end,
2474                                );
2475                                builder.pop_div();
2476                            }
2477                            builder.push_div(
2478                                div()
2479                                    .pt_1()
2480                                    .mb_1()
2481                                    .line_height(rems(1.3))
2482                                    .text_size(rems(0.85))
2483                                    .h_flex()
2484                                    .items_start()
2485                                    .gap_2()
2486                                    .child(
2487                                        div().text_size(rems(0.85)).child(format!("{}.", label)),
2488                                    ),
2489                                range,
2490                                markdown_end,
2491                            );
2492                            builder.push_div(div().flex_1().w_0(), range, markdown_end);
2493                        }
2494                        MarkdownTag::MetadataBlock(_) => {
2495                            if let Some(metadata_block) =
2496                                parsed_markdown.metadata_blocks.get(&range.start)
2497                            {
2498                                self.push_metadata_block(
2499                                    &mut builder,
2500                                    &parsed_markdown.source,
2501                                    metadata_block,
2502                                    markdown_end,
2503                                    cx,
2504                                );
2505                                rendered_metadata_block = true;
2506                            }
2507                        }
2508                        MarkdownTag::Table(alignments) => {
2509                            builder.table.start(alignments.clone());
2510
2511                            let column_count = alignments.len();
2512                            builder.push_div(
2513                                div()
2514                                    .id(("table", range.start))
2515                                    .grid()
2516                                    .grid_cols(column_count as u16)
2517                                    .when(self.style.table_columns_min_size, |this| {
2518                                        this.grid_cols_min_content(column_count as u16)
2519                                    })
2520                                    .when(!self.style.table_columns_min_size, |this| {
2521                                        this.grid_cols(column_count as u16)
2522                                    })
2523                                    .w_full()
2524                                    .mb_2()
2525                                    .border(px(1.5))
2526                                    .border_color(cx.theme().colors().border)
2527                                    .rounded_sm()
2528                                    .overflow_hidden(),
2529                                range,
2530                                markdown_end,
2531                            );
2532                        }
2533                        MarkdownTag::TableHead => {
2534                            builder.table.start_head();
2535                            builder.push_text_style(TextStyleRefinement {
2536                                font_weight: Some(FontWeight::SEMIBOLD),
2537                                ..Default::default()
2538                            });
2539                        }
2540                        MarkdownTag::TableRow => {
2541                            builder.table.start_row();
2542                        }
2543                        MarkdownTag::TableCell => {
2544                            let is_header = builder.table.in_head;
2545                            let row_index = builder.table.row_index;
2546                            let col_index = builder.table.col_index;
2547                            let alignment = builder.table.current_cell_alignment();
2548                            let text_align = alignment
2549                                .and_then(alignment_to_text_align)
2550                                .unwrap_or(self.style.base_text_style.text_align);
2551
2552                            let mut cell_div = div()
2553                                .flex()
2554                                .flex_col()
2555                                .h_full()
2556                                .when(col_index > 0, |this| this.border_l_1())
2557                                .when(row_index > 0, |this| this.border_t_1())
2558                                .border_color(cx.theme().colors().border)
2559                                .px_1()
2560                                .py_0p5()
2561                                .when(is_header, |this| {
2562                                    this.bg(cx.theme().colors().title_bar_background)
2563                                })
2564                                .when(!is_header && row_index % 2 == 1, |this| {
2565                                    this.bg(cx.theme().colors().panel_background)
2566                                });
2567
2568                            cell_div = match alignment {
2569                                Some(Alignment::Center) => cell_div.items_center(),
2570                                Some(Alignment::Right) => cell_div.items_end(),
2571                                _ => cell_div,
2572                            };
2573
2574                            builder.push_text_style(TextStyleRefinement {
2575                                text_align: Some(text_align),
2576                                ..Default::default()
2577                            });
2578                            builder.push_div(cell_div, range, markdown_end);
2579                            builder.push_div(
2580                                div()
2581                                    .flex()
2582                                    .flex_col()
2583                                    .flex_1()
2584                                    .w_full()
2585                                    .justify_center()
2586                                    .text_align(text_align),
2587                                range,
2588                                markdown_end,
2589                            );
2590                        }
2591                        _ => log::debug!("unsupported markdown tag {:?}", tag),
2592                    }
2593                }
2594                MarkdownEvent::End(tag) => match tag {
2595                    MarkdownTagEnd::Image => {
2596                        current_img_block_range.take();
2597                    }
2598                    MarkdownTagEnd::Paragraph => {
2599                        self.pop_markdown_paragraph(&mut builder);
2600                    }
2601                    MarkdownTagEnd::Heading(_) => {
2602                        self.pop_markdown_heading(&mut builder);
2603                    }
2604                    MarkdownTagEnd::BlockQuote(_kind) => {
2605                        self.pop_markdown_block_quote(&mut builder);
2606                    }
2607                    MarkdownTagEnd::CodeBlock => {
2608                        builder.trim_trailing_newline();
2609
2610                        builder.pop_div();
2611                        builder.pop_code_block();
2612                        builder.pop_text_style();
2613
2614                        if let CodeBlockRenderer::Default {
2615                            copy_button_visibility,
2616                            wrap_button_visibility,
2617                            ..
2618                        } = &self.code_block_renderer
2619                            && (*copy_button_visibility != CopyButtonVisibility::Hidden
2620                                || *wrap_button_visibility != WrapButtonVisibility::Hidden)
2621                        {
2622                            let copy_button_visibility = *copy_button_visibility;
2623                            let wrap_button_visibility = *wrap_button_visibility;
2624                            builder.modify_current_div(|el| {
2625                                let content_range = parser::extract_code_block_content_range(
2626                                    &parsed_markdown.source()[range.clone()],
2627                                );
2628                                let content_range = content_range.start + range.start
2629                                    ..content_range.end + range.start;
2630
2631                                let code = parsed_markdown.source()[content_range].to_string();
2632
2633                                let any_hover = copy_button_visibility
2634                                    == CopyButtonVisibility::VisibleOnHover
2635                                    || wrap_button_visibility
2636                                        == WrapButtonVisibility::VisibleOnHover;
2637                                let any_always = copy_button_visibility
2638                                    == CopyButtonVisibility::AlwaysVisible
2639                                    || wrap_button_visibility
2640                                        == WrapButtonVisibility::AlwaysVisible;
2641                                let use_hover = any_hover && !any_always;
2642
2643                                let button_row = h_flex()
2644                                    .gap_0p5()
2645                                    .absolute()
2646                                    .bg(cx.theme().colors().editor_background)
2647                                    .when_else(
2648                                        use_hover,
2649                                        |this| {
2650                                            this.top_1().right_1().visible_on_hover("code_block")
2651                                        },
2652                                        |this| this.top_1p5().right_1p5(),
2653                                    )
2654                                    .when(
2655                                        wrap_button_visibility != WrapButtonVisibility::Hidden,
2656                                        |this| {
2657                                            let is_wrapped = self
2658                                                .markdown
2659                                                .read(cx)
2660                                                .is_code_block_wrapped(range.start);
2661
2662                                            this.child(render_wrap_code_block_button(
2663                                                range.start,
2664                                                is_wrapped,
2665                                                self.markdown.clone(),
2666                                            ))
2667                                        },
2668                                    )
2669                                    .when(
2670                                        copy_button_visibility != CopyButtonVisibility::Hidden,
2671                                        |this| {
2672                                            this.child(render_copy_code_block_button(
2673                                                range.end,
2674                                                code,
2675                                                self.markdown.clone(),
2676                                            ))
2677                                        },
2678                                    );
2679
2680                                el.child(button_row)
2681                            });
2682                        }
2683
2684                        // Pop the parent container.
2685                        builder.pop_div();
2686                    }
2687                    MarkdownTagEnd::HtmlBlock => builder.pop_div(),
2688                    MarkdownTagEnd::List(_) => {
2689                        builder.pop_list();
2690                        builder.pop_div();
2691                    }
2692                    MarkdownTagEnd::Item => {
2693                        self.pop_markdown_list_item(&mut builder);
2694                    }
2695                    MarkdownTagEnd::Emphasis => builder.pop_text_style(),
2696                    MarkdownTagEnd::Strong => builder.pop_text_style(),
2697                    MarkdownTagEnd::Strikethrough => builder.pop_text_style(),
2698                    MarkdownTagEnd::Link => {
2699                        if builder.code_block_stack.is_empty() {
2700                            builder.link_depth = builder.link_depth.saturating_sub(1);
2701                            builder.pop_text_style()
2702                        }
2703                    }
2704                    MarkdownTagEnd::Table => {
2705                        builder.pop_div();
2706                        builder.table.end();
2707                    }
2708                    MarkdownTagEnd::TableHead => {
2709                        builder.pop_text_style();
2710                        builder.table.end_head();
2711                    }
2712                    MarkdownTagEnd::TableRow => {
2713                        builder.table.end_row();
2714                    }
2715                    MarkdownTagEnd::TableCell => {
2716                        builder.replace_pending_checkbox(self.on_checkbox_toggle.clone());
2717                        builder.pop_div();
2718                        builder.pop_div();
2719                        builder.pop_text_style();
2720                        builder.table.end_cell();
2721                    }
2722                    MarkdownTagEnd::FootnoteDefinition => {
2723                        builder.pop_div();
2724                        builder.pop_div();
2725                    }
2726                    MarkdownTagEnd::MetadataBlock(_) => {}
2727                    _ => log::debug!("unsupported markdown tag end: {:?}", tag),
2728                },
2729                MarkdownEvent::Text => {
2730                    builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
2731                }
2732                MarkdownEvent::SubstitutedText(text) => {
2733                    builder.push_text(text, range.clone());
2734                }
2735                MarkdownEvent::Code => {
2736                    self.push_markdown_code_span(
2737                        &mut builder,
2738                        &parsed_markdown.source[range.clone()],
2739                        range.clone(),
2740                        cx,
2741                    );
2742                }
2743                MarkdownEvent::SubstitutedCode(text) => {
2744                    self.push_markdown_code_span(&mut builder, text, range.clone(), cx);
2745                }
2746                MarkdownEvent::Html => {
2747                    let html = &parsed_markdown.source[range.clone()];
2748                    if html.starts_with("<!--") {
2749                        builder.html_comment = true;
2750                    }
2751                    if html.trim_end().ends_with("-->") {
2752                        builder.html_comment = false;
2753                        continue;
2754                    }
2755                    if builder.html_comment {
2756                        continue;
2757                    }
2758                    builder.push_text(html, range.clone());
2759                }
2760                MarkdownEvent::InlineHtml => {
2761                    let html = &parsed_markdown.source[range.clone()];
2762                    if let Some(code) = html
2763                        .strip_prefix("<code>")
2764                        .and_then(|html| html.strip_suffix("</code>"))
2765                    {
2766                        let code_start = range.start + "<code>".len();
2767                        self.push_markdown_code_span(
2768                            &mut builder,
2769                            code,
2770                            code_start..code_start + code.len(),
2771                            cx,
2772                        );
2773                        continue;
2774                    }
2775                    if html.starts_with("<code>") {
2776                        builder.push_text_style(self.style.inline_code.clone());
2777                        continue;
2778                    }
2779                    if html.trim_end().starts_with("</code>") {
2780                        builder.pop_text_style();
2781                        continue;
2782                    }
2783                    builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
2784                }
2785                MarkdownEvent::Rule => {
2786                    builder.push_div(
2787                        div()
2788                            .border_b_1()
2789                            .my_2()
2790                            .border_color(self.style.rule_color),
2791                        range,
2792                        markdown_end,
2793                    );
2794                    builder.pop_div()
2795                }
2796                MarkdownEvent::SoftBreak if !self.style.soft_break_as_hard_break => {
2797                    builder.push_soft_break(range.clone());
2798                }
2799                MarkdownEvent::SoftBreak | MarkdownEvent::HardBreak => {
2800                    builder.push_line_break(range.clone());
2801                }
2802                MarkdownEvent::TaskListMarker(_) => {
2803                    // handled inside the `MarkdownTag::Item` case
2804                }
2805                MarkdownEvent::FootnoteReference(label) => {
2806                    builder.push_footnote_ref(label.clone(), range.clone());
2807                    builder.push_text_style(self.style.link.clone());
2808                    builder.push_text(&format!("[{label}]"), range.clone());
2809                    builder.pop_text_style();
2810                }
2811            }
2812        }
2813        if self.style.code_block_overflow_x_scroll {
2814            let code_block_ids = code_block_ids;
2815            self.markdown.update(cx, move |markdown, _| {
2816                markdown.retain_code_block_scroll_handles(&code_block_ids);
2817            });
2818        } else {
2819            self.markdown
2820                .update(cx, |markdown, _| markdown.clear_code_block_scroll_handles());
2821        }
2822        let mut rendered_markdown = builder.build();
2823        let child_layout_id = rendered_markdown.element.request_layout(window, cx);
2824        let layout_id = window.request_layout(gpui::Style::default(), [child_layout_id], cx);
2825        (layout_id, rendered_markdown)
2826    }
2827
2828    fn prepaint(
2829        &mut self,
2830        _id: Option<&GlobalElementId>,
2831        _inspector_id: Option<&gpui::InspectorElementId>,
2832        bounds: Bounds<Pixels>,
2833        rendered_markdown: &mut Self::RequestLayoutState,
2834        window: &mut Window,
2835        cx: &mut App,
2836    ) -> Self::PrepaintState {
2837        let focus_handle = self.markdown.read(cx).focus_handle.clone();
2838        window.set_focus_handle(&focus_handle, cx);
2839        window.set_view_id(self.markdown.entity_id());
2840
2841        let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
2842        rendered_markdown.element.prepaint(window, cx);
2843        self.autoscroll(&rendered_markdown.text, window, cx);
2844        hitbox
2845    }
2846
2847    fn paint(
2848        &mut self,
2849        _id: Option<&GlobalElementId>,
2850        _inspector_id: Option<&gpui::InspectorElementId>,
2851        _bounds: Bounds<Pixels>,
2852        rendered_markdown: &mut Self::RequestLayoutState,
2853        hitbox: &mut Self::PrepaintState,
2854        window: &mut Window,
2855        cx: &mut App,
2856    ) {
2857        let mut context = KeyContext::default();
2858        context.add("Markdown");
2859        window.set_key_context(context);
2860        window.on_action(std::any::TypeId::of::<crate::Copy>(), {
2861            let entity = self.markdown.clone();
2862            let text = rendered_markdown.text.clone();
2863            move |_, phase, window, cx| {
2864                let text = text.clone();
2865                if phase == DispatchPhase::Bubble {
2866                    entity.update(cx, move |this, cx| this.copy(&text, window, cx))
2867                }
2868            }
2869        });
2870        window.on_action(std::any::TypeId::of::<crate::CopyAsMarkdown>(), {
2871            let entity = self.markdown.clone();
2872            move |_, phase, window, cx| {
2873                if phase == DispatchPhase::Bubble {
2874                    entity.update(cx, move |this, cx| this.copy_as_markdown(window, cx))
2875                }
2876            }
2877        });
2878
2879        self.paint_mouse_listeners(hitbox, &rendered_markdown.text, window, cx);
2880        rendered_markdown.element.paint(window, cx);
2881        self.paint_search_highlights(&rendered_markdown.text, window, cx);
2882        self.paint_selection(&rendered_markdown.text, window, cx);
2883    }
2884}
2885
2886fn collect_image_alt_text(
2887    events_from_image_start: &[(Range<usize>, MarkdownEvent)],
2888    source: &str,
2889) -> Option<SharedString> {
2890    let mut alt_text = String::new();
2891    for (range, event) in events_from_image_start.iter().skip(1) {
2892        match event {
2893            MarkdownEvent::End(MarkdownTagEnd::Image) => break,
2894            MarkdownEvent::Text => alt_text.push_str(&source[range.clone()]),
2895            _ => {}
2896        }
2897    }
2898    if alt_text.is_empty() {
2899        None
2900    } else {
2901        Some(alt_text.into())
2902    }
2903}
2904
2905fn image_fallback_element(
2906    dest_url: SharedString,
2907    alt_text: Option<SharedString>,
2908    open_image_url_on_click: bool,
2909) -> AnyElement {
2910    let link_label = alt_text
2911        .filter(|alt| !alt.is_empty())
2912        .unwrap_or_else(|| dest_url.clone());
2913
2914    let label = format!("Failed to Load: {link_label}");
2915
2916    div()
2917        .id("image-fallback")
2918        .min_w_0()
2919        .child(Label::new(label).color(Color::Warning).underline())
2920        .tooltip(Tooltip::text(
2921            "Image failed to load. Open `workspace: open log` for more details.",
2922        ))
2923        .when(open_image_url_on_click, |this| {
2924            this.cursor_pointer()
2925                .on_click(move |_, _, cx| cx.open_url(&dest_url))
2926        })
2927        .into_any_element()
2928}
2929
2930fn apply_heading_style(
2931    mut heading: Div,
2932    level: pulldown_cmark::HeadingLevel,
2933    custom_styles: Option<&HeadingLevelStyles>,
2934    border_color: Option<Hsla>,
2935) -> Div {
2936    heading = match level {
2937        pulldown_cmark::HeadingLevel::H1 => heading.text_3xl(),
2938        pulldown_cmark::HeadingLevel::H2 => heading.text_2xl(),
2939        pulldown_cmark::HeadingLevel::H3 => heading.text_xl(),
2940        pulldown_cmark::HeadingLevel::H4 => heading.text_lg(),
2941        pulldown_cmark::HeadingLevel::H5 => heading.text_base(),
2942        pulldown_cmark::HeadingLevel::H6 => heading.text_sm(),
2943    };
2944
2945    heading = match level {
2946        pulldown_cmark::HeadingLevel::H1 => heading,
2947        _ => heading.mt_6(),
2948    };
2949
2950    if let Some(border_color) = border_color
2951        && matches!(
2952            level,
2953            pulldown_cmark::HeadingLevel::H1
2954                | pulldown_cmark::HeadingLevel::H2
2955                | pulldown_cmark::HeadingLevel::H3
2956        )
2957    {
2958        heading = heading.pb_1().border_b_1().border_color(border_color);
2959    }
2960
2961    if let Some(styles) = custom_styles {
2962        let style_opt = match level {
2963            pulldown_cmark::HeadingLevel::H1 => &styles.h1,
2964            pulldown_cmark::HeadingLevel::H2 => &styles.h2,
2965            pulldown_cmark::HeadingLevel::H3 => &styles.h3,
2966            pulldown_cmark::HeadingLevel::H4 => &styles.h4,
2967            pulldown_cmark::HeadingLevel::H5 => &styles.h5,
2968            pulldown_cmark::HeadingLevel::H6 => &styles.h6,
2969        };
2970
2971        if let Some(style) = style_opt {
2972            heading.style().text = style.clone();
2973        }
2974    }
2975
2976    heading
2977}
2978
2979fn render_wrap_code_block_button(
2980    id: usize,
2981    is_wrapped: bool,
2982    markdown: Entity<Markdown>,
2983) -> impl IntoElement {
2984    let (icon, tooltip) = if is_wrapped {
2985        (IconName::TextUnwrap, "Unwrap Content")
2986    } else {
2987        (IconName::TextWrap, "Wrap Content")
2988    };
2989    let button_id = ElementId::NamedChild(
2990        Arc::new(ElementId::from(("wrap-code-block", markdown.entity_id()))),
2991        id.to_string().into(),
2992    );
2993
2994    IconButton::new(button_id, icon)
2995        .icon_size(IconSize::Small)
2996        .icon_color(Color::Muted)
2997        .tooltip(Tooltip::text(tooltip))
2998        .on_click(move |_event, _window, cx| {
2999            markdown.update(cx, |markdown, cx| {
3000                markdown.toggle_code_block_wrap(id);
3001                cx.notify();
3002            });
3003        })
3004}
3005
3006fn render_copy_code_block_button(
3007    id: usize,
3008    code: String,
3009    markdown: Entity<Markdown>,
3010) -> impl IntoElement {
3011    let id = ElementId::NamedChild(
3012        Arc::new(ElementId::from((
3013            "copy-markdown-code",
3014            markdown.entity_id(),
3015        ))),
3016        id.to_string().into(),
3017    );
3018
3019    CopyButton::new(id.clone(), code.clone()).custom_on_click({
3020        let markdown = markdown;
3021        move |_window, cx| {
3022            let id = id.clone();
3023            markdown.update(cx, |this, cx| {
3024                this.copied_code_blocks.insert(id.clone());
3025
3026                cx.write_to_clipboard(ClipboardItem::new_string(code.clone()));
3027
3028                cx.spawn(async move |this, cx| {
3029                    cx.background_executor().timer(Duration::from_secs(2)).await;
3030
3031                    cx.update(|cx| {
3032                        this.update(cx, |this, cx| {
3033                            this.copied_code_blocks.remove(&id);
3034                            cx.notify();
3035                        })
3036                    })
3037                    .ok();
3038                })
3039                .detach();
3040            });
3041        }
3042    })
3043}
3044
3045impl IntoElement for MarkdownElement {
3046    type Element = Self;
3047
3048    fn into_element(self) -> Self::Element {
3049        self
3050    }
3051}
3052
3053pub enum AnyDiv {
3054    Div(Div),
3055    Stateful(Stateful<Div>),
3056}
3057
3058impl AnyDiv {
3059    fn into_any_element(self) -> AnyElement {
3060        match self {
3061            Self::Div(div) => div.into_any_element(),
3062            Self::Stateful(div) => div.into_any_element(),
3063        }
3064    }
3065}
3066
3067impl From<Div> for AnyDiv {
3068    fn from(value: Div) -> Self {
3069        Self::Div(value)
3070    }
3071}
3072
3073impl From<Stateful<Div>> for AnyDiv {
3074    fn from(value: Stateful<Div>) -> Self {
3075        Self::Stateful(value)
3076    }
3077}
3078
3079impl Styled for AnyDiv {
3080    fn style(&mut self) -> &mut StyleRefinement {
3081        match self {
3082            Self::Div(div) => div.style(),
3083            Self::Stateful(div) => div.style(),
3084        }
3085    }
3086}
3087
3088impl ParentElement for AnyDiv {
3089    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
3090        match self {
3091            Self::Div(div) => div.extend(elements),
3092            Self::Stateful(div) => div.extend(elements),
3093        }
3094    }
3095}
3096
3097#[derive(Default)]
3098struct TableState {
3099    alignments: Vec<Alignment>,
3100    in_head: bool,
3101    row_index: usize,
3102    col_index: usize,
3103}
3104
3105impl TableState {
3106    fn start(&mut self, alignments: Vec<Alignment>) {
3107        self.alignments = alignments;
3108        self.in_head = false;
3109        self.row_index = 0;
3110        self.col_index = 0;
3111    }
3112
3113    fn end(&mut self) {
3114        self.alignments.clear();
3115        self.in_head = false;
3116        self.row_index = 0;
3117        self.col_index = 0;
3118    }
3119
3120    fn start_head(&mut self) {
3121        self.in_head = true;
3122    }
3123
3124    fn end_head(&mut self) {
3125        self.in_head = false;
3126    }
3127
3128    fn start_row(&mut self) {
3129        self.col_index = 0;
3130    }
3131
3132    fn end_row(&mut self) {
3133        self.row_index += 1;
3134    }
3135
3136    fn end_cell(&mut self) {
3137        self.col_index += 1;
3138    }
3139
3140    fn current_cell_alignment(&self) -> Option<Alignment> {
3141        if self.alignments.is_empty() {
3142            return None;
3143        }
3144        if self.in_head {
3145            return Some(Alignment::Center);
3146        }
3147        self.alignments.get(self.col_index).copied()
3148    }
3149}
3150
3151fn alignment_to_text_align(alignment: Alignment) -> Option<TextAlign> {
3152    match alignment {
3153        Alignment::Left => Some(TextAlign::Left),
3154        Alignment::Center => Some(TextAlign::Center),
3155        Alignment::Right => Some(TextAlign::Right),
3156        Alignment::None => None,
3157    }
3158}
3159
3160struct MetadataCellStyle {
3161    row_index: usize,
3162    is_key: bool,
3163}
3164
3165struct MarkdownElementBuilder {
3166    div_stack: Vec<DivStackEntry>,
3167    rendered_lines: Vec<RenderedLine>,
3168    pending_line: PendingLine,
3169    rendered_links: Vec<RenderedLink>,
3170    rendered_image_links: Vec<RenderedImageLink>,
3171    rendered_footnote_refs: Vec<RenderedFootnoteRef>,
3172    current_source_index: usize,
3173    html_comment: bool,
3174    rendered_footnote_separator: bool,
3175    base_text_style: TextStyle,
3176    text_style_stack: Vec<TextStyleRefinement>,
3177    code_block_stack: Vec<Option<Arc<Language>>>,
3178    link_depth: usize,
3179    list_stack: Vec<ListStackEntry>,
3180    table: TableState,
3181    syntax_theme: Arc<SyntaxTheme>,
3182}
3183
3184struct DivStackEntry {
3185    div: AnyDiv,
3186    line_break_mode: LineBreakMode,
3187}
3188
3189#[derive(Clone, Copy, Eq, PartialEq)]
3190enum LineBreakMode {
3191    TextLayout,
3192    FlexWrap,
3193}
3194
3195impl DivStackEntry {
3196    fn new(div: impl Into<AnyDiv>) -> Self {
3197        Self {
3198            div: div.into(),
3199            line_break_mode: LineBreakMode::TextLayout,
3200        }
3201    }
3202}
3203
3204#[derive(Default)]
3205struct PendingLine {
3206    text: String,
3207    runs: Vec<TextRun>,
3208    source_mappings: Vec<SourceMapping>,
3209}
3210
3211struct ListStackEntry {
3212    bullet_index: Option<u64>,
3213}
3214
3215impl MarkdownElementBuilder {
3216    fn new(
3217        container_style: &StyleRefinement,
3218        base_text_style: TextStyle,
3219        syntax_theme: Arc<SyntaxTheme>,
3220    ) -> Self {
3221        Self {
3222            div_stack: vec![{
3223                let mut base_div = div();
3224                base_div.style().refine(container_style);
3225                DivStackEntry::new(base_div.debug_selector(|| "inner".into()))
3226            }],
3227            rendered_lines: Vec::new(),
3228            pending_line: PendingLine::default(),
3229            rendered_links: Vec::new(),
3230            rendered_image_links: Vec::new(),
3231            rendered_footnote_refs: Vec::new(),
3232            current_source_index: 0,
3233            html_comment: false,
3234            rendered_footnote_separator: false,
3235            base_text_style,
3236            text_style_stack: Vec::new(),
3237            code_block_stack: Vec::new(),
3238            link_depth: 0,
3239            list_stack: Vec::new(),
3240            table: TableState::default(),
3241            syntax_theme,
3242        }
3243    }
3244
3245    fn push_text_style(&mut self, style: TextStyleRefinement) {
3246        self.text_style_stack.push(style);
3247    }
3248
3249    fn text_style(&self) -> TextStyle {
3250        let mut style = self.base_text_style.clone();
3251        for refinement in &self.text_style_stack {
3252            style.refine(refinement);
3253        }
3254        style
3255    }
3256
3257    fn pop_text_style(&mut self) {
3258        self.text_style_stack.pop();
3259    }
3260
3261    fn push_div(&mut self, div: impl Into<AnyDiv>, range: &Range<usize>, markdown_end: usize) {
3262        let mut div = div.into();
3263        self.flush_text();
3264
3265        if range.start == 0 {
3266            // Remove the top margin on the first element.
3267            div.style().refine(&StyleRefinement {
3268                margin: gpui::EdgesRefinement {
3269                    top: Some(Length::Definite(px(0.).into())),
3270                    left: None,
3271                    right: None,
3272                    bottom: None,
3273                },
3274                ..Default::default()
3275            });
3276        }
3277
3278        if range.end == markdown_end {
3279            div.style().refine(&StyleRefinement {
3280                margin: gpui::EdgesRefinement {
3281                    top: None,
3282                    left: None,
3283                    right: None,
3284                    bottom: Some(Length::Definite(rems(0.).into())),
3285                },
3286                ..Default::default()
3287            });
3288        }
3289
3290        self.div_stack.push(DivStackEntry::new(div));
3291    }
3292
3293    fn push_root_block(&mut self, range: &Range<usize>, markdown_end: usize) {
3294        self.push_div(
3295            div().group("markdown-root-block").relative(),
3296            range,
3297            markdown_end,
3298        );
3299        self.push_div(div().pl_4(), range, markdown_end);
3300    }
3301
3302    fn push_image_child(&mut self, child: impl IntoElement) {
3303        self.modify_current_div(|el| el.flex().flex_row().flex_wrap().items_start());
3304        self.div_stack.last_mut().unwrap().line_break_mode = LineBreakMode::FlexWrap;
3305        self.append_child(child.into_any_element());
3306    }
3307
3308    fn push_line_break(&mut self, source_range: Range<usize>) {
3309        if self.uses_flex_line_breaks() {
3310            self.modify_current_div(|el| el.child(div().w_full().h_0()));
3311        } else {
3312            self.push_text("\n", source_range);
3313        }
3314    }
3315
3316    fn push_soft_break(&mut self, source_range: Range<usize>) {
3317        // A soft break right after an item in flex wrap container would otherwise
3318        // render as a stray leading space before the next wrapped item.
3319        if self.uses_flex_line_breaks() && self.pending_line.text.is_empty() {
3320            return;
3321        }
3322        self.push_text(" ", source_range);
3323    }
3324
3325    fn append_child(&mut self, child: AnyElement) {
3326        self.div_stack.last_mut().unwrap().div.extend([child]);
3327    }
3328
3329    fn uses_flex_line_breaks(&self) -> bool {
3330        self.div_stack
3331            .last()
3332            .is_some_and(|entry| entry.line_break_mode == LineBreakMode::FlexWrap)
3333    }
3334
3335    fn modify_current_div(&mut self, f: impl FnOnce(AnyDiv) -> AnyDiv) {
3336        self.flush_text();
3337        if let Some(mut entry) = self.div_stack.pop() {
3338            entry.div = f(entry.div);
3339            self.div_stack.push(entry);
3340        }
3341    }
3342
3343    fn pop_root_block(
3344        &mut self,
3345        is_active: bool,
3346        active_gutter_color: Hsla,
3347        hovered_gutter_color: Hsla,
3348    ) {
3349        self.pop_div();
3350        self.modify_current_div(|el| {
3351            el.child(
3352                div()
3353                    .h_full()
3354                    .w(px(4.0))
3355                    .when(is_active, |this| this.bg(active_gutter_color))
3356                    .group_hover("markdown-root-block", |this| {
3357                        if is_active {
3358                            this
3359                        } else {
3360                            this.bg(hovered_gutter_color)
3361                        }
3362                    })
3363                    .rounded_xs()
3364                    .absolute()
3365                    .left_0()
3366                    .top_0(),
3367            )
3368        });
3369        self.pop_div();
3370    }
3371
3372    fn pop_div(&mut self) {
3373        self.flush_text();
3374        let div = self.div_stack.pop().unwrap().div.into_any_element();
3375        self.append_child(div);
3376    }
3377
3378    fn push_sourced_element(&mut self, source_range: Range<usize>, element: impl Into<AnyElement>) {
3379        self.flush_text();
3380        let anchor = self.render_source_anchor(source_range);
3381        self.append_child(
3382            div()
3383                .relative()
3384                .child(anchor)
3385                .child(element.into())
3386                .into_any_element(),
3387        );
3388    }
3389
3390    fn push_list(&mut self, bullet_index: Option<u64>) {
3391        self.list_stack.push(ListStackEntry { bullet_index });
3392    }
3393
3394    fn next_bullet_index(&mut self) -> Option<u64> {
3395        self.list_stack.last_mut().and_then(|entry| {
3396            let item_index = entry.bullet_index.as_mut()?;
3397            *item_index += 1;
3398            Some(*item_index - 1)
3399        })
3400    }
3401
3402    fn pop_list(&mut self) {
3403        self.list_stack.pop();
3404    }
3405
3406    fn push_code_block(&mut self, language: Option<Arc<Language>>) {
3407        self.code_block_stack.push(language);
3408    }
3409
3410    fn pop_code_block(&mut self) {
3411        self.code_block_stack.pop();
3412    }
3413
3414    fn push_link(&mut self, destination_url: SharedString, source_range: Range<usize>) {
3415        self.rendered_links.push(RenderedLink {
3416            source_range,
3417            destination_url,
3418        });
3419    }
3420
3421    fn push_image_link(
3422        &mut self,
3423        destination_url: SharedString,
3424        bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
3425    ) {
3426        self.rendered_image_links.push(RenderedImageLink {
3427            bounds,
3428            destination_url,
3429        });
3430    }
3431
3432    fn push_footnote_ref(&mut self, label: SharedString, source_range: Range<usize>) {
3433        self.rendered_footnote_refs.push(RenderedFootnoteRef {
3434            source_range,
3435            label,
3436        });
3437    }
3438
3439    fn push_text(&mut self, text: &str, source_range: Range<usize>) {
3440        self.pending_line.source_mappings.push(SourceMapping {
3441            rendered_index: self.pending_line.text.len(),
3442            source_index: source_range.start,
3443        });
3444        self.pending_line.text.push_str(text);
3445        self.current_source_index = source_range.end;
3446
3447        // Compute the base text style once
3448        let text_style = self.text_style();
3449
3450        if let Some(Some(language)) = self.code_block_stack.last() {
3451            let mut offset = 0;
3452            for (range, highlight_id) in language.highlight_text(&Rope::from(text), 0..text.len()) {
3453                if range.start > offset {
3454                    self.pending_line
3455                        .runs
3456                        .push(text_style.to_run(range.start - offset));
3457                }
3458
3459                let run_len = range.len();
3460                if let Some(highlight) = self.syntax_theme.get(highlight_id).cloned() {
3461                    self.pending_line
3462                        .runs
3463                        .push(text_style.clone().highlight(highlight).to_run(run_len));
3464                } else {
3465                    self.pending_line.runs.push(text_style.to_run(run_len));
3466                }
3467                offset = range.end;
3468            }
3469
3470            if offset < text.len() {
3471                self.pending_line
3472                    .runs
3473                    .push(text_style.to_run(text.len() - offset));
3474            }
3475        } else {
3476            self.pending_line.runs.push(text_style.to_run(text.len()));
3477        }
3478    }
3479
3480    fn trim_trailing_newline(&mut self) {
3481        if self.pending_line.text.ends_with('\n') {
3482            self.pending_line
3483                .text
3484                .truncate(self.pending_line.text.len() - 1);
3485            self.pending_line.runs.last_mut().unwrap().len -= 1;
3486            self.current_source_index -= 1;
3487        }
3488    }
3489
3490    fn replace_pending_checkbox(&mut self, on_toggle: Option<CheckboxToggleCallback>) {
3491        let text = &self.pending_line.text;
3492        let trimmed = text.trim();
3493        if trimmed != "[x]" && trimmed != "[X]" && trimmed != "[ ]" {
3494            return;
3495        }
3496        let checked = trimmed != "[ ]";
3497
3498        let leading_ws = text.len() - text.trim_start().len();
3499        let marker_rendered = leading_ws..leading_ws + trimmed.len();
3500        let marker_source = self
3501            .source_range_for_rendered(&marker_rendered)
3502            .expect("pending checkbox text must have source mappings");
3503
3504        self.pending_line = PendingLine::default();
3505
3506        let toggle_state = if checked {
3507            ToggleState::Selected
3508        } else {
3509            ToggleState::Unselected
3510        };
3511        let checkbox = Checkbox::new(
3512            ElementId::Name(
3513                format!(
3514                    "table_checkbox_{}_{}",
3515                    marker_source.start, marker_source.end
3516                )
3517                .into(),
3518            ),
3519            toggle_state,
3520        )
3521        .fill();
3522
3523        let checkbox = if let Some(on_toggle) = on_toggle {
3524            checkbox
3525                .on_click(move |_state, window, cx| {
3526                    on_toggle(marker_source.clone(), !checked, window, cx);
3527                })
3528                .into_any_element()
3529        } else {
3530            checkbox.visualization_only(true).into_any_element()
3531        };
3532
3533        let mut checkbox_container = h_flex().w_full();
3534        checkbox_container = match self.text_style().text_align {
3535            TextAlign::Left => checkbox_container.justify_start(),
3536            TextAlign::Center => checkbox_container.justify_center(),
3537            TextAlign::Right => checkbox_container.justify_end(),
3538        };
3539
3540        self.append_child(checkbox_container.child(checkbox).into_any_element());
3541    }
3542
3543    fn source_range_for_rendered(&self, rendered: &Range<usize>) -> Option<Range<usize>> {
3544        source_range_for_rendered(&self.pending_line.source_mappings, rendered)
3545    }
3546
3547    fn render_source_anchor(&mut self, source_range: Range<usize>) -> AnyElement {
3548        let mut text_style = self.base_text_style.clone();
3549        text_style.color = Hsla::transparent_black();
3550        let text = "\u{200B}";
3551        let styled_text = StyledText::new(text).with_runs(vec![text_style.to_run(text.len())]);
3552        self.rendered_lines.push(RenderedLine {
3553            layout: styled_text.layout().clone(),
3554            source_mappings: vec![SourceMapping {
3555                rendered_index: 0,
3556                source_index: source_range.start,
3557            }],
3558            source_end: source_range.end,
3559            language: None,
3560            text_align: TextAlign::Left,
3561        });
3562        div()
3563            .absolute()
3564            .top_0()
3565            .left_0()
3566            .opacity(0.)
3567            .child(styled_text)
3568            .into_any_element()
3569    }
3570
3571    fn flush_text(&mut self) {
3572        let text_align = self.text_style().text_align;
3573        let line = mem::take(&mut self.pending_line);
3574        if line.text.is_empty() {
3575            return;
3576        }
3577
3578        let text = StyledText::new(line.text).with_runs(line.runs);
3579        self.rendered_lines.push(RenderedLine {
3580            layout: text.layout().clone(),
3581            source_mappings: line.source_mappings,
3582            source_end: self.current_source_index,
3583            language: self.code_block_stack.last().cloned().flatten(),
3584            text_align,
3585        });
3586        self.append_child(text.into_any());
3587    }
3588
3589    fn build(mut self) -> RenderedMarkdown {
3590        debug_assert_eq!(self.div_stack.len(), 1);
3591        self.flush_text();
3592        RenderedMarkdown {
3593            element: self.div_stack.pop().unwrap().div.into_any_element(),
3594            text: RenderedText {
3595                lines: self.rendered_lines.into(),
3596                links: self.rendered_links.into(),
3597                image_links: self.rendered_image_links.into(),
3598                footnote_refs: self.rendered_footnote_refs.into(),
3599            },
3600        }
3601    }
3602}
3603
3604struct RenderedLine {
3605    layout: TextLayout,
3606    source_mappings: Vec<SourceMapping>,
3607    source_end: usize,
3608    language: Option<Arc<Language>>,
3609    text_align: TextAlign,
3610}
3611
3612impl RenderedLine {
3613    fn rendered_index_for_source_index(&self, source_index: usize) -> usize {
3614        if source_index >= self.source_end {
3615            return self.layout.len();
3616        }
3617
3618        let mapping = match self
3619            .source_mappings
3620            .binary_search_by_key(&source_index, |probe| probe.source_index)
3621        {
3622            Ok(ix) => &self.source_mappings[ix],
3623            Err(ix) => &self.source_mappings[ix - 1],
3624        };
3625        (mapping.rendered_index + (source_index - mapping.source_index)).min(self.layout.len())
3626    }
3627
3628    fn source_index_for_rendered_index(&self, rendered_index: usize) -> usize {
3629        if rendered_index >= self.layout.len() {
3630            return self.source_end;
3631        }
3632
3633        let mapping = match self
3634            .source_mappings
3635            .binary_search_by_key(&rendered_index, |probe| probe.rendered_index)
3636        {
3637            Ok(ix) => &self.source_mappings[ix],
3638            Err(ix) => &self.source_mappings[ix - 1],
3639        };
3640        mapping.source_index + (rendered_index - mapping.rendered_index)
3641    }
3642
3643    /// Returns the source index for use as an exclusive range end at a word/selection boundary.
3644    /// When the rendered index is exactly at the start of a segment with a gap from the previous
3645    /// segment (e.g., after stripped markdown syntax like backticks), this returns the end of the
3646    /// previous segment rather than the start of the current one.
3647    fn source_index_for_exclusive_rendered_end(&self, rendered_index: usize) -> usize {
3648        if rendered_index >= self.layout.len() {
3649            return self.source_end;
3650        }
3651
3652        let ix = match self
3653            .source_mappings
3654            .binary_search_by_key(&rendered_index, |probe| probe.rendered_index)
3655        {
3656            Ok(ix) => ix,
3657            Err(ix) => {
3658                return self.source_mappings[ix - 1].source_index
3659                    + (rendered_index - self.source_mappings[ix - 1].rendered_index);
3660            }
3661        };
3662
3663        // Exact match at the start of a segment. Check if there's a gap from the previous segment.
3664        if ix > 0 {
3665            let prev_mapping = &self.source_mappings[ix - 1];
3666            let mapping = &self.source_mappings[ix];
3667            let prev_segment_len = mapping.rendered_index - prev_mapping.rendered_index;
3668            let prev_source_end = prev_mapping.source_index + prev_segment_len;
3669            if prev_source_end < mapping.source_index {
3670                return prev_source_end;
3671            }
3672        }
3673
3674        self.source_mappings[ix].source_index
3675    }
3676
3677    fn alignment_offset_for_segment(
3678        &self,
3679        available_width: Pixels,
3680        segment_start_x: Pixels,
3681        segment_end_x: Pixels,
3682    ) -> Pixels {
3683        let segment_width = segment_end_x - segment_start_x;
3684        match self.text_align {
3685            TextAlign::Left => px(0.),
3686            TextAlign::Center => ((available_width - segment_width) / 2.).max(px(0.)),
3687            TextAlign::Right => (available_width - segment_width).max(px(0.)),
3688        }
3689    }
3690
3691    fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
3692        let adjusted_position = maybe!({
3693            if self.text_align == TextAlign::Left {
3694                return None;
3695            }
3696
3697            let Some(wrapped_line) = self.layout.line_layout_for_index(0) else {
3698                return None;
3699            };
3700
3701            let bounds = self.layout.bounds();
3702            let line_height = self.layout.line_height();
3703            let relative_y = (position.y - bounds.top()).max(px(0.));
3704            let wrapped_row_ix = (relative_y / line_height) as usize;
3705            let boundaries = wrapped_line.wrap_boundaries();
3706
3707            let segment_start_x = if wrapped_row_ix == 0 {
3708                px(0.)
3709            } else {
3710                boundaries
3711                    .get(wrapped_row_ix - 1)
3712                    .map(|b| {
3713                        wrapped_line.unwrapped_layout.runs[b.run_ix].glyphs[b.glyph_ix]
3714                            .position
3715                            .x
3716                    })
3717                    .unwrap_or(px(0.))
3718            };
3719            let segment_end_x = boundaries
3720                .get(wrapped_row_ix)
3721                .map(|b| {
3722                    wrapped_line.unwrapped_layout.runs[b.run_ix].glyphs[b.glyph_ix]
3723                        .position
3724                        .x
3725                })
3726                .unwrap_or(wrapped_line.unwrapped_layout.width);
3727
3728            let alignment_offset = self.alignment_offset_for_segment(
3729                bounds.size.width,
3730                segment_start_x,
3731                segment_end_x,
3732            );
3733            Some(point(position.x - alignment_offset, position.y))
3734        })
3735        .unwrap_or(position);
3736
3737        let line_rendered_index;
3738        let out_of_bounds;
3739        match self.layout.index_for_position(adjusted_position) {
3740            Ok(ix) => {
3741                line_rendered_index = ix;
3742                out_of_bounds = false;
3743            }
3744            Err(ix) => {
3745                line_rendered_index = ix;
3746                out_of_bounds = true;
3747            }
3748        };
3749        let source_index = self.source_index_for_rendered_index(line_rendered_index);
3750        if out_of_bounds {
3751            Err(source_index)
3752        } else {
3753            Ok(source_index)
3754        }
3755    }
3756}
3757
3758#[derive(Copy, Clone, Debug, Default)]
3759struct SourceMapping {
3760    rendered_index: usize,
3761    source_index: usize,
3762}
3763
3764fn source_range_for_rendered(
3765    mappings: &[SourceMapping],
3766    rendered: &Range<usize>,
3767) -> Option<Range<usize>> {
3768    if rendered.start >= rendered.end {
3769        return None;
3770    }
3771    let start = source_index_for_rendered(mappings, rendered.start)?;
3772    let end = source_index_for_rendered(mappings, rendered.end - 1)? + 1;
3773    Some(start..end)
3774}
3775
3776fn source_index_for_rendered(mappings: &[SourceMapping], rendered_index: usize) -> Option<usize> {
3777    let mut last: Option<&SourceMapping> = None;
3778    for mapping in mappings {
3779        if mapping.rendered_index <= rendered_index {
3780            last = Some(mapping);
3781        } else {
3782            break;
3783        }
3784    }
3785    last.map(|m| m.source_index + (rendered_index - m.rendered_index))
3786}
3787
3788pub struct RenderedMarkdown {
3789    element: AnyElement,
3790    text: RenderedText,
3791}
3792
3793#[derive(Clone)]
3794struct RenderedText {
3795    lines: Rc<[RenderedLine]>,
3796    links: Rc<[RenderedLink]>,
3797    image_links: Rc<[RenderedImageLink]>,
3798    footnote_refs: Rc<[RenderedFootnoteRef]>,
3799}
3800
3801struct WrappedLineSegment {
3802    start: usize,
3803    end: usize,
3804    row_top: Pixels,
3805    layout: Arc<WrappedLineLayout>,
3806}
3807
3808#[derive(Debug, Clone, Eq, PartialEq)]
3809struct RenderedLink {
3810    source_range: Range<usize>,
3811    destination_url: SharedString,
3812}
3813
3814#[derive(Clone)]
3815struct RenderedImageLink {
3816    // Populated once the image's `canvas` overlay is painted; images aren't part of the
3817    // text layout, so their hit-test region can't be derived from a source range.
3818    bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
3819    destination_url: SharedString,
3820}
3821
3822#[derive(Debug, Clone, Eq, PartialEq)]
3823struct RenderedFootnoteRef {
3824    source_range: Range<usize>,
3825    label: SharedString,
3826}
3827
3828impl RenderedText {
3829    fn bounds_for_source_range(&self, range: Range<usize>) -> Vec<Bounds<Pixels>> {
3830        self.bounds_for_sorted_source_ranges([(0, range)])
3831            .into_iter()
3832            .map(|(_, bounds)| bounds)
3833            .collect()
3834    }
3835
3836    fn bounds_for_sorted_source_ranges(
3837        &self,
3838        ranges: impl IntoIterator<Item = (usize, Range<usize>)>,
3839    ) -> Vec<(usize, Bounds<Pixels>)> {
3840        let ranges = ranges.into_iter().collect::<Vec<_>>();
3841        let mut all_bounds = Vec::new();
3842        let mut first_possible_range_ix = 0;
3843
3844        for line in self.lines.iter() {
3845            let line_source_start = line.source_mappings.first().unwrap().source_index;
3846            while ranges
3847                .get(first_possible_range_ix)
3848                .is_some_and(|(_, range)| range.end <= line_source_start)
3849            {
3850                first_possible_range_ix += 1;
3851            }
3852
3853            let Some((_, first_possible_range)) = ranges.get(first_possible_range_ix) else {
3854                break;
3855            };
3856            if first_possible_range.start >= line.source_end {
3857                continue;
3858            }
3859
3860            let wrapped_line_segments = Self::wrapped_line_segments(line);
3861            if wrapped_line_segments.is_empty() {
3862                continue;
3863            }
3864
3865            let mut range_ix = first_possible_range_ix;
3866            while let Some((highlight_ix, range)) = ranges.get(range_ix) {
3867                if range.start >= line.source_end {
3868                    break;
3869                }
3870                Self::push_bounds_for_line_source_range(
3871                    &mut all_bounds,
3872                    *highlight_ix,
3873                    line,
3874                    &wrapped_line_segments,
3875                    range.start.max(line_source_start)..range.end.min(line.source_end),
3876                );
3877                range_ix += 1;
3878            }
3879        }
3880
3881        all_bounds
3882    }
3883
3884    fn wrapped_line_segments(line: &RenderedLine) -> SmallVec<[WrappedLineSegment; 1]> {
3885        let layout = &line.layout;
3886        let line_height = layout.line_height();
3887        let mut row_top = layout.bounds().top();
3888        let mut wrapped_line_start = 0;
3889        let mut segments = SmallVec::new();
3890
3891        for wrapped_line in layout.line_layouts() {
3892            let wrapped_line_end = wrapped_line_start + wrapped_line.len();
3893            let wrapped_line_height = wrapped_line.size(line_height).height;
3894            segments.push(WrappedLineSegment {
3895                start: wrapped_line_start,
3896                end: wrapped_line_end,
3897                row_top,
3898                layout: wrapped_line,
3899            });
3900            row_top += wrapped_line_height;
3901            wrapped_line_start = wrapped_line_end + 1;
3902        }
3903
3904        segments
3905    }
3906
3907    fn push_bounds_for_line_source_range(
3908        all_bounds: &mut Vec<(usize, Bounds<Pixels>)>,
3909        highlight_ix: usize,
3910        line: &RenderedLine,
3911        wrapped_line_segments: &[WrappedLineSegment],
3912        range: Range<usize>,
3913    ) {
3914        if range.start >= range.end {
3915            return;
3916        }
3917
3918        let layout = &line.layout;
3919        let line_bounds = layout.bounds();
3920        let line_height = layout.line_height();
3921
3922        let rendered_start = line.rendered_index_for_source_index(range.start);
3923        let rendered_end = line.rendered_index_for_source_index(range.end);
3924
3925        for wrapped_line_segment in wrapped_line_segments {
3926            if wrapped_line_segment.start >= rendered_end {
3927                break;
3928            }
3929            if wrapped_line_segment.end <= rendered_start {
3930                continue;
3931            }
3932
3933            let wrapped_line = &wrapped_line_segment.layout;
3934            let unwrapped_layout = &wrapped_line.unwrapped_layout;
3935            let wrapped_line_start = wrapped_line_segment.start;
3936            let wrapped_line_end = wrapped_line_segment.end;
3937            let mut row_top = wrapped_line_segment.row_top;
3938
3939            let row_ends = wrapped_line
3940                .wrap_boundaries()
3941                .iter()
3942                .map(|wrap_boundary| {
3943                    let glyph =
3944                        &unwrapped_layout.runs[wrap_boundary.run_ix].glyphs[wrap_boundary.glyph_ix];
3945                    (wrapped_line_start + glyph.index, glyph.position.x)
3946                })
3947                .chain([(wrapped_line_end, unwrapped_layout.width)]);
3948
3949            let mut row_start = wrapped_line_start;
3950            let mut row_start_x = Pixels::ZERO;
3951
3952            for (row_end, row_end_x) in row_ends {
3953                let selection_start = rendered_start.max(row_start);
3954                let selection_end = rendered_end.min(row_end);
3955
3956                if selection_start < selection_end {
3957                    let alignment_offset = line.alignment_offset_for_segment(
3958                        line_bounds.size.width,
3959                        row_start_x,
3960                        row_end_x,
3961                    );
3962                    let x_for_index = |index| {
3963                        line_bounds.left()
3964                            + alignment_offset
3965                            + unwrapped_layout.x_for_index(index - wrapped_line_start)
3966                            - row_start_x
3967                    };
3968                    all_bounds.push((
3969                        highlight_ix,
3970                        Bounds::from_corners(
3971                            point(x_for_index(selection_start), row_top),
3972                            point(x_for_index(selection_end), row_top + line_height),
3973                        ),
3974                    ));
3975                }
3976
3977                row_start = row_end;
3978                row_start_x = row_end_x;
3979                row_top += line_height;
3980            }
3981        }
3982    }
3983
3984    fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
3985        let mut lines = self.lines.iter().peekable();
3986        let mut fallback_line: Option<&RenderedLine> = None;
3987
3988        while let Some(line) = lines.next() {
3989            let line_bounds = line.layout.bounds();
3990
3991            // Exact match: position is within bounds (handles overlapping bounds like table columns)
3992            if line_bounds.contains(&position) {
3993                return line.source_index_for_position(position);
3994            }
3995
3996            // Track fallback for Y-coordinate based matching
3997            if position.y <= line_bounds.bottom() && fallback_line.is_none() {
3998                fallback_line = Some(line);
3999            }
4000
4001            // Handle gap between lines
4002            if position.y > line_bounds.bottom() {
4003                if let Some(next_line) = lines.peek()
4004                    && position.y < next_line.layout.bounds().top()
4005                {
4006                    return Err(line.source_end);
4007                }
4008            }
4009        }
4010
4011        // Fall back to Y-coordinate matched line
4012        if let Some(line) = fallback_line {
4013            return line.source_index_for_position(position);
4014        }
4015
4016        Err(self.lines.last().map_or(0, |line| line.source_end))
4017    }
4018
4019    fn position_for_source_index(&self, source_index: usize) -> Option<(Point<Pixels>, Pixels)> {
4020        for line in self.lines.iter() {
4021            let line_source_start = line.source_mappings.first().unwrap().source_index;
4022            if source_index < line_source_start {
4023                break;
4024            } else if source_index > line.source_end {
4025                continue;
4026            } else {
4027                let line_height = line.layout.line_height();
4028                let rendered_index_within_line = line.rendered_index_for_source_index(source_index);
4029                let position = line.layout.position_for_index(rendered_index_within_line)?;
4030                return Some((position, line_height));
4031            }
4032        }
4033        None
4034    }
4035
4036    fn surrounding_word_range(&self, source_index: usize) -> Range<usize> {
4037        for line in self.lines.iter() {
4038            if source_index > line.source_end {
4039                continue;
4040            }
4041
4042            let line_rendered_start = line.source_mappings.first().unwrap().rendered_index;
4043            let rendered_index_in_line =
4044                line.rendered_index_for_source_index(source_index) - line_rendered_start;
4045            let text = line.layout.text();
4046
4047            let scope = line.language.as_ref().map(|l| l.default_scope());
4048            let classifier = CharClassifier::new(scope);
4049
4050            let mut prev_chars = text[..rendered_index_in_line].chars().rev().peekable();
4051            let mut next_chars = text[rendered_index_in_line..].chars().peekable();
4052
4053            let word_kind = std::cmp::max(
4054                prev_chars.peek().map(|&c| classifier.kind(c)),
4055                next_chars.peek().map(|&c| classifier.kind(c)),
4056            );
4057
4058            let mut start = rendered_index_in_line;
4059            for c in prev_chars {
4060                if Some(classifier.kind(c)) == word_kind {
4061                    start -= c.len_utf8();
4062                } else {
4063                    break;
4064                }
4065            }
4066
4067            let mut end = rendered_index_in_line;
4068            for c in next_chars {
4069                if Some(classifier.kind(c)) == word_kind {
4070                    end += c.len_utf8();
4071                } else {
4072                    break;
4073                }
4074            }
4075
4076            return line.source_index_for_rendered_index(line_rendered_start + start)
4077                ..line.source_index_for_exclusive_rendered_end(line_rendered_start + end);
4078        }
4079
4080        source_index..source_index
4081    }
4082
4083    fn surrounding_line_range(&self, source_index: usize) -> Range<usize> {
4084        for line in self.lines.iter() {
4085            if source_index > line.source_end {
4086                continue;
4087            }
4088            let line_source_start = line.source_mappings.first().unwrap().source_index;
4089            return line_source_start..line.source_end;
4090        }
4091
4092        source_index..source_index
4093    }
4094
4095    fn text_for_range(&self, range: Range<usize>) -> String {
4096        let mut accumulator = String::new();
4097
4098        for line in self.lines.iter() {
4099            if range.start > line.source_end {
4100                continue;
4101            }
4102            let line_source_start = line.source_mappings.first().unwrap().source_index;
4103            if range.end < line_source_start {
4104                break;
4105            }
4106
4107            let text = line.layout.text();
4108
4109            let start = if range.start < line_source_start {
4110                0
4111            } else {
4112                line.rendered_index_for_source_index(range.start)
4113            };
4114            let end = if range.end > line.source_end {
4115                line.rendered_index_for_source_index(line.source_end)
4116            } else {
4117                line.rendered_index_for_source_index(range.end)
4118            }
4119            .min(text.len());
4120
4121            accumulator.push_str(&text[start..end]);
4122            accumulator.push('\n');
4123        }
4124        // Remove trailing newline
4125        accumulator.pop();
4126        accumulator
4127    }
4128
4129    fn link_for_source_index(&self, source_index: usize) -> Option<&RenderedLink> {
4130        self.links
4131            .iter()
4132            .find(|link| link.source_range.contains(&source_index))
4133    }
4134
4135    fn image_link_for_position(&self, position: Point<Pixels>) -> Option<&RenderedImageLink> {
4136        self.image_links.iter().find(|image| {
4137            image
4138                .bounds
4139                .get()
4140                .is_some_and(|bounds| bounds.contains(&position))
4141        })
4142    }
4143
4144    fn footnote_ref_for_source_index(&self, source_index: usize) -> Option<&RenderedFootnoteRef> {
4145        self.footnote_refs
4146            .iter()
4147            .find(|fref| fref.source_range.contains(&source_index))
4148    }
4149}
4150
4151#[cfg(test)]
4152mod tests {
4153    use super::*;
4154    use gpui::{RenderImage, TestAppContext, UpdateGlobal, size};
4155    use language::{Language, LanguageConfig, LanguageMatcher};
4156    use std::cell::RefCell;
4157    use std::sync::{
4158        Arc,
4159        atomic::{AtomicUsize, Ordering},
4160    };
4161
4162    struct TestWindow;
4163
4164    impl Render for TestWindow {
4165        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
4166            div()
4167        }
4168    }
4169
4170    fn ensure_theme_initialized(cx: &mut TestAppContext) {
4171        cx.update(|cx| {
4172            if !cx.has_global::<settings::SettingsStore>() {
4173                settings::init(cx);
4174            }
4175            if !cx.has_global::<theme::GlobalTheme>() {
4176                theme_settings::init(theme::LoadThemes::JustBase, cx);
4177            }
4178        });
4179    }
4180
4181    #[gpui::test]
4182    fn test_code_block_controls_are_unique_across_markdown_entities(cx: &mut TestAppContext) {
4183        struct TestWindow;
4184
4185        impl Render for TestWindow {
4186            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
4187                div()
4188            }
4189        }
4190
4191        struct TestMarkdowns {
4192            first_markdown: Entity<Markdown>,
4193            second_markdown: Entity<Markdown>,
4194        }
4195
4196        impl Render for TestMarkdowns {
4197            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
4198                div()
4199                    .child(MarkdownElement::new(
4200                        self.first_markdown.clone(),
4201                        MarkdownStyle::default(),
4202                    ))
4203                    .child(MarkdownElement::new(
4204                        self.second_markdown.clone(),
4205                        MarkdownStyle::default(),
4206                    ))
4207            }
4208        }
4209
4210        ensure_theme_initialized(cx);
4211
4212        let (_, cx) = cx.add_window_view(|_, _| TestWindow);
4213        let markdown = "```sh\necho hello\n```";
4214        let first_markdown = cx.new(|cx| Markdown::new(markdown.into(), None, None, cx));
4215        let second_markdown = cx.new(|cx| Markdown::new(markdown.into(), None, None, cx));
4216        cx.run_until_parked();
4217
4218        cx.draw(Default::default(), size(px(600.0), px(600.0)), |_, cx| {
4219            cx.new(|_| TestMarkdowns {
4220                first_markdown: first_markdown.clone(),
4221                second_markdown: second_markdown.clone(),
4222            })
4223            .into_any_element()
4224        });
4225    }
4226
4227    #[gpui::test]
4228    fn test_mappings(cx: &mut TestAppContext) {
4229        // Formatting.
4230        assert_mappings(
4231            &render_markdown("He*l*lo", cx),
4232            vec![vec![(0, 0), (1, 1), (2, 3), (3, 5), (4, 6), (5, 7)]],
4233        );
4234
4235        // Multiple lines.
4236        assert_mappings(
4237            &render_markdown("Hello\n\nWorld", cx),
4238            vec![
4239                vec![(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5)],
4240                vec![(0, 7), (1, 8), (2, 9), (3, 10), (4, 11), (5, 12)],
4241            ],
4242        );
4243
4244        // Multi-byte characters.
4245        assert_mappings(
4246            &render_markdown("αβγ\n\nδεζ", cx),
4247            vec![
4248                vec![(0, 0), (2, 2), (4, 4), (6, 6)],
4249                vec![(0, 8), (2, 10), (4, 12), (6, 14)],
4250            ],
4251        );
4252
4253        // Smart quotes.
4254        assert_mappings(&render_markdown("\"", cx), vec![vec![(0, 0), (3, 1)]]);
4255        assert_mappings(
4256            &render_markdown("\"hey\"", cx),
4257            vec![vec![(0, 0), (3, 1), (4, 2), (5, 3), (6, 4), (9, 5)]],
4258        );
4259
4260        // HTML Comments are ignored
4261        assert_mappings(
4262            &render_markdown(
4263                "<!--\nrdoc-file=string.c\n- str.intern   -> symbol\n- str.to_sym   -> symbol\n-->\nReturns",
4264                cx,
4265            ),
4266            vec![vec![
4267                (0, 78),
4268                (1, 79),
4269                (2, 80),
4270                (3, 81),
4271                (4, 82),
4272                (5, 83),
4273                (6, 84),
4274            ]],
4275        );
4276    }
4277
4278    fn render_markdown(markdown: &str, cx: &mut TestAppContext) -> RenderedText {
4279        render_markdown_with_language_registry(markdown, None, cx)
4280    }
4281
4282    #[gpui::test]
4283    fn test_active_search_highlight_uses_match_index(cx: &mut TestAppContext) {
4284        let markdown = cx.new(|cx| Markdown::new("zero one two".into(), None, None, cx));
4285
4286        markdown.update(cx, |markdown, cx| {
4287            markdown.set_search_highlights(vec![0..4, 5..8, 9..12], Some(0), cx);
4288            assert_eq!(markdown.search_highlights(), &[0..4, 5..8, 9..12]);
4289            assert_eq!(markdown.active_search_highlight(), Some(0));
4290
4291            markdown.set_active_search_highlight(Some(1), cx);
4292            assert_eq!(markdown.active_search_highlight(), Some(1));
4293
4294            markdown.set_active_search_highlight(Some(2), cx);
4295            assert_eq!(markdown.active_search_highlight(), Some(2));
4296
4297            markdown.set_active_search_highlight(Some(3), cx);
4298            assert_eq!(markdown.active_search_highlight(), None);
4299        });
4300    }
4301
4302    #[gpui::test]
4303    fn test_wrapped_code_block_has_no_scroll_handle(cx: &mut TestAppContext) {
4304        let markdown =
4305            cx.new(|cx| Markdown::new("```rust\nlet value = 1;\n```".into(), None, None, cx));
4306
4307        markdown.update(cx, |markdown, _| {
4308            assert!(markdown.code_block_scroll_handle(0).is_some());
4309
4310            markdown.toggle_code_block_wrap(0);
4311            assert!(markdown.code_block_scroll_handle(0).is_none());
4312
4313            markdown.toggle_code_block_wrap(0);
4314            assert!(markdown.code_block_scroll_handle(0).is_some());
4315        });
4316    }
4317
4318    #[gpui::test]
4319    fn test_frontmatter_renders_without_delimiters(cx: &mut TestAppContext) {
4320        let rendered = render_markdown_with_options(
4321            "---\ntitle: Post\n---\nBody",
4322            None,
4323            MarkdownOptions {
4324                render_metadata_blocks: true,
4325                ..Default::default()
4326            },
4327            cx,
4328        );
4329        assert_eq!(rendered.text_for_range(0..24), "title\nPost\nBody");
4330    }
4331
4332    #[gpui::test]
4333    fn test_frontmatter_falls_back_to_code_block_for_nested_yaml(cx: &mut TestAppContext) {
4334        let rendered = render_markdown_with_options(
4335            "---\ntags:\n  - zed\n---\nBody",
4336            None,
4337            MarkdownOptions {
4338                render_metadata_blocks: true,
4339                ..Default::default()
4340            },
4341            cx,
4342        );
4343        assert_eq!(rendered.text_for_range(0..26), "tags:\n  - zed\nBody");
4344    }
4345
4346    fn render_markdown_with_code_span_link(
4347        markdown: &str,
4348        callback: impl Fn(&str, &App) -> Option<SharedString> + 'static,
4349        cx: &mut TestAppContext,
4350    ) -> RenderedText {
4351        render_markdown_with_code_span_link_style(markdown, MarkdownStyle::default(), callback, cx)
4352    }
4353
4354    fn render_markdown_with_code_span_link_style(
4355        markdown: &str,
4356        style: MarkdownStyle,
4357        callback: impl Fn(&str, &App) -> Option<SharedString> + 'static,
4358        cx: &mut TestAppContext,
4359    ) -> RenderedText {
4360        ensure_theme_initialized(cx);
4361
4362        let (_, cx) = cx.add_window_view(|_, _| TestWindow);
4363        let markdown = cx.new(|cx| Markdown::new(markdown.to_string().into(), None, None, cx));
4364        cx.run_until_parked();
4365        let (rendered, _) = cx.draw(
4366            Default::default(),
4367            size(px(600.0), px(600.0)),
4368            |_window, _cx| {
4369                MarkdownElement::new(markdown, style)
4370                    .on_code_span_link(callback)
4371                    .code_block_renderer(CodeBlockRenderer::Default {
4372                        copy_button_visibility: CopyButtonVisibility::Hidden,
4373                        wrap_button_visibility: WrapButtonVisibility::Hidden,
4374                        border: false,
4375                    })
4376            },
4377        );
4378        rendered.text
4379    }
4380
4381    fn render_markdown_with_language_registry(
4382        markdown: &str,
4383        language_registry: Option<Arc<LanguageRegistry>>,
4384        cx: &mut TestAppContext,
4385    ) -> RenderedText {
4386        render_markdown_with_options(markdown, language_registry, MarkdownOptions::default(), cx)
4387    }
4388
4389    fn render_markdown_with_options(
4390        markdown: &str,
4391        language_registry: Option<Arc<LanguageRegistry>>,
4392        options: MarkdownOptions,
4393        cx: &mut TestAppContext,
4394    ) -> RenderedText {
4395        ensure_theme_initialized(cx);
4396
4397        let (_, cx) = cx.add_window_view(|_, _| TestWindow);
4398        let markdown = cx.new(|cx| {
4399            Markdown::new_with_options(
4400                markdown.to_string().into(),
4401                language_registry,
4402                None,
4403                options,
4404                cx,
4405            )
4406        });
4407        cx.run_until_parked();
4408        let (rendered, _) = cx.draw(
4409            Default::default(),
4410            size(px(600.0), px(600.0)),
4411            |_window, _cx| {
4412                MarkdownElement::new(markdown, MarkdownStyle::default()).code_block_renderer(
4413                    CodeBlockRenderer::Default {
4414                        copy_button_visibility: CopyButtonVisibility::Hidden,
4415                        wrap_button_visibility: WrapButtonVisibility::Hidden,
4416                        border: false,
4417                    },
4418                )
4419            },
4420        );
4421        rendered.text
4422    }
4423
4424    fn render_markdown_with_image_resolver(
4425        markdown: &str,
4426        options: MarkdownOptions,
4427        resolver: impl Fn(&str) -> Option<ImageSource> + 'static,
4428        cx: &mut TestAppContext,
4429    ) -> RenderedText {
4430        ensure_theme_initialized(cx);
4431
4432        let (_, cx) = cx.add_window_view(|_, _| TestWindow);
4433        let markdown = cx.new(|cx| {
4434            Markdown::new_with_options(markdown.to_string().into(), None, None, options, cx)
4435        });
4436        cx.run_until_parked();
4437        let (rendered, _) = cx.draw(
4438            Default::default(),
4439            size(px(600.0), px(600.0)),
4440            |_window, _cx| {
4441                MarkdownElement::new(markdown, MarkdownStyle::default())
4442                    .image_resolver(resolver)
4443                    .code_block_renderer(CodeBlockRenderer::Default {
4444                        copy_button_visibility: CopyButtonVisibility::Hidden,
4445                        wrap_button_visibility: WrapButtonVisibility::Hidden,
4446                        border: false,
4447                    })
4448            },
4449        );
4450        rendered.text
4451    }
4452
4453    fn test_image(cx: &mut TestAppContext) -> Arc<RenderImage> {
4454        cx.update(|cx| {
4455            cx.svg_renderer()
4456                .render_single_frame(
4457                    br#"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"></svg>"#,
4458                    1.0,
4459                )
4460                .expect("test svg should render")
4461        })
4462    }
4463
4464    #[gpui::test]
4465    fn test_soft_break_keeps_space_in_paragraph_with_image(cx: &mut TestAppContext) {
4466        let image = test_image(cx);
4467        let rendered = render_markdown_with_image_resolver(
4468            "Here is an image ![alt](https://example.com/a.png) and more text\nthat continues",
4469            MarkdownOptions::default(),
4470            move |_| Some(ImageSource::Render(image.clone())),
4471            cx,
4472        );
4473        let text: String = rendered
4474            .lines
4475            .iter()
4476            .map(|line| line.layout.wrapped_text())
4477            .collect();
4478        assert!(
4479            text.contains("more text that continues"),
4480            "soft break in an image paragraph should still separate words with a space; got: {text:?}"
4481        );
4482        assert!(
4483            !text.contains("textthat"),
4484            "soft break between words must not be dropped; got: {text:?}"
4485        );
4486    }
4487
4488    #[gpui::test]
4489    fn test_soft_break_after_image_does_not_insert_leading_space(cx: &mut TestAppContext) {
4490        let image = test_image(cx);
4491        let rendered = render_markdown_with_image_resolver(
4492            "![alt](https://example.com/a.png)\ncaption",
4493            MarkdownOptions::default(),
4494            move |_| Some(ImageSource::Render(image.clone())),
4495            cx,
4496        );
4497        let text: String = rendered
4498            .lines
4499            .iter()
4500            .map(|line| line.layout.wrapped_text())
4501            .collect();
4502        assert_eq!(
4503            text, "caption",
4504            "a soft break after an image should not insert leading whitespace before caption text; got: {text:?}"
4505        );
4506    }
4507
4508    #[gpui::test]
4509    fn test_break_between_images_does_not_inject_leading_space(cx: &mut TestAppContext) {
4510        let image = test_image(cx);
4511        let rendered = render_markdown_with_image_resolver(
4512            "![Image 3](https://example.com/3.png)\n<br>\n![Image 4](https://example.com/4.png)",
4513            MarkdownOptions {
4514                parse_html: true,
4515                ..Default::default()
4516            },
4517            move |_| Some(ImageSource::Render(image.clone())),
4518            cx,
4519        );
4520        let stray_whitespace_line = rendered.lines.iter().find_map(|line| {
4521            let text = line.layout.wrapped_text();
4522            (!text.is_empty() && text.trim().is_empty()).then_some(text)
4523        });
4524        assert!(
4525            stray_whitespace_line.is_none(),
4526            "soft break after a <br> between images must not render a stray space \
4527             before the wrapped image; got whitespace-only line: {stray_whitespace_line:?}"
4528        );
4529    }
4530
4531    #[gpui::test]
4532    fn test_break_in_tight_list_item_after_image_item_is_newline(cx: &mut TestAppContext) {
4533        let image = test_image(cx);
4534        let rendered = render_markdown_with_image_resolver(
4535            "- ![alt](https://example.com/a.png)\n- first<br>second",
4536            MarkdownOptions {
4537                parse_html: true,
4538                ..Default::default()
4539            },
4540            move |_| Some(ImageSource::Render(image.clone())),
4541            cx,
4542        );
4543        let text: String = rendered
4544            .lines
4545            .iter()
4546            .map(|line| line.layout.wrapped_text())
4547            .collect();
4548        assert!(
4549            text.contains("first\nsecond"),
4550            "break in a text-only tight list item should render as a newline; got: {text:?}"
4551        );
4552    }
4553
4554    #[gpui::test]
4555    fn test_hard_style_soft_break_after_image_moves_caption_to_next_row(cx: &mut TestAppContext) {
4556        ensure_theme_initialized(cx);
4557
4558        let image = test_image(cx);
4559        let markdown_source = "![alt](https://example.com/a.png)\ncaption";
4560        let caption_range = markdown_source.len() - "caption".len()..markdown_source.len();
4561
4562        let (_, cx) = cx.add_window_view(|_, _| TestWindow);
4563        let markdown = cx.new(|cx| {
4564            Markdown::new_with_options(
4565                markdown_source.to_string().into(),
4566                None,
4567                None,
4568                MarkdownOptions::default(),
4569                cx,
4570            )
4571        });
4572        cx.run_until_parked();
4573
4574        let mut caption_top = |soft_break_as_hard_break: bool| {
4575            let mut style = MarkdownStyle::default();
4576            style.soft_break_as_hard_break = soft_break_as_hard_break;
4577            let image = image.clone();
4578            let (rendered, _) = cx.draw(
4579                Default::default(),
4580                size(px(600.0), px(600.0)),
4581                |_window, _cx| {
4582                    MarkdownElement::new(markdown.clone(), style)
4583                        .image_resolver(move |_| Some(ImageSource::Render(image.clone())))
4584                        .code_block_renderer(CodeBlockRenderer::Default {
4585                            copy_button_visibility: CopyButtonVisibility::Hidden,
4586                            wrap_button_visibility: WrapButtonVisibility::Hidden,
4587                            border: false,
4588                        })
4589                },
4590            );
4591            rendered
4592                .text
4593                .bounds_for_source_range(caption_range.clone())
4594                .into_iter()
4595                .next()
4596                .expect("caption should have text bounds")
4597                .top()
4598        };
4599
4600        let caption_top_with_break = caption_top(true);
4601        let caption_top_without_break = caption_top(false);
4602
4603        assert!(
4604            caption_top_with_break > caption_top_without_break,
4605            "caption should render below the image for hard-style soft breaks; \
4606             top with break: {caption_top_with_break:?}, top without break: {caption_top_without_break:?}"
4607        );
4608    }
4609
4610    #[gpui::test]
4611    fn test_surrounding_word_range(cx: &mut TestAppContext) {
4612        let rendered = render_markdown("Hello world tesεζ", cx);
4613
4614        // Test word selection for "Hello"
4615        let word_range = rendered.surrounding_word_range(2); // Simulate click on 'l' in "Hello"
4616        let selected_text = rendered.text_for_range(word_range);
4617        assert_eq!(selected_text, "Hello");
4618
4619        // Test word selection for "world"
4620        let word_range = rendered.surrounding_word_range(7); // Simulate click on 'o' in "world"
4621        let selected_text = rendered.text_for_range(word_range);
4622        assert_eq!(selected_text, "world");
4623
4624        // Test word selection for "tesεζ"
4625        let word_range = rendered.surrounding_word_range(14); // Simulate click on 's' in "tesεζ"
4626        let selected_text = rendered.text_for_range(word_range);
4627        assert_eq!(selected_text, "tesεζ");
4628
4629        // Test word selection at word boundary (space)
4630        let word_range = rendered.surrounding_word_range(5); // Simulate click on space between "Hello" and "world", expect highlighting word to the left
4631        let selected_text = rendered.text_for_range(word_range);
4632        assert_eq!(selected_text, "Hello");
4633    }
4634
4635    #[gpui::test]
4636    fn test_surrounding_line_range(cx: &mut TestAppContext) {
4637        let rendered = render_markdown("First line\n\nSecond line\n\nThird lineεζ", cx);
4638
4639        // Test getting line range for first line
4640        let line_range = rendered.surrounding_line_range(5); // Simulate click somewhere in first line
4641        let selected_text = rendered.text_for_range(line_range);
4642        assert_eq!(selected_text, "First line");
4643
4644        // Test getting line range for second line
4645        let line_range = rendered.surrounding_line_range(13); // Simulate click at beginning in second line
4646        let selected_text = rendered.text_for_range(line_range);
4647        assert_eq!(selected_text, "Second line");
4648
4649        // Test getting line range for third line
4650        let line_range = rendered.surrounding_line_range(37); // Simulate click at end of third line with multi-byte chars
4651        let selected_text = rendered.text_for_range(line_range);
4652        assert_eq!(selected_text, "Third lineεζ");
4653    }
4654
4655    #[gpui::test]
4656    fn test_selection_head_movement(cx: &mut TestAppContext) {
4657        let rendered = render_markdown("Hello world test", cx);
4658
4659        let mut selection = Selection {
4660            start: 5,
4661            end: 5,
4662            reversed: false,
4663            pending: false,
4664            mode: SelectMode::Character,
4665        };
4666
4667        // Test forward selection
4668        selection.set_head(10, &rendered);
4669        assert_eq!(selection.start, 5);
4670        assert_eq!(selection.end, 10);
4671        assert!(!selection.reversed);
4672        assert_eq!(selection.tail(), 5);
4673
4674        // Test backward selection
4675        selection.set_head(2, &rendered);
4676        assert_eq!(selection.start, 2);
4677        assert_eq!(selection.end, 5);
4678        assert!(selection.reversed);
4679        assert_eq!(selection.tail(), 5);
4680
4681        // Test forward selection again from reversed state
4682        selection.set_head(15, &rendered);
4683        assert_eq!(selection.start, 5);
4684        assert_eq!(selection.end, 15);
4685        assert!(!selection.reversed);
4686        assert_eq!(selection.tail(), 5);
4687    }
4688
4689    #[gpui::test]
4690    fn test_word_selection_drag(cx: &mut TestAppContext) {
4691        let rendered = render_markdown("Hello world test", cx);
4692
4693        // Start with a simulated double-click on "world" (index 6-10)
4694        let word_range = rendered.surrounding_word_range(7); // Click on 'o' in "world"
4695        let mut selection = Selection {
4696            start: word_range.start,
4697            end: word_range.end,
4698            reversed: false,
4699            pending: true,
4700            mode: SelectMode::Word(word_range),
4701        };
4702
4703        // Drag forward to "test" - should expand selection to include "test"
4704        selection.set_head(13, &rendered); // Index in "test"
4705        assert_eq!(selection.start, 6); // Start of "world"
4706        assert_eq!(selection.end, 16); // End of "test"
4707        assert!(!selection.reversed);
4708        let selected_text = rendered.text_for_range(selection.start..selection.end);
4709        assert_eq!(selected_text, "world test");
4710
4711        // Drag backward to "Hello" - should expand selection to include "Hello"
4712        selection.set_head(2, &rendered); // Index in "Hello"
4713        assert_eq!(selection.start, 0); // Start of "Hello"
4714        assert_eq!(selection.end, 11); // End of "world" (original selection)
4715        assert!(selection.reversed);
4716        let selected_text = rendered.text_for_range(selection.start..selection.end);
4717        assert_eq!(selected_text, "Hello world");
4718
4719        // Drag back within original word - should revert to original selection
4720        selection.set_head(8, &rendered); // Back within "world"
4721        assert_eq!(selection.start, 6); // Start of "world"
4722        assert_eq!(selection.end, 11); // End of "world"
4723        assert!(!selection.reversed);
4724        let selected_text = rendered.text_for_range(selection.start..selection.end);
4725        assert_eq!(selected_text, "world");
4726    }
4727
4728    #[gpui::test]
4729    fn test_selection_with_markdown_formatting(cx: &mut TestAppContext) {
4730        let rendered = render_markdown(
4731            "This is **bold** text, this is *italic* text, use `code` here",
4732            cx,
4733        );
4734        let word_range = rendered.surrounding_word_range(10); // Inside "bold"
4735        let selected_text = rendered.text_for_range(word_range);
4736        assert_eq!(selected_text, "bold");
4737
4738        let word_range = rendered.surrounding_word_range(32); // Inside "italic"
4739        let selected_text = rendered.text_for_range(word_range);
4740        assert_eq!(selected_text, "italic");
4741
4742        let word_range = rendered.surrounding_word_range(51); // Inside "code"
4743        let selected_text = rendered.text_for_range(word_range);
4744        assert_eq!(selected_text, "code");
4745    }
4746
4747    #[gpui::test]
4748    fn test_table_column_selection(cx: &mut TestAppContext) {
4749        let rendered = render_markdown("| a | b |\n|---|---|\n| c | d |", cx);
4750
4751        assert!(rendered.lines.len() >= 2);
4752        let first_bounds = rendered.lines[0].layout.bounds();
4753        let second_bounds = rendered.lines[1].layout.bounds();
4754
4755        let first_index = match rendered.source_index_for_position(first_bounds.center()) {
4756            Ok(index) | Err(index) => index,
4757        };
4758        let second_index = match rendered.source_index_for_position(second_bounds.center()) {
4759            Ok(index) | Err(index) => index,
4760        };
4761
4762        let first_word = rendered.text_for_range(rendered.surrounding_word_range(first_index));
4763        let second_word = rendered.text_for_range(rendered.surrounding_word_range(second_index));
4764
4765        assert_eq!(first_word, "a");
4766        assert_eq!(second_word, "b");
4767    }
4768
4769    #[test]
4770    fn test_table_state_current_cell_alignment_centers_headers() {
4771        let mut table = TableState::default();
4772        table.start(vec![Alignment::Left, Alignment::Right, Alignment::None]);
4773
4774        table.start_head();
4775        for _ in 0..3 {
4776            assert_eq!(table.current_cell_alignment(), Some(Alignment::Center));
4777            table.end_cell();
4778        }
4779
4780        table.end_head();
4781        table.start_row();
4782        assert_eq!(table.current_cell_alignment(), Some(Alignment::Left));
4783        table.end_cell();
4784        assert_eq!(table.current_cell_alignment(), Some(Alignment::Right));
4785        table.end_cell();
4786        assert_eq!(table.current_cell_alignment(), Some(Alignment::None));
4787        table.end_cell();
4788        table.end_row();
4789
4790        table.end();
4791        assert_eq!(table.current_cell_alignment(), None);
4792    }
4793
4794    #[test]
4795    fn test_table_state_current_cell_alignment_outside_table() {
4796        let table = TableState::default();
4797        assert_eq!(table.current_cell_alignment(), None);
4798    }
4799
4800    #[test]
4801    fn test_table_checkbox_detection() {
4802        let md = "| Done |\n|------|\n| [x] |\n| [ ] |";
4803        let events = crate::parser::parse_markdown_with_options(md, false, false, false).events;
4804
4805        let mut in_table = false;
4806        let mut cell_texts: Vec<String> = Vec::new();
4807        let mut current_cell = String::new();
4808
4809        for (range, event) in &events {
4810            match event {
4811                MarkdownEvent::Start(MarkdownTag::Table(_)) => in_table = true,
4812                MarkdownEvent::End(MarkdownTagEnd::Table) => in_table = false,
4813                MarkdownEvent::Start(MarkdownTag::TableCell) => current_cell.clear(),
4814                MarkdownEvent::End(MarkdownTagEnd::TableCell) => {
4815                    if in_table {
4816                        cell_texts.push(current_cell.clone());
4817                    }
4818                }
4819                MarkdownEvent::Text if in_table => {
4820                    current_cell.push_str(&md[range.clone()]);
4821                }
4822                _ => {}
4823            }
4824        }
4825
4826        let checkbox_cells: Vec<&String> = cell_texts
4827            .iter()
4828            .filter(|t| {
4829                let trimmed = t.trim();
4830                trimmed == "[x]" || trimmed == "[X]" || trimmed == "[ ]"
4831            })
4832            .collect();
4833        assert_eq!(
4834            checkbox_cells.len(),
4835            2,
4836            "Expected 2 checkbox cells, got: {cell_texts:?}"
4837        );
4838        assert_eq!(checkbox_cells[0].trim(), "[x]");
4839        assert_eq!(checkbox_cells[1].trim(), "[ ]");
4840    }
4841
4842    #[test]
4843    fn test_table_checkbox_marker_source_range() {
4844        let md = "| Done |\n|------|\n|  [x]  |\n| [ ] |";
4845        let events = crate::parser::parse_markdown_with_options(md, false, false, false).events;
4846
4847        let mut in_cell = false;
4848        let mut pending_text = String::new();
4849        let mut mappings: Vec<SourceMapping> = Vec::new();
4850        let mut cell_ranges: Vec<Range<usize>> = Vec::new();
4851
4852        for (range, event) in &events {
4853            match event {
4854                MarkdownEvent::Start(MarkdownTag::TableCell) => {
4855                    in_cell = true;
4856                    pending_text.clear();
4857                    mappings.clear();
4858                }
4859                MarkdownEvent::End(MarkdownTagEnd::TableCell) => {
4860                    if in_cell {
4861                        let trimmed = pending_text.trim();
4862                        if trimmed == "[x]" || trimmed == "[X]" || trimmed == "[ ]" {
4863                            let leading = pending_text.len() - pending_text.trim_start().len();
4864                            let rendered = leading..leading + trimmed.len();
4865                            let marker_source = source_range_for_rendered(&mappings, &rendered)
4866                                .expect("marker source range");
4867                            cell_ranges.push(marker_source);
4868                        }
4869                    }
4870                    in_cell = false;
4871                }
4872                MarkdownEvent::Text if in_cell => {
4873                    mappings.push(SourceMapping {
4874                        rendered_index: pending_text.len(),
4875                        source_index: range.start,
4876                    });
4877                    pending_text.push_str(&md[range.clone()]);
4878                }
4879                _ => {}
4880            }
4881        }
4882
4883        assert_eq!(cell_ranges.len(), 2);
4884        for marker_range in &cell_ranges {
4885            let slice = &md[marker_range.clone()];
4886            assert!(
4887                slice == "[x]" || slice == "[X]" || slice == "[ ]",
4888                "expected `[x]`/`[X]`/`[ ]`, got {slice:?} at {marker_range:?}"
4889            );
4890        }
4891    }
4892
4893    #[gpui::test]
4894    fn test_escaped_pipes_in_inline_code_inside_tables(cx: &mut TestAppContext) {
4895        let markdown = "\
4896| Pattern | What it does |
4897| --- | --- |
4898| `^echo(\\s\\|$)` | command pattern |
4899| `a\\|b` | alternation |
4900| `(a\\|b)` | grouped alternation |
4901| `a\\|\\|b` | empty middle alternative |";
4902        let rendered = render_markdown(markdown, cx);
4903        let text = rendered.text_for_range(0..markdown.len());
4904
4905        assert_eq!(
4906            text,
4907            "Pattern\n\
4908             What it does\n\
4909             ^echo(\\s|$)\n\
4910             command pattern\n\
4911             a|b\n\
4912             alternation\n\
4913             (a|b)\n\
4914             grouped alternation\n\
4915             a||b\n\
4916             empty middle alternative"
4917        );
4918    }
4919
4920    #[test]
4921    fn test_source_range_for_rendered_handles_split_chunks() {
4922        let mappings = vec![
4923            SourceMapping {
4924                rendered_index: 0,
4925                source_index: 20,
4926            },
4927            SourceMapping {
4928                rendered_index: 1,
4929                source_index: 21,
4930            },
4931            SourceMapping {
4932                rendered_index: 2,
4933                source_index: 22,
4934            },
4935        ];
4936
4937        let range = source_range_for_rendered(&mappings, &(0..3)).unwrap();
4938        assert_eq!(range, 20..23);
4939
4940        let range = source_range_for_rendered(&mappings, &(1..2)).unwrap();
4941        assert_eq!(range, 21..22);
4942
4943        assert_eq!(source_range_for_rendered(&mappings, &(2..2)), None);
4944    }
4945
4946    #[gpui::test]
4947    fn test_inline_code_word_selection_excludes_backticks(cx: &mut TestAppContext) {
4948        // Test that double-clicking on inline code selects just the code content,
4949        // not the backticks. This verifies the fix for the bug where selecting
4950        // inline code would include the trailing backtick.
4951        let rendered = render_markdown("use `blah` here", cx);
4952
4953        // Source layout: "use `blah` here"
4954        //                 0123456789...
4955        // The inline code "blah" is at source positions 5-8 (content range 5..9)
4956
4957        // Click inside "blah" - should select just "blah", not "blah`"
4958        let word_range = rendered.surrounding_word_range(6); // 'l' in "blah"
4959
4960        // text_for_range extracts from the rendered text (without backticks), so it
4961        // would return "blah" even with a wrong source range. We check it anyway.
4962        let selected_text = rendered.text_for_range(word_range.clone());
4963        assert_eq!(selected_text, "blah");
4964
4965        // The source range is what matters for copy_as_markdown and selected_text,
4966        // which extract directly from the source. With the bug, this would be 5..10
4967        // which includes the closing backtick at position 9.
4968        assert_eq!(word_range, 5..9);
4969    }
4970
4971    #[gpui::test]
4972    fn test_surrounding_word_range_respects_word_characters(cx: &mut TestAppContext) {
4973        let rendered = render_markdown("foo.bar() baz", cx);
4974
4975        // Double clicking on 'f' in "foo" - should select just "foo"
4976        let word_range = rendered.surrounding_word_range(0);
4977        let selected_text = rendered.text_for_range(word_range);
4978        assert_eq!(selected_text, "foo");
4979
4980        // Double clicking on 'b' in "bar" - should select just "bar"
4981        let word_range = rendered.surrounding_word_range(4);
4982        let selected_text = rendered.text_for_range(word_range);
4983        assert_eq!(selected_text, "bar");
4984
4985        // Double clicking on 'b' in "baz" - should select "baz"
4986        let word_range = rendered.surrounding_word_range(10);
4987        let selected_text = rendered.text_for_range(word_range);
4988        assert_eq!(selected_text, "baz");
4989
4990        // Double clicking selects word characters in code blocks
4991        let javascript_language = Arc::new(Language::new(
4992            LanguageConfig {
4993                name: "JavaScript".into(),
4994                matcher: (LanguageMatcher {
4995                    path_suffixes: vec!["js".to_string()],
4996                    ..Default::default()
4997                })
4998                .into(),
4999                word_characters: ['$', '#'].into_iter().collect(),
5000                ..Default::default()
5001            },
5002            None,
5003        ));
5004
5005        let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
5006        language_registry.add(javascript_language);
5007
5008        let rendered = render_markdown_with_language_registry(
5009            "```javascript\n$foo #bar\n```",
5010            Some(language_registry),
5011            cx,
5012        );
5013
5014        let word_range = rendered.surrounding_word_range(14);
5015        let selected_text = rendered.text_for_range(word_range);
5016        assert_eq!(selected_text, "$foo");
5017
5018        let word_range = rendered.surrounding_word_range(19);
5019        let selected_text = rendered.text_for_range(word_range);
5020        assert_eq!(selected_text, "#bar");
5021    }
5022
5023    #[gpui::test]
5024    fn test_all_selection(cx: &mut TestAppContext) {
5025        let rendered = render_markdown("Hello world\n\nThis is a test\n\nwith multiple lines", cx);
5026
5027        let total_length = rendered
5028            .lines
5029            .last()
5030            .map(|line| line.source_end)
5031            .unwrap_or(0);
5032
5033        let mut selection = Selection {
5034            start: 0,
5035            end: total_length,
5036            reversed: false,
5037            pending: true,
5038            mode: SelectMode::All,
5039        };
5040
5041        selection.set_head(5, &rendered); // Try to set head in middle
5042        assert_eq!(selection.start, 0);
5043        assert_eq!(selection.end, total_length);
5044        assert!(!selection.reversed);
5045
5046        selection.set_head(25, &rendered); // Try to set head near end
5047        assert_eq!(selection.start, 0);
5048        assert_eq!(selection.end, total_length);
5049        assert!(!selection.reversed);
5050
5051        let selected_text = rendered.text_for_range(selection.start..selection.end);
5052        assert_eq!(
5053            selected_text,
5054            "Hello world\nThis is a test\nwith multiple lines"
5055        );
5056    }
5057
5058    fn nbsp(n: usize) -> String {
5059        "\u{00A0}".repeat(n)
5060    }
5061
5062    #[test]
5063    fn test_escape_plain_text() {
5064        assert_eq!(Markdown::escape("hello world"), "hello world");
5065        assert_eq!(Markdown::escape(""), "");
5066        assert_eq!(Markdown::escape("café ☕ naïve"), "café ☕ naïve");
5067    }
5068
5069    #[test]
5070    fn test_escape_punctuation() {
5071        assert_eq!(Markdown::escape("hello `world`"), r"hello \`world\`");
5072        assert_eq!(Markdown::escape("a|b"), r"a\|b");
5073    }
5074
5075    #[test]
5076    fn test_escape_leading_spaces() {
5077        assert_eq!(Markdown::escape("    hello"), [&nbsp(4), "hello"].concat());
5078        assert_eq!(
5079            Markdown::escape("    | { a: string }"),
5080            [&nbsp(4), r"\| \{ a\: string \}"].concat()
5081        );
5082        assert_eq!(
5083            Markdown::escape("  first\n  second"),
5084            [&nbsp(2), "first\n\n", &nbsp(2), "second"].concat()
5085        );
5086        assert_eq!(Markdown::escape("hello   world"), "hello   world");
5087    }
5088
5089    #[test]
5090    fn test_escape_leading_tabs() {
5091        assert_eq!(Markdown::escape("\thello"), [&nbsp(4), "hello"].concat());
5092        assert_eq!(
5093            Markdown::escape("hello\n\t\tindented"),
5094            ["hello\n\n", &nbsp(8), "indented"].concat()
5095        );
5096        assert_eq!(
5097            Markdown::escape(" \t hello"),
5098            [&nbsp(1 + 4 + 1), "hello"].concat()
5099        );
5100        assert_eq!(Markdown::escape("hello\tworld"), "hello\tworld");
5101    }
5102
5103    #[test]
5104    fn test_escape_newlines() {
5105        assert_eq!(Markdown::escape("a\nb"), "a\n\nb");
5106        assert_eq!(Markdown::escape("a\n\nb"), "a\n\n\n\nb");
5107        assert_eq!(Markdown::escape("\nhello"), "\n\nhello");
5108    }
5109
5110    #[test]
5111    fn test_escape_multiline_diagnostic() {
5112        assert_eq!(
5113            Markdown::escape("    | { a: string }\n    | { b: number }"),
5114            [
5115                &nbsp(4),
5116                r"\| \{ a\: string \}",
5117                "\n\n",
5118                &nbsp(4),
5119                r"\| \{ b\: number \}",
5120            ]
5121            .concat()
5122        );
5123    }
5124
5125    #[test]
5126    fn test_escape_non_ascii() {
5127        // Cyrillic characters should not have backslashes added before them,
5128        // but ASCII punctuation should still be escaped.
5129        assert_eq!(Markdown::escape("Привет, мир"), r"Привет\, мир");
5130        // Test with markdown special characters mixed in
5131        assert_eq!(Markdown::escape("Привет, *мир*"), r"Привет\, \*мир\*");
5132        // Test with the exact example from the issue (single quotes are also ASCII punctuation)
5133        assert_eq!(
5134            Markdown::escape("Отсутствует пробел справа от ','"),
5135            r"Отсутствует пробел справа от \'\,\'"
5136        );
5137        // Test more non-ASCII scripts
5138        assert_eq!(
5139            Markdown::escape("こんにちは *world*"),
5140            r"こんにちは \*world\*"
5141        );
5142        assert_eq!(Markdown::escape("العربيّة [link]"), r"العربيّة \[link\]");
5143        assert_eq!(Markdown::escape("Ελληνικά _text_"), r"Ελληνικά \_text\_");
5144        assert_eq!(Markdown::escape("עברית `code`"), r"עברית \`code\`");
5145        // Non-ASCII followed by ASCII punctuation
5146        assert_eq!(Markdown::escape("Test: тест"), r"Test\: тест");
5147    }
5148
5149    fn has_code_block(markdown: &str) -> bool {
5150        let parsed_data = parse_markdown_with_options(markdown, false, false, false);
5151        parsed_data
5152            .events
5153            .iter()
5154            .any(|(_, event)| matches!(event, MarkdownEvent::Start(MarkdownTag::CodeBlock { .. })))
5155    }
5156
5157    #[test]
5158    fn test_escape_output_len_matches_precomputed() {
5159        let cases = [
5160            "",
5161            "hello world",
5162            "hello `world`",
5163            "    hello",
5164            "    | { a: string }",
5165            "\thello",
5166            "hello\n\t\tindented",
5167            " \t hello",
5168            "hello\tworld",
5169            "a\nb",
5170            "a\n\nb",
5171            "\nhello",
5172            "    | { a: string }\n    | { b: number }",
5173            "café ☕ naïve",
5174        ];
5175        for input in cases {
5176            let mut escaper = MarkdownEscaper::new();
5177            let precomputed: usize = input.chars().map(|c| escaper.next(c).output_len(c)).sum();
5178
5179            let mut escaper = MarkdownEscaper::new();
5180            let mut output = String::new();
5181            for c in input.chars() {
5182                escaper.next(c).write_to(c, &mut output);
5183            }
5184
5185            assert_eq!(precomputed, output.len(), "length mismatch for {:?}", input);
5186        }
5187    }
5188
5189    #[gpui::test]
5190    fn test_inline_br_renders_as_line_break(cx: &mut TestAppContext) {
5191        let options = MarkdownOptions {
5192            parse_html: true,
5193            ..Default::default()
5194        };
5195
5196        for br in ["<br>", "<br/>", "<br />"] {
5197            let md = format!("first{br}second");
5198            let rendered = render_markdown_with_options(&md, None, options, cx);
5199            let text: String = rendered
5200                .lines
5201                .iter()
5202                .map(|line| line.layout.wrapped_text())
5203                .collect();
5204            assert!(
5205                !text.contains(br),
5206                "{br} should not appear as literal text; got: {text:?}"
5207            );
5208            assert!(
5209                text.contains("first\nsecond"),
5210                "{br} should produce a newline between 'first' and 'second'; got: {text:?}"
5211            );
5212        }
5213    }
5214
5215    #[gpui::test]
5216    fn test_hard_break_in_text_paragraph_after_paragraph(cx: &mut TestAppContext) {
5217        let options = MarkdownOptions {
5218            parse_html: true,
5219            ..Default::default()
5220        };
5221        let rendered =
5222            render_markdown_with_options("para one\n\nfirst<br>second", None, options, cx);
5223        let all_text: String = rendered
5224            .lines
5225            .iter()
5226            .map(|line| line.layout.wrapped_text())
5227            .collect::<Vec<_>>()
5228            .join("\n");
5229        assert!(
5230            all_text.contains("first") && all_text.contains("second"),
5231            "both sides of <br> should be present; got: {all_text:?}"
5232        );
5233        assert!(
5234            !all_text.contains("<br>"),
5235            "<br> should not appear as literal text; got: {all_text:?}"
5236        );
5237    }
5238
5239    #[test]
5240    fn test_escape_prevents_code_block() {
5241        let diagnostic = "    | { a: string }";
5242        assert!(has_code_block(diagnostic));
5243        assert!(!has_code_block(&Markdown::escape(diagnostic)));
5244    }
5245
5246    #[gpui::test]
5247    fn test_link_detected_for_source_index(cx: &mut TestAppContext) {
5248        let rendered = render_markdown("[Click here](https://example.com)", cx);
5249
5250        assert_eq!(rendered.links.len(), 1);
5251        assert_eq!(rendered.links[0].destination_url, "https://example.com");
5252
5253        // Source index 1 ('C' in "Click") is inside the link's source range
5254        let link = rendered.link_for_source_index(1);
5255        assert!(link.is_some());
5256        assert_eq!(link.unwrap().destination_url, "https://example.com");
5257
5258        // A source index past the end of the link range returns None
5259        let past_end = rendered.links[0].source_range.end;
5260        assert!(rendered.link_for_source_index(past_end).is_none());
5261    }
5262
5263    #[gpui::test]
5264    fn test_link_for_source_index_ignores_plain_text(cx: &mut TestAppContext) {
5265        let rendered = render_markdown("Hello world", cx);
5266
5267        assert!(rendered.links.is_empty());
5268        assert!(rendered.link_for_source_index(0).is_none());
5269        assert!(rendered.link_for_source_index(5).is_none());
5270    }
5271
5272    #[gpui::test]
5273    fn test_code_span_link_detected_for_source_index(cx: &mut TestAppContext) {
5274        let source = "see `foo.rs` for details";
5275        let rendered = render_markdown_with_code_span_link(
5276            source,
5277            |text, _cx| (text == "foo.rs").then(|| "file:///tmp/foo.rs".into()),
5278            cx,
5279        );
5280
5281        assert_eq!(rendered.links.len(), 1);
5282        assert_eq!(rendered.links[0].destination_url, "file:///tmp/foo.rs");
5283
5284        let code_index = source.find("foo.rs").unwrap();
5285        let link = rendered.link_for_source_index(code_index);
5286        assert!(link.is_some());
5287        assert_eq!(link.unwrap().destination_url, "file:///tmp/foo.rs");
5288
5289        assert!(
5290            rendered
5291                .link_for_source_index(source.find("see").unwrap())
5292                .is_none()
5293        );
5294    }
5295
5296    #[gpui::test]
5297    fn test_code_span_link_receives_decoded_inline_code(cx: &mut TestAppContext) {
5298        let source = r"| Pattern |
5299| --- |
5300| `a\|b` |";
5301        let rendered = render_markdown_with_code_span_link(
5302            source,
5303            |text, _cx| (text == "a|b").then(|| "file:///tmp/a-or-b".into()),
5304            cx,
5305        );
5306
5307        assert_eq!(rendered.links.len(), 1);
5308        assert_eq!(rendered.links[0].destination_url, "file:///tmp/a-or-b");
5309    }
5310
5311    #[gpui::test]
5312    fn test_code_span_link_ignores_code_when_mouse_interaction_is_prevented(
5313        cx: &mut TestAppContext,
5314    ) {
5315        let callback_count = Arc::new(AtomicUsize::new(0));
5316        let rendered = render_markdown_with_code_span_link_style(
5317            "see `foo.rs` for details",
5318            MarkdownStyle {
5319                prevent_mouse_interaction: true,
5320                ..MarkdownStyle::default()
5321            },
5322            {
5323                let callback_count = callback_count.clone();
5324                move |text, _cx| {
5325                    callback_count.fetch_add(1, Ordering::Relaxed);
5326                    (text == "foo.rs").then(|| "file:///tmp/foo.rs".into())
5327                }
5328            },
5329            cx,
5330        );
5331
5332        assert!(rendered.links.is_empty());
5333        assert_eq!(callback_count.load(Ordering::Relaxed), 0);
5334    }
5335
5336    #[gpui::test]
5337    fn test_code_span_link_ignores_code_without_callback(cx: &mut TestAppContext) {
5338        let rendered = render_markdown("see `foo.rs` for details", cx);
5339
5340        assert!(rendered.links.is_empty());
5341    }
5342
5343    #[gpui::test]
5344    fn test_code_span_link_ignores_code_inside_markdown_link(cx: &mut TestAppContext) {
5345        let source = "see [`foo.rs`](https://example.com) for details";
5346        let rendered = render_markdown_with_code_span_link(
5347            source,
5348            |text, _cx| (text == "foo.rs").then(|| "file:///tmp/foo.rs".into()),
5349            cx,
5350        );
5351
5352        assert_eq!(rendered.links.len(), 1);
5353        assert_eq!(rendered.links[0].destination_url, "https://example.com");
5354    }
5355
5356    #[gpui::test]
5357    fn test_context_menu_link_initial_state(cx: &mut TestAppContext) {
5358        ensure_theme_initialized(cx);
5359        let (_, cx) = cx.add_window_view(|_, _| TestWindow);
5360        let markdown =
5361            cx.new(|cx| Markdown::new("Hello [world](https://example.com)".into(), None, None, cx));
5362        cx.run_until_parked();
5363
5364        cx.update(|_window, cx| {
5365            assert!(markdown.read(cx).context_menu_link().is_none());
5366        });
5367    }
5368
5369    #[gpui::test]
5370    fn test_url_hover_callback(cx: &mut TestAppContext) {
5371        struct HoverTestView {
5372            markdown: Entity<Markdown>,
5373            hovered_urls: Rc<RefCell<Vec<Option<SharedString>>>>,
5374        }
5375
5376        impl Render for HoverTestView {
5377            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
5378                let hovered_urls = self.hovered_urls.clone();
5379                div().size_full().child(
5380                    MarkdownElement::new(self.markdown.clone(), MarkdownStyle::default())
5381                        .on_url_hover(move |url, _, _| {
5382                            hovered_urls.borrow_mut().push(url);
5383                        }),
5384                )
5385            }
5386        }
5387
5388        ensure_theme_initialized(cx);
5389        let hovered_urls = Rc::new(RefCell::new(Vec::new()));
5390        let (_, cx) = cx.add_window_view({
5391            let hovered_urls = hovered_urls.clone();
5392            move |_, cx| HoverTestView {
5393                markdown: cx
5394                    .new(|cx| Markdown::new("[link](https://example.com)".into(), None, None, cx)),
5395                hovered_urls,
5396            }
5397        });
5398        cx.run_until_parked();
5399
5400        cx.simulate_mouse_move(point(px(8.), px(8.)), None, gpui::Modifiers::default());
5401        assert_eq!(
5402            hovered_urls.borrow().last().cloned().flatten().as_deref(),
5403            Some("https://example.com")
5404        );
5405
5406        cx.simulate_mouse_move(point(px(500.), px(500.)), None, gpui::Modifiers::default());
5407        assert_eq!(hovered_urls.borrow().last(), Some(&None));
5408    }
5409
5410    #[gpui::test]
5411    fn test_url_hover_callback_for_linked_image(cx: &mut TestAppContext) {
5412        struct HoverTestView {
5413            markdown: Entity<Markdown>,
5414            hovered_urls: Rc<RefCell<Vec<Option<SharedString>>>>,
5415        }
5416
5417        impl Render for HoverTestView {
5418            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
5419                let hovered_urls = self.hovered_urls.clone();
5420                div().size_full().child(
5421                    MarkdownElement::new(self.markdown.clone(), MarkdownStyle::default())
5422                        .image_resolver(|_| Some(loaded_image_source()))
5423                        .on_url_hover(move |url, _, _| {
5424                            hovered_urls.borrow_mut().push(url);
5425                        }),
5426                )
5427            }
5428        }
5429
5430        ensure_theme_initialized(cx);
5431        let hovered_urls = Rc::new(RefCell::new(Vec::new()));
5432        let (_, cx) = cx.add_window_view({
5433            let hovered_urls = hovered_urls.clone();
5434            move |_, cx| HoverTestView {
5435                markdown: cx.new(|cx| {
5436                    Markdown::new(
5437                        "[![badge](https://example.com/badge.png)](https://example.com)".into(),
5438                        None,
5439                        None,
5440                        cx,
5441                    )
5442                }),
5443                hovered_urls,
5444            }
5445        });
5446        cx.run_until_parked();
5447
5448        cx.simulate_mouse_move(point(px(4.), px(4.)), None, gpui::Modifiers::default());
5449        assert_eq!(
5450            hovered_urls.borrow().last().cloned().flatten().as_deref(),
5451            Some("https://example.com")
5452        );
5453
5454        cx.simulate_mouse_move(point(px(8.), px(8.)), None, gpui::Modifiers::default());
5455        assert_eq!(
5456            hovered_urls.borrow().last().cloned().flatten().as_deref(),
5457            Some("https://example.com")
5458        );
5459
5460        cx.simulate_mouse_move(point(px(500.), px(500.)), None, gpui::Modifiers::default());
5461        assert_eq!(hovered_urls.borrow().last(), Some(&None));
5462    }
5463
5464    #[gpui::test]
5465    fn test_capture_for_context_menu(cx: &mut TestAppContext) {
5466        ensure_theme_initialized(cx);
5467        let (_, cx) = cx.add_window_view(|_, _| TestWindow);
5468        let markdown = cx.new(|cx| Markdown::new("some text".into(), None, None, cx));
5469        cx.run_until_parked();
5470
5471        // Simulates right-clicking on a link, with "text" selected
5472        let url: SharedString = "https://example.com".into();
5473        markdown.update(cx, |md, _cx| {
5474            md.selection.start = 5;
5475            md.selection.end = 9;
5476            md.capture_for_context_menu(Some(url.clone()), None);
5477        });
5478        cx.update(|_window, cx| {
5479            let markdown = markdown.read(cx);
5480            assert_eq!(
5481                markdown.context_menu_link().map(SharedString::as_ref),
5482                Some("https://example.com")
5483            );
5484            assert_eq!(
5485                markdown
5486                    .context_menu_selected_markdown()
5487                    .map(SharedString::as_ref),
5488                Some("text")
5489            );
5490            assert_eq!(
5491                markdown
5492                    .context_menu_selected_text()
5493                    .map(SharedString::as_ref),
5494                Some("text")
5495            );
5496        });
5497
5498        // Simulates right-clicking on plain text with no selection — everything is cleared
5499        markdown.update(cx, |md, _cx| {
5500            md.selection.start = 0;
5501            md.selection.end = 0;
5502            md.capture_for_context_menu(None, None);
5503        });
5504        cx.update(|_window, cx| {
5505            let markdown = markdown.read(cx);
5506            assert!(markdown.context_menu_link().is_none());
5507            assert!(markdown.context_menu_selected_markdown().is_none());
5508            assert!(markdown.context_menu_selected_text().is_none());
5509        });
5510    }
5511
5512    #[gpui::test]
5513    fn test_preview_body_font_size_is_rem_based(cx: &mut TestAppContext) {
5514        ensure_theme_initialized(cx);
5515        let (_, cx) = cx.add_window_view(|_, _| TestWindow);
5516        cx.update(|window, cx| {
5517            let style = MarkdownStyle::themed(MarkdownFont::Preview, window, cx);
5518            assert!(
5519                matches!(style.base_text_style.font_size, AbsoluteLength::Rems(_)),
5520                "preview body font size must be rem-based, got {:?}",
5521                style.base_text_style.font_size
5522            );
5523            assert!(
5524                matches!(
5525                    style.container_style.text.font_size,
5526                    Some(AbsoluteLength::Rems(_))
5527                ),
5528                "preview container font size must be rem-based, got {:?}",
5529                style.container_style.text.font_size
5530            );
5531        });
5532    }
5533
5534    fn failing_image_source() -> ImageSource {
5535        ImageSource::Custom(Arc::new(|_, _| {
5536            Some(Err(gpui::ImageCacheError::Asset(
5537                "failed to load image".into(),
5538            )))
5539        }))
5540    }
5541
5542    fn loaded_image_source() -> ImageSource {
5543        let buffer = image::ImageBuffer::from_pixel(16, 16, image::Rgba([0, 0, 0, 255]));
5544        ImageSource::Render(Arc::new(gpui::RenderImage::new(SmallVec::from_elem(
5545            image::Frame::new(buffer),
5546            1,
5547        ))))
5548    }
5549
5550    fn open_markdown_image_test_window<'a>(
5551        source: &str,
5552        image_source: ImageSource,
5553        cx: &'a mut TestAppContext,
5554    ) -> &'a mut gpui::VisualTestContext {
5555        struct ImageTestView {
5556            markdown: Entity<Markdown>,
5557            image_source: ImageSource,
5558        }
5559
5560        impl Render for ImageTestView {
5561            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
5562                let image_source = self.image_source.clone();
5563                div().size_full().child(
5564                    MarkdownElement::new(self.markdown.clone(), MarkdownStyle::default())
5565                        .image_resolver(move |_| Some(image_source.clone())),
5566                )
5567            }
5568        }
5569
5570        ensure_theme_initialized(cx);
5571
5572        let source = source.to_string();
5573        let (_, cx) = cx.add_window_view(|_, cx| ImageTestView {
5574            markdown: cx.new(|cx| Markdown::new(source.into(), None, None, cx)),
5575            image_source,
5576        });
5577        cx.run_until_parked();
5578        cx
5579    }
5580
5581    #[gpui::test]
5582    fn test_clicking_image_fallback_opens_image_url(cx: &mut TestAppContext) {
5583        let cx = open_markdown_image_test_window(
5584            "![alt text](https://example.com/image.png)",
5585            failing_image_source(),
5586            cx,
5587        );
5588
5589        cx.simulate_click(point(px(8.), px(8.)), gpui::Modifiers::default());
5590        assert_eq!(
5591            cx.opened_url(),
5592            Some("https://example.com/image.png".to_string())
5593        );
5594    }
5595
5596    #[gpui::test]
5597    fn test_clicking_image_fallback_inside_link_opens_link_url(cx: &mut TestAppContext) {
5598        let cx = open_markdown_image_test_window(
5599            "[![alt text](https://example.com/image.png)](https://example.com/link)",
5600            failing_image_source(),
5601            cx,
5602        );
5603
5604        cx.simulate_click(point(px(8.), px(8.)), gpui::Modifiers::default());
5605        assert_eq!(
5606            cx.opened_url(),
5607            Some("https://example.com/link".to_string())
5608        );
5609    }
5610
5611    #[gpui::test]
5612    fn test_clicking_loaded_image_inside_link_opens_link_url(cx: &mut TestAppContext) {
5613        let cx = open_markdown_image_test_window(
5614            "[![alt text](https://example.com/image.png)](https://example.com/link)",
5615            loaded_image_source(),
5616            cx,
5617        );
5618
5619        cx.simulate_click(point(px(8.), px(8.)), gpui::Modifiers::default());
5620        assert_eq!(
5621            cx.opened_url(),
5622            Some("https://example.com/link".to_string())
5623        );
5624    }
5625
5626    #[track_caller]
5627    fn assert_mappings(rendered: &RenderedText, expected: Vec<Vec<(usize, usize)>>) {
5628        assert_eq!(rendered.lines.len(), expected.len(), "line count mismatch");
5629        for (line_ix, line_mappings) in expected.into_iter().enumerate() {
5630            let line = &rendered.lines[line_ix];
5631
5632            assert!(
5633                line.source_mappings.windows(2).all(|mappings| {
5634                    mappings[0].source_index < mappings[1].source_index
5635                        && mappings[0].rendered_index < mappings[1].rendered_index
5636                }),
5637                "line {} has duplicate mappings: {:?}",
5638                line_ix,
5639                line.source_mappings
5640            );
5641
5642            for (rendered_ix, source_ix) in line_mappings {
5643                assert_eq!(
5644                    line.source_index_for_rendered_index(rendered_ix),
5645                    source_ix,
5646                    "line {}, rendered_ix {}",
5647                    line_ix,
5648                    rendered_ix
5649                );
5650
5651                assert_eq!(
5652                    line.rendered_index_for_source_index(source_ix),
5653                    rendered_ix,
5654                    "line {}, source_ix {}",
5655                    line_ix,
5656                    source_ix
5657                );
5658            }
5659        }
5660    }
5661
5662    #[gpui::test]
5663    fn test_bounds_for_source_range_skips_gaps_between_rendered_lines(cx: &mut TestAppContext) {
5664        let source = "First\n\nSecond";
5665        let rendered = render_markdown(source, cx);
5666        let highlight_bounds = rendered.bounds_for_source_range(0..source.len());
5667        assert_eq!(highlight_bounds.len(), rendered.lines.len());
5668
5669        for (line, highlight_bounds) in rendered.lines.iter().zip(highlight_bounds.iter()) {
5670            let line_bounds = line.layout.bounds();
5671            assert_eq!(highlight_bounds.top(), line_bounds.top());
5672            assert_eq!(
5673                highlight_bounds.bottom(),
5674                line_bounds.top() + line.layout.line_height()
5675            );
5676        }
5677    }
5678
5679    #[gpui::test]
5680    fn test_bounds_for_source_range_returns_one_bound_per_soft_wrap_row(cx: &mut TestAppContext) {
5681        let sentence = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, \
5682            sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
5683        let source = [sentence, sentence, sentence, sentence].join(" ");
5684        let rendered = render_markdown(&source, cx);
5685        let line = &rendered.lines[0];
5686        let line_bounds = line.layout.bounds();
5687        let line_height = line.layout.line_height();
5688        let wrapped_line = line.layout.line_layout_for_index(0).unwrap();
5689        let visual_row_count = wrapped_line.wrap_boundaries().len() + 1;
5690
5691        let highlight_bounds = rendered.bounds_for_source_range(0..source.len());
5692        assert_eq!(highlight_bounds.len(), visual_row_count);
5693
5694        let mut row_top = line_bounds.top();
5695        for (row_index, row_bounds) in highlight_bounds.iter().enumerate() {
5696            assert_eq!(row_bounds.top(), row_top);
5697            assert_eq!(row_bounds.bottom(), row_top + line_height);
5698            assert!(
5699                row_bounds.size.width > Pixels::ZERO,
5700                "row {row_index} should have a non-empty highlight"
5701            );
5702            row_top += line_height;
5703        }
5704    }
5705
5706    #[gpui::test]
5707    fn test_heading_font_sizes_are_distinct(cx: &mut TestAppContext) {
5708        let rendered = render_markdown("# H1\n\n## H2\n\n### H3\n\nBody text", cx);
5709
5710        assert!(
5711            rendered.lines.len() >= 4,
5712            "expected at least 4 rendered lines, got {}",
5713            rendered.lines.len()
5714        );
5715
5716        let h1_line_height = rendered.lines[0].layout.line_height();
5717        let h2_line_height = rendered.lines[1].layout.line_height();
5718        let h3_line_height = rendered.lines[2].layout.line_height();
5719        let body_line_height = rendered.lines[3].layout.line_height();
5720
5721        assert!(
5722            h1_line_height > h2_line_height,
5723            "H1 line height ({h1_line_height:?}) should be greater than H2 ({h2_line_height:?})"
5724        );
5725        assert!(
5726            h2_line_height > h3_line_height,
5727            "H2 line height ({h2_line_height:?}) should be greater than H3 ({h3_line_height:?})"
5728        );
5729        assert!(
5730            h3_line_height > body_line_height,
5731            "H3 line height ({h3_line_height:?}) should be greater than body text ({body_line_height:?})"
5732        );
5733    }
5734
5735    #[gpui::test]
5736    fn test_ui_zoom_does_not_affect_markdown_preview(cx: &mut TestAppContext) {
5737        ensure_theme_initialized(cx);
5738
5739        cx.update(|cx| {
5740            settings::SettingsStore::update_global(cx, |store, cx| {
5741                store.update_user_settings(cx, |settings| {
5742                    settings.theme.ui_font_size = Some(16.0.into());
5743                    settings.theme.markdown_preview_font_size = None;
5744                });
5745            });
5746        });
5747        cx.run_until_parked();
5748
5749        cx.update(|cx| {
5750            let before = ThemeSettings::get_global(cx).markdown_preview_font_size(cx);
5751            assert_eq!(before, px(16.0));
5752
5753            theme_settings::adjust_ui_font_size(cx, |size| size + px(3.0));
5754
5755            assert_eq!(ThemeSettings::get_global(cx).ui_font_size(cx), px(19.0));
5756            assert_eq!(
5757                ThemeSettings::get_global(cx).markdown_preview_font_size(cx),
5758                before
5759            );
5760        });
5761    }
5762
5763    #[gpui::test]
5764    fn test_markdown_preview_follows_ui_font_size_setting_when_unset(cx: &mut TestAppContext) {
5765        ensure_theme_initialized(cx);
5766
5767        cx.update(|cx| {
5768            settings::SettingsStore::update_global(cx, |store, cx| {
5769                store.update_user_settings(cx, |settings| {
5770                    settings.theme.ui_font_size = Some(20.0.into());
5771                    settings.theme.markdown_preview_font_size = None;
5772                });
5773            });
5774        });
5775        cx.run_until_parked();
5776        cx.update(|cx| {
5777            assert_eq!(
5778                ThemeSettings::get_global(cx).markdown_preview_font_size(cx),
5779                px(20.0)
5780            );
5781        });
5782
5783        cx.update(|cx| {
5784            settings::SettingsStore::update_global(cx, |store, cx| {
5785                store.update_user_settings(cx, |settings| {
5786                    settings.theme.ui_font_size = Some(24.0.into());
5787                });
5788            });
5789        });
5790        cx.run_until_parked();
5791        cx.update(|cx| {
5792            assert_eq!(
5793                ThemeSettings::get_global(cx).markdown_preview_font_size(cx),
5794                px(24.0)
5795            );
5796        });
5797    }
5798}
5799
Served at tenant.openagents/omega Member data and write actions are omitted.