Skip to main content

gpui_base/text/
node.rs

1use std::{
2    collections::HashMap,
3    ops::Range,
4    sync::{Arc, Mutex, OnceLock, Weak},
5};
6
7use gpui::{
8    AnyElement, App, Axis, DefiniteLength, Div, ElementId, FontStyle, FontWeight, HighlightStyle,
9    Hsla, Image, ImageFormat, ImageSource, InteractiveElement as _, IntoElement, IsZero as _,
10    Length, ObjectFit, Overflow, ParentElement, Pixels, Rems, ScrollHandle, SharedString,
11    SharedUri, StatefulInteractiveElement, StyleRefinement, Styled, StyledImage as _, WhiteSpace,
12    Window, div, img, prelude::FluentBuilder as _, px, relative, rems,
13};
14use markdown::mdast;
15
16use crate::{
17    ScrollableMask, Scrollbar, StyledExt, h_flex,
18    scrollable_mask::horizontal_scroll_area,
19    text::{
20        CodeBlockActionsFn, CodeBlockHighlighterFn, LinkClickHandlerFn, MarkdownExtensions,
21        MarkdownNode, TableActionsFn,
22        document::NodeRenderOptions,
23        inline::{
24            Inline, InlineHighlight, InlineState, combine_highlights, fade_highlights, text_runs,
25            text_size_ranges,
26        },
27        inline_flow::{InlineFlow, InlineFlowItem, slice_ranges},
28        range_highlight::{RangeHighlightFrame, RevealAt, RevealRequest},
29        stream_fade::{StreamFadeFrame, TextLeafKey},
30        text_view::handle_link_click,
31    },
32    theme::ActiveTheme as _,
33};
34
35use super::{
36    SelectionFormat, TextViewStyle,
37    utils::{data_url_image, list_item_prefix, ordered_list_ordinal},
38};
39
40const CHECK_SVG_LIGHT: &[u8] = br#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none"><path d="m3.25 8.25 3 3 6.5-7" stroke="white" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>"#;
41const CHECK_SVG_DARK: &[u8] = br#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none"><path d="m3.25 8.25 3 3 6.5-7" stroke="black" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>"#;
42
43/// The block-level nodes.
44#[derive(Debug, Clone, PartialEq)]
45pub(crate) enum BlockNode {
46    /// Something like a Div container in HTML.
47    Root {
48        children: Vec<BlockNode>,
49        span: Option<Span>,
50    },
51    Paragraph(Paragraph),
52    Heading {
53        level: u8,
54        children: Paragraph,
55        span: Option<Span>,
56    },
57    Blockquote {
58        children: Vec<BlockNode>,
59        span: Option<Span>,
60    },
61    List {
62        /// Only contains ListItem, others will be ignored
63        children: Vec<BlockNode>,
64        ordered: bool,
65        /// The first ordinal for an ordered list. HTML lists leave this unset.
66        start: Option<u32>,
67        span: Option<Span>,
68    },
69    ListItem {
70        children: Vec<BlockNode>,
71        spread: bool,
72        /// Whether the list item is checked, if None, it's not a checkbox
73        checked: Option<bool>,
74        span: Option<Span>,
75    },
76    CodeBlock(CodeBlock),
77    /// A custom Markdown node produced by [`MarkdownExtensions`].
78    Custom(MarkdownNode),
79    Table(Table),
80    Break {
81        html: bool,
82        span: Option<Span>,
83    },
84    HorizontalRule {
85        span: Option<Span>,
86    },
87    /// Use for to_markdown get raw definition
88    Definition {
89        identifier: SharedString,
90        url: SharedString,
91        title: Option<SharedString>,
92        span: Option<Span>,
93    },
94    Unknown,
95}
96
97#[derive(Clone, Copy)]
98enum BlockTextKind {
99    All,
100    Selected,
101    /// Like `Selected`, but reconstructs Markdown source for the selection
102    /// instead of the rendered plain text.
103    SelectedSource,
104}
105
106impl BlockNode {
107    pub(super) fn is_list_item(&self) -> bool {
108        matches!(self, Self::ListItem { .. })
109    }
110
111    /// Combine all children, omitting the empt parent nodes.
112    pub(super) fn compact(self) -> BlockNode {
113        match self {
114            Self::Root { mut children, .. } if children.len() == 1 => children.remove(0).compact(),
115            _ => self,
116        }
117    }
118
119    /// Get the span of the node.
120    pub(crate) fn span(&self) -> Option<Span> {
121        match self {
122            BlockNode::Root { span, .. } => *span,
123            BlockNode::Paragraph(paragraph) => paragraph.span,
124            BlockNode::Heading { span, .. } => *span,
125            BlockNode::Blockquote { span, .. } => *span,
126            BlockNode::List { span, .. } => *span,
127            BlockNode::ListItem { span, .. } => *span,
128            BlockNode::CodeBlock(code_block) => code_block.span,
129            BlockNode::Custom(el) => el.span,
130            BlockNode::Table(table) => table.span,
131            BlockNode::Break { span, .. } => *span,
132            BlockNode::HorizontalRule { span, .. } => *span,
133            BlockNode::Definition { span, .. } => *span,
134            BlockNode::Unknown { .. } => None,
135        }
136    }
137
138    pub(super) fn text(&self) -> String {
139        self.text_by_kind(BlockTextKind::All)
140    }
141
142    /// The selected text within this block, in `format`.
143    ///
144    /// [`SelectionFormat::Source`] reconstructs the Markdown source of the
145    /// selection instead of the rendered text.
146    pub(super) fn selected_text(&self, format: SelectionFormat) -> String {
147        self.text_by_kind(match format {
148            SelectionFormat::Plain => BlockTextKind::Selected,
149            SelectionFormat::Source => BlockTextKind::SelectedSource,
150        })
151    }
152
153    pub(super) fn selected_source_range(&self) -> SourceRangeSelection {
154        let mut selected = SourceRangeSelection::Unselected;
155        match self {
156            BlockNode::Root { children, .. }
157            | BlockNode::Blockquote { children, .. }
158            | BlockNode::List { children, .. }
159            | BlockNode::ListItem { children, .. } => {
160                for child in children {
161                    selected.merge(child.selected_source_range());
162                }
163            }
164            BlockNode::Paragraph(paragraph) => selected = paragraph.selected_source_range(),
165            BlockNode::Heading { children, .. } => selected = children.selected_source_range(),
166            BlockNode::Table(table) => {
167                for row in &table.children {
168                    for cell in &row.children {
169                        selected.merge(cell.children.selected_source_range());
170                    }
171                }
172            }
173            BlockNode::CodeBlock(code_block) => selected = code_block.selected_source_range(),
174            BlockNode::Custom(_)
175            | BlockNode::Definition { .. }
176            | BlockNode::Break { .. }
177            | BlockNode::HorizontalRule { .. }
178            | BlockNode::Unknown => {}
179        }
180        selected
181    }
182
183    fn text_by_kind(&self, kind: BlockTextKind) -> String {
184        let mut text = String::new();
185        match self {
186            BlockNode::Root { children, .. } => {
187                let block_text = Self::children_text(children, kind);
188                if !block_text.is_empty() {
189                    text.push_str(&block_text);
190                    text.push('\n');
191                }
192            }
193            BlockNode::Paragraph(paragraph) => {
194                let block_text = match kind {
195                    BlockTextKind::All => paragraph.text(),
196                    BlockTextKind::Selected => paragraph.selected_text(),
197                    BlockTextKind::SelectedSource => paragraph.selected_source(),
198                };
199                if !block_text.is_empty() {
200                    text.push_str(&block_text);
201                    text.push('\n');
202                }
203            }
204            BlockNode::Heading {
205                level, children, ..
206            } => {
207                let block_text = match kind {
208                    BlockTextKind::All => children.text(),
209                    BlockTextKind::Selected => children.selected_text(),
210                    BlockTextKind::SelectedSource => children.selected_source(),
211                };
212                if !block_text.is_empty() {
213                    // In source mode, prefix the heading marker so a selected
214                    // heading round-trips as Markdown (e.g. `## Title`).
215                    if matches!(kind, BlockTextKind::SelectedSource) {
216                        text.push_str(&"#".repeat(*level as usize));
217                        text.push(' ');
218                    }
219                    text.push_str(&block_text);
220                    text.push('\n');
221                }
222            }
223            BlockNode::List {
224                children,
225                ordered,
226                start,
227                ..
228            } => {
229                if matches!(kind, BlockTextKind::SelectedSource) {
230                    // Reconstruct the list source, indenting nested lists and
231                    // restoring list markers and task-list checkboxes.
232                    text.push_str(&list_selected_source(children, *ordered, *start, ""));
233                } else {
234                    text.push_str(&Self::children_text(children, kind));
235                }
236            }
237            BlockNode::ListItem { children, .. } => {
238                text.push_str(&Self::children_text(children, kind));
239            }
240            BlockNode::Blockquote { children, .. } => {
241                let block_text = Self::children_text(children, kind);
242
243                if !block_text.is_empty() {
244                    if matches!(kind, BlockTextKind::SelectedSource) {
245                        // Prefix every line with `> ` so a selected blockquote
246                        // round-trips as Markdown.
247                        let quoted = block_text
248                            .trim_end_matches('\n')
249                            .lines()
250                            .map(|line| {
251                                if line.is_empty() {
252                                    ">".to_string()
253                                } else {
254                                    format!("> {}", line)
255                                }
256                            })
257                            .collect::<Vec<_>>()
258                            .join("\n");
259                        text.push_str(&quoted);
260                    } else {
261                        text.push_str(&block_text);
262                    }
263                    text.push('\n');
264                }
265            }
266            BlockNode::Table(table) => {
267                if matches!(kind, BlockTextKind::SelectedSource) {
268                    let block_text = table_selected_source(table);
269                    if !block_text.is_empty() {
270                        text.push_str(&block_text);
271                        text.push('\n');
272                    }
273                } else {
274                    let mut block_text = String::new();
275                    for row in table.children.iter() {
276                        let mut row_texts = vec![];
277                        for cell in row.children.iter() {
278                            row_texts.push(match kind {
279                                BlockTextKind::All => cell.children.text(),
280                                // Source is handled above; only Selected reaches here.
281                                _ => cell.children.selected_text(),
282                            });
283                        }
284                        if !row_texts.is_empty() {
285                            block_text.push_str(&row_texts.join(" "));
286                            block_text.push('\n');
287                        }
288                    }
289
290                    if !block_text.is_empty() {
291                        text.push_str(&block_text);
292                        text.push('\n');
293                    }
294                }
295            }
296            BlockNode::CodeBlock(code_block) => {
297                let block_text = match kind {
298                    BlockTextKind::All => code_block.text(),
299                    BlockTextKind::Selected => code_block.selected_text(),
300                    BlockTextKind::SelectedSource => code_block.selected_source(),
301                };
302                if !block_text.is_empty() {
303                    text.push_str(&block_text);
304                    text.push('\n');
305                }
306            }
307            BlockNode::Custom(node) => {
308                if let BlockTextKind::All = kind {
309                    let content = node.as_text();
310                    if !content.is_empty() {
311                        text.push_str(content);
312                        text.push('\n');
313                    }
314                }
315            }
316            BlockNode::Definition { .. }
317            | BlockNode::Break { .. }
318            | BlockNode::HorizontalRule { .. }
319            | BlockNode::Unknown { .. } => {}
320        }
321
322        text
323    }
324
325    fn children_text(children: &[BlockNode], kind: BlockTextKind) -> String {
326        let mut text = String::new();
327        for child in children.iter() {
328            text.push_str(&child.text_by_kind(kind));
329        }
330
331        text
332    }
333
334    /// Synchronously clear the selection stored in every inline state.
335    ///
336    /// Mirrors the [`selected_text`](Self::selected_text) traversal so the
337    /// selection can be cleared without relying on a repaint.
338    /// Whether this block carries a selection, even an empty one.
339    ///
340    /// A block only learns its selection when it is painted, so this doubles as
341    /// "this block was on screen while the selection was made". An empty
342    /// selection is the caret left by the press that started the drag, which is
343    /// why it counts (see [`ParsedDocument::selected_text`]).
344    pub(super) fn has_selection(&self) -> bool {
345        match self {
346            BlockNode::Root { children, .. }
347            | BlockNode::Blockquote { children, .. }
348            | BlockNode::List { children, .. }
349            | BlockNode::ListItem { children, .. } => {
350                children.iter().any(|child| child.has_selection())
351            }
352            BlockNode::Paragraph(paragraph) => paragraph.has_selection(),
353            BlockNode::Heading { children, .. } => children.has_selection(),
354            BlockNode::Table(table) => table.children.iter().any(|row| {
355                row.children
356                    .iter()
357                    .any(|cell| cell.children.has_selection())
358            }),
359            BlockNode::CodeBlock(code_block) => code_block.has_selection(),
360            BlockNode::Custom { .. }
361            | BlockNode::Definition { .. }
362            | BlockNode::Break { .. }
363            | BlockNode::HorizontalRule { .. }
364            | BlockNode::Unknown { .. } => false,
365        }
366    }
367
368    pub(super) fn clear_selection(&self) {
369        match self {
370            BlockNode::Root { children, .. }
371            | BlockNode::Blockquote { children, .. }
372            | BlockNode::List { children, .. }
373            | BlockNode::ListItem { children, .. } => {
374                for child in children.iter() {
375                    child.clear_selection();
376                }
377            }
378            BlockNode::Paragraph(paragraph) => paragraph.clear_selection(),
379            BlockNode::Heading { children, .. } => children.clear_selection(),
380            BlockNode::Table(table) => {
381                for row in table.children.iter() {
382                    for cell in row.children.iter() {
383                        cell.children.clear_selection();
384                    }
385                }
386            }
387            BlockNode::CodeBlock(code_block) => code_block.clear_selection(),
388            BlockNode::Custom { .. }
389            | BlockNode::Definition { .. }
390            | BlockNode::Break { .. }
391            | BlockNode::HorizontalRule { .. }
392            | BlockNode::Unknown { .. } => {}
393        }
394    }
395}
396
397#[allow(unused)]
398#[derive(Debug, Default, Clone, PartialEq)]
399pub struct LinkMark {
400    pub url: SharedString,
401    /// Optional identifier for footnotes.
402    pub identifier: Option<SharedString>,
403    pub title: Option<SharedString>,
404}
405
406#[derive(Debug, Default, Clone, PartialEq)]
407pub struct TextMark {
408    pub bold: bool,
409    pub italic: bool,
410    pub strikethrough: bool,
411    pub underline: bool,
412    pub code: bool,
413    /// Highlight (`<mark>`) the text with this background color.
414    ///
415    /// `None` means the text is not highlighted.
416    pub highlight: Option<Hsla>,
417    pub link: Option<LinkMark>,
418}
419
420impl TextMark {
421    pub fn bold(mut self) -> Self {
422        self.bold = true;
423        self
424    }
425
426    pub fn italic(mut self) -> Self {
427        self.italic = true;
428        self
429    }
430
431    pub fn strikethrough(mut self) -> Self {
432        self.strikethrough = true;
433        self
434    }
435
436    pub fn underline(mut self) -> Self {
437        self.underline = true;
438        self
439    }
440
441    pub fn code(mut self) -> Self {
442        self.code = true;
443        self
444    }
445
446    /// Mark the text as highlighted (`<mark>`) with the given background color.
447    pub fn highlight(mut self, color: Hsla) -> Self {
448        self.highlight = Some(color);
449        self
450    }
451
452    pub fn link(mut self, link: impl Into<LinkMark>) -> Self {
453        self.link = Some(link.into());
454        self
455    }
456
457    pub fn merge(&mut self, other: TextMark) {
458        self.bold |= other.bold;
459        self.italic |= other.italic;
460        self.strikethrough |= other.strikethrough;
461        self.underline |= other.underline;
462        self.code |= other.code;
463        if other.highlight.is_some() {
464            self.highlight = other.highlight;
465        }
466        if let Some(link) = other.link {
467            self.link = Some(link);
468        }
469    }
470}
471
472/// The bytes
473#[derive(Debug, Default, Copy, Clone, PartialEq)]
474pub struct Span {
475    pub start: usize,
476    pub end: usize,
477}
478
479#[allow(unused)]
480#[derive(Default, Clone)]
481pub struct ImageNode {
482    pub url: SharedUri,
483    pub link: Option<LinkMark>,
484    pub title: Option<SharedString>,
485    pub alt: Option<SharedString>,
486    pub width: Option<DefiniteLength>,
487    pub height: Option<DefiniteLength>,
488    pub(crate) span: Option<Span>,
489    /// The image a `data:` URL carries, decoded on first render and kept for
490    /// the node's lifetime so it is not decoded again every frame.
491    pub(super) embedded: OnceLock<Option<Arc<Image>>>,
492}
493
494impl ImageNode {
495    pub fn title(&self) -> String {
496        self.title
497            .clone()
498            .unwrap_or_else(|| self.alt.clone().unwrap_or_default())
499            .to_string()
500    }
501
502    /// The [`ImageSource`] to render, without granting implicit filesystem
503    /// access.
504    ///
505    /// A `data:` URL carries its image inline, so it is decoded here rather
506    /// than handed to GPUI's resource loader, which only fetches over HTTP.
507    /// Every other document-provided value remains URI-backed, including
508    /// `file://` and scheme-less strings.
509    pub(super) fn source(&self) -> ImageSource {
510        match self.embedded.get_or_init(|| data_url_image(&self.url)) {
511            Some(image) => ImageSource::Image(image.clone()),
512            None => self.url.clone().into(),
513        }
514    }
515}
516
517impl std::fmt::Debug for ImageNode {
518    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
519        f.debug_struct("ImageNode")
520            .field("url", &self.url)
521            .field("link", &self.link)
522            .field("title", &self.title)
523            .field("alt", &self.alt)
524            .field("width", &self.width)
525            .field("height", &self.height)
526            .field("span", &self.span)
527            .finish()
528    }
529}
530
531impl PartialEq for ImageNode {
532    fn eq(&self, other: &Self) -> bool {
533        self.url == other.url
534            && self.link == other.link
535            && self.title == other.title
536            && self.alt == other.alt
537            && self.width == other.width
538            && self.height == other.height
539            && self.span == other.span
540    }
541}
542
543#[derive(Debug, Clone, PartialEq)]
544pub(crate) struct SourceSegment {
545    pub(crate) rendered: Range<usize>,
546    pub(crate) source: Range<usize>,
547    /// Whether each rendered character came from a source character of the
548    /// same length, so that part of the segment maps to part of its source.
549    /// A decoded entity or an escape maps only as a whole, even an entity
550    /// whose characters take as many bytes as its source, like `&acE;`.
551    pub(crate) linear: bool,
552}
553
554pub(crate) enum SourceRangeSelection {
555    Unselected,
556    Mapped(Range<usize>),
557    Unmapped,
558}
559
560impl SourceRangeSelection {
561    pub(crate) fn merge(&mut self, other: Self) {
562        match (&mut *self, other) {
563            (_, Self::Unselected) => {}
564            (_, Self::Unmapped) => *self = Self::Unmapped,
565            (Self::Unselected, mapped @ Self::Mapped(_)) => *self = mapped,
566            (Self::Mapped(selected), Self::Mapped(range)) => {
567                selected.start = selected.start.min(range.start);
568                selected.end = selected.end.max(range.end);
569            }
570            (Self::Unmapped, Self::Mapped(_)) => {}
571        }
572    }
573
574    pub(crate) fn into_range(self) -> Option<Range<usize>> {
575        match self {
576            Self::Mapped(range) => Some(range),
577            Self::Unselected | Self::Unmapped => None,
578        }
579    }
580}
581
582fn source_range_for_segments(
583    segments: &[SourceSegment],
584    selection: Range<usize>,
585) -> Option<Range<usize>> {
586    fn mapped_source_start(segment: &SourceSegment, rendered_start: usize) -> usize {
587        if segment.linear {
588            segment.source.start + rendered_start.saturating_sub(segment.rendered.start)
589        } else {
590            segment.source.start
591        }
592    }
593
594    fn mapped_source_end(segment: &SourceSegment, rendered_end: usize) -> usize {
595        if segment.linear {
596            segment.source.start
597                + rendered_end
598                    .min(segment.rendered.end)
599                    .saturating_sub(segment.rendered.start)
600        } else {
601            segment.source.end
602        }
603    }
604
605    if selection.start >= selection.end {
606        return None;
607    }
608
609    let mut overlapping = segments.iter().filter(|segment| {
610        segment.rendered.start < selection.end && segment.rendered.end > selection.start
611    });
612    let first = overlapping.next()?;
613    if first.rendered.start > selection.start {
614        return None;
615    }
616    let mut rendered_end = first.rendered.end;
617    let source_start = mapped_source_start(first, selection.start);
618    let mut source_end = mapped_source_end(first, selection.end);
619    for segment in overlapping {
620        if segment.rendered.start > rendered_end {
621            return None;
622        }
623        rendered_end = rendered_end.max(segment.rendered.end);
624        source_end = mapped_source_end(segment, selection.end);
625    }
626    (rendered_end >= selection.end).then_some(source_start..source_end)
627}
628
629#[derive(Default, Clone, Debug)]
630pub(crate) struct InlineNode {
631    /// The text content.
632    pub(crate) text: SharedString,
633    pub(crate) image: Option<ImageNode>,
634    pub(crate) custom: Option<MarkdownNode>,
635    custom_selection: Arc<Mutex<bool>>,
636    /// The text styles, each tuple contains the range of the text and the style.
637    pub(crate) marks: Vec<(Range<usize>, TextMark)>,
638    /// Rendered UTF-8 byte spans paired with their exact Markdown source spans.
639    pub(crate) source_segments: Vec<SourceSegment>,
640
641    pub(super) state: Arc<Mutex<InlineState>>,
642}
643
644impl PartialEq for InlineNode {
645    fn eq(&self, other: &Self) -> bool {
646        self.text == other.text
647            && self.image == other.image
648            && self.custom == other.custom
649            && self.marks == other.marks
650            && self.source_segments == other.source_segments
651    }
652}
653
654/// Wrap `text` with the Markdown syntax implied by `mark`.
655///
656/// This mirrors the per-mark formatting in [`Paragraph::to_markdown`] but
657/// operates on an already-sliced run, so it can reconstruct the Markdown
658/// source for a *partial* text selection. Applied inside-out (innermost markup
659/// first) so nested emphasis like `**_x_**` round-trips.
660pub(crate) fn wrap_with_mark(text: &str, mark: &TextMark) -> String {
661    if text.is_empty() {
662        return String::new();
663    }
664
665    let mut out = text.to_string();
666    if mark.code {
667        out = format!("`{}`", out);
668    }
669    if mark.italic {
670        out = format!("*{}*", out);
671    }
672    if mark.bold {
673        out = format!("**{}**", out);
674    }
675    if mark.strikethrough {
676        out = format!("~~{}~~", out);
677    }
678    if mark.underline {
679        // Markdown has no underline syntax, and `__` reads as bold to most
680        // parsers, so fall back to the inline HTML `<u>` parses from.
681        out = format!("<u>{}</u>", out);
682    }
683    if mark.highlight.is_some() {
684        out = format!("=={}==", out);
685    }
686    if let Some(link) = &mark.link {
687        out = match &link.title {
688            Some(title) => format!("[{}]({} \"{}\")", out, link.url, title),
689            None => format!("[{}]({})", out, link.url),
690        };
691    }
692    out
693}
694
695/// Keep syntax and marks separate until all selected runs and atomic objects
696/// have been collected. Wrapping every run independently creates adjacent `*`
697/// delimiters that can turn italic formulas into bold ones when pasted.
698#[derive(Default)]
699struct MarkdownSource {
700    pieces: Vec<(String, Vec<TextMark>)>,
701}
702
703impl MarkdownSource {
704    fn push_str(&mut self, source: &str) {
705        self.push_marked(source, &TextMark::default());
706    }
707
708    fn push_marked(&mut self, source: &str, mark: &TextMark) {
709        if source.is_empty() {
710            return;
711        }
712        // Outer-to-inner order for equally extensive marks. When a mark spans
713        // more neighboring pieces, serialize it outside the shorter marks.
714        let mut layers = Vec::new();
715        if let Some(link) = &mark.link {
716            layers.push(TextMark {
717                link: Some(link.clone()),
718                ..Default::default()
719            });
720        }
721        if let Some(highlight) = mark.highlight {
722            layers.push(TextMark {
723                highlight: Some(highlight),
724                ..Default::default()
725            });
726        }
727        if mark.underline {
728            layers.push(TextMark::default().underline());
729        }
730        if mark.strikethrough {
731            layers.push(TextMark::default().strikethrough());
732        }
733        if mark.bold {
734            layers.push(TextMark::default().bold());
735        }
736        if mark.italic {
737            layers.push(TextMark::default().italic());
738        }
739        if mark.code {
740            layers.push(TextMark::default().code());
741        }
742        self.pieces.push((source.to_string(), layers));
743    }
744
745    fn push_text(
746        &mut self,
747        text: &str,
748        marks: &[(Range<usize>, TextMark)],
749        selection: Range<usize>,
750    ) {
751        let start = selection.start.min(text.len());
752        let end = selection.end.min(text.len());
753        if start >= end {
754            return;
755        }
756        let mut cursor = start;
757        for (range, mark) in marks {
758            let lo = range.start.max(start);
759            let hi = range.end.min(end);
760            if lo >= hi {
761                continue;
762            }
763            if cursor < lo {
764                self.push_str(&text[cursor..lo]);
765            }
766            self.push_marked(&text[lo..hi], mark);
767            cursor = hi;
768        }
769        if cursor < end {
770            self.push_str(&text[cursor..end]);
771        }
772    }
773
774    fn push_object(&mut self, node: &InlineNode) {
775        let mut mark = TextMark::default();
776        for (_, layer) in &node.marks {
777            mark.merge(layer.clone());
778        }
779        self.push_marked(&node.custom.as_ref().unwrap().to_markdown(), &mark);
780    }
781
782    fn is_empty(&self) -> bool {
783        self.pieces.is_empty()
784    }
785
786    fn finish(mut self) -> String {
787        fn write(pieces: &mut [(String, Vec<TextMark>)]) -> String {
788            let mut out = String::new();
789            let mut offset = 0;
790            while offset < pieces.len() {
791                let Some((mark, count)) = pieces[offset]
792                    .1
793                    .iter()
794                    .map(|mark| {
795                        let count = pieces[offset..]
796                            .iter()
797                            .take_while(|(_, layers)| layers.contains(mark))
798                            .count();
799                        (mark.clone(), count)
800                    })
801                    .reduce(|best, candidate| {
802                        if candidate.1 > best.1 {
803                            candidate
804                        } else {
805                            best
806                        }
807                    })
808                else {
809                    out.push_str(&pieces[offset].0);
810                    offset += 1;
811                    continue;
812                };
813                let group = &mut pieces[offset..offset + count];
814                for (_, layers) in group.iter_mut() {
815                    layers.retain(|layer| layer != &mark);
816                }
817                let content = write(group);
818                out.push_str(&wrap_with_mark(&content, &mark));
819                offset += count;
820            }
821            out
822        }
823        write(&mut self.pieces)
824    }
825}
826
827/// How a selection covers one rendered run, so the caller can tell whether it
828/// continues into an adjacent inline image.
829#[derive(Default)]
830struct RunSelection {
831    emitted: bool,
832    at_start: bool,
833    at_end: bool,
834}
835
836/// Emit the selected part of one rendered run, preceded by the images the
837/// selection has run into. `run` holds the run's children with their offset
838/// into the run's concatenated text.
839fn emit_run(
840    state: &Arc<Mutex<InlineState>>,
841    run: &[(usize, &InlineNode)],
842    pending_images: &mut Vec<String>,
843    out: &mut MarkdownSource,
844) -> RunSelection {
845    let mut selected = RunSelection::default();
846    let Ok(state) = state.lock() else {
847        return selected;
848    };
849    let Some(selection) = &state.selection else {
850        return selected;
851    };
852    if selection.start >= selection.end {
853        return selected;
854    }
855
856    selected.at_start = selection.start == 0;
857    selected.at_end = selection.end >= state.text.len();
858
859    for (start, child) in run {
860        let end = start + child.text.len();
861        let lo = selection.start.max(*start);
862        let hi = selection.end.min(end);
863        if lo >= hi {
864            continue;
865        }
866
867        if !selected.emitted {
868            if selected.at_start {
869                out.push_str(&pending_images.join(""));
870            }
871            pending_images.clear();
872        }
873        selected.emitted = true;
874
875        out.push_text(&child.text, &child.marks, (lo - start)..(hi - start));
876    }
877
878    selected
879}
880
881/// The Markdown source for an inline image, e.g. `![alt](url "title")`.
882fn image_markdown(image: &ImageNode) -> String {
883    let alt = image.alt.clone().unwrap_or_default();
884    let title = image
885        .title
886        .clone()
887        .map_or(String::new(), |title| format!(" \"{}\"", title));
888    format!("![{}]({}{})", alt, image.url, title)
889}
890
891/// Reconstruct the Markdown source for the `selection` sub-range of a text run
892/// carrying `marks`.
893///
894/// `selection` is a byte range into `text`. For each mark that overlaps the
895/// selection, the overlapping slice is wrapped in the mark's Markdown syntax
896/// (see [`wrap_with_mark`]); slices not covered by any mark are emitted
897/// verbatim. This lets a rendered-offset selection be copied back as Markdown
898/// source (e.g. selecting inside a `**bold**` run yields `**bold**`).
899#[cfg(test)]
900pub(crate) fn reconstruct_markdown(
901    text: &str,
902    marks: &[(Range<usize>, TextMark)],
903    selection: Range<usize>,
904) -> String {
905    let mut source = MarkdownSource::default();
906    source.push_text(text, marks, selection);
907    source.finish()
908}
909
910/// Reconstruct the Markdown source of the selected cells of `table`.
911///
912/// Cells emit their own selected source; rows are piped (`| a | b |`) and the
913/// delimiter/alignment row is inserted after the first row, so a selected
914/// table round-trips as a Markdown table. Returns an empty string when no cell
915/// is selected.
916fn table_selected_source(table: &Table) -> String {
917    let cell_source = |cell: &TableCell| cell.children.selected_source().replace('\n', " ");
918
919    let any_selected = table.children.iter().any(|row| {
920        row.children
921            .iter()
922            .any(|cell| !cell_source(cell).trim().is_empty())
923    });
924    if !any_selected {
925        return String::new();
926    }
927
928    let mut lines: Vec<String> = Vec::new();
929    for (row_ix, row) in table.children.iter().enumerate() {
930        let cells: Vec<String> = row
931            .children
932            .iter()
933            .map(|cell| cell_source(cell).trim().to_string())
934            .collect();
935        lines.push(format!("| {} |", cells.join(" | ")));
936
937        // The Markdown delimiter row carries the column alignments and must
938        // follow the header row.
939        if row_ix == 0 {
940            let aligns: Vec<String> = (0..row.children.len())
941                .map(|ix| {
942                    match table.column_align(ix) {
943                        ColumnumnAlign::Left => ":--",
944                        ColumnumnAlign::Center => ":-:",
945                        ColumnumnAlign::Right => "--:",
946                    }
947                    .to_string()
948                })
949                .collect();
950            lines.push(format!("| {} |", aligns.join(" | ")));
951        }
952    }
953
954    lines.join("\n")
955}
956
957/// Reconstruct the Markdown source of the selected items of a list.
958///
959/// Restores the list marker (`- ` / `N. `) and task-list checkbox (`[x] ` /
960/// `[ ] `) of each item, and recurses into nested lists with a deeper `indent`
961/// so nesting is preserved. `indent` is the leading whitespace for this level;
962/// nested levels are indented by the width of the parent marker so continuation
963/// and sub-list lines align under the item text. Items with no selected content
964/// are skipped but still consume an ordered number, so the remaining items keep
965/// their original numbering.
966fn list_selected_source(
967    children: &[BlockNode],
968    ordered: bool,
969    start: Option<u32>,
970    indent: &str,
971) -> String {
972    let mut out = String::new();
973    let mut item_ix = 0usize;
974
975    for child in children {
976        let BlockNode::ListItem {
977            children: item_children,
978            checked,
979            ..
980        } = child
981        else {
982            continue;
983        };
984
985        let marker = if ordered {
986            format!("{}. ", ordered_list_ordinal(start, item_ix))
987        } else {
988            "- ".to_string()
989        };
990        let checkbox = match checked {
991            Some(true) => "[x] ",
992            Some(false) => "[ ] ",
993            None => "",
994        };
995        let child_indent = format!("{}{}", indent, " ".repeat(marker.len()));
996
997        // Split the item into its own content and any nested lists, so the
998        // nested lists can be indented under the content.
999        let mut content = String::new();
1000        let mut nested = String::new();
1001        for sub in item_children {
1002            if let BlockNode::List {
1003                children: sub_children,
1004                ordered: sub_ordered,
1005                start: sub_start,
1006                ..
1007            } = sub
1008            {
1009                nested.push_str(&list_selected_source(
1010                    sub_children,
1011                    *sub_ordered,
1012                    *sub_start,
1013                    &child_indent,
1014                ));
1015            } else {
1016                content.push_str(&sub.text_by_kind(BlockTextKind::SelectedSource));
1017            }
1018        }
1019        let content = content.trim_end_matches('\n');
1020
1021        if content.is_empty() && nested.is_empty() {
1022            item_ix += 1;
1023            continue;
1024        }
1025
1026        if content.is_empty() {
1027            // An item whose only selected content is a nested list.
1028            out.push_str(indent);
1029            out.push_str(&marker);
1030            out.push_str(checkbox.trim_end());
1031            out.push('\n');
1032        } else {
1033            // The first line carries the marker and checkbox; continuation
1034            // lines are indented to align under the item text.
1035            let mut lines = content.lines();
1036            if let Some(first) = lines.next() {
1037                out.push_str(indent);
1038                out.push_str(&marker);
1039                out.push_str(checkbox);
1040                out.push_str(first);
1041                out.push('\n');
1042            }
1043            for line in lines {
1044                out.push_str(&child_indent);
1045                out.push_str(line);
1046                out.push('\n');
1047            }
1048        }
1049        out.push_str(&nested);
1050        item_ix += 1;
1051    }
1052
1053    out
1054}
1055
1056impl InlineNode {
1057    pub(crate) fn new(text: impl Into<SharedString>) -> Self {
1058        Self {
1059            text: text.into(),
1060            image: None,
1061            custom: None,
1062            custom_selection: Arc::default(),
1063            marks: vec![],
1064            source_segments: vec![],
1065            state: Arc::new(Mutex::new(InlineState::default())),
1066        }
1067    }
1068
1069    pub(crate) fn custom(node: MarkdownNode) -> Self {
1070        let mut this = Self::new(node.as_text().to_string());
1071        this.custom = Some(node);
1072        this
1073    }
1074
1075    pub(crate) fn image(image: ImageNode) -> Self {
1076        let mut this = Self::new("");
1077        this.image = Some(image);
1078        this
1079    }
1080
1081    pub(crate) fn marks(mut self, marks: Vec<(Range<usize>, TextMark)>) -> Self {
1082        self.marks = marks;
1083        self
1084    }
1085
1086    pub(crate) fn source_segments(mut self, source_segments: Vec<SourceSegment>) -> Self {
1087        self.source_segments = source_segments;
1088        self
1089    }
1090
1091    fn selected_source_range(&self, selection: Range<usize>) -> Option<Range<usize>> {
1092        source_range_for_segments(&self.source_segments, selection)
1093    }
1094}
1095
1096/// The paragraph element, contains multiple text nodes.
1097///
1098/// Unlike other Element, this is cloneable, because it is used in the Node AST.
1099/// We are keep the selection state inside this AST Nodes.
1100#[derive(Debug, Clone, Default)]
1101pub(crate) struct Paragraph {
1102    pub(super) span: Option<Span>,
1103    pub(super) children: Vec<InlineNode>,
1104    /// The link references in this paragraph, used for reference links.
1105    ///
1106    /// The key is the identifier, the value is the url.
1107    pub(super) link_refs: HashMap<SharedString, SharedString>,
1108
1109    pub(crate) state: Arc<Mutex<InlineState>>,
1110    /// What the plain (text-only) render path derives from `children`, kept
1111    /// between frames; see [`ParagraphRender`].
1112    pub(super) render_cache: ParagraphRenderCache,
1113}
1114
1115/// Derived state, invisible to `Debug` and equality.
1116///
1117/// A clone carries what was cached so far: the cached value is a pure
1118/// function of the children and the style it is keyed on, a clone has the
1119/// same children, and every mutation of the children replaces the cache
1120/// (`invalidate_render_cache`). Streaming reparses deep-clone the document on
1121/// every append, and an empty clone made every paragraph rebuild.
1122#[derive(Default)]
1123pub(super) struct ParagraphRenderCache(Mutex<Option<Arc<ParagraphRender>>>);
1124
1125impl Clone for ParagraphRenderCache {
1126    fn clone(&self) -> Self {
1127        let cached = self.0.lock().ok().and_then(|cache| cache.clone());
1128        Self(Mutex::new(cached))
1129    }
1130}
1131
1132impl std::fmt::Debug for ParagraphRenderCache {
1133    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1134        f.write_str("ParagraphRenderCache")
1135    }
1136}
1137
1138/// The text, highlights and links a text-only paragraph renders with.
1139///
1140/// They are a pure function of the paragraph's children and the style they
1141/// are rendered under, yet every frame rebuilt them: the paragraph's text was
1142/// re-concatenated and copied into a fresh `SharedString`, and its marks
1143/// merged into highlights through `combine_highlights` once per child. On a
1144/// scroll that was ~8% of the frame for nothing. Streamed fades change every
1145/// frame and are layered on afterwards; reference links are resolved
1146/// afterwards too, since a definition can arrive later in the document.
1147struct ParagraphRender {
1148    style: Arc<TextViewStyle>,
1149    mono_font: SharedString,
1150    text: SharedString,
1151    highlights: Vec<(Range<usize>, InlineHighlight)>,
1152    /// Links as written, before reference resolution.
1153    links: Vec<(Range<usize>, LinkMark)>,
1154}
1155
1156impl PartialEq for Paragraph {
1157    fn eq(&self, other: &Self) -> bool {
1158        self.span == other.span
1159            && self.children == other.children
1160            && self.link_refs == other.link_refs
1161    }
1162}
1163
1164impl Paragraph {
1165    pub(crate) fn new(text: String) -> Self {
1166        Self {
1167            span: None,
1168            children: vec![InlineNode::new(&text)],
1169            link_refs: HashMap::new(),
1170            state: Arc::new(Mutex::new(InlineState::default())),
1171            render_cache: ParagraphRenderCache::default(),
1172        }
1173    }
1174
1175    /// The text, highlights and (unresolved) links of a text-only paragraph,
1176    /// from the cache when the style has not changed since they were built.
1177    fn plain_render(
1178        &self,
1179        node_cx: &NodeContext,
1180        cx: &App,
1181    ) -> (
1182        SharedString,
1183        Vec<(Range<usize>, InlineHighlight)>,
1184        Vec<(Range<usize>, LinkMark)>,
1185    ) {
1186        let mono_font = cx.theme().tokens.typography.mono.clone();
1187        if let Ok(cache) = self.render_cache.0.lock()
1188            && let Some(cached) = cache.as_ref()
1189            && (Arc::ptr_eq(&cached.style, &node_cx.style) || *cached.style == *node_cx.style)
1190            && cached.mono_font == mono_font
1191        {
1192            return (
1193                cached.text.clone(),
1194                cached.highlights.clone(),
1195                cached.links.clone(),
1196            );
1197        }
1198
1199        let mut text = String::new();
1200        let mut highlights: Vec<(Range<usize>, InlineHighlight)> = vec![];
1201        let mut links: Vec<(Range<usize>, LinkMark)> = vec![];
1202        let mut offset = 0;
1203        for inline_node in &self.children {
1204            let text_len = inline_node.text.len();
1205            text.push_str(&inline_node.text);
1206            let mut node_highlights = vec![];
1207            for (range, style) in &inline_node.marks {
1208                let inner_range = (offset + range.start)..(offset + range.end);
1209                let mut highlight = mark_highlight(style, node_cx, cx);
1210                if let Some(link_mark) = style.link.clone() {
1211                    highlight.style.color = Some(node_cx.style.link());
1212                    highlight.style.underline = Some(gpui::UnderlineStyle {
1213                        thickness: gpui::px(1.),
1214                        ..Default::default()
1215                    });
1216                    links.push((inner_range.clone(), link_mark));
1217                }
1218                node_highlights.push((inner_range, highlight));
1219            }
1220            highlights = combine_highlights(highlights, node_highlights);
1221            offset += text_len;
1222        }
1223        let text = SharedString::from(text);
1224        if let Ok(mut cache) = self.render_cache.0.lock() {
1225            *cache = Some(Arc::new(ParagraphRender {
1226                style: node_cx.style.clone(),
1227                mono_font,
1228                text: text.clone(),
1229                highlights: highlights.clone(),
1230                links: links.clone(),
1231            }));
1232        }
1233        (text, highlights, links)
1234    }
1235
1236    pub(super) fn selected_text(&self) -> String {
1237        let mut text = String::new();
1238
1239        for c in self.children.iter() {
1240            let Ok(state) = c.state.lock() else {
1241                continue;
1242            };
1243            if let Some(selection) = &state.selection {
1244                text.push_str(&state.text[selection.start..selection.end]);
1245            }
1246            if let Some(custom) = &c.custom
1247                && c.custom_selection.lock().is_ok_and(|selected| *selected)
1248            {
1249                text.push_str(custom.as_text());
1250            }
1251        }
1252
1253        if let Ok(state) = self.state.lock()
1254            && let Some(selection) = &state.selection
1255        {
1256            text.push_str(&state.text[selection.start..selection.end]);
1257        }
1258
1259        text
1260    }
1261
1262    /// Map the current rendered selection to its exact Markdown source range.
1263    pub(super) fn selected_source_range(&self) -> SourceRangeSelection {
1264        let mut selected = SourceRangeSelection::Unselected;
1265        let mut run: Vec<(usize, &InlineNode)> = Vec::new();
1266        let mut offset = 0;
1267        let mut pending_images: Vec<Option<Span>> = Vec::new();
1268        let mut enters_image = true;
1269
1270        let include_run = |state: &Arc<Mutex<InlineState>>,
1271                           run: &[(usize, &InlineNode)]|
1272         -> (SourceRangeSelection, RunSelection) {
1273            let Ok(state) = state.lock() else {
1274                return (SourceRangeSelection::Unmapped, RunSelection::default());
1275            };
1276            let Some(selection) = state.selection else {
1277                return (SourceRangeSelection::Unselected, RunSelection::default());
1278            };
1279            if selection.start >= selection.end {
1280                return (SourceRangeSelection::Unselected, RunSelection::default());
1281            }
1282
1283            let mut run_selection = RunSelection {
1284                at_start: selection.start == 0,
1285                at_end: selection.end >= state.text.len(),
1286                ..Default::default()
1287            };
1288            let mut mapped = SourceRangeSelection::Unselected;
1289            let mut rendered_end = selection.start;
1290            for (start, child) in run {
1291                let end = start + child.text.len();
1292                let lo = selection.start.max(*start);
1293                let hi = selection.end.min(end);
1294                if lo >= hi {
1295                    continue;
1296                }
1297                run_selection.emitted = true;
1298                if lo > rendered_end {
1299                    return (SourceRangeSelection::Unmapped, run_selection);
1300                }
1301                let Some(range) = child.selected_source_range((lo - start)..(hi - start)) else {
1302                    return (SourceRangeSelection::Unmapped, run_selection);
1303                };
1304                mapped.merge(SourceRangeSelection::Mapped(range));
1305                rendered_end = rendered_end.max(hi);
1306            }
1307            if rendered_end < selection.end {
1308                (SourceRangeSelection::Unmapped, run_selection)
1309            } else {
1310                (mapped, run_selection)
1311            }
1312        };
1313
1314        let merge_images = |selected: &mut SourceRangeSelection, images: &mut Vec<Option<Span>>| {
1315            for span in images.drain(..) {
1316                selected.merge(
1317                    span.map(|span| SourceRangeSelection::Mapped(span.start..span.end))
1318                        .unwrap_or(SourceRangeSelection::Unmapped),
1319                );
1320            }
1321        };
1322
1323        for child in &self.children {
1324            if child.custom.is_some() {
1325                let (run_range, run_selection) = include_run(&child.state, &run);
1326                if run_selection.emitted && run_selection.at_start {
1327                    merge_images(&mut selected, &mut pending_images);
1328                }
1329                selected.merge(run_range);
1330
1331                match child.custom_selection.lock() {
1332                    Ok(value) if *value => {
1333                        if run.is_empty() || (run_selection.emitted && run_selection.at_end) {
1334                            merge_images(&mut selected, &mut pending_images);
1335                        }
1336                        selected.merge(
1337                            child
1338                                .custom
1339                                .as_ref()
1340                                .and_then(MarkdownNode::source_range)
1341                                .map(SourceRangeSelection::Mapped)
1342                                .unwrap_or(SourceRangeSelection::Unmapped),
1343                        );
1344                        enters_image = true;
1345                    }
1346                    Ok(_) => enters_image = false,
1347                    Err(_) => {
1348                        selected.merge(SourceRangeSelection::Unmapped);
1349                        enters_image = false;
1350                    }
1351                }
1352                pending_images.clear();
1353                run.clear();
1354                offset = 0;
1355                continue;
1356            }
1357            if let Some(image) = &child.image {
1358                let run_before = !run.is_empty();
1359                let (run_range, run_selection) = include_run(&child.state, &run);
1360                if run_selection.emitted && run_selection.at_start {
1361                    merge_images(&mut selected, &mut pending_images);
1362                }
1363                selected.merge(run_range);
1364                if run_before {
1365                    enters_image = run_selection.emitted && run_selection.at_end;
1366                }
1367                if enters_image {
1368                    pending_images.push(image.span);
1369                } else {
1370                    pending_images.clear();
1371                }
1372                run.clear();
1373                offset = 0;
1374                continue;
1375            }
1376            run.push((offset, child));
1377            offset += child.text.len();
1378        }
1379
1380        let (trailing_range, trailing) = include_run(&self.state, &run);
1381        if trailing.emitted && trailing.at_start {
1382            merge_images(&mut selected, &mut pending_images);
1383        }
1384        selected.merge(trailing_range);
1385        if !trailing.emitted
1386            && enters_image
1387            && !matches!(selected, SourceRangeSelection::Unselected)
1388        {
1389            merge_images(&mut selected, &mut pending_images);
1390        }
1391        selected
1392    }
1393
1394    /// Reconstruct the Markdown source for the current selection.
1395    ///
1396    /// Mirrors [`selected_text`](Self::selected_text), but emits Markdown
1397    /// instead of the rendered text, using each inline node's `marks` (see
1398    /// [`reconstruct_markdown`]).
1399    ///
1400    /// Selection offsets index an `InlineState.text`, and one such state spans
1401    /// *several* children: [`Paragraph::render`] concatenates children until it
1402    /// hits an inline image, stores that run in the image child's state, then
1403    /// starts over; whatever follows the last image is stored in `self.state`.
1404    /// So walk the children in the same runs and map each selected byte back to
1405    /// the child it was rendered from — mapping against a single child's text
1406    /// would attribute the same offsets to children in other runs.
1407    ///
1408    /// An image has no selection of its own, so it is emitted when the
1409    /// selection runs into it: reaching the end of the run before it, and
1410    /// starting at the beginning of the run after it. A paragraph that begins
1411    /// or ends with an image has no run on that side, which counts as reaching
1412    /// it.
1413    pub(super) fn selected_source(&self) -> String {
1414        let mut source = MarkdownSource::default();
1415        let mut pending_images: Vec<String> = Vec::new();
1416        let mut run: Vec<(usize, &InlineNode)> = Vec::new();
1417        let mut offset = 0;
1418        let mut enters_image = true;
1419
1420        for child in self.children.iter() {
1421            if child.custom.is_some() {
1422                let selected = emit_run(&child.state, &run, &mut pending_images, &mut source);
1423                let object_selected = child
1424                    .custom_selection
1425                    .lock()
1426                    .is_ok_and(|selected| *selected);
1427                if object_selected {
1428                    if run.is_empty() || (selected.emitted && selected.at_end) {
1429                        source.push_str(&pending_images.join(""));
1430                    }
1431                    source.push_object(child);
1432                }
1433                pending_images.clear();
1434                enters_image = object_selected;
1435                run.clear();
1436                offset = 0;
1437                continue;
1438            }
1439            let Some(image) = &child.image else {
1440                run.push((offset, child));
1441                offset += child.text.len();
1442                continue;
1443            };
1444
1445            // The run before an image is stored in that image's own state.
1446            let run_before = !run.is_empty();
1447            let selected = emit_run(&child.state, &run, &mut pending_images, &mut source);
1448            if run_before {
1449                enters_image = selected.emitted && selected.at_end;
1450            }
1451            if enters_image {
1452                pending_images.push(image_markdown(image));
1453            } else {
1454                pending_images.clear();
1455            }
1456
1457            run.clear();
1458            offset = 0;
1459        }
1460
1461        let trailing = emit_run(&self.state, &run, &mut pending_images, &mut source);
1462        // Trailing images have no run after them to flush them.
1463        if !trailing.emitted && enters_image && !source.is_empty() {
1464            source.push_str(&pending_images.join(""));
1465        }
1466
1467        source.finish()
1468    }
1469
1470    pub(super) fn text(&self) -> String {
1471        let mut text = String::new();
1472        for node in self.children.iter() {
1473            text.push_str(&node.text);
1474        }
1475        text
1476    }
1477
1478    /// Synchronously clear the selection stored in every inline state.
1479    ///
1480    /// Mirrors the [`selected_text`](Self::selected_text) traversal.
1481    pub(super) fn has_selection(&self) -> bool {
1482        self.children.iter().any(|c| {
1483            c.state.lock().is_ok_and(|state| state.selection.is_some())
1484                || c.custom_selection.lock().is_ok_and(|selected| *selected)
1485        }) || self
1486            .state
1487            .lock()
1488            .is_ok_and(|state| state.selection.is_some())
1489    }
1490
1491    pub(super) fn clear_selection(&self) {
1492        for c in self.children.iter() {
1493            if let Ok(mut selected) = c.custom_selection.lock() {
1494                *selected = false;
1495            }
1496            if let Ok(mut state) = c.state.lock() {
1497                state.selection = None;
1498            }
1499        }
1500
1501        if let Ok(mut state) = self.state.lock() {
1502            state.selection = None;
1503        }
1504    }
1505}
1506
1507#[derive(Debug, Clone, Default, PartialEq)]
1508pub(crate) struct Table {
1509    pub(crate) children: Vec<TableRow>,
1510    pub(crate) column_aligns: Vec<ColumnumnAlign>,
1511    pub(crate) span: Option<Span>,
1512    /// The [`TableData`] handed to the `table_actions` hook, kept between
1513    /// frames; see [`Table::cached_table_data`].
1514    pub(crate) table_data_cache: TableDataCache,
1515    pub(crate) column_widths_cache: TableColumnWidthsCache,
1516}
1517
1518/// Derived state, invisible to `Debug` and equality.
1519///
1520/// A clone carries what was cached so far: a parsed table does not change
1521/// after parsing (cell mutations only touch selection state, which
1522/// [`TableData`] does not read), so a clone has the same data. Streaming
1523/// reparses deep-clone the document on every append, and an empty clone made
1524/// every table rebuild.
1525#[derive(Default)]
1526pub(crate) struct TableDataCache(Mutex<Option<Arc<TableData>>>);
1527
1528impl Clone for TableDataCache {
1529    fn clone(&self) -> Self {
1530        let cached = self.0.lock().ok().and_then(|cache| cache.clone());
1531        Self(Mutex::new(cached))
1532    }
1533}
1534
1535impl PartialEq for TableDataCache {
1536    fn eq(&self, _: &Self) -> bool {
1537        true
1538    }
1539}
1540
1541impl std::fmt::Debug for TableDataCache {
1542    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1543        f.write_str("TableDataCache")
1544    }
1545}
1546
1547/// Like `TableDataCache`, derived widths survive clones of an unchanged
1548/// parsed table. A newly parsed table starts with an empty cache.
1549#[derive(Default)]
1550pub(crate) struct TableColumnWidthsCache(Mutex<Option<Arc<TableColumnWidths>>>);
1551
1552impl Clone for TableColumnWidthsCache {
1553    fn clone(&self) -> Self {
1554        let cached = self.0.lock().ok().and_then(|cache| cache.clone());
1555        Self(Mutex::new(cached))
1556    }
1557}
1558
1559impl PartialEq for TableColumnWidthsCache {
1560    fn eq(&self, _: &Self) -> bool {
1561        true
1562    }
1563}
1564
1565impl std::fmt::Debug for TableColumnWidthsCache {
1566    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1567        f.write_str("TableColumnWidthsCache")
1568    }
1569}
1570
1571struct TableColumnWidths {
1572    text_system: Weak<gpui::WindowTextSystem>,
1573    text_style: gpui::TextStyle,
1574    rem_size: Pixels,
1575    mono_family: SharedString,
1576    inline_code: HighlightStyle,
1577    /// Maxima from ordinary text only. Custom cells may shrink after an update.
1578    widths: Vec<f32>,
1579    custom_cells: Vec<(usize, usize)>,
1580}
1581
1582/// Plain snapshot of a rendered Markdown table, passed to the
1583/// [`crate::text::TextView::table_actions`] hook.
1584#[derive(Debug, Clone, Default, PartialEq)]
1585pub struct TableData {
1586    /// First table row (header cells) as plain text.
1587    pub headers: Vec<String>,
1588    /// Rows after the header as plain text cells. May be ragged while
1589    /// a table is still streaming in.
1590    pub rows: Vec<Vec<String>>,
1591    /// The table serialized back to GFM pipe-table Markdown, alignments kept.
1592    pub markdown: String,
1593    /// Byte range of the table in the Markdown source, for callers that need
1594    /// to map the table back to the document.
1595    ///
1596    /// Not needed to keep element ids apart: the actions row is wrapped in its
1597    /// own identified element, so plain ids like `"copy"` are already scoped
1598    /// per table.
1599    pub span: Option<Range<usize>>,
1600}
1601
1602impl Table {
1603    pub(crate) fn column_align(&self, index: usize) -> ColumnumnAlign {
1604        self.column_aligns.get(index).copied().unwrap_or_default()
1605    }
1606
1607    /// Serialize the table back to GFM pipe-table Markdown (`| a | b |`),
1608    /// preserving column alignments. Cell newlines collapse to spaces and
1609    /// `|` is escaped so rows stay intact.
1610    ///
1611    /// Mirrors [`table_selected_source`], which does the same for the selected
1612    /// cells only.
1613    pub(crate) fn to_markdown(&self) -> String {
1614        let mut lines: Vec<String> = Vec::with_capacity(self.children.len() + 1);
1615
1616        for (row_ix, row) in self.children.iter().enumerate() {
1617            let cells: Vec<String> = row
1618                .children
1619                .iter()
1620                .map(|cell| {
1621                    cell.children
1622                        .to_markdown()
1623                        .trim()
1624                        .replace('\n', " ")
1625                        .replace('|', "\\|")
1626                })
1627                .collect();
1628            lines.push(format!("| {} |", cells.join(" | ")));
1629
1630            // The Markdown delimiter row carries the column alignments and must
1631            // follow the header row.
1632            if row_ix == 0 {
1633                let aligns: Vec<String> = (0..row.children.len())
1634                    .map(|ix| {
1635                        match self.column_align(ix) {
1636                            ColumnumnAlign::Left => ":--",
1637                            ColumnumnAlign::Center => ":-:",
1638                            ColumnumnAlign::Right => "--:",
1639                        }
1640                        .to_string()
1641                    })
1642                    .collect();
1643                lines.push(format!("| {} |", aligns.join(" | ")));
1644            }
1645        }
1646
1647        lines.join("\n")
1648    }
1649
1650    /// Snapshot of this table for the [`crate::text::TextView::table_actions`]
1651    /// hook.
1652    pub(crate) fn table_data(&self) -> TableData {
1653        let row_text = |row: &TableRow| {
1654            row.children
1655                .iter()
1656                .map(|cell| cell.children.text().trim().to_string())
1657                .collect::<Vec<_>>()
1658        };
1659
1660        TableData {
1661            headers: self.children.first().map(row_text).unwrap_or_default(),
1662            rows: self.children.iter().skip(1).map(row_text).collect(),
1663            markdown: self.to_markdown(),
1664            span: self.span.map(|span| span.start..span.end),
1665        }
1666    }
1667
1668    /// [`Self::table_data`], built on the first render and reused by later
1669    /// ones: serializing every cell and the whole table back to Markdown on
1670    /// every scroll or stream frame is wasted work, and a parsed table does
1671    /// not change after parsing (a reparse builds a new one).
1672    fn cached_table_data(&self) -> Arc<TableData> {
1673        if let Ok(cache) = self.table_data_cache.0.lock()
1674            && let Some(cached) = cache.as_ref()
1675        {
1676            return cached.clone();
1677        }
1678
1679        let data = Arc::new(self.table_data());
1680        if let Ok(mut cache) = self.table_data_cache.0.lock() {
1681            *cache = Some(data.clone());
1682        }
1683        data
1684    }
1685}
1686
1687#[derive(Debug, Default, Copy, Clone, PartialEq)]
1688pub(crate) enum ColumnumnAlign {
1689    #[default]
1690    Left,
1691    Center,
1692    Right,
1693}
1694
1695impl From<mdast::AlignKind> for ColumnumnAlign {
1696    fn from(value: mdast::AlignKind) -> Self {
1697        match value {
1698            mdast::AlignKind::None => ColumnumnAlign::Left,
1699            mdast::AlignKind::Left => ColumnumnAlign::Left,
1700            mdast::AlignKind::Center => ColumnumnAlign::Center,
1701            mdast::AlignKind::Right => ColumnumnAlign::Right,
1702        }
1703    }
1704}
1705
1706#[derive(Debug, Clone, Default, PartialEq)]
1707pub(crate) struct TableRow {
1708    pub children: Vec<TableCell>,
1709}
1710
1711#[derive(Debug, Clone, Default, PartialEq)]
1712pub(crate) struct TableCell {
1713    pub children: Paragraph,
1714    pub width: Option<DefiniteLength>,
1715}
1716
1717impl Paragraph {
1718    pub(crate) fn take(&mut self) -> Paragraph {
1719        std::mem::replace(
1720            self,
1721            Paragraph {
1722                span: None,
1723                children: vec![],
1724                link_refs: Default::default(),
1725                state: Arc::new(Mutex::new(InlineState::default())),
1726                render_cache: ParagraphRenderCache::default(),
1727            },
1728        )
1729    }
1730
1731    pub(crate) fn is_image(&self) -> bool {
1732        false
1733    }
1734
1735    pub(crate) fn set_span(&mut self, span: Span) {
1736        self.span = Some(span);
1737    }
1738
1739    pub(crate) fn push_str(&mut self, text: &str) {
1740        self.children.push(
1741            InlineNode::new(text.to_string()).marks(vec![(0..text.len(), TextMark::default())]),
1742        );
1743        self.invalidate_render_cache();
1744    }
1745
1746    pub(crate) fn push(&mut self, text: InlineNode) {
1747        self.children.push(text);
1748        self.invalidate_render_cache();
1749    }
1750
1751    pub(crate) fn push_image(&mut self, image: ImageNode) {
1752        self.children.push(InlineNode::image(image));
1753        self.invalidate_render_cache();
1754    }
1755
1756    /// The children changed, so what was derived from them is stale.
1757    fn invalidate_render_cache(&mut self) {
1758        self.render_cache = ParagraphRenderCache::default();
1759    }
1760
1761    pub(crate) fn is_empty(&self) -> bool {
1762        self.children.is_empty()
1763            || self
1764                .children
1765                .iter()
1766                .all(|node| node.text.is_empty() && node.image.is_none())
1767    }
1768
1769    /// Return length of children text.
1770    pub(crate) fn text_len(&self) -> usize {
1771        self.children
1772            .iter()
1773            .map(|node| node.text.len())
1774            .sum::<usize>()
1775    }
1776
1777    pub(crate) fn merge(&mut self, other: Self) {
1778        self.children.extend(other.children);
1779        self.invalidate_render_cache();
1780    }
1781}
1782
1783#[derive(Debug, Clone)]
1784pub struct CodeBlock {
1785    lang: Option<SharedString>,
1786    state: Arc<Mutex<InlineState>>,
1787    highlight_cache: Arc<Mutex<Option<CachedCodeBlockHighlights>>>,
1788    /// Rendered UTF-8 byte spans of the code paired with their exact Markdown
1789    /// source spans.
1790    pub(crate) source_segments: Vec<SourceSegment>,
1791    pub span: Option<Span>,
1792}
1793
1794struct CachedCodeBlockHighlights {
1795    highlighter: Arc<CodeBlockHighlighterFn>,
1796    styles: Vec<(Range<usize>, HighlightStyle)>,
1797}
1798
1799impl std::fmt::Debug for CachedCodeBlockHighlights {
1800    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1801        f.debug_struct("CachedCodeBlockHighlights")
1802            .field("styles", &self.styles)
1803            .finish_non_exhaustive()
1804    }
1805}
1806
1807impl PartialEq for CodeBlock {
1808    fn eq(&self, other: &Self) -> bool {
1809        self.lang == other.lang && self.code() == other.code() && self.span == other.span
1810    }
1811}
1812
1813impl CodeBlock {
1814    /// Get the language of the code block.
1815    pub fn lang(&self) -> Option<SharedString> {
1816        self.lang.clone()
1817    }
1818
1819    /// Get the code content of the code block.
1820    pub fn code(&self) -> SharedString {
1821        self.state
1822            .lock()
1823            .map(|state| state.text.clone())
1824            .unwrap_or_default()
1825    }
1826
1827    /// Builds a code block that is not tied to a parsed document.
1828    ///
1829    /// [`crate::TextView::code_block_highlighter`] hands a `&CodeBlock` to the
1830    /// highlighter it is given, so anyone writing one needs a way to build a
1831    /// block to exercise it against.
1832    pub fn from_code(code: impl Into<SharedString>, lang: Option<impl Into<SharedString>>) -> Self {
1833        Self::new(code.into(), lang.map(Into::into), None::<Span>)
1834    }
1835
1836    pub(crate) fn new(
1837        code: SharedString,
1838        lang: Option<SharedString>,
1839        span: Option<impl Into<Span>>,
1840    ) -> Self {
1841        let state = Arc::new(Mutex::new(InlineState::default()));
1842        if let Ok(mut state) = state.lock() {
1843            state.set_text(code);
1844        }
1845
1846        Self {
1847            lang,
1848            state,
1849            highlight_cache: Arc::new(Mutex::new(None)),
1850            source_segments: vec![],
1851            span: span.map(|s| s.into()),
1852        }
1853    }
1854
1855    pub(crate) fn source_segments(mut self, source_segments: Vec<SourceSegment>) -> Self {
1856        self.source_segments = source_segments;
1857        self
1858    }
1859
1860    #[cfg(test)]
1861    pub(crate) fn set_selection(&self, selection: Range<usize>) {
1862        if let Ok(mut state) = self.state.lock() {
1863            state.selection = Some(selection.into());
1864        }
1865    }
1866
1867    pub(super) fn selected_source_range(&self) -> SourceRangeSelection {
1868        let Ok(state) = self.state.lock() else {
1869            return SourceRangeSelection::Unmapped;
1870        };
1871        let Some(selection) = state.selection else {
1872            return SourceRangeSelection::Unselected;
1873        };
1874        if selection.start >= selection.end {
1875            return SourceRangeSelection::Unselected;
1876        }
1877        source_range_for_segments(&self.source_segments, selection.start..selection.end)
1878            .map(SourceRangeSelection::Mapped)
1879            .unwrap_or(SourceRangeSelection::Unmapped)
1880    }
1881
1882    fn highlighted_styles(
1883        &self,
1884        highlighter: &Arc<CodeBlockHighlighterFn>,
1885    ) -> Vec<(Range<usize>, HighlightStyle)> {
1886        if let Ok(cache) = self.highlight_cache.lock()
1887            && let Some(cache) = cache.as_ref()
1888            && Arc::ptr_eq(&cache.highlighter, highlighter)
1889        {
1890            return cache.styles.clone();
1891        }
1892
1893        let code_len = self.code().len();
1894        let styles = highlighter(self)
1895            .into_iter()
1896            .filter(|(range, _)| range.start <= range.end && range.end <= code_len)
1897            .collect::<Vec<_>>();
1898        if let Ok(mut cache) = self.highlight_cache.lock() {
1899            *cache = Some(CachedCodeBlockHighlights {
1900                highlighter: highlighter.clone(),
1901                styles: styles.clone(),
1902            });
1903        }
1904        styles
1905    }
1906
1907    pub(super) fn selected_text(&self) -> String {
1908        let mut text = String::new();
1909        if let Ok(state) = self.state.lock()
1910            && let Some(selection) = &state.selection
1911        {
1912            text.push_str(&state.text[selection.start..selection.end]);
1913        }
1914        text
1915    }
1916
1917    /// Markdown source for the current selection.
1918    ///
1919    /// The selected code is wrapped in a fenced code block carrying the block's
1920    /// language, so a selected code block round-trips as Markdown (e.g.
1921    /// ```` ```rust\n…\n``` ````) instead of pasting as bare, unfenced text.
1922    /// A partial selection is still emitted as a valid fenced block.
1923    pub(super) fn selected_source(&self) -> String {
1924        let code = self.selected_text();
1925        if code.is_empty() {
1926            return String::new();
1927        }
1928        let lang = self.lang.clone().unwrap_or_default();
1929        // Trim trailing newlines so the closing fence sits on its own line
1930        // directly after the last code line (no blank line before it).
1931        let code = code.trim_end_matches('\n');
1932        format!("```{}\n{}\n```", lang, code)
1933    }
1934
1935    pub(super) fn text(&self) -> String {
1936        self.state
1937            .lock()
1938            .map(|state| state.text.to_string())
1939            .unwrap_or_default()
1940    }
1941
1942    /// Synchronously clear the selection stored in the inline state.
1943    ///
1944    /// Mirrors the [`selected_text`](Self::selected_text) traversal.
1945    pub(super) fn has_selection(&self) -> bool {
1946        self.state
1947            .lock()
1948            .is_ok_and(|state| state.selection.is_some())
1949    }
1950
1951    pub(super) fn clear_selection(&self) {
1952        if let Ok(mut state) = self.state.lock() {
1953            state.selection = None;
1954        }
1955    }
1956
1957    fn render(
1958        &self,
1959        options: &NodeRenderOptions,
1960        node_cx: &NodeContext,
1961        window: &mut Window,
1962        cx: &mut App,
1963    ) -> AnyElement {
1964        let style = &node_cx.style;
1965        let leaf_key = self.span.map(|span| TextLeafKey::block(span.start));
1966
1967        let block = div()
1968            .w_full()
1969            .min_w_0()
1970            .p_3()
1971            .bg(style.code_background())
1972            .font_family(cx.theme().tokens.typography.mono.clone())
1973            .text_size(cx.theme().tokens.typography.mono_md.size)
1974            .relative()
1975            .refine_style(&style.code_block())
1976            .child(
1977                Inline::new(
1978                    self.state.clone(),
1979                    vec![],
1980                    fade_highlights(
1981                        node_cx
1982                            .code_block_highlighter
1983                            .as_ref()
1984                            .map(|highlighter| self.highlighted_styles(highlighter))
1985                            .unwrap_or_default()
1986                            .into_iter()
1987                            .map(|(range, style)| (range, InlineHighlight::from(style)))
1988                            .collect(),
1989                        node_cx.stream_fades(leaf_key),
1990                    ),
1991                    node_cx.link_click_handler.clone(),
1992                )
1993                .range_backgrounds(node_cx.range_backgrounds(leaf_key).to_vec())
1994                .reveal(node_cx.reveal_at(leaf_key, 0, self.code().len())),
1995            );
1996        let actions = node_cx.code_block_actions.clone().map(|actions| {
1997            div()
1998                .id("actions")
1999                .absolute()
2000                .top_2()
2001                .right_2()
2002                .bg(style.code_background())
2003                .rounded(cx.theme().tokens.radius.md)
2004                .child(actions(&self, window, cx))
2005        });
2006        // The id scopes the caller's action ids per code block, so plain ids
2007        // like `"copy"` don't collide across blocks.
2008        let id = block_element_id("codeblock", self.span, options.ix);
2009        let block = if matches!(style.code_block().overflow.y, Some(Overflow::Scroll)) {
2010            let scroll_handle = window
2011                .use_keyed_state(
2012                    block_element_id("codeblock-scroll", self.span, options.ix),
2013                    cx,
2014                    |_, _| ScrollHandle::default(),
2015                )
2016                .read(cx)
2017                .clone();
2018            // Scroll mode is opted in by setting `style.code_block`'s
2019            // `overflow.y` to `Overflow::Scroll`.
2020            // The mask consumes the wheel in the capture phase, so an ancestor
2021            // `gpui::list` doesn't scroll on the same event. Mask, scrollbar
2022            // and actions are siblings of the scrolled block, so they stay
2023            // pinned to the viewport instead of moving with the code.
2024            div()
2025                .id(id.clone())
2026                .w_full()
2027                .min_w_0()
2028                .relative()
2029                .child(block.id("scroll").track_scroll(&scroll_handle))
2030                .child(ScrollableMask::new(Axis::Vertical, &scroll_handle).id(id))
2031                .child(
2032                    div()
2033                        .absolute()
2034                        .inset_0()
2035                        .child(Scrollbar::vertical(&scroll_handle)),
2036                )
2037                .children(actions)
2038                .into_any_element()
2039        } else {
2040            // Without actions nothing under the block needs element state.
2041            match actions {
2042                Some(actions) => block.id(id).child(actions).into_any_element(),
2043                None => block.into_any_element(),
2044            }
2045        };
2046
2047        gapped(
2048            block,
2049            if options.is_last {
2050                rems(0.)
2051            } else {
2052                style.paragraph_gap()
2053            },
2054        )
2055    }
2056}
2057
2058/// A context for rendering nodes, contains link references.
2059#[derive(Default, Clone)]
2060pub(crate) struct NodeContext {
2061    /// The byte offset of the node in the original markdown text.
2062    /// Used for incremental updates.
2063    pub(crate) offset: usize,
2064    pub(crate) link_refs: HashMap<SharedString, LinkMark>,
2065    pub(crate) style: Arc<TextViewStyle>,
2066    pub(crate) code_block_actions: Option<Arc<CodeBlockActionsFn>>,
2067    pub(crate) code_block_highlighter: Option<Arc<CodeBlockHighlighterFn>>,
2068    pub(crate) table_actions: Option<Arc<TableActionsFn>>,
2069    pub(crate) image_source: Option<Arc<super::text_view::ImageSourceFn>>,
2070    pub(crate) link_click_handler: Option<Arc<LinkClickHandlerFn>>,
2071    pub(crate) markdown_extensions: Arc<MarkdownExtensions>,
2072    /// This frame's streamed fade-in, when any text is still fading.
2073    pub(crate) stream_fade: Option<Arc<StreamFadeFrame>>,
2074    /// The application's range highlights, when there are any.
2075    pub(crate) range_highlights: Option<Arc<RangeHighlightFrame>>,
2076    /// The line being scrolled into view, when there is one.
2077    pub(crate) reveal: Option<RevealRequest>,
2078}
2079
2080impl NodeContext {
2081    fn image_source(&self, image: &ImageNode) -> ImageSource {
2082        match &self.image_source {
2083            Some(resolve) => resolve(&image.url),
2084            None => image.source(),
2085        }
2086    }
2087
2088    pub(super) fn add_ref(&mut self, identifier: SharedString, link: LinkMark) {
2089        self.link_refs.insert(identifier, link);
2090    }
2091
2092    /// The fade ranges of the text leaf `key`, in its rendered byte space.
2093    fn stream_fades(&self, key: Option<TextLeafKey>) -> &[(Range<usize>, f32)] {
2094        match (&self.stream_fade, key) {
2095            (Some(frame), Some(key)) => frame.fades(key).unwrap_or_default(),
2096            _ => &[],
2097        }
2098    }
2099
2100    /// The range highlight backgrounds of the text leaf `key`, in its
2101    /// rendered byte space.
2102    fn range_backgrounds(&self, key: Option<TextLeafKey>) -> &[(Range<usize>, Hsla)] {
2103        match (&self.range_highlights, key) {
2104            (Some(frame), Some(key)) => frame.backgrounds(key),
2105            _ => &[],
2106        }
2107    }
2108
2109    /// The pending reveal, when it starts in the text leaf `key` between
2110    /// `start` and `end`, rebased to `start`.
2111    fn reveal_at(&self, key: Option<TextLeafKey>, start: usize, end: usize) -> Option<RevealAt> {
2112        self.reveal.as_ref()?.at(key, start, end)
2113    }
2114}
2115
2116impl PartialEq for NodeContext {
2117    fn eq(&self, other: &Self) -> bool {
2118        self.link_refs == other.link_refs && self.style == other.style
2119        // Note: code_block_actions, table_actions and markdown_extensions are
2120        // intentionally not compared (closures can't be compared)
2121    }
2122}
2123
2124/// The highlight a text mark renders with. The link decoration is applied by
2125/// the caller, which also has to record the link range.
2126fn mark_highlight(mark: &TextMark, node_cx: &NodeContext, cx: &App) -> InlineHighlight {
2127    let mut highlight = HighlightStyle::default();
2128    if mark.bold {
2129        highlight.font_weight = Some(FontWeight::BOLD);
2130    }
2131    if mark.italic {
2132        highlight.font_style = Some(FontStyle::Italic);
2133    }
2134    if mark.strikethrough {
2135        highlight.strikethrough = Some(gpui::StrikethroughStyle {
2136            thickness: gpui::px(1.),
2137            ..Default::default()
2138        });
2139    }
2140    if mark.underline {
2141        highlight.underline = Some(gpui::UnderlineStyle {
2142            thickness: gpui::px(1.),
2143            ..Default::default()
2144        });
2145    }
2146    let mut font_family = None;
2147    if mark.code {
2148        highlight = highlight.highlight(node_cx.style.inline_code_highlight());
2149        font_family = Some(cx.theme().tokens.typography.mono.clone());
2150    }
2151    if let Some(color) = mark.highlight {
2152        highlight.background_color = Some(color);
2153    }
2154    InlineHighlight {
2155        style: highlight,
2156        font_family,
2157        font_size_scale: mark.code.then_some(0.875),
2158    }
2159}
2160
2161impl Paragraph {
2162    /// The highlights over [`Self::text`], for measuring the paragraph with
2163    /// the runs it renders with. Link colors are left out: they do not move
2164    /// glyphs.
2165    fn inline_highlights(
2166        &self,
2167        node_cx: &NodeContext,
2168        cx: &App,
2169    ) -> Vec<(Range<usize>, InlineHighlight)> {
2170        let mut highlights = vec![];
2171        let mut offset = 0;
2172        for inline_node in &self.children {
2173            let node_highlights = inline_node
2174                .marks
2175                .iter()
2176                .map(|(range, mark)| {
2177                    (
2178                        (offset + range.start)..(offset + range.end),
2179                        mark_highlight(mark, node_cx, cx),
2180                    )
2181                })
2182                .collect::<Vec<_>>();
2183            highlights = combine_highlights(highlights, node_highlights);
2184            offset += inline_node.text.len();
2185        }
2186        highlights
2187    }
2188
2189    /// `fade_key` names this paragraph's text for the streamed fade-in and
2190    /// for range highlights; the owning block supplies it because a heading
2191    /// or table cell paragraph carries no span of its own.
2192    fn render(
2193        &self,
2194        fade_key: Option<TextLeafKey>,
2195        node_cx: &NodeContext,
2196        _window: &mut Window,
2197        cx: &mut App,
2198    ) -> AnyElement {
2199        let children = &self.children;
2200        let fades = node_cx.stream_fades(fade_key);
2201        let backgrounds = node_cx.range_backgrounds(fade_key);
2202
2203        if self.should_render_inline_flow() {
2204            return InlineFlow::new(
2205                leaf_element_id(fade_key),
2206                self.inline_flow_items(fade_key, fades, backgrounds, node_cx, cx),
2207                node_cx.link_click_handler.clone(),
2208            )
2209            .into_any_element();
2210        }
2211
2212        let has_image = children.iter().any(|child| child.image.is_some());
2213        // Text alone is one `Inline`, which needs no box of its own, and its
2214        // text, highlights and links are cached across frames.
2215        if !has_image {
2216            let (text, highlights, mut links) = self.plain_render(node_cx, cx);
2217            if text.is_empty() {
2218                return div().into_any_element();
2219            }
2220            for (_, link_mark) in &mut links {
2221                if let Some(identifier) = link_mark.identifier.as_ref()
2222                    && let Some(mark) = node_cx.link_refs.get(identifier)
2223                {
2224                    *link_mark = mark.clone();
2225                }
2226            }
2227            let highlights = fade_highlights(highlights, &slice_fades(fades, 0, text.len()));
2228            let backgrounds = slice_backgrounds(backgrounds, 0, text.len());
2229            let reveal = node_cx.reveal_at(fade_key, 0, text.len());
2230            if let Ok(mut state) = self.state.lock() {
2231                state.set_text(text);
2232            }
2233            return Inline::new(
2234                self.state.clone(),
2235                links,
2236                highlights,
2237                node_cx.link_click_handler.clone(),
2238            )
2239            .range_backgrounds(backgrounds)
2240            .reveal(reveal)
2241            .into_any_element();
2242        }
2243
2244        let mut child_nodes: Vec<AnyElement> = vec![];
2245
2246        let mut text = String::new();
2247        let mut highlights: Vec<(Range<usize>, InlineHighlight)> = vec![];
2248        let mut links: Vec<(Range<usize>, LinkMark)> = vec![];
2249        let mut offset = 0;
2250        // Where `text` starts in the paragraph's whole rendered text, which
2251        // is the byte space the fade ranges use.
2252        let mut consumed = 0;
2253
2254        for (ix, inline_node) in children.iter().enumerate() {
2255            let text_len = inline_node.text.len();
2256            text.push_str(&inline_node.text);
2257
2258            if let Some(image) = &inline_node.image {
2259                if text.len() > 0 {
2260                    if let Ok(mut state) = inline_node.state.lock() {
2261                        state.set_text(text.clone().into());
2262                    }
2263                    child_nodes.push(
2264                        Inline::new(
2265                            inline_node.state.clone(),
2266                            links.clone(),
2267                            fade_highlights(
2268                                highlights.clone(),
2269                                &slice_fades(fades, consumed, consumed + text.len()),
2270                            ),
2271                            node_cx.link_click_handler.clone(),
2272                        )
2273                        .range_backgrounds(slice_backgrounds(
2274                            backgrounds,
2275                            consumed,
2276                            consumed + text.len(),
2277                        ))
2278                        .reveal(node_cx.reveal_at(fade_key, consumed, consumed + text.len()))
2279                        .into_any_element(),
2280                    );
2281                }
2282                let link_click_handler = node_cx.link_click_handler.clone();
2283                child_nodes.push(
2284                    img(node_cx.image_source(image))
2285                        .id(ix)
2286                        .object_fit(ObjectFit::Contain)
2287                        .max_w(relative(1.))
2288                        .when_some(image.width, |this, width| this.w(width))
2289                        .when_some(image.link.clone(), |this, link| {
2290                            let link_click_handler = link_click_handler.clone();
2291                            let aux_link = link.clone();
2292                            let aux_link_click_handler = link_click_handler.clone();
2293                            this.cursor_pointer()
2294                                .on_click(move |event, window, cx| {
2295                                    crate::TextSelection::end(window, cx);
2296                                    cx.stop_propagation();
2297                                    handle_link_click(
2298                                        &link_click_handler,
2299                                        link.url.clone(),
2300                                        event.clone(),
2301                                        window,
2302                                        cx,
2303                                    );
2304                                })
2305                                .on_aux_click(move |event, window, cx| {
2306                                    crate::TextSelection::end(window, cx);
2307                                    cx.stop_propagation();
2308                                    handle_link_click(
2309                                        &aux_link_click_handler,
2310                                        aux_link.url.clone(),
2311                                        event.clone(),
2312                                        window,
2313                                        cx,
2314                                    );
2315                                })
2316                        })
2317                        .into_any_element(),
2318                );
2319
2320                consumed += text.len();
2321                text.clear();
2322                links.clear();
2323                highlights.clear();
2324                offset = 0;
2325            } else {
2326                let mut node_highlights = vec![];
2327                for (range, style) in &inline_node.marks {
2328                    let inner_range = (offset + range.start)..(offset + range.end);
2329                    let mut highlight = mark_highlight(style, node_cx, cx);
2330
2331                    if let Some(mut link_mark) = style.link.clone() {
2332                        highlight.style.color = Some(node_cx.style.link());
2333                        highlight.style.underline = Some(gpui::UnderlineStyle {
2334                            thickness: gpui::px(1.),
2335                            ..Default::default()
2336                        });
2337
2338                        // convert link references, replace link
2339                        if let Some(identifier) = link_mark.identifier.as_ref() {
2340                            if let Some(mark) = node_cx.link_refs.get(identifier) {
2341                                link_mark = mark.clone();
2342                            }
2343                        }
2344
2345                        links.push((inner_range.clone(), link_mark));
2346                    }
2347
2348                    node_highlights.push((inner_range, highlight));
2349                }
2350
2351                highlights = combine_highlights(highlights, node_highlights);
2352                offset += text_len;
2353            }
2354        }
2355
2356        // Add the last text node
2357        if text.len() > 0 {
2358            let text_end = consumed + text.len();
2359            let highlights = fade_highlights(highlights, &slice_fades(fades, consumed, text_end));
2360            if let Ok(mut state) = self.state.lock() {
2361                state.set_text(text.into());
2362            }
2363            child_nodes.push(
2364                Inline::new(
2365                    self.state.clone(),
2366                    links,
2367                    highlights,
2368                    node_cx.link_click_handler.clone(),
2369                )
2370                .range_backgrounds(slice_backgrounds(backgrounds, consumed, text_end))
2371                .reveal(node_cx.reveal_at(fade_key, consumed, text_end))
2372                .into_any_element(),
2373            );
2374        }
2375
2376        // Text alone is one `Inline`, which needs no box of its own. Images
2377        // keep an identified box: an image's element state is its animation,
2378        // and the box scopes that state per paragraph.
2379        if !has_image {
2380            return child_nodes
2381                .pop()
2382                .unwrap_or_else(|| div().into_any_element());
2383        }
2384
2385        div()
2386            .id(leaf_element_id(fade_key))
2387            .children(child_nodes)
2388            .into_any_element()
2389    }
2390
2391    fn should_render_inline_flow(&self) -> bool {
2392        let has_image = self.children.iter().any(|child| child.image.is_some());
2393        let has_text = self.children.iter().any(|child| !child.text.is_empty());
2394        self.children.iter().any(|child| child.custom.is_some())
2395            || (has_image && has_text)
2396            || self
2397                .children
2398                .iter()
2399                .any(|child| child.marks.iter().any(|(_, mark)| mark.code))
2400    }
2401
2402    fn inline_flow_items(
2403        &self,
2404        leaf_key: Option<TextLeafKey>,
2405        fades: &[(Range<usize>, f32)],
2406        backgrounds: &[(Range<usize>, Hsla)],
2407        node_cx: &NodeContext,
2408        cx: &mut App,
2409    ) -> Vec<InlineFlowItem> {
2410        let mut items = Vec::new();
2411        let mut text = String::new();
2412        let mut highlights: Vec<(Range<usize>, InlineHighlight)> = vec![];
2413        let mut links: Vec<(Range<usize>, LinkMark)> = vec![];
2414        let mut offset = 0;
2415        // Where `text` starts in the paragraph's whole rendered text, which
2416        // is the byte space the fade and background ranges use.
2417        let mut consumed = 0;
2418
2419        for inline_node in &self.children {
2420            if let Some(node) = &inline_node.custom {
2421                if let Ok(mut state) = inline_node.state.lock() {
2422                    state.set_text(text.clone().into());
2423                }
2424                if !text.is_empty() {
2425                    let item_fades = slice_fades(fades, consumed, consumed + text.len());
2426                    let item_backgrounds =
2427                        slice_backgrounds(backgrounds, consumed, consumed + text.len());
2428                    let item_reveal = node_cx.reveal_at(leaf_key, consumed, consumed + text.len());
2429                    consumed += text.len();
2430                    items.push(InlineFlowItem::Text {
2431                        state: inline_node.state.clone(),
2432                        text: std::mem::take(&mut text).into(),
2433                        links: std::mem::take(&mut links),
2434                        highlights: fade_highlights(std::mem::take(&mut highlights), &item_fades),
2435                        backgrounds: item_backgrounds,
2436                        reveal: item_reveal,
2437                    });
2438                }
2439                let mut object_style = HighlightStyle::default();
2440                let mut object_link = None;
2441                for (_, mark) in &inline_node.marks {
2442                    object_style = object_style.highlight(mark_highlight(mark, node_cx, cx).style);
2443                    if let Some(link) = &mark.link {
2444                        object_link = Some(
2445                            link.identifier
2446                                .as_ref()
2447                                .and_then(|id| node_cx.link_refs.get(id))
2448                                .unwrap_or(link)
2449                                .clone(),
2450                        );
2451                        object_style.color = Some(node_cx.style.link());
2452                        object_style.underline = Some(gpui::UnderlineStyle {
2453                            thickness: px(1.),
2454                            ..Default::default()
2455                        });
2456                    }
2457                }
2458                let rendered_node = node.clone();
2459                let extensions = node_cx.markdown_extensions.clone();
2460                items.push(InlineFlowItem::Object {
2461                    text: node.shared_text(),
2462                    accessibility_label: node.shared_accessibility_name(),
2463                    id: node.source_range().map_or(items.len(), |range| range.start),
2464                    renderer: Arc::new(move |context, window, cx| {
2465                        extensions.render_inline(&rendered_node, context, window, cx)
2466                    }),
2467                    selected: inline_node.custom_selection.clone(),
2468                    style: object_style,
2469                    link: object_link,
2470                });
2471                consumed += inline_node.text.len();
2472                offset = 0;
2473                continue;
2474            }
2475            let text_len = inline_node.text.len();
2476            text.push_str(&inline_node.text);
2477
2478            if let Some(image) = &inline_node.image {
2479                if !text.is_empty() {
2480                    if let Ok(mut state) = inline_node.state.lock() {
2481                        state.set_text(text.clone().into());
2482                    }
2483                    items.push(InlineFlowItem::Text {
2484                        state: inline_node.state.clone(),
2485                        text: text.clone().into(),
2486                        links: links.clone(),
2487                        highlights: fade_highlights(
2488                            highlights.clone(),
2489                            &slice_fades(fades, consumed, consumed + text.len()),
2490                        ),
2491                        backgrounds: slice_backgrounds(
2492                            backgrounds,
2493                            consumed,
2494                            consumed + text.len(),
2495                        ),
2496                        reveal: node_cx.reveal_at(leaf_key, consumed, consumed + text.len()),
2497                    });
2498                }
2499
2500                items.push(InlineFlowItem::Image {
2501                    source: node_cx.image_source(image),
2502                    link: image.link.clone(),
2503                    title: image.title(),
2504                    width: image.width,
2505                    height: image.height,
2506                });
2507
2508                consumed += text.len();
2509                text.clear();
2510                links.clear();
2511                highlights.clear();
2512                offset = 0;
2513            } else {
2514                let mut node_highlights = vec![];
2515                for (range, style) in &inline_node.marks {
2516                    let inner_range = (offset + range.start)..(offset + range.end);
2517                    let mut highlight = mark_highlight(style, node_cx, cx);
2518
2519                    if let Some(mut link_mark) = style.link.clone() {
2520                        highlight.style.color = Some(node_cx.style.link());
2521                        highlight.style.underline = Some(gpui::UnderlineStyle {
2522                            thickness: gpui::px(1.),
2523                            ..Default::default()
2524                        });
2525
2526                        if let Some(identifier) = link_mark.identifier.as_ref()
2527                            && let Some(mark) = node_cx.link_refs.get(identifier)
2528                        {
2529                            link_mark = mark.clone();
2530                        }
2531
2532                        links.push((inner_range.clone(), link_mark));
2533                    }
2534
2535                    node_highlights.push((inner_range, highlight));
2536                }
2537
2538                highlights = combine_highlights(highlights, node_highlights);
2539                offset += text_len;
2540            }
2541        }
2542
2543        if !text.is_empty() {
2544            if let Ok(mut state) = self.state.lock() {
2545                state.set_text(text.clone().into());
2546            }
2547            let highlights = fade_highlights(
2548                highlights,
2549                &slice_fades(fades, consumed, consumed + text.len()),
2550            );
2551            let backgrounds = slice_backgrounds(backgrounds, consumed, consumed + text.len());
2552            let reveal = node_cx.reveal_at(leaf_key, consumed, consumed + text.len());
2553            items.push(InlineFlowItem::Text {
2554                state: self.state.clone(),
2555                text: text.into(),
2556                links,
2557                highlights,
2558                backgrounds,
2559                reveal,
2560            });
2561        }
2562
2563        items
2564    }
2565}
2566
2567/// The element id of a block that needs one, from `kind` and the block's
2568/// source start, which is unique across a parsed Markdown document. The HTML
2569/// parser records no spans, so its blocks fall back to their index among
2570/// their siblings: unique among them, though not across nesting levels.
2571fn block_element_id(kind: &'static str, span: Option<Span>, ix: usize) -> ElementId {
2572    (kind, span.map_or(ix, |span| span.start)).into()
2573}
2574
2575/// The element id of a paragraph that needs one: an inline flow, whose
2576/// objects are accessibility nodes, or an image, whose element state is its
2577/// animation. The leaf key is unique per paragraph in a parsed Markdown
2578/// document; the HTML parser records no spans, so its paragraphs share one.
2579fn leaf_element_id(fade_key: Option<TextLeafKey>) -> ElementId {
2580    fade_key.map_or_else(|| ElementId::from("p"), ElementId::from)
2581}
2582
2583/// `block` with `gap` below it. The box only exists to hold the padding,
2584/// so a block with no gap below it is returned as is.
2585fn gapped(block: AnyElement, gap: Rems) -> AnyElement {
2586    if gap.is_zero() {
2587        block
2588    } else {
2589        div().pb(gap).child(block).into_any_element()
2590    }
2591}
2592
2593/// The fade ranges overlapping `start..end`, rebased to start at `start`.
2594fn slice_fades(
2595    fades: &[(Range<usize>, f32)],
2596    start: usize,
2597    end: usize,
2598) -> Vec<(Range<usize>, f32)> {
2599    slice_ranges(fades, start, end, |range, fade_out| (range, *fade_out))
2600}
2601
2602/// The range highlight backgrounds overlapping `start..end`, rebased to
2603/// start at `start`.
2604fn slice_backgrounds(
2605    backgrounds: &[(Range<usize>, Hsla)],
2606    start: usize,
2607    end: usize,
2608) -> Vec<(Range<usize>, Hsla)> {
2609    slice_ranges(backgrounds, start, end, |range, color| (range, *color))
2610}
2611
2612const CELL_PAD_PX: f32 = 16.0; // px_2 horizontal padding
2613const CELL_MIN_PX: f32 = 48.0;
2614const CELL_BORDER_PX: f32 = 1.0; // border_r_1 drawn by every column but the last
2615
2616/// The max-content width of every table column: the widest cell line,
2617/// shaped with the runs the cell renders with, plus the cell's padding and
2618/// border. Never capped: a cap would clip overflowing text *and* leave it
2619/// outside the scrollable width, making it unreachable.
2620fn measure_table_columns(
2621    table: &Table,
2622    col_count: usize,
2623    node_cx: &NodeContext,
2624    window: &mut Window,
2625    cx: &mut App,
2626) -> Vec<f32> {
2627    let text_style = window.text_style();
2628    let rem_size = window.rem_size();
2629    let mono_family = cx.theme().tokens.typography.mono.clone();
2630    let inline_code = node_cx.style.inline_code_highlight();
2631    let cached = table.column_widths_cache.0.lock().ok().and_then(|cache| {
2632        cache
2633            .as_ref()
2634            .filter(|cached| {
2635                cached.widths.len() == col_count
2636                    && cached.text_style == text_style
2637                    && cached.rem_size == rem_size
2638                    && cached.mono_family == mono_family
2639                    && cached.inline_code == inline_code
2640                    && cached
2641                        .text_system
2642                        .upgrade()
2643                        .is_some_and(|system| Arc::ptr_eq(&system, window.text_system()))
2644            })
2645            .cloned()
2646    });
2647    let measured = cached.unwrap_or_else(|| {
2648        let (widths, custom_cells) =
2649            measure_table_text_columns(table, col_count, node_cx, window, cx);
2650        let measured = Arc::new(TableColumnWidths {
2651            text_system: Arc::downgrade(window.text_system()),
2652            text_style,
2653            rem_size,
2654            mono_family,
2655            inline_code,
2656            widths,
2657            custom_cells,
2658        });
2659        if let Ok(mut cache) = table.column_widths_cache.0.lock() {
2660            *cache = Some(measured.clone());
2661        }
2662        measured
2663    });
2664
2665    // Resource-backed cells must be measured again, even when their text is
2666    // unchanged. Keep callbacks outside the cache lock and do not retain their
2667    // widths: a loaded image or a plugin can get narrower as well as wider.
2668    let mut col_w = measured.widths.clone();
2669    for &(row_ix, ix) in &measured.custom_cells {
2670        let cell = &table.children[row_ix].children[ix];
2671        let items = cell.children.inline_flow_items(None, &[], &[], node_cx, cx);
2672        let width = super::inline_flow::intrinsic_width(&items, window, cx);
2673        let border = if ix + 1 < col_count {
2674            CELL_BORDER_PX
2675        } else {
2676            0.
2677        };
2678        col_w[ix] = col_w[ix].max(f32::from(width) + CELL_PAD_PX + border);
2679    }
2680    col_w
2681}
2682
2683// The first measurement still visits all ordinary cells. This cache avoids
2684// repeating that work; it is not a budget for the initial table layout.
2685fn measure_table_text_columns(
2686    table: &Table,
2687    col_count: usize,
2688    node_cx: &NodeContext,
2689    window: &mut Window,
2690    cx: &mut App,
2691) -> (Vec<f32>, Vec<(usize, usize)>) {
2692    let text_style = window.text_style();
2693    let font_size = text_style.font_size.to_pixels(window.rem_size());
2694    let mut col_w = vec![CELL_MIN_PX; col_count];
2695    let mut custom_cells = Vec::new();
2696    for (row_ix, row) in table.children.iter().enumerate() {
2697        for (ix, cell) in row.children.iter().enumerate() {
2698            let Some(slot) = col_w.get_mut(ix) else {
2699                continue;
2700            };
2701            if cell
2702                .children
2703                .children
2704                .iter()
2705                .any(|node| node.custom.is_some())
2706            {
2707                custom_cells.push((row_ix, ix));
2708                continue;
2709            }
2710            let text = cell.children.text();
2711            let highlights = cell.children.inline_highlights(node_cx, cx);
2712            let mut w = 0.0_f32;
2713            let mut line_start = 0;
2714            for line in text.split('\n') {
2715                let start = line_start + (line.len() - line.trim_start().len());
2716                let line_end = line_start + line.len();
2717                line_start = line_end + 1;
2718                let line = line.trim();
2719                if line.is_empty() {
2720                    continue;
2721                }
2722                let end = start + line.len();
2723                let line_highlights = highlights
2724                    .iter()
2725                    .filter_map(|(range, highlight)| {
2726                        let clipped = range.start.max(start)..range.end.min(end);
2727                        (clipped.start < clipped.end).then(|| {
2728                            (
2729                                clipped.start - start..clipped.end - start,
2730                                highlight.clone(),
2731                            )
2732                        })
2733                    })
2734                    .collect::<Vec<_>>();
2735                let mut line_w = gpui::Pixels::ZERO;
2736                for (range, scale) in text_size_ranges(line.len(), &line_highlights) {
2737                    let highlights = slice_ranges(
2738                        &line_highlights,
2739                        range.start,
2740                        range.end,
2741                        |range, highlight| (range, highlight.clone()),
2742                    );
2743                    if highlights.iter().any(|(_, h)| h.font_size_scale.is_some()) {
2744                        line_w += px(crate::text::inline_flow::INLINE_CODE_PADDING * 2.);
2745                    }
2746                    let runs = text_runs(range.len(), &text_style, &highlights);
2747                    line_w += window
2748                        .text_system()
2749                        .layout_line(&line[range], font_size * scale, &runs, None)
2750                        .width;
2751                }
2752                w = w.max(f32::from(line_w));
2753            }
2754            // Border-box widths, so the padding and border the cell draws
2755            // must leave the measured text its full width.
2756            let border = if ix + 1 < col_count {
2757                CELL_BORDER_PX
2758            } else {
2759                0.
2760            };
2761            *slot = slot.max(w + CELL_PAD_PX + border);
2762        }
2763    }
2764    (col_w, custom_cells)
2765}
2766
2767impl Paragraph {
2768    fn to_markdown(&self) -> String {
2769        if self.children.iter().any(|node| node.custom.is_some()) {
2770            let mut source = MarkdownSource::default();
2771            for node in &self.children {
2772                if node.custom.is_some() {
2773                    source.push_object(node);
2774                } else {
2775                    source.push_text(&node.text, &node.marks, 0..node.text.len());
2776                    if let Some(image) = &node.image {
2777                        source.push_str(&image_markdown(image));
2778                    }
2779                }
2780            }
2781            let mut text = source.finish();
2782            text.push_str("\n\n");
2783            return text;
2784        }
2785        let mut text = self
2786            .children
2787            .iter()
2788            .map(|text_node| {
2789                let mut text = text_node.text.to_string();
2790                for (range, style) in &text_node.marks {
2791                    if style.bold {
2792                        text = format!("**{}**", &text_node.text[range.clone()]);
2793                    }
2794                    if style.italic {
2795                        text = format!("*{}*", &text_node.text[range.clone()]);
2796                    }
2797                    if style.strikethrough {
2798                        text = format!("~~{}~~", &text_node.text[range.clone()]);
2799                    }
2800                    if style.code {
2801                        text = format!("`{}`", &text_node.text[range.clone()]);
2802                    }
2803                    if style.highlight.is_some() {
2804                        text = format!("=={}==", &text_node.text[range.clone()]);
2805                    }
2806                    if let Some(link) = &style.link {
2807                        text = format!("[{}]({})", &text_node.text[range.clone()], link.url);
2808                    }
2809                }
2810
2811                if let Some(image) = &text_node.image {
2812                    let alt = image.alt.clone().unwrap_or_default();
2813                    let title = image
2814                        .title
2815                        .clone()
2816                        .map_or(String::new(), |t| format!(" \"{}\"", t));
2817                    text.push_str(&format!("![{}]({}{})", alt, image.url, title))
2818                }
2819
2820                text
2821            })
2822            .collect::<Vec<_>>()
2823            .join("");
2824
2825        text.push_str("\n\n");
2826        text
2827    }
2828}
2829
2830impl BlockNode {
2831    /// Converts the node to markdown format.
2832    ///
2833    /// This is used to generate markdown for test.
2834    #[allow(dead_code)]
2835    pub(crate) fn to_markdown(&self) -> String {
2836        match self {
2837            BlockNode::Root { children, .. } => children
2838                .iter()
2839                .map(|child| child.to_markdown())
2840                .collect::<Vec<_>>()
2841                .join("\n\n"),
2842            BlockNode::Paragraph(paragraph) => paragraph.to_markdown(),
2843            BlockNode::Heading {
2844                level, children, ..
2845            } => {
2846                let hashes = "#".repeat(*level as usize);
2847                format!("{} {}", hashes, children.to_markdown())
2848            }
2849            BlockNode::Blockquote { children, .. } => {
2850                let content = children
2851                    .iter()
2852                    .map(|child| child.to_markdown())
2853                    .collect::<Vec<_>>()
2854                    .join("\n\n");
2855
2856                content
2857                    .lines()
2858                    .map(|line| format!("> {}", line))
2859                    .collect::<Vec<_>>()
2860                    .join("\n")
2861            }
2862            BlockNode::List {
2863                children,
2864                ordered,
2865                start,
2866                ..
2867            } => children
2868                .iter()
2869                .enumerate()
2870                .map(|(i, child)| {
2871                    let prefix = if *ordered {
2872                        format!("{}. ", ordered_list_ordinal(*start, i))
2873                    } else {
2874                        "- ".to_string()
2875                    };
2876                    format!("{}{}", prefix, child.to_markdown())
2877                })
2878                .collect::<Vec<_>>()
2879                .join("\n"),
2880            BlockNode::ListItem {
2881                children, checked, ..
2882            } => {
2883                let checkbox = if let Some(checked) = checked {
2884                    if *checked { "[x] " } else { "[ ] " }
2885                } else {
2886                    ""
2887                };
2888                format!(
2889                    "{}{}",
2890                    checkbox,
2891                    children
2892                        .iter()
2893                        .map(|child| child.to_markdown())
2894                        .collect::<Vec<_>>()
2895                        .join("\n")
2896                )
2897            }
2898            BlockNode::CodeBlock(code_block) => {
2899                format!(
2900                    "```{}\n{}\n```",
2901                    code_block.lang.clone().unwrap_or_default(),
2902                    code_block.code()
2903                )
2904            }
2905            BlockNode::Table(table) => table.to_markdown(),
2906            BlockNode::Break { html, .. } => {
2907                if *html {
2908                    "<br>".to_string()
2909                } else {
2910                    "\n".to_string()
2911                }
2912            }
2913            BlockNode::HorizontalRule { .. } => "---".to_string(),
2914            BlockNode::Custom(node) => node.to_markdown(),
2915            BlockNode::Definition {
2916                identifier,
2917                url,
2918                title,
2919                ..
2920            } => {
2921                if let Some(title) = title {
2922                    format!("[{}]: {} \"{}\"", identifier, url, title)
2923                } else {
2924                    format!("[{}]: {}", identifier, url)
2925                }
2926            }
2927            BlockNode::Unknown { .. } => "".to_string(),
2928        }
2929        .trim()
2930        .to_string()
2931    }
2932}
2933
2934impl BlockNode {
2935    fn render_list_item_row(
2936        content: AnyElement,
2937        ix: usize,
2938        options: NodeRenderOptions,
2939        checked: Option<bool>,
2940        style: &TextViewStyle,
2941        line_height: Pixels,
2942    ) -> Div {
2943        h_flex()
2944            .w_full()
2945            .min_w_0()
2946            .relative()
2947            .items_start()
2948            .content_start()
2949            .when(!options.todo && checked.is_none(), |this| {
2950                this.child(list_item_prefix(
2951                    ix,
2952                    options.list_start,
2953                    options.ordered,
2954                    options.depth,
2955                ))
2956            })
2957            .when_some(checked, |this, checked| {
2958                // Todo list checkbox
2959                let check_svg = if style.is_dark() {
2960                    CHECK_SVG_DARK
2961                } else {
2962                    CHECK_SVG_LIGHT
2963                };
2964                this.child(
2965                    div()
2966                        .flex()
2967                        .mr_1p5()
2968                        .h(line_height)
2969                        .flex_none()
2970                        .items_center()
2971                        .justify_center()
2972                        .child(
2973                            div()
2974                                .flex()
2975                                .size(rems(0.875))
2976                                .items_center()
2977                                .justify_center()
2978                                .border_1()
2979                                .border_color(style.foreground())
2980                                .when(checked, |this| {
2981                                    this.bg(style.foreground()).child(
2982                                        img(Arc::new(Image::from_bytes(
2983                                            ImageFormat::Svg,
2984                                            check_svg.to_vec(),
2985                                        )))
2986                                        .size(rems(0.625)),
2987                                    )
2988                                }),
2989                        ),
2990                )
2991            })
2992            .child(div().flex_1().min_w_0().overflow_hidden().child(content))
2993    }
2994
2995    fn render_list_item(
2996        item: &BlockNode,
2997        ix: usize,
2998        options: NodeRenderOptions,
2999        node_cx: &NodeContext,
3000        window: &mut Window,
3001        cx: &mut App,
3002    ) -> AnyElement {
3003        match item {
3004            BlockNode::ListItem {
3005                children,
3006                spread,
3007                checked,
3008                ..
3009            } => div()
3010                .w_full()
3011                .min_w_0()
3012                .when(*spread, |this| this.child(div()))
3013                .children({
3014                    let mut items: Vec<Div> = Vec::with_capacity(children.len());
3015
3016                    for (child_ix, child) in children.iter().enumerate() {
3017                        match child {
3018                            BlockNode::Paragraph { .. } => {
3019                                let last_not_list = child_ix > 0
3020                                    && !matches!(children[child_ix - 1], BlockNode::List { .. });
3021
3022                                let text = child.render_block(
3023                                    NodeRenderOptions {
3024                                        depth: options.depth + 1,
3025                                        todo: checked.is_some(),
3026                                        is_last: true,
3027                                        ..options
3028                                    },
3029                                    node_cx,
3030                                    window,
3031                                    cx,
3032                                );
3033
3034                                // Continuation paragraph — stack vertically below
3035                                // the previous row, indented to align with the text
3036                                // column (past bullet/number prefix).
3037                                if last_not_list {
3038                                    if let Some(preceding_row) = items.pop() {
3039                                        items.push(
3040                                            div().child(preceding_row).child(
3041                                                div()
3042                                                    .w_full()
3043                                                    .pl(rems(1.))
3044                                                    .overflow_hidden()
3045                                                    .child(text),
3046                                            ),
3047                                        );
3048                                        continue;
3049                                    }
3050                                }
3051
3052                                items.push(Self::render_list_item_row(
3053                                    text,
3054                                    ix,
3055                                    options,
3056                                    *checked,
3057                                    &node_cx.style,
3058                                    window.line_height(),
3059                                ));
3060                            }
3061                            BlockNode::List { .. } => {
3062                                items.push(div().ml(rems(1.)).child(child.render_block(
3063                                    NodeRenderOptions {
3064                                        depth: options.depth + 1,
3065                                        todo: checked.is_some(),
3066                                        is_last: true,
3067                                        ..options
3068                                    },
3069                                    node_cx,
3070                                    window,
3071                                    cx,
3072                                )));
3073                            }
3074                            BlockNode::Root { .. }
3075                            | BlockNode::Heading { .. }
3076                            | BlockNode::Blockquote { .. }
3077                            | BlockNode::CodeBlock(_)
3078                            | BlockNode::Custom(_)
3079                            | BlockNode::Table(_)
3080                            | BlockNode::HorizontalRule { .. } => {
3081                                let block = child.render_block(
3082                                    NodeRenderOptions {
3083                                        depth: options.depth + 1,
3084                                        todo: checked.is_some(),
3085                                        is_last: true,
3086                                        ..options
3087                                    },
3088                                    node_cx,
3089                                    window,
3090                                    cx,
3091                                );
3092
3093                                if child_ix == 0 {
3094                                    items.push(Self::render_list_item_row(
3095                                        block,
3096                                        ix,
3097                                        options,
3098                                        *checked,
3099                                        &node_cx.style,
3100                                        window.line_height(),
3101                                    ));
3102                                } else {
3103                                    // Indent continuation blocks to align with a
3104                                    // nested sub-list (`ml(rems(1.))`) and with
3105                                    // continuation paragraphs.
3106                                    items.push(
3107                                        div()
3108                                            .w_full()
3109                                            .min_w_0()
3110                                            .pl(rems(1.))
3111                                            .overflow_hidden()
3112                                            .child(block),
3113                                    );
3114                                }
3115                            }
3116                            BlockNode::ListItem { .. }
3117                            | BlockNode::Break { .. }
3118                            | BlockNode::Definition { .. }
3119                            | BlockNode::Unknown => {}
3120                        }
3121                    }
3122                    items
3123                })
3124                .into_any_element(),
3125            _ => div().into_any_element(),
3126        }
3127    }
3128
3129    /// Render a Markdown table. Dispatches to a horizontally scrollable layout
3130    /// when `style.table` opts in with overflow-x: scroll, otherwise to the
3131    /// default layout that fits the container width and wraps cell content.
3132    fn render_table(
3133        item: &BlockNode,
3134        options: &NodeRenderOptions,
3135        node_cx: &NodeContext,
3136        window: &mut Window,
3137        cx: &mut App,
3138    ) -> impl IntoElement {
3139        const DEFAULT_LENGTH: usize = 5;
3140
3141        let table = match item {
3142            BlockNode::Table(table) => table,
3143            _ => return div().into_any_element(),
3144        };
3145
3146        // Scroll mode is opted in via `style.table` overflow-x: scroll.
3147        if matches!(node_cx.style.table().overflow.x, Some(Overflow::Scroll)) {
3148            let col_count = table
3149                .children
3150                .iter()
3151                .map(|row| row.children.len())
3152                .max()
3153                .unwrap_or(0);
3154            return Self::render_scroll_table(table, col_count, options, node_cx, window, cx);
3155        }
3156
3157        // Per-column max text length (in chars), used to proportion the columns
3158        // in the default (wrap) layout.
3159        let mut col_lens: Vec<usize> = vec![];
3160        for row in table.children.iter() {
3161            for (ix, cell) in row.children.iter().enumerate() {
3162                if col_lens.len() <= ix {
3163                    col_lens.push(DEFAULT_LENGTH);
3164                }
3165                col_lens[ix] = col_lens[ix].max(cell.children.text_len());
3166            }
3167        }
3168
3169        Self::render_wrap_table(table, &col_lens, options, node_cx, window, cx)
3170    }
3171
3172    /// Horizontally scrollable table layout (opt-in via `style.table`
3173    /// overflow-x: scroll).
3174    ///
3175    /// Column widths come from the **measured** shaped text of each cell (the
3176    /// widest per column across all rows), so columns line up and fit their
3177    /// content exactly — char-count heuristics are inaccurate on proportional
3178    /// fonts. The layout adapts to the frame like CSS auto table layout:
3179    ///
3180    /// - Wider than the content: cells `flex_grow` proportionally to fill.
3181    /// - Narrower: columns shrink and their text wraps, but not below a
3182    ///   per-column floor.
3183    /// - Narrower than the floors: the table keeps the floor widths and
3184    ///   scrolls horizontally, so no content ever becomes unreachable.
3185    ///
3186    /// `white_space: nowrap` on `style.table_cell` composes like in CSS: the
3187    /// refinement keeps cell text on a single line, and the floors are raised
3188    /// to the full content widths so the single-line columns never shrink —
3189    /// the table scrolls as soon as the content is wider than the frame.
3190    fn render_scroll_table(
3191        table: &Table,
3192        col_count: usize,
3193        options: &NodeRenderOptions,
3194        node_cx: &NodeContext,
3195        window: &mut Window,
3196        cx: &mut App,
3197    ) -> AnyElement {
3198        // Shrinking columns stop (and the table starts to scroll) at a floor
3199        // scaled to their content: roughly the width at which the text wraps
3200        // to `CELL_WRAP_MAX_LINES` lines, clamped between the two bounds so
3201        // moderate columns can still wrap meaningfully while one huge column
3202        // cannot push the scroll threshold arbitrarily high.
3203        const CELL_WRAP_MAX_LINES: f32 = 2.0;
3204        const CELL_WRAP_MIN_PX: f32 = 160.0;
3205        const CELL_WRAP_MAX_PX: f32 = 480.0;
3206        const TABLE_BORDER_PX: f32 = 2.0; // the track's border_1, left + right
3207
3208        let col_w = measure_table_columns(table, col_count, node_cx, window, cx);
3209        let style = &node_cx.style;
3210        // Nowrap cells (via the `table_cell` refinement, which cascades to
3211        // the cell text) must never shrink below their single-line content,
3212        // so their floor is the content width itself.
3213        let nowrap = style.table_cell().text.white_space == Some(WhiteSpace::Nowrap);
3214        let col_min_w: Vec<f32> = if nowrap {
3215            col_w.clone()
3216        } else {
3217            col_w
3218                .iter()
3219                .map(|w| {
3220                    (w / CELL_WRAP_MAX_LINES)
3221                        .clamp(CELL_WRAP_MIN_PX, CELL_WRAP_MAX_PX)
3222                        .min(*w)
3223                })
3224                .collect()
3225        };
3226        let min_total_w: f32 = col_min_w.iter().sum::<f32>() + TABLE_BORDER_PX;
3227
3228        let scroll_handle = window
3229            .use_keyed_state(
3230                block_element_id("table-scroll", table.span, options.ix),
3231                cx,
3232                |_, _| ScrollHandle::default(),
3233            )
3234            .read(cx)
3235            .clone();
3236        let row_count = table.children.len();
3237        let mut rows = Vec::with_capacity(row_count);
3238        let mut cell_ordinal = 0;
3239        for (row_ix, row) in table.children.iter().enumerate() {
3240            let mut cells = Vec::with_capacity(row.children.len());
3241            for (ix, cell) in row.children.iter().enumerate() {
3242                let fade_key = table
3243                    .span
3244                    .map(|span| TextLeafKey::table_cell(span.start, cell_ordinal));
3245                cell_ordinal += 1;
3246                let align = table.column_align(ix);
3247                let is_last_col = ix == row.children.len() - 1;
3248                let width = col_w.get(ix).copied().unwrap_or(CELL_MIN_PX);
3249                let min_width = col_min_w.get(ix).copied().unwrap_or(CELL_MIN_PX);
3250                cells.push(
3251                    div()
3252                        // Measured max-content width is the flex-basis;
3253                        // `flex_grow` (proportional to it) distributes extra
3254                        // space so a narrow table still fills the frame, while
3255                        // shrinking is clamped at `min_w` — the flex engine
3256                        // squeezes columns (their text wraps) down to the
3257                        // floors before the track starts to scroll.
3258                        .flex_basis(px(width))
3259                        .flex_grow(width)
3260                        .flex_shrink(1.)
3261                        .min_w(px(min_width))
3262                        .overflow_hidden()
3263                        .when(align == ColumnumnAlign::Center, |this| this.text_center())
3264                        .when(align == ColumnumnAlign::Right, |this| this.text_right())
3265                        .px_2()
3266                        .py_1()
3267                        .when(!is_last_col, |this| {
3268                            this.border_r_1().border_color(style.border())
3269                        })
3270                        .refine_style(&style.table_cell())
3271                        .child(cell.children.render(fade_key, node_cx, window, cx)),
3272                );
3273            }
3274            rows.push(
3275                div()
3276                    .w_full()
3277                    .when(row_ix < row_count - 1, |this| this.border_b_1())
3278                    .border_color(style.border())
3279                    .flex()
3280                    .flex_row()
3281                    // The first row is the header, as everywhere else that
3282                    // reads a table (`table_data`, `to_markdown`). The
3283                    // refinement comes last so it can override the defaults.
3284                    .when(row_ix == 0, |this| {
3285                        this.bg(style.code_background())
3286                            .text_color(style.foreground())
3287                            .refine_style(&style.table_head())
3288                    })
3289                    .children(cells),
3290            );
3291        }
3292
3293        div()
3294            .pb(rems(1.))
3295            .w_full()
3296            .child(
3297                // Scroll viewport owns the visible frame, including any
3298                // caller-provided radius. Keeping the border here makes the
3299                // rounded frame stable while the wider row track moves below it.
3300                //
3301                // `horizontal_scroll_area` clips with `overflow_hidden` and
3302                // delegates the wheel to a sibling `ScrollableMask`, so the
3303                // gesture is locked to its starting axis and a horizontal swipe
3304                // is consumed before an ancestor scroller (`gpui::list` under
3305                // `TextView::scrollable`) can take its vertical component.
3306                horizontal_scroll_area(
3307                    block_element_id("table", table.span, options.ix),
3308                    &scroll_handle,
3309                    &StyleRefinement::default()
3310                        .bg(style
3311                            .table_background()
3312                            .unwrap_or(cx.theme().tokens.colors.surface))
3313                        .border_1()
3314                        .border_color(style.border())
3315                        .refine_style(style.table()),
3316                    // Row track sized to `max(viewport, column floors)`:
3317                    // `min_w_full` fills the frame while the columns can still
3318                    // shrink-to-fit (their text wrapping), the definite
3319                    // `w(min_total_w)` keeps the floors once they are reached,
3320                    // letting the track exceed the viewport and scroll.
3321                    div().min_w_full().w(px(min_total_w)).children(rows),
3322                ),
3323            )
3324            // Custom actions row (e.g. copy / download) rendered below the
3325            // table. The hook's element spans full width; alignment is up to
3326            // the caller (e.g. `h_flex().justify_end()`). The gap keeps hover
3327            // backgrounds of the action buttons off the table border, and the
3328            // id scopes the caller's element ids per table, so plain ids like
3329            // `"copy"` don't collide across tables (same as code blocks).
3330            .children(node_cx.table_actions.clone().map(|f| {
3331                div()
3332                    .id(block_element_id("table-actions", table.span, options.ix))
3333                    .mt_1()
3334                    .child(f(&table.cached_table_data(), window, cx))
3335            }))
3336            .into_any_element()
3337    }
3338
3339    /// Default table layout: a flex grid whose columns are proportioned by
3340    /// content length and shrink to fit the container width (cell text wraps).
3341    fn render_wrap_table(
3342        table: &Table,
3343        col_lens: &[usize],
3344        options: &NodeRenderOptions,
3345        node_cx: &NodeContext,
3346        window: &mut Window,
3347        cx: &mut App,
3348    ) -> AnyElement {
3349        const MAX_LENGTH: usize = 150;
3350
3351        let style = &node_cx.style;
3352        let row_count = table.children.len();
3353        let mut rows = Vec::with_capacity(row_count);
3354        let mut cell_ordinal = 0;
3355        for (row_ix, row) in table.children.iter().enumerate() {
3356            let mut cells = Vec::with_capacity(row.children.len());
3357            for (ix, cell) in row.children.iter().enumerate() {
3358                let fade_key = table
3359                    .span
3360                    .map(|span| TextLeafKey::table_cell(span.start, cell_ordinal));
3361                cell_ordinal += 1;
3362                let align = table.column_align(ix);
3363                let is_last_col = ix == row.children.len() - 1;
3364                let len = col_lens
3365                    .get(ix)
3366                    .copied()
3367                    .unwrap_or(MAX_LENGTH)
3368                    .min(MAX_LENGTH);
3369
3370                cells.push(
3371                    div()
3372                        .overflow_hidden()
3373                        .when(align == ColumnumnAlign::Center, |this| this.text_center())
3374                        .when(align == ColumnumnAlign::Right, |this| this.text_right())
3375                        .min_w_16()
3376                        .w(Length::Definite(relative(len as f32)))
3377                        .px_2()
3378                        .py_1()
3379                        .when(!is_last_col, |this| {
3380                            this.border_r_1().border_color(style.border())
3381                        })
3382                        .refine_style(&style.table_cell())
3383                        .child(cell.children.render(fade_key, node_cx, window, cx)),
3384                );
3385            }
3386
3387            rows.push(
3388                div()
3389                    .w_full()
3390                    .when(row_ix < row_count - 1, |this| this.border_b_1())
3391                    .border_color(style.border())
3392                    .flex()
3393                    .flex_row()
3394                    // The first row is the header, as everywhere else that
3395                    // reads a table (`table_data`, `to_markdown`). The
3396                    // refinement comes last so it can override the defaults.
3397                    .when(row_ix == 0, |this| {
3398                        this.bg(style.code_background())
3399                            .text_color(style.foreground())
3400                            .refine_style(&style.table_head())
3401                    })
3402                    .children(cells),
3403            );
3404        }
3405
3406        div()
3407            .pb(rems(1.))
3408            .w_full()
3409            .child(
3410                div()
3411                    .w_full()
3412                    .bg(style
3413                        .table_background()
3414                        .unwrap_or(cx.theme().tokens.colors.surface))
3415                    .border_1()
3416                    .border_color(style.border())
3417                    .overflow_hidden()
3418                    .children(rows)
3419                    .refine_style(&style.table()),
3420            )
3421            // Custom actions row (e.g. copy / download) rendered below the
3422            // table. The hook's element spans full width; alignment is up to
3423            // the caller (e.g. `h_flex().justify_end()`). The gap keeps hover
3424            // backgrounds of the action buttons off the table border, and the
3425            // id scopes the caller's element ids per table, so plain ids like
3426            // `"copy"` don't collide across tables (same as code blocks).
3427            .children(node_cx.table_actions.clone().map(|f| {
3428                div()
3429                    .id(block_element_id("table-actions", table.span, options.ix))
3430                    .mt_1()
3431                    .child(f(&table.cached_table_data(), window, cx))
3432            }))
3433            .into_any_element()
3434    }
3435
3436    pub(crate) fn render_block(
3437        &self,
3438        options: NodeRenderOptions,
3439        node_cx: &NodeContext,
3440        window: &mut Window,
3441        cx: &mut App,
3442    ) -> AnyElement {
3443        let mb = if options.in_list || options.is_last {
3444            rems(0.)
3445        } else {
3446            node_cx.style.paragraph_gap()
3447        };
3448
3449        match self {
3450            BlockNode::Root { children, .. } => div()
3451                .children(children.into_iter().enumerate().map(move |(ix, node)| {
3452                    node.render_block(NodeRenderOptions { ix, ..options }, node_cx, window, cx)
3453                }))
3454                .into_any_element(),
3455            BlockNode::Paragraph(paragraph) => gapped(
3456                paragraph.render(
3457                    paragraph.span.map(|span| TextLeafKey::block(span.start)),
3458                    node_cx,
3459                    window,
3460                    cx,
3461                ),
3462                mb,
3463            ),
3464            BlockNode::Heading {
3465                level,
3466                children,
3467                span,
3468            } => {
3469                let (text_size, font_weight) = match level {
3470                    1 => (rems(2.), FontWeight::BOLD),
3471                    2 => (rems(1.5), FontWeight::SEMIBOLD),
3472                    3 => (rems(1.25), FontWeight::SEMIBOLD),
3473                    4 => (rems(1.125), FontWeight::SEMIBOLD),
3474                    5 => (rems(1.), FontWeight::SEMIBOLD),
3475                    6 => (rems(1.), FontWeight::MEDIUM),
3476                    _ => (rems(1.), FontWeight::NORMAL),
3477                };
3478
3479                let text_size = text_size.to_pixels(px(14.));
3480
3481                div()
3482                    .pb(rems(0.3))
3483                    .whitespace_normal()
3484                    .text_size(text_size)
3485                    .font_weight(font_weight)
3486                    .refine_style(&node_cx.style.heading(*level))
3487                    .child(children.render(
3488                        span.map(|span| TextLeafKey::block(span.start)),
3489                        node_cx,
3490                        window,
3491                        cx,
3492                    ))
3493                    .into_any_element()
3494            }
3495            BlockNode::Blockquote { children, .. } => gapped(
3496                div()
3497                    .w_full()
3498                    .text_color(node_cx.style.muted_foreground())
3499                    .border_l_3()
3500                    .border_color(node_cx.style.border())
3501                    .px_4()
3502                    .children({
3503                        let children_len = children.len();
3504                        children.into_iter().enumerate().map(move |(index, c)| {
3505                            let is_last = index == children_len - 1;
3506                            c.render_block(options.is_last(is_last), node_cx, window, cx)
3507                        })
3508                    })
3509                    .into_any_element(),
3510                mb,
3511            ),
3512            BlockNode::List {
3513                children,
3514                ordered,
3515                start,
3516                ..
3517            } => div()
3518                .w_full()
3519                .min_w_0()
3520                .pb(mb)
3521                .children({
3522                    let mut items = Vec::with_capacity(children.len());
3523                    let mut item_index = 0;
3524                    for (ix, item) in children.into_iter().enumerate() {
3525                        let is_item = item.is_list_item();
3526
3527                        items.push(Self::render_list_item(
3528                            item,
3529                            item_index,
3530                            NodeRenderOptions {
3531                                ix,
3532                                ordered: *ordered,
3533                                list_start: *start,
3534                                ..options
3535                            },
3536                            node_cx,
3537                            window,
3538                            cx,
3539                        ));
3540
3541                        if is_item {
3542                            item_index += 1;
3543                        }
3544                    }
3545                    items
3546                })
3547                .into_any_element(),
3548            BlockNode::CodeBlock(code_block) => code_block.render(&options, node_cx, window, cx),
3549            BlockNode::Custom(node) => {
3550                let inner = match node_cx.markdown_extensions.render_block(node, window, cx) {
3551                    Some(rendered) => rendered,
3552                    None => div().child(node.as_text().to_string()).into_any_element(),
3553                };
3554
3555                div().pb(mb).child(inner).into_any_element()
3556            }
3557            BlockNode::Table { .. } => {
3558                Self::render_table(self, &options, node_cx, window, cx).into_any_element()
3559            }
3560            BlockNode::HorizontalRule { .. } => gapped(
3561                div()
3562                    .bg(node_cx.style.border())
3563                    .h(px(2.))
3564                    .into_any_element(),
3565                mb,
3566            ),
3567            BlockNode::Break { .. } => div().into_any_element(),
3568            BlockNode::Unknown { .. } | BlockNode::Definition { .. } => div().into_any_element(),
3569            _ => {
3570                if cfg!(debug_assertions) {
3571                    tracing::warn!("unknown implementation: {:?}", self);
3572                }
3573
3574                div().into_any_element()
3575            }
3576        }
3577    }
3578}
3579
3580#[cfg(test)]
3581mod tests {
3582    use super::*;
3583
3584    #[test]
3585    fn selected_inline_objects_coalesce_surrounding_emphasis() {
3586        for (object_mark, expected) in [
3587            (TextMark::default().italic(), "*fore $x$ aft*"),
3588            (TextMark::default().italic().bold(), "*fore **$x$** aft*"),
3589        ] {
3590            let italic = TextMark::default().italic();
3591            let before = InlineNode::new("before ").marks(vec![(0..7, italic.clone())]);
3592            let formula =
3593                InlineNode::custom(MarkdownNode::new("math", ()).text("x").markdown("$x$"))
3594                    .marks(vec![(0..1, object_mark)]);
3595            let after = InlineNode::new(" after").marks(vec![(0..6, italic)]);
3596            {
3597                let mut state = formula.state.lock().unwrap();
3598                state.text = "before ".into();
3599                state.selection = Some((2..7).into());
3600            }
3601            *formula.custom_selection.lock().unwrap() = true;
3602            let paragraph = Paragraph {
3603                children: vec![before, formula, after],
3604                ..Default::default()
3605            };
3606            {
3607                let mut state = paragraph.state.lock().unwrap();
3608                state.text = " after".into();
3609                state.selection = Some((0..4).into());
3610            }
3611            assert_eq!(paragraph.selected_source(), expected);
3612        }
3613    }
3614
3615    #[test]
3616    fn consecutive_inline_objects_copy_atomically_without_neighboring_text() {
3617        let first =
3618            InlineNode::custom(MarkdownNode::new("math", ()).text("甲²").markdown("$甲^2$"));
3619        let second = InlineNode::custom(MarkdownNode::new("math", ()).text("b").markdown("$b$"));
3620        *first.custom_selection.lock().unwrap() = true;
3621        *second.custom_selection.lock().unwrap() = true;
3622        let paragraph = Paragraph {
3623            children: vec![first, second],
3624            ..Default::default()
3625        };
3626        assert_eq!(paragraph.selected_text(), "甲²b");
3627        assert_eq!(paragraph.selected_source(), "$甲^2$$b$");
3628        assert!(paragraph.has_selection());
3629        paragraph.clear_selection();
3630        assert_eq!(paragraph.selected_text(), "");
3631        assert_eq!(paragraph.selected_source(), "");
3632        assert!(!paragraph.has_selection());
3633    }
3634
3635    #[test]
3636    fn selected_inline_object_interleaves_runs_and_preserves_enclosing_mark() {
3637        let before = InlineNode::new("中文 ");
3638        let formula =
3639            InlineNode::custom(MarkdownNode::new("math", ()).text("x²").markdown("$x^2$"))
3640                .marks(vec![(0..3, TextMark::default().bold())]);
3641        let after = InlineNode::new(" English");
3642        {
3643            let mut preceding = formula.state.lock().unwrap();
3644            preceding.text = "中文 ".into();
3645            preceding.selection = Some((3..7).into());
3646        }
3647        *formula.custom_selection.lock().unwrap() = true;
3648        let paragraph = Paragraph {
3649            children: vec![before, formula, after],
3650            ..Default::default()
3651        };
3652        {
3653            let mut trailing = paragraph.state.lock().unwrap();
3654            trailing.text = " English".into();
3655            trailing.selection = Some((0..4).into());
3656        }
3657        assert_eq!(paragraph.selected_text(), "文 x² Eng");
3658        assert_eq!(paragraph.selected_source(), "文 **$x^2$** Eng");
3659    }
3660
3661    #[test]
3662    fn custom_inline_inherits_marks_and_resolves_link_references() {
3663        use gpui::{Empty, TestApp};
3664        let mut app = TestApp::new();
3665        let mut window = app.open_window(|_, _| Empty);
3666        window.update(|_, _, cx| {
3667            cx.set_global(crate::Theme::default());
3668            let mut node_cx = NodeContext::default();
3669            let mut mark = TextMark::default().bold();
3670            mark.italic = true;
3671            mark.strikethrough = true;
3672            mark.link = Some(LinkMark {
3673                identifier: Some("ref".into()),
3674                ..Default::default()
3675            });
3676            node_cx.link_refs.insert(
3677                "ref".into(),
3678                LinkMark {
3679                    url: "https://example.com".into(),
3680                    ..Default::default()
3681                },
3682            );
3683            let paragraph = Paragraph {
3684                children: vec![
3685                    InlineNode::custom(MarkdownNode::new("test", ()).text("x"))
3686                        .marks(vec![(0..1, mark)]),
3687                ],
3688                ..Default::default()
3689            };
3690            let items = paragraph.inline_flow_items(None, &[], &[], &node_cx, cx);
3691            let InlineFlowItem::Object { style, link, .. } = &items[0] else {
3692                panic!()
3693            };
3694            assert_eq!(style.font_weight, Some(FontWeight::BOLD));
3695            assert_eq!(style.font_style, Some(FontStyle::Italic));
3696            assert!(style.strikethrough.is_some());
3697            assert_eq!(link.as_ref().unwrap().url.as_ref(), "https://example.com");
3698        });
3699    }
3700
3701    #[test]
3702    fn table_column_uses_prepared_inline_metrics_after_resource_update() {
3703        use crate::text::InlineElement;
3704        use crate::text::inline::test_draw::in_prepaint;
3705        use gpui::{Image, ImageFormat, TestApp};
3706        let mut app = TestApp::new();
3707        let width = Arc::new(std::sync::atomic::AtomicUsize::new(400));
3708        let render_width = width.clone();
3709        let mut node_cx = NodeContext::default();
3710        node_cx.markdown_extensions = Arc::new(MarkdownExtensions::default().plugin(
3711            crate::text::markdown_ext::TestInlinePlugin::new("test").render_with(
3712                move |_, _, _, _| {
3713                    Some(
3714                        InlineElement::new(
3715                            gpui::img(Arc::new(Image::from_bytes(
3716                                ImageFormat::Svg,
3717                                b"<svg/>".to_vec(),
3718                            )))
3719                            .w(px(
3720                                render_width.load(std::sync::atomic::Ordering::Relaxed) as f32
3721                            ))
3722                            .h(px(20.)),
3723                        )
3724                        .with_baseline(px(15.)),
3725                    )
3726                },
3727            ),
3728        ));
3729        let table = table_of(
3730            vec![
3731                vec![plain_cell("ordinary text")],
3732                vec![TableCell {
3733                    children: Paragraph {
3734                        children: vec![InlineNode::custom(MarkdownNode::new("test", ()).text("x"))],
3735                        ..Default::default()
3736                    },
3737                    width: None,
3738                }],
3739            ],
3740            vec![],
3741        );
3742        in_prepaint(&mut app, move |window, cx| {
3743            let mut cached = None;
3744            for expected in [400, 600, 10, 400] {
3745                width.store(expected, std::sync::atomic::Ordering::Relaxed);
3746                let measured = measure_table_columns(&table, 1, &node_cx, window, cx)[0];
3747                let current = cached_column_widths(&table);
3748                assert_eq!(
3749                    measured,
3750                    current.widths[0].max(expected as f32 + CELL_PAD_PX)
3751                );
3752                assert_eq!(current.custom_cells, vec![(1, 0)]);
3753                if let Some(cached) = cached.replace(current.clone()) {
3754                    assert!(Arc::ptr_eq(&cached, &current));
3755                }
3756            }
3757        });
3758    }
3759
3760    fn cached_column_widths(table: &Table) -> Arc<TableColumnWidths> {
3761        table.column_widths_cache.0.lock().unwrap().clone().unwrap()
3762    }
3763
3764    #[test]
3765    fn table_columns_cache_preserves_plain_widths_and_clones() {
3766        use crate::text::inline::test_draw::in_prepaint;
3767        use crate::text::inline::test_fonts::{BODY, WideMonoTextSystem, record_shaped_lines};
3768        use gpui::{Refineable as _, TestApp, TextStyle};
3769
3770        let long = "é界".repeat(2048);
3771        let table = table_of(
3772            vec![
3773                vec![plain_cell(&format!(" \t{long} \nshort\n ")), plain_cell("")],
3774                vec![plain_cell("short")],
3775                vec![],
3776                vec![plain_cell(""), plain_cell("a\nabcdefghijklmnop")],
3777            ],
3778            vec![],
3779        );
3780        let uncached = table.clone();
3781        let node_cx = NodeContext::default();
3782        let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
3783        let (table, node_cx, cached) = in_prepaint(&mut app, move |window, cx| {
3784            window.set_rem_size(px(20.));
3785            let style = TextStyle {
3786                font_family: BODY.into(),
3787                font_size: rems(1.).into(),
3788                ..Default::default()
3789            };
3790            let cached =
3791                window.with_text_style(Some(style.subtract(&Default::default())), |window| {
3792                    let (widths, shaped) = record_shaped_lines(|| {
3793                        measure_table_columns(&table, 2, &node_cx, window, cx)
3794                    });
3795                    assert_eq!(widths, vec![40960. + CELL_PAD_PX + CELL_BORDER_PX, 176.]);
3796                    assert!(shaped.iter().any(|line| line == &long));
3797                    assert_eq!(
3798                        table, uncached,
3799                        "derived widths do not change table equality"
3800                    );
3801                    assert!(uncached.column_widths_cache.0.lock().unwrap().is_none());
3802
3803                    let cached = cached_column_widths(&table);
3804                    let cloned = table.clone();
3805                    for same in [&table, &cloned] {
3806                        let (again, shaped) = record_shaped_lines(|| {
3807                            measure_table_columns(same, 2, &node_cx, window, cx)
3808                        });
3809                        assert_eq!(again, widths);
3810                        assert!(shaped.is_empty());
3811                        assert!(Arc::ptr_eq(&cached, &cached_column_widths(same)));
3812                    }
3813
3814                    // A different column count changes which cells draw borders.
3815                    assert_eq!(
3816                        measure_table_columns(&cloned, 3, &node_cx, window, cx),
3817                        vec![widths[0], widths[1] + CELL_BORDER_PX, CELL_MIN_PX]
3818                    );
3819                    assert!(!Arc::ptr_eq(&cached, &cached_column_widths(&cloned)));
3820                    assert!(Arc::ptr_eq(&cached, &cached_column_widths(&table)));
3821                    assert!(
3822                        measure_table_columns(&Table::default(), 0, &node_cx, window, cx)
3823                            .is_empty()
3824                    );
3825                    cached
3826                });
3827            (table, node_cx, cached)
3828        });
3829        // A parsed table can be rendered in another window. Do not reuse a
3830        // pixel-width cache from a different window's text system.
3831        in_prepaint(&mut app, move |window, cx| {
3832            window.set_rem_size(cached.rem_size);
3833            window.with_text_style(
3834                Some(cached.text_style.subtract(&Default::default())),
3835                |window| {
3836                    measure_table_columns(&table, 2, &node_cx, window, cx);
3837                    assert!(!Arc::ptr_eq(&cached, &cached_column_widths(&table)));
3838                },
3839            );
3840        });
3841    }
3842
3843    #[test]
3844    fn table_columns_cache_tracks_typography() {
3845        use crate::text::inline::test_draw::in_prepaint;
3846        use crate::text::inline::test_fonts::{BODY, MONO, WideMonoTextSystem};
3847        use gpui::{Refineable as _, TestApp, TextStyle};
3848
3849        let code = "0123456789";
3850        let mut paragraph = Paragraph::default();
3851        paragraph
3852            .push(InlineNode::new(code).marks(vec![(0..code.len(), TextMark::default().code())]));
3853        let table = table_of(
3854            vec![vec![
3855                plain_cell(code),
3856                TableCell {
3857                    children: paragraph,
3858                    width: None,
3859                },
3860            ]],
3861            vec![],
3862        );
3863        let base = TextStyle {
3864            font_family: BODY.into(),
3865            font_size: rems(1.).into(),
3866            ..Default::default()
3867        };
3868        let mut larger = base.clone();
3869        larger.font_size = rems(2.).into();
3870        let mut bold = base.clone();
3871        bold.font_weight = FontWeight::BOLD;
3872        let mut mono = base.clone();
3873        mono.font_family = MONO.into();
3874        let normal_code = HighlightStyle::default();
3875        let bold_code = HighlightStyle {
3876            font_weight: Some(FontWeight::BOLD),
3877            ..Default::default()
3878        };
3879        let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
3880        in_prepaint(&mut app, move |window, cx| {
3881            let mut previous = None;
3882            for (style, rem_size, family, inline_code, expected) in [
3883                (base.clone(), 20., MONO, normal_code, [117., 195.]),
3884                (base.clone(), 30., MONO, normal_code, [167., 282.5]),
3885                (larger, 20., MONO, normal_code, [217., 370.]),
3886                (bold, 20., MONO, normal_code, [167., 238.75]),
3887                (mono, 20., MONO, normal_code, [217., 195.]),
3888                (base.clone(), 20., MONO, normal_code, [117., 195.]),
3889                (base.clone(), 20., BODY, normal_code, [117., 107.5]),
3890                (base.clone(), 20., MONO, normal_code, [117., 195.]),
3891                (base, 20., MONO, bold_code, [117., 238.75]),
3892            ] {
3893                let mut theme = crate::Theme::default();
3894                theme.tokens.typography.mono = family.into();
3895                cx.set_global(theme);
3896                window.set_rem_size(px(rem_size));
3897                let node_cx = NodeContext {
3898                    style: Arc::new(TextViewStyle::default().with_inline_code(inline_code)),
3899                    ..Default::default()
3900                };
3901                window.with_text_style(Some(style.subtract(&Default::default())), |window| {
3902                    let widths = measure_table_columns(&table, 2, &node_cx, window, cx);
3903                    for (width, expected) in widths.iter().zip(expected) {
3904                        assert!((width - expected).abs() < 0.01, "{width} != {expected}");
3905                    }
3906                    let cached = cached_column_widths(&table);
3907                    if let Some(previous) = previous.replace(cached.clone()) {
3908                        assert!(!Arc::ptr_eq(&previous, &cached));
3909                    }
3910                    assert_eq!(
3911                        measure_table_columns(&table, 2, &node_cx, window, cx),
3912                        widths
3913                    );
3914                    assert!(Arc::ptr_eq(&cached, &cached_column_widths(&table)));
3915                });
3916            }
3917        });
3918    }
3919
3920    #[test]
3921    fn table_columns_cache_is_rebuilt_for_reparsed_tables() {
3922        use crate::text::format::markdown;
3923        use crate::text::inline::test_draw::in_prepaint;
3924        use crate::text::inline::test_fonts::{BODY, WideMonoTextSystem};
3925        use gpui::{Refineable as _, TestApp, TextStyle};
3926
3927        let mut node_cx = NodeContext::default();
3928        let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
3929        in_prepaint(&mut app, move |window, cx| {
3930            let style = TextStyle {
3931                font_family: BODY.into(),
3932                font_size: px(20.).into(),
3933                ..Default::default()
3934            };
3935            window.with_text_style(Some(style.subtract(&Default::default())), |window| {
3936                let mut previous = None;
3937                for (text, expected) in [("longer cell", 126.), ("x", CELL_MIN_PX)] {
3938                    let source = format!("| h |\n| - |\n| {text} |\n");
3939                    let document = markdown::parse(&source, &mut node_cx).unwrap();
3940                    let BlockNode::Table(table) = &document.blocks[0] else {
3941                        panic!("expected a parsed table");
3942                    };
3943                    assert!(table.column_widths_cache.0.lock().unwrap().is_none());
3944                    assert_eq!(
3945                        measure_table_columns(table, 1, &node_cx, window, cx),
3946                        vec![expected]
3947                    );
3948                    let cached = cached_column_widths(table);
3949                    if let Some(previous) = previous.replace(cached.clone()) {
3950                        assert!(!Arc::ptr_eq(&previous, &cached));
3951                    }
3952                    assert_eq!(document.source.as_ref(), source);
3953                }
3954            });
3955        });
3956    }
3957
3958    /// Table columns are sized from shaped text, so a column of inline code
3959    /// has to be measured in the code family. Measured in the body font, the
3960    /// wide-mono test font makes `col_w` come out at half the rendered width.
3961    #[test]
3962    fn table_column_of_inline_code_cells_fits_the_mono_width() {
3963        use crate::text::inline::test_fonts::{MONO, WideMonoTextSystem};
3964        use gpui::{Empty, TestApp};
3965
3966        let code = "method_name()";
3967        let mut paragraph = Paragraph::default();
3968        paragraph
3969            .push(InlineNode::new(code).marks(vec![(0..code.len(), TextMark::default().code())]));
3970        let table = Table {
3971            children: vec![TableRow {
3972                children: vec![TableCell {
3973                    children: paragraph,
3974                    width: None,
3975                }],
3976            }],
3977            column_aligns: vec![],
3978            span: None,
3979            table_data_cache: TableDataCache::default(),
3980            column_widths_cache: TableColumnWidthsCache::default(),
3981        };
3982        let node_cx = NodeContext::default();
3983
3984        let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
3985        let mut window = app.open_window(|_, _| Empty);
3986        let (col_w, font_size) = window.update(|_, window, cx| {
3987            let mut theme = crate::Theme::default();
3988            theme.tokens.typography.mono = MONO.into();
3989            cx.set_global(theme);
3990            let font_size = window.text_style().font_size.to_pixels(window.rem_size());
3991            (
3992                measure_table_columns(&table, 1, &node_cx, window, cx),
3993                font_size,
3994            )
3995        });
3996
3997        let mono_w = f32::from(WideMonoTextSystem::width_of(code, MONO, font_size * 0.875));
3998        assert!(
3999            (col_w[0]
4000                - (mono_w + CELL_PAD_PX + crate::text::inline_flow::INLINE_CODE_PADDING * 2.))
4001                .abs()
4002                < 0.01,
4003            "col_w {} must fit the mono width {} plus padding {}",
4004            col_w[0],
4005            mono_w,
4006            CELL_PAD_PX
4007        );
4008    }
4009
4010    #[test]
4011    fn code_block_highlights_are_cached_by_highlighter_identity() {
4012        use std::sync::atomic::{AtomicUsize, Ordering};
4013
4014        let calls = Arc::new(AtomicUsize::new(0));
4015        let calls_for_highlighter = calls.clone();
4016        let highlighter: Arc<CodeBlockHighlighterFn> = Arc::new(move |_| {
4017            calls_for_highlighter.fetch_add(1, Ordering::Relaxed);
4018            Vec::new()
4019        });
4020        let block = CodeBlock::new("fn main() {}".into(), Some("rust".into()), None::<Span>);
4021
4022        block.highlighted_styles(&highlighter);
4023        block.highlighted_styles(&highlighter);
4024        assert_eq!(calls.load(Ordering::Relaxed), 1);
4025
4026        let replacement: Arc<CodeBlockHighlighterFn> = Arc::new(|_| Vec::new());
4027        block.highlighted_styles(&replacement);
4028        assert!(Arc::ptr_eq(
4029            &block
4030                .highlight_cache
4031                .lock()
4032                .unwrap()
4033                .as_ref()
4034                .unwrap()
4035                .highlighter,
4036            &replacement
4037        ));
4038    }
4039
4040    #[test]
4041    fn a_new_highlighter_replaces_styles_instead_of_reusing_the_cache() {
4042        // Swapping the highlighter is how a theme change reaches a code block:
4043        // the parsed document is untouched, so the styles must come from the
4044        // new highlighter rather than from the styles cached for the old one.
4045        let light: Arc<CodeBlockHighlighterFn> = Arc::new(|_| {
4046            vec![(
4047                0..2,
4048                HighlightStyle {
4049                    color: Some(gpui::rgb(0x0000ff).into()),
4050                    ..Default::default()
4051                },
4052            )]
4053        });
4054        let dark: Arc<CodeBlockHighlighterFn> = Arc::new(|_| {
4055            vec![(
4056                0..2,
4057                HighlightStyle {
4058                    color: Some(gpui::rgb(0xffff00).into()),
4059                    ..Default::default()
4060                },
4061            )]
4062        });
4063        let block = CodeBlock::from_code("42", Some("json"));
4064
4065        let light_styles = block.highlighted_styles(&light);
4066        let dark_styles = block.highlighted_styles(&dark);
4067
4068        assert_eq!(light_styles[0].1.color, Some(gpui::rgb(0x0000ff).into()));
4069        assert_eq!(dark_styles[0].1.color, Some(gpui::rgb(0xffff00).into()));
4070        assert_eq!(block.code(), "42", "the document must survive the swap");
4071    }
4072
4073    #[test]
4074    fn reconstruct_markdown_wraps_marked_runs() {
4075        // "bold" fully covered by a bold mark.
4076        let marks = vec![(0..4, TextMark::default().bold())];
4077        assert_eq!(reconstruct_markdown("bold", &marks, 0..4), "**bold**");
4078        // Partial selection inside the bold run still wraps the slice.
4079        assert_eq!(reconstruct_markdown("bold", &marks, 1..3), "**ol**");
4080    }
4081
4082    #[test]
4083    fn reconstruct_markdown_emits_unmarked_text_verbatim() {
4084        // "a b c": plain, code, plain across three runs concatenated.
4085        let text = "a b c";
4086        let marks = vec![(2..3, TextMark::default().code())];
4087        assert_eq!(reconstruct_markdown(text, &marks, 0..5), "a `b` c");
4088        // Selecting only the plain tail.
4089        assert_eq!(reconstruct_markdown(text, &marks, 3..5), " c");
4090    }
4091
4092    #[test]
4093    fn reconstruct_markdown_handles_code_italic_strike_link() {
4094        assert_eq!(
4095            reconstruct_markdown("x", &[(0..1, TextMark::default().code())], 0..1),
4096            "`x`"
4097        );
4098        assert_eq!(
4099            reconstruct_markdown("x", &[(0..1, TextMark::default().italic())], 0..1),
4100            "*x*"
4101        );
4102        assert_eq!(
4103            reconstruct_markdown("x", &[(0..1, TextMark::default().strikethrough())], 0..1),
4104            "~~x~~"
4105        );
4106        let link = TextMark::default().link(LinkMark {
4107            url: "https://example.com".into(),
4108            ..Default::default()
4109        });
4110        assert_eq!(
4111            reconstruct_markdown("x", &[(0..1, link)], 0..1),
4112            "[x](https://example.com)"
4113        );
4114    }
4115
4116    #[test]
4117    fn reconstruct_markdown_nested_bold_italic() {
4118        // A single run marked both bold and italic (as produced by `**_x_**`).
4119        let mark = TextMark::default().bold().italic();
4120        // Inner (italic) is applied first, then bold: `***x***`.
4121        assert_eq!(reconstruct_markdown("x", &[(0..1, mark)], 0..1), "***x***");
4122    }
4123
4124    /// Build a paragraph whose combined `state.text` is the concatenation of
4125    /// its children (mirroring `Paragraph::render`), then set the paragraph
4126    /// selection so `selected_source` can be exercised without a real paint.
4127    fn paragraph_with_children(children: Vec<InlineNode>) -> Paragraph {
4128        let combined: String = children.iter().map(|c| c.text.to_string()).collect();
4129        let paragraph = Paragraph {
4130            span: None,
4131            children,
4132            link_refs: HashMap::new(),
4133            state: Arc::new(Mutex::new(InlineState::default())),
4134            render_cache: ParagraphRenderCache::default(),
4135        };
4136        if let Ok(mut state) = paragraph.state.lock() {
4137            state.set_text(combined.into());
4138        }
4139        paragraph
4140    }
4141
4142    fn set_paragraph_selection(paragraph: &Paragraph, range: Range<usize>) {
4143        if let Ok(mut state) = paragraph.state.lock() {
4144            state.selection = Some(range.into());
4145        }
4146    }
4147
4148    #[test]
4149    fn paragraph_selected_source_maps_partial_selection_across_runs() {
4150        // "This has **bold** text." rendered as ["This has ", "bold", " text."].
4151        let children = vec![
4152            InlineNode::new("This has ").marks(vec![(0..9, TextMark::default())]),
4153            InlineNode::new("bold").marks(vec![(0..4, TextMark::default().bold())]),
4154            InlineNode::new(" text.").marks(vec![(0..6, TextMark::default())]),
4155        ];
4156        let paragraph = paragraph_with_children(children);
4157
4158        // Select the whole paragraph: "This has bold text." -> source with **.
4159        set_paragraph_selection(&paragraph, 0..(9 + 4 + 6));
4160        assert_eq!(paragraph.selected_source(), "This has **bold** text.");
4161
4162        // Select only across the boundary "has **bold** te".
4163        // Rendered offsets: "has " starts at 5, "bold" at 9..13, " te" 13..16.
4164        set_paragraph_selection(&paragraph, 5..16);
4165        assert_eq!(paragraph.selected_source(), "has **bold** te");
4166
4167        // Select entirely inside the bold run -> still wrapped.
4168        set_paragraph_selection(&paragraph, 10..12);
4169        assert_eq!(paragraph.selected_source(), "**ol**");
4170    }
4171
4172    #[test]
4173    fn paragraph_selected_source_matches_text_when_no_marks() {
4174        let children =
4175            vec![InlineNode::new("plain words").marks(vec![(0..11, TextMark::default())])];
4176        let paragraph = paragraph_with_children(children);
4177        set_paragraph_selection(&paragraph, 0..11);
4178        assert_eq!(paragraph.selected_source(), "plain words");
4179        assert_eq!(paragraph.selected_text(), "plain words");
4180    }
4181
4182    fn selected_paragraph(text: &str) -> Paragraph {
4183        let len = text.len();
4184        let paragraph = paragraph_with_children(vec![
4185            InlineNode::new(text).marks(vec![(0..len, TextMark::default())]),
4186        ]);
4187        set_paragraph_selection(&paragraph, 0..len);
4188        paragraph
4189    }
4190
4191    #[test]
4192    fn heading_selected_source_prefixes_hashes() {
4193        let heading = BlockNode::Heading {
4194            level: 2,
4195            children: selected_paragraph("Title"),
4196            span: None,
4197        };
4198        assert_eq!(heading.selected_text(SelectionFormat::Source), "## Title\n");
4199        // Rendered text keeps no marker.
4200        assert_eq!(heading.selected_text(SelectionFormat::Plain), "Title\n");
4201    }
4202
4203    #[test]
4204    fn unordered_list_selected_source_prefixes_dash() {
4205        let list = BlockNode::List {
4206            ordered: false,
4207            start: None,
4208            span: None,
4209            children: vec![
4210                BlockNode::ListItem {
4211                    children: vec![BlockNode::Paragraph(selected_paragraph("one"))],
4212                    spread: false,
4213                    checked: None,
4214                    span: None,
4215                },
4216                BlockNode::ListItem {
4217                    children: vec![BlockNode::Paragraph(selected_paragraph("two"))],
4218                    spread: false,
4219                    checked: None,
4220                    span: None,
4221                },
4222            ],
4223        };
4224        assert_eq!(
4225            list.selected_text(SelectionFormat::Source),
4226            "- one\n- two\n"
4227        );
4228    }
4229
4230    #[test]
4231    fn ordered_list_selected_source_preserves_start() {
4232        let list = BlockNode::List {
4233            ordered: true,
4234            start: Some(3),
4235            span: None,
4236            children: vec![
4237                BlockNode::ListItem {
4238                    children: vec![BlockNode::Paragraph(selected_paragraph("three"))],
4239                    spread: false,
4240                    checked: None,
4241                    span: None,
4242                },
4243                BlockNode::ListItem {
4244                    children: vec![BlockNode::Paragraph(selected_paragraph("four"))],
4245                    spread: false,
4246                    checked: None,
4247                    span: None,
4248                },
4249            ],
4250        };
4251        assert_eq!(list.to_markdown(), "3. three\n4. four");
4252        assert_eq!(
4253            list.selected_text(SelectionFormat::Source),
4254            "3. three\n4. four\n"
4255        );
4256    }
4257
4258    #[test]
4259    fn ordered_list_selected_source_preserves_nested_starts_and_continuations() {
4260        let nested = BlockNode::List {
4261            ordered: true,
4262            start: Some(4),
4263            span: None,
4264            children: vec![
4265                BlockNode::ListItem {
4266                    children: vec![BlockNode::Paragraph(selected_paragraph("nested"))],
4267                    spread: false,
4268                    checked: None,
4269                    span: None,
4270                },
4271                BlockNode::ListItem {
4272                    children: vec![BlockNode::Paragraph(selected_paragraph("again"))],
4273                    spread: false,
4274                    checked: None,
4275                    span: None,
4276                },
4277            ],
4278        };
4279        let nested_list = BlockNode::List {
4280            ordered: true,
4281            start: Some(3),
4282            span: None,
4283            children: vec![
4284                BlockNode::ListItem {
4285                    children: vec![BlockNode::Paragraph(selected_paragraph("three")), nested],
4286                    spread: false,
4287                    checked: None,
4288                    span: None,
4289                },
4290                BlockNode::ListItem {
4291                    children: vec![BlockNode::Paragraph(selected_paragraph("four"))],
4292                    spread: false,
4293                    checked: None,
4294                    span: None,
4295                },
4296            ],
4297        };
4298        assert_eq!(
4299            nested_list.selected_text(SelectionFormat::Source),
4300            "3. three\n   4. nested\n   5. again\n4. four\n"
4301        );
4302
4303        let loose_list = BlockNode::List {
4304            ordered: true,
4305            start: Some(3),
4306            span: None,
4307            children: vec![
4308                BlockNode::ListItem {
4309                    children: vec![
4310                        BlockNode::Paragraph(selected_paragraph("loose")),
4311                        BlockNode::Paragraph(selected_paragraph("continuation")),
4312                    ],
4313                    spread: true,
4314                    checked: None,
4315                    span: None,
4316                },
4317                BlockNode::ListItem {
4318                    children: vec![BlockNode::Paragraph(selected_paragraph("four"))],
4319                    spread: false,
4320                    checked: None,
4321                    span: None,
4322                },
4323            ],
4324        };
4325        assert_eq!(
4326            loose_list.selected_text(SelectionFormat::Source),
4327            "3. loose\n   continuation\n4. four\n"
4328        );
4329    }
4330
4331    #[test]
4332    fn nested_list_selected_source_indents_sublists() {
4333        // - one
4334        //   - nested
4335        // - two
4336        let nested = BlockNode::List {
4337            ordered: false,
4338            start: None,
4339            span: None,
4340            children: vec![BlockNode::ListItem {
4341                children: vec![BlockNode::Paragraph(selected_paragraph("nested"))],
4342                spread: false,
4343                checked: None,
4344                span: None,
4345            }],
4346        };
4347        let list = BlockNode::List {
4348            ordered: false,
4349            start: None,
4350            span: None,
4351            children: vec![
4352                BlockNode::ListItem {
4353                    children: vec![BlockNode::Paragraph(selected_paragraph("one")), nested],
4354                    spread: false,
4355                    checked: None,
4356                    span: None,
4357                },
4358                BlockNode::ListItem {
4359                    children: vec![BlockNode::Paragraph(selected_paragraph("two"))],
4360                    spread: false,
4361                    checked: None,
4362                    span: None,
4363                },
4364            ],
4365        };
4366        assert_eq!(
4367            list.selected_text(SelectionFormat::Source),
4368            "- one\n  - nested\n- two\n"
4369        );
4370    }
4371
4372    #[test]
4373    fn task_list_selected_source_restores_checkboxes() {
4374        let list = BlockNode::List {
4375            ordered: false,
4376            start: None,
4377            span: None,
4378            children: vec![
4379                BlockNode::ListItem {
4380                    children: vec![BlockNode::Paragraph(selected_paragraph("done"))],
4381                    spread: false,
4382                    checked: Some(true),
4383                    span: None,
4384                },
4385                BlockNode::ListItem {
4386                    children: vec![BlockNode::Paragraph(selected_paragraph("todo"))],
4387                    spread: false,
4388                    checked: Some(false),
4389                    span: None,
4390                },
4391            ],
4392        };
4393        assert_eq!(
4394            list.selected_text(SelectionFormat::Source),
4395            "- [x] done\n- [ ] todo\n"
4396        );
4397    }
4398
4399    #[test]
4400    fn blockquote_selected_source_prefixes_gt() {
4401        let quote = BlockNode::Blockquote {
4402            span: None,
4403            children: vec![BlockNode::Paragraph(selected_paragraph("quoted text"))],
4404        };
4405        assert_eq!(
4406            quote.selected_text(SelectionFormat::Source),
4407            "> quoted text\n"
4408        );
4409    }
4410
4411    #[test]
4412    fn table_selected_source_pipes_cells_with_alignment_row() {
4413        let cell = |text: &str| TableCell {
4414            children: selected_paragraph(text),
4415            width: None,
4416        };
4417        let table = Table {
4418            children: vec![
4419                TableRow {
4420                    children: vec![cell("Name"), cell("Age")],
4421                },
4422                TableRow {
4423                    children: vec![cell("Alice"), cell("30")],
4424                },
4425            ],
4426            column_aligns: vec![ColumnumnAlign::Left, ColumnumnAlign::Right],
4427            span: None,
4428            table_data_cache: TableDataCache::default(),
4429            column_widths_cache: TableColumnWidthsCache::default(),
4430        };
4431        let block = BlockNode::Table(table);
4432        assert_eq!(
4433            block.selected_text(SelectionFormat::Source),
4434            "| Name | Age |\n| :-- | --: |\n| Alice | 30 |\n"
4435        );
4436    }
4437
4438    /// A cell holding plain text, as `Table::to_markdown` and
4439    /// `Table::table_data` see it (neither needs a selection).
4440    fn plain_cell(text: &str) -> TableCell {
4441        TableCell {
4442            children: Paragraph::new(text.to_string()),
4443            width: None,
4444        }
4445    }
4446
4447    fn table_of(rows: Vec<Vec<TableCell>>, column_aligns: Vec<ColumnumnAlign>) -> Table {
4448        Table {
4449            children: rows
4450                .into_iter()
4451                .map(|children| TableRow { children })
4452                .collect(),
4453            column_aligns,
4454            span: None,
4455            table_data_cache: TableDataCache::default(),
4456            column_widths_cache: TableColumnWidthsCache::default(),
4457        }
4458    }
4459
4460    #[test]
4461    fn table_to_markdown_pipes_cells_with_alignment_row() {
4462        let table = table_of(
4463            vec![
4464                vec![plain_cell("Name"), plain_cell("Age"), plain_cell("Score")],
4465                vec![plain_cell("Alice"), plain_cell("30"), plain_cell("9.5")],
4466            ],
4467            vec![
4468                ColumnumnAlign::Left,
4469                ColumnumnAlign::Center,
4470                ColumnumnAlign::Right,
4471            ],
4472        );
4473
4474        assert_eq!(
4475            table.to_markdown(),
4476            "| Name | Age | Score |\n| :-- | :-: | --: |\n| Alice | 30 | 9.5 |"
4477        );
4478        // The block arm delegates to it.
4479        assert_eq!(
4480            BlockNode::Table(table.clone()).to_markdown(),
4481            table.to_markdown()
4482        );
4483    }
4484
4485    #[test]
4486    fn table_to_markdown_keeps_outer_pipes_for_a_single_column() {
4487        let table = table_of(
4488            vec![vec![plain_cell("Symbol")], vec![plain_cell("TSLA.US")]],
4489            vec![ColumnumnAlign::Left],
4490        );
4491
4492        assert_eq!(table.to_markdown(), "| Symbol |\n| :-- |\n| TSLA.US |");
4493    }
4494
4495    #[test]
4496    fn table_to_markdown_escapes_pipes_and_keeps_inline_marks() {
4497        let bold = TableCell {
4498            children: paragraph_with_children(vec![
4499                InlineNode::new("bold").marks(vec![(0..4, TextMark::default().bold())]),
4500            ]),
4501            width: None,
4502        };
4503        let table = table_of(
4504            vec![
4505                vec![plain_cell("a | b"), plain_cell("plain")],
4506                vec![plain_cell("c"), bold],
4507            ],
4508            vec![ColumnumnAlign::Left, ColumnumnAlign::Left],
4509        );
4510
4511        assert_eq!(
4512            table.to_markdown(),
4513            "| a \\| b | plain |\n| :-- | :-- |\n| c | **bold** |"
4514        );
4515    }
4516
4517    #[test]
4518    fn table_data_snapshots_plain_cells_and_markdown() {
4519        let mut table = table_of(
4520            vec![
4521                vec![plain_cell("  Name  "), plain_cell("Age")],
4522                vec![plain_cell("Alice"), plain_cell("30")],
4523            ],
4524            vec![ColumnumnAlign::Left, ColumnumnAlign::Right],
4525        );
4526        table.span = Some(Span { start: 4, end: 42 });
4527
4528        let data = table.table_data();
4529        assert_eq!(data.headers, vec!["Name", "Age"]);
4530        assert_eq!(data.rows, vec![vec!["Alice", "30"]]);
4531        assert_eq!(data.markdown, table.to_markdown());
4532        assert_eq!(data.span, Some(4..42));
4533    }
4534
4535    #[test]
4536    fn cloned_table_keeps_cached_table_data() {
4537        // Streaming appends deep-clone every block; the clone must reuse the
4538        // cached snapshot instead of rebuilding it.
4539        let table = table_of(
4540            vec![vec![plain_cell("Name")], vec![plain_cell("Alice")]],
4541            vec![ColumnumnAlign::Left],
4542        );
4543        let data = table.cached_table_data();
4544
4545        let cloned = table.clone();
4546        assert!(Arc::ptr_eq(&data, &cloned.cached_table_data()));
4547    }
4548
4549    #[test]
4550    fn table_data_handles_tables_without_rows() {
4551        // Header only: still a valid table, with no data rows.
4552        let header_only = table_of(
4553            vec![vec![plain_cell("Name"), plain_cell("Age")]],
4554            vec![ColumnumnAlign::Left, ColumnumnAlign::Left],
4555        );
4556        let data = header_only.table_data();
4557        assert_eq!(data.headers, vec!["Name", "Age"]);
4558        assert!(data.rows.is_empty());
4559        assert_eq!(data.markdown, "| Name | Age |\n| :-- | :-- |");
4560
4561        // No rows at all (a table still streaming in): an empty snapshot.
4562        assert_eq!(Table::default().table_data(), TableData::default());
4563    }
4564
4565    #[test]
4566    fn test_image_node_source() {
4567        use gpui::{ImageFormat, ImageSource, Resource};
4568
4569        fn image_node(url: &str) -> ImageNode {
4570            ImageNode {
4571                url: url.into(),
4572                ..Default::default()
4573            }
4574        }
4575
4576        // Document-provided values stay URI-backed, including `file://` and
4577        // scheme-less strings, so the document never gets implicit
4578        // filesystem access through `Resource::Embedded`.
4579        fn assert_uri(url: &str) {
4580            match image_node(url).source() {
4581                ImageSource::Resource(Resource::Uri(uri)) => assert_eq!(uri.as_ref(), url),
4582                _ => panic!("expected Uri for {url:?}"),
4583            }
4584        }
4585        assert_uri("https://example.com/logo.png");
4586        assert_uri("http://example.com/logo.png");
4587        assert_uri("website/public/logo.svg");
4588        assert_uri("./images/a.png");
4589        assert_uri("../images/a.png");
4590        assert_uri("/absolute/path/logo.svg");
4591        assert_uri("file:///absolute/path/logo.svg");
4592        assert_uri(r"C:\images\logo.png");
4593        assert_uri("docs/a:b.png");
4594        assert_uri("data:text/plain;base64,aGVsbG8=");
4595
4596        // A `data:` image is decoded once and the same decoded image is
4597        // handed to every render.
4598        let node = image_node("data:image/png;base64,iVBORw0KGgo=");
4599        let ImageSource::Image(first) = node.source() else {
4600            panic!("expected an embedded image");
4601        };
4602        assert_eq!(first.format(), ImageFormat::Png);
4603        assert_eq!(first.bytes(), b"\x89PNG\r\n\x1a\n");
4604        let ImageSource::Image(second) = node.source() else {
4605            panic!("expected an embedded image");
4606        };
4607        assert!(Arc::ptr_eq(&first, &second));
4608    }
4609
4610    fn image_paragraph(alt: &str, url: &str) -> Paragraph {
4611        let image = ImageNode {
4612            url: url.into(),
4613            alt: Some(alt.into()),
4614            ..Default::default()
4615        };
4616        Paragraph {
4617            span: None,
4618            children: vec![InlineNode::image(image)],
4619            link_refs: HashMap::new(),
4620            state: Arc::new(Mutex::new(InlineState::default())),
4621            render_cache: ParagraphRenderCache::default(),
4622        }
4623    }
4624
4625    /// Every mark round-trips, including the two Markdown has no plain syntax
4626    /// for.
4627    #[test]
4628    fn marks_round_trip_through_reconstruction() {
4629        let wrap = |mark: TextMark| reconstruct_markdown("x", &[(0..1, mark)], 0..1);
4630
4631        assert_eq!(wrap(TextMark::default().bold()), "**x**");
4632        assert_eq!(wrap(TextMark::default().italic()), "*x*");
4633        assert_eq!(wrap(TextMark::default().code()), "`x`");
4634        assert_eq!(wrap(TextMark::default().strikethrough()), "~~x~~");
4635        assert_eq!(
4636            wrap(TextMark::default().highlight(gpui::rgb(0xfef08a).into())),
4637            "==x=="
4638        );
4639        // No Markdown syntax for underline, so it keeps the tag it came from.
4640        assert_eq!(wrap(TextMark::default().underline()), "<u>x</u>");
4641
4642        // A link keeps its title, which Markdown carries after the URL.
4643        assert_eq!(
4644            wrap(TextMark::default().link(LinkMark {
4645                url: "https://example.com".into(),
4646                title: Some("Tip".into()),
4647                ..Default::default()
4648            })),
4649            "[x](https://example.com \"Tip\")"
4650        );
4651    }
4652
4653    /// A block the selection covers whole comes straight from the source, so it
4654    /// keeps what the author wrote instead of a normalized reconstruction.
4655    #[test]
4656    fn document_selected_source_slices_covered_blocks_from_the_source() {
4657        use crate::text::document::ParsedDocument;
4658
4659        // `_italic_`, the `3.` start and the column padding all survive only
4660        // because the block is copied, not rebuilt.
4661        let source = "start\n\n3. _one_\n4. two\n\n---\n\nend";
4662        let list = "3. _one_\n4. two";
4663        let list_start = source.find(list).unwrap();
4664        let rule_start = source.find("---").unwrap();
4665
4666        let document = ParsedDocument {
4667            source: source.into(),
4668            blocks: vec![
4669                BlockNode::Paragraph(selected_paragraph("start")),
4670                BlockNode::List {
4671                    ordered: true,
4672                    start: Some(3),
4673                    children: vec![],
4674                    span: Some(Span {
4675                        start: list_start,
4676                        end: list_start + list.len(),
4677                    }),
4678                },
4679                BlockNode::HorizontalRule {
4680                    span: Some(Span {
4681                        start: rule_start,
4682                        end: rule_start + 3,
4683                    }),
4684                },
4685                BlockNode::Paragraph(selected_paragraph("end")),
4686            ]
4687            .into(),
4688        };
4689
4690        assert_eq!(
4691            document.selected_text(SelectionFormat::Source, None),
4692            "start\n\n3. _one_\n4. two\n\n---\n\nend"
4693        );
4694    }
4695
4696    #[test]
4697    fn document_selected_source_includes_enclosed_image() {
4698        use crate::text::document::ParsedDocument;
4699
4700        // A standalone image between two selected paragraphs is covered by the
4701        // selection, so it is copied whole even though it holds no selection of
4702        // its own — straight out of the source the parser located it in.
4703        let source = "before\n\n![alt](https://example.com/i.png)\n\nafter";
4704        let image_markdown = "![alt](https://example.com/i.png)";
4705        let start = source.find(image_markdown).unwrap();
4706        let mut image = image_paragraph("alt", "https://example.com/i.png");
4707        image.span = Some(Span {
4708            start,
4709            end: start + image_markdown.len(),
4710        });
4711
4712        let document = ParsedDocument {
4713            source: source.into(),
4714            blocks: vec![
4715                BlockNode::Paragraph(selected_paragraph("before")),
4716                BlockNode::Paragraph(image),
4717                BlockNode::Paragraph(selected_paragraph("after")),
4718            ]
4719            .into(),
4720        };
4721        assert_eq!(
4722            document.selected_text(SelectionFormat::Source, None),
4723            "before\n\n![alt](https://example.com/i.png)\n\nafter"
4724        );
4725    }
4726
4727    #[test]
4728    fn document_selected_source_drops_unenclosed_image() {
4729        use crate::text::document::ParsedDocument;
4730
4731        // An image after the only selected block, with nothing selected after
4732        // it, is not enclosed and is dropped.
4733        let document = ParsedDocument {
4734            source: String::new().into(),
4735            blocks: vec![
4736                BlockNode::Paragraph(selected_paragraph("before")),
4737                BlockNode::Paragraph(image_paragraph("alt", "u")),
4738            ]
4739            .into(),
4740        };
4741        assert_eq!(
4742            document.selected_text(SelectionFormat::Source, None),
4743            "before"
4744        );
4745    }
4746
4747    fn selected_code_block(code: &str, lang: Option<&str>) -> BlockNode {
4748        let block = CodeBlock::new(
4749            code.to_string().into(),
4750            lang.map(|l| l.to_string().into()),
4751            None::<Span>,
4752        );
4753        if let Ok(mut state) = block.state.lock() {
4754            let len = state.text.len();
4755            state.selection = Some((0..len).into());
4756        }
4757        BlockNode::CodeBlock(block)
4758    }
4759
4760    #[test]
4761    fn code_block_selected_source_wraps_in_fence_with_lang() {
4762        let block = selected_code_block("let x = 1;\n", Some("rust"));
4763        let code = block.selected_text(SelectionFormat::Plain);
4764        let code_trimmed = code.trim_end_matches('\n');
4765        // The source wraps the (trailing-newline-trimmed) selected code in a
4766        // fenced block carrying the language; the block arm adds one trailing
4767        // newline.
4768        assert_eq!(
4769            block.selected_text(SelectionFormat::Source),
4770            format!("```rust\n{}\n```\n", code_trimmed)
4771        );
4772        assert!(
4773            block
4774                .selected_text(SelectionFormat::Source)
4775                .starts_with("```rust\n")
4776        );
4777        assert!(
4778            block
4779                .selected_text(SelectionFormat::Source)
4780                .trim_end()
4781                .ends_with("\n```")
4782        );
4783    }
4784
4785    #[test]
4786    fn code_block_selected_source_without_lang() {
4787        let block = selected_code_block("plain\n", None);
4788        let code_trimmed = block.selected_text(SelectionFormat::Plain);
4789        let code_trimmed = code_trimmed.trim_end_matches('\n');
4790        assert_eq!(
4791            block.selected_text(SelectionFormat::Source),
4792            format!("```\n{}\n```\n", code_trimmed)
4793        );
4794    }
4795
4796    #[test]
4797    fn document_selected_source_joins_blocks_with_blank_line() {
4798        use crate::text::document::ParsedDocument;
4799
4800        // A heading, a paragraph, and a two-item ordered list, each fully
4801        // selected. Top-level blocks must be separated by a blank line so the
4802        // copied Markdown re-renders with the same structure.
4803        let document = ParsedDocument {
4804            source: String::new().into(),
4805            blocks: vec![
4806                BlockNode::Heading {
4807                    level: 1,
4808                    children: selected_paragraph("Title"),
4809                    span: None,
4810                },
4811                BlockNode::Paragraph(selected_paragraph("A paragraph.")),
4812                selected_code_block("let x = 1;\n", Some("rust")),
4813                BlockNode::List {
4814                    ordered: true,
4815                    start: Some(1),
4816                    span: None,
4817                    children: vec![
4818                        BlockNode::ListItem {
4819                            children: vec![BlockNode::Paragraph(selected_paragraph("one"))],
4820                            spread: false,
4821                            checked: None,
4822                            span: None,
4823                        },
4824                        BlockNode::ListItem {
4825                            children: vec![BlockNode::Paragraph(selected_paragraph("two"))],
4826                            spread: false,
4827                            checked: None,
4828                            span: None,
4829                        },
4830                    ],
4831                },
4832            ]
4833            .into(),
4834        };
4835
4836        assert_eq!(
4837            document.selected_text(SelectionFormat::Source, None),
4838            "# Title\n\nA paragraph.\n\n```rust\nlet x = 1;\n```\n\n1. one\n2. two"
4839        );
4840    }
4841
4842    #[test]
4843    fn code_block_equality_includes_code_content() {
4844        let first = CodeBlock::new("let value = 1;".into(), Some("rust".into()), None::<Span>);
4845        let second = CodeBlock::new("let value = 2;".into(), Some("rust".into()), None::<Span>);
4846
4847        assert_ne!(first, second);
4848    }
4849}