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