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