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