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