Skip to main content

gpui_base/text/
node.rs

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