markdown.rs

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