markdown.rs

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