markdown.rs

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