Skip to main content

gpui_base/text/
node.rs

1use std::{
2    collections::HashMap,
3    ops::Range,
4    sync::{Arc, Mutex},
5};
6
7use gpui::{
8    AnyElement, App, DefiniteLength, Div, ElementId, FontStyle, FontWeight, HighlightStyle, Hsla,
9    Image, ImageFormat, InteractiveElement as _, IntoElement, Length, ObjectFit, Overflow,
10    ParentElement, Pixels, ScrollHandle, SharedString, SharedUri, StatefulInteractiveElement,
11    StyleRefinement, Styled, StyledImage as _, WhiteSpace, Window, div, img,
12    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::{Inline, InlineState},
24        inline_flow::{InlineFlow, InlineFlowItem},
25        text_view::handle_link_click,
26    },
27    theme::ActiveTheme as _,
28    v_flex,
29};
30
31use super::{
32    SelectionFormat, TextViewStyle,
33    utils::{image_source, list_item_prefix},
34};
35
36const 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>"#;
37const 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>"#;
38
39/// The block-level nodes.
40#[derive(Debug, Clone, PartialEq)]
41pub(crate) enum BlockNode {
42    /// Something like a Div container in HTML.
43    Root {
44        children: Vec<BlockNode>,
45        span: Option<Span>,
46    },
47    Paragraph(Paragraph),
48    Heading {
49        level: u8,
50        children: Paragraph,
51        span: Option<Span>,
52    },
53    Blockquote {
54        children: Vec<BlockNode>,
55        span: Option<Span>,
56    },
57    List {
58        /// Only contains ListItem, others will be ignored
59        children: Vec<BlockNode>,
60        ordered: bool,
61        span: Option<Span>,
62    },
63    ListItem {
64        children: Vec<BlockNode>,
65        spread: bool,
66        /// Whether the list item is checked, if None, it's not a checkbox
67        checked: Option<bool>,
68        span: Option<Span>,
69    },
70    CodeBlock(CodeBlock),
71    /// A custom Markdown node produced by [`MarkdownExtensions`].
72    Custom(MarkdownNode),
73    Table(Table),
74    Break {
75        html: bool,
76        span: Option<Span>,
77    },
78    HorizontalRule {
79        span: Option<Span>,
80    },
81    /// Use for to_markdown get raw definition
82    Definition {
83        identifier: SharedString,
84        url: SharedString,
85        title: Option<SharedString>,
86        span: Option<Span>,
87    },
88    Unknown,
89}
90
91#[derive(Clone, Copy)]
92enum BlockTextKind {
93    All,
94    Selected,
95    /// Like `Selected`, but reconstructs Markdown source for the selection
96    /// instead of the rendered plain text.
97    SelectedSource,
98}
99
100impl BlockNode {
101    pub(super) fn is_list_item(&self) -> bool {
102        matches!(self, Self::ListItem { .. })
103    }
104
105    /// Combine all children, omitting the empt parent nodes.
106    pub(super) fn compact(self) -> BlockNode {
107        match self {
108            Self::Root { mut children, .. } if children.len() == 1 => children.remove(0).compact(),
109            _ => self,
110        }
111    }
112
113    /// Get the span of the node.
114    pub(crate) fn span(&self) -> Option<Span> {
115        match self {
116            BlockNode::Root { span, .. } => *span,
117            BlockNode::Paragraph(paragraph) => paragraph.span,
118            BlockNode::Heading { span, .. } => *span,
119            BlockNode::Blockquote { span, .. } => *span,
120            BlockNode::List { span, .. } => *span,
121            BlockNode::ListItem { span, .. } => *span,
122            BlockNode::CodeBlock(code_block) => code_block.span,
123            BlockNode::Custom(el) => el.span,
124            BlockNode::Table(table) => table.span,
125            BlockNode::Break { span, .. } => *span,
126            BlockNode::HorizontalRule { span, .. } => *span,
127            BlockNode::Definition { span, .. } => *span,
128            BlockNode::Unknown { .. } => None,
129        }
130    }
131
132    pub(super) fn text(&self) -> String {
133        self.text_by_kind(BlockTextKind::All)
134    }
135
136    /// The selected text within this block, in `format`.
137    ///
138    /// [`SelectionFormat::Source`] reconstructs the Markdown source of the
139    /// selection instead of the rendered text.
140    pub(super) fn selected_text(&self, format: SelectionFormat) -> String {
141        self.text_by_kind(match format {
142            SelectionFormat::Plain => BlockTextKind::Selected,
143            SelectionFormat::Source => BlockTextKind::SelectedSource,
144        })
145    }
146
147    fn text_by_kind(&self, kind: BlockTextKind) -> String {
148        let mut text = String::new();
149        match self {
150            BlockNode::Root { children, .. } => {
151                let block_text = Self::children_text(children, kind);
152                if !block_text.is_empty() {
153                    text.push_str(&block_text);
154                    text.push('\n');
155                }
156            }
157            BlockNode::Paragraph(paragraph) => {
158                let block_text = match kind {
159                    BlockTextKind::All => paragraph.text(),
160                    BlockTextKind::Selected => paragraph.selected_text(),
161                    BlockTextKind::SelectedSource => paragraph.selected_source(),
162                };
163                if !block_text.is_empty() {
164                    text.push_str(&block_text);
165                    text.push('\n');
166                }
167            }
168            BlockNode::Heading {
169                level, children, ..
170            } => {
171                let block_text = match kind {
172                    BlockTextKind::All => children.text(),
173                    BlockTextKind::Selected => children.selected_text(),
174                    BlockTextKind::SelectedSource => children.selected_source(),
175                };
176                if !block_text.is_empty() {
177                    // In source mode, prefix the heading marker so a selected
178                    // heading round-trips as Markdown (e.g. `## Title`).
179                    if matches!(kind, BlockTextKind::SelectedSource) {
180                        text.push_str(&"#".repeat(*level as usize));
181                        text.push(' ');
182                    }
183                    text.push_str(&block_text);
184                    text.push('\n');
185                }
186            }
187            BlockNode::List {
188                children, ordered, ..
189            } => {
190                if matches!(kind, BlockTextKind::SelectedSource) {
191                    // Reconstruct the list source, indenting nested lists and
192                    // restoring list markers and task-list checkboxes.
193                    text.push_str(&list_selected_source(children, *ordered, ""));
194                } else {
195                    text.push_str(&Self::children_text(children, kind));
196                }
197            }
198            BlockNode::ListItem { children, .. } => {
199                text.push_str(&Self::children_text(children, kind));
200            }
201            BlockNode::Blockquote { children, .. } => {
202                let block_text = Self::children_text(children, kind);
203
204                if !block_text.is_empty() {
205                    if matches!(kind, BlockTextKind::SelectedSource) {
206                        // Prefix every line with `> ` so a selected blockquote
207                        // round-trips as Markdown.
208                        let quoted = block_text
209                            .trim_end_matches('\n')
210                            .lines()
211                            .map(|line| {
212                                if line.is_empty() {
213                                    ">".to_string()
214                                } else {
215                                    format!("> {}", line)
216                                }
217                            })
218                            .collect::<Vec<_>>()
219                            .join("\n");
220                        text.push_str(&quoted);
221                    } else {
222                        text.push_str(&block_text);
223                    }
224                    text.push('\n');
225                }
226            }
227            BlockNode::Table(table) => {
228                if matches!(kind, BlockTextKind::SelectedSource) {
229                    let block_text = table_selected_source(table);
230                    if !block_text.is_empty() {
231                        text.push_str(&block_text);
232                        text.push('\n');
233                    }
234                } else {
235                    let mut block_text = String::new();
236                    for row in table.children.iter() {
237                        let mut row_texts = vec![];
238                        for cell in row.children.iter() {
239                            row_texts.push(match kind {
240                                BlockTextKind::All => cell.children.text(),
241                                // Source is handled above; only Selected reaches here.
242                                _ => cell.children.selected_text(),
243                            });
244                        }
245                        if !row_texts.is_empty() {
246                            block_text.push_str(&row_texts.join(" "));
247                            block_text.push('\n');
248                        }
249                    }
250
251                    if !block_text.is_empty() {
252                        text.push_str(&block_text);
253                        text.push('\n');
254                    }
255                }
256            }
257            BlockNode::CodeBlock(code_block) => {
258                let block_text = match kind {
259                    BlockTextKind::All => code_block.text(),
260                    BlockTextKind::Selected => code_block.selected_text(),
261                    BlockTextKind::SelectedSource => code_block.selected_source(),
262                };
263                if !block_text.is_empty() {
264                    text.push_str(&block_text);
265                    text.push('\n');
266                }
267            }
268            BlockNode::Custom(node) => {
269                if let BlockTextKind::All = kind {
270                    let content = node.as_text();
271                    if !content.is_empty() {
272                        text.push_str(content);
273                        text.push('\n');
274                    }
275                }
276            }
277            BlockNode::Definition { .. }
278            | BlockNode::Break { .. }
279            | BlockNode::HorizontalRule { .. }
280            | BlockNode::Unknown { .. } => {}
281        }
282
283        text
284    }
285
286    fn children_text(children: &[BlockNode], kind: BlockTextKind) -> String {
287        let mut text = String::new();
288        for child in children.iter() {
289            text.push_str(&child.text_by_kind(kind));
290        }
291
292        text
293    }
294
295    /// Synchronously clear the selection stored in every inline state.
296    ///
297    /// Mirrors the [`selected_text`](Self::selected_text) traversal so the
298    /// selection can be cleared without relying on a repaint.
299    /// Whether this block carries a selection, even an empty one.
300    ///
301    /// A block only learns its selection when it is painted, so this doubles as
302    /// "this block was on screen while the selection was made". An empty
303    /// selection is the caret left by the press that started the drag, which is
304    /// why it counts (see [`ParsedDocument::selected_text`]).
305    pub(super) fn has_selection(&self) -> bool {
306        match self {
307            BlockNode::Root { children, .. }
308            | BlockNode::Blockquote { children, .. }
309            | BlockNode::List { children, .. }
310            | BlockNode::ListItem { children, .. } => {
311                children.iter().any(|child| child.has_selection())
312            }
313            BlockNode::Paragraph(paragraph) => paragraph.has_selection(),
314            BlockNode::Heading { children, .. } => children.has_selection(),
315            BlockNode::Table(table) => table.children.iter().any(|row| {
316                row.children
317                    .iter()
318                    .any(|cell| cell.children.has_selection())
319            }),
320            BlockNode::CodeBlock(code_block) => code_block.has_selection(),
321            BlockNode::Custom { .. }
322            | BlockNode::Definition { .. }
323            | BlockNode::Break { .. }
324            | BlockNode::HorizontalRule { .. }
325            | BlockNode::Unknown { .. } => false,
326        }
327    }
328
329    pub(super) fn clear_selection(&self) {
330        match self {
331            BlockNode::Root { children, .. }
332            | BlockNode::Blockquote { children, .. }
333            | BlockNode::List { children, .. }
334            | BlockNode::ListItem { children, .. } => {
335                for child in children.iter() {
336                    child.clear_selection();
337                }
338            }
339            BlockNode::Paragraph(paragraph) => paragraph.clear_selection(),
340            BlockNode::Heading { children, .. } => children.clear_selection(),
341            BlockNode::Table(table) => {
342                for row in table.children.iter() {
343                    for cell in row.children.iter() {
344                        cell.children.clear_selection();
345                    }
346                }
347            }
348            BlockNode::CodeBlock(code_block) => code_block.clear_selection(),
349            BlockNode::Custom { .. }
350            | BlockNode::Definition { .. }
351            | BlockNode::Break { .. }
352            | BlockNode::HorizontalRule { .. }
353            | BlockNode::Unknown { .. } => {}
354        }
355    }
356}
357
358#[allow(unused)]
359#[derive(Debug, Default, Clone, PartialEq)]
360pub struct LinkMark {
361    pub url: SharedString,
362    /// Optional identifier for footnotes.
363    pub identifier: Option<SharedString>,
364    pub title: Option<SharedString>,
365}
366
367#[derive(Debug, Default, Clone, PartialEq)]
368pub struct TextMark {
369    pub bold: bool,
370    pub italic: bool,
371    pub strikethrough: bool,
372    pub underline: bool,
373    pub code: bool,
374    /// Highlight (`<mark>`) the text with this background color.
375    ///
376    /// `None` means the text is not highlighted.
377    pub highlight: Option<Hsla>,
378    pub link: Option<LinkMark>,
379}
380
381impl TextMark {
382    pub fn bold(mut self) -> Self {
383        self.bold = true;
384        self
385    }
386
387    pub fn italic(mut self) -> Self {
388        self.italic = true;
389        self
390    }
391
392    pub fn strikethrough(mut self) -> Self {
393        self.strikethrough = true;
394        self
395    }
396
397    pub fn underline(mut self) -> Self {
398        self.underline = true;
399        self
400    }
401
402    pub fn code(mut self) -> Self {
403        self.code = true;
404        self
405    }
406
407    /// Mark the text as highlighted (`<mark>`) with the given background color.
408    pub fn highlight(mut self, color: Hsla) -> Self {
409        self.highlight = Some(color);
410        self
411    }
412
413    pub fn link(mut self, link: impl Into<LinkMark>) -> Self {
414        self.link = Some(link.into());
415        self
416    }
417
418    pub fn merge(&mut self, other: TextMark) {
419        self.bold |= other.bold;
420        self.italic |= other.italic;
421        self.strikethrough |= other.strikethrough;
422        self.underline |= other.underline;
423        self.code |= other.code;
424        if other.highlight.is_some() {
425            self.highlight = other.highlight;
426        }
427        if let Some(link) = other.link {
428            self.link = Some(link);
429        }
430    }
431}
432
433/// The bytes
434#[derive(Debug, Default, Copy, Clone, PartialEq)]
435pub struct Span {
436    pub start: usize,
437    pub end: usize,
438}
439
440impl From<Span> for ElementId {
441    fn from(value: Span) -> Self {
442        ElementId::Name(format!("md-{}:{}", value.start, value.end).into())
443    }
444}
445
446#[allow(unused)]
447#[derive(Debug, Default, Clone)]
448pub struct ImageNode {
449    pub url: SharedUri,
450    pub link: Option<LinkMark>,
451    pub title: Option<SharedString>,
452    pub alt: Option<SharedString>,
453    pub width: Option<DefiniteLength>,
454    pub height: Option<DefiniteLength>,
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
466impl PartialEq for ImageNode {
467    fn eq(&self, other: &Self) -> bool {
468        self.url == other.url
469            && self.link == other.link
470            && self.title == other.title
471            && self.alt == other.alt
472            && self.width == other.width
473            && self.height == other.height
474    }
475}
476
477#[derive(Default, Clone, Debug)]
478pub(crate) struct InlineNode {
479    /// The text content.
480    pub(crate) text: SharedString,
481    pub(crate) image: Option<ImageNode>,
482    /// The text styles, each tuple contains the range of the text and the style.
483    pub(crate) marks: Vec<(Range<usize>, TextMark)>,
484
485    state: Arc<Mutex<InlineState>>,
486}
487
488impl PartialEq for InlineNode {
489    fn eq(&self, other: &Self) -> bool {
490        self.text == other.text && self.image == other.image && self.marks == other.marks
491    }
492}
493
494/// Wrap `text` with the Markdown syntax implied by `mark`.
495///
496/// This mirrors the per-mark formatting in [`Paragraph::to_markdown`] but
497/// operates on an already-sliced run, so it can reconstruct the Markdown
498/// source for a *partial* text selection. Applied inside-out (innermost markup
499/// first) so nested emphasis like `**_x_**` round-trips.
500pub(crate) fn wrap_with_mark(text: &str, mark: &TextMark) -> String {
501    if text.is_empty() {
502        return String::new();
503    }
504
505    let mut out = text.to_string();
506    if mark.code {
507        out = format!("`{}`", out);
508    }
509    if mark.italic {
510        out = format!("*{}*", out);
511    }
512    if mark.bold {
513        out = format!("**{}**", out);
514    }
515    if mark.strikethrough {
516        out = format!("~~{}~~", out);
517    }
518    if mark.underline {
519        // Markdown has no underline syntax, and `__` reads as bold to most
520        // parsers, so fall back to the inline HTML `<u>` parses from.
521        out = format!("<u>{}</u>", out);
522    }
523    if mark.highlight.is_some() {
524        out = format!("=={}==", out);
525    }
526    if let Some(link) = &mark.link {
527        out = match &link.title {
528            Some(title) => format!("[{}]({} \"{}\")", out, link.url, title),
529            None => format!("[{}]({})", out, link.url),
530        };
531    }
532    out
533}
534
535/// How a selection covers one rendered run, so the caller can tell whether it
536/// continues into an adjacent inline image.
537#[derive(Default)]
538struct RunSelection {
539    emitted: bool,
540    at_start: bool,
541    at_end: bool,
542}
543
544/// Emit the selected part of one rendered run, preceded by the images the
545/// selection has run into. `run` holds the run's children with their offset
546/// into the run's concatenated text.
547fn emit_run(
548    state: &Arc<Mutex<InlineState>>,
549    run: &[(usize, &InlineNode)],
550    pending_images: &mut Vec<String>,
551    out: &mut String,
552) -> RunSelection {
553    let mut selected = RunSelection::default();
554    let Ok(state) = state.lock() else {
555        return selected;
556    };
557    let Some(selection) = &state.selection else {
558        return selected;
559    };
560    if selection.start >= selection.end {
561        return selected;
562    }
563
564    selected.at_start = selection.start == 0;
565    selected.at_end = selection.end >= state.text.len();
566
567    for (start, child) in run {
568        let end = start + child.text.len();
569        let lo = selection.start.max(*start);
570        let hi = selection.end.min(end);
571        if lo >= hi {
572            continue;
573        }
574
575        if !selected.emitted {
576            if selected.at_start {
577                out.push_str(&pending_images.join(""));
578            }
579            pending_images.clear();
580        }
581        selected.emitted = true;
582
583        out.push_str(&reconstruct_markdown(
584            &child.text,
585            &child.marks,
586            (lo - start)..(hi - start),
587        ));
588    }
589
590    selected
591}
592
593/// The Markdown source for an inline image, e.g. `![alt](url "title")`.
594fn image_markdown(image: &ImageNode) -> String {
595    let alt = image.alt.clone().unwrap_or_default();
596    let title = image
597        .title
598        .clone()
599        .map_or(String::new(), |title| format!(" \"{}\"", title));
600    format!("![{}]({}{})", alt, image.url, title)
601}
602
603/// Reconstruct the Markdown source for the `selection` sub-range of a text run
604/// carrying `marks`.
605///
606/// `selection` is a byte range into `text`. For each mark that overlaps the
607/// selection, the overlapping slice is wrapped in the mark's Markdown syntax
608/// (see [`wrap_with_mark`]); slices not covered by any mark are emitted
609/// verbatim. This lets a rendered-offset selection be copied back as Markdown
610/// source (e.g. selecting inside a `**bold**` run yields `**bold**`).
611pub(crate) fn reconstruct_markdown(
612    text: &str,
613    marks: &[(Range<usize>, TextMark)],
614    selection: Range<usize>,
615) -> String {
616    let start = selection.start.min(text.len());
617    let end = selection.end.min(text.len());
618    if start >= end {
619        return String::new();
620    }
621
622    let mut out = String::new();
623    let mut cursor = start;
624    // Marks are stored in ascending, non-overlapping order by the parser.
625    for (range, mark) in marks.iter() {
626        let seg_start = range.start.max(start);
627        let seg_end = range.end.min(end);
628        if seg_start >= seg_end {
629            continue;
630        }
631        // Emit any unmarked text before this mark verbatim.
632        if cursor < seg_start {
633            out.push_str(&text[cursor..seg_start]);
634        }
635        out.push_str(&wrap_with_mark(&text[seg_start..seg_end], mark));
636        cursor = seg_end;
637    }
638    // Trailing unmarked text.
639    if cursor < end {
640        out.push_str(&text[cursor..end]);
641    }
642    out
643}
644
645/// Reconstruct the Markdown source of the selected cells of `table`.
646///
647/// Cells emit their own selected source; rows are piped (`| a | b |`) and the
648/// delimiter/alignment row is inserted after the first row, so a selected
649/// table round-trips as a Markdown table. Returns an empty string when no cell
650/// is selected.
651fn table_selected_source(table: &Table) -> String {
652    let cell_source = |cell: &TableCell| cell.children.selected_source().replace('\n', " ");
653
654    let any_selected = table.children.iter().any(|row| {
655        row.children
656            .iter()
657            .any(|cell| !cell_source(cell).trim().is_empty())
658    });
659    if !any_selected {
660        return String::new();
661    }
662
663    let mut lines: Vec<String> = Vec::new();
664    for (row_ix, row) in table.children.iter().enumerate() {
665        let cells: Vec<String> = row
666            .children
667            .iter()
668            .map(|cell| cell_source(cell).trim().to_string())
669            .collect();
670        lines.push(format!("| {} |", cells.join(" | ")));
671
672        // The Markdown delimiter row carries the column alignments and must
673        // follow the header row.
674        if row_ix == 0 {
675            let aligns: Vec<String> = (0..row.children.len())
676                .map(|ix| {
677                    match table.column_align(ix) {
678                        ColumnumnAlign::Left => ":--",
679                        ColumnumnAlign::Center => ":-:",
680                        ColumnumnAlign::Right => "--:",
681                    }
682                    .to_string()
683                })
684                .collect();
685            lines.push(format!("| {} |", aligns.join(" | ")));
686        }
687    }
688
689    lines.join("\n")
690}
691
692/// Reconstruct the Markdown source of the selected items of a list.
693///
694/// Restores the list marker (`- ` / `N. `) and task-list checkbox (`[x] ` /
695/// `[ ] `) of each item, and recurses into nested lists with a deeper `indent`
696/// so nesting is preserved. `indent` is the leading whitespace for this level;
697/// nested levels are indented by the width of the parent marker so continuation
698/// and sub-list lines align under the item text. Items with no selected content
699/// are skipped but still consume an ordered number, so the remaining items keep
700/// their original numbering.
701fn list_selected_source(children: &[BlockNode], ordered: bool, indent: &str) -> String {
702    let mut out = String::new();
703    let mut item_ix = 0usize;
704
705    for child in children {
706        let BlockNode::ListItem {
707            children: item_children,
708            checked,
709            ..
710        } = child
711        else {
712            continue;
713        };
714
715        let marker = if ordered {
716            format!("{}. ", item_ix + 1)
717        } else {
718            "- ".to_string()
719        };
720        let checkbox = match checked {
721            Some(true) => "[x] ",
722            Some(false) => "[ ] ",
723            None => "",
724        };
725        let child_indent = format!("{}{}", indent, " ".repeat(marker.len()));
726
727        // Split the item into its own content and any nested lists, so the
728        // nested lists can be indented under the content.
729        let mut content = String::new();
730        let mut nested = String::new();
731        for sub in item_children {
732            if let BlockNode::List {
733                children: sub_children,
734                ordered: sub_ordered,
735                ..
736            } = sub
737            {
738                nested.push_str(&list_selected_source(
739                    sub_children,
740                    *sub_ordered,
741                    &child_indent,
742                ));
743            } else {
744                content.push_str(&sub.text_by_kind(BlockTextKind::SelectedSource));
745            }
746        }
747        let content = content.trim_end_matches('\n');
748
749        if content.is_empty() && nested.is_empty() {
750            item_ix += 1;
751            continue;
752        }
753
754        if content.is_empty() {
755            // An item whose only selected content is a nested list.
756            out.push_str(indent);
757            out.push_str(&marker);
758            out.push_str(checkbox.trim_end());
759            out.push('\n');
760        } else {
761            // The first line carries the marker and checkbox; continuation
762            // lines are indented to align under the item text.
763            let mut lines = content.lines();
764            if let Some(first) = lines.next() {
765                out.push_str(indent);
766                out.push_str(&marker);
767                out.push_str(checkbox);
768                out.push_str(first);
769                out.push('\n');
770            }
771            for line in lines {
772                out.push_str(&child_indent);
773                out.push_str(line);
774                out.push('\n');
775            }
776        }
777        out.push_str(&nested);
778        item_ix += 1;
779    }
780
781    out
782}
783
784impl InlineNode {
785    pub(crate) fn new(text: impl Into<SharedString>) -> Self {
786        Self {
787            text: text.into(),
788            image: None,
789            marks: vec![],
790            state: Arc::new(Mutex::new(InlineState::default())),
791        }
792    }
793
794    pub(crate) fn image(image: ImageNode) -> Self {
795        let mut this = Self::new("");
796        this.image = Some(image);
797        this
798    }
799
800    pub(crate) fn marks(mut self, marks: Vec<(Range<usize>, TextMark)>) -> Self {
801        self.marks = marks;
802        self
803    }
804}
805
806/// The paragraph element, contains multiple text nodes.
807///
808/// Unlike other Element, this is cloneable, because it is used in the Node AST.
809/// We are keep the selection state inside this AST Nodes.
810#[derive(Debug, Clone, Default)]
811pub(crate) struct Paragraph {
812    pub(super) span: Option<Span>,
813    pub(super) children: Vec<InlineNode>,
814    /// The link references in this paragraph, used for reference links.
815    ///
816    /// The key is the identifier, the value is the url.
817    pub(super) link_refs: HashMap<SharedString, SharedString>,
818
819    pub(crate) state: Arc<Mutex<InlineState>>,
820}
821
822impl PartialEq for Paragraph {
823    fn eq(&self, other: &Self) -> bool {
824        self.span == other.span
825            && self.children == other.children
826            && self.link_refs == other.link_refs
827    }
828}
829
830impl Paragraph {
831    pub(crate) fn new(text: String) -> Self {
832        Self {
833            span: None,
834            children: vec![InlineNode::new(&text)],
835            link_refs: HashMap::new(),
836            state: Arc::new(Mutex::new(InlineState::default())),
837        }
838    }
839
840    pub(super) fn selected_text(&self) -> String {
841        let mut text = String::new();
842
843        for c in self.children.iter() {
844            let Ok(state) = c.state.lock() else {
845                continue;
846            };
847            if let Some(selection) = &state.selection {
848                text.push_str(&state.text[selection.start..selection.end]);
849            }
850        }
851
852        if let Ok(state) = self.state.lock()
853            && let Some(selection) = &state.selection
854        {
855            text.push_str(&state.text[selection.start..selection.end]);
856        }
857
858        text
859    }
860
861    /// Reconstruct the Markdown source for the current selection.
862    ///
863    /// Mirrors [`selected_text`](Self::selected_text), but emits Markdown
864    /// instead of the rendered text, using each inline node's `marks` (see
865    /// [`reconstruct_markdown`]).
866    ///
867    /// Selection offsets index an `InlineState.text`, and one such state spans
868    /// *several* children: [`Paragraph::render`] concatenates children until it
869    /// hits an inline image, stores that run in the image child's state, then
870    /// starts over; whatever follows the last image is stored in `self.state`.
871    /// So walk the children in the same runs and map each selected byte back to
872    /// the child it was rendered from — mapping against a single child's text
873    /// would attribute the same offsets to children in other runs.
874    ///
875    /// An image has no selection of its own, so it is emitted when the
876    /// selection runs into it: reaching the end of the run before it, and
877    /// starting at the beginning of the run after it. A paragraph that begins
878    /// or ends with an image has no run on that side, which counts as reaching
879    /// it.
880    pub(super) fn selected_source(&self) -> String {
881        let mut source = String::new();
882        let mut pending_images: Vec<String> = Vec::new();
883        let mut run: Vec<(usize, &InlineNode)> = Vec::new();
884        let mut offset = 0;
885        let mut enters_image = true;
886
887        for child in self.children.iter() {
888            let Some(image) = &child.image else {
889                run.push((offset, child));
890                offset += child.text.len();
891                continue;
892            };
893
894            // The run before an image is stored in that image's own state.
895            let run_before = !run.is_empty();
896            let selected = emit_run(&child.state, &run, &mut pending_images, &mut source);
897            if run_before {
898                enters_image = selected.emitted && selected.at_end;
899            }
900            if enters_image {
901                pending_images.push(image_markdown(image));
902            } else {
903                pending_images.clear();
904            }
905
906            run.clear();
907            offset = 0;
908        }
909
910        let trailing = emit_run(&self.state, &run, &mut pending_images, &mut source);
911        // Trailing images have no run after them to flush them.
912        if !trailing.emitted && enters_image && !source.is_empty() {
913            source.push_str(&pending_images.join(""));
914        }
915
916        source
917    }
918
919    pub(super) fn text(&self) -> String {
920        let mut text = String::new();
921        for node in self.children.iter() {
922            text.push_str(&node.text);
923        }
924        text
925    }
926
927    /// Synchronously clear the selection stored in every inline state.
928    ///
929    /// Mirrors the [`selected_text`](Self::selected_text) traversal.
930    pub(super) fn has_selection(&self) -> bool {
931        self.children
932            .iter()
933            .any(|c| c.state.lock().is_ok_and(|state| state.selection.is_some()))
934            || self
935                .state
936                .lock()
937                .is_ok_and(|state| state.selection.is_some())
938    }
939
940    pub(super) fn clear_selection(&self) {
941        for c in self.children.iter() {
942            if let Ok(mut state) = c.state.lock() {
943                state.selection = None;
944            }
945        }
946
947        if let Ok(mut state) = self.state.lock() {
948            state.selection = None;
949        }
950    }
951}
952
953#[derive(Debug, Clone, Default, PartialEq)]
954pub(crate) struct Table {
955    pub(crate) children: Vec<TableRow>,
956    pub(crate) column_aligns: Vec<ColumnumnAlign>,
957    pub(crate) span: Option<Span>,
958}
959
960/// Plain snapshot of a rendered Markdown table, passed to the
961/// [`crate::text::TextView::table_actions`] hook.
962#[derive(Debug, Clone, Default, PartialEq)]
963pub struct TableData {
964    /// First table row (header cells) as plain text.
965    pub headers: Vec<String>,
966    /// Rows after the header as plain text cells. May be ragged while
967    /// a table is still streaming in.
968    pub rows: Vec<Vec<String>>,
969    /// The table serialized back to GFM pipe-table Markdown, alignments kept.
970    pub markdown: String,
971    /// Byte range of the table in the Markdown source, for callers that need
972    /// to map the table back to the document.
973    ///
974    /// Not needed to keep element ids apart: the actions row is wrapped in its
975    /// own identified element, so plain ids like `"copy"` are already scoped
976    /// per table.
977    pub span: Option<Range<usize>>,
978}
979
980impl Table {
981    pub(crate) fn column_align(&self, index: usize) -> ColumnumnAlign {
982        self.column_aligns.get(index).copied().unwrap_or_default()
983    }
984
985    /// Serialize the table back to GFM pipe-table Markdown (`| a | b |`),
986    /// preserving column alignments. Cell newlines collapse to spaces and
987    /// `|` is escaped so rows stay intact.
988    ///
989    /// Mirrors [`table_selected_source`], which does the same for the selected
990    /// cells only.
991    pub(crate) fn to_markdown(&self) -> String {
992        let mut lines: Vec<String> = Vec::with_capacity(self.children.len() + 1);
993
994        for (row_ix, row) in self.children.iter().enumerate() {
995            let cells: Vec<String> = row
996                .children
997                .iter()
998                .map(|cell| {
999                    cell.children
1000                        .to_markdown()
1001                        .trim()
1002                        .replace('\n', " ")
1003                        .replace('|', "\\|")
1004                })
1005                .collect();
1006            lines.push(format!("| {} |", cells.join(" | ")));
1007
1008            // The Markdown delimiter row carries the column alignments and must
1009            // follow the header row.
1010            if row_ix == 0 {
1011                let aligns: Vec<String> = (0..row.children.len())
1012                    .map(|ix| {
1013                        match self.column_align(ix) {
1014                            ColumnumnAlign::Left => ":--",
1015                            ColumnumnAlign::Center => ":-:",
1016                            ColumnumnAlign::Right => "--:",
1017                        }
1018                        .to_string()
1019                    })
1020                    .collect();
1021                lines.push(format!("| {} |", aligns.join(" | ")));
1022            }
1023        }
1024
1025        lines.join("\n")
1026    }
1027
1028    /// Snapshot of this table for the [`crate::text::TextView::table_actions`]
1029    /// hook.
1030    pub(crate) fn table_data(&self) -> TableData {
1031        let row_text = |row: &TableRow| {
1032            row.children
1033                .iter()
1034                .map(|cell| cell.children.text().trim().to_string())
1035                .collect::<Vec<_>>()
1036        };
1037
1038        TableData {
1039            headers: self.children.first().map(row_text).unwrap_or_default(),
1040            rows: self.children.iter().skip(1).map(row_text).collect(),
1041            markdown: self.to_markdown(),
1042            span: self.span.map(|span| span.start..span.end),
1043        }
1044    }
1045}
1046
1047#[derive(Debug, Default, Copy, Clone, PartialEq)]
1048pub(crate) enum ColumnumnAlign {
1049    #[default]
1050    Left,
1051    Center,
1052    Right,
1053}
1054
1055impl From<mdast::AlignKind> for ColumnumnAlign {
1056    fn from(value: mdast::AlignKind) -> Self {
1057        match value {
1058            mdast::AlignKind::None => ColumnumnAlign::Left,
1059            mdast::AlignKind::Left => ColumnumnAlign::Left,
1060            mdast::AlignKind::Center => ColumnumnAlign::Center,
1061            mdast::AlignKind::Right => ColumnumnAlign::Right,
1062        }
1063    }
1064}
1065
1066#[derive(Debug, Clone, Default, PartialEq)]
1067pub(crate) struct TableRow {
1068    pub children: Vec<TableCell>,
1069}
1070
1071#[derive(Debug, Clone, Default, PartialEq)]
1072pub(crate) struct TableCell {
1073    pub children: Paragraph,
1074    pub width: Option<DefiniteLength>,
1075}
1076
1077impl Paragraph {
1078    pub(crate) fn take(&mut self) -> Paragraph {
1079        std::mem::replace(
1080            self,
1081            Paragraph {
1082                span: None,
1083                children: vec![],
1084                link_refs: Default::default(),
1085                state: Arc::new(Mutex::new(InlineState::default())),
1086            },
1087        )
1088    }
1089
1090    pub(crate) fn is_image(&self) -> bool {
1091        false
1092    }
1093
1094    pub(crate) fn set_span(&mut self, span: Span) {
1095        self.span = Some(span);
1096    }
1097
1098    pub(crate) fn push_str(&mut self, text: &str) {
1099        self.children.push(
1100            InlineNode::new(text.to_string()).marks(vec![(0..text.len(), TextMark::default())]),
1101        );
1102    }
1103
1104    pub(crate) fn push(&mut self, text: InlineNode) {
1105        self.children.push(text);
1106    }
1107
1108    pub(crate) fn push_image(&mut self, image: ImageNode) {
1109        self.children.push(InlineNode::image(image));
1110    }
1111
1112    pub(crate) fn is_empty(&self) -> bool {
1113        self.children.is_empty()
1114            || self
1115                .children
1116                .iter()
1117                .all(|node| node.text.is_empty() && node.image.is_none())
1118    }
1119
1120    /// Return length of children text.
1121    pub(crate) fn text_len(&self) -> usize {
1122        self.children
1123            .iter()
1124            .map(|node| node.text.len())
1125            .sum::<usize>()
1126    }
1127
1128    pub(crate) fn merge(&mut self, other: Self) {
1129        self.children.extend(other.children);
1130    }
1131}
1132
1133#[derive(Debug, Clone)]
1134pub struct CodeBlock {
1135    lang: Option<SharedString>,
1136    state: Arc<Mutex<InlineState>>,
1137    highlight_cache: Arc<Mutex<Option<CachedCodeBlockHighlights>>>,
1138    pub span: Option<Span>,
1139}
1140
1141struct CachedCodeBlockHighlights {
1142    highlighter: Arc<CodeBlockHighlighterFn>,
1143    styles: Vec<(Range<usize>, HighlightStyle)>,
1144}
1145
1146impl std::fmt::Debug for CachedCodeBlockHighlights {
1147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1148        f.debug_struct("CachedCodeBlockHighlights")
1149            .field("styles", &self.styles)
1150            .finish_non_exhaustive()
1151    }
1152}
1153
1154impl PartialEq for CodeBlock {
1155    fn eq(&self, other: &Self) -> bool {
1156        self.lang == other.lang && self.code() == other.code() && self.span == other.span
1157    }
1158}
1159
1160impl CodeBlock {
1161    /// Get the language of the code block.
1162    pub fn lang(&self) -> Option<SharedString> {
1163        self.lang.clone()
1164    }
1165
1166    /// Get the code content of the code block.
1167    pub fn code(&self) -> SharedString {
1168        self.state
1169            .lock()
1170            .map(|state| state.text.clone())
1171            .unwrap_or_default()
1172    }
1173
1174    /// Builds a code block that is not tied to a parsed document.
1175    ///
1176    /// [`crate::TextView::code_block_highlighter`] hands a `&CodeBlock` to the
1177    /// highlighter it is given, so anyone writing one needs a way to build a
1178    /// block to exercise it against.
1179    pub fn from_code(code: impl Into<SharedString>, lang: Option<impl Into<SharedString>>) -> Self {
1180        Self::new(code.into(), lang.map(Into::into), None::<Span>)
1181    }
1182
1183    pub(crate) fn new(
1184        code: SharedString,
1185        lang: Option<SharedString>,
1186        span: Option<impl Into<Span>>,
1187    ) -> Self {
1188        let state = Arc::new(Mutex::new(InlineState::default()));
1189        if let Ok(mut state) = state.lock() {
1190            state.set_text(code);
1191        }
1192
1193        Self {
1194            lang,
1195            state,
1196            highlight_cache: Arc::new(Mutex::new(None)),
1197            span: span.map(|s| s.into()),
1198        }
1199    }
1200
1201    fn highlighted_styles(
1202        &self,
1203        highlighter: &Arc<CodeBlockHighlighterFn>,
1204    ) -> Vec<(Range<usize>, HighlightStyle)> {
1205        if let Ok(cache) = self.highlight_cache.lock()
1206            && let Some(cache) = cache.as_ref()
1207            && Arc::ptr_eq(&cache.highlighter, highlighter)
1208        {
1209            return cache.styles.clone();
1210        }
1211
1212        let code_len = self.code().len();
1213        let styles = highlighter(self)
1214            .into_iter()
1215            .filter(|(range, _)| range.start <= range.end && range.end <= code_len)
1216            .collect::<Vec<_>>();
1217        if let Ok(mut cache) = self.highlight_cache.lock() {
1218            *cache = Some(CachedCodeBlockHighlights {
1219                highlighter: highlighter.clone(),
1220                styles: styles.clone(),
1221            });
1222        }
1223        styles
1224    }
1225
1226    pub(super) fn selected_text(&self) -> String {
1227        let mut text = String::new();
1228        if let Ok(state) = self.state.lock()
1229            && let Some(selection) = &state.selection
1230        {
1231            text.push_str(&state.text[selection.start..selection.end]);
1232        }
1233        text
1234    }
1235
1236    /// Markdown source for the current selection.
1237    ///
1238    /// The selected code is wrapped in a fenced code block carrying the block's
1239    /// language, so a selected code block round-trips as Markdown (e.g.
1240    /// ```` ```rust\n…\n``` ````) instead of pasting as bare, unfenced text.
1241    /// A partial selection is still emitted as a valid fenced block.
1242    pub(super) fn selected_source(&self) -> String {
1243        let code = self.selected_text();
1244        if code.is_empty() {
1245            return String::new();
1246        }
1247        let lang = self.lang.clone().unwrap_or_default();
1248        // Trim trailing newlines so the closing fence sits on its own line
1249        // directly after the last code line (no blank line before it).
1250        let code = code.trim_end_matches('\n');
1251        format!("```{}\n{}\n```", lang, code)
1252    }
1253
1254    pub(super) fn text(&self) -> String {
1255        self.state
1256            .lock()
1257            .map(|state| state.text.to_string())
1258            .unwrap_or_default()
1259    }
1260
1261    /// Synchronously clear the selection stored in the inline state.
1262    ///
1263    /// Mirrors the [`selected_text`](Self::selected_text) traversal.
1264    pub(super) fn has_selection(&self) -> bool {
1265        self.state
1266            .lock()
1267            .is_ok_and(|state| state.selection.is_some())
1268    }
1269
1270    pub(super) fn clear_selection(&self) {
1271        if let Ok(mut state) = self.state.lock() {
1272            state.selection = None;
1273        }
1274    }
1275
1276    fn render(
1277        &self,
1278        options: &NodeRenderOptions,
1279        node_cx: &NodeContext,
1280        window: &mut Window,
1281        cx: &mut App,
1282    ) -> AnyElement {
1283        let style = &node_cx.style;
1284
1285        div()
1286            .w_full()
1287            .min_w_0()
1288            .when(!options.is_last, |this| this.pb(style.paragraph_gap()))
1289            .child(
1290                div()
1291                    .id(("codeblock", options.ix))
1292                    .w_full()
1293                    .min_w_0()
1294                    .p_3()
1295                    .bg(style.code_background())
1296                    .font_family(cx.theme().tokens.typography.mono.clone())
1297                    .text_size(cx.theme().tokens.typography.mono_md.size)
1298                    .relative()
1299                    .refine_style(&style.code_block())
1300                    .child(Inline::new(
1301                        "code",
1302                        self.state.clone(),
1303                        vec![],
1304                        node_cx
1305                            .code_block_highlighter
1306                            .as_ref()
1307                            .map(|highlighter| self.highlighted_styles(highlighter))
1308                            .unwrap_or_default(),
1309                        node_cx.link_click_handler.clone(),
1310                    ))
1311                    .when_some(node_cx.code_block_actions.clone(), |this, actions| {
1312                        this.child(
1313                            div()
1314                                .id("actions")
1315                                .absolute()
1316                                .top_2()
1317                                .right_2()
1318                                .bg(style.code_background())
1319                                .rounded(cx.theme().tokens.radius.md)
1320                                .child(actions(&self, window, cx)),
1321                        )
1322                    }),
1323            )
1324            .into_any_element()
1325    }
1326}
1327
1328/// A context for rendering nodes, contains link references.
1329#[derive(Default, Clone)]
1330pub(crate) struct NodeContext {
1331    /// The byte offset of the node in the original markdown text.
1332    /// Used for incremental updates.
1333    pub(crate) offset: usize,
1334    pub(crate) link_refs: HashMap<SharedString, LinkMark>,
1335    pub(crate) style: TextViewStyle,
1336    pub(crate) code_block_actions: Option<Arc<CodeBlockActionsFn>>,
1337    pub(crate) code_block_highlighter: Option<Arc<CodeBlockHighlighterFn>>,
1338    pub(crate) table_actions: Option<Arc<TableActionsFn>>,
1339    pub(crate) link_click_handler: Option<Arc<LinkClickHandlerFn>>,
1340    pub(crate) markdown_extensions: Arc<MarkdownExtensions>,
1341}
1342
1343impl NodeContext {
1344    pub(super) fn add_ref(&mut self, identifier: SharedString, link: LinkMark) {
1345        self.link_refs.insert(identifier, link);
1346    }
1347}
1348
1349impl PartialEq for NodeContext {
1350    fn eq(&self, other: &Self) -> bool {
1351        self.link_refs == other.link_refs && self.style == other.style
1352        // Note: code_block_actions, table_actions and markdown_extensions are
1353        // intentionally not compared (closures can't be compared)
1354    }
1355}
1356
1357impl Paragraph {
1358    fn render(&self, node_cx: &NodeContext, _window: &mut Window, cx: &mut App) -> AnyElement {
1359        let span = self.span;
1360        let children = &self.children;
1361
1362        if self.should_render_inline_flow() {
1363            return InlineFlow::new(
1364                span.unwrap_or_default(),
1365                self.inline_flow_items(node_cx, cx),
1366                node_cx.link_click_handler.clone(),
1367            )
1368            .into_any_element();
1369        }
1370
1371        let mut child_nodes: Vec<AnyElement> = vec![];
1372
1373        let mut text = String::new();
1374        let mut highlights: Vec<(Range<usize>, HighlightStyle)> = vec![];
1375        let mut links: Vec<(Range<usize>, LinkMark)> = vec![];
1376        let mut offset = 0;
1377
1378        let mut ix = 0;
1379        for inline_node in children {
1380            let text_len = inline_node.text.len();
1381            text.push_str(&inline_node.text);
1382
1383            if let Some(image) = &inline_node.image {
1384                if text.len() > 0 {
1385                    if let Ok(mut state) = inline_node.state.lock() {
1386                        state.set_text(text.clone().into());
1387                    }
1388                    child_nodes.push(
1389                        Inline::new(
1390                            ix,
1391                            inline_node.state.clone(),
1392                            links.clone(),
1393                            highlights.clone(),
1394                            node_cx.link_click_handler.clone(),
1395                        )
1396                        .into_any_element(),
1397                    );
1398                }
1399                let link_click_handler = node_cx.link_click_handler.clone();
1400                child_nodes.push(
1401                    img(image_source(&image.url))
1402                        .id(ix)
1403                        .object_fit(ObjectFit::Contain)
1404                        .max_w(relative(1.))
1405                        .when_some(image.width, |this, width| this.w(width))
1406                        .when_some(image.link.clone(), |this, link| {
1407                            let link_click_handler = link_click_handler.clone();
1408                            let aux_link = link.clone();
1409                            let aux_link_click_handler = link_click_handler.clone();
1410                            this.cursor_pointer()
1411                                .on_click(move |event, window, cx| {
1412                                    crate::TextSelection::end(window, cx);
1413                                    cx.stop_propagation();
1414                                    handle_link_click(
1415                                        &link_click_handler,
1416                                        link.url.clone(),
1417                                        event.clone(),
1418                                        window,
1419                                        cx,
1420                                    );
1421                                })
1422                                .on_aux_click(move |event, window, cx| {
1423                                    crate::TextSelection::end(window, cx);
1424                                    cx.stop_propagation();
1425                                    handle_link_click(
1426                                        &aux_link_click_handler,
1427                                        aux_link.url.clone(),
1428                                        event.clone(),
1429                                        window,
1430                                        cx,
1431                                    );
1432                                })
1433                        })
1434                        .into_any_element(),
1435                );
1436
1437                text.clear();
1438                links.clear();
1439                highlights.clear();
1440                offset = 0;
1441            } else {
1442                let mut node_highlights = vec![];
1443                for (range, style) in &inline_node.marks {
1444                    let inner_range = (offset + range.start)..(offset + range.end);
1445
1446                    let mut highlight = HighlightStyle::default();
1447                    if style.bold {
1448                        highlight.font_weight = Some(FontWeight::BOLD);
1449                    }
1450                    if style.italic {
1451                        highlight.font_style = Some(FontStyle::Italic);
1452                    }
1453                    if style.strikethrough {
1454                        highlight.strikethrough = Some(gpui::StrikethroughStyle {
1455                            thickness: gpui::px(1.),
1456                            ..Default::default()
1457                        });
1458                    }
1459                    if style.underline {
1460                        highlight.underline = Some(gpui::UnderlineStyle {
1461                            thickness: gpui::px(1.),
1462                            ..Default::default()
1463                        });
1464                    }
1465                    if style.code {
1466                        highlight = highlight.highlight(node_cx.style.inline_code_highlight());
1467                    }
1468                    if let Some(color) = style.highlight {
1469                        highlight.background_color = Some(color);
1470                    }
1471
1472                    if let Some(mut link_mark) = style.link.clone() {
1473                        highlight.color = Some(node_cx.style.link());
1474                        highlight.underline = Some(gpui::UnderlineStyle {
1475                            thickness: gpui::px(1.),
1476                            ..Default::default()
1477                        });
1478
1479                        // convert link references, replace link
1480                        if let Some(identifier) = link_mark.identifier.as_ref() {
1481                            if let Some(mark) = node_cx.link_refs.get(identifier) {
1482                                link_mark = mark.clone();
1483                            }
1484                        }
1485
1486                        links.push((inner_range.clone(), link_mark));
1487                    }
1488
1489                    node_highlights.push((inner_range, highlight));
1490                }
1491
1492                highlights = gpui::combine_highlights(highlights, node_highlights).collect();
1493                offset += text_len;
1494            }
1495            ix += 1;
1496        }
1497
1498        // Add the last text node
1499        if text.len() > 0 {
1500            if let Ok(mut state) = self.state.lock() {
1501                state.set_text(text.into());
1502            }
1503            child_nodes.push(
1504                Inline::new(
1505                    ix,
1506                    self.state.clone(),
1507                    links,
1508                    highlights,
1509                    node_cx.link_click_handler.clone(),
1510                )
1511                .into_any_element(),
1512            );
1513        }
1514
1515        div()
1516            .id(span.unwrap_or_default())
1517            .children(child_nodes)
1518            .into_any_element()
1519    }
1520
1521    fn should_render_inline_flow(&self) -> bool {
1522        let has_image = self.children.iter().any(|child| child.image.is_some());
1523        let has_text = self.children.iter().any(|child| !child.text.is_empty());
1524        has_image && has_text
1525    }
1526
1527    fn inline_flow_items(&self, node_cx: &NodeContext, _cx: &mut App) -> Vec<InlineFlowItem> {
1528        let mut items = Vec::new();
1529        let mut text = String::new();
1530        let mut highlights: Vec<(Range<usize>, HighlightStyle)> = vec![];
1531        let mut links: Vec<(Range<usize>, LinkMark)> = vec![];
1532        let mut offset = 0;
1533
1534        for inline_node in &self.children {
1535            let text_len = inline_node.text.len();
1536            text.push_str(&inline_node.text);
1537
1538            if let Some(image) = &inline_node.image {
1539                if !text.is_empty() {
1540                    if let Ok(mut state) = inline_node.state.lock() {
1541                        state.set_text(text.clone().into());
1542                    }
1543                    items.push(InlineFlowItem::Text {
1544                        state: inline_node.state.clone(),
1545                        text: text.clone().into(),
1546                        links: links.clone(),
1547                        highlights: highlights.clone(),
1548                    });
1549                }
1550
1551                items.push(InlineFlowItem::Image {
1552                    url: image.url.clone(),
1553                    link: image.link.clone(),
1554                    title: image.title(),
1555                    width: image.width,
1556                    height: image.height,
1557                });
1558
1559                text.clear();
1560                links.clear();
1561                highlights.clear();
1562                offset = 0;
1563            } else {
1564                let mut node_highlights = vec![];
1565                for (range, style) in &inline_node.marks {
1566                    let inner_range = (offset + range.start)..(offset + range.end);
1567
1568                    let mut highlight = HighlightStyle::default();
1569                    if style.bold {
1570                        highlight.font_weight = Some(FontWeight::BOLD);
1571                    }
1572                    if style.italic {
1573                        highlight.font_style = Some(FontStyle::Italic);
1574                    }
1575                    if style.strikethrough {
1576                        highlight.strikethrough = Some(gpui::StrikethroughStyle {
1577                            thickness: gpui::px(1.),
1578                            ..Default::default()
1579                        });
1580                    }
1581                    if style.underline {
1582                        highlight.underline = Some(gpui::UnderlineStyle {
1583                            thickness: gpui::px(1.),
1584                            ..Default::default()
1585                        });
1586                    }
1587                    if style.code {
1588                        highlight = highlight.highlight(node_cx.style.inline_code_highlight());
1589                    }
1590                    if let Some(color) = style.highlight {
1591                        highlight.background_color = Some(color);
1592                    }
1593
1594                    if let Some(mut link_mark) = style.link.clone() {
1595                        highlight.color = Some(node_cx.style.link());
1596                        highlight.underline = Some(gpui::UnderlineStyle {
1597                            thickness: gpui::px(1.),
1598                            ..Default::default()
1599                        });
1600
1601                        if let Some(identifier) = link_mark.identifier.as_ref()
1602                            && let Some(mark) = node_cx.link_refs.get(identifier)
1603                        {
1604                            link_mark = mark.clone();
1605                        }
1606
1607                        links.push((inner_range.clone(), link_mark));
1608                    }
1609
1610                    node_highlights.push((inner_range, highlight));
1611                }
1612
1613                highlights = gpui::combine_highlights(highlights, node_highlights).collect();
1614                offset += text_len;
1615            }
1616        }
1617
1618        if !text.is_empty() {
1619            if let Ok(mut state) = self.state.lock() {
1620                state.set_text(text.clone().into());
1621            }
1622            items.push(InlineFlowItem::Text {
1623                state: self.state.clone(),
1624                text: text.into(),
1625                links,
1626                highlights,
1627            });
1628        }
1629
1630        items
1631    }
1632}
1633
1634impl Paragraph {
1635    fn to_markdown(&self) -> String {
1636        let mut text = self
1637            .children
1638            .iter()
1639            .map(|text_node| {
1640                let mut text = text_node.text.to_string();
1641                for (range, style) in &text_node.marks {
1642                    if style.bold {
1643                        text = format!("**{}**", &text_node.text[range.clone()]);
1644                    }
1645                    if style.italic {
1646                        text = format!("*{}*", &text_node.text[range.clone()]);
1647                    }
1648                    if style.strikethrough {
1649                        text = format!("~~{}~~", &text_node.text[range.clone()]);
1650                    }
1651                    if style.code {
1652                        text = format!("`{}`", &text_node.text[range.clone()]);
1653                    }
1654                    if style.highlight.is_some() {
1655                        text = format!("=={}==", &text_node.text[range.clone()]);
1656                    }
1657                    if let Some(link) = &style.link {
1658                        text = format!("[{}]({})", &text_node.text[range.clone()], link.url);
1659                    }
1660                }
1661
1662                if let Some(image) = &text_node.image {
1663                    let alt = image.alt.clone().unwrap_or_default();
1664                    let title = image
1665                        .title
1666                        .clone()
1667                        .map_or(String::new(), |t| format!(" \"{}\"", t));
1668                    text.push_str(&format!("![{}]({}{})", alt, image.url, title))
1669                }
1670
1671                text
1672            })
1673            .collect::<Vec<_>>()
1674            .join("");
1675
1676        text.push_str("\n\n");
1677        text
1678    }
1679}
1680
1681impl BlockNode {
1682    /// Converts the node to markdown format.
1683    ///
1684    /// This is used to generate markdown for test.
1685    #[allow(dead_code)]
1686    pub(crate) fn to_markdown(&self) -> String {
1687        match self {
1688            BlockNode::Root { children, .. } => children
1689                .iter()
1690                .map(|child| child.to_markdown())
1691                .collect::<Vec<_>>()
1692                .join("\n\n"),
1693            BlockNode::Paragraph(paragraph) => paragraph.to_markdown(),
1694            BlockNode::Heading {
1695                level, children, ..
1696            } => {
1697                let hashes = "#".repeat(*level as usize);
1698                format!("{} {}", hashes, children.to_markdown())
1699            }
1700            BlockNode::Blockquote { children, .. } => {
1701                let content = children
1702                    .iter()
1703                    .map(|child| child.to_markdown())
1704                    .collect::<Vec<_>>()
1705                    .join("\n\n");
1706
1707                content
1708                    .lines()
1709                    .map(|line| format!("> {}", line))
1710                    .collect::<Vec<_>>()
1711                    .join("\n")
1712            }
1713            BlockNode::List {
1714                children, ordered, ..
1715            } => children
1716                .iter()
1717                .enumerate()
1718                .map(|(i, child)| {
1719                    let prefix = if *ordered {
1720                        format!("{}. ", i + 1)
1721                    } else {
1722                        "- ".to_string()
1723                    };
1724                    format!("{}{}", prefix, child.to_markdown())
1725                })
1726                .collect::<Vec<_>>()
1727                .join("\n"),
1728            BlockNode::ListItem {
1729                children, checked, ..
1730            } => {
1731                let checkbox = if let Some(checked) = checked {
1732                    if *checked { "[x] " } else { "[ ] " }
1733                } else {
1734                    ""
1735                };
1736                format!(
1737                    "{}{}",
1738                    checkbox,
1739                    children
1740                        .iter()
1741                        .map(|child| child.to_markdown())
1742                        .collect::<Vec<_>>()
1743                        .join("\n")
1744                )
1745            }
1746            BlockNode::CodeBlock(code_block) => {
1747                format!(
1748                    "```{}\n{}\n```",
1749                    code_block.lang.clone().unwrap_or_default(),
1750                    code_block.code()
1751                )
1752            }
1753            BlockNode::Table(table) => table.to_markdown(),
1754            BlockNode::Break { html, .. } => {
1755                if *html {
1756                    "<br>".to_string()
1757                } else {
1758                    "\n".to_string()
1759                }
1760            }
1761            BlockNode::HorizontalRule { .. } => "---".to_string(),
1762            BlockNode::Custom(node) => node.to_markdown(),
1763            BlockNode::Definition {
1764                identifier,
1765                url,
1766                title,
1767                ..
1768            } => {
1769                if let Some(title) = title {
1770                    format!("[{}]: {} \"{}\"", identifier, url, title)
1771                } else {
1772                    format!("[{}]: {}", identifier, url)
1773                }
1774            }
1775            BlockNode::Unknown { .. } => "".to_string(),
1776        }
1777        .trim()
1778        .to_string()
1779    }
1780}
1781
1782impl BlockNode {
1783    fn render_list_item_row(
1784        content: AnyElement,
1785        ix: usize,
1786        options: NodeRenderOptions,
1787        checked: Option<bool>,
1788        style: &TextViewStyle,
1789        line_height: Pixels,
1790    ) -> Div {
1791        h_flex()
1792            .w_full()
1793            .flex_1()
1794            .min_w_0()
1795            .relative()
1796            .items_start()
1797            .content_start()
1798            .when(!options.todo && checked.is_none(), |this| {
1799                this.child(list_item_prefix(ix, options.ordered, options.depth))
1800            })
1801            .when_some(checked, |this, checked| {
1802                // Todo list checkbox
1803                let check_svg = if style.is_dark() {
1804                    CHECK_SVG_DARK
1805                } else {
1806                    CHECK_SVG_LIGHT
1807                };
1808                this.child(
1809                    div()
1810                        .flex()
1811                        .mr_1p5()
1812                        .h(line_height)
1813                        .flex_none()
1814                        .items_center()
1815                        .justify_center()
1816                        .child(
1817                            div()
1818                                .flex()
1819                                .size(rems(0.875))
1820                                .items_center()
1821                                .justify_center()
1822                                .border_1()
1823                                .border_color(style.foreground())
1824                                .when(checked, |this| {
1825                                    this.bg(style.foreground()).child(
1826                                        img(Arc::new(Image::from_bytes(
1827                                            ImageFormat::Svg,
1828                                            check_svg.to_vec(),
1829                                        )))
1830                                        .size(rems(0.625)),
1831                                    )
1832                                }),
1833                        ),
1834                )
1835            })
1836            .child(div().flex_1().min_w_0().overflow_hidden().child(content))
1837    }
1838
1839    fn render_list_item(
1840        item: &BlockNode,
1841        ix: usize,
1842        options: NodeRenderOptions,
1843        node_cx: &NodeContext,
1844        window: &mut Window,
1845        cx: &mut App,
1846    ) -> AnyElement {
1847        match item {
1848            BlockNode::ListItem {
1849                children,
1850                spread,
1851                checked,
1852                ..
1853            } => v_flex()
1854                .id(("li", options.ix))
1855                .w_full()
1856                .min_w_0()
1857                .when(*spread, |this| this.child(div()))
1858                .children({
1859                    let mut items: Vec<Div> = Vec::with_capacity(children.len());
1860
1861                    for (child_ix, child) in children.iter().enumerate() {
1862                        match child {
1863                            BlockNode::Paragraph { .. } => {
1864                                let last_not_list = child_ix > 0
1865                                    && !matches!(children[child_ix - 1], BlockNode::List { .. });
1866
1867                                let text = child.render_block(
1868                                    NodeRenderOptions {
1869                                        depth: options.depth + 1,
1870                                        todo: checked.is_some(),
1871                                        is_last: true,
1872                                        ..options
1873                                    },
1874                                    node_cx,
1875                                    window,
1876                                    cx,
1877                                );
1878
1879                                // Continuation paragraph — stack vertically below
1880                                // the previous row, indented to align with the text
1881                                // column (past bullet/number prefix).
1882                                if last_not_list {
1883                                    if let Some(preceding_row) = items.pop() {
1884                                        items.push(
1885                                            v_flex().child(preceding_row).child(
1886                                                div()
1887                                                    .w_full()
1888                                                    .pl(rems(1.))
1889                                                    .overflow_hidden()
1890                                                    .child(text),
1891                                            ),
1892                                        );
1893                                        continue;
1894                                    }
1895                                }
1896
1897                                items.push(Self::render_list_item_row(
1898                                    text,
1899                                    ix,
1900                                    options,
1901                                    *checked,
1902                                    &node_cx.style,
1903                                    window.line_height(),
1904                                ));
1905                            }
1906                            BlockNode::List { .. } => {
1907                                items.push(div().ml(rems(1.)).child(child.render_block(
1908                                    NodeRenderOptions {
1909                                        depth: options.depth + 1,
1910                                        todo: checked.is_some(),
1911                                        is_last: true,
1912                                        ..options
1913                                    },
1914                                    node_cx,
1915                                    window,
1916                                    cx,
1917                                )));
1918                            }
1919                            BlockNode::Root { .. }
1920                            | BlockNode::Heading { .. }
1921                            | BlockNode::Blockquote { .. }
1922                            | BlockNode::CodeBlock(_)
1923                            | BlockNode::Custom(_)
1924                            | BlockNode::Table(_)
1925                            | BlockNode::HorizontalRule { .. } => {
1926                                let block = child.render_block(
1927                                    NodeRenderOptions {
1928                                        depth: options.depth + 1,
1929                                        todo: checked.is_some(),
1930                                        is_last: true,
1931                                        ..options
1932                                    },
1933                                    node_cx,
1934                                    window,
1935                                    cx,
1936                                );
1937
1938                                if child_ix == 0 {
1939                                    items.push(Self::render_list_item_row(
1940                                        block,
1941                                        ix,
1942                                        options,
1943                                        *checked,
1944                                        &node_cx.style,
1945                                        window.line_height(),
1946                                    ));
1947                                } else {
1948                                    // Indent continuation blocks to align with a
1949                                    // nested sub-list (`ml(rems(1.))`) and with
1950                                    // continuation paragraphs.
1951                                    items.push(
1952                                        div()
1953                                            .w_full()
1954                                            .min_w_0()
1955                                            .pl(rems(1.))
1956                                            .overflow_hidden()
1957                                            .child(block),
1958                                    );
1959                                }
1960                            }
1961                            BlockNode::ListItem { .. }
1962                            | BlockNode::Break { .. }
1963                            | BlockNode::Definition { .. }
1964                            | BlockNode::Unknown => {}
1965                        }
1966                    }
1967                    items
1968                })
1969                .into_any_element(),
1970            _ => div().into_any_element(),
1971        }
1972    }
1973
1974    /// Render a Markdown table. Dispatches to a horizontally scrollable layout
1975    /// when `style.table` opts in with overflow-x: scroll, otherwise to the
1976    /// default layout that fits the container width and wraps cell content.
1977    fn render_table(
1978        item: &BlockNode,
1979        options: &NodeRenderOptions,
1980        node_cx: &NodeContext,
1981        window: &mut Window,
1982        cx: &mut App,
1983    ) -> impl IntoElement {
1984        const DEFAULT_LENGTH: usize = 5;
1985
1986        let table = match item {
1987            BlockNode::Table(table) => table,
1988            _ => return div().into_any_element(),
1989        };
1990
1991        // Per-column max text length (in chars), used to proportion the columns
1992        // in the default (wrap) layout.
1993        let mut col_lens: Vec<usize> = vec![];
1994        for row in table.children.iter() {
1995            for (ix, cell) in row.children.iter().enumerate() {
1996                if col_lens.len() <= ix {
1997                    col_lens.push(DEFAULT_LENGTH);
1998                }
1999                col_lens[ix] = col_lens[ix].max(cell.children.text_len());
2000            }
2001        }
2002
2003        // Scroll mode is opted in via `style.table` overflow-x: scroll.
2004        if matches!(node_cx.style.table().overflow.x, Some(Overflow::Scroll)) {
2005            Self::render_scroll_table(table, col_lens.len(), options, node_cx, window, cx)
2006        } else {
2007            Self::render_wrap_table(table, &col_lens, options, node_cx, window, cx)
2008        }
2009    }
2010
2011    /// Horizontally scrollable table layout (opt-in via `style.table`
2012    /// overflow-x: scroll).
2013    ///
2014    /// Column widths come from the **measured** shaped text of each cell (the
2015    /// widest per column across all rows), so columns line up and fit their
2016    /// content exactly — char-count heuristics are inaccurate on proportional
2017    /// fonts. The layout adapts to the frame like CSS auto table layout:
2018    ///
2019    /// - Wider than the content: cells `flex_grow` proportionally to fill.
2020    /// - Narrower: columns shrink and their text wraps, but not below a
2021    ///   per-column floor.
2022    /// - Narrower than the floors: the table keeps the floor widths and
2023    ///   scrolls horizontally, so no content ever becomes unreachable.
2024    ///
2025    /// `white_space: nowrap` on `style.table_cell` composes like in CSS: the
2026    /// refinement keeps cell text on a single line, and the floors are raised
2027    /// to the full content widths so the single-line columns never shrink —
2028    /// the table scrolls as soon as the content is wider than the frame.
2029    fn render_scroll_table(
2030        table: &Table,
2031        col_count: usize,
2032        options: &NodeRenderOptions,
2033        node_cx: &NodeContext,
2034        window: &mut Window,
2035        cx: &mut App,
2036    ) -> AnyElement {
2037        const CELL_PAD_PX: f32 = 16.0; // px_2 horizontal padding
2038        const CELL_MIN_PX: f32 = 48.0;
2039        // Shrinking columns stop (and the table starts to scroll) at a floor
2040        // scaled to their content: roughly the width at which the text wraps
2041        // to `CELL_WRAP_MAX_LINES` lines, clamped between the two bounds so
2042        // moderate columns can still wrap meaningfully while one huge column
2043        // cannot push the scroll threshold arbitrarily high.
2044        const CELL_WRAP_MAX_LINES: f32 = 2.0;
2045        const CELL_WRAP_MIN_PX: f32 = 160.0;
2046        const CELL_WRAP_MAX_PX: f32 = 480.0;
2047        const CELL_BORDER_PX: f32 = 1.0; // border_r_1 drawn by every column but the last
2048        const TABLE_BORDER_PX: f32 = 2.0; // the track's border_1, left + right
2049
2050        // Measure the widest text per column (max-content width). Never
2051        // capped: a cap would clip overflowing text *and* leave it outside
2052        // the scrollable width, making it unreachable.
2053        let text_style = window.text_style();
2054        let font_size = text_style.font_size.to_pixels(window.rem_size());
2055        let mut col_w = vec![CELL_MIN_PX; col_count];
2056        for row in table.children.iter() {
2057            for (ix, cell) in row.children.iter().enumerate() {
2058                let Some(slot) = col_w.get_mut(ix) else {
2059                    continue;
2060                };
2061                let mut w = 0.0_f32;
2062                for line in cell.children.text().split('\n') {
2063                    let line = line.trim();
2064                    if line.is_empty() {
2065                        continue;
2066                    }
2067                    let run = text_style.to_run(line.len());
2068                    let line_w = window
2069                        .text_system()
2070                        .layout_line(line, font_size, &[run], None)
2071                        .width;
2072                    w = w.max(f32::from(line_w));
2073                }
2074                // Border-box widths, so the padding and border the cell draws
2075                // must leave the measured text its full width.
2076                let border = if ix + 1 < col_count {
2077                    CELL_BORDER_PX
2078                } else {
2079                    0.
2080                };
2081                *slot = slot.max(w + CELL_PAD_PX + border);
2082            }
2083        }
2084        let style = &node_cx.style;
2085        // Nowrap cells (via the `table_cell` refinement, which cascades to
2086        // the cell text) must never shrink below their single-line content,
2087        // so their floor is the content width itself.
2088        let nowrap = style.table_cell().text.white_space == Some(WhiteSpace::Nowrap);
2089        let col_min_w: Vec<f32> = if nowrap {
2090            col_w.clone()
2091        } else {
2092            col_w
2093                .iter()
2094                .map(|w| {
2095                    (w / CELL_WRAP_MAX_LINES)
2096                        .clamp(CELL_WRAP_MIN_PX, CELL_WRAP_MAX_PX)
2097                        .min(*w)
2098                })
2099                .collect()
2100        };
2101        let min_total_w: f32 = col_min_w.iter().sum::<f32>() + TABLE_BORDER_PX;
2102
2103        let table_scroll_key = if let Some(span) = table.span {
2104            SharedString::from(format!(
2105                "{}-table-scroll-{}:{}",
2106                window.current_view(),
2107                span.start,
2108                span.end
2109            ))
2110        } else {
2111            SharedString::from(format!(
2112                "{}-table-scroll-{}",
2113                window.current_view(),
2114                options.ix
2115            ))
2116        };
2117        let scroll_handle = window
2118            .use_keyed_state(table_scroll_key, cx, |_, _| ScrollHandle::default())
2119            .read(cx)
2120            .clone();
2121        let row_count = table.children.len();
2122        let mut rows = Vec::with_capacity(row_count);
2123        for (row_ix, row) in table.children.iter().enumerate() {
2124            let mut cells = Vec::with_capacity(row.children.len());
2125            for (ix, cell) in row.children.iter().enumerate() {
2126                let align = table.column_align(ix);
2127                let is_last_col = ix == row.children.len() - 1;
2128                let width = col_w.get(ix).copied().unwrap_or(CELL_MIN_PX);
2129                let min_width = col_min_w.get(ix).copied().unwrap_or(CELL_MIN_PX);
2130                cells.push(
2131                    div()
2132                        .id(("cell", ix))
2133                        // Measured max-content width is the flex-basis;
2134                        // `flex_grow` (proportional to it) distributes extra
2135                        // space so a narrow table still fills the frame, while
2136                        // shrinking is clamped at `min_w` — the flex engine
2137                        // squeezes columns (their text wraps) down to the
2138                        // floors before the track starts to scroll.
2139                        .flex_basis(px(width))
2140                        .flex_grow(width)
2141                        .flex_shrink(1.)
2142                        .min_w(px(min_width))
2143                        .overflow_hidden()
2144                        .when(align == ColumnumnAlign::Center, |this| this.text_center())
2145                        .when(align == ColumnumnAlign::Right, |this| this.text_right())
2146                        .px_2()
2147                        .py_1()
2148                        .when(!is_last_col, |this| {
2149                            this.border_r_1().border_color(style.border())
2150                        })
2151                        .refine_style(&style.table_cell())
2152                        .child(cell.children.render(node_cx, window, cx)),
2153                );
2154            }
2155            rows.push(
2156                div()
2157                    .id("row")
2158                    .w_full()
2159                    .when(row_ix < row_count - 1, |this| this.border_b_1())
2160                    .border_color(style.border())
2161                    .flex()
2162                    .flex_row()
2163                    // The first row is the header, as everywhere else that
2164                    // reads a table (`table_data`, `to_markdown`). The
2165                    // refinement comes last so it can override the defaults.
2166                    .when(row_ix == 0, |this| {
2167                        this.bg(style.code_background())
2168                            .text_color(style.foreground())
2169                            .refine_style(&style.table_head())
2170                    })
2171                    .children(cells),
2172            );
2173        }
2174
2175        div()
2176            .pb(rems(1.))
2177            .w_full()
2178            .child(
2179                // Scroll viewport owns the visible frame, including any
2180                // caller-provided radius. Keeping the border here makes the
2181                // rounded frame stable while the wider row track moves below it.
2182                //
2183                // `horizontal_scroll_area` clips with `overflow_hidden` and
2184                // delegates the wheel to a sibling `ScrollableMask`, so the
2185                // gesture is locked to its starting axis and a horizontal swipe
2186                // is consumed before an ancestor scroller (`gpui::list` under
2187                // `TextView::scrollable`) can take its vertical component.
2188                horizontal_scroll_area(
2189                    ("table", options.ix),
2190                    &scroll_handle,
2191                    &StyleRefinement::default()
2192                        .bg(cx.theme().tokens.colors.surface)
2193                        .border_1()
2194                        .border_color(style.border())
2195                        .refine_style(style.table()),
2196                    // Row track sized to `max(viewport, column floors)`:
2197                    // `min_w_full` fills the frame while the columns can still
2198                    // shrink-to-fit (their text wrapping), the definite
2199                    // `w(min_total_w)` keeps the floors once they are reached,
2200                    // letting the track exceed the viewport and scroll.
2201                    div().min_w_full().w(px(min_total_w)).children(rows),
2202                ),
2203            )
2204            // Custom actions row (e.g. copy / download) rendered below the
2205            // table. The hook's element spans full width; alignment is up to
2206            // the caller (e.g. `h_flex().justify_end()`). The gap keeps hover
2207            // backgrounds of the action buttons off the table border, and the
2208            // id scopes the caller's element ids per table, so plain ids like
2209            // `"copy"` don't collide across tables (same as code blocks).
2210            .children(node_cx.table_actions.clone().map(|f| {
2211                div().id(("table-actions", options.ix)).mt_1().child(f(
2212                    &table.table_data(),
2213                    window,
2214                    cx,
2215                ))
2216            }))
2217            .into_any_element()
2218    }
2219
2220    /// Default table layout: a flex grid whose columns are proportioned by
2221    /// content length and shrink to fit the container width (cell text wraps).
2222    fn render_wrap_table(
2223        table: &Table,
2224        col_lens: &[usize],
2225        options: &NodeRenderOptions,
2226        node_cx: &NodeContext,
2227        window: &mut Window,
2228        cx: &mut App,
2229    ) -> AnyElement {
2230        const MAX_LENGTH: usize = 150;
2231
2232        let style = &node_cx.style;
2233        let row_count = table.children.len();
2234        let mut rows = Vec::with_capacity(row_count);
2235        for (row_ix, row) in table.children.iter().enumerate() {
2236            let mut cells = Vec::with_capacity(row.children.len());
2237            for (ix, cell) in row.children.iter().enumerate() {
2238                let align = table.column_align(ix);
2239                let is_last_col = ix == row.children.len() - 1;
2240                let len = col_lens
2241                    .get(ix)
2242                    .copied()
2243                    .unwrap_or(MAX_LENGTH)
2244                    .min(MAX_LENGTH);
2245
2246                cells.push(
2247                    div()
2248                        .id(("cell", ix))
2249                        .overflow_hidden()
2250                        .when(align == ColumnumnAlign::Center, |this| this.text_center())
2251                        .when(align == ColumnumnAlign::Right, |this| this.text_right())
2252                        .min_w_16()
2253                        .w(Length::Definite(relative(len as f32)))
2254                        .px_2()
2255                        .py_1()
2256                        .when(!is_last_col, |this| {
2257                            this.border_r_1().border_color(style.border())
2258                        })
2259                        .refine_style(&style.table_cell())
2260                        .child(cell.children.render(node_cx, window, cx)),
2261                );
2262            }
2263
2264            rows.push(
2265                div()
2266                    .id("row")
2267                    .w_full()
2268                    .when(row_ix < row_count - 1, |this| this.border_b_1())
2269                    .border_color(style.border())
2270                    .flex()
2271                    .flex_row()
2272                    // The first row is the header, as everywhere else that
2273                    // reads a table (`table_data`, `to_markdown`). The
2274                    // refinement comes last so it can override the defaults.
2275                    .when(row_ix == 0, |this| {
2276                        this.bg(style.code_background())
2277                            .text_color(style.foreground())
2278                            .refine_style(&style.table_head())
2279                    })
2280                    .children(cells),
2281            );
2282        }
2283
2284        div()
2285            .pb(rems(1.))
2286            .w_full()
2287            .child(
2288                div()
2289                    .id(("table", options.ix))
2290                    .w_full()
2291                    .bg(cx.theme().tokens.colors.surface)
2292                    .border_1()
2293                    .border_color(style.border())
2294                    .overflow_hidden()
2295                    .children(rows)
2296                    .refine_style(&style.table()),
2297            )
2298            // Custom actions row (e.g. copy / download) rendered below the
2299            // table. The hook's element spans full width; alignment is up to
2300            // the caller (e.g. `h_flex().justify_end()`). The gap keeps hover
2301            // backgrounds of the action buttons off the table border, and the
2302            // id scopes the caller's element ids per table, so plain ids like
2303            // `"copy"` don't collide across tables (same as code blocks).
2304            .children(node_cx.table_actions.clone().map(|f| {
2305                div().id(("table-actions", options.ix)).mt_1().child(f(
2306                    &table.table_data(),
2307                    window,
2308                    cx,
2309                ))
2310            }))
2311            .into_any_element()
2312    }
2313
2314    pub(crate) fn render_block(
2315        &self,
2316        options: NodeRenderOptions,
2317        node_cx: &NodeContext,
2318        window: &mut Window,
2319        cx: &mut App,
2320    ) -> AnyElement {
2321        let ix = options.ix;
2322        let mb = if options.in_list || options.is_last {
2323            rems(0.)
2324        } else {
2325            node_cx.style.paragraph_gap()
2326        };
2327
2328        match self {
2329            BlockNode::Root { children, .. } => div()
2330                .id(("div", ix))
2331                .children(children.into_iter().enumerate().map(move |(ix, node)| {
2332                    node.render_block(NodeRenderOptions { ix, ..options }, node_cx, window, cx)
2333                }))
2334                .into_any_element(),
2335            BlockNode::Paragraph(paragraph) => div()
2336                .id(("p", ix))
2337                .pb(mb)
2338                .child(paragraph.render(node_cx, window, cx))
2339                .into_any_element(),
2340            BlockNode::Heading {
2341                level, children, ..
2342            } => {
2343                let (text_size, font_weight) = match level {
2344                    1 => (rems(2.), FontWeight::BOLD),
2345                    2 => (rems(1.5), FontWeight::SEMIBOLD),
2346                    3 => (rems(1.25), FontWeight::SEMIBOLD),
2347                    4 => (rems(1.125), FontWeight::SEMIBOLD),
2348                    5 => (rems(1.), FontWeight::SEMIBOLD),
2349                    6 => (rems(1.), FontWeight::MEDIUM),
2350                    _ => (rems(1.), FontWeight::NORMAL),
2351                };
2352
2353                let mut text_size = text_size.to_pixels(node_cx.style.heading_base_font_size());
2354                if let Some(size) = node_cx.style.heading_font_size(*level) {
2355                    text_size = size;
2356                }
2357
2358                div()
2359                    .id(SharedString::from(format!("h{}-{}", level, ix)))
2360                    .pb(rems(0.3))
2361                    .whitespace_normal()
2362                    .text_size(text_size)
2363                    .font_weight(font_weight)
2364                    .child(children.render(node_cx, window, cx))
2365                    .into_any_element()
2366            }
2367            BlockNode::Blockquote { children, .. } => div()
2368                .w_full()
2369                .pb(mb)
2370                .child(
2371                    div()
2372                        .id(("blockquote", ix))
2373                        .w_full()
2374                        .text_color(node_cx.style.muted_foreground())
2375                        .border_l_3()
2376                        .border_color(node_cx.style.border())
2377                        .px_4()
2378                        .children({
2379                            let children_len = children.len();
2380                            children.into_iter().enumerate().map(move |(index, c)| {
2381                                let is_last = index == children_len - 1;
2382                                c.render_block(options.is_last(is_last), node_cx, window, cx)
2383                            })
2384                        }),
2385                )
2386                .into_any_element(),
2387            BlockNode::List {
2388                children, ordered, ..
2389            } => v_flex()
2390                .id((if *ordered { "ol" } else { "ul" }, ix))
2391                .w_full()
2392                .min_w_0()
2393                .pb(mb)
2394                .children({
2395                    let mut items = Vec::with_capacity(children.len());
2396                    let mut item_index = 0;
2397                    for (ix, item) in children.into_iter().enumerate() {
2398                        let is_item = item.is_list_item();
2399
2400                        items.push(Self::render_list_item(
2401                            item,
2402                            item_index,
2403                            NodeRenderOptions {
2404                                ix,
2405                                ordered: *ordered,
2406                                ..options
2407                            },
2408                            node_cx,
2409                            window,
2410                            cx,
2411                        ));
2412
2413                        if is_item {
2414                            item_index += 1;
2415                        }
2416                    }
2417                    items
2418                })
2419                .into_any_element(),
2420            BlockNode::CodeBlock(code_block) => code_block.render(&options, node_cx, window, cx),
2421            BlockNode::Custom(node) => {
2422                let inner = match node_cx.markdown_extensions.render_block(node, window, cx) {
2423                    Some(rendered) => rendered,
2424                    None => div().child(node.as_text().to_string()).into_any_element(),
2425                };
2426
2427                div().pb(mb).child(inner).into_any_element()
2428            }
2429            BlockNode::Table { .. } => {
2430                Self::render_table(self, &options, node_cx, window, cx).into_any_element()
2431            }
2432            BlockNode::HorizontalRule { .. } => div()
2433                .pb(mb)
2434                .child(
2435                    div()
2436                        .id("horizontal-rule")
2437                        .bg(node_cx.style.border())
2438                        .h(px(2.)),
2439                )
2440                .into_any_element(),
2441            BlockNode::Break { .. } => div().id("break").into_any_element(),
2442            BlockNode::Unknown { .. } | BlockNode::Definition { .. } => div().into_any_element(),
2443            _ => {
2444                if cfg!(debug_assertions) {
2445                    tracing::warn!("unknown implementation: {:?}", self);
2446                }
2447
2448                div().into_any_element()
2449            }
2450        }
2451    }
2452}
2453
2454#[cfg(test)]
2455mod tests {
2456    use super::*;
2457
2458    #[test]
2459    fn code_block_highlights_are_cached_by_highlighter_identity() {
2460        use std::sync::atomic::{AtomicUsize, Ordering};
2461
2462        let calls = Arc::new(AtomicUsize::new(0));
2463        let calls_for_highlighter = calls.clone();
2464        let highlighter: Arc<CodeBlockHighlighterFn> = Arc::new(move |_| {
2465            calls_for_highlighter.fetch_add(1, Ordering::Relaxed);
2466            Vec::new()
2467        });
2468        let block = CodeBlock::new("fn main() {}".into(), Some("rust".into()), None::<Span>);
2469
2470        block.highlighted_styles(&highlighter);
2471        block.highlighted_styles(&highlighter);
2472        assert_eq!(calls.load(Ordering::Relaxed), 1);
2473
2474        let replacement: Arc<CodeBlockHighlighterFn> = Arc::new(|_| Vec::new());
2475        block.highlighted_styles(&replacement);
2476        assert!(Arc::ptr_eq(
2477            &block
2478                .highlight_cache
2479                .lock()
2480                .unwrap()
2481                .as_ref()
2482                .unwrap()
2483                .highlighter,
2484            &replacement
2485        ));
2486    }
2487
2488    #[test]
2489    fn a_new_highlighter_replaces_styles_instead_of_reusing_the_cache() {
2490        // Swapping the highlighter is how a theme change reaches a code block:
2491        // the parsed document is untouched, so the styles must come from the
2492        // new highlighter rather than from the styles cached for the old one.
2493        let light: Arc<CodeBlockHighlighterFn> = Arc::new(|_| {
2494            vec![(
2495                0..2,
2496                HighlightStyle {
2497                    color: Some(gpui::rgb(0x0000ff).into()),
2498                    ..Default::default()
2499                },
2500            )]
2501        });
2502        let dark: Arc<CodeBlockHighlighterFn> = Arc::new(|_| {
2503            vec![(
2504                0..2,
2505                HighlightStyle {
2506                    color: Some(gpui::rgb(0xffff00).into()),
2507                    ..Default::default()
2508                },
2509            )]
2510        });
2511        let block = CodeBlock::from_code("42", Some("json"));
2512
2513        let light_styles = block.highlighted_styles(&light);
2514        let dark_styles = block.highlighted_styles(&dark);
2515
2516        assert_eq!(light_styles[0].1.color, Some(gpui::rgb(0x0000ff).into()));
2517        assert_eq!(dark_styles[0].1.color, Some(gpui::rgb(0xffff00).into()));
2518        assert_eq!(block.code(), "42", "the document must survive the swap");
2519    }
2520
2521    #[test]
2522    fn reconstruct_markdown_wraps_marked_runs() {
2523        // "bold" fully covered by a bold mark.
2524        let marks = vec![(0..4, TextMark::default().bold())];
2525        assert_eq!(reconstruct_markdown("bold", &marks, 0..4), "**bold**");
2526        // Partial selection inside the bold run still wraps the slice.
2527        assert_eq!(reconstruct_markdown("bold", &marks, 1..3), "**ol**");
2528    }
2529
2530    #[test]
2531    fn reconstruct_markdown_emits_unmarked_text_verbatim() {
2532        // "a b c": plain, code, plain across three runs concatenated.
2533        let text = "a b c";
2534        let marks = vec![(2..3, TextMark::default().code())];
2535        assert_eq!(reconstruct_markdown(text, &marks, 0..5), "a `b` c");
2536        // Selecting only the plain tail.
2537        assert_eq!(reconstruct_markdown(text, &marks, 3..5), " c");
2538    }
2539
2540    #[test]
2541    fn reconstruct_markdown_handles_code_italic_strike_link() {
2542        assert_eq!(
2543            reconstruct_markdown("x", &[(0..1, TextMark::default().code())], 0..1),
2544            "`x`"
2545        );
2546        assert_eq!(
2547            reconstruct_markdown("x", &[(0..1, TextMark::default().italic())], 0..1),
2548            "*x*"
2549        );
2550        assert_eq!(
2551            reconstruct_markdown("x", &[(0..1, TextMark::default().strikethrough())], 0..1),
2552            "~~x~~"
2553        );
2554        let link = TextMark::default().link(LinkMark {
2555            url: "https://example.com".into(),
2556            ..Default::default()
2557        });
2558        assert_eq!(
2559            reconstruct_markdown("x", &[(0..1, link)], 0..1),
2560            "[x](https://example.com)"
2561        );
2562    }
2563
2564    #[test]
2565    fn reconstruct_markdown_nested_bold_italic() {
2566        // A single run marked both bold and italic (as produced by `**_x_**`).
2567        let mark = TextMark::default().bold().italic();
2568        // Inner (italic) is applied first, then bold: `***x***`.
2569        assert_eq!(reconstruct_markdown("x", &[(0..1, mark)], 0..1), "***x***");
2570    }
2571
2572    /// Build a paragraph whose combined `state.text` is the concatenation of
2573    /// its children (mirroring `Paragraph::render`), then set the paragraph
2574    /// selection so `selected_source` can be exercised without a real paint.
2575    fn paragraph_with_children(children: Vec<InlineNode>) -> Paragraph {
2576        let combined: String = children.iter().map(|c| c.text.to_string()).collect();
2577        let paragraph = Paragraph {
2578            span: None,
2579            children,
2580            link_refs: HashMap::new(),
2581            state: Arc::new(Mutex::new(InlineState::default())),
2582        };
2583        if let Ok(mut state) = paragraph.state.lock() {
2584            state.set_text(combined.into());
2585        }
2586        paragraph
2587    }
2588
2589    fn set_paragraph_selection(paragraph: &Paragraph, range: Range<usize>) {
2590        if let Ok(mut state) = paragraph.state.lock() {
2591            state.selection = Some(range.into());
2592        }
2593    }
2594
2595    #[test]
2596    fn paragraph_selected_source_maps_partial_selection_across_runs() {
2597        // "This has **bold** text." rendered as ["This has ", "bold", " text."].
2598        let children = vec![
2599            InlineNode::new("This has ").marks(vec![(0..9, TextMark::default())]),
2600            InlineNode::new("bold").marks(vec![(0..4, TextMark::default().bold())]),
2601            InlineNode::new(" text.").marks(vec![(0..6, TextMark::default())]),
2602        ];
2603        let paragraph = paragraph_with_children(children);
2604
2605        // Select the whole paragraph: "This has bold text." -> source with **.
2606        set_paragraph_selection(&paragraph, 0..(9 + 4 + 6));
2607        assert_eq!(paragraph.selected_source(), "This has **bold** text.");
2608
2609        // Select only across the boundary "has **bold** te".
2610        // Rendered offsets: "has " starts at 5, "bold" at 9..13, " te" 13..16.
2611        set_paragraph_selection(&paragraph, 5..16);
2612        assert_eq!(paragraph.selected_source(), "has **bold** te");
2613
2614        // Select entirely inside the bold run -> still wrapped.
2615        set_paragraph_selection(&paragraph, 10..12);
2616        assert_eq!(paragraph.selected_source(), "**ol**");
2617    }
2618
2619    #[test]
2620    fn paragraph_selected_source_matches_text_when_no_marks() {
2621        let children =
2622            vec![InlineNode::new("plain words").marks(vec![(0..11, TextMark::default())])];
2623        let paragraph = paragraph_with_children(children);
2624        set_paragraph_selection(&paragraph, 0..11);
2625        assert_eq!(paragraph.selected_source(), "plain words");
2626        assert_eq!(paragraph.selected_text(), "plain words");
2627    }
2628
2629    fn selected_paragraph(text: &str) -> Paragraph {
2630        let len = text.len();
2631        let paragraph = paragraph_with_children(vec![
2632            InlineNode::new(text).marks(vec![(0..len, TextMark::default())]),
2633        ]);
2634        set_paragraph_selection(&paragraph, 0..len);
2635        paragraph
2636    }
2637
2638    #[test]
2639    fn heading_selected_source_prefixes_hashes() {
2640        let heading = BlockNode::Heading {
2641            level: 2,
2642            children: selected_paragraph("Title"),
2643            span: None,
2644        };
2645        assert_eq!(heading.selected_text(SelectionFormat::Source), "## Title\n");
2646        // Rendered text keeps no marker.
2647        assert_eq!(heading.selected_text(SelectionFormat::Plain), "Title\n");
2648    }
2649
2650    #[test]
2651    fn unordered_list_selected_source_prefixes_dash() {
2652        let list = BlockNode::List {
2653            ordered: false,
2654            span: None,
2655            children: vec![
2656                BlockNode::ListItem {
2657                    children: vec![BlockNode::Paragraph(selected_paragraph("one"))],
2658                    spread: false,
2659                    checked: None,
2660                    span: None,
2661                },
2662                BlockNode::ListItem {
2663                    children: vec![BlockNode::Paragraph(selected_paragraph("two"))],
2664                    spread: false,
2665                    checked: None,
2666                    span: None,
2667                },
2668            ],
2669        };
2670        assert_eq!(
2671            list.selected_text(SelectionFormat::Source),
2672            "- one\n- two\n"
2673        );
2674    }
2675
2676    #[test]
2677    fn ordered_list_selected_source_prefixes_numbers() {
2678        let list = BlockNode::List {
2679            ordered: true,
2680            span: None,
2681            children: vec![
2682                BlockNode::ListItem {
2683                    children: vec![BlockNode::Paragraph(selected_paragraph("first"))],
2684                    spread: false,
2685                    checked: None,
2686                    span: None,
2687                },
2688                BlockNode::ListItem {
2689                    children: vec![BlockNode::Paragraph(selected_paragraph("second"))],
2690                    spread: false,
2691                    checked: None,
2692                    span: None,
2693                },
2694            ],
2695        };
2696        assert_eq!(
2697            list.selected_text(SelectionFormat::Source),
2698            "1. first\n2. second\n"
2699        );
2700    }
2701
2702    #[test]
2703    fn nested_list_selected_source_indents_sublists() {
2704        // - one
2705        //   - nested
2706        // - two
2707        let nested = BlockNode::List {
2708            ordered: false,
2709            span: None,
2710            children: vec![BlockNode::ListItem {
2711                children: vec![BlockNode::Paragraph(selected_paragraph("nested"))],
2712                spread: false,
2713                checked: None,
2714                span: None,
2715            }],
2716        };
2717        let list = BlockNode::List {
2718            ordered: false,
2719            span: None,
2720            children: vec![
2721                BlockNode::ListItem {
2722                    children: vec![BlockNode::Paragraph(selected_paragraph("one")), nested],
2723                    spread: false,
2724                    checked: None,
2725                    span: None,
2726                },
2727                BlockNode::ListItem {
2728                    children: vec![BlockNode::Paragraph(selected_paragraph("two"))],
2729                    spread: false,
2730                    checked: None,
2731                    span: None,
2732                },
2733            ],
2734        };
2735        assert_eq!(
2736            list.selected_text(SelectionFormat::Source),
2737            "- one\n  - nested\n- two\n"
2738        );
2739    }
2740
2741    #[test]
2742    fn task_list_selected_source_restores_checkboxes() {
2743        let list = BlockNode::List {
2744            ordered: false,
2745            span: None,
2746            children: vec![
2747                BlockNode::ListItem {
2748                    children: vec![BlockNode::Paragraph(selected_paragraph("done"))],
2749                    spread: false,
2750                    checked: Some(true),
2751                    span: None,
2752                },
2753                BlockNode::ListItem {
2754                    children: vec![BlockNode::Paragraph(selected_paragraph("todo"))],
2755                    spread: false,
2756                    checked: Some(false),
2757                    span: None,
2758                },
2759            ],
2760        };
2761        assert_eq!(
2762            list.selected_text(SelectionFormat::Source),
2763            "- [x] done\n- [ ] todo\n"
2764        );
2765    }
2766
2767    #[test]
2768    fn blockquote_selected_source_prefixes_gt() {
2769        let quote = BlockNode::Blockquote {
2770            span: None,
2771            children: vec![BlockNode::Paragraph(selected_paragraph("quoted text"))],
2772        };
2773        assert_eq!(
2774            quote.selected_text(SelectionFormat::Source),
2775            "> quoted text\n"
2776        );
2777    }
2778
2779    #[test]
2780    fn table_selected_source_pipes_cells_with_alignment_row() {
2781        let cell = |text: &str| TableCell {
2782            children: selected_paragraph(text),
2783            width: None,
2784        };
2785        let table = Table {
2786            children: vec![
2787                TableRow {
2788                    children: vec![cell("Name"), cell("Age")],
2789                },
2790                TableRow {
2791                    children: vec![cell("Alice"), cell("30")],
2792                },
2793            ],
2794            column_aligns: vec![ColumnumnAlign::Left, ColumnumnAlign::Right],
2795            span: None,
2796        };
2797        let block = BlockNode::Table(table);
2798        assert_eq!(
2799            block.selected_text(SelectionFormat::Source),
2800            "| Name | Age |\n| :-- | --: |\n| Alice | 30 |\n"
2801        );
2802    }
2803
2804    /// A cell holding plain text, as `Table::to_markdown` and
2805    /// `Table::table_data` see it (neither needs a selection).
2806    fn plain_cell(text: &str) -> TableCell {
2807        TableCell {
2808            children: Paragraph::new(text.to_string()),
2809            width: None,
2810        }
2811    }
2812
2813    fn table_of(rows: Vec<Vec<TableCell>>, column_aligns: Vec<ColumnumnAlign>) -> Table {
2814        Table {
2815            children: rows
2816                .into_iter()
2817                .map(|children| TableRow { children })
2818                .collect(),
2819            column_aligns,
2820            span: None,
2821        }
2822    }
2823
2824    #[test]
2825    fn table_to_markdown_pipes_cells_with_alignment_row() {
2826        let table = table_of(
2827            vec![
2828                vec![plain_cell("Name"), plain_cell("Age"), plain_cell("Score")],
2829                vec![plain_cell("Alice"), plain_cell("30"), plain_cell("9.5")],
2830            ],
2831            vec![
2832                ColumnumnAlign::Left,
2833                ColumnumnAlign::Center,
2834                ColumnumnAlign::Right,
2835            ],
2836        );
2837
2838        assert_eq!(
2839            table.to_markdown(),
2840            "| Name | Age | Score |\n| :-- | :-: | --: |\n| Alice | 30 | 9.5 |"
2841        );
2842        // The block arm delegates to it.
2843        assert_eq!(
2844            BlockNode::Table(table.clone()).to_markdown(),
2845            table.to_markdown()
2846        );
2847    }
2848
2849    #[test]
2850    fn table_to_markdown_keeps_outer_pipes_for_a_single_column() {
2851        let table = table_of(
2852            vec![vec![plain_cell("Symbol")], vec![plain_cell("TSLA.US")]],
2853            vec![ColumnumnAlign::Left],
2854        );
2855
2856        assert_eq!(table.to_markdown(), "| Symbol |\n| :-- |\n| TSLA.US |");
2857    }
2858
2859    #[test]
2860    fn table_to_markdown_escapes_pipes_and_keeps_inline_marks() {
2861        let bold = TableCell {
2862            children: paragraph_with_children(vec![
2863                InlineNode::new("bold").marks(vec![(0..4, TextMark::default().bold())]),
2864            ]),
2865            width: None,
2866        };
2867        let table = table_of(
2868            vec![
2869                vec![plain_cell("a | b"), plain_cell("plain")],
2870                vec![plain_cell("c"), bold],
2871            ],
2872            vec![ColumnumnAlign::Left, ColumnumnAlign::Left],
2873        );
2874
2875        assert_eq!(
2876            table.to_markdown(),
2877            "| a \\| b | plain |\n| :-- | :-- |\n| c | **bold** |"
2878        );
2879    }
2880
2881    #[test]
2882    fn table_data_snapshots_plain_cells_and_markdown() {
2883        let mut table = table_of(
2884            vec![
2885                vec![plain_cell("  Name  "), plain_cell("Age")],
2886                vec![plain_cell("Alice"), plain_cell("30")],
2887            ],
2888            vec![ColumnumnAlign::Left, ColumnumnAlign::Right],
2889        );
2890        table.span = Some(Span { start: 4, end: 42 });
2891
2892        let data = table.table_data();
2893        assert_eq!(data.headers, vec!["Name", "Age"]);
2894        assert_eq!(data.rows, vec![vec!["Alice", "30"]]);
2895        assert_eq!(data.markdown, table.to_markdown());
2896        assert_eq!(data.span, Some(4..42));
2897    }
2898
2899    #[test]
2900    fn table_data_handles_tables_without_rows() {
2901        // Header only: still a valid table, with no data rows.
2902        let header_only = table_of(
2903            vec![vec![plain_cell("Name"), plain_cell("Age")]],
2904            vec![ColumnumnAlign::Left, ColumnumnAlign::Left],
2905        );
2906        let data = header_only.table_data();
2907        assert_eq!(data.headers, vec!["Name", "Age"]);
2908        assert!(data.rows.is_empty());
2909        assert_eq!(data.markdown, "| Name | Age |\n| :-- | :-- |");
2910
2911        // No rows at all (a table still streaming in): an empty snapshot.
2912        assert_eq!(Table::default().table_data(), TableData::default());
2913    }
2914
2915    fn image_paragraph(alt: &str, url: &str) -> Paragraph {
2916        let image = ImageNode {
2917            url: url.into(),
2918            alt: Some(alt.into()),
2919            ..Default::default()
2920        };
2921        Paragraph {
2922            span: None,
2923            children: vec![InlineNode::image(image)],
2924            link_refs: HashMap::new(),
2925            state: Arc::new(Mutex::new(InlineState::default())),
2926        }
2927    }
2928
2929    /// Every mark round-trips, including the two Markdown has no plain syntax
2930    /// for.
2931    #[test]
2932    fn marks_round_trip_through_reconstruction() {
2933        let wrap = |mark: TextMark| reconstruct_markdown("x", &[(0..1, mark)], 0..1);
2934
2935        assert_eq!(wrap(TextMark::default().bold()), "**x**");
2936        assert_eq!(wrap(TextMark::default().italic()), "*x*");
2937        assert_eq!(wrap(TextMark::default().code()), "`x`");
2938        assert_eq!(wrap(TextMark::default().strikethrough()), "~~x~~");
2939        assert_eq!(
2940            wrap(TextMark::default().highlight(gpui::rgb(0xfef08a).into())),
2941            "==x=="
2942        );
2943        // No Markdown syntax for underline, so it keeps the tag it came from.
2944        assert_eq!(wrap(TextMark::default().underline()), "<u>x</u>");
2945
2946        // A link keeps its title, which Markdown carries after the URL.
2947        assert_eq!(
2948            wrap(TextMark::default().link(LinkMark {
2949                url: "https://example.com".into(),
2950                title: Some("Tip".into()),
2951                ..Default::default()
2952            })),
2953            "[x](https://example.com \"Tip\")"
2954        );
2955    }
2956
2957    /// A block the selection covers whole comes straight from the source, so it
2958    /// keeps what the author wrote instead of a normalized reconstruction.
2959    #[test]
2960    fn document_selected_source_slices_covered_blocks_from_the_source() {
2961        use crate::text::document::ParsedDocument;
2962
2963        // `_italic_`, the `3.` start and the column padding all survive only
2964        // because the block is copied, not rebuilt.
2965        let source = "start\n\n3. _one_\n4. two\n\n---\n\nend";
2966        let list = "3. _one_\n4. two";
2967        let list_start = source.find(list).unwrap();
2968        let rule_start = source.find("---").unwrap();
2969
2970        let document = ParsedDocument {
2971            source: source.into(),
2972            blocks: vec![
2973                BlockNode::Paragraph(selected_paragraph("start")),
2974                BlockNode::List {
2975                    ordered: true,
2976                    children: vec![],
2977                    span: Some(Span {
2978                        start: list_start,
2979                        end: list_start + list.len(),
2980                    }),
2981                },
2982                BlockNode::HorizontalRule {
2983                    span: Some(Span {
2984                        start: rule_start,
2985                        end: rule_start + 3,
2986                    }),
2987                },
2988                BlockNode::Paragraph(selected_paragraph("end")),
2989            ]
2990            .into(),
2991        };
2992
2993        assert_eq!(
2994            document.selected_text(SelectionFormat::Source, None),
2995            "start\n\n3. _one_\n4. two\n\n---\n\nend"
2996        );
2997    }
2998
2999    #[test]
3000    fn document_selected_source_includes_enclosed_image() {
3001        use crate::text::document::ParsedDocument;
3002
3003        // A standalone image between two selected paragraphs is covered by the
3004        // selection, so it is copied whole even though it holds no selection of
3005        // its own — straight out of the source the parser located it in.
3006        let source = "before\n\n![alt](https://example.com/i.png)\n\nafter";
3007        let image_markdown = "![alt](https://example.com/i.png)";
3008        let start = source.find(image_markdown).unwrap();
3009        let mut image = image_paragraph("alt", "https://example.com/i.png");
3010        image.span = Some(Span {
3011            start,
3012            end: start + image_markdown.len(),
3013        });
3014
3015        let document = ParsedDocument {
3016            source: source.into(),
3017            blocks: vec![
3018                BlockNode::Paragraph(selected_paragraph("before")),
3019                BlockNode::Paragraph(image),
3020                BlockNode::Paragraph(selected_paragraph("after")),
3021            ]
3022            .into(),
3023        };
3024        assert_eq!(
3025            document.selected_text(SelectionFormat::Source, None),
3026            "before\n\n![alt](https://example.com/i.png)\n\nafter"
3027        );
3028    }
3029
3030    #[test]
3031    fn document_selected_source_drops_unenclosed_image() {
3032        use crate::text::document::ParsedDocument;
3033
3034        // An image after the only selected block, with nothing selected after
3035        // it, is not enclosed and is dropped.
3036        let document = ParsedDocument {
3037            source: String::new().into(),
3038            blocks: vec![
3039                BlockNode::Paragraph(selected_paragraph("before")),
3040                BlockNode::Paragraph(image_paragraph("alt", "u")),
3041            ]
3042            .into(),
3043        };
3044        assert_eq!(
3045            document.selected_text(SelectionFormat::Source, None),
3046            "before"
3047        );
3048    }
3049
3050    fn selected_code_block(code: &str, lang: Option<&str>) -> BlockNode {
3051        let block = CodeBlock::new(
3052            code.to_string().into(),
3053            lang.map(|l| l.to_string().into()),
3054            None::<Span>,
3055        );
3056        if let Ok(mut state) = block.state.lock() {
3057            let len = state.text.len();
3058            state.selection = Some((0..len).into());
3059        }
3060        BlockNode::CodeBlock(block)
3061    }
3062
3063    #[test]
3064    fn code_block_selected_source_wraps_in_fence_with_lang() {
3065        let block = selected_code_block("let x = 1;\n", Some("rust"));
3066        let code = block.selected_text(SelectionFormat::Plain);
3067        let code_trimmed = code.trim_end_matches('\n');
3068        // The source wraps the (trailing-newline-trimmed) selected code in a
3069        // fenced block carrying the language; the block arm adds one trailing
3070        // newline.
3071        assert_eq!(
3072            block.selected_text(SelectionFormat::Source),
3073            format!("```rust\n{}\n```\n", code_trimmed)
3074        );
3075        assert!(
3076            block
3077                .selected_text(SelectionFormat::Source)
3078                .starts_with("```rust\n")
3079        );
3080        assert!(
3081            block
3082                .selected_text(SelectionFormat::Source)
3083                .trim_end()
3084                .ends_with("\n```")
3085        );
3086    }
3087
3088    #[test]
3089    fn code_block_selected_source_without_lang() {
3090        let block = selected_code_block("plain\n", None);
3091        let code_trimmed = block.selected_text(SelectionFormat::Plain);
3092        let code_trimmed = code_trimmed.trim_end_matches('\n');
3093        assert_eq!(
3094            block.selected_text(SelectionFormat::Source),
3095            format!("```\n{}\n```\n", code_trimmed)
3096        );
3097    }
3098
3099    #[test]
3100    fn document_selected_source_joins_blocks_with_blank_line() {
3101        use crate::text::document::ParsedDocument;
3102
3103        // A heading, a paragraph, and a two-item ordered list, each fully
3104        // selected. Top-level blocks must be separated by a blank line so the
3105        // copied Markdown re-renders with the same structure.
3106        let document = ParsedDocument {
3107            source: String::new().into(),
3108            blocks: vec![
3109                BlockNode::Heading {
3110                    level: 1,
3111                    children: selected_paragraph("Title"),
3112                    span: None,
3113                },
3114                BlockNode::Paragraph(selected_paragraph("A paragraph.")),
3115                selected_code_block("let x = 1;\n", Some("rust")),
3116                BlockNode::List {
3117                    ordered: true,
3118                    span: None,
3119                    children: vec![
3120                        BlockNode::ListItem {
3121                            children: vec![BlockNode::Paragraph(selected_paragraph("one"))],
3122                            spread: false,
3123                            checked: None,
3124                            span: None,
3125                        },
3126                        BlockNode::ListItem {
3127                            children: vec![BlockNode::Paragraph(selected_paragraph("two"))],
3128                            spread: false,
3129                            checked: None,
3130                            span: None,
3131                        },
3132                    ],
3133                },
3134            ]
3135            .into(),
3136        };
3137
3138        assert_eq!(
3139            document.selected_text(SelectionFormat::Source, None),
3140            "# Title\n\nA paragraph.\n\n```rust\nlet x = 1;\n```\n\n1. one\n2. two"
3141        );
3142    }
3143
3144    #[test]
3145    fn code_block_equality_includes_code_content() {
3146        let first = CodeBlock::new("let value = 1;".into(), Some("rust".into()), None::<Span>);
3147        let second = CodeBlock::new("let value = 2;".into(), Some("rust".into()), None::<Span>);
3148
3149        assert_ne!(first, second);
3150    }
3151}