markdown.rs

   1pub mod html;
   2mod mermaid;
   3pub mod parser;
   4mod path_range;
   5
   6use base64::Engine as _;
   7use futures::FutureExt as _;
   8use gpui::EdgesRefinement;
   9use gpui::HitboxBehavior;
  10use gpui::UnderlineStyle;
  11use language::LanguageName;
  12
  13use log::Level;
  14use mermaid::{
  15    MermaidState, ParsedMarkdownMermaidDiagram, extract_mermaid_diagrams, render_mermaid_diagram,
  16};
  17pub use path_range::{LineCol, PathWithRange};
  18use settings::Settings as _;
  19use theme_settings::ThemeSettings;
  20use ui::Checkbox;
  21use ui::CopyButton;
  22
  23use std::borrow::Cow;
  24use std::collections::BTreeMap;
  25use std::iter;
  26use std::mem;
  27use std::ops::Range;
  28use std::path::Path;
  29use std::rc::Rc;
  30use std::sync::Arc;
  31use std::time::Duration;
  32
  33use collections::{HashMap, HashSet};
  34use gpui::{
  35    AnyElement, App, BorderStyle, Bounds, ClipboardItem, CursorStyle, DispatchPhase, Edges, Entity,
  36    FocusHandle, Focusable, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla, Image,
  37    ImageFormat, ImageSource, KeyContext, Length, MouseButton, MouseDownEvent, MouseEvent,
  38    MouseMoveEvent, MouseUpEvent, Point, ScrollHandle, Stateful, StrikethroughStyle,
  39    StyleRefinement, StyledText, Task, TextLayout, TextRun, TextStyle, TextStyleRefinement,
  40    actions, img, point, quad,
  41};
  42use language::{CharClassifier, Language, LanguageRegistry, Rope};
  43use parser::CodeBlockMetadata;
  44use parser::{
  45    MarkdownEvent, MarkdownTag, MarkdownTagEnd, parse_links_only, parse_markdown_with_options,
  46};
  47use pulldown_cmark::Alignment;
  48use sum_tree::TreeMap;
  49use theme::SyntaxTheme;
  50use ui::{ScrollAxes, Scrollbars, WithScrollbar, prelude::*};
  51use util::ResultExt;
  52
  53use crate::parser::CodeBlockKind;
  54
  55/// A callback function that can be used to customize the style of links based on the destination URL.
  56/// If the callback returns `None`, the default link style will be used.
  57type LinkStyleCallback = Rc<dyn Fn(&str, &App) -> Option<TextStyleRefinement>>;
  58type SourceClickCallback = Box<dyn Fn(usize, usize, &mut Window, &mut App) -> bool>;
  59type CheckboxToggleCallback = Rc<dyn Fn(Range<usize>, bool, &mut Window, &mut App)>;
  60/// Defines custom style refinements for each heading level (H1-H6)
  61#[derive(Clone, Default)]
  62pub struct HeadingLevelStyles {
  63    pub h1: Option<TextStyleRefinement>,
  64    pub h2: Option<TextStyleRefinement>,
  65    pub h3: Option<TextStyleRefinement>,
  66    pub h4: Option<TextStyleRefinement>,
  67    pub h5: Option<TextStyleRefinement>,
  68    pub h6: Option<TextStyleRefinement>,
  69}
  70
  71#[derive(Clone)]
  72pub struct MarkdownStyle {
  73    pub base_text_style: TextStyle,
  74    pub container_style: StyleRefinement,
  75    pub code_block: StyleRefinement,
  76    pub code_block_overflow_x_scroll: bool,
  77    pub inline_code: TextStyleRefinement,
  78    pub block_quote: TextStyleRefinement,
  79    pub link: TextStyleRefinement,
  80    pub link_callback: Option<LinkStyleCallback>,
  81    pub rule_color: Hsla,
  82    pub block_quote_border_color: Hsla,
  83    pub syntax: Arc<SyntaxTheme>,
  84    pub selection_background_color: Hsla,
  85    pub heading: StyleRefinement,
  86    pub heading_level_styles: Option<HeadingLevelStyles>,
  87    pub height_is_multiple_of_line_height: bool,
  88    pub prevent_mouse_interaction: bool,
  89    pub table_columns_min_size: bool,
  90}
  91
  92impl Default for MarkdownStyle {
  93    fn default() -> Self {
  94        Self {
  95            base_text_style: Default::default(),
  96            container_style: Default::default(),
  97            code_block: Default::default(),
  98            code_block_overflow_x_scroll: false,
  99            inline_code: Default::default(),
 100            block_quote: Default::default(),
 101            link: Default::default(),
 102            link_callback: None,
 103            rule_color: Default::default(),
 104            block_quote_border_color: Default::default(),
 105            syntax: Arc::new(SyntaxTheme::default()),
 106            selection_background_color: Default::default(),
 107            heading: Default::default(),
 108            heading_level_styles: None,
 109            height_is_multiple_of_line_height: false,
 110            prevent_mouse_interaction: false,
 111            table_columns_min_size: false,
 112        }
 113    }
 114}
 115
 116pub enum MarkdownFont {
 117    Agent,
 118    Editor,
 119}
 120
 121impl MarkdownStyle {
 122    pub fn themed(font: MarkdownFont, window: &Window, cx: &App) -> Self {
 123        let theme_settings = ThemeSettings::get_global(cx);
 124        let colors = cx.theme().colors();
 125
 126        let buffer_font_weight = theme_settings.buffer_font.weight;
 127        let (buffer_font_size, ui_font_size) = match font {
 128            MarkdownFont::Agent => (
 129                theme_settings.agent_buffer_font_size(cx),
 130                theme_settings.agent_ui_font_size(cx),
 131            ),
 132            MarkdownFont::Editor => (
 133                theme_settings.buffer_font_size(cx),
 134                theme_settings.ui_font_size(cx),
 135            ),
 136        };
 137
 138        let text_color = colors.text;
 139
 140        let mut text_style = window.text_style();
 141        let line_height = buffer_font_size * 1.75;
 142
 143        text_style.refine(&TextStyleRefinement {
 144            font_family: Some(theme_settings.ui_font.family.clone()),
 145            font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
 146            font_features: Some(theme_settings.ui_font.features.clone()),
 147            font_size: Some(ui_font_size.into()),
 148            line_height: Some(line_height.into()),
 149            color: Some(text_color),
 150            ..Default::default()
 151        });
 152
 153        MarkdownStyle {
 154            base_text_style: text_style.clone(),
 155            syntax: cx.theme().syntax().clone(),
 156            selection_background_color: colors.element_selection_background,
 157            code_block_overflow_x_scroll: true,
 158            heading_level_styles: Some(HeadingLevelStyles {
 159                h1: Some(TextStyleRefinement {
 160                    font_size: Some(rems(1.15).into()),
 161                    ..Default::default()
 162                }),
 163                h2: Some(TextStyleRefinement {
 164                    font_size: Some(rems(1.1).into()),
 165                    ..Default::default()
 166                }),
 167                h3: Some(TextStyleRefinement {
 168                    font_size: Some(rems(1.05).into()),
 169                    ..Default::default()
 170                }),
 171                h4: Some(TextStyleRefinement {
 172                    font_size: Some(rems(1.).into()),
 173                    ..Default::default()
 174                }),
 175                h5: Some(TextStyleRefinement {
 176                    font_size: Some(rems(0.95).into()),
 177                    ..Default::default()
 178                }),
 179                h6: Some(TextStyleRefinement {
 180                    font_size: Some(rems(0.875).into()),
 181                    ..Default::default()
 182                }),
 183            }),
 184            code_block: StyleRefinement {
 185                padding: EdgesRefinement {
 186                    top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
 187                    left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
 188                    right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
 189                    bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
 190                },
 191                margin: EdgesRefinement {
 192                    top: Some(Length::Definite(px(8.).into())),
 193                    left: Some(Length::Definite(px(0.).into())),
 194                    right: Some(Length::Definite(px(0.).into())),
 195                    bottom: Some(Length::Definite(px(12.).into())),
 196                },
 197                border_style: Some(BorderStyle::Solid),
 198                border_widths: EdgesRefinement {
 199                    top: Some(AbsoluteLength::Pixels(px(1.))),
 200                    left: Some(AbsoluteLength::Pixels(px(1.))),
 201                    right: Some(AbsoluteLength::Pixels(px(1.))),
 202                    bottom: Some(AbsoluteLength::Pixels(px(1.))),
 203                },
 204                border_color: Some(colors.border_variant),
 205                background: Some(colors.editor_background.into()),
 206                text: TextStyleRefinement {
 207                    font_family: Some(theme_settings.buffer_font.family.clone()),
 208                    font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
 209                    font_features: Some(theme_settings.buffer_font.features.clone()),
 210                    font_size: Some(buffer_font_size.into()),
 211                    font_weight: Some(buffer_font_weight),
 212                    ..Default::default()
 213                },
 214                ..Default::default()
 215            },
 216            inline_code: TextStyleRefinement {
 217                font_family: Some(theme_settings.buffer_font.family.clone()),
 218                font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
 219                font_features: Some(theme_settings.buffer_font.features.clone()),
 220                font_size: Some(buffer_font_size.into()),
 221                font_weight: Some(buffer_font_weight),
 222                background_color: Some(colors.editor_foreground.opacity(0.08)),
 223                ..Default::default()
 224            },
 225            link: TextStyleRefinement {
 226                background_color: Some(colors.editor_foreground.opacity(0.025)),
 227                color: Some(colors.text_accent),
 228                underline: Some(UnderlineStyle {
 229                    color: Some(colors.text_accent.opacity(0.5)),
 230                    thickness: px(1.),
 231                    ..Default::default()
 232                }),
 233                ..Default::default()
 234            },
 235            ..Default::default()
 236        }
 237    }
 238
 239    pub fn with_muted_text(mut self, cx: &App) -> Self {
 240        let colors = cx.theme().colors();
 241        self.base_text_style.color = colors.text_muted;
 242        self
 243    }
 244}
 245
 246pub struct Markdown {
 247    source: SharedString,
 248    selection: Selection,
 249    pressed_link: Option<RenderedLink>,
 250    autoscroll_request: Option<usize>,
 251    active_root_block: Option<usize>,
 252    parsed_markdown: ParsedMarkdown,
 253    images_by_source_offset: HashMap<usize, Arc<Image>>,
 254    should_reparse: bool,
 255    pending_parse: Option<Task<()>>,
 256    focus_handle: FocusHandle,
 257    language_registry: Option<Arc<LanguageRegistry>>,
 258    fallback_code_block_language: Option<LanguageName>,
 259    options: MarkdownOptions,
 260    mermaid_state: MermaidState,
 261    copied_code_blocks: HashSet<ElementId>,
 262    code_block_scroll_handles: BTreeMap<usize, ScrollHandle>,
 263    context_menu_selected_text: Option<String>,
 264}
 265
 266#[derive(Clone, Copy, Default)]
 267pub struct MarkdownOptions {
 268    pub parse_links_only: bool,
 269    pub parse_html: bool,
 270    pub render_mermaid_diagrams: bool,
 271}
 272
 273#[derive(Clone, Copy, PartialEq, Eq)]
 274pub enum CopyButtonVisibility {
 275    Hidden,
 276    AlwaysVisible,
 277    VisibleOnHover,
 278}
 279
 280pub enum CodeBlockRenderer {
 281    Default {
 282        copy_button_visibility: CopyButtonVisibility,
 283        border: bool,
 284    },
 285    Custom {
 286        render: CodeBlockRenderFn,
 287        /// A function that can modify the parent container after the code block
 288        /// content has been appended as a child element.
 289        transform: Option<CodeBlockTransformFn>,
 290    },
 291}
 292
 293pub type CodeBlockRenderFn = Arc<
 294    dyn Fn(
 295        &CodeBlockKind,
 296        &ParsedMarkdown,
 297        Range<usize>,
 298        CodeBlockMetadata,
 299        &mut Window,
 300        &App,
 301    ) -> Div,
 302>;
 303
 304pub type CodeBlockTransformFn =
 305    Arc<dyn Fn(AnyDiv, Range<usize>, CodeBlockMetadata, &mut Window, &App) -> AnyDiv>;
 306
 307actions!(
 308    markdown,
 309    [
 310        /// Copies the selected text to the clipboard.
 311        Copy,
 312        /// Copies the selected text as markdown to the clipboard.
 313        CopyAsMarkdown
 314    ]
 315);
 316
 317enum EscapeAction {
 318    PassThrough,
 319    Nbsp(usize),
 320    DoubleNewline,
 321    PrefixBackslash,
 322}
 323
 324impl EscapeAction {
 325    fn output_len(&self) -> usize {
 326        match self {
 327            Self::PassThrough => 1,
 328            Self::Nbsp(count) => count * '\u{00A0}'.len_utf8(),
 329            Self::DoubleNewline => 2,
 330            Self::PrefixBackslash => 2,
 331        }
 332    }
 333
 334    fn write_to(&self, c: char, output: &mut String) {
 335        match self {
 336            Self::PassThrough => output.push(c),
 337            Self::Nbsp(count) => {
 338                for _ in 0..*count {
 339                    output.push('\u{00A0}');
 340                }
 341            }
 342            Self::DoubleNewline => {
 343                output.push('\n');
 344                output.push('\n');
 345            }
 346            Self::PrefixBackslash => {
 347                // '\\' is a single backslash in Rust, e.g. '|' -> '\|'
 348                output.push('\\');
 349                output.push(c);
 350            }
 351        }
 352    }
 353}
 354
 355// Valid to operate on raw bytes since multi-byte UTF-8
 356// sequences never contain ASCII-range bytes.
 357struct MarkdownEscaper {
 358    in_leading_whitespace: bool,
 359}
 360
 361impl MarkdownEscaper {
 362    const TAB_SIZE: usize = 4;
 363
 364    fn new() -> Self {
 365        Self {
 366            in_leading_whitespace: true,
 367        }
 368    }
 369
 370    fn next(&mut self, byte: u8) -> EscapeAction {
 371        let action = if self.in_leading_whitespace && byte == b'\t' {
 372            EscapeAction::Nbsp(Self::TAB_SIZE)
 373        } else if self.in_leading_whitespace && byte == b' ' {
 374            EscapeAction::Nbsp(1)
 375        } else if byte == b'\n' {
 376            EscapeAction::DoubleNewline
 377        } else if byte.is_ascii_punctuation() {
 378            EscapeAction::PrefixBackslash
 379        } else {
 380            EscapeAction::PassThrough
 381        };
 382
 383        self.in_leading_whitespace =
 384            byte == b'\n' || (self.in_leading_whitespace && (byte == b' ' || byte == b'\t'));
 385        action
 386    }
 387}
 388
 389impl Markdown {
 390    pub fn new(
 391        source: SharedString,
 392        language_registry: Option<Arc<LanguageRegistry>>,
 393        fallback_code_block_language: Option<LanguageName>,
 394        cx: &mut Context<Self>,
 395    ) -> Self {
 396        Self::new_with_options(
 397            source,
 398            language_registry,
 399            fallback_code_block_language,
 400            MarkdownOptions::default(),
 401            cx,
 402        )
 403    }
 404
 405    pub fn new_with_options(
 406        source: SharedString,
 407        language_registry: Option<Arc<LanguageRegistry>>,
 408        fallback_code_block_language: Option<LanguageName>,
 409        options: MarkdownOptions,
 410        cx: &mut Context<Self>,
 411    ) -> Self {
 412        let focus_handle = cx.focus_handle();
 413        let mut this = Self {
 414            source,
 415            selection: Selection::default(),
 416            pressed_link: None,
 417            autoscroll_request: None,
 418            active_root_block: None,
 419            should_reparse: false,
 420            images_by_source_offset: Default::default(),
 421            parsed_markdown: ParsedMarkdown::default(),
 422            pending_parse: None,
 423            focus_handle,
 424            language_registry,
 425            fallback_code_block_language,
 426            options,
 427            mermaid_state: MermaidState::default(),
 428            copied_code_blocks: HashSet::default(),
 429            code_block_scroll_handles: BTreeMap::default(),
 430            context_menu_selected_text: None,
 431        };
 432        this.parse(cx);
 433        this
 434    }
 435
 436    pub fn new_text(source: SharedString, cx: &mut Context<Self>) -> Self {
 437        Self::new_with_options(
 438            source,
 439            None,
 440            None,
 441            MarkdownOptions {
 442                parse_links_only: true,
 443                ..Default::default()
 444            },
 445            cx,
 446        )
 447    }
 448
 449    fn code_block_scroll_handle(&mut self, id: usize) -> ScrollHandle {
 450        self.code_block_scroll_handles
 451            .entry(id)
 452            .or_insert_with(ScrollHandle::new)
 453            .clone()
 454    }
 455
 456    fn retain_code_block_scroll_handles(&mut self, ids: &HashSet<usize>) {
 457        self.code_block_scroll_handles
 458            .retain(|id, _| ids.contains(id));
 459    }
 460
 461    fn clear_code_block_scroll_handles(&mut self) {
 462        self.code_block_scroll_handles.clear();
 463    }
 464
 465    fn autoscroll_code_block(&self, source_index: usize, cursor_position: Point<Pixels>) {
 466        let Some((_, scroll_handle)) = self
 467            .code_block_scroll_handles
 468            .range(..=source_index)
 469            .next_back()
 470        else {
 471            return;
 472        };
 473
 474        let bounds = scroll_handle.bounds();
 475        if cursor_position.y < bounds.top() || cursor_position.y > bounds.bottom() {
 476            return;
 477        }
 478
 479        let horizontal_delta = if cursor_position.x < bounds.left() {
 480            bounds.left() - cursor_position.x
 481        } else if cursor_position.x > bounds.right() {
 482            bounds.right() - cursor_position.x
 483        } else {
 484            return;
 485        };
 486
 487        let offset = scroll_handle.offset();
 488        scroll_handle.set_offset(point(offset.x + horizontal_delta, offset.y));
 489    }
 490
 491    pub fn is_parsing(&self) -> bool {
 492        self.pending_parse.is_some()
 493    }
 494
 495    pub fn source(&self) -> &str {
 496        &self.source
 497    }
 498
 499    pub fn append(&mut self, text: &str, cx: &mut Context<Self>) {
 500        self.source = SharedString::new(self.source.to_string() + text);
 501        self.parse(cx);
 502    }
 503
 504    pub fn replace(&mut self, source: impl Into<SharedString>, cx: &mut Context<Self>) {
 505        self.source = source.into();
 506        self.parse(cx);
 507    }
 508
 509    pub fn request_autoscroll_to_source_index(
 510        &mut self,
 511        source_index: usize,
 512        cx: &mut Context<Self>,
 513    ) {
 514        self.autoscroll_request = Some(source_index);
 515        cx.refresh_windows();
 516    }
 517
 518    pub fn set_active_root_for_source_index(
 519        &mut self,
 520        source_index: Option<usize>,
 521        cx: &mut Context<Self>,
 522    ) {
 523        let active_root_block =
 524            source_index.and_then(|index| self.parsed_markdown.root_block_for_source_index(index));
 525        if self.active_root_block == active_root_block {
 526            return;
 527        }
 528
 529        self.active_root_block = active_root_block;
 530        cx.notify();
 531    }
 532
 533    pub fn reset(&mut self, source: SharedString, cx: &mut Context<Self>) {
 534        if source == self.source() {
 535            return;
 536        }
 537        self.source = source;
 538        self.selection = Selection::default();
 539        self.autoscroll_request = None;
 540        self.pending_parse = None;
 541        self.should_reparse = false;
 542        // Don't clear parsed_markdown here - keep existing content visible until new parse completes
 543        self.parse(cx);
 544    }
 545
 546    #[cfg(any(test, feature = "test-support"))]
 547    pub fn parsed_markdown(&self) -> &ParsedMarkdown {
 548        &self.parsed_markdown
 549    }
 550
 551    pub fn escape(s: &str) -> Cow<'_, str> {
 552        let output_len: usize = {
 553            let mut escaper = MarkdownEscaper::new();
 554            s.bytes().map(|byte| escaper.next(byte).output_len()).sum()
 555        };
 556
 557        if output_len == s.len() {
 558            return s.into();
 559        }
 560
 561        let mut escaper = MarkdownEscaper::new();
 562        let mut output = String::with_capacity(output_len);
 563        for c in s.chars() {
 564            escaper.next(c as u8).write_to(c, &mut output);
 565        }
 566        output.into()
 567    }
 568
 569    pub fn selected_text(&self) -> Option<String> {
 570        if self.selection.end <= self.selection.start {
 571            None
 572        } else {
 573            Some(self.source[self.selection.start..self.selection.end].to_string())
 574        }
 575    }
 576
 577    fn copy(&self, text: &RenderedText, _: &mut Window, cx: &mut Context<Self>) {
 578        if self.selection.end <= self.selection.start {
 579            return;
 580        }
 581        let text = text.text_for_range(self.selection.start..self.selection.end);
 582        cx.write_to_clipboard(ClipboardItem::new_string(text));
 583    }
 584
 585    fn copy_as_markdown(&mut self, _: &mut Window, cx: &mut Context<Self>) {
 586        if let Some(text) = self.context_menu_selected_text.take() {
 587            cx.write_to_clipboard(ClipboardItem::new_string(text));
 588            return;
 589        }
 590        if self.selection.end <= self.selection.start {
 591            return;
 592        }
 593        let text = self.source[self.selection.start..self.selection.end].to_string();
 594        cx.write_to_clipboard(ClipboardItem::new_string(text));
 595    }
 596
 597    fn capture_selection_for_context_menu(&mut self) {
 598        self.context_menu_selected_text = self.selected_text();
 599    }
 600
 601    fn parse(&mut self, cx: &mut Context<Self>) {
 602        if self.source.is_empty() {
 603            self.should_reparse = false;
 604            self.pending_parse.take();
 605            self.parsed_markdown = ParsedMarkdown {
 606                source: self.source.clone(),
 607                ..Default::default()
 608            };
 609            self.active_root_block = None;
 610            self.images_by_source_offset.clear();
 611            self.mermaid_state.clear();
 612            cx.notify();
 613            cx.refresh_windows();
 614            return;
 615        }
 616
 617        if self.pending_parse.is_some() {
 618            self.should_reparse = true;
 619            return;
 620        }
 621        self.should_reparse = false;
 622        self.pending_parse = Some(self.start_background_parse(cx));
 623    }
 624
 625    fn start_background_parse(&self, cx: &Context<Self>) -> Task<()> {
 626        let source = self.source.clone();
 627        let should_parse_links_only = self.options.parse_links_only;
 628        let should_parse_html = self.options.parse_html;
 629        let should_render_mermaid_diagrams = self.options.render_mermaid_diagrams;
 630        let language_registry = self.language_registry.clone();
 631        let fallback = self.fallback_code_block_language.clone();
 632
 633        let parsed = cx.background_spawn(async move {
 634            if should_parse_links_only {
 635                return (
 636                    ParsedMarkdown {
 637                        events: Arc::from(parse_links_only(source.as_ref())),
 638                        source,
 639                        languages_by_name: TreeMap::default(),
 640                        languages_by_path: TreeMap::default(),
 641                        root_block_starts: Arc::default(),
 642                        html_blocks: BTreeMap::default(),
 643                        mermaid_diagrams: BTreeMap::default(),
 644                    },
 645                    Default::default(),
 646                );
 647            }
 648
 649            let parsed = parse_markdown_with_options(&source, should_parse_html);
 650            let events = parsed.events;
 651            let language_names = parsed.language_names;
 652            let paths = parsed.language_paths;
 653            let root_block_starts = parsed.root_block_starts;
 654            let html_blocks = parsed.html_blocks;
 655            let mermaid_diagrams = if should_render_mermaid_diagrams {
 656                extract_mermaid_diagrams(&source, &events)
 657            } else {
 658                BTreeMap::default()
 659            };
 660            let mut images_by_source_offset = HashMap::default();
 661            let mut languages_by_name = TreeMap::default();
 662            let mut languages_by_path = TreeMap::default();
 663            if let Some(registry) = language_registry.as_ref() {
 664                for name in language_names {
 665                    let language = if !name.is_empty() {
 666                        registry.language_for_name_or_extension(&name).left_future()
 667                    } else if let Some(fallback) = &fallback {
 668                        registry.language_for_name(fallback.as_ref()).right_future()
 669                    } else {
 670                        continue;
 671                    };
 672                    if let Ok(language) = language.await {
 673                        languages_by_name.insert(name, language);
 674                    }
 675                }
 676
 677                for path in paths {
 678                    if let Ok(language) = registry
 679                        .load_language_for_file_path(Path::new(path.as_ref()))
 680                        .await
 681                    {
 682                        languages_by_path.insert(path, language);
 683                    }
 684                }
 685            }
 686
 687            for (range, event) in &events {
 688                if let MarkdownEvent::Start(MarkdownTag::Image { dest_url, .. }) = event
 689                    && let Some(data_url) = dest_url.strip_prefix("data:")
 690                {
 691                    let Some((mime_info, data)) = data_url.split_once(',') else {
 692                        continue;
 693                    };
 694                    let Some((mime_type, encoding)) = mime_info.split_once(';') else {
 695                        continue;
 696                    };
 697                    let Some(format) = ImageFormat::from_mime_type(mime_type) else {
 698                        continue;
 699                    };
 700                    let is_base64 = encoding == "base64";
 701                    if is_base64
 702                        && let Some(bytes) = base64::prelude::BASE64_STANDARD
 703                            .decode(data)
 704                            .log_with_level(Level::Debug)
 705                    {
 706                        let image = Arc::new(Image::from_bytes(format, bytes));
 707                        images_by_source_offset.insert(range.start, image);
 708                    }
 709                }
 710            }
 711
 712            (
 713                ParsedMarkdown {
 714                    source,
 715                    events: Arc::from(events),
 716                    languages_by_name,
 717                    languages_by_path,
 718                    root_block_starts: Arc::from(root_block_starts),
 719                    html_blocks,
 720                    mermaid_diagrams,
 721                },
 722                images_by_source_offset,
 723            )
 724        });
 725
 726        cx.spawn(async move |this, cx| {
 727            let (parsed, images_by_source_offset) = parsed.await;
 728
 729            this.update(cx, |this, cx| {
 730                this.parsed_markdown = parsed;
 731                this.images_by_source_offset = images_by_source_offset;
 732                if this.active_root_block.is_some_and(|block_index| {
 733                    block_index >= this.parsed_markdown.root_block_starts.len()
 734                }) {
 735                    this.active_root_block = None;
 736                }
 737                if this.options.render_mermaid_diagrams {
 738                    let parsed_markdown = this.parsed_markdown.clone();
 739                    this.mermaid_state.update(&parsed_markdown, cx);
 740                } else {
 741                    this.mermaid_state.clear();
 742                }
 743                this.pending_parse.take();
 744                if this.should_reparse {
 745                    this.parse(cx);
 746                }
 747                cx.notify();
 748                cx.refresh_windows();
 749            })
 750            .ok();
 751        })
 752    }
 753}
 754
 755impl Focusable for Markdown {
 756    fn focus_handle(&self, _cx: &App) -> FocusHandle {
 757        self.focus_handle.clone()
 758    }
 759}
 760
 761#[derive(Debug, Default, Clone)]
 762enum SelectMode {
 763    #[default]
 764    Character,
 765    Word(Range<usize>),
 766    Line(Range<usize>),
 767    All,
 768}
 769
 770#[derive(Clone, Default)]
 771struct Selection {
 772    start: usize,
 773    end: usize,
 774    reversed: bool,
 775    pending: bool,
 776    mode: SelectMode,
 777}
 778
 779impl Selection {
 780    fn set_head(&mut self, head: usize, rendered_text: &RenderedText) {
 781        match &self.mode {
 782            SelectMode::Character => {
 783                if head < self.tail() {
 784                    if !self.reversed {
 785                        self.end = self.start;
 786                        self.reversed = true;
 787                    }
 788                    self.start = head;
 789                } else {
 790                    if self.reversed {
 791                        self.start = self.end;
 792                        self.reversed = false;
 793                    }
 794                    self.end = head;
 795                }
 796            }
 797            SelectMode::Word(original_range) | SelectMode::Line(original_range) => {
 798                let head_range = if matches!(self.mode, SelectMode::Word(_)) {
 799                    rendered_text.surrounding_word_range(head)
 800                } else {
 801                    rendered_text.surrounding_line_range(head)
 802                };
 803
 804                if head < original_range.start {
 805                    self.start = head_range.start;
 806                    self.end = original_range.end;
 807                    self.reversed = true;
 808                } else if head >= original_range.end {
 809                    self.start = original_range.start;
 810                    self.end = head_range.end;
 811                    self.reversed = false;
 812                } else {
 813                    self.start = original_range.start;
 814                    self.end = original_range.end;
 815                    self.reversed = false;
 816                }
 817            }
 818            SelectMode::All => {
 819                self.start = 0;
 820                self.end = rendered_text
 821                    .lines
 822                    .last()
 823                    .map(|line| line.source_end)
 824                    .unwrap_or(0);
 825                self.reversed = false;
 826            }
 827        }
 828    }
 829
 830    fn tail(&self) -> usize {
 831        if self.reversed { self.end } else { self.start }
 832    }
 833}
 834
 835#[derive(Debug, Clone, Default)]
 836pub struct ParsedMarkdown {
 837    pub source: SharedString,
 838    pub events: Arc<[(Range<usize>, MarkdownEvent)]>,
 839    pub languages_by_name: TreeMap<SharedString, Arc<Language>>,
 840    pub languages_by_path: TreeMap<Arc<str>, Arc<Language>>,
 841    pub root_block_starts: Arc<[usize]>,
 842    pub(crate) html_blocks: BTreeMap<usize, html::html_parser::ParsedHtmlBlock>,
 843    pub(crate) mermaid_diagrams: BTreeMap<usize, ParsedMarkdownMermaidDiagram>,
 844}
 845
 846impl ParsedMarkdown {
 847    pub fn source(&self) -> &SharedString {
 848        &self.source
 849    }
 850
 851    pub fn events(&self) -> &Arc<[(Range<usize>, MarkdownEvent)]> {
 852        &self.events
 853    }
 854
 855    pub fn root_block_starts(&self) -> &Arc<[usize]> {
 856        &self.root_block_starts
 857    }
 858
 859    pub fn root_block_for_source_index(&self, source_index: usize) -> Option<usize> {
 860        if self.root_block_starts.is_empty() {
 861            return None;
 862        }
 863
 864        let partition = self
 865            .root_block_starts
 866            .partition_point(|block_start| *block_start <= source_index);
 867
 868        Some(partition.saturating_sub(1))
 869    }
 870}
 871
 872pub enum AutoscrollBehavior {
 873    /// Propagate the request up the element tree for the nearest
 874    /// scrollable ancestor (e.g. `List`) to handle.
 875    Propagate,
 876    /// Directly control a specific scroll handle.
 877    Controlled(ScrollHandle),
 878}
 879
 880pub struct MarkdownElement {
 881    markdown: Entity<Markdown>,
 882    style: MarkdownStyle,
 883    code_block_renderer: CodeBlockRenderer,
 884    on_url_click: Option<Box<dyn Fn(SharedString, &mut Window, &mut App)>>,
 885    on_source_click: Option<SourceClickCallback>,
 886    on_checkbox_toggle: Option<CheckboxToggleCallback>,
 887    image_resolver: Option<Box<dyn Fn(&str) -> Option<ImageSource>>>,
 888    show_root_block_markers: bool,
 889    autoscroll: AutoscrollBehavior,
 890}
 891
 892impl MarkdownElement {
 893    pub fn new(markdown: Entity<Markdown>, style: MarkdownStyle) -> Self {
 894        Self {
 895            markdown,
 896            style,
 897            code_block_renderer: CodeBlockRenderer::Default {
 898                copy_button_visibility: CopyButtonVisibility::VisibleOnHover,
 899                border: false,
 900            },
 901            on_url_click: None,
 902            on_source_click: None,
 903            on_checkbox_toggle: None,
 904            image_resolver: None,
 905            show_root_block_markers: false,
 906            autoscroll: AutoscrollBehavior::Propagate,
 907        }
 908    }
 909
 910    #[cfg(any(test, feature = "test-support"))]
 911    pub fn rendered_text(
 912        markdown: Entity<Markdown>,
 913        cx: &mut gpui::VisualTestContext,
 914        style: impl FnOnce(&Window, &App) -> MarkdownStyle,
 915    ) -> String {
 916        use gpui::size;
 917
 918        let (text, _) = cx.draw(
 919            Default::default(),
 920            size(px(600.0), px(600.0)),
 921            |window, cx| Self::new(markdown, style(window, cx)),
 922        );
 923        text.text
 924            .lines
 925            .iter()
 926            .map(|line| line.layout.wrapped_text())
 927            .collect::<Vec<_>>()
 928            .join("\n")
 929    }
 930
 931    pub fn code_block_renderer(mut self, variant: CodeBlockRenderer) -> Self {
 932        self.code_block_renderer = variant;
 933        self
 934    }
 935
 936    pub fn on_url_click(
 937        mut self,
 938        handler: impl Fn(SharedString, &mut Window, &mut App) + 'static,
 939    ) -> Self {
 940        self.on_url_click = Some(Box::new(handler));
 941        self
 942    }
 943
 944    pub fn on_source_click(
 945        mut self,
 946        handler: impl Fn(usize, usize, &mut Window, &mut App) -> bool + 'static,
 947    ) -> Self {
 948        self.on_source_click = Some(Box::new(handler));
 949        self
 950    }
 951
 952    pub fn on_checkbox_toggle(
 953        mut self,
 954        handler: impl Fn(Range<usize>, bool, &mut Window, &mut App) + 'static,
 955    ) -> Self {
 956        self.on_checkbox_toggle = Some(Rc::new(handler));
 957        self
 958    }
 959
 960    pub fn image_resolver(
 961        mut self,
 962        resolver: impl Fn(&str) -> Option<ImageSource> + 'static,
 963    ) -> Self {
 964        self.image_resolver = Some(Box::new(resolver));
 965        self
 966    }
 967
 968    pub fn show_root_block_markers(mut self) -> Self {
 969        self.show_root_block_markers = true;
 970        self
 971    }
 972
 973    pub fn scroll_handle(mut self, scroll_handle: ScrollHandle) -> Self {
 974        self.autoscroll = AutoscrollBehavior::Controlled(scroll_handle);
 975        self
 976    }
 977
 978    fn push_markdown_image(
 979        &self,
 980        builder: &mut MarkdownElementBuilder,
 981        range: &Range<usize>,
 982        source: ImageSource,
 983        width: Option<DefiniteLength>,
 984        height: Option<DefiniteLength>,
 985    ) {
 986        builder.modify_current_div(|el| {
 987            el.items_center().flex().flex_row().child(
 988                img(source)
 989                    .max_w_full()
 990                    .when_some(height, |this, height| this.h(height))
 991                    .when_some(width, |this, width| this.w(width)),
 992            )
 993        });
 994        let _ = range;
 995    }
 996
 997    fn push_markdown_paragraph(
 998        &self,
 999        builder: &mut MarkdownElementBuilder,
1000        range: &Range<usize>,
1001        markdown_end: usize,
1002    ) {
1003        builder.push_div(
1004            div().when(!self.style.height_is_multiple_of_line_height, |el| {
1005                el.mb_2().line_height(rems(1.3))
1006            }),
1007            range,
1008            markdown_end,
1009        );
1010    }
1011
1012    fn push_markdown_heading(
1013        &self,
1014        builder: &mut MarkdownElementBuilder,
1015        level: pulldown_cmark::HeadingLevel,
1016        range: &Range<usize>,
1017        markdown_end: usize,
1018    ) {
1019        let mut heading = div().mb_2();
1020        heading = apply_heading_style(heading, level, self.style.heading_level_styles.as_ref());
1021
1022        let mut heading_style = self.style.heading.clone();
1023        let heading_text_style = heading_style.text_style().clone();
1024        heading.style().refine(&heading_style);
1025
1026        builder.push_text_style(heading_text_style);
1027        builder.push_div(heading, range, markdown_end);
1028    }
1029
1030    fn pop_markdown_heading(&self, builder: &mut MarkdownElementBuilder) {
1031        builder.pop_div();
1032        builder.pop_text_style();
1033    }
1034
1035    fn push_markdown_block_quote(
1036        &self,
1037        builder: &mut MarkdownElementBuilder,
1038        range: &Range<usize>,
1039        markdown_end: usize,
1040    ) {
1041        builder.push_text_style(self.style.block_quote.clone());
1042        builder.push_div(
1043            div()
1044                .pl_4()
1045                .mb_2()
1046                .border_l_4()
1047                .border_color(self.style.block_quote_border_color),
1048            range,
1049            markdown_end,
1050        );
1051    }
1052
1053    fn pop_markdown_block_quote(&self, builder: &mut MarkdownElementBuilder) {
1054        builder.pop_div();
1055        builder.pop_text_style();
1056    }
1057
1058    fn push_markdown_list_item(
1059        &self,
1060        builder: &mut MarkdownElementBuilder,
1061        bullet: AnyElement,
1062        range: &Range<usize>,
1063        markdown_end: usize,
1064    ) {
1065        builder.push_div(
1066            div()
1067                .when(!self.style.height_is_multiple_of_line_height, |el| {
1068                    el.mb_1().gap_1().line_height(rems(1.3))
1069                })
1070                .h_flex()
1071                .items_start()
1072                .child(bullet),
1073            range,
1074            markdown_end,
1075        );
1076        // Without `w_0`, text doesn't wrap to the width of the container.
1077        builder.push_div(div().flex_1().w_0(), range, markdown_end);
1078    }
1079
1080    fn pop_markdown_list_item(&self, builder: &mut MarkdownElementBuilder) {
1081        builder.pop_div();
1082        builder.pop_div();
1083    }
1084
1085    fn paint_selection(
1086        &self,
1087        bounds: Bounds<Pixels>,
1088        rendered_text: &RenderedText,
1089        window: &mut Window,
1090        cx: &mut App,
1091    ) {
1092        let selection = self.markdown.read(cx).selection.clone();
1093        let selection_start = rendered_text.position_for_source_index(selection.start);
1094        let selection_end = rendered_text.position_for_source_index(selection.end);
1095        if let Some(((start_position, start_line_height), (end_position, end_line_height))) =
1096            selection_start.zip(selection_end)
1097        {
1098            if start_position.y == end_position.y {
1099                window.paint_quad(quad(
1100                    Bounds::from_corners(
1101                        start_position,
1102                        point(end_position.x, end_position.y + end_line_height),
1103                    ),
1104                    Pixels::ZERO,
1105                    self.style.selection_background_color,
1106                    Edges::default(),
1107                    Hsla::transparent_black(),
1108                    BorderStyle::default(),
1109                ));
1110            } else {
1111                window.paint_quad(quad(
1112                    Bounds::from_corners(
1113                        start_position,
1114                        point(bounds.right(), start_position.y + start_line_height),
1115                    ),
1116                    Pixels::ZERO,
1117                    self.style.selection_background_color,
1118                    Edges::default(),
1119                    Hsla::transparent_black(),
1120                    BorderStyle::default(),
1121                ));
1122
1123                if end_position.y > start_position.y + start_line_height {
1124                    window.paint_quad(quad(
1125                        Bounds::from_corners(
1126                            point(bounds.left(), start_position.y + start_line_height),
1127                            point(bounds.right(), end_position.y),
1128                        ),
1129                        Pixels::ZERO,
1130                        self.style.selection_background_color,
1131                        Edges::default(),
1132                        Hsla::transparent_black(),
1133                        BorderStyle::default(),
1134                    ));
1135                }
1136
1137                window.paint_quad(quad(
1138                    Bounds::from_corners(
1139                        point(bounds.left(), end_position.y),
1140                        point(end_position.x, end_position.y + end_line_height),
1141                    ),
1142                    Pixels::ZERO,
1143                    self.style.selection_background_color,
1144                    Edges::default(),
1145                    Hsla::transparent_black(),
1146                    BorderStyle::default(),
1147                ));
1148            }
1149        }
1150    }
1151
1152    fn paint_mouse_listeners(
1153        &mut self,
1154        hitbox: &Hitbox,
1155        rendered_text: &RenderedText,
1156        window: &mut Window,
1157        cx: &mut App,
1158    ) {
1159        if self.style.prevent_mouse_interaction {
1160            return;
1161        }
1162
1163        let is_hovering_link = hitbox.is_hovered(window)
1164            && !self.markdown.read(cx).selection.pending
1165            && rendered_text
1166                .link_for_position(window.mouse_position())
1167                .is_some();
1168
1169        if !self.style.prevent_mouse_interaction {
1170            if is_hovering_link {
1171                window.set_cursor_style(CursorStyle::PointingHand, hitbox);
1172            } else {
1173                window.set_cursor_style(CursorStyle::IBeam, hitbox);
1174            }
1175        }
1176
1177        let on_open_url = self.on_url_click.take();
1178        let on_source_click = self.on_source_click.take();
1179
1180        self.on_mouse_event(window, cx, {
1181            let hitbox = hitbox.clone();
1182            move |markdown, event: &MouseDownEvent, phase, window, _| {
1183                if phase.capture()
1184                    && event.button == MouseButton::Right
1185                    && hitbox.is_hovered(window)
1186                {
1187                    // Capture selected text so it survives until menu item is clicked
1188                    markdown.capture_selection_for_context_menu();
1189                }
1190            }
1191        });
1192
1193        self.on_mouse_event(window, cx, {
1194            let rendered_text = rendered_text.clone();
1195            let hitbox = hitbox.clone();
1196            move |markdown, event: &MouseDownEvent, phase, window, cx| {
1197                if hitbox.is_hovered(window) {
1198                    if phase.bubble() {
1199                        if let Some(link) = rendered_text.link_for_position(event.position) {
1200                            markdown.pressed_link = Some(link.clone());
1201                        } else {
1202                            let source_index =
1203                                match rendered_text.source_index_for_position(event.position) {
1204                                    Ok(ix) | Err(ix) => ix,
1205                                };
1206                            if let Some(handler) = on_source_click.as_ref() {
1207                                let blocked = handler(source_index, event.click_count, window, cx);
1208                                if blocked {
1209                                    markdown.selection = Selection::default();
1210                                    markdown.pressed_link = None;
1211                                    window.prevent_default();
1212                                    cx.notify();
1213                                    return;
1214                                }
1215                            }
1216                            let (range, mode) = match event.click_count {
1217                                1 => {
1218                                    let range = source_index..source_index;
1219                                    (range, SelectMode::Character)
1220                                }
1221                                2 => {
1222                                    let range = rendered_text.surrounding_word_range(source_index);
1223                                    (range.clone(), SelectMode::Word(range))
1224                                }
1225                                3 => {
1226                                    let range = rendered_text.surrounding_line_range(source_index);
1227                                    (range.clone(), SelectMode::Line(range))
1228                                }
1229                                _ => {
1230                                    let range = 0..rendered_text
1231                                        .lines
1232                                        .last()
1233                                        .map(|line| line.source_end)
1234                                        .unwrap_or(0);
1235                                    (range, SelectMode::All)
1236                                }
1237                            };
1238                            markdown.selection = Selection {
1239                                start: range.start,
1240                                end: range.end,
1241                                reversed: false,
1242                                pending: true,
1243                                mode,
1244                            };
1245                            window.focus(&markdown.focus_handle, cx);
1246                        }
1247
1248                        window.prevent_default();
1249                        cx.notify();
1250                    }
1251                } else if phase.capture() && event.button == MouseButton::Left {
1252                    markdown.selection = Selection::default();
1253                    markdown.pressed_link = None;
1254                    cx.notify();
1255                }
1256            }
1257        });
1258        self.on_mouse_event(window, cx, {
1259            let rendered_text = rendered_text.clone();
1260            let hitbox = hitbox.clone();
1261            let was_hovering_link = is_hovering_link;
1262            move |markdown, event: &MouseMoveEvent, phase, window, cx| {
1263                if phase.capture() {
1264                    return;
1265                }
1266
1267                if markdown.selection.pending {
1268                    let source_index = match rendered_text.source_index_for_position(event.position)
1269                    {
1270                        Ok(ix) | Err(ix) => ix,
1271                    };
1272                    markdown.selection.set_head(source_index, &rendered_text);
1273                    markdown.autoscroll_code_block(source_index, event.position);
1274                    markdown.autoscroll_request = Some(source_index);
1275                    cx.notify();
1276                } else {
1277                    let is_hovering_link = hitbox.is_hovered(window)
1278                        && rendered_text.link_for_position(event.position).is_some();
1279                    if is_hovering_link != was_hovering_link {
1280                        cx.notify();
1281                    }
1282                }
1283            }
1284        });
1285        self.on_mouse_event(window, cx, {
1286            let rendered_text = rendered_text.clone();
1287            move |markdown, event: &MouseUpEvent, phase, window, cx| {
1288                if phase.bubble() {
1289                    if let Some(pressed_link) = markdown.pressed_link.take()
1290                        && Some(&pressed_link) == rendered_text.link_for_position(event.position)
1291                    {
1292                        if let Some(open_url) = on_open_url.as_ref() {
1293                            open_url(pressed_link.destination_url, window, cx);
1294                        } else {
1295                            cx.open_url(&pressed_link.destination_url);
1296                        }
1297                    }
1298                } else if markdown.selection.pending {
1299                    markdown.selection.pending = false;
1300                    #[cfg(any(target_os = "linux", target_os = "freebsd"))]
1301                    {
1302                        let text = rendered_text
1303                            .text_for_range(markdown.selection.start..markdown.selection.end);
1304                        cx.write_to_primary(ClipboardItem::new_string(text))
1305                    }
1306                    cx.notify();
1307                }
1308            }
1309        });
1310    }
1311
1312    fn autoscroll(
1313        &self,
1314        rendered_text: &RenderedText,
1315        window: &mut Window,
1316        cx: &mut App,
1317    ) -> Option<()> {
1318        let autoscroll_index = self
1319            .markdown
1320            .update(cx, |markdown, _| markdown.autoscroll_request.take())?;
1321        let (position, line_height) = rendered_text.position_for_source_index(autoscroll_index)?;
1322
1323        match &self.autoscroll {
1324            AutoscrollBehavior::Controlled(scroll_handle) => {
1325                let viewport = scroll_handle.bounds();
1326                let margin = line_height * 3.;
1327                let top_goal = viewport.top() + margin;
1328                let bottom_goal = viewport.bottom() - margin;
1329                let current_offset = scroll_handle.offset();
1330
1331                let new_offset_y = if position.y < top_goal {
1332                    current_offset.y + (top_goal - position.y)
1333                } else if position.y + line_height > bottom_goal {
1334                    current_offset.y + (bottom_goal - (position.y + line_height))
1335                } else {
1336                    current_offset.y
1337                };
1338
1339                scroll_handle.set_offset(point(
1340                    current_offset.x,
1341                    new_offset_y.clamp(-scroll_handle.max_offset().y, Pixels::ZERO),
1342                ));
1343            }
1344            AutoscrollBehavior::Propagate => {
1345                let text_style = self.style.base_text_style.clone();
1346                let font_id = window.text_system().resolve_font(&text_style.font());
1347                let font_size = text_style.font_size.to_pixels(window.rem_size());
1348                let em_width = window.text_system().em_width(font_id, font_size).unwrap();
1349                window.request_autoscroll(Bounds::from_corners(
1350                    point(position.x - 3. * em_width, position.y - 3. * line_height),
1351                    point(position.x + 3. * em_width, position.y + 3. * line_height),
1352                ));
1353            }
1354        }
1355        Some(())
1356    }
1357
1358    fn on_mouse_event<T: MouseEvent>(
1359        &self,
1360        window: &mut Window,
1361        _cx: &mut App,
1362        mut f: impl 'static
1363        + FnMut(&mut Markdown, &T, DispatchPhase, &mut Window, &mut Context<Markdown>),
1364    ) {
1365        window.on_mouse_event({
1366            let markdown = self.markdown.downgrade();
1367            move |event, phase, window, cx| {
1368                markdown
1369                    .update(cx, |markdown, cx| f(markdown, event, phase, window, cx))
1370                    .log_err();
1371            }
1372        });
1373    }
1374}
1375
1376impl Styled for MarkdownElement {
1377    fn style(&mut self) -> &mut StyleRefinement {
1378        &mut self.style.container_style
1379    }
1380}
1381
1382impl Element for MarkdownElement {
1383    type RequestLayoutState = RenderedMarkdown;
1384    type PrepaintState = Hitbox;
1385
1386    fn id(&self) -> Option<ElementId> {
1387        None
1388    }
1389
1390    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
1391        None
1392    }
1393
1394    fn request_layout(
1395        &mut self,
1396        _id: Option<&GlobalElementId>,
1397        _inspector_id: Option<&gpui::InspectorElementId>,
1398        window: &mut Window,
1399        cx: &mut App,
1400    ) -> (gpui::LayoutId, Self::RequestLayoutState) {
1401        let mut builder = MarkdownElementBuilder::new(
1402            &self.style.container_style,
1403            self.style.base_text_style.clone(),
1404            self.style.syntax.clone(),
1405        );
1406        let (parsed_markdown, images, active_root_block, render_mermaid_diagrams, mermaid_state) = {
1407            let markdown = self.markdown.read(cx);
1408            (
1409                markdown.parsed_markdown.clone(),
1410                markdown.images_by_source_offset.clone(),
1411                markdown.active_root_block,
1412                markdown.options.render_mermaid_diagrams,
1413                markdown.mermaid_state.clone(),
1414            )
1415        };
1416        let markdown_end = if let Some(last) = parsed_markdown.events.last() {
1417            last.0.end
1418        } else {
1419            0
1420        };
1421        let mut code_block_ids = HashSet::default();
1422
1423        let mut current_img_block_range: Option<Range<usize>> = None;
1424        let mut handled_html_block = false;
1425        let mut rendered_mermaid_block = false;
1426        for (index, (range, event)) in parsed_markdown.events.iter().enumerate() {
1427            // Skip alt text for images that rendered
1428            if let Some(current_img_block_range) = &current_img_block_range
1429                && current_img_block_range.end > range.end
1430            {
1431                continue;
1432            }
1433
1434            if handled_html_block {
1435                if let MarkdownEvent::End(MarkdownTagEnd::HtmlBlock) = event {
1436                    handled_html_block = false;
1437                } else {
1438                    continue;
1439                }
1440            }
1441
1442            if rendered_mermaid_block {
1443                if matches!(event, MarkdownEvent::End(MarkdownTagEnd::CodeBlock)) {
1444                    rendered_mermaid_block = false;
1445                }
1446                continue;
1447            }
1448
1449            match event {
1450                MarkdownEvent::RootStart => {
1451                    if self.show_root_block_markers {
1452                        builder.push_root_block(range, markdown_end);
1453                    }
1454                }
1455                MarkdownEvent::RootEnd(root_block_index) => {
1456                    if self.show_root_block_markers {
1457                        builder.pop_root_block(
1458                            active_root_block == Some(*root_block_index),
1459                            cx.theme().colors().border,
1460                            cx.theme().colors().border_variant,
1461                        );
1462                    }
1463                }
1464                MarkdownEvent::Start(tag) => {
1465                    match tag {
1466                        MarkdownTag::Image { dest_url, .. } => {
1467                            if let Some(image) = images.get(&range.start) {
1468                                current_img_block_range = Some(range.clone());
1469                                self.push_markdown_image(
1470                                    &mut builder,
1471                                    range,
1472                                    image.clone().into(),
1473                                    None,
1474                                    None,
1475                                );
1476                            } else if let Some(source) = self
1477                                .image_resolver
1478                                .as_ref()
1479                                .and_then(|resolve| resolve(dest_url.as_ref()))
1480                            {
1481                                current_img_block_range = Some(range.clone());
1482                                self.push_markdown_image(&mut builder, range, source, None, None);
1483                            }
1484                        }
1485                        MarkdownTag::Paragraph => {
1486                            self.push_markdown_paragraph(&mut builder, range, markdown_end);
1487                        }
1488                        MarkdownTag::Heading { level, .. } => {
1489                            self.push_markdown_heading(&mut builder, *level, range, markdown_end);
1490                        }
1491                        MarkdownTag::BlockQuote => {
1492                            self.push_markdown_block_quote(&mut builder, range, markdown_end);
1493                        }
1494                        MarkdownTag::CodeBlock { kind, .. } => {
1495                            if render_mermaid_diagrams
1496                                && let Some(mermaid_diagram) =
1497                                    parsed_markdown.mermaid_diagrams.get(&range.start)
1498                            {
1499                                builder.push_sourced_element(
1500                                    mermaid_diagram.content_range.clone(),
1501                                    render_mermaid_diagram(
1502                                        mermaid_diagram,
1503                                        &mermaid_state,
1504                                        &self.style,
1505                                    ),
1506                                );
1507                                rendered_mermaid_block = true;
1508                                continue;
1509                            }
1510
1511                            let language = match kind {
1512                                CodeBlockKind::Fenced => None,
1513                                CodeBlockKind::FencedLang(language) => {
1514                                    parsed_markdown.languages_by_name.get(language).cloned()
1515                                }
1516                                CodeBlockKind::FencedSrc(path_range) => parsed_markdown
1517                                    .languages_by_path
1518                                    .get(&path_range.path)
1519                                    .cloned(),
1520                                _ => None,
1521                            };
1522
1523                            let is_indented = matches!(kind, CodeBlockKind::Indented);
1524                            let scroll_handle = if self.style.code_block_overflow_x_scroll {
1525                                code_block_ids.insert(range.start);
1526                                Some(self.markdown.update(cx, |markdown, _| {
1527                                    markdown.code_block_scroll_handle(range.start)
1528                                }))
1529                            } else {
1530                                None
1531                            };
1532
1533                            match (&self.code_block_renderer, is_indented) {
1534                                (CodeBlockRenderer::Default { .. }, _) | (_, true) => {
1535                                    // This is a parent container that we can position the copy button inside.
1536                                    let parent_container =
1537                                        div().group("code_block").relative().w_full();
1538
1539                                    let mut parent_container: AnyDiv = if let Some(scroll_handle) =
1540                                        scroll_handle.as_ref()
1541                                    {
1542                                        let scrollbars = Scrollbars::new(ScrollAxes::Horizontal)
1543                                            .id(("markdown-code-block-scrollbar", range.start))
1544                                            .tracked_scroll_handle(scroll_handle)
1545                                            .with_track_along(
1546                                                ScrollAxes::Horizontal,
1547                                                cx.theme().colors().editor_background,
1548                                            )
1549                                            .notify_content();
1550
1551                                        parent_container
1552                                            .rounded_lg()
1553                                            .custom_scrollbars(scrollbars, window, cx)
1554                                            .into()
1555                                    } else {
1556                                        parent_container.into()
1557                                    };
1558
1559                                    if let CodeBlockRenderer::Default { border: true, .. } =
1560                                        &self.code_block_renderer
1561                                    {
1562                                        parent_container = parent_container
1563                                            .rounded_md()
1564                                            .border_1()
1565                                            .border_color(cx.theme().colors().border_variant);
1566                                    }
1567
1568                                    parent_container.style().refine(&self.style.code_block);
1569                                    builder.push_div(parent_container, range, markdown_end);
1570
1571                                    let code_block = div()
1572                                        .id(("code-block", range.start))
1573                                        .rounded_lg()
1574                                        .map(|mut code_block| {
1575                                            if let Some(scroll_handle) = scroll_handle.as_ref() {
1576                                                code_block.style().restrict_scroll_to_axis =
1577                                                    Some(true);
1578                                                code_block
1579                                                    .flex()
1580                                                    .overflow_x_scroll()
1581                                                    .track_scroll(scroll_handle)
1582                                            } else {
1583                                                code_block.w_full()
1584                                            }
1585                                        });
1586
1587                                    builder.push_text_style(self.style.code_block.text.to_owned());
1588                                    builder.push_code_block(language);
1589                                    builder.push_div(code_block, range, markdown_end);
1590                                }
1591                                (CodeBlockRenderer::Custom { .. }, _) => {}
1592                            }
1593                        }
1594                        MarkdownTag::HtmlBlock => {
1595                            builder.push_div(div(), range, markdown_end);
1596                            if let Some(block) = parsed_markdown.html_blocks.get(&range.start) {
1597                                self.render_html_block(block, &mut builder, markdown_end, cx);
1598                                handled_html_block = true;
1599                            }
1600                        }
1601                        MarkdownTag::List(bullet_index) => {
1602                            builder.push_list(*bullet_index);
1603                            builder.push_div(div().pl_2p5(), range, markdown_end);
1604                        }
1605                        MarkdownTag::Item => {
1606                            let bullet =
1607                                if let Some((task_range, MarkdownEvent::TaskListMarker(checked))) =
1608                                    parsed_markdown.events.get(index.saturating_add(1))
1609                                {
1610                                    let source = &parsed_markdown.source()[range.clone()];
1611                                    let checked = *checked;
1612                                    let toggle_state = if checked {
1613                                        ToggleState::Selected
1614                                    } else {
1615                                        ToggleState::Unselected
1616                                    };
1617
1618                                    let checkbox = Checkbox::new(
1619                                        ElementId::Name(source.to_string().into()),
1620                                        toggle_state,
1621                                    )
1622                                    .fill();
1623
1624                                    if let Some(on_toggle) = self.on_checkbox_toggle.clone() {
1625                                        let task_source_range = task_range.clone();
1626                                        checkbox
1627                                            .on_click(move |_state, window, cx| {
1628                                                on_toggle(
1629                                                    task_source_range.clone(),
1630                                                    !checked,
1631                                                    window,
1632                                                    cx,
1633                                                );
1634                                            })
1635                                            .into_any_element()
1636                                    } else {
1637                                        checkbox.visualization_only(true).into_any_element()
1638                                    }
1639                                } else if let Some(bullet_index) = builder.next_bullet_index() {
1640                                    div().child(format!("{}.", bullet_index)).into_any_element()
1641                                } else {
1642                                    div().child("").into_any_element()
1643                                };
1644                            self.push_markdown_list_item(&mut builder, bullet, range, markdown_end);
1645                        }
1646                        MarkdownTag::Emphasis => builder.push_text_style(TextStyleRefinement {
1647                            font_style: Some(FontStyle::Italic),
1648                            ..Default::default()
1649                        }),
1650                        MarkdownTag::Strong => builder.push_text_style(TextStyleRefinement {
1651                            font_weight: Some(FontWeight::BOLD),
1652                            ..Default::default()
1653                        }),
1654                        MarkdownTag::Strikethrough => {
1655                            builder.push_text_style(TextStyleRefinement {
1656                                strikethrough: Some(StrikethroughStyle {
1657                                    thickness: px(1.),
1658                                    color: None,
1659                                }),
1660                                ..Default::default()
1661                            })
1662                        }
1663                        MarkdownTag::Link { dest_url, .. } => {
1664                            if builder.code_block_stack.is_empty() {
1665                                builder.push_link(dest_url.clone(), range.clone());
1666                                let style = self
1667                                    .style
1668                                    .link_callback
1669                                    .as_ref()
1670                                    .and_then(|callback| callback(dest_url, cx))
1671                                    .unwrap_or_else(|| self.style.link.clone());
1672                                builder.push_text_style(style)
1673                            }
1674                        }
1675                        MarkdownTag::MetadataBlock(_) => {}
1676                        MarkdownTag::Table(alignments) => {
1677                            builder.table.start(alignments.clone());
1678
1679                            let column_count = alignments.len();
1680                            builder.push_div(
1681                                div()
1682                                    .id(("table", range.start))
1683                                    .grid()
1684                                    .grid_cols(column_count as u16)
1685                                    .when(self.style.table_columns_min_size, |this| {
1686                                        this.grid_cols_min_content(column_count as u16)
1687                                    })
1688                                    .when(!self.style.table_columns_min_size, |this| {
1689                                        this.grid_cols(column_count as u16)
1690                                    })
1691                                    .w_full()
1692                                    .mb_2()
1693                                    .border(px(1.5))
1694                                    .border_color(cx.theme().colors().border)
1695                                    .rounded_sm()
1696                                    .overflow_hidden(),
1697                                range,
1698                                markdown_end,
1699                            );
1700                        }
1701                        MarkdownTag::TableHead => {
1702                            builder.table.start_head();
1703                            builder.push_text_style(TextStyleRefinement {
1704                                font_weight: Some(FontWeight::SEMIBOLD),
1705                                ..Default::default()
1706                            });
1707                        }
1708                        MarkdownTag::TableRow => {
1709                            builder.table.start_row();
1710                        }
1711                        MarkdownTag::TableCell => {
1712                            let is_header = builder.table.in_head;
1713                            let row_index = builder.table.row_index;
1714                            let col_index = builder.table.col_index;
1715
1716                            builder.push_div(
1717                                div()
1718                                    .when(col_index > 0, |this| this.border_l_1())
1719                                    .when(row_index > 0, |this| this.border_t_1())
1720                                    .border_color(cx.theme().colors().border)
1721                                    .px_1()
1722                                    .py_0p5()
1723                                    .when(is_header, |this| {
1724                                        this.bg(cx.theme().colors().title_bar_background)
1725                                    })
1726                                    .when(!is_header && row_index % 2 == 1, |this| {
1727                                        this.bg(cx.theme().colors().panel_background)
1728                                    }),
1729                                range,
1730                                markdown_end,
1731                            );
1732                        }
1733                        _ => log::debug!("unsupported markdown tag {:?}", tag),
1734                    }
1735                }
1736                MarkdownEvent::End(tag) => match tag {
1737                    MarkdownTagEnd::Image => {
1738                        current_img_block_range.take();
1739                    }
1740                    MarkdownTagEnd::Paragraph => {
1741                        builder.pop_div();
1742                    }
1743                    MarkdownTagEnd::Heading(_) => {
1744                        self.pop_markdown_heading(&mut builder);
1745                    }
1746                    MarkdownTagEnd::BlockQuote(_kind) => {
1747                        self.pop_markdown_block_quote(&mut builder);
1748                    }
1749                    MarkdownTagEnd::CodeBlock => {
1750                        builder.trim_trailing_newline();
1751
1752                        builder.pop_div();
1753                        builder.pop_code_block();
1754                        builder.pop_text_style();
1755
1756                        if let CodeBlockRenderer::Default {
1757                            copy_button_visibility,
1758                            ..
1759                        } = &self.code_block_renderer
1760                            && *copy_button_visibility != CopyButtonVisibility::Hidden
1761                        {
1762                            builder.modify_current_div(|el| {
1763                                let content_range = parser::extract_code_block_content_range(
1764                                    &parsed_markdown.source()[range.clone()],
1765                                );
1766                                let content_range = content_range.start + range.start
1767                                    ..content_range.end + range.start;
1768
1769                                let code = parsed_markdown.source()[content_range].to_string();
1770                                let codeblock = render_copy_code_block_button(
1771                                    range.end,
1772                                    code,
1773                                    self.markdown.clone(),
1774                                );
1775                                el.child(
1776                                    h_flex()
1777                                        .w_4()
1778                                        .absolute()
1779                                        .justify_end()
1780                                        .when_else(
1781                                            *copy_button_visibility
1782                                                == CopyButtonVisibility::VisibleOnHover,
1783                                            |this| {
1784                                                this.top_0()
1785                                                    .right_0()
1786                                                    .visible_on_hover("code_block")
1787                                            },
1788                                            |this| this.top_1p5().right_1p5(),
1789                                        )
1790                                        .child(codeblock),
1791                                )
1792                            });
1793                        }
1794
1795                        // Pop the parent container.
1796                        builder.pop_div();
1797                    }
1798                    MarkdownTagEnd::HtmlBlock => builder.pop_div(),
1799                    MarkdownTagEnd::List(_) => {
1800                        builder.pop_list();
1801                        builder.pop_div();
1802                    }
1803                    MarkdownTagEnd::Item => {
1804                        self.pop_markdown_list_item(&mut builder);
1805                    }
1806                    MarkdownTagEnd::Emphasis => builder.pop_text_style(),
1807                    MarkdownTagEnd::Strong => builder.pop_text_style(),
1808                    MarkdownTagEnd::Strikethrough => builder.pop_text_style(),
1809                    MarkdownTagEnd::Link => {
1810                        if builder.code_block_stack.is_empty() {
1811                            builder.pop_text_style()
1812                        }
1813                    }
1814                    MarkdownTagEnd::Table => {
1815                        builder.pop_div();
1816                        builder.table.end();
1817                    }
1818                    MarkdownTagEnd::TableHead => {
1819                        builder.pop_text_style();
1820                        builder.table.end_head();
1821                    }
1822                    MarkdownTagEnd::TableRow => {
1823                        builder.table.end_row();
1824                    }
1825                    MarkdownTagEnd::TableCell => {
1826                        builder.replace_pending_checkbox(range);
1827                        builder.pop_div();
1828                        builder.table.end_cell();
1829                    }
1830                    _ => log::debug!("unsupported markdown tag end: {:?}", tag),
1831                },
1832                MarkdownEvent::Text => {
1833                    builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
1834                }
1835                MarkdownEvent::SubstitutedText(text) => {
1836                    builder.push_text(text, range.clone());
1837                }
1838                MarkdownEvent::Code => {
1839                    builder.push_text_style(self.style.inline_code.clone());
1840                    builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
1841                    builder.pop_text_style();
1842                }
1843                MarkdownEvent::Html => {
1844                    let html = &parsed_markdown.source[range.clone()];
1845                    if html.starts_with("<!--") {
1846                        builder.html_comment = true;
1847                    }
1848                    if html.trim_end().ends_with("-->") {
1849                        builder.html_comment = false;
1850                        continue;
1851                    }
1852                    if builder.html_comment {
1853                        continue;
1854                    }
1855                    builder.push_text(html, range.clone());
1856                }
1857                MarkdownEvent::InlineHtml => {
1858                    let html = &parsed_markdown.source[range.clone()];
1859                    if html.starts_with("<code>") {
1860                        builder.push_text_style(self.style.inline_code.clone());
1861                        continue;
1862                    }
1863                    if html.trim_end().starts_with("</code>") {
1864                        builder.pop_text_style();
1865                        continue;
1866                    }
1867                    builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
1868                }
1869                MarkdownEvent::Rule => {
1870                    builder.push_div(
1871                        div()
1872                            .border_b_1()
1873                            .my_2()
1874                            .border_color(self.style.rule_color),
1875                        range,
1876                        markdown_end,
1877                    );
1878                    builder.pop_div()
1879                }
1880                MarkdownEvent::SoftBreak => builder.push_text(" ", range.clone()),
1881                MarkdownEvent::HardBreak => builder.push_text("\n", range.clone()),
1882                MarkdownEvent::TaskListMarker(_) => {
1883                    // handled inside the `MarkdownTag::Item` case
1884                }
1885                _ => log::debug!("unsupported markdown event {:?}", event),
1886            }
1887        }
1888        if self.style.code_block_overflow_x_scroll {
1889            let code_block_ids = code_block_ids;
1890            self.markdown.update(cx, move |markdown, _| {
1891                markdown.retain_code_block_scroll_handles(&code_block_ids);
1892            });
1893        } else {
1894            self.markdown
1895                .update(cx, |markdown, _| markdown.clear_code_block_scroll_handles());
1896        }
1897        let mut rendered_markdown = builder.build();
1898        let child_layout_id = rendered_markdown.element.request_layout(window, cx);
1899        let layout_id = window.request_layout(gpui::Style::default(), [child_layout_id], cx);
1900        (layout_id, rendered_markdown)
1901    }
1902
1903    fn prepaint(
1904        &mut self,
1905        _id: Option<&GlobalElementId>,
1906        _inspector_id: Option<&gpui::InspectorElementId>,
1907        bounds: Bounds<Pixels>,
1908        rendered_markdown: &mut Self::RequestLayoutState,
1909        window: &mut Window,
1910        cx: &mut App,
1911    ) -> Self::PrepaintState {
1912        let focus_handle = self.markdown.read(cx).focus_handle.clone();
1913        window.set_focus_handle(&focus_handle, cx);
1914        window.set_view_id(self.markdown.entity_id());
1915
1916        let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
1917        rendered_markdown.element.prepaint(window, cx);
1918        self.autoscroll(&rendered_markdown.text, window, cx);
1919        hitbox
1920    }
1921
1922    fn paint(
1923        &mut self,
1924        _id: Option<&GlobalElementId>,
1925        _inspector_id: Option<&gpui::InspectorElementId>,
1926        bounds: Bounds<Pixels>,
1927        rendered_markdown: &mut Self::RequestLayoutState,
1928        hitbox: &mut Self::PrepaintState,
1929        window: &mut Window,
1930        cx: &mut App,
1931    ) {
1932        let mut context = KeyContext::default();
1933        context.add("Markdown");
1934        window.set_key_context(context);
1935        window.on_action(std::any::TypeId::of::<crate::Copy>(), {
1936            let entity = self.markdown.clone();
1937            let text = rendered_markdown.text.clone();
1938            move |_, phase, window, cx| {
1939                let text = text.clone();
1940                if phase == DispatchPhase::Bubble {
1941                    entity.update(cx, move |this, cx| this.copy(&text, window, cx))
1942                }
1943            }
1944        });
1945        window.on_action(std::any::TypeId::of::<crate::CopyAsMarkdown>(), {
1946            let entity = self.markdown.clone();
1947            move |_, phase, window, cx| {
1948                if phase == DispatchPhase::Bubble {
1949                    entity.update(cx, move |this, cx| this.copy_as_markdown(window, cx))
1950                }
1951            }
1952        });
1953
1954        self.paint_mouse_listeners(hitbox, &rendered_markdown.text, window, cx);
1955        rendered_markdown.element.paint(window, cx);
1956        self.paint_selection(bounds, &rendered_markdown.text, window, cx);
1957    }
1958}
1959
1960fn apply_heading_style(
1961    mut heading: Div,
1962    level: pulldown_cmark::HeadingLevel,
1963    custom_styles: Option<&HeadingLevelStyles>,
1964) -> Div {
1965    heading = match level {
1966        pulldown_cmark::HeadingLevel::H1 => heading.text_3xl(),
1967        pulldown_cmark::HeadingLevel::H2 => heading.text_2xl(),
1968        pulldown_cmark::HeadingLevel::H3 => heading.text_xl(),
1969        pulldown_cmark::HeadingLevel::H4 => heading.text_lg(),
1970        pulldown_cmark::HeadingLevel::H5 => heading.text_base(),
1971        pulldown_cmark::HeadingLevel::H6 => heading.text_sm(),
1972    };
1973
1974    if let Some(styles) = custom_styles {
1975        let style_opt = match level {
1976            pulldown_cmark::HeadingLevel::H1 => &styles.h1,
1977            pulldown_cmark::HeadingLevel::H2 => &styles.h2,
1978            pulldown_cmark::HeadingLevel::H3 => &styles.h3,
1979            pulldown_cmark::HeadingLevel::H4 => &styles.h4,
1980            pulldown_cmark::HeadingLevel::H5 => &styles.h5,
1981            pulldown_cmark::HeadingLevel::H6 => &styles.h6,
1982        };
1983
1984        if let Some(style) = style_opt {
1985            heading.style().text = style.clone();
1986        }
1987    }
1988
1989    heading
1990}
1991
1992fn render_copy_code_block_button(
1993    id: usize,
1994    code: String,
1995    markdown: Entity<Markdown>,
1996) -> impl IntoElement {
1997    let id = ElementId::named_usize("copy-markdown-code", id);
1998
1999    CopyButton::new(id.clone(), code.clone()).custom_on_click({
2000        let markdown = markdown;
2001        move |_window, cx| {
2002            let id = id.clone();
2003            markdown.update(cx, |this, cx| {
2004                this.copied_code_blocks.insert(id.clone());
2005
2006                cx.write_to_clipboard(ClipboardItem::new_string(code.clone()));
2007
2008                cx.spawn(async move |this, cx| {
2009                    cx.background_executor().timer(Duration::from_secs(2)).await;
2010
2011                    cx.update(|cx| {
2012                        this.update(cx, |this, cx| {
2013                            this.copied_code_blocks.remove(&id);
2014                            cx.notify();
2015                        })
2016                    })
2017                    .ok();
2018                })
2019                .detach();
2020            });
2021        }
2022    })
2023}
2024
2025impl IntoElement for MarkdownElement {
2026    type Element = Self;
2027
2028    fn into_element(self) -> Self::Element {
2029        self
2030    }
2031}
2032
2033pub enum AnyDiv {
2034    Div(Div),
2035    Stateful(Stateful<Div>),
2036}
2037
2038impl AnyDiv {
2039    fn into_any_element(self) -> AnyElement {
2040        match self {
2041            Self::Div(div) => div.into_any_element(),
2042            Self::Stateful(div) => div.into_any_element(),
2043        }
2044    }
2045}
2046
2047impl From<Div> for AnyDiv {
2048    fn from(value: Div) -> Self {
2049        Self::Div(value)
2050    }
2051}
2052
2053impl From<Stateful<Div>> for AnyDiv {
2054    fn from(value: Stateful<Div>) -> Self {
2055        Self::Stateful(value)
2056    }
2057}
2058
2059impl Styled for AnyDiv {
2060    fn style(&mut self) -> &mut StyleRefinement {
2061        match self {
2062            Self::Div(div) => div.style(),
2063            Self::Stateful(div) => div.style(),
2064        }
2065    }
2066}
2067
2068impl ParentElement for AnyDiv {
2069    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
2070        match self {
2071            Self::Div(div) => div.extend(elements),
2072            Self::Stateful(div) => div.extend(elements),
2073        }
2074    }
2075}
2076
2077#[derive(Default)]
2078struct TableState {
2079    alignments: Vec<Alignment>,
2080    in_head: bool,
2081    row_index: usize,
2082    col_index: usize,
2083}
2084
2085impl TableState {
2086    fn start(&mut self, alignments: Vec<Alignment>) {
2087        self.alignments = alignments;
2088        self.in_head = false;
2089        self.row_index = 0;
2090        self.col_index = 0;
2091    }
2092
2093    fn end(&mut self) {
2094        self.alignments.clear();
2095        self.in_head = false;
2096        self.row_index = 0;
2097        self.col_index = 0;
2098    }
2099
2100    fn start_head(&mut self) {
2101        self.in_head = true;
2102    }
2103
2104    fn end_head(&mut self) {
2105        self.in_head = false;
2106    }
2107
2108    fn start_row(&mut self) {
2109        self.col_index = 0;
2110    }
2111
2112    fn end_row(&mut self) {
2113        self.row_index += 1;
2114    }
2115
2116    fn end_cell(&mut self) {
2117        self.col_index += 1;
2118    }
2119}
2120
2121struct MarkdownElementBuilder {
2122    div_stack: Vec<AnyDiv>,
2123    rendered_lines: Vec<RenderedLine>,
2124    pending_line: PendingLine,
2125    rendered_links: Vec<RenderedLink>,
2126    current_source_index: usize,
2127    html_comment: bool,
2128    base_text_style: TextStyle,
2129    text_style_stack: Vec<TextStyleRefinement>,
2130    code_block_stack: Vec<Option<Arc<Language>>>,
2131    list_stack: Vec<ListStackEntry>,
2132    table: TableState,
2133    syntax_theme: Arc<SyntaxTheme>,
2134}
2135
2136#[derive(Default)]
2137struct PendingLine {
2138    text: String,
2139    runs: Vec<TextRun>,
2140    source_mappings: Vec<SourceMapping>,
2141}
2142
2143struct ListStackEntry {
2144    bullet_index: Option<u64>,
2145}
2146
2147impl MarkdownElementBuilder {
2148    fn new(
2149        container_style: &StyleRefinement,
2150        base_text_style: TextStyle,
2151        syntax_theme: Arc<SyntaxTheme>,
2152    ) -> Self {
2153        Self {
2154            div_stack: vec![{
2155                let mut base_div = div();
2156                base_div.style().refine(container_style);
2157                base_div.debug_selector(|| "inner".into()).into()
2158            }],
2159            rendered_lines: Vec::new(),
2160            pending_line: PendingLine::default(),
2161            rendered_links: Vec::new(),
2162            current_source_index: 0,
2163            html_comment: false,
2164            base_text_style,
2165            text_style_stack: Vec::new(),
2166            code_block_stack: Vec::new(),
2167            list_stack: Vec::new(),
2168            table: TableState::default(),
2169            syntax_theme,
2170        }
2171    }
2172
2173    fn push_text_style(&mut self, style: TextStyleRefinement) {
2174        self.text_style_stack.push(style);
2175    }
2176
2177    fn text_style(&self) -> TextStyle {
2178        let mut style = self.base_text_style.clone();
2179        for refinement in &self.text_style_stack {
2180            style.refine(refinement);
2181        }
2182        style
2183    }
2184
2185    fn pop_text_style(&mut self) {
2186        self.text_style_stack.pop();
2187    }
2188
2189    fn push_div(&mut self, div: impl Into<AnyDiv>, range: &Range<usize>, markdown_end: usize) {
2190        let mut div = div.into();
2191        self.flush_text();
2192
2193        if range.start == 0 {
2194            // Remove the top margin on the first element.
2195            div.style().refine(&StyleRefinement {
2196                margin: gpui::EdgesRefinement {
2197                    top: Some(Length::Definite(px(0.).into())),
2198                    left: None,
2199                    right: None,
2200                    bottom: None,
2201                },
2202                ..Default::default()
2203            });
2204        }
2205
2206        if range.end == markdown_end {
2207            div.style().refine(&StyleRefinement {
2208                margin: gpui::EdgesRefinement {
2209                    top: None,
2210                    left: None,
2211                    right: None,
2212                    bottom: Some(Length::Definite(rems(0.).into())),
2213                },
2214                ..Default::default()
2215            });
2216        }
2217
2218        self.div_stack.push(div);
2219    }
2220
2221    fn push_root_block(&mut self, range: &Range<usize>, markdown_end: usize) {
2222        self.push_div(
2223            div().group("markdown-root-block").relative(),
2224            range,
2225            markdown_end,
2226        );
2227        self.push_div(div().pl_4(), range, markdown_end);
2228    }
2229
2230    fn modify_current_div(&mut self, f: impl FnOnce(AnyDiv) -> AnyDiv) {
2231        self.flush_text();
2232        if let Some(div) = self.div_stack.pop() {
2233            self.div_stack.push(f(div));
2234        }
2235    }
2236
2237    fn pop_root_block(
2238        &mut self,
2239        is_active: bool,
2240        active_gutter_color: Hsla,
2241        hovered_gutter_color: Hsla,
2242    ) {
2243        self.pop_div();
2244        self.modify_current_div(|el| {
2245            el.child(
2246                div()
2247                    .h_full()
2248                    .w(px(4.0))
2249                    .when(is_active, |this| this.bg(active_gutter_color))
2250                    .group_hover("markdown-root-block", |this| {
2251                        if is_active {
2252                            this
2253                        } else {
2254                            this.bg(hovered_gutter_color)
2255                        }
2256                    })
2257                    .rounded_xs()
2258                    .absolute()
2259                    .left_0()
2260                    .top_0(),
2261            )
2262        });
2263        self.pop_div();
2264    }
2265
2266    fn pop_div(&mut self) {
2267        self.flush_text();
2268        let div = self.div_stack.pop().unwrap().into_any_element();
2269        self.div_stack.last_mut().unwrap().extend(iter::once(div));
2270    }
2271
2272    fn push_sourced_element(&mut self, source_range: Range<usize>, element: impl Into<AnyElement>) {
2273        self.flush_text();
2274        let anchor = self.render_source_anchor(source_range);
2275        self.div_stack.last_mut().unwrap().extend([{
2276            div()
2277                .relative()
2278                .child(anchor)
2279                .child(element.into())
2280                .into_any_element()
2281        }]);
2282    }
2283
2284    fn push_list(&mut self, bullet_index: Option<u64>) {
2285        self.list_stack.push(ListStackEntry { bullet_index });
2286    }
2287
2288    fn next_bullet_index(&mut self) -> Option<u64> {
2289        self.list_stack.last_mut().and_then(|entry| {
2290            let item_index = entry.bullet_index.as_mut()?;
2291            *item_index += 1;
2292            Some(*item_index - 1)
2293        })
2294    }
2295
2296    fn pop_list(&mut self) {
2297        self.list_stack.pop();
2298    }
2299
2300    fn push_code_block(&mut self, language: Option<Arc<Language>>) {
2301        self.code_block_stack.push(language);
2302    }
2303
2304    fn pop_code_block(&mut self) {
2305        self.code_block_stack.pop();
2306    }
2307
2308    fn push_link(&mut self, destination_url: SharedString, source_range: Range<usize>) {
2309        self.rendered_links.push(RenderedLink {
2310            source_range,
2311            destination_url,
2312        });
2313    }
2314
2315    fn push_text(&mut self, text: &str, source_range: Range<usize>) {
2316        self.pending_line.source_mappings.push(SourceMapping {
2317            rendered_index: self.pending_line.text.len(),
2318            source_index: source_range.start,
2319        });
2320        self.pending_line.text.push_str(text);
2321        self.current_source_index = source_range.end;
2322
2323        if let Some(Some(language)) = self.code_block_stack.last() {
2324            let mut offset = 0;
2325            for (range, highlight_id) in language.highlight_text(&Rope::from(text), 0..text.len()) {
2326                if range.start > offset {
2327                    self.pending_line
2328                        .runs
2329                        .push(self.text_style().to_run(range.start - offset));
2330                }
2331
2332                let mut run_style = self.text_style();
2333                if let Some(highlight) = self.syntax_theme.get(highlight_id).cloned() {
2334                    run_style = run_style.highlight(highlight);
2335                }
2336
2337                self.pending_line.runs.push(run_style.to_run(range.len()));
2338                offset = range.end;
2339            }
2340
2341            if offset < text.len() {
2342                self.pending_line
2343                    .runs
2344                    .push(self.text_style().to_run(text.len() - offset));
2345            }
2346        } else {
2347            self.pending_line
2348                .runs
2349                .push(self.text_style().to_run(text.len()));
2350        }
2351    }
2352
2353    fn trim_trailing_newline(&mut self) {
2354        if self.pending_line.text.ends_with('\n') {
2355            self.pending_line
2356                .text
2357                .truncate(self.pending_line.text.len() - 1);
2358            self.pending_line.runs.last_mut().unwrap().len -= 1;
2359            self.current_source_index -= 1;
2360        }
2361    }
2362
2363    fn replace_pending_checkbox(&mut self, source_range: &Range<usize>) {
2364        let trimmed = self.pending_line.text.trim();
2365        if trimmed == "[x]" || trimmed == "[X]" || trimmed == "[ ]" {
2366            let checked = trimmed != "[ ]";
2367            self.pending_line = PendingLine::default();
2368            let checkbox = Checkbox::new(
2369                ElementId::Name(
2370                    format!("table_checkbox_{}_{}", source_range.start, source_range.end).into(),
2371                ),
2372                if checked {
2373                    ToggleState::Selected
2374                } else {
2375                    ToggleState::Unselected
2376                },
2377            )
2378            .fill()
2379            .visualization_only(true)
2380            .into_any_element();
2381            self.div_stack.last_mut().unwrap().extend([checkbox]);
2382        }
2383    }
2384
2385    fn render_source_anchor(&mut self, source_range: Range<usize>) -> AnyElement {
2386        let mut text_style = self.base_text_style.clone();
2387        text_style.color = Hsla::transparent_black();
2388        let text = "\u{200B}";
2389        let styled_text = StyledText::new(text).with_runs(vec![text_style.to_run(text.len())]);
2390        self.rendered_lines.push(RenderedLine {
2391            layout: styled_text.layout().clone(),
2392            source_mappings: vec![SourceMapping {
2393                rendered_index: 0,
2394                source_index: source_range.start,
2395            }],
2396            source_end: source_range.end,
2397            language: None,
2398        });
2399        div()
2400            .absolute()
2401            .top_0()
2402            .left_0()
2403            .opacity(0.)
2404            .child(styled_text)
2405            .into_any_element()
2406    }
2407
2408    fn flush_text(&mut self) {
2409        let line = mem::take(&mut self.pending_line);
2410        if line.text.is_empty() {
2411            return;
2412        }
2413
2414        let text = StyledText::new(line.text).with_runs(line.runs);
2415        self.rendered_lines.push(RenderedLine {
2416            layout: text.layout().clone(),
2417            source_mappings: line.source_mappings,
2418            source_end: self.current_source_index,
2419            language: self.code_block_stack.last().cloned().flatten(),
2420        });
2421        self.div_stack.last_mut().unwrap().extend([text.into_any()]);
2422    }
2423
2424    fn build(mut self) -> RenderedMarkdown {
2425        debug_assert_eq!(self.div_stack.len(), 1);
2426        self.flush_text();
2427        RenderedMarkdown {
2428            element: self.div_stack.pop().unwrap().into_any_element(),
2429            text: RenderedText {
2430                lines: self.rendered_lines.into(),
2431                links: self.rendered_links.into(),
2432            },
2433        }
2434    }
2435}
2436
2437struct RenderedLine {
2438    layout: TextLayout,
2439    source_mappings: Vec<SourceMapping>,
2440    source_end: usize,
2441    language: Option<Arc<Language>>,
2442}
2443
2444impl RenderedLine {
2445    fn rendered_index_for_source_index(&self, source_index: usize) -> usize {
2446        if source_index >= self.source_end {
2447            return self.layout.len();
2448        }
2449
2450        let mapping = match self
2451            .source_mappings
2452            .binary_search_by_key(&source_index, |probe| probe.source_index)
2453        {
2454            Ok(ix) => &self.source_mappings[ix],
2455            Err(ix) => &self.source_mappings[ix - 1],
2456        };
2457        (mapping.rendered_index + (source_index - mapping.source_index)).min(self.layout.len())
2458    }
2459
2460    fn source_index_for_rendered_index(&self, rendered_index: usize) -> usize {
2461        if rendered_index >= self.layout.len() {
2462            return self.source_end;
2463        }
2464
2465        let mapping = match self
2466            .source_mappings
2467            .binary_search_by_key(&rendered_index, |probe| probe.rendered_index)
2468        {
2469            Ok(ix) => &self.source_mappings[ix],
2470            Err(ix) => &self.source_mappings[ix - 1],
2471        };
2472        mapping.source_index + (rendered_index - mapping.rendered_index)
2473    }
2474
2475    /// Returns the source index for use as an exclusive range end at a word/selection boundary.
2476    /// When the rendered index is exactly at the start of a segment with a gap from the previous
2477    /// segment (e.g., after stripped markdown syntax like backticks), this returns the end of the
2478    /// previous segment rather than the start of the current one.
2479    fn source_index_for_exclusive_rendered_end(&self, rendered_index: usize) -> usize {
2480        if rendered_index >= self.layout.len() {
2481            return self.source_end;
2482        }
2483
2484        let ix = match self
2485            .source_mappings
2486            .binary_search_by_key(&rendered_index, |probe| probe.rendered_index)
2487        {
2488            Ok(ix) => ix,
2489            Err(ix) => {
2490                return self.source_mappings[ix - 1].source_index
2491                    + (rendered_index - self.source_mappings[ix - 1].rendered_index);
2492            }
2493        };
2494
2495        // Exact match at the start of a segment. Check if there's a gap from the previous segment.
2496        if ix > 0 {
2497            let prev_mapping = &self.source_mappings[ix - 1];
2498            let mapping = &self.source_mappings[ix];
2499            let prev_segment_len = mapping.rendered_index - prev_mapping.rendered_index;
2500            let prev_source_end = prev_mapping.source_index + prev_segment_len;
2501            if prev_source_end < mapping.source_index {
2502                return prev_source_end;
2503            }
2504        }
2505
2506        self.source_mappings[ix].source_index
2507    }
2508
2509    fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
2510        let line_rendered_index;
2511        let out_of_bounds;
2512        match self.layout.index_for_position(position) {
2513            Ok(ix) => {
2514                line_rendered_index = ix;
2515                out_of_bounds = false;
2516            }
2517            Err(ix) => {
2518                line_rendered_index = ix;
2519                out_of_bounds = true;
2520            }
2521        };
2522        let source_index = self.source_index_for_rendered_index(line_rendered_index);
2523        if out_of_bounds {
2524            Err(source_index)
2525        } else {
2526            Ok(source_index)
2527        }
2528    }
2529}
2530
2531#[derive(Copy, Clone, Debug, Default)]
2532struct SourceMapping {
2533    rendered_index: usize,
2534    source_index: usize,
2535}
2536
2537pub struct RenderedMarkdown {
2538    element: AnyElement,
2539    text: RenderedText,
2540}
2541
2542#[derive(Clone)]
2543struct RenderedText {
2544    lines: Rc<[RenderedLine]>,
2545    links: Rc<[RenderedLink]>,
2546}
2547
2548#[derive(Debug, Clone, Eq, PartialEq)]
2549struct RenderedLink {
2550    source_range: Range<usize>,
2551    destination_url: SharedString,
2552}
2553
2554impl RenderedText {
2555    fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
2556        let mut lines = self.lines.iter().peekable();
2557        let mut fallback_line: Option<&RenderedLine> = None;
2558
2559        while let Some(line) = lines.next() {
2560            let line_bounds = line.layout.bounds();
2561
2562            // Exact match: position is within bounds (handles overlapping bounds like table columns)
2563            if line_bounds.contains(&position) {
2564                return line.source_index_for_position(position);
2565            }
2566
2567            // Track fallback for Y-coordinate based matching
2568            if position.y <= line_bounds.bottom() && fallback_line.is_none() {
2569                fallback_line = Some(line);
2570            }
2571
2572            // Handle gap between lines
2573            if position.y > line_bounds.bottom() {
2574                if let Some(next_line) = lines.peek()
2575                    && position.y < next_line.layout.bounds().top()
2576                {
2577                    return Err(line.source_end);
2578                }
2579            }
2580        }
2581
2582        // Fall back to Y-coordinate matched line
2583        if let Some(line) = fallback_line {
2584            return line.source_index_for_position(position);
2585        }
2586
2587        Err(self.lines.last().map_or(0, |line| line.source_end))
2588    }
2589
2590    fn position_for_source_index(&self, source_index: usize) -> Option<(Point<Pixels>, Pixels)> {
2591        for line in self.lines.iter() {
2592            let line_source_start = line.source_mappings.first().unwrap().source_index;
2593            if source_index < line_source_start {
2594                break;
2595            } else if source_index > line.source_end {
2596                continue;
2597            } else {
2598                let line_height = line.layout.line_height();
2599                let rendered_index_within_line = line.rendered_index_for_source_index(source_index);
2600                let position = line.layout.position_for_index(rendered_index_within_line)?;
2601                return Some((position, line_height));
2602            }
2603        }
2604        None
2605    }
2606
2607    fn surrounding_word_range(&self, source_index: usize) -> Range<usize> {
2608        for line in self.lines.iter() {
2609            if source_index > line.source_end {
2610                continue;
2611            }
2612
2613            let line_rendered_start = line.source_mappings.first().unwrap().rendered_index;
2614            let rendered_index_in_line =
2615                line.rendered_index_for_source_index(source_index) - line_rendered_start;
2616            let text = line.layout.text();
2617
2618            let scope = line.language.as_ref().map(|l| l.default_scope());
2619            let classifier = CharClassifier::new(scope);
2620
2621            let mut prev_chars = text[..rendered_index_in_line].chars().rev().peekable();
2622            let mut next_chars = text[rendered_index_in_line..].chars().peekable();
2623
2624            let word_kind = std::cmp::max(
2625                prev_chars.peek().map(|&c| classifier.kind(c)),
2626                next_chars.peek().map(|&c| classifier.kind(c)),
2627            );
2628
2629            let mut start = rendered_index_in_line;
2630            for c in prev_chars {
2631                if Some(classifier.kind(c)) == word_kind {
2632                    start -= c.len_utf8();
2633                } else {
2634                    break;
2635                }
2636            }
2637
2638            let mut end = rendered_index_in_line;
2639            for c in next_chars {
2640                if Some(classifier.kind(c)) == word_kind {
2641                    end += c.len_utf8();
2642                } else {
2643                    break;
2644                }
2645            }
2646
2647            return line.source_index_for_rendered_index(line_rendered_start + start)
2648                ..line.source_index_for_exclusive_rendered_end(line_rendered_start + end);
2649        }
2650
2651        source_index..source_index
2652    }
2653
2654    fn surrounding_line_range(&self, source_index: usize) -> Range<usize> {
2655        for line in self.lines.iter() {
2656            if source_index > line.source_end {
2657                continue;
2658            }
2659            let line_source_start = line.source_mappings.first().unwrap().source_index;
2660            return line_source_start..line.source_end;
2661        }
2662
2663        source_index..source_index
2664    }
2665
2666    fn text_for_range(&self, range: Range<usize>) -> String {
2667        let mut accumulator = String::new();
2668
2669        for line in self.lines.iter() {
2670            if range.start > line.source_end {
2671                continue;
2672            }
2673            let line_source_start = line.source_mappings.first().unwrap().source_index;
2674            if range.end < line_source_start {
2675                break;
2676            }
2677
2678            let text = line.layout.text();
2679
2680            let start = if range.start < line_source_start {
2681                0
2682            } else {
2683                line.rendered_index_for_source_index(range.start)
2684            };
2685            let end = if range.end > line.source_end {
2686                line.rendered_index_for_source_index(line.source_end)
2687            } else {
2688                line.rendered_index_for_source_index(range.end)
2689            }
2690            .min(text.len());
2691
2692            accumulator.push_str(&text[start..end]);
2693            accumulator.push('\n');
2694        }
2695        // Remove trailing newline
2696        accumulator.pop();
2697        accumulator
2698    }
2699
2700    fn link_for_position(&self, position: Point<Pixels>) -> Option<&RenderedLink> {
2701        let source_index = self.source_index_for_position(position).ok()?;
2702        self.links
2703            .iter()
2704            .find(|link| link.source_range.contains(&source_index))
2705    }
2706}
2707
2708#[cfg(test)]
2709mod tests {
2710    use super::*;
2711    use gpui::{TestAppContext, size};
2712    use language::{Language, LanguageConfig, LanguageMatcher};
2713    use std::sync::Arc;
2714
2715    fn ensure_theme_initialized(cx: &mut TestAppContext) {
2716        cx.update(|cx| {
2717            if !cx.has_global::<settings::SettingsStore>() {
2718                settings::init(cx);
2719            }
2720            if !cx.has_global::<theme::GlobalTheme>() {
2721                theme_settings::init(theme::LoadThemes::JustBase, cx);
2722            }
2723        });
2724    }
2725
2726    #[gpui::test]
2727    fn test_mappings(cx: &mut TestAppContext) {
2728        // Formatting.
2729        assert_mappings(
2730            &render_markdown("He*l*lo", cx),
2731            vec![vec![(0, 0), (1, 1), (2, 3), (3, 5), (4, 6), (5, 7)]],
2732        );
2733
2734        // Multiple lines.
2735        assert_mappings(
2736            &render_markdown("Hello\n\nWorld", cx),
2737            vec![
2738                vec![(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5)],
2739                vec![(0, 7), (1, 8), (2, 9), (3, 10), (4, 11), (5, 12)],
2740            ],
2741        );
2742
2743        // Multi-byte characters.
2744        assert_mappings(
2745            &render_markdown("αβγ\n\nδεζ", cx),
2746            vec![
2747                vec![(0, 0), (2, 2), (4, 4), (6, 6)],
2748                vec![(0, 8), (2, 10), (4, 12), (6, 14)],
2749            ],
2750        );
2751
2752        // Smart quotes.
2753        assert_mappings(&render_markdown("\"", cx), vec![vec![(0, 0), (3, 1)]]);
2754        assert_mappings(
2755            &render_markdown("\"hey\"", cx),
2756            vec![vec![(0, 0), (3, 1), (4, 2), (5, 3), (6, 4), (9, 5)]],
2757        );
2758
2759        // HTML Comments are ignored
2760        assert_mappings(
2761            &render_markdown(
2762                "<!--\nrdoc-file=string.c\n- str.intern   -> symbol\n- str.to_sym   -> symbol\n-->\nReturns",
2763                cx,
2764            ),
2765            vec![vec![
2766                (0, 78),
2767                (1, 79),
2768                (2, 80),
2769                (3, 81),
2770                (4, 82),
2771                (5, 83),
2772                (6, 84),
2773            ]],
2774        );
2775    }
2776
2777    fn render_markdown(markdown: &str, cx: &mut TestAppContext) -> RenderedText {
2778        render_markdown_with_language_registry(markdown, None, cx)
2779    }
2780
2781    fn render_markdown_with_language_registry(
2782        markdown: &str,
2783        language_registry: Option<Arc<LanguageRegistry>>,
2784        cx: &mut TestAppContext,
2785    ) -> RenderedText {
2786        render_markdown_with_options(markdown, language_registry, MarkdownOptions::default(), cx)
2787    }
2788
2789    fn render_markdown_with_options(
2790        markdown: &str,
2791        language_registry: Option<Arc<LanguageRegistry>>,
2792        options: MarkdownOptions,
2793        cx: &mut TestAppContext,
2794    ) -> RenderedText {
2795        struct TestWindow;
2796
2797        impl Render for TestWindow {
2798            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2799                div()
2800            }
2801        }
2802
2803        ensure_theme_initialized(cx);
2804
2805        let (_, cx) = cx.add_window_view(|_, _| TestWindow);
2806        let markdown = cx.new(|cx| {
2807            Markdown::new_with_options(
2808                markdown.to_string().into(),
2809                language_registry,
2810                None,
2811                options,
2812                cx,
2813            )
2814        });
2815        cx.run_until_parked();
2816        let (rendered, _) = cx.draw(
2817            Default::default(),
2818            size(px(600.0), px(600.0)),
2819            |_window, _cx| {
2820                MarkdownElement::new(markdown, MarkdownStyle::default()).code_block_renderer(
2821                    CodeBlockRenderer::Default {
2822                        copy_button_visibility: CopyButtonVisibility::Hidden,
2823                        border: false,
2824                    },
2825                )
2826            },
2827        );
2828        rendered.text
2829    }
2830
2831    #[gpui::test]
2832    fn test_surrounding_word_range(cx: &mut TestAppContext) {
2833        let rendered = render_markdown("Hello world tesεζ", cx);
2834
2835        // Test word selection for "Hello"
2836        let word_range = rendered.surrounding_word_range(2); // Simulate click on 'l' in "Hello"
2837        let selected_text = rendered.text_for_range(word_range);
2838        assert_eq!(selected_text, "Hello");
2839
2840        // Test word selection for "world"
2841        let word_range = rendered.surrounding_word_range(7); // Simulate click on 'o' in "world"
2842        let selected_text = rendered.text_for_range(word_range);
2843        assert_eq!(selected_text, "world");
2844
2845        // Test word selection for "tesεζ"
2846        let word_range = rendered.surrounding_word_range(14); // Simulate click on 's' in "tesεζ"
2847        let selected_text = rendered.text_for_range(word_range);
2848        assert_eq!(selected_text, "tesεζ");
2849
2850        // Test word selection at word boundary (space)
2851        let word_range = rendered.surrounding_word_range(5); // Simulate click on space between "Hello" and "world", expect highlighting word to the left
2852        let selected_text = rendered.text_for_range(word_range);
2853        assert_eq!(selected_text, "Hello");
2854    }
2855
2856    #[gpui::test]
2857    fn test_surrounding_line_range(cx: &mut TestAppContext) {
2858        let rendered = render_markdown("First line\n\nSecond line\n\nThird lineεζ", cx);
2859
2860        // Test getting line range for first line
2861        let line_range = rendered.surrounding_line_range(5); // Simulate click somewhere in first line
2862        let selected_text = rendered.text_for_range(line_range);
2863        assert_eq!(selected_text, "First line");
2864
2865        // Test getting line range for second line
2866        let line_range = rendered.surrounding_line_range(13); // Simulate click at beginning in second line
2867        let selected_text = rendered.text_for_range(line_range);
2868        assert_eq!(selected_text, "Second line");
2869
2870        // Test getting line range for third line
2871        let line_range = rendered.surrounding_line_range(37); // Simulate click at end of third line with multi-byte chars
2872        let selected_text = rendered.text_for_range(line_range);
2873        assert_eq!(selected_text, "Third lineεζ");
2874    }
2875
2876    #[gpui::test]
2877    fn test_selection_head_movement(cx: &mut TestAppContext) {
2878        let rendered = render_markdown("Hello world test", cx);
2879
2880        let mut selection = Selection {
2881            start: 5,
2882            end: 5,
2883            reversed: false,
2884            pending: false,
2885            mode: SelectMode::Character,
2886        };
2887
2888        // Test forward selection
2889        selection.set_head(10, &rendered);
2890        assert_eq!(selection.start, 5);
2891        assert_eq!(selection.end, 10);
2892        assert!(!selection.reversed);
2893        assert_eq!(selection.tail(), 5);
2894
2895        // Test backward selection
2896        selection.set_head(2, &rendered);
2897        assert_eq!(selection.start, 2);
2898        assert_eq!(selection.end, 5);
2899        assert!(selection.reversed);
2900        assert_eq!(selection.tail(), 5);
2901
2902        // Test forward selection again from reversed state
2903        selection.set_head(15, &rendered);
2904        assert_eq!(selection.start, 5);
2905        assert_eq!(selection.end, 15);
2906        assert!(!selection.reversed);
2907        assert_eq!(selection.tail(), 5);
2908    }
2909
2910    #[gpui::test]
2911    fn test_word_selection_drag(cx: &mut TestAppContext) {
2912        let rendered = render_markdown("Hello world test", cx);
2913
2914        // Start with a simulated double-click on "world" (index 6-10)
2915        let word_range = rendered.surrounding_word_range(7); // Click on 'o' in "world"
2916        let mut selection = Selection {
2917            start: word_range.start,
2918            end: word_range.end,
2919            reversed: false,
2920            pending: true,
2921            mode: SelectMode::Word(word_range),
2922        };
2923
2924        // Drag forward to "test" - should expand selection to include "test"
2925        selection.set_head(13, &rendered); // Index in "test"
2926        assert_eq!(selection.start, 6); // Start of "world"
2927        assert_eq!(selection.end, 16); // End of "test"
2928        assert!(!selection.reversed);
2929        let selected_text = rendered.text_for_range(selection.start..selection.end);
2930        assert_eq!(selected_text, "world test");
2931
2932        // Drag backward to "Hello" - should expand selection to include "Hello"
2933        selection.set_head(2, &rendered); // Index in "Hello"
2934        assert_eq!(selection.start, 0); // Start of "Hello"
2935        assert_eq!(selection.end, 11); // End of "world" (original selection)
2936        assert!(selection.reversed);
2937        let selected_text = rendered.text_for_range(selection.start..selection.end);
2938        assert_eq!(selected_text, "Hello world");
2939
2940        // Drag back within original word - should revert to original selection
2941        selection.set_head(8, &rendered); // Back within "world"
2942        assert_eq!(selection.start, 6); // Start of "world"
2943        assert_eq!(selection.end, 11); // End of "world"
2944        assert!(!selection.reversed);
2945        let selected_text = rendered.text_for_range(selection.start..selection.end);
2946        assert_eq!(selected_text, "world");
2947    }
2948
2949    #[gpui::test]
2950    fn test_selection_with_markdown_formatting(cx: &mut TestAppContext) {
2951        let rendered = render_markdown(
2952            "This is **bold** text, this is *italic* text, use `code` here",
2953            cx,
2954        );
2955        let word_range = rendered.surrounding_word_range(10); // Inside "bold"
2956        let selected_text = rendered.text_for_range(word_range);
2957        assert_eq!(selected_text, "bold");
2958
2959        let word_range = rendered.surrounding_word_range(32); // Inside "italic"
2960        let selected_text = rendered.text_for_range(word_range);
2961        assert_eq!(selected_text, "italic");
2962
2963        let word_range = rendered.surrounding_word_range(51); // Inside "code"
2964        let selected_text = rendered.text_for_range(word_range);
2965        assert_eq!(selected_text, "code");
2966    }
2967
2968    #[gpui::test]
2969    fn test_table_column_selection(cx: &mut TestAppContext) {
2970        let rendered = render_markdown("| a | b |\n|---|---|\n| c | d |", cx);
2971
2972        assert!(rendered.lines.len() >= 2);
2973        let first_bounds = rendered.lines[0].layout.bounds();
2974        let second_bounds = rendered.lines[1].layout.bounds();
2975
2976        let first_index = match rendered.source_index_for_position(first_bounds.center()) {
2977            Ok(index) | Err(index) => index,
2978        };
2979        let second_index = match rendered.source_index_for_position(second_bounds.center()) {
2980            Ok(index) | Err(index) => index,
2981        };
2982
2983        let first_word = rendered.text_for_range(rendered.surrounding_word_range(first_index));
2984        let second_word = rendered.text_for_range(rendered.surrounding_word_range(second_index));
2985
2986        assert_eq!(first_word, "a");
2987        assert_eq!(second_word, "b");
2988    }
2989
2990    #[test]
2991    fn test_table_checkbox_detection() {
2992        let md = "| Done |\n|------|\n| [x] |\n| [ ] |";
2993        let events = crate::parser::parse_markdown_with_options(md, false).events;
2994
2995        let mut in_table = false;
2996        let mut cell_texts: Vec<String> = Vec::new();
2997        let mut current_cell = String::new();
2998
2999        for (range, event) in &events {
3000            match event {
3001                MarkdownEvent::Start(MarkdownTag::Table(_)) => in_table = true,
3002                MarkdownEvent::End(MarkdownTagEnd::Table) => in_table = false,
3003                MarkdownEvent::Start(MarkdownTag::TableCell) => current_cell.clear(),
3004                MarkdownEvent::End(MarkdownTagEnd::TableCell) => {
3005                    if in_table {
3006                        cell_texts.push(current_cell.clone());
3007                    }
3008                }
3009                MarkdownEvent::Text if in_table => {
3010                    current_cell.push_str(&md[range.clone()]);
3011                }
3012                _ => {}
3013            }
3014        }
3015
3016        let checkbox_cells: Vec<&String> = cell_texts
3017            .iter()
3018            .filter(|t| {
3019                let trimmed = t.trim();
3020                trimmed == "[x]" || trimmed == "[X]" || trimmed == "[ ]"
3021            })
3022            .collect();
3023        assert_eq!(
3024            checkbox_cells.len(),
3025            2,
3026            "Expected 2 checkbox cells, got: {cell_texts:?}"
3027        );
3028        assert_eq!(checkbox_cells[0].trim(), "[x]");
3029        assert_eq!(checkbox_cells[1].trim(), "[ ]");
3030    }
3031
3032    #[gpui::test]
3033    fn test_inline_code_word_selection_excludes_backticks(cx: &mut TestAppContext) {
3034        // Test that double-clicking on inline code selects just the code content,
3035        // not the backticks. This verifies the fix for the bug where selecting
3036        // inline code would include the trailing backtick.
3037        let rendered = render_markdown("use `blah` here", cx);
3038
3039        // Source layout: "use `blah` here"
3040        //                 0123456789...
3041        // The inline code "blah" is at source positions 5-8 (content range 5..9)
3042
3043        // Click inside "blah" - should select just "blah", not "blah`"
3044        let word_range = rendered.surrounding_word_range(6); // 'l' in "blah"
3045
3046        // text_for_range extracts from the rendered text (without backticks), so it
3047        // would return "blah" even with a wrong source range. We check it anyway.
3048        let selected_text = rendered.text_for_range(word_range.clone());
3049        assert_eq!(selected_text, "blah");
3050
3051        // The source range is what matters for copy_as_markdown and selected_text,
3052        // which extract directly from the source. With the bug, this would be 5..10
3053        // which includes the closing backtick at position 9.
3054        assert_eq!(word_range, 5..9);
3055    }
3056
3057    #[gpui::test]
3058    fn test_surrounding_word_range_respects_word_characters(cx: &mut TestAppContext) {
3059        let rendered = render_markdown("foo.bar() baz", cx);
3060
3061        // Double clicking on 'f' in "foo" - should select just "foo"
3062        let word_range = rendered.surrounding_word_range(0);
3063        let selected_text = rendered.text_for_range(word_range);
3064        assert_eq!(selected_text, "foo");
3065
3066        // Double clicking on 'b' in "bar" - should select just "bar"
3067        let word_range = rendered.surrounding_word_range(4);
3068        let selected_text = rendered.text_for_range(word_range);
3069        assert_eq!(selected_text, "bar");
3070
3071        // Double clicking on 'b' in "baz" - should select "baz"
3072        let word_range = rendered.surrounding_word_range(10);
3073        let selected_text = rendered.text_for_range(word_range);
3074        assert_eq!(selected_text, "baz");
3075
3076        // Double clicking selects word characters in code blocks
3077        let javascript_language = Arc::new(Language::new(
3078            LanguageConfig {
3079                name: "JavaScript".into(),
3080                matcher: LanguageMatcher {
3081                    path_suffixes: vec!["js".to_string()],
3082                    ..Default::default()
3083                },
3084                word_characters: ['$', '#'].into_iter().collect(),
3085                ..Default::default()
3086            },
3087            None,
3088        ));
3089
3090        let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
3091        language_registry.add(javascript_language);
3092
3093        let rendered = render_markdown_with_language_registry(
3094            "```javascript\n$foo #bar\n```",
3095            Some(language_registry),
3096            cx,
3097        );
3098
3099        let word_range = rendered.surrounding_word_range(14);
3100        let selected_text = rendered.text_for_range(word_range);
3101        assert_eq!(selected_text, "$foo");
3102
3103        let word_range = rendered.surrounding_word_range(19);
3104        let selected_text = rendered.text_for_range(word_range);
3105        assert_eq!(selected_text, "#bar");
3106    }
3107
3108    #[gpui::test]
3109    fn test_all_selection(cx: &mut TestAppContext) {
3110        let rendered = render_markdown("Hello world\n\nThis is a test\n\nwith multiple lines", cx);
3111
3112        let total_length = rendered
3113            .lines
3114            .last()
3115            .map(|line| line.source_end)
3116            .unwrap_or(0);
3117
3118        let mut selection = Selection {
3119            start: 0,
3120            end: total_length,
3121            reversed: false,
3122            pending: true,
3123            mode: SelectMode::All,
3124        };
3125
3126        selection.set_head(5, &rendered); // Try to set head in middle
3127        assert_eq!(selection.start, 0);
3128        assert_eq!(selection.end, total_length);
3129        assert!(!selection.reversed);
3130
3131        selection.set_head(25, &rendered); // Try to set head near end
3132        assert_eq!(selection.start, 0);
3133        assert_eq!(selection.end, total_length);
3134        assert!(!selection.reversed);
3135
3136        let selected_text = rendered.text_for_range(selection.start..selection.end);
3137        assert_eq!(
3138            selected_text,
3139            "Hello world\nThis is a test\nwith multiple lines"
3140        );
3141    }
3142
3143    fn nbsp(n: usize) -> String {
3144        "\u{00A0}".repeat(n)
3145    }
3146
3147    #[test]
3148    fn test_escape_plain_text() {
3149        assert_eq!(Markdown::escape("hello world"), "hello world");
3150        assert_eq!(Markdown::escape(""), "");
3151        assert_eq!(Markdown::escape("café ☕ naïve"), "café ☕ naïve");
3152    }
3153
3154    #[test]
3155    fn test_escape_punctuation() {
3156        assert_eq!(Markdown::escape("hello `world`"), r"hello \`world\`");
3157        assert_eq!(Markdown::escape("a|b"), r"a\|b");
3158    }
3159
3160    #[test]
3161    fn test_escape_leading_spaces() {
3162        assert_eq!(Markdown::escape("    hello"), [&nbsp(4), "hello"].concat());
3163        assert_eq!(
3164            Markdown::escape("    | { a: string }"),
3165            [&nbsp(4), r"\| \{ a\: string \}"].concat()
3166        );
3167        assert_eq!(
3168            Markdown::escape("  first\n  second"),
3169            [&nbsp(2), "first\n\n", &nbsp(2), "second"].concat()
3170        );
3171        assert_eq!(Markdown::escape("hello   world"), "hello   world");
3172    }
3173
3174    #[test]
3175    fn test_escape_leading_tabs() {
3176        assert_eq!(Markdown::escape("\thello"), [&nbsp(4), "hello"].concat());
3177        assert_eq!(
3178            Markdown::escape("hello\n\t\tindented"),
3179            ["hello\n\n", &nbsp(8), "indented"].concat()
3180        );
3181        assert_eq!(
3182            Markdown::escape(" \t hello"),
3183            [&nbsp(1 + 4 + 1), "hello"].concat()
3184        );
3185        assert_eq!(Markdown::escape("hello\tworld"), "hello\tworld");
3186    }
3187
3188    #[test]
3189    fn test_escape_newlines() {
3190        assert_eq!(Markdown::escape("a\nb"), "a\n\nb");
3191        assert_eq!(Markdown::escape("a\n\nb"), "a\n\n\n\nb");
3192        assert_eq!(Markdown::escape("\nhello"), "\n\nhello");
3193    }
3194
3195    #[test]
3196    fn test_escape_multiline_diagnostic() {
3197        assert_eq!(
3198            Markdown::escape("    | { a: string }\n    | { b: number }"),
3199            [
3200                &nbsp(4),
3201                r"\| \{ a\: string \}",
3202                "\n\n",
3203                &nbsp(4),
3204                r"\| \{ b\: number \}",
3205            ]
3206            .concat()
3207        );
3208    }
3209
3210    fn has_code_block(markdown: &str) -> bool {
3211        let parsed_data = parse_markdown_with_options(markdown, false);
3212        parsed_data
3213            .events
3214            .iter()
3215            .any(|(_, event)| matches!(event, MarkdownEvent::Start(MarkdownTag::CodeBlock { .. })))
3216    }
3217
3218    #[test]
3219    fn test_escape_output_len_matches_precomputed() {
3220        let cases = [
3221            "",
3222            "hello world",
3223            "hello `world`",
3224            "    hello",
3225            "    | { a: string }",
3226            "\thello",
3227            "hello\n\t\tindented",
3228            " \t hello",
3229            "hello\tworld",
3230            "a\nb",
3231            "a\n\nb",
3232            "\nhello",
3233            "    | { a: string }\n    | { b: number }",
3234            "café ☕ naïve",
3235        ];
3236        for input in cases {
3237            let mut escaper = MarkdownEscaper::new();
3238            let precomputed: usize = input.bytes().map(|b| escaper.next(b).output_len()).sum();
3239
3240            let mut escaper = MarkdownEscaper::new();
3241            let mut output = String::new();
3242            for c in input.chars() {
3243                escaper.next(c as u8).write_to(c, &mut output);
3244            }
3245
3246            assert_eq!(precomputed, output.len(), "length mismatch for {:?}", input);
3247        }
3248    }
3249
3250    #[test]
3251    fn test_escape_prevents_code_block() {
3252        let diagnostic = "    | { a: string }";
3253        assert!(has_code_block(diagnostic));
3254        assert!(!has_code_block(&Markdown::escape(diagnostic)));
3255    }
3256
3257    #[track_caller]
3258    fn assert_mappings(rendered: &RenderedText, expected: Vec<Vec<(usize, usize)>>) {
3259        assert_eq!(rendered.lines.len(), expected.len(), "line count mismatch");
3260        for (line_ix, line_mappings) in expected.into_iter().enumerate() {
3261            let line = &rendered.lines[line_ix];
3262
3263            assert!(
3264                line.source_mappings.windows(2).all(|mappings| {
3265                    mappings[0].source_index < mappings[1].source_index
3266                        && mappings[0].rendered_index < mappings[1].rendered_index
3267                }),
3268                "line {} has duplicate mappings: {:?}",
3269                line_ix,
3270                line.source_mappings
3271            );
3272
3273            for (rendered_ix, source_ix) in line_mappings {
3274                assert_eq!(
3275                    line.source_index_for_rendered_index(rendered_ix),
3276                    source_ix,
3277                    "line {}, rendered_ix {}",
3278                    line_ix,
3279                    rendered_ix
3280                );
3281
3282                assert_eq!(
3283                    line.rendered_index_for_source_index(source_ix),
3284                    rendered_ix,
3285                    "line {}, source_ix {}",
3286                    line_ix,
3287                    source_ix
3288                );
3289            }
3290        }
3291    }
3292}