markdown.rs

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