markdown.rs

   1pub mod parser;
   2mod path_range;
   3
   4use base64::Engine as _;
   5use futures::FutureExt as _;
   6use gpui::EdgesRefinement;
   7use gpui::HitboxBehavior;
   8use gpui::UnderlineStyle;
   9use language::LanguageName;
  10use log::Level;
  11pub use path_range::{LineCol, PathWithRange};
  12use settings::Settings as _;
  13use theme::ThemeSettings;
  14use ui::Checkbox;
  15use ui::CopyButton;
  16
  17use std::borrow::Cow;
  18use std::collections::BTreeMap;
  19use std::iter;
  20use std::mem;
  21use std::ops::Range;
  22use std::path::Path;
  23use std::rc::Rc;
  24use std::sync::Arc;
  25use std::time::Duration;
  26
  27use collections::{HashMap, HashSet};
  28use gpui::{
  29    AnyElement, App, BorderStyle, Bounds, ClipboardItem, CursorStyle, DispatchPhase, Edges, Entity,
  30    FocusHandle, Focusable, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla, Image,
  31    ImageFormat, KeyContext, Length, MouseButton, MouseDownEvent, MouseEvent, MouseMoveEvent,
  32    MouseUpEvent, Point, ScrollHandle, Stateful, StrikethroughStyle, StyleRefinement, StyledText,
  33    Task, TextLayout, TextRun, TextStyle, TextStyleRefinement, actions, img, point, quad,
  34};
  35use language::{CharClassifier, CharKind, Language, LanguageRegistry, Rope};
  36use parser::CodeBlockMetadata;
  37use parser::{MarkdownEvent, MarkdownTag, MarkdownTagEnd, parse_links_only, parse_markdown};
  38use pulldown_cmark::Alignment;
  39use sum_tree::TreeMap;
  40use theme::SyntaxTheme;
  41use ui::{ScrollAxes, Scrollbars, WithScrollbar, prelude::*};
  42use util::ResultExt;
  43
  44use crate::parser::CodeBlockKind;
  45
  46/// A callback function that can be used to customize the style of links based on the destination URL.
  47/// If the callback returns `None`, the default link style will be used.
  48type LinkStyleCallback = Rc<dyn Fn(&str, &App) -> Option<TextStyleRefinement>>;
  49
  50/// Defines custom style refinements for each heading level (H1-H6)
  51#[derive(Clone, Default)]
  52pub struct HeadingLevelStyles {
  53    pub h1: Option<TextStyleRefinement>,
  54    pub h2: Option<TextStyleRefinement>,
  55    pub h3: Option<TextStyleRefinement>,
  56    pub h4: Option<TextStyleRefinement>,
  57    pub h5: Option<TextStyleRefinement>,
  58    pub h6: Option<TextStyleRefinement>,
  59}
  60
  61#[derive(Clone)]
  62pub struct MarkdownStyle {
  63    pub base_text_style: TextStyle,
  64    pub container_style: StyleRefinement,
  65    pub code_block: StyleRefinement,
  66    pub code_block_overflow_x_scroll: bool,
  67    pub inline_code: TextStyleRefinement,
  68    pub block_quote: TextStyleRefinement,
  69    pub link: TextStyleRefinement,
  70    pub link_callback: Option<LinkStyleCallback>,
  71    pub rule_color: Hsla,
  72    pub block_quote_border_color: Hsla,
  73    pub syntax: Arc<SyntaxTheme>,
  74    pub selection_background_color: Hsla,
  75    pub heading: StyleRefinement,
  76    pub heading_level_styles: Option<HeadingLevelStyles>,
  77    pub height_is_multiple_of_line_height: bool,
  78    pub prevent_mouse_interaction: bool,
  79    pub table_columns_min_size: bool,
  80}
  81
  82impl Default for MarkdownStyle {
  83    fn default() -> Self {
  84        Self {
  85            base_text_style: Default::default(),
  86            container_style: Default::default(),
  87            code_block: Default::default(),
  88            code_block_overflow_x_scroll: false,
  89            inline_code: Default::default(),
  90            block_quote: Default::default(),
  91            link: Default::default(),
  92            link_callback: None,
  93            rule_color: Default::default(),
  94            block_quote_border_color: Default::default(),
  95            syntax: Arc::new(SyntaxTheme::default()),
  96            selection_background_color: Default::default(),
  97            heading: Default::default(),
  98            heading_level_styles: None,
  99            height_is_multiple_of_line_height: false,
 100            prevent_mouse_interaction: false,
 101            table_columns_min_size: false,
 102        }
 103    }
 104}
 105
 106pub enum MarkdownFont {
 107    Agent,
 108    Editor,
 109}
 110
 111impl MarkdownStyle {
 112    pub fn themed(font: MarkdownFont, window: &Window, cx: &App) -> Self {
 113        let theme_settings = ThemeSettings::get_global(cx);
 114        let colors = cx.theme().colors();
 115
 116        let buffer_font_weight = theme_settings.buffer_font.weight;
 117        let (buffer_font_size, ui_font_size) = match font {
 118            MarkdownFont::Agent => (
 119                theme_settings.agent_buffer_font_size(cx),
 120                theme_settings.agent_ui_font_size(cx),
 121            ),
 122            MarkdownFont::Editor => (
 123                theme_settings.buffer_font_size(cx),
 124                theme_settings.ui_font_size(cx),
 125            ),
 126        };
 127
 128        let text_color = colors.text;
 129
 130        let mut text_style = window.text_style();
 131        let line_height = buffer_font_size * 1.75;
 132
 133        text_style.refine(&TextStyleRefinement {
 134            font_family: Some(theme_settings.ui_font.family.clone()),
 135            font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
 136            font_features: Some(theme_settings.ui_font.features.clone()),
 137            font_size: Some(ui_font_size.into()),
 138            line_height: Some(line_height.into()),
 139            color: Some(text_color),
 140            ..Default::default()
 141        });
 142
 143        MarkdownStyle {
 144            base_text_style: text_style.clone(),
 145            syntax: cx.theme().syntax().clone(),
 146            selection_background_color: colors.element_selection_background,
 147            code_block_overflow_x_scroll: true,
 148            heading_level_styles: Some(HeadingLevelStyles {
 149                h1: Some(TextStyleRefinement {
 150                    font_size: Some(rems(1.15).into()),
 151                    ..Default::default()
 152                }),
 153                h2: Some(TextStyleRefinement {
 154                    font_size: Some(rems(1.1).into()),
 155                    ..Default::default()
 156                }),
 157                h3: Some(TextStyleRefinement {
 158                    font_size: Some(rems(1.05).into()),
 159                    ..Default::default()
 160                }),
 161                h4: Some(TextStyleRefinement {
 162                    font_size: Some(rems(1.).into()),
 163                    ..Default::default()
 164                }),
 165                h5: Some(TextStyleRefinement {
 166                    font_size: Some(rems(0.95).into()),
 167                    ..Default::default()
 168                }),
 169                h6: Some(TextStyleRefinement {
 170                    font_size: Some(rems(0.875).into()),
 171                    ..Default::default()
 172                }),
 173            }),
 174            code_block: StyleRefinement {
 175                padding: EdgesRefinement {
 176                    top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
 177                    left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
 178                    right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
 179                    bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))),
 180                },
 181                margin: EdgesRefinement {
 182                    top: Some(Length::Definite(px(8.).into())),
 183                    left: Some(Length::Definite(px(0.).into())),
 184                    right: Some(Length::Definite(px(0.).into())),
 185                    bottom: Some(Length::Definite(px(12.).into())),
 186                },
 187                border_style: Some(BorderStyle::Solid),
 188                border_widths: EdgesRefinement {
 189                    top: Some(AbsoluteLength::Pixels(px(1.))),
 190                    left: Some(AbsoluteLength::Pixels(px(1.))),
 191                    right: Some(AbsoluteLength::Pixels(px(1.))),
 192                    bottom: Some(AbsoluteLength::Pixels(px(1.))),
 193                },
 194                border_color: Some(colors.border_variant),
 195                background: Some(colors.editor_background.into()),
 196                text: TextStyleRefinement {
 197                    font_family: Some(theme_settings.buffer_font.family.clone()),
 198                    font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
 199                    font_features: Some(theme_settings.buffer_font.features.clone()),
 200                    font_size: Some(buffer_font_size.into()),
 201                    font_weight: Some(buffer_font_weight),
 202                    ..Default::default()
 203                },
 204                ..Default::default()
 205            },
 206            inline_code: TextStyleRefinement {
 207                font_family: Some(theme_settings.buffer_font.family.clone()),
 208                font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
 209                font_features: Some(theme_settings.buffer_font.features.clone()),
 210                font_size: Some(buffer_font_size.into()),
 211                font_weight: Some(buffer_font_weight),
 212                background_color: Some(colors.editor_foreground.opacity(0.08)),
 213                ..Default::default()
 214            },
 215            link: TextStyleRefinement {
 216                background_color: Some(colors.editor_foreground.opacity(0.025)),
 217                color: Some(colors.text_accent),
 218                underline: Some(UnderlineStyle {
 219                    color: Some(colors.text_accent.opacity(0.5)),
 220                    thickness: px(1.),
 221                    ..Default::default()
 222                }),
 223                ..Default::default()
 224            },
 225            ..Default::default()
 226        }
 227    }
 228
 229    pub fn with_muted_text(mut self, cx: &App) -> Self {
 230        let colors = cx.theme().colors();
 231        self.base_text_style.color = colors.text_muted;
 232        self
 233    }
 234}
 235
 236pub struct Markdown {
 237    source: SharedString,
 238    selection: Selection,
 239    pressed_link: Option<RenderedLink>,
 240    pressed_code_block_word: Option<SharedString>,
 241    autoscroll_request: Option<usize>,
 242    parsed_markdown: ParsedMarkdown,
 243    images_by_source_offset: HashMap<usize, Arc<Image>>,
 244    should_reparse: bool,
 245    pending_parse: Option<Task<()>>,
 246    focus_handle: FocusHandle,
 247    language_registry: Option<Arc<LanguageRegistry>>,
 248    fallback_code_block_language: Option<LanguageName>,
 249    options: Options,
 250    copied_code_blocks: HashSet<ElementId>,
 251    code_block_scroll_handles: BTreeMap<usize, ScrollHandle>,
 252    context_menu_selected_text: Option<String>,
 253}
 254
 255struct Options {
 256    parse_links_only: bool,
 257}
 258
 259pub enum CodeBlockRenderer {
 260    Default {
 261        copy_button: bool,
 262        copy_button_on_hover: bool,
 263        border: bool,
 264    },
 265    Custom {
 266        render: CodeBlockRenderFn,
 267        /// A function that can modify the parent container after the code block
 268        /// content has been appended as a child element.
 269        transform: Option<CodeBlockTransformFn>,
 270    },
 271}
 272
 273pub type CodeBlockRenderFn = Arc<
 274    dyn Fn(
 275        &CodeBlockKind,
 276        &ParsedMarkdown,
 277        Range<usize>,
 278        CodeBlockMetadata,
 279        &mut Window,
 280        &App,
 281    ) -> Div,
 282>;
 283
 284pub type CodeBlockTransformFn =
 285    Arc<dyn Fn(AnyDiv, Range<usize>, CodeBlockMetadata, &mut Window, &App) -> AnyDiv>;
 286
 287actions!(
 288    markdown,
 289    [
 290        /// Copies the selected text to the clipboard.
 291        Copy,
 292        /// Copies the selected text as markdown to the clipboard.
 293        CopyAsMarkdown
 294    ]
 295);
 296
 297impl Markdown {
 298    pub fn new(
 299        source: SharedString,
 300        language_registry: Option<Arc<LanguageRegistry>>,
 301        fallback_code_block_language: Option<LanguageName>,
 302        cx: &mut Context<Self>,
 303    ) -> Self {
 304        let focus_handle = cx.focus_handle();
 305        let mut this = Self {
 306            source,
 307            selection: Selection::default(),
 308            pressed_link: None,
 309            pressed_code_block_word: None,
 310            autoscroll_request: None,
 311            should_reparse: false,
 312            images_by_source_offset: Default::default(),
 313            parsed_markdown: ParsedMarkdown::default(),
 314            pending_parse: None,
 315            focus_handle,
 316            language_registry,
 317            fallback_code_block_language,
 318            options: Options {
 319                parse_links_only: false,
 320            },
 321            copied_code_blocks: HashSet::default(),
 322            code_block_scroll_handles: BTreeMap::default(),
 323            context_menu_selected_text: None,
 324        };
 325        this.parse(cx);
 326        this
 327    }
 328
 329    pub fn new_text(source: SharedString, cx: &mut Context<Self>) -> Self {
 330        let focus_handle = cx.focus_handle();
 331        let mut this = Self {
 332            source,
 333            selection: Selection::default(),
 334            pressed_link: None,
 335            pressed_code_block_word: None,
 336            autoscroll_request: None,
 337            should_reparse: false,
 338            parsed_markdown: ParsedMarkdown::default(),
 339            images_by_source_offset: Default::default(),
 340            pending_parse: None,
 341            focus_handle,
 342            language_registry: None,
 343            fallback_code_block_language: None,
 344            options: Options {
 345                parse_links_only: true,
 346            },
 347            copied_code_blocks: HashSet::default(),
 348            code_block_scroll_handles: BTreeMap::default(),
 349            context_menu_selected_text: None,
 350        };
 351        this.parse(cx);
 352        this
 353    }
 354
 355    fn code_block_scroll_handle(&mut self, id: usize) -> ScrollHandle {
 356        self.code_block_scroll_handles
 357            .entry(id)
 358            .or_insert_with(ScrollHandle::new)
 359            .clone()
 360    }
 361
 362    fn retain_code_block_scroll_handles(&mut self, ids: &HashSet<usize>) {
 363        self.code_block_scroll_handles
 364            .retain(|id, _| ids.contains(id));
 365    }
 366
 367    fn clear_code_block_scroll_handles(&mut self) {
 368        self.code_block_scroll_handles.clear();
 369    }
 370
 371    fn autoscroll_code_block(&self, source_index: usize, cursor_position: Point<Pixels>) {
 372        let Some((_, scroll_handle)) = self
 373            .code_block_scroll_handles
 374            .range(..=source_index)
 375            .next_back()
 376        else {
 377            return;
 378        };
 379
 380        let bounds = scroll_handle.bounds();
 381        if cursor_position.y < bounds.top() || cursor_position.y > bounds.bottom() {
 382            return;
 383        }
 384
 385        let horizontal_delta = if cursor_position.x < bounds.left() {
 386            bounds.left() - cursor_position.x
 387        } else if cursor_position.x > bounds.right() {
 388            bounds.right() - cursor_position.x
 389        } else {
 390            return;
 391        };
 392
 393        let offset = scroll_handle.offset();
 394        scroll_handle.set_offset(point(offset.x + horizontal_delta, offset.y));
 395    }
 396
 397    pub fn is_parsing(&self) -> bool {
 398        self.pending_parse.is_some()
 399    }
 400
 401    pub fn source(&self) -> &str {
 402        &self.source
 403    }
 404
 405    pub fn append(&mut self, text: &str, cx: &mut Context<Self>) {
 406        self.source = SharedString::new(self.source.to_string() + text);
 407        self.parse(cx);
 408    }
 409
 410    pub fn replace(&mut self, source: impl Into<SharedString>, cx: &mut Context<Self>) {
 411        self.source = source.into();
 412        self.parse(cx);
 413    }
 414
 415    pub fn reset(&mut self, source: SharedString, cx: &mut Context<Self>) {
 416        if source == self.source() {
 417            return;
 418        }
 419        self.source = source;
 420        self.selection = Selection::default();
 421        self.autoscroll_request = None;
 422        self.pending_parse = None;
 423        self.should_reparse = false;
 424        // Don't clear parsed_markdown here - keep existing content visible until new parse completes
 425        self.parse(cx);
 426    }
 427
 428    #[cfg(any(test, feature = "test-support"))]
 429    pub fn parsed_markdown(&self) -> &ParsedMarkdown {
 430        &self.parsed_markdown
 431    }
 432
 433    pub fn escape(s: &str) -> Cow<'_, str> {
 434        // Valid to use bytes since multi-byte UTF-8 doesn't use ASCII chars.
 435        let count = s
 436            .bytes()
 437            .filter(|c| *c == b'\n' || c.is_ascii_punctuation())
 438            .count();
 439        if count > 0 {
 440            let mut output = String::with_capacity(s.len() + count);
 441            let mut is_newline = false;
 442            for c in s.chars() {
 443                if is_newline && c == ' ' {
 444                    continue;
 445                }
 446                is_newline = c == '\n';
 447                if c == '\n' {
 448                    output.push('\n')
 449                } else if c.is_ascii_punctuation() {
 450                    output.push('\\')
 451                }
 452                output.push(c)
 453            }
 454            output.into()
 455        } else {
 456            s.into()
 457        }
 458    }
 459
 460    pub fn selected_text(&self) -> Option<String> {
 461        if self.selection.end <= self.selection.start {
 462            None
 463        } else {
 464            Some(self.source[self.selection.start..self.selection.end].to_string())
 465        }
 466    }
 467
 468    fn copy(&self, text: &RenderedText, _: &mut Window, cx: &mut Context<Self>) {
 469        if self.selection.end <= self.selection.start {
 470            return;
 471        }
 472        let text = text.text_for_range(self.selection.start..self.selection.end);
 473        cx.write_to_clipboard(ClipboardItem::new_string(text));
 474    }
 475
 476    fn copy_as_markdown(&mut self, _: &mut Window, cx: &mut Context<Self>) {
 477        if let Some(text) = self.context_menu_selected_text.take() {
 478            cx.write_to_clipboard(ClipboardItem::new_string(text));
 479            return;
 480        }
 481        if self.selection.end <= self.selection.start {
 482            return;
 483        }
 484        let text = self.source[self.selection.start..self.selection.end].to_string();
 485        cx.write_to_clipboard(ClipboardItem::new_string(text));
 486    }
 487
 488    fn capture_selection_for_context_menu(&mut self) {
 489        self.context_menu_selected_text = self.selected_text();
 490    }
 491
 492    fn parse(&mut self, cx: &mut Context<Self>) {
 493        if self.source.is_empty() {
 494            return;
 495        }
 496
 497        if self.pending_parse.is_some() {
 498            self.should_reparse = true;
 499            return;
 500        }
 501        self.should_reparse = false;
 502        self.pending_parse = Some(self.start_background_parse(cx));
 503    }
 504
 505    fn start_background_parse(&self, cx: &Context<Self>) -> Task<()> {
 506        let source = self.source.clone();
 507        let should_parse_links_only = self.options.parse_links_only;
 508        let language_registry = self.language_registry.clone();
 509        let fallback = self.fallback_code_block_language.clone();
 510
 511        let parsed = cx.background_spawn(async move {
 512            if should_parse_links_only {
 513                return (
 514                    ParsedMarkdown {
 515                        events: Arc::from(parse_links_only(source.as_ref())),
 516                        source,
 517                        languages_by_name: TreeMap::default(),
 518                        languages_by_path: TreeMap::default(),
 519                    },
 520                    Default::default(),
 521                );
 522            }
 523
 524            let (events, language_names, paths) = parse_markdown(&source);
 525            let mut images_by_source_offset = HashMap::default();
 526            let mut languages_by_name = TreeMap::default();
 527            let mut languages_by_path = TreeMap::default();
 528            if let Some(registry) = language_registry.as_ref() {
 529                for name in language_names {
 530                    let language = if !name.is_empty() {
 531                        registry.language_for_name_or_extension(&name).left_future()
 532                    } else if let Some(fallback) = &fallback {
 533                        registry.language_for_name(fallback.as_ref()).right_future()
 534                    } else {
 535                        continue;
 536                    };
 537                    if let Ok(language) = language.await {
 538                        languages_by_name.insert(name, language);
 539                    }
 540                }
 541
 542                for path in paths {
 543                    if let Ok(language) = registry
 544                        .load_language_for_file_path(Path::new(path.as_ref()))
 545                        .await
 546                    {
 547                        languages_by_path.insert(path, language);
 548                    }
 549                }
 550            }
 551
 552            for (range, event) in &events {
 553                if let MarkdownEvent::Start(MarkdownTag::Image { dest_url, .. }) = event
 554                    && let Some(data_url) = dest_url.strip_prefix("data:")
 555                {
 556                    let Some((mime_info, data)) = data_url.split_once(',') else {
 557                        continue;
 558                    };
 559                    let Some((mime_type, encoding)) = mime_info.split_once(';') else {
 560                        continue;
 561                    };
 562                    let Some(format) = ImageFormat::from_mime_type(mime_type) else {
 563                        continue;
 564                    };
 565                    let is_base64 = encoding == "base64";
 566                    if is_base64
 567                        && let Some(bytes) = base64::prelude::BASE64_STANDARD
 568                            .decode(data)
 569                            .log_with_level(Level::Debug)
 570                    {
 571                        let image = Arc::new(Image::from_bytes(format, bytes));
 572                        images_by_source_offset.insert(range.start, image);
 573                    }
 574                }
 575            }
 576
 577            (
 578                ParsedMarkdown {
 579                    source,
 580                    events: Arc::from(events),
 581                    languages_by_name,
 582                    languages_by_path,
 583                },
 584                images_by_source_offset,
 585            )
 586        });
 587
 588        cx.spawn(async move |this, cx| {
 589            let (parsed, images_by_source_offset) = parsed.await;
 590
 591            this.update(cx, |this, cx| {
 592                this.parsed_markdown = parsed;
 593                this.images_by_source_offset = images_by_source_offset;
 594                this.pending_parse.take();
 595                if this.should_reparse {
 596                    this.parse(cx);
 597                }
 598                cx.refresh_windows();
 599            })
 600            .ok();
 601        })
 602    }
 603}
 604
 605impl Focusable for Markdown {
 606    fn focus_handle(&self, _cx: &App) -> FocusHandle {
 607        self.focus_handle.clone()
 608    }
 609}
 610
 611#[derive(Debug, Default, Clone)]
 612enum SelectMode {
 613    #[default]
 614    Character,
 615    Word(Range<usize>),
 616    Line(Range<usize>),
 617    All,
 618}
 619
 620#[derive(Clone, Default)]
 621struct Selection {
 622    start: usize,
 623    end: usize,
 624    reversed: bool,
 625    pending: bool,
 626    mode: SelectMode,
 627}
 628
 629impl Selection {
 630    fn set_head(&mut self, head: usize, rendered_text: &RenderedText) {
 631        match &self.mode {
 632            SelectMode::Character => {
 633                if head < self.tail() {
 634                    if !self.reversed {
 635                        self.end = self.start;
 636                        self.reversed = true;
 637                    }
 638                    self.start = head;
 639                } else {
 640                    if self.reversed {
 641                        self.start = self.end;
 642                        self.reversed = false;
 643                    }
 644                    self.end = head;
 645                }
 646            }
 647            SelectMode::Word(original_range) | SelectMode::Line(original_range) => {
 648                let head_range = if matches!(self.mode, SelectMode::Word(_)) {
 649                    rendered_text.surrounding_word_range(head)
 650                } else {
 651                    rendered_text.surrounding_line_range(head)
 652                };
 653
 654                if head < original_range.start {
 655                    self.start = head_range.start;
 656                    self.end = original_range.end;
 657                    self.reversed = true;
 658                } else if head >= original_range.end {
 659                    self.start = original_range.start;
 660                    self.end = head_range.end;
 661                    self.reversed = false;
 662                } else {
 663                    self.start = original_range.start;
 664                    self.end = original_range.end;
 665                    self.reversed = false;
 666                }
 667            }
 668            SelectMode::All => {
 669                self.start = 0;
 670                self.end = rendered_text
 671                    .lines
 672                    .last()
 673                    .map(|line| line.source_end)
 674                    .unwrap_or(0);
 675                self.reversed = false;
 676            }
 677        }
 678    }
 679
 680    fn tail(&self) -> usize {
 681        if self.reversed { self.end } else { self.start }
 682    }
 683}
 684
 685#[derive(Debug, Clone, Default)]
 686pub struct ParsedMarkdown {
 687    pub source: SharedString,
 688    pub events: Arc<[(Range<usize>, MarkdownEvent)]>,
 689    pub languages_by_name: TreeMap<SharedString, Arc<Language>>,
 690    pub languages_by_path: TreeMap<Arc<str>, Arc<Language>>,
 691}
 692
 693impl ParsedMarkdown {
 694    pub fn source(&self) -> &SharedString {
 695        &self.source
 696    }
 697
 698    pub fn events(&self) -> &Arc<[(Range<usize>, MarkdownEvent)]> {
 699        &self.events
 700    }
 701}
 702
 703pub struct MarkdownElement {
 704    markdown: Entity<Markdown>,
 705    style: MarkdownStyle,
 706    code_block_renderer: CodeBlockRenderer,
 707    on_url_click: Option<Box<dyn Fn(SharedString, &mut Window, &mut App)>>,
 708    on_code_block_click: Option<Box<dyn Fn(SharedString, &mut Window, &mut App)>>,
 709    clickable_code_words: Option<Rc<HashSet<SharedString>>>,
 710}
 711
 712impl MarkdownElement {
 713    pub fn new(markdown: Entity<Markdown>, style: MarkdownStyle) -> Self {
 714        Self {
 715            markdown,
 716            style,
 717            code_block_renderer: CodeBlockRenderer::Default {
 718                copy_button: true,
 719                copy_button_on_hover: false,
 720                border: false,
 721            },
 722            on_url_click: None,
 723            on_code_block_click: None,
 724            clickable_code_words: None,
 725        }
 726    }
 727
 728    #[cfg(any(test, feature = "test-support"))]
 729    pub fn rendered_text(
 730        markdown: Entity<Markdown>,
 731        cx: &mut gpui::VisualTestContext,
 732        style: impl FnOnce(&Window, &App) -> MarkdownStyle,
 733    ) -> String {
 734        use gpui::size;
 735
 736        let (text, _) = cx.draw(
 737            Default::default(),
 738            size(px(600.0), px(600.0)),
 739            |window, cx| Self::new(markdown, style(window, cx)),
 740        );
 741        text.text
 742            .lines
 743            .iter()
 744            .map(|line| line.layout.wrapped_text())
 745            .collect::<Vec<_>>()
 746            .join("\n")
 747    }
 748
 749    pub fn code_block_renderer(mut self, variant: CodeBlockRenderer) -> Self {
 750        self.code_block_renderer = variant;
 751        self
 752    }
 753
 754    pub fn on_url_click(
 755        mut self,
 756        handler: impl Fn(SharedString, &mut Window, &mut App) + 'static,
 757    ) -> Self {
 758        self.on_url_click = Some(Box::new(handler));
 759        self
 760    }
 761
 762    pub fn on_code_block_click(
 763        mut self,
 764        handler: impl Fn(SharedString, &mut Window, &mut App) + 'static,
 765    ) -> Self {
 766        self.on_code_block_click = Some(Box::new(handler));
 767        self
 768    }
 769
 770    pub fn clickable_code_words(mut self, words: HashSet<SharedString>) -> Self {
 771        self.clickable_code_words = Some(Rc::new(words));
 772        self
 773    }
 774
 775    fn paint_selection(
 776        &self,
 777        bounds: Bounds<Pixels>,
 778        rendered_text: &RenderedText,
 779        window: &mut Window,
 780        cx: &mut App,
 781    ) {
 782        let selection = self.markdown.read(cx).selection.clone();
 783        let selection_start = rendered_text.position_for_source_index(selection.start);
 784        let selection_end = rendered_text.position_for_source_index(selection.end);
 785        if let Some(((start_position, start_line_height), (end_position, end_line_height))) =
 786            selection_start.zip(selection_end)
 787        {
 788            if start_position.y == end_position.y {
 789                window.paint_quad(quad(
 790                    Bounds::from_corners(
 791                        start_position,
 792                        point(end_position.x, end_position.y + end_line_height),
 793                    ),
 794                    Pixels::ZERO,
 795                    self.style.selection_background_color,
 796                    Edges::default(),
 797                    Hsla::transparent_black(),
 798                    BorderStyle::default(),
 799                ));
 800            } else {
 801                window.paint_quad(quad(
 802                    Bounds::from_corners(
 803                        start_position,
 804                        point(bounds.right(), start_position.y + start_line_height),
 805                    ),
 806                    Pixels::ZERO,
 807                    self.style.selection_background_color,
 808                    Edges::default(),
 809                    Hsla::transparent_black(),
 810                    BorderStyle::default(),
 811                ));
 812
 813                if end_position.y > start_position.y + start_line_height {
 814                    window.paint_quad(quad(
 815                        Bounds::from_corners(
 816                            point(bounds.left(), start_position.y + start_line_height),
 817                            point(bounds.right(), end_position.y),
 818                        ),
 819                        Pixels::ZERO,
 820                        self.style.selection_background_color,
 821                        Edges::default(),
 822                        Hsla::transparent_black(),
 823                        BorderStyle::default(),
 824                    ));
 825                }
 826
 827                window.paint_quad(quad(
 828                    Bounds::from_corners(
 829                        point(bounds.left(), end_position.y),
 830                        point(end_position.x, end_position.y + end_line_height),
 831                    ),
 832                    Pixels::ZERO,
 833                    self.style.selection_background_color,
 834                    Edges::default(),
 835                    Hsla::transparent_black(),
 836                    BorderStyle::default(),
 837                ));
 838            }
 839        }
 840    }
 841
 842    fn is_clickable_code_word(
 843        word: &SharedString,
 844        clickable_code_words: &Option<Rc<HashSet<SharedString>>>,
 845    ) -> bool {
 846        clickable_code_words
 847            .as_ref()
 848            .map_or(true, |set| set.contains(word))
 849    }
 850
 851    fn paint_mouse_listeners(
 852        &mut self,
 853        hitbox: &Hitbox,
 854        rendered_text: &RenderedText,
 855        window: &mut Window,
 856        cx: &mut App,
 857    ) {
 858        if self.style.prevent_mouse_interaction {
 859            return;
 860        }
 861
 862        let has_code_block_click = self.on_code_block_click.is_some();
 863        let clickable_code_words = self.clickable_code_words.take();
 864        let is_hovering_link = hitbox.is_hovered(window)
 865            && !self.markdown.read(cx).selection.pending
 866            && rendered_text
 867                .link_for_position(window.mouse_position())
 868                .is_some();
 869        let is_hovering_code_block_word = has_code_block_click
 870            && hitbox.is_hovered(window)
 871            && !self.markdown.read(cx).selection.pending
 872            && rendered_text
 873                .code_block_word_for_position(window.mouse_position())
 874                .filter(|word| Self::is_clickable_code_word(word, &clickable_code_words))
 875                .is_some();
 876
 877        if !self.style.prevent_mouse_interaction {
 878            if is_hovering_link || is_hovering_code_block_word {
 879                window.set_cursor_style(CursorStyle::PointingHand, hitbox);
 880            } else {
 881                window.set_cursor_style(CursorStyle::IBeam, hitbox);
 882            }
 883        }
 884
 885        let on_open_url = self.on_url_click.take();
 886        let on_code_block_click = self.on_code_block_click.take();
 887
 888        self.on_mouse_event(window, cx, {
 889            let hitbox = hitbox.clone();
 890            move |markdown, event: &MouseDownEvent, phase, window, _| {
 891                if phase.capture()
 892                    && event.button == MouseButton::Right
 893                    && hitbox.is_hovered(window)
 894                {
 895                    // Capture selected text so it survives until menu item is clicked
 896                    markdown.capture_selection_for_context_menu();
 897                }
 898            }
 899        });
 900
 901        self.on_mouse_event(window, cx, {
 902            let rendered_text = rendered_text.clone();
 903            let hitbox = hitbox.clone();
 904            let clickable_code_words = clickable_code_words.clone();
 905            move |markdown, event: &MouseDownEvent, phase, window, cx| {
 906                if hitbox.is_hovered(window) {
 907                    if phase.bubble() {
 908                        if let Some(link) = rendered_text.link_for_position(event.position) {
 909                            markdown.pressed_link = Some(link.clone());
 910                        } else if has_code_block_click
 911                            && let Some(word) = rendered_text
 912                                .code_block_word_for_position(event.position)
 913                                .filter(|word| {
 914                                    Self::is_clickable_code_word(word, &clickable_code_words)
 915                                })
 916                        {
 917                            markdown.pressed_code_block_word = Some(word);
 918                        } else {
 919                            let source_index =
 920                                match rendered_text.source_index_for_position(event.position) {
 921                                    Ok(ix) | Err(ix) => ix,
 922                                };
 923                            let (range, mode) = match event.click_count {
 924                                1 => {
 925                                    let range = source_index..source_index;
 926                                    (range, SelectMode::Character)
 927                                }
 928                                2 => {
 929                                    let range = rendered_text.surrounding_word_range(source_index);
 930                                    (range.clone(), SelectMode::Word(range))
 931                                }
 932                                3 => {
 933                                    let range = rendered_text.surrounding_line_range(source_index);
 934                                    (range.clone(), SelectMode::Line(range))
 935                                }
 936                                _ => {
 937                                    let range = 0..rendered_text
 938                                        .lines
 939                                        .last()
 940                                        .map(|line| line.source_end)
 941                                        .unwrap_or(0);
 942                                    (range, SelectMode::All)
 943                                }
 944                            };
 945                            markdown.selection = Selection {
 946                                start: range.start,
 947                                end: range.end,
 948                                reversed: false,
 949                                pending: true,
 950                                mode,
 951                            };
 952                            window.focus(&markdown.focus_handle, cx);
 953                        }
 954
 955                        window.prevent_default();
 956                        cx.notify();
 957                    }
 958                } else if phase.capture() && event.button == MouseButton::Left {
 959                    markdown.selection = Selection::default();
 960                    markdown.pressed_link = None;
 961                    markdown.pressed_code_block_word = None;
 962                    cx.notify();
 963                }
 964            }
 965        });
 966        self.on_mouse_event(window, cx, {
 967            let rendered_text = rendered_text.clone();
 968            let hitbox = hitbox.clone();
 969            let clickable_code_words = clickable_code_words.clone();
 970            let was_hovering_clickable = is_hovering_link || is_hovering_code_block_word;
 971            move |markdown, event: &MouseMoveEvent, phase, window, cx| {
 972                if phase.capture() {
 973                    return;
 974                }
 975
 976                if markdown.selection.pending {
 977                    let source_index = match rendered_text.source_index_for_position(event.position)
 978                    {
 979                        Ok(ix) | Err(ix) => ix,
 980                    };
 981                    markdown.selection.set_head(source_index, &rendered_text);
 982                    markdown.autoscroll_code_block(source_index, event.position);
 983                    markdown.autoscroll_request = Some(source_index);
 984                    cx.notify();
 985                } else {
 986                    let is_hovering_clickable = hitbox.is_hovered(window)
 987                        && (rendered_text.link_for_position(event.position).is_some()
 988                            || (has_code_block_click
 989                                && rendered_text
 990                                    .code_block_word_for_position(event.position)
 991                                    .filter(|word| {
 992                                        Self::is_clickable_code_word(word, &clickable_code_words)
 993                                    })
 994                                    .is_some()));
 995                    if is_hovering_clickable != was_hovering_clickable {
 996                        cx.notify();
 997                    }
 998                }
 999            }
1000        });
1001        self.on_mouse_event(window, cx, {
1002            let rendered_text = rendered_text.clone();
1003            move |markdown, event: &MouseUpEvent, phase, window, cx| {
1004                if phase.bubble() {
1005                    if let Some(pressed_link) = markdown.pressed_link.take()
1006                        && Some(&pressed_link) == rendered_text.link_for_position(event.position)
1007                    {
1008                        if let Some(open_url) = on_open_url.as_ref() {
1009                            open_url(pressed_link.destination_url, window, cx);
1010                        } else {
1011                            cx.open_url(&pressed_link.destination_url);
1012                        }
1013                    } else if let Some(pressed_word) = markdown.pressed_code_block_word.take()
1014                        && rendered_text
1015                            .code_block_word_for_position(event.position)
1016                            .filter(|word| {
1017                                Self::is_clickable_code_word(word, &clickable_code_words)
1018                            })
1019                            .as_ref()
1020                            == Some(&pressed_word)
1021                    {
1022                        if let Some(on_click) = on_code_block_click.as_ref() {
1023                            on_click(pressed_word, window, cx);
1024                        }
1025                    }
1026                } else if markdown.selection.pending {
1027                    markdown.selection.pending = false;
1028                    #[cfg(any(target_os = "linux", target_os = "freebsd"))]
1029                    {
1030                        let text = rendered_text
1031                            .text_for_range(markdown.selection.start..markdown.selection.end);
1032                        cx.write_to_primary(ClipboardItem::new_string(text))
1033                    }
1034                    cx.notify();
1035                }
1036            }
1037        });
1038    }
1039
1040    fn autoscroll(
1041        &self,
1042        rendered_text: &RenderedText,
1043        window: &mut Window,
1044        cx: &mut App,
1045    ) -> Option<()> {
1046        let autoscroll_index = self
1047            .markdown
1048            .update(cx, |markdown, _| markdown.autoscroll_request.take())?;
1049        let (position, line_height) = rendered_text.position_for_source_index(autoscroll_index)?;
1050
1051        let text_style = self.style.base_text_style.clone();
1052        let font_id = window.text_system().resolve_font(&text_style.font());
1053        let font_size = text_style.font_size.to_pixels(window.rem_size());
1054        let em_width = window.text_system().em_width(font_id, font_size).unwrap();
1055        window.request_autoscroll(Bounds::from_corners(
1056            point(position.x - 3. * em_width, position.y - 3. * line_height),
1057            point(position.x + 3. * em_width, position.y + 3. * line_height),
1058        ));
1059        Some(())
1060    }
1061
1062    fn on_mouse_event<T: MouseEvent>(
1063        &self,
1064        window: &mut Window,
1065        _cx: &mut App,
1066        mut f: impl 'static
1067        + FnMut(&mut Markdown, &T, DispatchPhase, &mut Window, &mut Context<Markdown>),
1068    ) {
1069        window.on_mouse_event({
1070            let markdown = self.markdown.downgrade();
1071            move |event, phase, window, cx| {
1072                markdown
1073                    .update(cx, |markdown, cx| f(markdown, event, phase, window, cx))
1074                    .log_err();
1075            }
1076        });
1077    }
1078}
1079
1080impl Styled for MarkdownElement {
1081    fn style(&mut self) -> &mut StyleRefinement {
1082        &mut self.style.container_style
1083    }
1084}
1085
1086impl Element for MarkdownElement {
1087    type RequestLayoutState = RenderedMarkdown;
1088    type PrepaintState = Hitbox;
1089
1090    fn id(&self) -> Option<ElementId> {
1091        None
1092    }
1093
1094    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
1095        None
1096    }
1097
1098    fn request_layout(
1099        &mut self,
1100        _id: Option<&GlobalElementId>,
1101        _inspector_id: Option<&gpui::InspectorElementId>,
1102        window: &mut Window,
1103        cx: &mut App,
1104    ) -> (gpui::LayoutId, Self::RequestLayoutState) {
1105        let mut builder = MarkdownElementBuilder::new(
1106            &self.style.container_style,
1107            self.style.base_text_style.clone(),
1108            self.style.syntax.clone(),
1109        );
1110        let (parsed_markdown, images) = {
1111            let markdown = self.markdown.read(cx);
1112            (
1113                markdown.parsed_markdown.clone(),
1114                markdown.images_by_source_offset.clone(),
1115            )
1116        };
1117        let markdown_end = if let Some(last) = parsed_markdown.events.last() {
1118            last.0.end
1119        } else {
1120            0
1121        };
1122        let mut code_block_ids = HashSet::default();
1123
1124        let mut current_img_block_range: Option<Range<usize>> = None;
1125        for (index, (range, event)) in parsed_markdown.events.iter().enumerate() {
1126            // Skip alt text for images that rendered
1127            if let Some(current_img_block_range) = &current_img_block_range
1128                && current_img_block_range.end > range.end
1129            {
1130                continue;
1131            }
1132
1133            match event {
1134                MarkdownEvent::Start(tag) => {
1135                    match tag {
1136                        MarkdownTag::Image { .. } => {
1137                            if let Some(image) = images.get(&range.start) {
1138                                current_img_block_range = Some(range.clone());
1139                                builder.modify_current_div(|el| {
1140                                    el.items_center()
1141                                        .flex()
1142                                        .flex_row()
1143                                        .child(img(image.clone()))
1144                                });
1145                            }
1146                        }
1147                        MarkdownTag::Paragraph => {
1148                            builder.push_div(
1149                                div().when(!self.style.height_is_multiple_of_line_height, |el| {
1150                                    el.mb_2().line_height(rems(1.3))
1151                                }),
1152                                range,
1153                                markdown_end,
1154                            );
1155                        }
1156                        MarkdownTag::Heading { level, .. } => {
1157                            let mut heading = div().mb_2();
1158
1159                            heading = apply_heading_style(
1160                                heading,
1161                                *level,
1162                                self.style.heading_level_styles.as_ref(),
1163                            );
1164
1165                            heading.style().refine(&self.style.heading);
1166
1167                            let text_style = self.style.heading.text_style().clone();
1168
1169                            builder.push_text_style(text_style);
1170                            builder.push_div(heading, range, markdown_end);
1171                        }
1172                        MarkdownTag::BlockQuote => {
1173                            builder.push_text_style(self.style.block_quote.clone());
1174                            builder.push_div(
1175                                div()
1176                                    .pl_4()
1177                                    .mb_2()
1178                                    .border_l_4()
1179                                    .border_color(self.style.block_quote_border_color),
1180                                range,
1181                                markdown_end,
1182                            );
1183                        }
1184                        MarkdownTag::CodeBlock { kind, .. } => {
1185                            let language = match kind {
1186                                CodeBlockKind::Fenced => None,
1187                                CodeBlockKind::FencedLang(language) => {
1188                                    parsed_markdown.languages_by_name.get(language).cloned()
1189                                }
1190                                CodeBlockKind::FencedSrc(path_range) => parsed_markdown
1191                                    .languages_by_path
1192                                    .get(&path_range.path)
1193                                    .cloned(),
1194                                _ => None,
1195                            };
1196
1197                            let is_indented = matches!(kind, CodeBlockKind::Indented);
1198                            let scroll_handle = if self.style.code_block_overflow_x_scroll {
1199                                code_block_ids.insert(range.start);
1200                                Some(self.markdown.update(cx, |markdown, _| {
1201                                    markdown.code_block_scroll_handle(range.start)
1202                                }))
1203                            } else {
1204                                None
1205                            };
1206
1207                            match (&self.code_block_renderer, is_indented) {
1208                                (CodeBlockRenderer::Default { .. }, _) | (_, true) => {
1209                                    // This is a parent container that we can position the copy button inside.
1210                                    let parent_container =
1211                                        div().group("code_block").relative().w_full();
1212
1213                                    let mut parent_container: AnyDiv = if let Some(scroll_handle) =
1214                                        scroll_handle.as_ref()
1215                                    {
1216                                        let scrollbars = Scrollbars::new(ScrollAxes::Horizontal)
1217                                            .id(("markdown-code-block-scrollbar", range.start))
1218                                            .tracked_scroll_handle(scroll_handle)
1219                                            .with_track_along(
1220                                                ScrollAxes::Horizontal,
1221                                                cx.theme().colors().editor_background,
1222                                            )
1223                                            .notify_content();
1224
1225                                        parent_container
1226                                            .rounded_lg()
1227                                            .custom_scrollbars(scrollbars, window, cx)
1228                                            .into()
1229                                    } else {
1230                                        parent_container.into()
1231                                    };
1232
1233                                    if let CodeBlockRenderer::Default { border: true, .. } =
1234                                        &self.code_block_renderer
1235                                    {
1236                                        parent_container = parent_container
1237                                            .rounded_md()
1238                                            .border_1()
1239                                            .border_color(cx.theme().colors().border_variant);
1240                                    }
1241
1242                                    parent_container.style().refine(&self.style.code_block);
1243                                    builder.push_div(parent_container, range, markdown_end);
1244
1245                                    let code_block = div()
1246                                        .id(("code-block", range.start))
1247                                        .rounded_lg()
1248                                        .map(|mut code_block| {
1249                                            if let Some(scroll_handle) = scroll_handle.as_ref() {
1250                                                code_block.style().restrict_scroll_to_axis =
1251                                                    Some(true);
1252                                                code_block
1253                                                    .flex()
1254                                                    .overflow_x_scroll()
1255                                                    .track_scroll(scroll_handle)
1256                                            } else {
1257                                                code_block.w_full()
1258                                            }
1259                                        });
1260
1261                                    builder.push_text_style(self.style.code_block.text.to_owned());
1262                                    builder.push_code_block(language);
1263                                    builder.push_div(code_block, range, markdown_end);
1264                                }
1265                                (CodeBlockRenderer::Custom { .. }, _) => {}
1266                            }
1267                        }
1268                        MarkdownTag::HtmlBlock => builder.push_div(div(), range, markdown_end),
1269                        MarkdownTag::List(bullet_index) => {
1270                            builder.push_list(*bullet_index);
1271                            builder.push_div(div().pl_2p5(), range, markdown_end);
1272                        }
1273                        MarkdownTag::Item => {
1274                            let bullet = if let Some((_, MarkdownEvent::TaskListMarker(checked))) =
1275                                parsed_markdown.events.get(index.saturating_add(1))
1276                            {
1277                                let source = &parsed_markdown.source()[range.clone()];
1278
1279                                Checkbox::new(
1280                                    ElementId::Name(source.to_string().into()),
1281                                    if *checked {
1282                                        ToggleState::Selected
1283                                    } else {
1284                                        ToggleState::Unselected
1285                                    },
1286                                )
1287                                .fill()
1288                                .visualization_only(true)
1289                                .into_any_element()
1290                            } else if let Some(bullet_index) = builder.next_bullet_index() {
1291                                div().child(format!("{}.", bullet_index)).into_any_element()
1292                            } else {
1293                                div().child("").into_any_element()
1294                            };
1295                            builder.push_div(
1296                                div()
1297                                    .when(!self.style.height_is_multiple_of_line_height, |el| {
1298                                        el.mb_1().gap_1().line_height(rems(1.3))
1299                                    })
1300                                    .h_flex()
1301                                    .items_start()
1302                                    .child(bullet),
1303                                range,
1304                                markdown_end,
1305                            );
1306                            // Without `w_0`, text doesn't wrap to the width of the container.
1307                            builder.push_div(div().flex_1().w_0(), range, markdown_end);
1308                        }
1309                        MarkdownTag::Emphasis => builder.push_text_style(TextStyleRefinement {
1310                            font_style: Some(FontStyle::Italic),
1311                            ..Default::default()
1312                        }),
1313                        MarkdownTag::Strong => builder.push_text_style(TextStyleRefinement {
1314                            font_weight: Some(FontWeight::BOLD),
1315                            ..Default::default()
1316                        }),
1317                        MarkdownTag::Strikethrough => {
1318                            builder.push_text_style(TextStyleRefinement {
1319                                strikethrough: Some(StrikethroughStyle {
1320                                    thickness: px(1.),
1321                                    color: None,
1322                                }),
1323                                ..Default::default()
1324                            })
1325                        }
1326                        MarkdownTag::Link { dest_url, .. } => {
1327                            if builder.code_block_stack.is_empty() {
1328                                builder.push_link(dest_url.clone(), range.clone());
1329                                let style = self
1330                                    .style
1331                                    .link_callback
1332                                    .as_ref()
1333                                    .and_then(|callback| callback(dest_url, cx))
1334                                    .unwrap_or_else(|| self.style.link.clone());
1335                                builder.push_text_style(style)
1336                            }
1337                        }
1338                        MarkdownTag::MetadataBlock(_) => {}
1339                        MarkdownTag::Table(alignments) => {
1340                            builder.table.start(alignments.clone());
1341
1342                            let column_count = alignments.len();
1343                            builder.push_div(
1344                                div()
1345                                    .id(("table", range.start))
1346                                    .grid()
1347                                    .grid_cols(column_count as u16)
1348                                    .when(self.style.table_columns_min_size, |this| {
1349                                        this.grid_cols_min_content(column_count as u16)
1350                                    })
1351                                    .when(!self.style.table_columns_min_size, |this| {
1352                                        this.grid_cols(column_count as u16)
1353                                    })
1354                                    .w_full()
1355                                    .mb_2()
1356                                    .border(px(1.5))
1357                                    .border_color(cx.theme().colors().border)
1358                                    .rounded_sm()
1359                                    .overflow_hidden(),
1360                                range,
1361                                markdown_end,
1362                            );
1363                        }
1364                        MarkdownTag::TableHead => {
1365                            builder.table.start_head();
1366                            builder.push_text_style(TextStyleRefinement {
1367                                font_weight: Some(FontWeight::SEMIBOLD),
1368                                ..Default::default()
1369                            });
1370                        }
1371                        MarkdownTag::TableRow => {
1372                            builder.table.start_row();
1373                        }
1374                        MarkdownTag::TableCell => {
1375                            let is_header = builder.table.in_head;
1376                            let row_index = builder.table.row_index;
1377                            let col_index = builder.table.col_index;
1378
1379                            builder.push_div(
1380                                div()
1381                                    .when(col_index > 0, |this| this.border_l_1())
1382                                    .when(row_index > 0, |this| this.border_t_1())
1383                                    .border_color(cx.theme().colors().border)
1384                                    .px_1()
1385                                    .py_0p5()
1386                                    .when(is_header, |this| {
1387                                        this.bg(cx.theme().colors().title_bar_background)
1388                                    })
1389                                    .when(!is_header && row_index % 2 == 1, |this| {
1390                                        this.bg(cx.theme().colors().panel_background)
1391                                    }),
1392                                range,
1393                                markdown_end,
1394                            );
1395                        }
1396                        _ => log::debug!("unsupported markdown tag {:?}", tag),
1397                    }
1398                }
1399                MarkdownEvent::End(tag) => match tag {
1400                    MarkdownTagEnd::Image => {
1401                        current_img_block_range.take();
1402                    }
1403                    MarkdownTagEnd::Paragraph => {
1404                        builder.pop_div();
1405                    }
1406                    MarkdownTagEnd::Heading(_) => {
1407                        builder.pop_div();
1408                        builder.pop_text_style()
1409                    }
1410                    MarkdownTagEnd::BlockQuote(_kind) => {
1411                        builder.pop_text_style();
1412                        builder.pop_div()
1413                    }
1414                    MarkdownTagEnd::CodeBlock => {
1415                        builder.trim_trailing_newline();
1416
1417                        builder.pop_div();
1418                        builder.pop_code_block();
1419                        builder.pop_text_style();
1420
1421                        if let CodeBlockRenderer::Default {
1422                            copy_button: true, ..
1423                        } = &self.code_block_renderer
1424                        {
1425                            builder.modify_current_div(|el| {
1426                                let content_range = parser::extract_code_block_content_range(
1427                                    &parsed_markdown.source()[range.clone()],
1428                                );
1429                                let content_range = content_range.start + range.start
1430                                    ..content_range.end + range.start;
1431
1432                                let code = parsed_markdown.source()[content_range].to_string();
1433                                let codeblock = render_copy_code_block_button(
1434                                    range.end,
1435                                    code,
1436                                    self.markdown.clone(),
1437                                );
1438                                el.child(
1439                                    h_flex()
1440                                        .w_4()
1441                                        .absolute()
1442                                        .top_1p5()
1443                                        .right_1p5()
1444                                        .justify_end()
1445                                        .child(codeblock),
1446                                )
1447                            });
1448                        }
1449
1450                        if let CodeBlockRenderer::Default {
1451                            copy_button_on_hover: true,
1452                            ..
1453                        } = &self.code_block_renderer
1454                        {
1455                            builder.modify_current_div(|el| {
1456                                let content_range = parser::extract_code_block_content_range(
1457                                    &parsed_markdown.source()[range.clone()],
1458                                );
1459                                let content_range = content_range.start + range.start
1460                                    ..content_range.end + range.start;
1461
1462                                let code = parsed_markdown.source()[content_range].to_string();
1463                                let codeblock = render_copy_code_block_button(
1464                                    range.end,
1465                                    code,
1466                                    self.markdown.clone(),
1467                                );
1468                                el.child(
1469                                    h_flex()
1470                                        .w_4()
1471                                        .absolute()
1472                                        .top_0()
1473                                        .right_0()
1474                                        .justify_end()
1475                                        .visible_on_hover("code_block")
1476                                        .child(codeblock),
1477                                )
1478                            });
1479                        }
1480
1481                        // Pop the parent container.
1482                        builder.pop_div();
1483                    }
1484                    MarkdownTagEnd::HtmlBlock => builder.pop_div(),
1485                    MarkdownTagEnd::List(_) => {
1486                        builder.pop_list();
1487                        builder.pop_div();
1488                    }
1489                    MarkdownTagEnd::Item => {
1490                        builder.pop_div();
1491                        builder.pop_div();
1492                    }
1493                    MarkdownTagEnd::Emphasis => builder.pop_text_style(),
1494                    MarkdownTagEnd::Strong => builder.pop_text_style(),
1495                    MarkdownTagEnd::Strikethrough => builder.pop_text_style(),
1496                    MarkdownTagEnd::Link => {
1497                        if builder.code_block_stack.is_empty() {
1498                            builder.pop_text_style()
1499                        }
1500                    }
1501                    MarkdownTagEnd::Table => {
1502                        builder.pop_div();
1503                        builder.table.end();
1504                    }
1505                    MarkdownTagEnd::TableHead => {
1506                        builder.pop_text_style();
1507                        builder.table.end_head();
1508                    }
1509                    MarkdownTagEnd::TableRow => {
1510                        builder.table.end_row();
1511                    }
1512                    MarkdownTagEnd::TableCell => {
1513                        builder.pop_div();
1514                        builder.table.end_cell();
1515                    }
1516                    _ => log::debug!("unsupported markdown tag end: {:?}", tag),
1517                },
1518                MarkdownEvent::Text => {
1519                    builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
1520                }
1521                MarkdownEvent::SubstitutedText(text) => {
1522                    builder.push_text(text, range.clone());
1523                }
1524                MarkdownEvent::Code => {
1525                    builder.push_text_style(self.style.inline_code.clone());
1526                    builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
1527                    builder.pop_text_style();
1528                }
1529                MarkdownEvent::Html => {
1530                    let html = &parsed_markdown.source[range.clone()];
1531                    if html.starts_with("<!--") {
1532                        builder.html_comment = true;
1533                    }
1534                    if html.trim_end().ends_with("-->") {
1535                        builder.html_comment = false;
1536                        continue;
1537                    }
1538                    if builder.html_comment {
1539                        continue;
1540                    }
1541                    builder.push_text(html, range.clone());
1542                }
1543                MarkdownEvent::InlineHtml => {
1544                    let html = &parsed_markdown.source[range.clone()];
1545                    if html.starts_with("<code>") {
1546                        builder.push_text_style(self.style.inline_code.clone());
1547                        continue;
1548                    }
1549                    if html.trim_end().starts_with("</code>") {
1550                        builder.pop_text_style();
1551                        continue;
1552                    }
1553                    builder.push_text(&parsed_markdown.source[range.clone()], range.clone());
1554                }
1555                MarkdownEvent::Rule => {
1556                    builder.push_div(
1557                        div()
1558                            .border_b_1()
1559                            .my_2()
1560                            .border_color(self.style.rule_color),
1561                        range,
1562                        markdown_end,
1563                    );
1564                    builder.pop_div()
1565                }
1566                MarkdownEvent::SoftBreak => builder.push_text(" ", range.clone()),
1567                MarkdownEvent::HardBreak => builder.push_text("\n", range.clone()),
1568                MarkdownEvent::TaskListMarker(_) => {
1569                    // handled inside the `MarkdownTag::Item` case
1570                }
1571                _ => log::debug!("unsupported markdown event {:?}", event),
1572            }
1573        }
1574        if self.style.code_block_overflow_x_scroll {
1575            let code_block_ids = code_block_ids;
1576            self.markdown.update(cx, move |markdown, _| {
1577                markdown.retain_code_block_scroll_handles(&code_block_ids);
1578            });
1579        } else {
1580            self.markdown
1581                .update(cx, |markdown, _| markdown.clear_code_block_scroll_handles());
1582        }
1583        let mut rendered_markdown = builder.build();
1584        let child_layout_id = rendered_markdown.element.request_layout(window, cx);
1585        let layout_id = window.request_layout(gpui::Style::default(), [child_layout_id], cx);
1586        (layout_id, rendered_markdown)
1587    }
1588
1589    fn prepaint(
1590        &mut self,
1591        _id: Option<&GlobalElementId>,
1592        _inspector_id: Option<&gpui::InspectorElementId>,
1593        bounds: Bounds<Pixels>,
1594        rendered_markdown: &mut Self::RequestLayoutState,
1595        window: &mut Window,
1596        cx: &mut App,
1597    ) -> Self::PrepaintState {
1598        let focus_handle = self.markdown.read(cx).focus_handle.clone();
1599        window.set_focus_handle(&focus_handle, cx);
1600        window.set_view_id(self.markdown.entity_id());
1601
1602        let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
1603        rendered_markdown.element.prepaint(window, cx);
1604        self.autoscroll(&rendered_markdown.text, window, cx);
1605        hitbox
1606    }
1607
1608    fn paint(
1609        &mut self,
1610        _id: Option<&GlobalElementId>,
1611        _inspector_id: Option<&gpui::InspectorElementId>,
1612        bounds: Bounds<Pixels>,
1613        rendered_markdown: &mut Self::RequestLayoutState,
1614        hitbox: &mut Self::PrepaintState,
1615        window: &mut Window,
1616        cx: &mut App,
1617    ) {
1618        let mut context = KeyContext::default();
1619        context.add("Markdown");
1620        window.set_key_context(context);
1621        window.on_action(std::any::TypeId::of::<crate::Copy>(), {
1622            let entity = self.markdown.clone();
1623            let text = rendered_markdown.text.clone();
1624            move |_, phase, window, cx| {
1625                let text = text.clone();
1626                if phase == DispatchPhase::Bubble {
1627                    entity.update(cx, move |this, cx| this.copy(&text, window, cx))
1628                }
1629            }
1630        });
1631        window.on_action(std::any::TypeId::of::<crate::CopyAsMarkdown>(), {
1632            let entity = self.markdown.clone();
1633            move |_, phase, window, cx| {
1634                if phase == DispatchPhase::Bubble {
1635                    entity.update(cx, move |this, cx| this.copy_as_markdown(window, cx))
1636                }
1637            }
1638        });
1639
1640        self.paint_mouse_listeners(hitbox, &rendered_markdown.text, window, cx);
1641        rendered_markdown.element.paint(window, cx);
1642        self.paint_selection(bounds, &rendered_markdown.text, window, cx);
1643    }
1644}
1645
1646fn apply_heading_style(
1647    mut heading: Div,
1648    level: pulldown_cmark::HeadingLevel,
1649    custom_styles: Option<&HeadingLevelStyles>,
1650) -> Div {
1651    heading = match level {
1652        pulldown_cmark::HeadingLevel::H1 => heading.text_3xl(),
1653        pulldown_cmark::HeadingLevel::H2 => heading.text_2xl(),
1654        pulldown_cmark::HeadingLevel::H3 => heading.text_xl(),
1655        pulldown_cmark::HeadingLevel::H4 => heading.text_lg(),
1656        pulldown_cmark::HeadingLevel::H5 => heading.text_base(),
1657        pulldown_cmark::HeadingLevel::H6 => heading.text_sm(),
1658    };
1659
1660    if let Some(styles) = custom_styles {
1661        let style_opt = match level {
1662            pulldown_cmark::HeadingLevel::H1 => &styles.h1,
1663            pulldown_cmark::HeadingLevel::H2 => &styles.h2,
1664            pulldown_cmark::HeadingLevel::H3 => &styles.h3,
1665            pulldown_cmark::HeadingLevel::H4 => &styles.h4,
1666            pulldown_cmark::HeadingLevel::H5 => &styles.h5,
1667            pulldown_cmark::HeadingLevel::H6 => &styles.h6,
1668        };
1669
1670        if let Some(style) = style_opt {
1671            heading.style().text = style.clone();
1672        }
1673    }
1674
1675    heading
1676}
1677
1678fn render_copy_code_block_button(
1679    id: usize,
1680    code: String,
1681    markdown: Entity<Markdown>,
1682) -> impl IntoElement {
1683    let id = ElementId::named_usize("copy-markdown-code", id);
1684
1685    CopyButton::new(id.clone(), code.clone()).custom_on_click({
1686        let markdown = markdown;
1687        move |_window, cx| {
1688            let id = id.clone();
1689            markdown.update(cx, |this, cx| {
1690                this.copied_code_blocks.insert(id.clone());
1691
1692                cx.write_to_clipboard(ClipboardItem::new_string(code.clone()));
1693
1694                cx.spawn(async move |this, cx| {
1695                    cx.background_executor().timer(Duration::from_secs(2)).await;
1696
1697                    cx.update(|cx| {
1698                        this.update(cx, |this, cx| {
1699                            this.copied_code_blocks.remove(&id);
1700                            cx.notify();
1701                        })
1702                    })
1703                    .ok();
1704                })
1705                .detach();
1706            });
1707        }
1708    })
1709}
1710
1711impl IntoElement for MarkdownElement {
1712    type Element = Self;
1713
1714    fn into_element(self) -> Self::Element {
1715        self
1716    }
1717}
1718
1719pub enum AnyDiv {
1720    Div(Div),
1721    Stateful(Stateful<Div>),
1722}
1723
1724impl AnyDiv {
1725    fn into_any_element(self) -> AnyElement {
1726        match self {
1727            Self::Div(div) => div.into_any_element(),
1728            Self::Stateful(div) => div.into_any_element(),
1729        }
1730    }
1731}
1732
1733impl From<Div> for AnyDiv {
1734    fn from(value: Div) -> Self {
1735        Self::Div(value)
1736    }
1737}
1738
1739impl From<Stateful<Div>> for AnyDiv {
1740    fn from(value: Stateful<Div>) -> Self {
1741        Self::Stateful(value)
1742    }
1743}
1744
1745impl Styled for AnyDiv {
1746    fn style(&mut self) -> &mut StyleRefinement {
1747        match self {
1748            Self::Div(div) => div.style(),
1749            Self::Stateful(div) => div.style(),
1750        }
1751    }
1752}
1753
1754impl ParentElement for AnyDiv {
1755    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
1756        match self {
1757            Self::Div(div) => div.extend(elements),
1758            Self::Stateful(div) => div.extend(elements),
1759        }
1760    }
1761}
1762
1763#[derive(Default)]
1764struct TableState {
1765    alignments: Vec<Alignment>,
1766    in_head: bool,
1767    row_index: usize,
1768    col_index: usize,
1769}
1770
1771impl TableState {
1772    fn start(&mut self, alignments: Vec<Alignment>) {
1773        self.alignments = alignments;
1774        self.in_head = false;
1775        self.row_index = 0;
1776        self.col_index = 0;
1777    }
1778
1779    fn end(&mut self) {
1780        self.alignments.clear();
1781        self.in_head = false;
1782        self.row_index = 0;
1783        self.col_index = 0;
1784    }
1785
1786    fn start_head(&mut self) {
1787        self.in_head = true;
1788    }
1789
1790    fn end_head(&mut self) {
1791        self.in_head = false;
1792    }
1793
1794    fn start_row(&mut self) {
1795        self.col_index = 0;
1796    }
1797
1798    fn end_row(&mut self) {
1799        self.row_index += 1;
1800    }
1801
1802    fn end_cell(&mut self) {
1803        self.col_index += 1;
1804    }
1805}
1806
1807struct MarkdownElementBuilder {
1808    div_stack: Vec<AnyDiv>,
1809    rendered_lines: Vec<RenderedLine>,
1810    pending_line: PendingLine,
1811    rendered_links: Vec<RenderedLink>,
1812    current_source_index: usize,
1813    html_comment: bool,
1814    base_text_style: TextStyle,
1815    text_style_stack: Vec<TextStyleRefinement>,
1816    code_block_stack: Vec<Option<Arc<Language>>>,
1817    list_stack: Vec<ListStackEntry>,
1818    table: TableState,
1819    syntax_theme: Arc<SyntaxTheme>,
1820}
1821
1822#[derive(Default)]
1823struct PendingLine {
1824    text: String,
1825    runs: Vec<TextRun>,
1826    source_mappings: Vec<SourceMapping>,
1827}
1828
1829struct ListStackEntry {
1830    bullet_index: Option<u64>,
1831}
1832
1833impl MarkdownElementBuilder {
1834    fn new(
1835        container_style: &StyleRefinement,
1836        base_text_style: TextStyle,
1837        syntax_theme: Arc<SyntaxTheme>,
1838    ) -> Self {
1839        Self {
1840            div_stack: vec![{
1841                let mut base_div = div();
1842                base_div.style().refine(container_style);
1843                base_div.debug_selector(|| "inner".into()).into()
1844            }],
1845            rendered_lines: Vec::new(),
1846            pending_line: PendingLine::default(),
1847            rendered_links: Vec::new(),
1848            current_source_index: 0,
1849            html_comment: false,
1850            base_text_style,
1851            text_style_stack: Vec::new(),
1852            code_block_stack: Vec::new(),
1853            list_stack: Vec::new(),
1854            table: TableState::default(),
1855            syntax_theme,
1856        }
1857    }
1858
1859    fn push_text_style(&mut self, style: TextStyleRefinement) {
1860        self.text_style_stack.push(style);
1861    }
1862
1863    fn text_style(&self) -> TextStyle {
1864        let mut style = self.base_text_style.clone();
1865        for refinement in &self.text_style_stack {
1866            style.refine(refinement);
1867        }
1868        style
1869    }
1870
1871    fn pop_text_style(&mut self) {
1872        self.text_style_stack.pop();
1873    }
1874
1875    fn push_div(&mut self, div: impl Into<AnyDiv>, range: &Range<usize>, markdown_end: usize) {
1876        let mut div = div.into();
1877        self.flush_text();
1878
1879        if range.start == 0 {
1880            // Remove the top margin on the first element.
1881            div.style().refine(&StyleRefinement {
1882                margin: gpui::EdgesRefinement {
1883                    top: Some(Length::Definite(px(0.).into())),
1884                    left: None,
1885                    right: None,
1886                    bottom: None,
1887                },
1888                ..Default::default()
1889            });
1890        }
1891
1892        if range.end == markdown_end {
1893            div.style().refine(&StyleRefinement {
1894                margin: gpui::EdgesRefinement {
1895                    top: None,
1896                    left: None,
1897                    right: None,
1898                    bottom: Some(Length::Definite(rems(0.).into())),
1899                },
1900                ..Default::default()
1901            });
1902        }
1903
1904        self.div_stack.push(div);
1905    }
1906
1907    fn modify_current_div(&mut self, f: impl FnOnce(AnyDiv) -> AnyDiv) {
1908        self.flush_text();
1909        if let Some(div) = self.div_stack.pop() {
1910            self.div_stack.push(f(div));
1911        }
1912    }
1913
1914    fn pop_div(&mut self) {
1915        self.flush_text();
1916        let div = self.div_stack.pop().unwrap().into_any_element();
1917        self.div_stack.last_mut().unwrap().extend(iter::once(div));
1918    }
1919
1920    fn push_list(&mut self, bullet_index: Option<u64>) {
1921        self.list_stack.push(ListStackEntry { bullet_index });
1922    }
1923
1924    fn next_bullet_index(&mut self) -> Option<u64> {
1925        self.list_stack.last_mut().and_then(|entry| {
1926            let item_index = entry.bullet_index.as_mut()?;
1927            *item_index += 1;
1928            Some(*item_index - 1)
1929        })
1930    }
1931
1932    fn pop_list(&mut self) {
1933        self.list_stack.pop();
1934    }
1935
1936    fn push_code_block(&mut self, language: Option<Arc<Language>>) {
1937        self.code_block_stack.push(language);
1938    }
1939
1940    fn pop_code_block(&mut self) {
1941        self.code_block_stack.pop();
1942    }
1943
1944    fn push_link(&mut self, destination_url: SharedString, source_range: Range<usize>) {
1945        self.rendered_links.push(RenderedLink {
1946            source_range,
1947            destination_url,
1948        });
1949    }
1950
1951    fn push_text(&mut self, text: &str, source_range: Range<usize>) {
1952        self.pending_line.source_mappings.push(SourceMapping {
1953            rendered_index: self.pending_line.text.len(),
1954            source_index: source_range.start,
1955        });
1956        self.pending_line.text.push_str(text);
1957        self.current_source_index = source_range.end;
1958
1959        if let Some(Some(language)) = self.code_block_stack.last() {
1960            let mut offset = 0;
1961            for (range, highlight_id) in language.highlight_text(&Rope::from(text), 0..text.len()) {
1962                if range.start > offset {
1963                    self.pending_line
1964                        .runs
1965                        .push(self.text_style().to_run(range.start - offset));
1966                }
1967
1968                let mut run_style = self.text_style();
1969                if let Some(highlight) = highlight_id.style(&self.syntax_theme) {
1970                    run_style = run_style.highlight(highlight);
1971                }
1972                self.pending_line.runs.push(run_style.to_run(range.len()));
1973                offset = range.end;
1974            }
1975
1976            if offset < text.len() {
1977                self.pending_line
1978                    .runs
1979                    .push(self.text_style().to_run(text.len() - offset));
1980            }
1981        } else {
1982            self.pending_line
1983                .runs
1984                .push(self.text_style().to_run(text.len()));
1985        }
1986    }
1987
1988    fn trim_trailing_newline(&mut self) {
1989        if self.pending_line.text.ends_with('\n') {
1990            self.pending_line
1991                .text
1992                .truncate(self.pending_line.text.len() - 1);
1993            self.pending_line.runs.last_mut().unwrap().len -= 1;
1994            self.current_source_index -= 1;
1995        }
1996    }
1997
1998    fn flush_text(&mut self) {
1999        let line = mem::take(&mut self.pending_line);
2000        if line.text.is_empty() {
2001            return;
2002        }
2003
2004        let text = StyledText::new(line.text).with_runs(line.runs);
2005        self.rendered_lines.push(RenderedLine {
2006            layout: text.layout().clone(),
2007            source_mappings: line.source_mappings,
2008            source_end: self.current_source_index,
2009            language: self.code_block_stack.last().cloned().flatten(),
2010        });
2011        self.div_stack.last_mut().unwrap().extend([text.into_any()]);
2012    }
2013
2014    fn build(mut self) -> RenderedMarkdown {
2015        debug_assert_eq!(self.div_stack.len(), 1);
2016        self.flush_text();
2017        RenderedMarkdown {
2018            element: self.div_stack.pop().unwrap().into_any_element(),
2019            text: RenderedText {
2020                lines: self.rendered_lines.into(),
2021                links: self.rendered_links.into(),
2022            },
2023        }
2024    }
2025}
2026
2027struct RenderedLine {
2028    layout: TextLayout,
2029    source_mappings: Vec<SourceMapping>,
2030    source_end: usize,
2031    language: Option<Arc<Language>>,
2032}
2033
2034impl RenderedLine {
2035    fn rendered_index_for_source_index(&self, source_index: usize) -> usize {
2036        if source_index >= self.source_end {
2037            return self.layout.len();
2038        }
2039
2040        let mapping = match self
2041            .source_mappings
2042            .binary_search_by_key(&source_index, |probe| probe.source_index)
2043        {
2044            Ok(ix) => &self.source_mappings[ix],
2045            Err(ix) => &self.source_mappings[ix - 1],
2046        };
2047        mapping.rendered_index + (source_index - mapping.source_index)
2048    }
2049
2050    fn source_index_for_rendered_index(&self, rendered_index: usize) -> usize {
2051        if rendered_index >= self.layout.len() {
2052            return self.source_end;
2053        }
2054
2055        let mapping = match self
2056            .source_mappings
2057            .binary_search_by_key(&rendered_index, |probe| probe.rendered_index)
2058        {
2059            Ok(ix) => &self.source_mappings[ix],
2060            Err(ix) => &self.source_mappings[ix - 1],
2061        };
2062        mapping.source_index + (rendered_index - mapping.rendered_index)
2063    }
2064
2065    /// Returns the source index for use as an exclusive range end at a word/selection boundary.
2066    /// When the rendered index is exactly at the start of a segment with a gap from the previous
2067    /// segment (e.g., after stripped markdown syntax like backticks), this returns the end of the
2068    /// previous segment rather than the start of the current one.
2069    fn source_index_for_exclusive_rendered_end(&self, rendered_index: usize) -> usize {
2070        if rendered_index >= self.layout.len() {
2071            return self.source_end;
2072        }
2073
2074        let ix = match self
2075            .source_mappings
2076            .binary_search_by_key(&rendered_index, |probe| probe.rendered_index)
2077        {
2078            Ok(ix) => ix,
2079            Err(ix) => {
2080                return self.source_mappings[ix - 1].source_index
2081                    + (rendered_index - self.source_mappings[ix - 1].rendered_index);
2082            }
2083        };
2084
2085        // Exact match at the start of a segment. Check if there's a gap from the previous segment.
2086        if ix > 0 {
2087            let prev_mapping = &self.source_mappings[ix - 1];
2088            let mapping = &self.source_mappings[ix];
2089            let prev_segment_len = mapping.rendered_index - prev_mapping.rendered_index;
2090            let prev_source_end = prev_mapping.source_index + prev_segment_len;
2091            if prev_source_end < mapping.source_index {
2092                return prev_source_end;
2093            }
2094        }
2095
2096        self.source_mappings[ix].source_index
2097    }
2098
2099    fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
2100        let line_rendered_index;
2101        let out_of_bounds;
2102        match self.layout.index_for_position(position) {
2103            Ok(ix) => {
2104                line_rendered_index = ix;
2105                out_of_bounds = false;
2106            }
2107            Err(ix) => {
2108                line_rendered_index = ix;
2109                out_of_bounds = true;
2110            }
2111        };
2112        let source_index = self.source_index_for_rendered_index(line_rendered_index);
2113        if out_of_bounds {
2114            Err(source_index)
2115        } else {
2116            Ok(source_index)
2117        }
2118    }
2119}
2120
2121#[derive(Copy, Clone, Debug, Default)]
2122struct SourceMapping {
2123    rendered_index: usize,
2124    source_index: usize,
2125}
2126
2127pub struct RenderedMarkdown {
2128    element: AnyElement,
2129    text: RenderedText,
2130}
2131
2132#[derive(Clone)]
2133struct RenderedText {
2134    lines: Rc<[RenderedLine]>,
2135    links: Rc<[RenderedLink]>,
2136}
2137
2138#[derive(Debug, Clone, Eq, PartialEq)]
2139struct RenderedLink {
2140    source_range: Range<usize>,
2141    destination_url: SharedString,
2142}
2143
2144impl RenderedText {
2145    fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
2146        let mut lines = self.lines.iter().peekable();
2147        let mut fallback_line: Option<&RenderedLine> = None;
2148
2149        while let Some(line) = lines.next() {
2150            let line_bounds = line.layout.bounds();
2151
2152            // Exact match: position is within bounds (handles overlapping bounds like table columns)
2153            if line_bounds.contains(&position) {
2154                return line.source_index_for_position(position);
2155            }
2156
2157            // Track fallback for Y-coordinate based matching
2158            if position.y <= line_bounds.bottom() && fallback_line.is_none() {
2159                fallback_line = Some(line);
2160            }
2161
2162            // Handle gap between lines
2163            if position.y > line_bounds.bottom() {
2164                if let Some(next_line) = lines.peek()
2165                    && position.y < next_line.layout.bounds().top()
2166                {
2167                    return Err(line.source_end);
2168                }
2169            }
2170        }
2171
2172        // Fall back to Y-coordinate matched line
2173        if let Some(line) = fallback_line {
2174            return line.source_index_for_position(position);
2175        }
2176
2177        Err(self.lines.last().map_or(0, |line| line.source_end))
2178    }
2179
2180    fn position_for_source_index(&self, source_index: usize) -> Option<(Point<Pixels>, Pixels)> {
2181        for line in self.lines.iter() {
2182            let line_source_start = line.source_mappings.first().unwrap().source_index;
2183            if source_index < line_source_start {
2184                break;
2185            } else if source_index > line.source_end {
2186                continue;
2187            } else {
2188                let line_height = line.layout.line_height();
2189                let rendered_index_within_line = line.rendered_index_for_source_index(source_index);
2190                let position = line.layout.position_for_index(rendered_index_within_line)?;
2191                return Some((position, line_height));
2192            }
2193        }
2194        None
2195    }
2196
2197    fn surrounding_word_range(&self, source_index: usize) -> Range<usize> {
2198        for line in self.lines.iter() {
2199            if source_index > line.source_end {
2200                continue;
2201            }
2202
2203            let line_rendered_start = line.source_mappings.first().unwrap().rendered_index;
2204            let rendered_index_in_line =
2205                line.rendered_index_for_source_index(source_index) - line_rendered_start;
2206            let text = line.layout.text();
2207
2208            let scope = line.language.as_ref().map(|l| l.default_scope());
2209            let classifier = CharClassifier::new(scope);
2210
2211            let mut prev_chars = text[..rendered_index_in_line].chars().rev().peekable();
2212            let mut next_chars = text[rendered_index_in_line..].chars().peekable();
2213
2214            let word_kind = std::cmp::max(
2215                prev_chars.peek().map(|&c| classifier.kind(c)),
2216                next_chars.peek().map(|&c| classifier.kind(c)),
2217            );
2218
2219            let mut start = rendered_index_in_line;
2220            for c in prev_chars {
2221                if Some(classifier.kind(c)) == word_kind {
2222                    start -= c.len_utf8();
2223                } else {
2224                    break;
2225                }
2226            }
2227
2228            let mut end = rendered_index_in_line;
2229            for c in next_chars {
2230                if Some(classifier.kind(c)) == word_kind {
2231                    end += c.len_utf8();
2232                } else {
2233                    break;
2234                }
2235            }
2236
2237            return line.source_index_for_rendered_index(line_rendered_start + start)
2238                ..line.source_index_for_exclusive_rendered_end(line_rendered_start + end);
2239        }
2240
2241        source_index..source_index
2242    }
2243
2244    fn surrounding_line_range(&self, source_index: usize) -> Range<usize> {
2245        for line in self.lines.iter() {
2246            if source_index > line.source_end {
2247                continue;
2248            }
2249            let line_source_start = line.source_mappings.first().unwrap().source_index;
2250            return line_source_start..line.source_end;
2251        }
2252
2253        source_index..source_index
2254    }
2255
2256    fn text_for_range(&self, range: Range<usize>) -> String {
2257        let mut accumulator = String::new();
2258
2259        for line in self.lines.iter() {
2260            if range.start > line.source_end {
2261                continue;
2262            }
2263            let line_source_start = line.source_mappings.first().unwrap().source_index;
2264            if range.end < line_source_start {
2265                break;
2266            }
2267
2268            let text = line.layout.text();
2269
2270            let start = if range.start < line_source_start {
2271                0
2272            } else {
2273                line.rendered_index_for_source_index(range.start)
2274            };
2275            let end = if range.end > line.source_end {
2276                line.rendered_index_for_source_index(line.source_end)
2277            } else {
2278                line.rendered_index_for_source_index(range.end)
2279            }
2280            .min(text.len());
2281
2282            accumulator.push_str(&text[start..end]);
2283            accumulator.push('\n');
2284        }
2285        // Remove trailing newline
2286        accumulator.pop();
2287        accumulator
2288    }
2289
2290    fn link_for_position(&self, position: Point<Pixels>) -> Option<&RenderedLink> {
2291        let source_index = self.source_index_for_position(position).ok()?;
2292        self.links
2293            .iter()
2294            .find(|link| link.source_range.contains(&source_index))
2295    }
2296
2297    fn code_block_word_for_position(&self, position: Point<Pixels>) -> Option<SharedString> {
2298        let source_index = self.source_index_for_position(position).ok()?;
2299
2300        for line in self.lines.iter() {
2301            if source_index > line.source_end {
2302                continue;
2303            }
2304
2305            // Only return words for code block lines
2306            if line.language.is_none() {
2307                return None;
2308            }
2309
2310            let line_rendered_start = line.source_mappings.first()?.rendered_index;
2311            let rendered_index_in_line =
2312                line.rendered_index_for_source_index(source_index) - line_rendered_start;
2313            let text = line.layout.text();
2314
2315            let scope = line.language.as_ref().map(|l| l.default_scope());
2316            let classifier = CharClassifier::new(scope);
2317
2318            // Check that we're on a word character
2319            let char_at_cursor = text[rendered_index_in_line..].chars().next()?;
2320            if classifier.kind(char_at_cursor) != CharKind::Word {
2321                return None;
2322            }
2323
2324            // Find word boundaries
2325            let mut start = rendered_index_in_line;
2326            for c in text[..rendered_index_in_line].chars().rev() {
2327                if classifier.kind(c) == CharKind::Word {
2328                    start -= c.len_utf8();
2329                } else {
2330                    break;
2331                }
2332            }
2333
2334            let mut end = rendered_index_in_line;
2335            for c in text[rendered_index_in_line..].chars() {
2336                if classifier.kind(c) == CharKind::Word {
2337                    end += c.len_utf8();
2338                } else {
2339                    break;
2340                }
2341            }
2342
2343            let word = &text[start..end];
2344            if word.is_empty() {
2345                return None;
2346            }
2347
2348            return Some(SharedString::from(word.to_string()));
2349        }
2350
2351        None
2352    }
2353}
2354
2355#[cfg(test)]
2356mod tests {
2357    use super::*;
2358    use gpui::{TestAppContext, size};
2359    use language::{Language, LanguageConfig, LanguageMatcher};
2360    use std::sync::Arc;
2361
2362    fn ensure_theme_initialized(cx: &mut TestAppContext) {
2363        cx.update(|cx| {
2364            if !cx.has_global::<settings::SettingsStore>() {
2365                settings::init(cx);
2366            }
2367            if !cx.has_global::<theme::GlobalTheme>() {
2368                theme::init(theme::LoadThemes::JustBase, cx);
2369            }
2370        });
2371    }
2372
2373    #[gpui::test]
2374    fn test_mappings(cx: &mut TestAppContext) {
2375        // Formatting.
2376        assert_mappings(
2377            &render_markdown("He*l*lo", cx),
2378            vec![vec![(0, 0), (1, 1), (2, 3), (3, 5), (4, 6), (5, 7)]],
2379        );
2380
2381        // Multiple lines.
2382        assert_mappings(
2383            &render_markdown("Hello\n\nWorld", cx),
2384            vec![
2385                vec![(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5)],
2386                vec![(0, 7), (1, 8), (2, 9), (3, 10), (4, 11), (5, 12)],
2387            ],
2388        );
2389
2390        // Multi-byte characters.
2391        assert_mappings(
2392            &render_markdown("αβγ\n\nδεζ", cx),
2393            vec![
2394                vec![(0, 0), (2, 2), (4, 4), (6, 6)],
2395                vec![(0, 8), (2, 10), (4, 12), (6, 14)],
2396            ],
2397        );
2398
2399        // Smart quotes.
2400        assert_mappings(&render_markdown("\"", cx), vec![vec![(0, 0), (3, 1)]]);
2401        assert_mappings(
2402            &render_markdown("\"hey\"", cx),
2403            vec![vec![(0, 0), (3, 1), (4, 2), (5, 3), (6, 4), (9, 5)]],
2404        );
2405
2406        // HTML Comments are ignored
2407        assert_mappings(
2408            &render_markdown(
2409                "<!--\nrdoc-file=string.c\n- str.intern   -> symbol\n- str.to_sym   -> symbol\n-->\nReturns",
2410                cx,
2411            ),
2412            vec![vec![
2413                (0, 78),
2414                (1, 79),
2415                (2, 80),
2416                (3, 81),
2417                (4, 82),
2418                (5, 83),
2419                (6, 84),
2420            ]],
2421        );
2422    }
2423
2424    fn render_markdown(markdown: &str, cx: &mut TestAppContext) -> RenderedText {
2425        render_markdown_with_language_registry(markdown, None, cx)
2426    }
2427
2428    fn render_markdown_with_language_registry(
2429        markdown: &str,
2430        language_registry: Option<Arc<LanguageRegistry>>,
2431        cx: &mut TestAppContext,
2432    ) -> RenderedText {
2433        struct TestWindow;
2434
2435        impl Render for TestWindow {
2436            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2437                div()
2438            }
2439        }
2440
2441        ensure_theme_initialized(cx);
2442
2443        let (_, cx) = cx.add_window_view(|_, _| TestWindow);
2444        let markdown =
2445            cx.new(|cx| Markdown::new(markdown.to_string().into(), language_registry, None, cx));
2446        cx.run_until_parked();
2447        let (rendered, _) = cx.draw(
2448            Default::default(),
2449            size(px(600.0), px(600.0)),
2450            |_window, _cx| {
2451                MarkdownElement::new(markdown, MarkdownStyle::default()).code_block_renderer(
2452                    CodeBlockRenderer::Default {
2453                        copy_button: false,
2454                        copy_button_on_hover: false,
2455                        border: false,
2456                    },
2457                )
2458            },
2459        );
2460        rendered.text
2461    }
2462
2463    #[gpui::test]
2464    fn test_surrounding_word_range(cx: &mut TestAppContext) {
2465        let rendered = render_markdown("Hello world tesεζ", cx);
2466
2467        // Test word selection for "Hello"
2468        let word_range = rendered.surrounding_word_range(2); // Simulate click on 'l' in "Hello"
2469        let selected_text = rendered.text_for_range(word_range);
2470        assert_eq!(selected_text, "Hello");
2471
2472        // Test word selection for "world"
2473        let word_range = rendered.surrounding_word_range(7); // Simulate click on 'o' in "world"
2474        let selected_text = rendered.text_for_range(word_range);
2475        assert_eq!(selected_text, "world");
2476
2477        // Test word selection for "tesεζ"
2478        let word_range = rendered.surrounding_word_range(14); // Simulate click on 's' in "tesεζ"
2479        let selected_text = rendered.text_for_range(word_range);
2480        assert_eq!(selected_text, "tesεζ");
2481
2482        // Test word selection at word boundary (space)
2483        let word_range = rendered.surrounding_word_range(5); // Simulate click on space between "Hello" and "world", expect highlighting word to the left
2484        let selected_text = rendered.text_for_range(word_range);
2485        assert_eq!(selected_text, "Hello");
2486    }
2487
2488    #[gpui::test]
2489    fn test_surrounding_line_range(cx: &mut TestAppContext) {
2490        let rendered = render_markdown("First line\n\nSecond line\n\nThird lineεζ", cx);
2491
2492        // Test getting line range for first line
2493        let line_range = rendered.surrounding_line_range(5); // Simulate click somewhere in first line
2494        let selected_text = rendered.text_for_range(line_range);
2495        assert_eq!(selected_text, "First line");
2496
2497        // Test getting line range for second line
2498        let line_range = rendered.surrounding_line_range(13); // Simulate click at beginning in second line
2499        let selected_text = rendered.text_for_range(line_range);
2500        assert_eq!(selected_text, "Second line");
2501
2502        // Test getting line range for third line
2503        let line_range = rendered.surrounding_line_range(37); // Simulate click at end of third line with multi-byte chars
2504        let selected_text = rendered.text_for_range(line_range);
2505        assert_eq!(selected_text, "Third lineεζ");
2506    }
2507
2508    #[gpui::test]
2509    fn test_selection_head_movement(cx: &mut TestAppContext) {
2510        let rendered = render_markdown("Hello world test", cx);
2511
2512        let mut selection = Selection {
2513            start: 5,
2514            end: 5,
2515            reversed: false,
2516            pending: false,
2517            mode: SelectMode::Character,
2518        };
2519
2520        // Test forward selection
2521        selection.set_head(10, &rendered);
2522        assert_eq!(selection.start, 5);
2523        assert_eq!(selection.end, 10);
2524        assert!(!selection.reversed);
2525        assert_eq!(selection.tail(), 5);
2526
2527        // Test backward selection
2528        selection.set_head(2, &rendered);
2529        assert_eq!(selection.start, 2);
2530        assert_eq!(selection.end, 5);
2531        assert!(selection.reversed);
2532        assert_eq!(selection.tail(), 5);
2533
2534        // Test forward selection again from reversed state
2535        selection.set_head(15, &rendered);
2536        assert_eq!(selection.start, 5);
2537        assert_eq!(selection.end, 15);
2538        assert!(!selection.reversed);
2539        assert_eq!(selection.tail(), 5);
2540    }
2541
2542    #[gpui::test]
2543    fn test_word_selection_drag(cx: &mut TestAppContext) {
2544        let rendered = render_markdown("Hello world test", cx);
2545
2546        // Start with a simulated double-click on "world" (index 6-10)
2547        let word_range = rendered.surrounding_word_range(7); // Click on 'o' in "world"
2548        let mut selection = Selection {
2549            start: word_range.start,
2550            end: word_range.end,
2551            reversed: false,
2552            pending: true,
2553            mode: SelectMode::Word(word_range),
2554        };
2555
2556        // Drag forward to "test" - should expand selection to include "test"
2557        selection.set_head(13, &rendered); // Index in "test"
2558        assert_eq!(selection.start, 6); // Start of "world"
2559        assert_eq!(selection.end, 16); // End of "test"
2560        assert!(!selection.reversed);
2561        let selected_text = rendered.text_for_range(selection.start..selection.end);
2562        assert_eq!(selected_text, "world test");
2563
2564        // Drag backward to "Hello" - should expand selection to include "Hello"
2565        selection.set_head(2, &rendered); // Index in "Hello"
2566        assert_eq!(selection.start, 0); // Start of "Hello"
2567        assert_eq!(selection.end, 11); // End of "world" (original selection)
2568        assert!(selection.reversed);
2569        let selected_text = rendered.text_for_range(selection.start..selection.end);
2570        assert_eq!(selected_text, "Hello world");
2571
2572        // Drag back within original word - should revert to original selection
2573        selection.set_head(8, &rendered); // Back within "world"
2574        assert_eq!(selection.start, 6); // Start of "world"
2575        assert_eq!(selection.end, 11); // End of "world"
2576        assert!(!selection.reversed);
2577        let selected_text = rendered.text_for_range(selection.start..selection.end);
2578        assert_eq!(selected_text, "world");
2579    }
2580
2581    #[gpui::test]
2582    fn test_selection_with_markdown_formatting(cx: &mut TestAppContext) {
2583        let rendered = render_markdown(
2584            "This is **bold** text, this is *italic* text, use `code` here",
2585            cx,
2586        );
2587        let word_range = rendered.surrounding_word_range(10); // Inside "bold"
2588        let selected_text = rendered.text_for_range(word_range);
2589        assert_eq!(selected_text, "bold");
2590
2591        let word_range = rendered.surrounding_word_range(32); // Inside "italic"
2592        let selected_text = rendered.text_for_range(word_range);
2593        assert_eq!(selected_text, "italic");
2594
2595        let word_range = rendered.surrounding_word_range(51); // Inside "code"
2596        let selected_text = rendered.text_for_range(word_range);
2597        assert_eq!(selected_text, "code");
2598    }
2599
2600    #[gpui::test]
2601    fn test_table_column_selection(cx: &mut TestAppContext) {
2602        let rendered = render_markdown("| a | b |\n|---|---|\n| c | d |", cx);
2603
2604        assert!(rendered.lines.len() >= 2);
2605        let first_bounds = rendered.lines[0].layout.bounds();
2606        let second_bounds = rendered.lines[1].layout.bounds();
2607
2608        let first_index = match rendered.source_index_for_position(first_bounds.center()) {
2609            Ok(index) | Err(index) => index,
2610        };
2611        let second_index = match rendered.source_index_for_position(second_bounds.center()) {
2612            Ok(index) | Err(index) => index,
2613        };
2614
2615        let first_word = rendered.text_for_range(rendered.surrounding_word_range(first_index));
2616        let second_word = rendered.text_for_range(rendered.surrounding_word_range(second_index));
2617
2618        assert_eq!(first_word, "a");
2619        assert_eq!(second_word, "b");
2620    }
2621
2622    #[gpui::test]
2623    fn test_inline_code_word_selection_excludes_backticks(cx: &mut TestAppContext) {
2624        // Test that double-clicking on inline code selects just the code content,
2625        // not the backticks. This verifies the fix for the bug where selecting
2626        // inline code would include the trailing backtick.
2627        let rendered = render_markdown("use `blah` here", cx);
2628
2629        // Source layout: "use `blah` here"
2630        //                 0123456789...
2631        // The inline code "blah" is at source positions 5-8 (content range 5..9)
2632
2633        // Click inside "blah" - should select just "blah", not "blah`"
2634        let word_range = rendered.surrounding_word_range(6); // 'l' in "blah"
2635
2636        // text_for_range extracts from the rendered text (without backticks), so it
2637        // would return "blah" even with a wrong source range. We check it anyway.
2638        let selected_text = rendered.text_for_range(word_range.clone());
2639        assert_eq!(selected_text, "blah");
2640
2641        // The source range is what matters for copy_as_markdown and selected_text,
2642        // which extract directly from the source. With the bug, this would be 5..10
2643        // which includes the closing backtick at position 9.
2644        assert_eq!(word_range, 5..9);
2645    }
2646
2647    #[gpui::test]
2648    fn test_surrounding_word_range_respects_word_characters(cx: &mut TestAppContext) {
2649        let rendered = render_markdown("foo.bar() baz", cx);
2650
2651        // Double clicking on 'f' in "foo" - should select just "foo"
2652        let word_range = rendered.surrounding_word_range(0);
2653        let selected_text = rendered.text_for_range(word_range);
2654        assert_eq!(selected_text, "foo");
2655
2656        // Double clicking on 'b' in "bar" - should select just "bar"
2657        let word_range = rendered.surrounding_word_range(4);
2658        let selected_text = rendered.text_for_range(word_range);
2659        assert_eq!(selected_text, "bar");
2660
2661        // Double clicking on 'b' in "baz" - should select "baz"
2662        let word_range = rendered.surrounding_word_range(10);
2663        let selected_text = rendered.text_for_range(word_range);
2664        assert_eq!(selected_text, "baz");
2665
2666        // Double clicking selects word characters in code blocks
2667        let javascript_language = Arc::new(Language::new(
2668            LanguageConfig {
2669                name: "JavaScript".into(),
2670                matcher: LanguageMatcher {
2671                    path_suffixes: vec!["js".to_string()],
2672                    ..Default::default()
2673                },
2674                word_characters: ['$', '#'].into_iter().collect(),
2675                ..Default::default()
2676            },
2677            None,
2678        ));
2679
2680        let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
2681        language_registry.add(javascript_language);
2682
2683        let rendered = render_markdown_with_language_registry(
2684            "```javascript\n$foo #bar\n```",
2685            Some(language_registry),
2686            cx,
2687        );
2688
2689        let word_range = rendered.surrounding_word_range(14);
2690        let selected_text = rendered.text_for_range(word_range);
2691        assert_eq!(selected_text, "$foo");
2692
2693        let word_range = rendered.surrounding_word_range(19);
2694        let selected_text = rendered.text_for_range(word_range);
2695        assert_eq!(selected_text, "#bar");
2696    }
2697
2698    #[gpui::test]
2699    fn test_all_selection(cx: &mut TestAppContext) {
2700        let rendered = render_markdown("Hello world\n\nThis is a test\n\nwith multiple lines", cx);
2701
2702        let total_length = rendered
2703            .lines
2704            .last()
2705            .map(|line| line.source_end)
2706            .unwrap_or(0);
2707
2708        let mut selection = Selection {
2709            start: 0,
2710            end: total_length,
2711            reversed: false,
2712            pending: true,
2713            mode: SelectMode::All,
2714        };
2715
2716        selection.set_head(5, &rendered); // Try to set head in middle
2717        assert_eq!(selection.start, 0);
2718        assert_eq!(selection.end, total_length);
2719        assert!(!selection.reversed);
2720
2721        selection.set_head(25, &rendered); // Try to set head near end
2722        assert_eq!(selection.start, 0);
2723        assert_eq!(selection.end, total_length);
2724        assert!(!selection.reversed);
2725
2726        let selected_text = rendered.text_for_range(selection.start..selection.end);
2727        assert_eq!(
2728            selected_text,
2729            "Hello world\nThis is a test\nwith multiple lines"
2730        );
2731    }
2732
2733    #[test]
2734    fn test_escape() {
2735        assert_eq!(Markdown::escape("hello `world`"), "hello \\`world\\`");
2736        assert_eq!(
2737            Markdown::escape("hello\n    cool world"),
2738            "hello\n\ncool world"
2739        );
2740    }
2741
2742    #[track_caller]
2743    fn assert_mappings(rendered: &RenderedText, expected: Vec<Vec<(usize, usize)>>) {
2744        assert_eq!(rendered.lines.len(), expected.len(), "line count mismatch");
2745        for (line_ix, line_mappings) in expected.into_iter().enumerate() {
2746            let line = &rendered.lines[line_ix];
2747
2748            assert!(
2749                line.source_mappings.windows(2).all(|mappings| {
2750                    mappings[0].source_index < mappings[1].source_index
2751                        && mappings[0].rendered_index < mappings[1].rendered_index
2752                }),
2753                "line {} has duplicate mappings: {:?}",
2754                line_ix,
2755                line.source_mappings
2756            );
2757
2758            for (rendered_ix, source_ix) in line_mappings {
2759                assert_eq!(
2760                    line.source_index_for_rendered_index(rendered_ix),
2761                    source_ix,
2762                    "line {}, rendered_ix {}",
2763                    line_ix,
2764                    rendered_ix
2765                );
2766
2767                assert_eq!(
2768                    line.rendered_index_for_source_index(source_ix),
2769                    rendered_ix,
2770                    "line {}, source_ix {}",
2771                    line_ix,
2772                    source_ix
2773                );
2774            }
2775        }
2776    }
2777}