Skip to main content

gpui_base/text/
node.rs

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