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