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