markdown.rs

   1pub mod parser;
   2
   3use std::collections::HashMap;
   4use std::iter;
   5use std::mem;
   6use std::ops::Range;
   7use std::rc::Rc;
   8use std::sync::Arc;
   9
  10use gpui::{
  11    actions, point, quad, AnyElement, App, Bounds, ClipboardItem, CursorStyle, DispatchPhase,
  12    Edges, Entity, FocusHandle, Focusable, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla,
  13    KeyContext, Length, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, Point, Render,
  14    Stateful, StrikethroughStyle, StyleRefinement, StyledText, Task, TextLayout, TextRun,
  15    TextStyle, TextStyleRefinement,
  16};
  17use language::{Language, LanguageRegistry, Rope};
  18use parser::{parse_links_only, parse_markdown, MarkdownEvent, MarkdownTag, MarkdownTagEnd};
  19use pulldown_cmark::Alignment;
  20use theme::SyntaxTheme;
  21use ui::{prelude::*, Tooltip};
  22use util::{ResultExt, TryFutureExt};
  23
  24use crate::parser::CodeBlockKind;
  25
  26#[derive(Clone)]
  27pub struct MarkdownStyle {
  28    pub base_text_style: TextStyle,
  29    pub code_block: StyleRefinement,
  30    pub inline_code: TextStyleRefinement,
  31    pub block_quote: TextStyleRefinement,
  32    pub link: TextStyleRefinement,
  33    pub rule_color: Hsla,
  34    pub block_quote_border_color: Hsla,
  35    pub syntax: Arc<SyntaxTheme>,
  36    pub selection_background_color: Hsla,
  37    pub heading: StyleRefinement,
  38}
  39
  40impl Default for MarkdownStyle {
  41    fn default() -> Self {
  42        Self {
  43            base_text_style: Default::default(),
  44            code_block: Default::default(),
  45            inline_code: Default::default(),
  46            block_quote: Default::default(),
  47            link: Default::default(),
  48            rule_color: Default::default(),
  49            block_quote_border_color: Default::default(),
  50            syntax: Arc::new(SyntaxTheme::default()),
  51            selection_background_color: Default::default(),
  52            heading: Default::default(),
  53        }
  54    }
  55}
  56
  57pub struct Markdown {
  58    source: SharedString,
  59    selection: Selection,
  60    pressed_link: Option<RenderedLink>,
  61    autoscroll_request: Option<usize>,
  62    style: MarkdownStyle,
  63    parsed_markdown: ParsedMarkdown,
  64    should_reparse: bool,
  65    pending_parse: Option<Task<Option<()>>>,
  66    focus_handle: FocusHandle,
  67    language_registry: Option<Arc<LanguageRegistry>>,
  68    fallback_code_block_language: Option<String>,
  69    open_url: Option<Box<dyn Fn(SharedString, &mut Window, &mut App)>>,
  70    options: Options,
  71}
  72
  73#[derive(Debug)]
  74struct Options {
  75    parse_links_only: bool,
  76    copy_code_block_buttons: bool,
  77}
  78
  79actions!(markdown, [Copy]);
  80
  81impl Markdown {
  82    pub fn new(
  83        source: SharedString,
  84        style: MarkdownStyle,
  85        language_registry: Option<Arc<LanguageRegistry>>,
  86        fallback_code_block_language: Option<String>,
  87        cx: &mut Context<Self>,
  88    ) -> Self {
  89        let focus_handle = cx.focus_handle();
  90        let mut this = Self {
  91            source,
  92            selection: Selection::default(),
  93            pressed_link: None,
  94            autoscroll_request: None,
  95            style,
  96            should_reparse: false,
  97            parsed_markdown: ParsedMarkdown::default(),
  98            pending_parse: None,
  99            focus_handle,
 100            language_registry,
 101            fallback_code_block_language,
 102            options: Options {
 103                parse_links_only: false,
 104                copy_code_block_buttons: true,
 105            },
 106            open_url: None,
 107        };
 108        this.parse(cx);
 109        this
 110    }
 111
 112    pub fn open_url(
 113        self,
 114        open_url: impl Fn(SharedString, &mut Window, &mut App) + 'static,
 115    ) -> Self {
 116        Self {
 117            open_url: Some(Box::new(open_url)),
 118            ..self
 119        }
 120    }
 121
 122    pub fn new_text(source: SharedString, style: MarkdownStyle, cx: &mut Context<Self>) -> Self {
 123        let focus_handle = cx.focus_handle();
 124        let mut this = Self {
 125            source,
 126            selection: Selection::default(),
 127            pressed_link: None,
 128            autoscroll_request: None,
 129            style,
 130            should_reparse: false,
 131            parsed_markdown: ParsedMarkdown::default(),
 132            pending_parse: None,
 133            focus_handle,
 134            language_registry: None,
 135            fallback_code_block_language: None,
 136            options: Options {
 137                parse_links_only: true,
 138                copy_code_block_buttons: true,
 139            },
 140            open_url: None,
 141        };
 142        this.parse(cx);
 143        this
 144    }
 145
 146    pub fn source(&self) -> &str {
 147        &self.source
 148    }
 149
 150    pub fn append(&mut self, text: &str, cx: &mut Context<Self>) {
 151        self.source = SharedString::new(self.source.to_string() + text);
 152        self.parse(cx);
 153    }
 154
 155    pub fn reset(&mut self, source: SharedString, cx: &mut Context<Self>) {
 156        if source == self.source() {
 157            return;
 158        }
 159        self.source = source;
 160        self.selection = Selection::default();
 161        self.autoscroll_request = None;
 162        self.pending_parse = None;
 163        self.should_reparse = false;
 164        self.parsed_markdown = ParsedMarkdown::default();
 165        self.parse(cx);
 166    }
 167
 168    pub fn parsed_markdown(&self) -> &ParsedMarkdown {
 169        &self.parsed_markdown
 170    }
 171
 172    fn copy(&self, text: &RenderedText, _: &mut Window, cx: &mut Context<Self>) {
 173        if self.selection.end <= self.selection.start {
 174            return;
 175        }
 176        let text = text.text_for_range(self.selection.start..self.selection.end);
 177        cx.write_to_clipboard(ClipboardItem::new_string(text));
 178    }
 179
 180    fn parse(&mut self, cx: &mut Context<Self>) {
 181        if self.source.is_empty() {
 182            return;
 183        }
 184
 185        if self.pending_parse.is_some() {
 186            self.should_reparse = true;
 187            return;
 188        }
 189
 190        let source = self.source.clone();
 191        let parse_text_only = self.options.parse_links_only;
 192        let language_registry = self.language_registry.clone();
 193        let fallback = self.fallback_code_block_language.clone();
 194        let parsed = cx.background_spawn(async move {
 195            if parse_text_only {
 196                return anyhow::Ok(ParsedMarkdown {
 197                    events: Arc::from(parse_links_only(source.as_ref())),
 198                    source,
 199                    languages: HashMap::default(),
 200                });
 201            }
 202            let (events, language_names) = parse_markdown(&source);
 203            let mut languages = HashMap::with_capacity(language_names.len());
 204            for name in language_names {
 205                if let Some(registry) = language_registry.as_ref() {
 206                    let language = if !name.is_empty() {
 207                        registry.language_for_name(&name)
 208                    } else if let Some(fallback) = &fallback {
 209                        registry.language_for_name(fallback)
 210                    } else {
 211                        continue;
 212                    };
 213                    if let Ok(language) = language.await {
 214                        languages.insert(name, language);
 215                    }
 216                }
 217            }
 218            anyhow::Ok(ParsedMarkdown {
 219                source,
 220                events: Arc::from(events),
 221                languages,
 222            })
 223        });
 224
 225        self.should_reparse = false;
 226        self.pending_parse = Some(cx.spawn(|this, mut cx| {
 227            async move {
 228                let parsed = parsed.await?;
 229                this.update(&mut cx, |this, cx| {
 230                    this.parsed_markdown = parsed;
 231                    this.pending_parse.take();
 232                    if this.should_reparse {
 233                        this.parse(cx);
 234                    }
 235                    cx.notify();
 236                })
 237                .ok();
 238                anyhow::Ok(())
 239            }
 240            .log_err()
 241        }));
 242    }
 243
 244    pub fn copy_code_block_buttons(mut self, should_copy: bool) -> Self {
 245        self.options.copy_code_block_buttons = should_copy;
 246        self
 247    }
 248}
 249
 250impl Render for Markdown {
 251    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 252        MarkdownElement::new(cx.entity().clone(), self.style.clone())
 253    }
 254}
 255
 256impl Focusable for Markdown {
 257    fn focus_handle(&self, _cx: &App) -> FocusHandle {
 258        self.focus_handle.clone()
 259    }
 260}
 261
 262#[derive(Copy, Clone, Default, Debug)]
 263struct Selection {
 264    start: usize,
 265    end: usize,
 266    reversed: bool,
 267    pending: bool,
 268}
 269
 270impl Selection {
 271    fn set_head(&mut self, head: usize) {
 272        if head < self.tail() {
 273            if !self.reversed {
 274                self.end = self.start;
 275                self.reversed = true;
 276            }
 277            self.start = head;
 278        } else {
 279            if self.reversed {
 280                self.start = self.end;
 281                self.reversed = false;
 282            }
 283            self.end = head;
 284        }
 285    }
 286
 287    fn tail(&self) -> usize {
 288        if self.reversed {
 289            self.end
 290        } else {
 291            self.start
 292        }
 293    }
 294}
 295
 296#[derive(Default)]
 297pub struct ParsedMarkdown {
 298    source: SharedString,
 299    events: Arc<[(Range<usize>, MarkdownEvent)]>,
 300    languages: HashMap<SharedString, Arc<Language>>,
 301}
 302
 303impl ParsedMarkdown {
 304    pub fn source(&self) -> &SharedString {
 305        &self.source
 306    }
 307
 308    pub fn events(&self) -> &Arc<[(Range<usize>, MarkdownEvent)]> {
 309        &self.events
 310    }
 311}
 312
 313pub struct MarkdownElement {
 314    markdown: Entity<Markdown>,
 315    style: MarkdownStyle,
 316}
 317
 318impl MarkdownElement {
 319    fn new(markdown: Entity<Markdown>, style: MarkdownStyle) -> Self {
 320        Self { markdown, style }
 321    }
 322
 323    fn paint_selection(
 324        &self,
 325        bounds: Bounds<Pixels>,
 326        rendered_text: &RenderedText,
 327        window: &mut Window,
 328        cx: &mut App,
 329    ) {
 330        let selection = self.markdown.read(cx).selection;
 331        let selection_start = rendered_text.position_for_source_index(selection.start);
 332        let selection_end = rendered_text.position_for_source_index(selection.end);
 333
 334        if let Some(((start_position, start_line_height), (end_position, end_line_height))) =
 335            selection_start.zip(selection_end)
 336        {
 337            if start_position.y == end_position.y {
 338                window.paint_quad(quad(
 339                    Bounds::from_corners(
 340                        start_position,
 341                        point(end_position.x, end_position.y + end_line_height),
 342                    ),
 343                    Pixels::ZERO,
 344                    self.style.selection_background_color,
 345                    Edges::default(),
 346                    Hsla::transparent_black(),
 347                ));
 348            } else {
 349                window.paint_quad(quad(
 350                    Bounds::from_corners(
 351                        start_position,
 352                        point(bounds.right(), start_position.y + start_line_height),
 353                    ),
 354                    Pixels::ZERO,
 355                    self.style.selection_background_color,
 356                    Edges::default(),
 357                    Hsla::transparent_black(),
 358                ));
 359
 360                if end_position.y > start_position.y + start_line_height {
 361                    window.paint_quad(quad(
 362                        Bounds::from_corners(
 363                            point(bounds.left(), start_position.y + start_line_height),
 364                            point(bounds.right(), end_position.y),
 365                        ),
 366                        Pixels::ZERO,
 367                        self.style.selection_background_color,
 368                        Edges::default(),
 369                        Hsla::transparent_black(),
 370                    ));
 371                }
 372
 373                window.paint_quad(quad(
 374                    Bounds::from_corners(
 375                        point(bounds.left(), end_position.y),
 376                        point(end_position.x, end_position.y + end_line_height),
 377                    ),
 378                    Pixels::ZERO,
 379                    self.style.selection_background_color,
 380                    Edges::default(),
 381                    Hsla::transparent_black(),
 382                ));
 383            }
 384        }
 385    }
 386
 387    fn paint_mouse_listeners(
 388        &self,
 389        hitbox: &Hitbox,
 390        rendered_text: &RenderedText,
 391        window: &mut Window,
 392        cx: &mut App,
 393    ) {
 394        let is_hovering_link = hitbox.is_hovered(window)
 395            && !self.markdown.read(cx).selection.pending
 396            && rendered_text
 397                .link_for_position(window.mouse_position())
 398                .is_some();
 399
 400        if is_hovering_link {
 401            window.set_cursor_style(CursorStyle::PointingHand, hitbox);
 402        } else {
 403            window.set_cursor_style(CursorStyle::IBeam, hitbox);
 404        }
 405
 406        self.on_mouse_event(window, cx, {
 407            let rendered_text = rendered_text.clone();
 408            let hitbox = hitbox.clone();
 409            move |markdown, event: &MouseDownEvent, phase, window, cx| {
 410                if hitbox.is_hovered(window) {
 411                    if phase.bubble() {
 412                        if let Some(link) = rendered_text.link_for_position(event.position) {
 413                            markdown.pressed_link = Some(link.clone());
 414                        } else {
 415                            let source_index =
 416                                match rendered_text.source_index_for_position(event.position) {
 417                                    Ok(ix) | Err(ix) => ix,
 418                                };
 419                            let range = if event.click_count == 2 {
 420                                rendered_text.surrounding_word_range(source_index)
 421                            } else if event.click_count == 3 {
 422                                rendered_text.surrounding_line_range(source_index)
 423                            } else {
 424                                source_index..source_index
 425                            };
 426                            markdown.selection = Selection {
 427                                start: range.start,
 428                                end: range.end,
 429                                reversed: false,
 430                                pending: true,
 431                            };
 432                            window.focus(&markdown.focus_handle);
 433                            window.prevent_default();
 434                        }
 435
 436                        cx.notify();
 437                    }
 438                } else if phase.capture() {
 439                    markdown.selection = Selection::default();
 440                    markdown.pressed_link = None;
 441                    cx.notify();
 442                }
 443            }
 444        });
 445        self.on_mouse_event(window, cx, {
 446            let rendered_text = rendered_text.clone();
 447            let hitbox = hitbox.clone();
 448            let was_hovering_link = is_hovering_link;
 449            move |markdown, event: &MouseMoveEvent, phase, window, cx| {
 450                if phase.capture() {
 451                    return;
 452                }
 453
 454                if markdown.selection.pending {
 455                    let source_index = match rendered_text.source_index_for_position(event.position)
 456                    {
 457                        Ok(ix) | Err(ix) => ix,
 458                    };
 459                    markdown.selection.set_head(source_index);
 460                    markdown.autoscroll_request = Some(source_index);
 461                    cx.notify();
 462                } else {
 463                    let is_hovering_link = hitbox.is_hovered(window)
 464                        && rendered_text.link_for_position(event.position).is_some();
 465                    if is_hovering_link != was_hovering_link {
 466                        cx.notify();
 467                    }
 468                }
 469            }
 470        });
 471        self.on_mouse_event(window, cx, {
 472            let rendered_text = rendered_text.clone();
 473            move |markdown, event: &MouseUpEvent, phase, window, cx| {
 474                if phase.bubble() {
 475                    if let Some(pressed_link) = markdown.pressed_link.take() {
 476                        if Some(&pressed_link) == rendered_text.link_for_position(event.position) {
 477                            if let Some(open_url) = markdown.open_url.as_mut() {
 478                                open_url(pressed_link.destination_url, window, cx);
 479                            } else {
 480                                cx.open_url(&pressed_link.destination_url);
 481                            }
 482                        }
 483                    }
 484                } else if markdown.selection.pending {
 485                    markdown.selection.pending = false;
 486                    #[cfg(any(target_os = "linux", target_os = "freebsd"))]
 487                    {
 488                        let text = rendered_text
 489                            .text_for_range(markdown.selection.start..markdown.selection.end);
 490                        cx.write_to_primary(ClipboardItem::new_string(text))
 491                    }
 492                    cx.notify();
 493                }
 494            }
 495        });
 496    }
 497
 498    fn autoscroll(
 499        &self,
 500        rendered_text: &RenderedText,
 501        window: &mut Window,
 502        cx: &mut App,
 503    ) -> Option<()> {
 504        let autoscroll_index = self
 505            .markdown
 506            .update(cx, |markdown, _| markdown.autoscroll_request.take())?;
 507        let (position, line_height) = rendered_text.position_for_source_index(autoscroll_index)?;
 508
 509        let text_style = self.style.base_text_style.clone();
 510        let font_id = window.text_system().resolve_font(&text_style.font());
 511        let font_size = text_style.font_size.to_pixels(window.rem_size());
 512        let em_width = window.text_system().em_width(font_id, font_size).unwrap();
 513        window.request_autoscroll(Bounds::from_corners(
 514            point(position.x - 3. * em_width, position.y - 3. * line_height),
 515            point(position.x + 3. * em_width, position.y + 3. * line_height),
 516        ));
 517        Some(())
 518    }
 519
 520    fn on_mouse_event<T: MouseEvent>(
 521        &self,
 522        window: &mut Window,
 523        _cx: &mut App,
 524        mut f: impl 'static
 525            + FnMut(&mut Markdown, &T, DispatchPhase, &mut Window, &mut Context<Markdown>),
 526    ) {
 527        window.on_mouse_event({
 528            let markdown = self.markdown.downgrade();
 529            move |event, phase, window, cx| {
 530                markdown
 531                    .update(cx, |markdown, cx| f(markdown, event, phase, window, cx))
 532                    .log_err();
 533            }
 534        });
 535    }
 536}
 537
 538impl Element for MarkdownElement {
 539    type RequestLayoutState = RenderedMarkdown;
 540    type PrepaintState = Hitbox;
 541
 542    fn id(&self) -> Option<ElementId> {
 543        None
 544    }
 545
 546    fn request_layout(
 547        &mut self,
 548        _id: Option<&GlobalElementId>,
 549        window: &mut Window,
 550        cx: &mut App,
 551    ) -> (gpui::LayoutId, Self::RequestLayoutState) {
 552        let mut builder = MarkdownElementBuilder::new(
 553            self.style.base_text_style.clone(),
 554            self.style.syntax.clone(),
 555        );
 556        let parsed_markdown = &self.markdown.read(cx).parsed_markdown;
 557        let markdown_end = if let Some(last) = parsed_markdown.events.last() {
 558            last.0.end
 559        } else {
 560            0
 561        };
 562        for (range, event) in parsed_markdown.events.iter() {
 563            match event {
 564                MarkdownEvent::Start(tag) => {
 565                    match tag {
 566                        MarkdownTag::Paragraph => {
 567                            builder.push_div(
 568                                div().mb_2().line_height(rems(1.3)),
 569                                range,
 570                                markdown_end,
 571                            );
 572                        }
 573                        MarkdownTag::Heading { level, .. } => {
 574                            let mut heading = div().mb_2();
 575                            heading = match level {
 576                                pulldown_cmark::HeadingLevel::H1 => heading.text_3xl(),
 577                                pulldown_cmark::HeadingLevel::H2 => heading.text_2xl(),
 578                                pulldown_cmark::HeadingLevel::H3 => heading.text_xl(),
 579                                pulldown_cmark::HeadingLevel::H4 => heading.text_lg(),
 580                                _ => heading,
 581                            };
 582                            heading.style().refine(&self.style.heading);
 583                            builder.push_text_style(
 584                                self.style.heading.text_style().clone().unwrap_or_default(),
 585                            );
 586                            builder.push_div(heading, range, markdown_end);
 587                        }
 588                        MarkdownTag::BlockQuote => {
 589                            builder.push_text_style(self.style.block_quote.clone());
 590                            builder.push_div(
 591                                div()
 592                                    .pl_4()
 593                                    .mb_2()
 594                                    .border_l_4()
 595                                    .border_color(self.style.block_quote_border_color),
 596                                range,
 597                                markdown_end,
 598                            );
 599                        }
 600                        MarkdownTag::CodeBlock(kind) => {
 601                            let language = if let CodeBlockKind::Fenced(language) = kind {
 602                                parsed_markdown.languages.get(language).cloned()
 603                            } else {
 604                                None
 605                            };
 606
 607                            let mut d = div().w_full().rounded_lg();
 608                            d.style().refine(&self.style.code_block);
 609                            if let Some(code_block_text_style) = &self.style.code_block.text {
 610                                builder.push_text_style(code_block_text_style.to_owned());
 611                            }
 612                            builder.push_code_block(language);
 613                            builder.push_div(d, range, markdown_end);
 614                        }
 615                        MarkdownTag::HtmlBlock => builder.push_div(div(), range, markdown_end),
 616                        MarkdownTag::List(bullet_index) => {
 617                            builder.push_list(*bullet_index);
 618                            builder.push_div(div().pl_4(), range, markdown_end);
 619                        }
 620                        MarkdownTag::Item => {
 621                            let bullet = if let Some(bullet_index) = builder.next_bullet_index() {
 622                                format!("{}.", bullet_index)
 623                            } else {
 624                                "".to_string()
 625                            };
 626                            builder.push_div(
 627                                div()
 628                                    .mb_1()
 629                                    .h_flex()
 630                                    .items_start()
 631                                    .gap_1()
 632                                    .line_height(rems(1.3))
 633                                    .child(bullet),
 634                                range,
 635                                markdown_end,
 636                            );
 637                            // Without `w_0`, text doesn't wrap to the width of the container.
 638                            builder.push_div(div().flex_1().w_0(), range, markdown_end);
 639                        }
 640                        MarkdownTag::Emphasis => builder.push_text_style(TextStyleRefinement {
 641                            font_style: Some(FontStyle::Italic),
 642                            ..Default::default()
 643                        }),
 644                        MarkdownTag::Strong => builder.push_text_style(TextStyleRefinement {
 645                            font_weight: Some(FontWeight::BOLD),
 646                            ..Default::default()
 647                        }),
 648                        MarkdownTag::Strikethrough => {
 649                            builder.push_text_style(TextStyleRefinement {
 650                                strikethrough: Some(StrikethroughStyle {
 651                                    thickness: px(1.),
 652                                    color: None,
 653                                }),
 654                                ..Default::default()
 655                            })
 656                        }
 657                        MarkdownTag::Link { dest_url, .. } => {
 658                            if builder.code_block_stack.is_empty() {
 659                                builder.push_link(dest_url.clone(), range.clone());
 660                                builder.push_text_style(self.style.link.clone())
 661                            }
 662                        }
 663                        MarkdownTag::MetadataBlock(_) => {}
 664                        MarkdownTag::Table(alignments) => {
 665                            builder.table_alignments = alignments.clone();
 666                            builder.push_div(
 667                                div()
 668                                    .id(("table", range.start))
 669                                    .flex()
 670                                    .border_1()
 671                                    .border_color(cx.theme().colors().border)
 672                                    .rounded_md()
 673                                    .overflow_x_scroll(),
 674                                range,
 675                                markdown_end,
 676                            );
 677                            // This inner `v_flex` is so the table rows will stack vertically without disrupting the `overflow_x_scroll`.
 678                            builder.push_div(div().v_flex().flex_grow(), range, markdown_end);
 679                        }
 680                        MarkdownTag::TableHead => {
 681                            builder.push_div(
 682                                div()
 683                                    .flex()
 684                                    .justify_between()
 685                                    .border_b_1()
 686                                    .border_color(cx.theme().colors().border),
 687                                range,
 688                                markdown_end,
 689                            );
 690                            builder.push_text_style(TextStyleRefinement {
 691                                font_weight: Some(FontWeight::BOLD),
 692                                ..Default::default()
 693                            });
 694                        }
 695                        MarkdownTag::TableRow => {
 696                            builder.push_div(
 697                                div().h_flex().justify_between().px_1().py_0p5(),
 698                                range,
 699                                markdown_end,
 700                            );
 701                        }
 702                        MarkdownTag::TableCell => {
 703                            let column_count = builder.table_alignments.len();
 704
 705                            builder.push_div(
 706                                div()
 707                                    .flex()
 708                                    .px_1()
 709                                    .w(relative(1. / column_count as f32))
 710                                    .truncate(),
 711                                range,
 712                                markdown_end,
 713                            );
 714                        }
 715                        _ => log::error!("unsupported markdown tag {:?}", tag),
 716                    }
 717                }
 718                MarkdownEvent::End(tag) => match tag {
 719                    MarkdownTagEnd::Paragraph => {
 720                        builder.pop_div();
 721                    }
 722                    MarkdownTagEnd::Heading(_) => {
 723                        builder.pop_div();
 724                        builder.pop_text_style()
 725                    }
 726                    MarkdownTagEnd::BlockQuote(_kind) => {
 727                        builder.pop_text_style();
 728                        builder.pop_div()
 729                    }
 730                    MarkdownTagEnd::CodeBlock => {
 731                        builder.trim_trailing_newline();
 732
 733                        if self.markdown.read(cx).options.copy_code_block_buttons {
 734                            builder.flush_text();
 735                            builder.modify_current_div(|el| {
 736                                let id =
 737                                    ElementId::NamedInteger("copy-markdown-code".into(), range.end);
 738                                let copy_button = div().absolute().top_1().right_1().w_5().child(
 739                                    IconButton::new(id, IconName::Copy)
 740                                        .icon_color(Color::Muted)
 741                                        .shape(ui::IconButtonShape::Square)
 742                                        .tooltip(Tooltip::text("Copy Code Block"))
 743                                        .on_click({
 744                                            let code = without_fences(
 745                                                parsed_markdown.source()[range.clone()].trim(),
 746                                            )
 747                                            .to_string();
 748
 749                                            move |_, _, cx| {
 750                                                cx.write_to_clipboard(ClipboardItem::new_string(
 751                                                    code.clone(),
 752                                                ))
 753                                            }
 754                                        }),
 755                                );
 756
 757                                el.child(copy_button)
 758                            });
 759                        }
 760
 761                        builder.pop_div();
 762                        builder.pop_code_block();
 763                        if self.style.code_block.text.is_some() {
 764                            builder.pop_text_style();
 765                        }
 766                    }
 767                    MarkdownTagEnd::HtmlBlock => builder.pop_div(),
 768                    MarkdownTagEnd::List(_) => {
 769                        builder.pop_list();
 770                        builder.pop_div();
 771                    }
 772                    MarkdownTagEnd::Item => {
 773                        builder.pop_div();
 774                        builder.pop_div();
 775                    }
 776                    MarkdownTagEnd::Emphasis => builder.pop_text_style(),
 777                    MarkdownTagEnd::Strong => builder.pop_text_style(),
 778                    MarkdownTagEnd::Strikethrough => builder.pop_text_style(),
 779                    MarkdownTagEnd::Link => {
 780                        if builder.code_block_stack.is_empty() {
 781                            builder.pop_text_style()
 782                        }
 783                    }
 784                    MarkdownTagEnd::Table => {
 785                        builder.pop_div();
 786                        builder.pop_div();
 787                        builder.table_alignments.clear();
 788                    }
 789                    MarkdownTagEnd::TableHead => {
 790                        builder.pop_div();
 791                        builder.pop_text_style();
 792                    }
 793                    MarkdownTagEnd::TableRow => {
 794                        builder.pop_div();
 795                    }
 796                    MarkdownTagEnd::TableCell => {
 797                        builder.pop_div();
 798                    }
 799                    _ => log::error!("unsupported markdown tag end: {:?}", tag),
 800                },
 801                MarkdownEvent::Text(parsed) => {
 802                    builder.push_text(parsed, range.start);
 803                }
 804                MarkdownEvent::Code => {
 805                    builder.push_text_style(self.style.inline_code.clone());
 806                    builder.push_text(&parsed_markdown.source[range.clone()], range.start);
 807                    builder.pop_text_style();
 808                }
 809                MarkdownEvent::Html => {
 810                    builder.push_text(&parsed_markdown.source[range.clone()], range.start);
 811                }
 812                MarkdownEvent::InlineHtml => {
 813                    builder.push_text(&parsed_markdown.source[range.clone()], range.start);
 814                }
 815                MarkdownEvent::Rule => {
 816                    builder.push_div(
 817                        div()
 818                            .border_b_1()
 819                            .my_2()
 820                            .border_color(self.style.rule_color),
 821                        range,
 822                        markdown_end,
 823                    );
 824                    builder.pop_div()
 825                }
 826                MarkdownEvent::SoftBreak => builder.push_text(" ", range.start),
 827                MarkdownEvent::HardBreak => builder.push_text("\n", range.start),
 828                _ => log::error!("unsupported markdown event {:?}", event),
 829            }
 830        }
 831        let mut rendered_markdown = builder.build();
 832        let child_layout_id = rendered_markdown.element.request_layout(window, cx);
 833        let layout_id = window.request_layout(gpui::Style::default(), [child_layout_id], cx);
 834        (layout_id, rendered_markdown)
 835    }
 836
 837    fn prepaint(
 838        &mut self,
 839        _id: Option<&GlobalElementId>,
 840        bounds: Bounds<Pixels>,
 841        rendered_markdown: &mut Self::RequestLayoutState,
 842        window: &mut Window,
 843        cx: &mut App,
 844    ) -> Self::PrepaintState {
 845        let focus_handle = self.markdown.read(cx).focus_handle.clone();
 846        window.set_focus_handle(&focus_handle, cx);
 847
 848        let hitbox = window.insert_hitbox(bounds, false);
 849        rendered_markdown.element.prepaint(window, cx);
 850        self.autoscroll(&rendered_markdown.text, window, cx);
 851        hitbox
 852    }
 853
 854    fn paint(
 855        &mut self,
 856        _id: Option<&GlobalElementId>,
 857        bounds: Bounds<Pixels>,
 858        rendered_markdown: &mut Self::RequestLayoutState,
 859        hitbox: &mut Self::PrepaintState,
 860        window: &mut Window,
 861        cx: &mut App,
 862    ) {
 863        let mut context = KeyContext::default();
 864        context.add("Markdown");
 865        window.set_key_context(context);
 866        let entity = self.markdown.clone();
 867        window.on_action(std::any::TypeId::of::<crate::Copy>(), {
 868            let text = rendered_markdown.text.clone();
 869            move |_, phase, window, cx| {
 870                let text = text.clone();
 871                if phase == DispatchPhase::Bubble {
 872                    entity.update(cx, move |this, cx| this.copy(&text, window, cx))
 873                }
 874            }
 875        });
 876
 877        self.paint_mouse_listeners(hitbox, &rendered_markdown.text, window, cx);
 878        rendered_markdown.element.paint(window, cx);
 879        self.paint_selection(bounds, &rendered_markdown.text, window, cx);
 880    }
 881}
 882
 883impl IntoElement for MarkdownElement {
 884    type Element = Self;
 885
 886    fn into_element(self) -> Self::Element {
 887        self
 888    }
 889}
 890
 891enum AnyDiv {
 892    Div(Div),
 893    Stateful(Stateful<Div>),
 894}
 895
 896impl AnyDiv {
 897    fn into_any_element(self) -> AnyElement {
 898        match self {
 899            Self::Div(div) => div.into_any_element(),
 900            Self::Stateful(div) => div.into_any_element(),
 901        }
 902    }
 903}
 904
 905impl From<Div> for AnyDiv {
 906    fn from(value: Div) -> Self {
 907        Self::Div(value)
 908    }
 909}
 910
 911impl From<Stateful<Div>> for AnyDiv {
 912    fn from(value: Stateful<Div>) -> Self {
 913        Self::Stateful(value)
 914    }
 915}
 916
 917impl Styled for AnyDiv {
 918    fn style(&mut self) -> &mut StyleRefinement {
 919        match self {
 920            Self::Div(div) => div.style(),
 921            Self::Stateful(div) => div.style(),
 922        }
 923    }
 924}
 925
 926impl ParentElement for AnyDiv {
 927    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
 928        match self {
 929            Self::Div(div) => div.extend(elements),
 930            Self::Stateful(div) => div.extend(elements),
 931        }
 932    }
 933}
 934
 935struct MarkdownElementBuilder {
 936    div_stack: Vec<AnyDiv>,
 937    rendered_lines: Vec<RenderedLine>,
 938    pending_line: PendingLine,
 939    rendered_links: Vec<RenderedLink>,
 940    current_source_index: usize,
 941    base_text_style: TextStyle,
 942    text_style_stack: Vec<TextStyleRefinement>,
 943    code_block_stack: Vec<Option<Arc<Language>>>,
 944    list_stack: Vec<ListStackEntry>,
 945    table_alignments: Vec<Alignment>,
 946    syntax_theme: Arc<SyntaxTheme>,
 947}
 948
 949#[derive(Default)]
 950struct PendingLine {
 951    text: String,
 952    runs: Vec<TextRun>,
 953    source_mappings: Vec<SourceMapping>,
 954}
 955
 956struct ListStackEntry {
 957    bullet_index: Option<u64>,
 958}
 959
 960impl MarkdownElementBuilder {
 961    fn new(base_text_style: TextStyle, syntax_theme: Arc<SyntaxTheme>) -> Self {
 962        Self {
 963            div_stack: vec![div().debug_selector(|| "inner".into()).into()],
 964            rendered_lines: Vec::new(),
 965            pending_line: PendingLine::default(),
 966            rendered_links: Vec::new(),
 967            current_source_index: 0,
 968            base_text_style,
 969            text_style_stack: Vec::new(),
 970            code_block_stack: Vec::new(),
 971            list_stack: Vec::new(),
 972            table_alignments: Vec::new(),
 973            syntax_theme,
 974        }
 975    }
 976
 977    fn push_text_style(&mut self, style: TextStyleRefinement) {
 978        self.text_style_stack.push(style);
 979    }
 980
 981    fn text_style(&self) -> TextStyle {
 982        let mut style = self.base_text_style.clone();
 983        for refinement in &self.text_style_stack {
 984            style.refine(refinement);
 985        }
 986        style
 987    }
 988
 989    fn pop_text_style(&mut self) {
 990        self.text_style_stack.pop();
 991    }
 992
 993    fn push_div(&mut self, div: impl Into<AnyDiv>, range: &Range<usize>, markdown_end: usize) {
 994        let mut div = div.into();
 995        self.flush_text();
 996
 997        if range.start == 0 {
 998            // Remove the top margin on the first element.
 999            div.style().refine(&StyleRefinement {
1000                margin: gpui::EdgesRefinement {
1001                    top: Some(Length::Definite(px(0.).into())),
1002                    left: None,
1003                    right: None,
1004                    bottom: None,
1005                },
1006                ..Default::default()
1007            });
1008        }
1009
1010        if range.end == markdown_end {
1011            div.style().refine(&StyleRefinement {
1012                margin: gpui::EdgesRefinement {
1013                    top: None,
1014                    left: None,
1015                    right: None,
1016                    bottom: Some(Length::Definite(rems(0.).into())),
1017                },
1018                ..Default::default()
1019            });
1020        }
1021
1022        self.div_stack.push(div);
1023    }
1024
1025    fn modify_current_div(&mut self, f: impl FnOnce(AnyDiv) -> AnyDiv) {
1026        self.flush_text();
1027        if let Some(div) = self.div_stack.pop() {
1028            self.div_stack.push(f(div));
1029        }
1030    }
1031
1032    fn pop_div(&mut self) {
1033        self.flush_text();
1034        let div = self.div_stack.pop().unwrap().into_any_element();
1035        self.div_stack.last_mut().unwrap().extend(iter::once(div));
1036    }
1037
1038    fn push_list(&mut self, bullet_index: Option<u64>) {
1039        self.list_stack.push(ListStackEntry { bullet_index });
1040    }
1041
1042    fn next_bullet_index(&mut self) -> Option<u64> {
1043        self.list_stack.last_mut().and_then(|entry| {
1044            let item_index = entry.bullet_index.as_mut()?;
1045            *item_index += 1;
1046            Some(*item_index - 1)
1047        })
1048    }
1049
1050    fn pop_list(&mut self) {
1051        self.list_stack.pop();
1052    }
1053
1054    fn push_code_block(&mut self, language: Option<Arc<Language>>) {
1055        self.code_block_stack.push(language);
1056    }
1057
1058    fn pop_code_block(&mut self) {
1059        self.code_block_stack.pop();
1060    }
1061
1062    fn push_link(&mut self, destination_url: SharedString, source_range: Range<usize>) {
1063        self.rendered_links.push(RenderedLink {
1064            source_range,
1065            destination_url,
1066        });
1067    }
1068
1069    fn push_text(&mut self, text: &str, source_index: usize) {
1070        self.pending_line.source_mappings.push(SourceMapping {
1071            rendered_index: self.pending_line.text.len(),
1072            source_index,
1073        });
1074        self.pending_line.text.push_str(text);
1075        self.current_source_index = source_index + text.len();
1076
1077        if let Some(Some(language)) = self.code_block_stack.last() {
1078            let mut offset = 0;
1079            for (range, highlight_id) in language.highlight_text(&Rope::from(text), 0..text.len()) {
1080                if range.start > offset {
1081                    self.pending_line
1082                        .runs
1083                        .push(self.text_style().to_run(range.start - offset));
1084                }
1085
1086                let mut run_style = self.text_style();
1087                if let Some(highlight) = highlight_id.style(&self.syntax_theme) {
1088                    run_style = run_style.highlight(highlight);
1089                }
1090                self.pending_line.runs.push(run_style.to_run(range.len()));
1091                offset = range.end;
1092            }
1093
1094            if offset < text.len() {
1095                self.pending_line
1096                    .runs
1097                    .push(self.text_style().to_run(text.len() - offset));
1098            }
1099        } else {
1100            self.pending_line
1101                .runs
1102                .push(self.text_style().to_run(text.len()));
1103        }
1104    }
1105
1106    fn trim_trailing_newline(&mut self) {
1107        if self.pending_line.text.ends_with('\n') {
1108            self.pending_line
1109                .text
1110                .truncate(self.pending_line.text.len() - 1);
1111            self.pending_line.runs.last_mut().unwrap().len -= 1;
1112            self.current_source_index -= 1;
1113        }
1114    }
1115
1116    fn flush_text(&mut self) {
1117        let line = mem::take(&mut self.pending_line);
1118        if line.text.is_empty() {
1119            return;
1120        }
1121
1122        let text = StyledText::new(line.text).with_runs(line.runs);
1123        self.rendered_lines.push(RenderedLine {
1124            layout: text.layout().clone(),
1125            source_mappings: line.source_mappings,
1126            source_end: self.current_source_index,
1127        });
1128        self.div_stack.last_mut().unwrap().extend([text.into_any()]);
1129    }
1130
1131    fn build(mut self) -> RenderedMarkdown {
1132        debug_assert_eq!(self.div_stack.len(), 1);
1133        self.flush_text();
1134        RenderedMarkdown {
1135            element: self.div_stack.pop().unwrap().into_any_element(),
1136            text: RenderedText {
1137                lines: self.rendered_lines.into(),
1138                links: self.rendered_links.into(),
1139            },
1140        }
1141    }
1142}
1143
1144struct RenderedLine {
1145    layout: TextLayout,
1146    source_mappings: Vec<SourceMapping>,
1147    source_end: usize,
1148}
1149
1150impl RenderedLine {
1151    fn rendered_index_for_source_index(&self, source_index: usize) -> usize {
1152        let mapping = match self
1153            .source_mappings
1154            .binary_search_by_key(&source_index, |probe| probe.source_index)
1155        {
1156            Ok(ix) => &self.source_mappings[ix],
1157            Err(ix) => &self.source_mappings[ix - 1],
1158        };
1159        mapping.rendered_index + (source_index - mapping.source_index)
1160    }
1161
1162    fn source_index_for_rendered_index(&self, rendered_index: usize) -> usize {
1163        let mapping = match self
1164            .source_mappings
1165            .binary_search_by_key(&rendered_index, |probe| probe.rendered_index)
1166        {
1167            Ok(ix) => &self.source_mappings[ix],
1168            Err(ix) => &self.source_mappings[ix - 1],
1169        };
1170        mapping.source_index + (rendered_index - mapping.rendered_index)
1171    }
1172
1173    fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
1174        let line_rendered_index;
1175        let out_of_bounds;
1176        match self.layout.index_for_position(position) {
1177            Ok(ix) => {
1178                line_rendered_index = ix;
1179                out_of_bounds = false;
1180            }
1181            Err(ix) => {
1182                line_rendered_index = ix;
1183                out_of_bounds = true;
1184            }
1185        };
1186        let source_index = self.source_index_for_rendered_index(line_rendered_index);
1187        if out_of_bounds {
1188            Err(source_index)
1189        } else {
1190            Ok(source_index)
1191        }
1192    }
1193}
1194
1195#[derive(Copy, Clone, Debug, Default)]
1196struct SourceMapping {
1197    rendered_index: usize,
1198    source_index: usize,
1199}
1200
1201pub struct RenderedMarkdown {
1202    element: AnyElement,
1203    text: RenderedText,
1204}
1205
1206#[derive(Clone)]
1207struct RenderedText {
1208    lines: Rc<[RenderedLine]>,
1209    links: Rc<[RenderedLink]>,
1210}
1211
1212#[derive(Clone, Eq, PartialEq)]
1213struct RenderedLink {
1214    source_range: Range<usize>,
1215    destination_url: SharedString,
1216}
1217
1218impl RenderedText {
1219    fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
1220        let mut lines = self.lines.iter().peekable();
1221
1222        while let Some(line) = lines.next() {
1223            let line_bounds = line.layout.bounds();
1224            if position.y > line_bounds.bottom() {
1225                if let Some(next_line) = lines.peek() {
1226                    if position.y < next_line.layout.bounds().top() {
1227                        return Err(line.source_end);
1228                    }
1229                }
1230
1231                continue;
1232            }
1233
1234            return line.source_index_for_position(position);
1235        }
1236
1237        Err(self.lines.last().map_or(0, |line| line.source_end))
1238    }
1239
1240    fn position_for_source_index(&self, source_index: usize) -> Option<(Point<Pixels>, Pixels)> {
1241        for line in self.lines.iter() {
1242            let line_source_start = line.source_mappings.first().unwrap().source_index;
1243            if source_index < line_source_start {
1244                break;
1245            } else if source_index > line.source_end {
1246                continue;
1247            } else {
1248                let line_height = line.layout.line_height();
1249                let rendered_index_within_line = line.rendered_index_for_source_index(source_index);
1250                let position = line.layout.position_for_index(rendered_index_within_line)?;
1251                return Some((position, line_height));
1252            }
1253        }
1254        None
1255    }
1256
1257    fn surrounding_word_range(&self, source_index: usize) -> Range<usize> {
1258        for line in self.lines.iter() {
1259            if source_index > line.source_end {
1260                continue;
1261            }
1262
1263            let line_rendered_start = line.source_mappings.first().unwrap().rendered_index;
1264            let rendered_index_in_line =
1265                line.rendered_index_for_source_index(source_index) - line_rendered_start;
1266            let text = line.layout.text();
1267            let previous_space = if let Some(idx) = text[0..rendered_index_in_line].rfind(' ') {
1268                idx + ' '.len_utf8()
1269            } else {
1270                0
1271            };
1272            let next_space = if let Some(idx) = text[rendered_index_in_line..].find(' ') {
1273                rendered_index_in_line + idx
1274            } else {
1275                text.len()
1276            };
1277
1278            return line.source_index_for_rendered_index(line_rendered_start + previous_space)
1279                ..line.source_index_for_rendered_index(line_rendered_start + next_space);
1280        }
1281
1282        source_index..source_index
1283    }
1284
1285    fn surrounding_line_range(&self, source_index: usize) -> Range<usize> {
1286        for line in self.lines.iter() {
1287            if source_index > line.source_end {
1288                continue;
1289            }
1290            let line_source_start = line.source_mappings.first().unwrap().source_index;
1291            return line_source_start..line.source_end;
1292        }
1293
1294        source_index..source_index
1295    }
1296
1297    fn text_for_range(&self, range: Range<usize>) -> String {
1298        let mut ret = vec![];
1299
1300        for line in self.lines.iter() {
1301            if range.start > line.source_end {
1302                continue;
1303            }
1304            let line_source_start = line.source_mappings.first().unwrap().source_index;
1305            if range.end < line_source_start {
1306                break;
1307            }
1308
1309            let text = line.layout.text();
1310
1311            let start = if range.start < line_source_start {
1312                0
1313            } else {
1314                line.rendered_index_for_source_index(range.start)
1315            };
1316            let end = if range.end > line.source_end {
1317                line.rendered_index_for_source_index(line.source_end)
1318            } else {
1319                line.rendered_index_for_source_index(range.end)
1320            }
1321            .min(text.len());
1322
1323            ret.push(text[start..end].to_string());
1324        }
1325        ret.join("\n")
1326    }
1327
1328    fn link_for_position(&self, position: Point<Pixels>) -> Option<&RenderedLink> {
1329        let source_index = self.source_index_for_position(position).ok()?;
1330        self.links
1331            .iter()
1332            .find(|link| link.source_range.contains(&source_index))
1333    }
1334}
1335
1336/// Some markdown blocks are indented, and others have e.g. ```rust … ``` around them.
1337/// If this block is fenced with backticks, strip them off (and the language name).
1338/// We use this when copying code blocks to the clipboard.
1339fn without_fences(mut markdown: &str) -> &str {
1340    if let Some(opening_backticks) = markdown.find("```") {
1341        markdown = &markdown[opening_backticks..];
1342
1343        // Trim off the next newline. This also trims off a language name if it's there.
1344        if let Some(newline) = markdown.find('\n') {
1345            markdown = &markdown[newline + 1..];
1346        }
1347    };
1348
1349    if let Some(closing_backticks) = markdown.rfind("```") {
1350        markdown = &markdown[..closing_backticks];
1351    };
1352
1353    markdown
1354}
1355
1356#[cfg(test)]
1357mod tests {
1358    use super::*;
1359
1360    #[test]
1361    fn test_without_fences() {
1362        let input = "```rust\nlet x = 5;\n```";
1363        assert_eq!(without_fences(input), "let x = 5;\n");
1364
1365        let input = "   ```\nno language\n```   ";
1366        assert_eq!(without_fences(input), "no language\n");
1367
1368        let input = "plain text";
1369        assert_eq!(without_fences(input), "plain text");
1370
1371        let input = "```python\nprint('hello')\nprint('world')\n```";
1372        assert_eq!(without_fences(input), "print('hello')\nprint('world')\n");
1373    }
1374}