markdown.rs

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