Skip to main content

gpui_base/text/
text_view.rs

1use std::{ops::Range, rc::Rc, sync::Arc};
2
3use gpui::prelude::FluentBuilder as _;
4use gpui::{
5    AnyElement, App, Bounds, ClickEvent, ContentMask, Element, ElementId, Entity, Global,
6    GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId, InteractiveElement, IntoElement,
7    LayoutId, MouseButton, ParentElement, Pixels, Refineable as _, SharedString, StyleRefinement,
8    Styled, Window, div, point, px,
9};
10
11use crate::StyledExt;
12use crate::text::TextViewFormat;
13use crate::text::markdown_ext::{MarkdownExtensions, MarkdownNode, MarkdownPlugin};
14use crate::text::node::{CodeBlock, TableData};
15use crate::text::range_highlight::{PendingReveal, RevealProgress};
16use crate::text::state::{LineSpan, SelectionFormat, TextViewState};
17use crate::text::stream_fade::TextViewMotion;
18use crate::{GlobalState, TextSelection, text::TextViewStyle};
19
20/// Type for code block actions generator function.
21pub(crate) type CodeBlockActionsFn =
22    dyn Fn(&CodeBlock, &mut Window, &mut App) -> AnyElement + Send + Sync;
23
24pub(crate) type CodeBlockHighlighterFn =
25    dyn Fn(&CodeBlock) -> Vec<(Range<usize>, gpui::HighlightStyle)> + Send + Sync;
26
27/// Application-wide defaults for TextViews that do not provide explicit
28/// presentation or syntax-highlighting overrides.
29#[derive(Clone, Default)]
30pub struct TextViewDefaults {
31    style: Option<TextViewStyle>,
32    code_block_highlighter: Option<Arc<CodeBlockHighlighterFn>>,
33    inherit_text_color: bool,
34}
35
36impl Global for TextViewDefaults {}
37
38impl TextViewDefaults {
39    /// Creates defaults that leave every text view as Base renders it.
40    pub fn new() -> Self {
41        Self::default()
42    }
43
44    /// Sets the style every text view starts from.
45    pub fn with_style(mut self, style: TextViewStyle) -> Self {
46        self.style = Some(style);
47        self
48    }
49
50    /// The style every text view starts from, when set.
51    pub(crate) fn style(&self) -> Option<&TextViewStyle> {
52        self.style.as_ref()
53    }
54
55    /// The syntax highlighter for fenced code blocks, when set.
56    pub(crate) fn code_block_highlighter(&self) -> Option<Arc<CodeBlockHighlighterFn>> {
57        self.code_block_highlighter.clone()
58    }
59
60    /// Sets the syntax highlighter used for fenced code blocks.
61    pub fn with_code_block_highlighter<F>(mut self, highlighter: F) -> Self
62    where
63        F: Fn(&CodeBlock) -> Vec<(Range<usize>, gpui::HighlightStyle)> + Send + Sync + 'static,
64    {
65        self.code_block_highlighter = Some(Arc::new(highlighter));
66        self
67    }
68
69    /// Sets whether text views without an explicit [`TextView::style`] follow
70    /// the text color their container sets, default false.
71    ///
72    /// The body text takes that color. On a surface inverted from the page,
73    /// such as a `primary` fill, links, muted text, code, borders and
74    /// selection are derived from it too, and the installed syntax highlighter
75    /// is left out because its colors are made for the page. Enable this only
76    /// when every window sets a text color at its root, as Component's does.
77    pub fn with_inherit_text_color(mut self, inherit: bool) -> Self {
78        self.inherit_text_color = inherit;
79        self
80    }
81
82    /// Installs these defaults for the whole application.
83    pub fn install(self, cx: &mut App) {
84        cx.set_global(self);
85    }
86
87    /// Returns the installed defaults, or the Base ones when none were.
88    pub fn global(cx: &App) -> Self {
89        cx.try_global::<Self>().cloned().unwrap_or_default()
90    }
91
92    /// Whether a syntax highlighter was installed.
93    pub fn has_code_block_highlighter(&self) -> bool {
94        self.code_block_highlighter.is_some()
95    }
96
97    /// Whether text views follow the text color their container sets.
98    pub fn inherit_text_color(&self) -> bool {
99        self.inherit_text_color
100    }
101}
102
103/// Type for the table actions generator function.
104pub(crate) type TableActionsFn =
105    dyn Fn(&TableData, &mut Window, &mut App) -> AnyElement + Send + Sync;
106
107pub(crate) type ImageSourceFn = dyn Fn(&gpui::SharedUri) -> gpui::ImageSource + Send + Sync;
108
109pub(crate) type LinkClickHandlerFn =
110    dyn Fn(&SharedString, &ClickEvent, &mut Window, &mut App) + Send + Sync;
111
112/// Kept by the element only, so unlike the handlers the state carries, it
113/// may hold a `ScrollHandle`.
114pub(crate) type RevealHandlerFn = dyn Fn(Bounds<Pixels>, &mut Window, &mut App);
115
116pub(crate) fn handle_link_click(
117    handler: &Option<Arc<LinkClickHandlerFn>>,
118    url: SharedString,
119    event: ClickEvent,
120    window: &mut Window,
121    cx: &mut App,
122) {
123    if let Some(handler) = handler {
124        handler(&url, &event, window, cx);
125    } else if match &event {
126        ClickEvent::Mouse(click) => {
127            matches!(click.up.button, MouseButton::Left | MouseButton::Middle)
128        }
129        ClickEvent::Keyboard(_) => true,
130        ClickEvent::Touch(click) => !click.long_press,
131    } {
132        cx.open_url(&url);
133    }
134}
135
136/// A text view that can render Markdown or HTML.
137///
138/// ## Goals
139///
140/// - Provide a rich text rendering component for such as Markdown or HTML,
141/// used to display rich text in GPUI application (e.g., Help messages, Release notes)
142/// - Support Markdown GFM and HTML (Simple HTML like Safari Reader Mode) for showing most common used markups.
143/// - Support Heading, Paragraph, Bold, Italic, StrikeThrough, Code, Link, Image, Blockquote, List, Table, HorizontalRule, CodeBlock ...
144///
145/// ## Not Goals
146///
147/// - Customization of the complex style (some simple styles will be supported)
148/// - As a Markdown editor or viewer (If you want to like this, you must fork your version).
149/// - As a HTML viewer, we not support CSS, we only support basic HTML tags for used to as a content reader.
150///
151/// See also [`MarkdownElement`], [`HtmlElement`]
152#[derive(Clone)]
153pub struct TextView {
154    id: ElementId,
155    format: Option<TextViewFormat>,
156    text: Option<SharedString>,
157    pub(crate) state: Option<Entity<TextViewState>>,
158    text_view_style: Option<TextViewStyle>,
159    style: StyleRefinement,
160    selectable: bool,
161    selection_format: SelectionFormat,
162    scrollable: bool,
163    max_lines: Option<usize>,
164    code_block_actions: Option<Arc<CodeBlockActionsFn>>,
165    code_block_highlighter: Option<Arc<CodeBlockHighlighterFn>>,
166    table_actions: Option<Arc<TableActionsFn>>,
167    link_click_handler: Option<Arc<LinkClickHandlerFn>>,
168    image_source: Option<Arc<ImageSourceFn>>,
169    reveal_handler: Option<Rc<RevealHandlerFn>>,
170    markdown_extensions: Arc<MarkdownExtensions>,
171    motion: Option<TextViewMotion>,
172}
173
174/// A plugin that can configure a [`TextView`].
175pub trait TextViewPlugin {
176    fn setup(self, text_view: TextView) -> TextView;
177}
178
179impl<P> TextViewPlugin for P
180where
181    P: MarkdownPlugin,
182{
183    fn setup(self, mut text_view: TextView) -> TextView {
184        let extensions = Arc::make_mut(&mut text_view.markdown_extensions);
185        let current = std::mem::take(extensions);
186        *extensions = current.plugin(self);
187        text_view
188    }
189}
190
191impl Styled for TextView {
192    fn style(&mut self) -> &mut StyleRefinement {
193        &mut self.style
194    }
195}
196
197impl TextView {
198    /// Create new TextView with managed state.
199    pub fn new(state: &Entity<TextViewState>) -> Self {
200        Self {
201            id: ElementId::Name(state.entity_id().to_string().into()),
202            state: Some(state.clone()),
203            format: None,
204            text: None,
205            text_view_style: None,
206            style: StyleRefinement::default(),
207            selectable: true,
208            selection_format: SelectionFormat::default(),
209            scrollable: false,
210            max_lines: None,
211            code_block_actions: None,
212            code_block_highlighter: None,
213            table_actions: None,
214            link_click_handler: None,
215            image_source: None,
216            reveal_handler: None,
217            markdown_extensions: Arc::default(),
218            motion: None,
219        }
220    }
221
222    /// Create a new markdown text view.
223    pub fn markdown(id: impl Into<ElementId>, markdown: impl Into<SharedString>) -> Self {
224        Self {
225            id: id.into(),
226            format: Some(TextViewFormat::Markdown),
227            text: Some(markdown.into()),
228            text_view_style: None,
229            style: StyleRefinement::default(),
230            state: None,
231            selectable: true,
232            selection_format: SelectionFormat::default(),
233            scrollable: false,
234            max_lines: None,
235            code_block_actions: None,
236            code_block_highlighter: None,
237            table_actions: None,
238            link_click_handler: None,
239            image_source: None,
240            reveal_handler: None,
241            markdown_extensions: Arc::default(),
242            motion: None,
243        }
244    }
245
246    /// Create a new html text view.
247    pub fn html(id: impl Into<ElementId>, html: impl Into<SharedString>) -> Self {
248        Self {
249            id: id.into(),
250            format: Some(TextViewFormat::Html),
251            text: Some(html.into()),
252            text_view_style: None,
253            style: StyleRefinement::default(),
254            state: None,
255            selectable: true,
256            selection_format: SelectionFormat::default(),
257            scrollable: false,
258            max_lines: None,
259            code_block_actions: None,
260            code_block_highlighter: None,
261            table_actions: None,
262            link_click_handler: None,
263            image_source: None,
264            reveal_handler: None,
265            markdown_extensions: Arc::default(),
266            motion: None,
267        }
268    }
269
270    /// Overrides the source of every document image, including embedded data URLs.
271    ///
272    /// Used for both rendering and intrinsic-size measurement. The returned source
273    /// is authoritative: pending or failed loads never fall back to the document URL.
274    /// Without this override, images use Base's default URI and data URL handling.
275    pub fn image_source<F>(mut self, resolver: F) -> Self
276    where
277        F: Fn(&gpui::SharedUri) -> gpui::ImageSource + Send + Sync + 'static,
278    {
279        self.image_source = Some(Arc::new(resolver));
280        self
281    }
282
283    /// Set [`TextViewStyle`].
284    pub fn style(mut self, style: TextViewStyle) -> Self {
285        self.text_view_style = Some(style);
286        self
287    }
288
289    /// Set whether the text view is selectable, default is true.
290    pub fn selectable(mut self, selectable: bool) -> Self {
291        self.selectable = selectable;
292        self
293    }
294
295    /// Set the [`SelectionFormat`], default is [`SelectionFormat::Plain`].
296    ///
297    /// With [`SelectionFormat::Source`], selecting inside `**bold**` yields
298    /// `**bold**` (the Markdown source) rather than `bold`.
299    pub fn selection_format(mut self, selection_format: SelectionFormat) -> Self {
300        self.selection_format = selection_format;
301        self
302    }
303
304    /// Set the text view to be scrollable, default is false.
305    ///
306    /// ## If true for `scrollable`
307    ///
308    /// The `scrollable` mode used for large content,
309    /// will show scrollbar, but requires the parent to have a fixed height,
310    /// and use [`gpui::list`] to render the content in a virtualized way.
311    ///
312    /// ## If false to fit content
313    ///
314    /// The TextView will expand to fit all content, no scrollbar.
315    /// This mode is suitable for small content, such as a few lines of text, a label, etc.
316    pub fn scrollable(mut self, scrollable: bool) -> Self {
317        self.scrollable = scrollable;
318        self
319    }
320
321    /// Clamp the rendered content to at most `n` lines of body text.
322    ///
323    /// The view's height is capped at `n` × the base line height, and a line
324    /// of glyphs is never cut in half: a line that would straddle the bottom
325    /// of the box is left out whole, across paragraphs, lists, headings, code
326    /// blocks and tables. Nothing is shown with less than a line of itself to
327    /// show, so the border and padding a table row leads with never strands at
328    /// the bottom; whatever has more than that is cut on the box edge and keeps
329    /// the part that fits, rather than disappearing and leaving blank space
330    /// behind.
331    ///
332    /// Check [`TextViewState::is_clamped`] (which answers for the frame that
333    /// was last painted) to decide whether to show an "expand" affordance.
334    ///
335    /// `n` counts lines of body text, so paragraph spacing and taller lines
336    /// mean fewer of them fit inside the capped height. A line taller than the
337    /// whole budget keeps the part that fits rather than emptying the box.
338    /// Ignored when [`Self::scrollable`] is set.
339    pub fn max_lines(mut self, max_lines: usize) -> Self {
340        self.max_lines = Some(max_lines);
341        self
342    }
343
344    /// Set custom block actions for code blocks.
345    ///
346    /// The closure receives the [`CodeBlock`],
347    /// and returns an element to display.
348    pub fn code_block_actions<F, E>(mut self, f: F) -> Self
349    where
350        F: Fn(&CodeBlock, &mut Window, &mut App) -> E + Send + Sync + 'static,
351        E: IntoElement,
352    {
353        self.code_block_actions = Some(Arc::new(move |code_block, window, cx| {
354            f(&code_block, window, cx).into_any_element()
355        }));
356        self
357    }
358
359    /// Adds opt-in syntax highlighting for fenced code blocks.
360    ///
361    /// Returned byte ranges are relative to [`CodeBlock::code`]. Invalid
362    /// ranges are discarded. Without this callback, code is unhighlighted.
363    pub fn code_block_highlighter<F>(mut self, highlighter: F) -> Self
364    where
365        F: Fn(&CodeBlock) -> Vec<(Range<usize>, gpui::HighlightStyle)> + Send + Sync + 'static,
366    {
367        self.code_block_highlighter = Some(Arc::new(highlighter));
368        self
369    }
370
371    /// Like [`Self::code_block_highlighter`], with a highlighter that can be
372    /// handed to every frame.
373    ///
374    /// A code block reuses its highlights only while the highlighter is the
375    /// same `Arc`, so a view built every frame with a fresh closure
376    /// re-highlights every code block on every frame.
377    #[doc(hidden)]
378    pub fn shared_code_block_highlighter(
379        mut self,
380        highlighter: Arc<
381            dyn Fn(&CodeBlock) -> Vec<(Range<usize>, gpui::HighlightStyle)> + Send + Sync,
382        >,
383    ) -> Self {
384        self.code_block_highlighter = Some(highlighter);
385        self
386    }
387
388    /// Set custom actions to be rendered below each Markdown table.
389    ///
390    /// The closure receives the [`TableData`],
391    /// and returns an element to display.
392    pub fn table_actions<F, E>(mut self, f: F) -> Self
393    where
394        F: Fn(&TableData, &mut Window, &mut App) -> E + Send + Sync + 'static,
395        E: IntoElement,
396    {
397        self.table_actions = Some(Arc::new(move |table, window, cx| {
398            f(table, window, cx).into_any_element()
399        }));
400        self
401    }
402
403    /// Handle pointer events on rendered links.
404    ///
405    /// The handler receives the resolved URL and the original GPUI click event.
406    /// Without a handler, links open through App::open_url.
407    pub fn on_link_click<F>(mut self, handler: F) -> Self
408    where
409        F: Fn(&SharedString, &ClickEvent, &mut Window, &mut App) + Send + Sync + 'static,
410    {
411        self.link_click_handler = Some(Arc::new(handler));
412        self
413    }
414
415    /// Scroll a container that does not follow scroll requests to the line
416    /// of [`TextViewState::reveal_range`].
417    ///
418    /// A `gpui::list` scrolls to that line by itself; a `div` with
419    /// `overflow_y_scroll`, for one, does not. After a frame in which the
420    /// line was laid out but not visible, the handler receives its bounds in
421    /// window coordinates, to scroll the container, e.g. through its
422    /// `ScrollHandle`, until the line is visible.
423    pub fn on_reveal<F>(mut self, handler: F) -> Self
424    where
425        F: Fn(Bounds<Pixels>, &mut Window, &mut App) + 'static,
426    {
427        self.reveal_handler = Some(Rc::new(handler));
428        self
429    }
430
431    /// Replace the Markdown extension registry.
432    pub fn markdown_extensions(mut self, extensions: MarkdownExtensions) -> Self {
433        self.markdown_extensions = Arc::new(extensions);
434        self
435    }
436
437    /// Set the motion policy; see [`TextViewMotion`]. Without one, the
438    /// state's own policy applies, which plays no motion by default.
439    pub fn motion(mut self, motion: TextViewMotion) -> Self {
440        self.motion = Some(motion);
441        self
442    }
443
444    /// Enable MDX JSX/expression parsing.
445    ///
446    /// This disables raw HTML parsing because `markdown-rs` gives HTML
447    /// priority over MDX when both are enabled.
448    pub fn markdown_mdx(mut self) -> Self {
449        let extensions = Arc::make_mut(&mut self.markdown_extensions);
450        *extensions = extensions.clone().mdx();
451        self
452    }
453
454    /// Register a custom block-level Markdown parser.
455    ///
456    /// The parser runs during Markdown AST conversion and must be independent
457    /// of [`Window`] / [`App`]. Store any parsed data in [`MarkdownNode`] and
458    /// render it later with [`Self::markdown_block_renderer`].
459    pub fn markdown_block_parser<F>(mut self, parser: F) -> Self
460    where
461        F: for<'a> Fn(
462                &markdown::mdast::Node,
463                &crate::text::MarkdownParseContext<'a>,
464            ) -> Option<MarkdownNode>
465            + Send
466            + Sync
467            + 'static,
468    {
469        Arc::make_mut(&mut self.markdown_extensions).push_block_parser(parser);
470        self
471    }
472
473    /// Register a renderer for a custom block-level Markdown node name.
474    pub fn markdown_block_renderer<F, E>(
475        mut self,
476        name: impl Into<SharedString>,
477        renderer: F,
478    ) -> Self
479    where
480        F: Fn(&MarkdownNode, &mut Window, &mut App) -> E + Send + Sync + 'static,
481        E: IntoElement,
482    {
483        Arc::make_mut(&mut self.markdown_extensions).push_block_renderer(name, renderer);
484        self
485    }
486
487    /// Apply a reusable text view plugin.
488    pub fn plugin<P>(self, plugin: P) -> Self
489    where
490        P: TextViewPlugin,
491    {
492        plugin.setup(self)
493    }
494}
495
496impl IntoElement for TextView {
497    type Element = Self;
498
499    fn into_element(self) -> Self::Element {
500        self
501    }
502}
503
504pub struct TextViewLayoutState {
505    state: Entity<TextViewState>,
506    element: AnyElement,
507}
508
509pub struct TextViewPrepaintState {
510    hitbox: Hitbox,
511    /// Where paint has to pull the `max_lines` clip up to, because a glyph line
512    /// straddles the bottom of the box. `None` leaves the clip at the box edge,
513    /// where the container's hidden overflow already applies it.
514    clip_bottom: Option<Pixels>,
515    /// The touch handles this view owns, with their hitboxes.
516    touch_handles: crate::TouchHandleLayout,
517}
518
519/// Absorbs sub-pixel layout jitter: a line ending within a pixel of the box
520/// bottom counts as fitting inside it.
521const CLIP_EPSILON: Pixels = px(1.);
522
523/// The bottom of the last whole line at or above `y`, with the height of a line
524/// where it sits.
525fn last_line_bottom_above(spans: &[LineSpan], y: Pixels) -> Option<(Pixels, Pixels)> {
526    let mut last: Option<(Pixels, Pixels)> = None;
527    let mut keep = |bottom: Pixels, line_height: Pixels| {
528        if bottom <= y + CLIP_EPSILON && last.is_none_or(|(last, _)| bottom > last) {
529            last = Some((bottom, line_height));
530        }
531    };
532
533    for span in spans {
534        if span.line_height <= px(0.) {
535            continue;
536        }
537        let mut bottom = span.top + span.line_height;
538        while bottom <= span.bottom + CLIP_EPSILON {
539            keep(bottom, span.line_height);
540            bottom += span.line_height;
541        }
542        // The span's own bottom covers a last line taller than the rest.
543        keep(span.bottom, span.line_height);
544    }
545
546    last
547}
548
549/// Where to clip, given the lines a descendant `Inline` reported. `None` leaves
550/// the clip on the box edge.
551///
552/// Two things are never shown: half a line of glyphs, and anything with less
553/// than a line of itself to show. A line straddling `box_bottom` is left out
554/// whole, and so is the strip between it and the line before — the border and
555/// padding a table row leads with reads as a rendering fault rather than as a
556/// row. Whatever has more than a line to show is cut on the edge and keeps the
557/// part that fits, so the box holds no blank space it could have filled.
558fn line_safe_clip_bottom(
559    spans: &[LineSpan],
560    box_bottom: Pixels,
561    content_bottom: Pixels,
562) -> Option<Pixels> {
563    let mut clip = box_bottom;
564
565    for span in spans {
566        if span.line_height <= px(0.)
567            || span.top >= box_bottom
568            || span.bottom <= box_bottom + CLIP_EPSILON
569        {
570            continue;
571        }
572        let whole_lines = ((box_bottom - span.top) / span.line_height).floor();
573        let line_top = span.top + span.line_height * whole_lines;
574        // A line starting on the box edge is not straddling it.
575        if line_top < box_bottom - CLIP_EPSILON {
576            clip = clip.min(line_top);
577        }
578    }
579
580    let Some((last_line_bottom, line_height)) = last_line_bottom_above(spans, clip) else {
581        // Leaving the straddling line out would leave nothing at all — a first
582        // line taller than the whole budget, a heading in a one-line box. It
583        // keeps the part that fits instead, because an empty clamp reads as
584        // broken where a cut one reads as more to come.
585        return None;
586    };
587
588    // Snap away a scrap. Only content that continues past the box can leave
589    // one: the space under the last line of a document that fits is the box's
590    // own, not a piece of something below.
591    if content_bottom > box_bottom + CLIP_EPSILON {
592        let strip = clip - last_line_bottom;
593        if strip > CLIP_EPSILON && strip < line_height {
594            clip = last_line_bottom;
595        }
596    }
597
598    (clip < box_bottom - CLIP_EPSILON).then_some(clip)
599}
600
601impl Element for TextView {
602    type RequestLayoutState = TextViewLayoutState;
603    type PrepaintState = TextViewPrepaintState;
604
605    fn id(&self) -> Option<ElementId> {
606        Some(self.id.clone())
607    }
608
609    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
610        None
611    }
612
613    fn request_layout(
614        &mut self,
615        _: Option<&GlobalElementId>,
616        _: Option<&InspectorElementId>,
617        window: &mut Window,
618        cx: &mut App,
619    ) -> (LayoutId, Self::RequestLayoutState) {
620        let state = if let Some(state) = self.state.clone() {
621            state
622        } else {
623            let default_format = self.format.unwrap_or(TextViewFormat::Markdown);
624            let default_text = self.text.clone().unwrap_or_default();
625
626            let state = window.use_keyed_state(
627                SharedString::from(format!("{}/state", self.id)),
628                cx,
629                move |_, cx| {
630                    if default_format == TextViewFormat::Markdown {
631                        TextViewState::markdown(default_text.as_str(), cx)
632                    } else {
633                        TextViewState::html(default_text.as_str(), cx)
634                    }
635                },
636            );
637            self.state = Some(state.clone());
638            state
639        };
640
641        // `max_lines` needs the whole document laid out to snap the clip to a
642        // whole line, so it only applies to the fit-content mode.
643        let max_lines = self.max_lines.filter(|_| !self.scrollable);
644
645        // Resolve the style by reference: this runs every frame, and the
646        // style only reaches the state when it changed.
647        let defaults = cx.try_global::<TextViewDefaults>();
648        let theme_style;
649        let mut text_view_style = match (
650            &self.text_view_style,
651            defaults.and_then(|d| d.style.as_ref()),
652        ) {
653            (Some(style), _) | (None, Some(style)) => style,
654            (None, None) => {
655                theme_style = TextViewStyle::from_theme(&crate::Theme::global(cx));
656                &theme_style
657            }
658        };
659        // Follow the text color the container sets for its surface, so rich
660        // text stays readable in a filled bubble or a selected row.
661        let surface_style;
662        let mut on_inverted_surface = false;
663        if self.text_view_style.is_none() && defaults.is_some_and(|d| d.inherit_text_color) {
664            let color = self
665                .style
666                .text
667                .color
668                .unwrap_or_else(|| window.text_style().color);
669            if color != text_view_style.foreground() {
670                on_inverted_surface = text_view_style.is_inverted_by(color);
671                surface_style = text_view_style.on_text_color(color);
672                text_view_style = &surface_style;
673            }
674        }
675        let foreground = text_view_style.foreground();
676        let text_view_style = (*state.read(cx).text_view_style != *text_view_style)
677            .then(|| Arc::new(text_view_style.clone()));
678        let code_block_highlighter = self.code_block_highlighter.clone().or_else(|| {
679            defaults
680                .filter(|_| !on_inverted_surface)
681                .and_then(|d| d.code_block_highlighter.clone())
682        });
683
684        state.update(cx, |state, cx| {
685            if text_view_style.is_some()
686                || match (&state.code_block_highlighter, &code_block_highlighter) {
687                    (Some(previous), Some(next)) => !Arc::ptr_eq(previous, next),
688                    (None, None) => false,
689                    _ => true,
690                }
691            {
692                state.preserve_selection_for_reflow();
693            }
694            state.code_block_actions = self.code_block_actions.clone();
695            state.code_block_highlighter = code_block_highlighter;
696            state.table_actions = self.table_actions.clone();
697            state.link_click_handler = self.link_click_handler.clone();
698            state.image_source = self.image_source.clone();
699            state.set_markdown_extensions(self.markdown_extensions.clone(), cx);
700            if let Some(motion) = &self.motion {
701                state.set_motion(motion.clone());
702            }
703            state.selectable = self.selectable;
704            state.selection_format = self.selection_format;
705            state.scrollable = self.scrollable;
706            state.max_lines = max_lines;
707            if let Some(text_view_style) = text_view_style {
708                state.text_view_style = text_view_style;
709            }
710
711            if let Some(text) = &self.text {
712                state.set_element_text(text, cx);
713            }
714        });
715
716        let focus_handle = state.read(cx).focus_handle.clone();
717        let list_state = state.read(cx).list_state.clone();
718        // Cap the box at `n` body-text lines (the effective text style may be
719        // refined by this view's own style, e.g. `.text_sm()`); hidden
720        // overflow also clips descendant hitboxes to the box during prepaint.
721        let max_lines_cap = max_lines.map(|max_lines| {
722            let mut text_style = window.text_style();
723            text_style.refine(&self.style.text);
724            text_style.line_height_in_pixels(window.rem_size()) * max_lines as f32
725        });
726
727        let mut el = div()
728            .id(("text-view-scroll", state.entity_id()))
729            .key_context("TextView")
730            .track_focus(&focus_handle)
731            .when(self.scrollable, |this| this.size_full())
732            .when_some(max_lines_cap, |this, cap| this.max_h(cap).overflow_hidden())
733            .relative()
734            .text_color(foreground)
735            .on_action(move |_: &crate::input::Copy, window, cx| {
736                let text = TextSelection::selected_text(window, cx).trim().to_string();
737                if text.is_empty() {
738                    cx.propagate();
739                    return;
740                }
741                cx.write_to_clipboard(gpui::ClipboardItem::new_string(text));
742            })
743            .on_action(window.listener_for(&state, TextViewState::on_action_select_all))
744            .map(|this| {
745                if self.scrollable {
746                    this.child(crate::ScrollBounce::new(
747                        ("text-view-bounce", state.entity_id()),
748                        &list_state,
749                        state.clone(),
750                    ))
751                } else {
752                    this.child(state.clone())
753                }
754            })
755            // Overlay controls must paint after the document, otherwise rich
756            // content and selection backgrounds cover the thumb and hitbox.
757            .when(self.scrollable, |this| {
758                this.child(
759                    div().absolute().inset_0().child(
760                        crate::Scrollbar::vertical(&list_state)
761                            .id(("text-view-scrollbar", state.entity_id()))
762                            .viewport_from_layout(),
763                    ),
764                )
765            })
766            .refine_style(&self.style)
767            .into_any_element();
768        let layout_id = el.request_layout(window, cx);
769        (layout_id, TextViewLayoutState { state, element: el })
770    }
771
772    fn prepaint(
773        &mut self,
774        _: Option<&GlobalElementId>,
775        _: Option<&InspectorElementId>,
776        bounds: Bounds<Pixels>,
777        request_layout: &mut Self::RequestLayoutState,
778        window: &mut Window,
779        cx: &mut App,
780    ) -> Self::PrepaintState {
781        let state = request_layout.state.clone();
782        let max_lines_active = state.read(cx).max_lines.is_some();
783        if max_lines_active {
784            if let Ok(mut line_spans) = state.read(cx).line_spans.lock() {
785                line_spans.clear();
786            }
787            // Descendant `Inline`s report their line spans through the state
788            // stack during prepaint (in addition to the paint-time push below).
789            GlobalState::global(cx).push_text_view_state(state.clone());
790        }
791        request_layout.element.prepaint(window, cx);
792        if max_lines_active {
793            GlobalState::global(cx).pop_text_view_state();
794        }
795
796        let mut clip_bottom = None;
797        if max_lines_active {
798            let (line_spans, content_bottom) = {
799                let state = state.read(cx);
800                (
801                    state
802                        .line_spans
803                        .lock()
804                        .map(|spans| spans.clone())
805                        .unwrap_or_default(),
806                    state.bounds().bottom(),
807                )
808            };
809            // The content keeps its natural height inside the capped box, so
810            // this sees everything the box cannot show — including a tall image
811            // that reports no lines of its own.
812            let clipped = content_bottom > bounds.bottom() + px(1.);
813            // Notify on change so observers (e.g. an "expand" button gated on
814            // `is_clamped`) re-render once the flag flips.
815            if state.read(cx).clamped != clipped {
816                state.update(cx, |state, cx| {
817                    state.clamped = clipped;
818                    cx.notify();
819                });
820            }
821            if clipped {
822                clip_bottom = line_safe_clip_bottom(&line_spans, bounds.bottom(), content_bottom);
823            }
824        }
825
826        let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
827        // Over the text, so after its hitbox.
828        let touch_handles = if self.selectable {
829            state
830                .read(cx)
831                .selection_adapter
832                .prepaint_touch_handles(window, cx)
833        } else {
834            crate::TouchHandleLayout::default()
835        };
836        TextViewPrepaintState {
837            hitbox,
838            clip_bottom,
839            touch_handles,
840        }
841    }
842
843    fn paint(
844        &mut self,
845        _: Option<&GlobalElementId>,
846        _: Option<&InspectorElementId>,
847        bounds: Bounds<Pixels>,
848        request_layout: &mut Self::RequestLayoutState,
849        prepaint: &mut Self::PrepaintState,
850        window: &mut Window,
851        cx: &mut App,
852    ) {
853        let state = &request_layout.state;
854        if self.selectable {
855            state.read(cx).selection_adapter.begin_frame();
856        }
857
858        GlobalState::global(cx).push_text_view_state(state.clone());
859        if let Some(clip_bottom) = prepaint.clip_bottom {
860            // Snap the `max_lines` clip to the last whole line that fits, so a
861            // line of glyphs is never cut in half.
862            let mask = ContentMask {
863                bounds: Bounds::from_corners(bounds.origin, point(bounds.right(), clip_bottom)),
864            };
865            window.with_content_mask(Some(mask), |window| {
866                request_layout.element.paint(window, cx);
867            });
868        } else {
869            request_layout.element.paint(window, cx);
870        }
871        GlobalState::global(cx).pop_text_view_state();
872
873        // Every list has scrolled by now, so the line of a reveal is where
874        // it ends up this frame.
875        if state.read(cx).pending_reveal.is_some() {
876            let progress = state.update(cx, |state, _| {
877                state.pending_reveal.as_mut().map(PendingReveal::progress)
878            });
879            match progress {
880                Some(RevealProgress::Shown) => {
881                    state.update(cx, |state, _| state.pending_reveal = None);
882                }
883                Some(RevealProgress::Hidden(line)) => {
884                    if let Some(handler) = &self.reveal_handler {
885                        handler(line, window, cx);
886                    }
887                }
888                Some(RevealProgress::NotLaidOut) | None => {}
889            }
890        }
891
892        if self.selectable {
893            let (adapter, scroll_offset, content_bounds, self_scroll, handle_color) = {
894                let state = state.read(cx);
895                (
896                    state.selection_adapter.clone(),
897                    state.scroll_offset(),
898                    state.bounds(),
899                    state.scrollable,
900                    state.text_view_style.selection().alpha(1.),
901                )
902            };
903            let document_order = GlobalState::global(cx).next_selection_document_order();
904            adapter.register(
905                prepaint.hitbox.clone(),
906                content_bounds,
907                scroll_offset,
908                document_order,
909                self_scroll,
910                window,
911                cx,
912            );
913            // The handles of a touch selection go over the text, and under
914            // whatever is painted over the text after it.
915            adapter.paint_touch_handles(&prepaint.touch_handles, handle_color, window, cx);
916        }
917    }
918}
919
920#[cfg(test)]
921mod tests {
922    use std::sync::{
923        Arc,
924        atomic::{AtomicUsize, Ordering},
925    };
926
927    use super::{TextView, TextViewPlugin};
928    use crate::text::{TableData, TextViewState, TextViewStyle};
929    use gpui::{
930        AppContext as _, Bounds, ClickEvent, Context, Entity, InteractiveElement as _, IntoElement,
931        Modifiers, MouseButton, MouseDownEvent, MouseUpEvent, Overflow, ParentElement as _, Pixels,
932        Render, SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled as _,
933        TestAppContext, VisualTestContext, Window, div, point, prelude::FluentBuilder as _, px,
934        rems,
935    };
936
937    struct TextViewTestRoot {
938        text_view: Entity<TextViewState>,
939    }
940
941    struct InlineHoverTestRoot {
942        view: Entity<TextViewState>,
943        builds: Arc<AtomicUsize>,
944        format: crate::text::SelectionFormat,
945    }
946
947    struct InlineHoverCard;
948    impl Render for InlineHoverCard {
949        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
950            div()
951                .debug_selector(|| "inline-test-card".into())
952                .w(px(120.))
953                .h(px(40.))
954                .child("Member profile")
955        }
956    }
957
958    impl Render for InlineHoverTestRoot {
959        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
960            let builds = self.builds.clone();
961            div()
962                .pl(px(150.))
963                .w(px(300.))
964                .text_size(px(16.))
965                .child(crate::TextSelectionLayer)
966                .child(
967                    TextView::new(&self.view)
968                        .selection_format(self.format)
969                        .plugin(
970                            crate::text::markdown_ext::TestInlinePlugin::new("mention")
971                                .parse_with(|node, _| {
972                                    let markdown::mdast::Node::Link(link) = node else {
973                                        return None;
974                                    };
975                                    let handle = link.url.strip_prefix("mention:")?;
976                                    Some(
977                                        crate::text::MarkdownNode::new("mention", ())
978                                            .text(format!("@{handle}")),
979                                    )
980                                })
981                                .render_with(move |_, _, _, _| {
982                                    let builds = builds.clone();
983                                    Some(crate::text::InlineElement::new(
984                                        crate::HoverCard::new("mention-hover")
985                                            .anchor(gpui::Anchor::TopCenter)
986                                            .trigger(div().child("@member"))
987                                            .content(move |_, _, cx| {
988                                                builds.fetch_add(1, Ordering::Relaxed);
989                                                div()
990                                                    .id("hover-content")
991                                                    .child(cx.new(|_| InlineHoverCard))
992                                            }),
993                                    ))
994                                }),
995                        ),
996                )
997        }
998    }
999
1000    #[gpui::test]
1001    fn text_view_follows_the_text_color_of_its_container(cx: &mut TestAppContext) {
1002        struct Root {
1003            inherited: Entity<TextViewState>,
1004            explicit: Entity<TextViewState>,
1005            surface_text: gpui::Hsla,
1006        }
1007        impl Render for Root {
1008            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1009                div()
1010                    .w(px(300.))
1011                    .text_color(crate::ColorTokens::light().foreground)
1012                    .child(
1013                        div()
1014                            .text_color(self.surface_text)
1015                            .child(TextView::new(&self.inherited))
1016                            .child(TextView::new(&self.explicit).style(TextViewStyle::default())),
1017                    )
1018            }
1019        }
1020
1021        let colors = crate::ColorTokens::light();
1022        cx.update(|cx| {
1023            crate::init(cx);
1024            super::TextViewDefaults::new()
1025                .with_style(TextViewStyle::default())
1026                .with_inherit_text_color(true)
1027                .install(cx);
1028        });
1029        let (root, cx) = cx.add_window_view(|_, cx| Root {
1030            inherited: cx.new(|cx| TextViewState::markdown("[link](https://a.b) `code`", cx)),
1031            explicit: cx.new(|cx| TextViewState::markdown("text", cx)),
1032            surface_text: colors.primary_foreground,
1033        });
1034        cx.run_until_parked();
1035        cx.update(|window, cx| {
1036            window.draw(cx).clear(cx);
1037        });
1038
1039        root.read_with(cx, |root, cx| {
1040            let inherited = &root.inherited.read(cx).text_view_style;
1041            assert_eq!(inherited.foreground(), colors.primary_foreground);
1042            assert_eq!(inherited.link(), colors.primary_foreground);
1043            assert!(inherited.is_dark());
1044
1045            let explicit = &root.explicit.read(cx).text_view_style;
1046            assert_eq!(explicit.foreground(), colors.foreground);
1047            assert_eq!(explicit.link(), TextViewStyle::default().link());
1048        });
1049    }
1050
1051    #[gpui::test]
1052    fn inline_plugin_reuses_render_and_preserves_native_child_events(cx: &mut TestAppContext) {
1053        use std::sync::Mutex;
1054        struct ControlPlugin(Arc<Mutex<Vec<String>>>);
1055        impl crate::text::MarkdownPlugin for ControlPlugin {
1056            fn name(&self) -> &str {
1057                "control"
1058            }
1059            fn parse(
1060                &self,
1061                node: &markdown::mdast::Node,
1062                _: &crate::text::MarkdownParseContext<'_>,
1063            ) -> Option<crate::text::MarkdownNode> {
1064                let markdown::mdast::Node::Link(link) = node else {
1065                    return None;
1066                };
1067                let label = link.url.strip_prefix("control:")?;
1068                Some(crate::text::MarkdownNode::new("control", ()).text(label.to_string()))
1069            }
1070            fn render(
1071                &self,
1072                node: &crate::text::MarkdownNode,
1073                _: &mut Window,
1074                _: &mut gpui::App,
1075            ) -> impl IntoElement {
1076                let clicks = self.0.clone();
1077                let label = node.as_text().to_string();
1078                div()
1079                    .id("same-control-id")
1080                    .w(px(60.))
1081                    .h(px(24.))
1082                    .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
1083                    .on_click(move |_, _, _| clicks.lock().unwrap().push(label.clone()))
1084                    .child(node.as_text().to_string())
1085            }
1086        }
1087        struct Root {
1088            view: Entity<TextViewState>,
1089            clicks: Arc<Mutex<Vec<String>>>,
1090        }
1091        impl Render for Root {
1092            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1093                div()
1094                    .w(px(300.))
1095                    .child(crate::TextSelectionLayer)
1096                    .child(TextView::new(&self.view).plugin(ControlPlugin(self.clicks.clone())))
1097            }
1098        }
1099        cx.update(crate::init);
1100        let clicks = Arc::new(Mutex::new(Vec::new()));
1101        let captured = clicks.clone();
1102        let (root, cx) = cx.add_window_view(move |_, cx| Root {
1103            view: cx.new(|cx| TextViewState::markdown("[one](control:one)[two](control:two)", cx)),
1104            clicks,
1105        });
1106        cx.run_until_parked();
1107        cx.update(|window, cx| {
1108            window.draw(cx).clear(cx);
1109        });
1110        let bounds = root.read_with(cx, |root, cx| {
1111            root.view.read(cx).selection_adapter.text_bounds()
1112        });
1113        assert_eq!(bounds.len(), 2);
1114        for bounds in bounds {
1115            cx.simulate_click(bounds.center(), Modifiers::default());
1116            cx.update(|window, cx| {
1117                window.draw(cx).clear(cx);
1118            });
1119        }
1120        assert_eq!(*captured.lock().unwrap(), vec!["one", "two"]);
1121    }
1122
1123    #[gpui::test]
1124    fn inline_hover_card_is_lazy_and_retains_atomic_copy(cx: &mut TestAppContext) {
1125        cx.update(crate::init);
1126        let builds = Arc::new(AtomicUsize::new(0));
1127        let source = "[@member](mention:member)";
1128        let (root, cx) = cx.add_window_view(|_, cx| InlineHoverTestRoot {
1129            view: cx.new(|cx| TextViewState::markdown(source, cx)),
1130            builds: builds.clone(),
1131            format: crate::text::SelectionFormat::Plain,
1132        });
1133        cx.run_until_parked();
1134        cx.update(|window, cx| window.draw(cx).clear(cx));
1135        assert_eq!(builds.load(Ordering::Relaxed), 0);
1136        let view = root.read_with(cx, |root, _| root.view.clone());
1137        let bounds = view.read_with(cx, |view, _| view.selection_adapter.text_bounds()[0]);
1138        cx.update(|window, cx| {
1139            window.simulate_mouse_move(point(bounds.right() - px(1.), bounds.center().y), cx)
1140        });
1141        cx.run_until_parked();
1142        cx.executor()
1143            .advance_clock(std::time::Duration::from_secs(1));
1144        cx.run_until_parked();
1145        cx.update(|window, cx| window.draw(cx).clear(cx));
1146        assert!(
1147            builds.load(Ordering::Relaxed) > 0,
1148            "hover did not build the card"
1149        );
1150        cx.update(|window, cx| window.draw(cx).clear(cx));
1151        let card = cx
1152            .debug_bounds("inline-test-card")
1153            .expect("hover card should be painted");
1154        assert!(
1155            (card.center().x - bounds.center().x).abs() < px(1.),
1156            "card {card:?} must be centered on mention {bounds:?}"
1157        );
1158        assert!(
1159            card.top() >= bounds.bottom(),
1160            "card should be below the mention"
1161        );
1162        for (format, expected) in [
1163            (crate::text::SelectionFormat::Plain, "@member"),
1164            (crate::text::SelectionFormat::Source, source),
1165        ] {
1166            root.update(cx, |root, cx| {
1167                root.format = format;
1168                cx.notify();
1169            });
1170            view.update(cx, |view, cx| {
1171                view.set_selection_format(format, cx);
1172                view.select_all(cx);
1173            });
1174            cx.update(|window, cx| window.draw(cx).clear(cx));
1175            assert_eq!(
1176                view.read_with(cx, |view, _| view.selected_text()).trim(),
1177                expected
1178            );
1179        }
1180    }
1181
1182    struct InlinePluginTestRoot {
1183        text_view: Entity<TextViewState>,
1184        width: Pixels,
1185        font_size: Pixels,
1186        source_format: bool,
1187        prepared_size: Option<Arc<AtomicUsize>>,
1188    }
1189
1190    impl Render for InlinePluginTestRoot {
1191        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1192            let prepared_size = self.prepared_size.clone();
1193            div().w(self.width).text_size(self.font_size).child(crate::TextSelectionLayer).child(
1194                TextView::new(&self.text_view)
1195                    .selection_format(if self.source_format { crate::text::SelectionFormat::Source }
1196                        else { crate::text::SelectionFormat::Plain })
1197
1198                    .plugin(crate::text::markdown_ext::TestInlinePlugin::new("math").parse_with(|node, _| {
1199                        let markdown::mdast::Node::InlineMath(math) = node else { return None };
1200                        Some(crate::text::MarkdownNode::new("math", ()).text(format!("{}²", math.value)))
1201                    }).render_with(move |_, context, _, _| {
1202                        let value = prepared_size.as_ref()?.load(Ordering::Relaxed) as f32;
1203                        let image = Arc::new(gpui::Image::from_bytes(gpui::ImageFormat::Svg,
1204                            b"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"40\" height=\"40\"><path d=\"M0 0L40 40\" stroke=\"black\"/></svg>".to_vec()));
1205                        let unit = context.font_size() / 16.;
1206                        Some(crate::text::InlineElement::new(gpui::img(image).w(unit * value).h(unit * value)).with_baseline(unit * value * 0.75))
1207                    })))
1208        }
1209    }
1210
1211    #[gpui::test]
1212    fn inline_plugins_drag_and_copy_across_formulas_in_both_directions(cx: &mut TestAppContext) {
1213        cx.update(crate::init);
1214        let (root, cx) = cx.add_window_view(|_, cx| InlinePluginTestRoot {
1215            text_view: cx.new(|cx| TextViewState::markdown("中文 $x$ $y$ English", cx)),
1216            width: px(420.),
1217            font_size: px(16.),
1218            source_format: false,
1219            prepared_size: None,
1220        });
1221        let cx: &mut VisualTestContext = cx;
1222        for font_size in [16., 24., 32.] {
1223            for width in [60., 160., 420.] {
1224                for prepared in [None, Some(32)] {
1225                    for source_format in [false, true] {
1226                        root.update(cx, |root, cx| {
1227                            root.source_format = source_format;
1228                            root.width = px(width);
1229                            root.font_size = px(font_size);
1230                            root.prepared_size =
1231                                prepared.map(|value| Arc::new(AtomicUsize::new(value)));
1232                            cx.notify();
1233                        });
1234                        cx.run_until_parked();
1235                        cx.update(|window, cx| window.draw(cx).clear(cx));
1236                        let bounds =
1237                            root.read_with(cx, |root, cx| root.text_view.read(cx).bounds());
1238                        let text_bounds = root.read_with(cx, |root, cx| {
1239                            root.text_view.read(cx).selection_adapter.text_bounds()
1240                        });
1241                        let first = text_bounds.first().unwrap();
1242                        let last = text_bounds.last().unwrap();
1243                        let left =
1244                            point(first.left() + px(0.1), first.top() + first.size.height / 2.);
1245                        let right =
1246                            point(last.right() - px(0.1), last.top() + last.size.height / 2.);
1247                        for (start, end) in [(left, right), (right, left)] {
1248                            cx.simulate_mouse_down(start, MouseButton::Left, Modifiers::default());
1249                            cx.update(|window, cx| window.draw(cx).clear(cx));
1250                            cx.simulate_mouse_move(end, MouseButton::Left, Modifiers::default());
1251                            cx.update(|window, cx| window.draw(cx).clear(cx));
1252                            cx.simulate_mouse_up(end, MouseButton::Left, Modifiers::default());
1253                            cx.update(|window, cx| window.draw(cx).clear(cx));
1254                            let selected = root
1255                                .read_with(cx, |root, cx| root.text_view.read(cx).selected_text());
1256                            assert_eq!(
1257                                selected.trim(),
1258                                if source_format {
1259                                    "中文 $x$ $y$ English"
1260                                } else {
1261                                    "中文 x² y² English"
1262                                },
1263                                "start={start:?} end={end:?} bounds={bounds:?} width={width} font_size={font_size} prepared={prepared:?}"
1264                            );
1265                        }
1266                    }
1267                }
1268            }
1269        }
1270    }
1271
1272    #[gpui::test]
1273    fn inline_resource_size_update_reflows_without_changing_selection(cx: &mut TestAppContext) {
1274        cx.update(crate::init);
1275        let prepared_size = Arc::new(AtomicUsize::new(16));
1276        let (root, cx) = cx.add_window_view(|_, cx| InlinePluginTestRoot {
1277            text_view: cx.new(|cx| TextViewState::markdown("中文 $x$ $y$ English", cx)),
1278            width: px(160.),
1279            font_size: px(16.),
1280            source_format: false,
1281            prepared_size: Some(prepared_size.clone()),
1282        });
1283        let cx: &mut VisualTestContext = cx;
1284        cx.run_until_parked();
1285        cx.update(|window, cx| window.draw(cx).clear(cx));
1286        let before = root.read_with(cx, |root, cx| root.text_view.read(cx).bounds());
1287        let regions = root.read_with(cx, |root, cx| {
1288            root.text_view.read(cx).selection_adapter.text_bounds()
1289        });
1290        let first = regions.first().unwrap();
1291        let last = regions.last().unwrap();
1292        let start = point(first.left() + px(0.1), first.top() + px(10.));
1293        let end = point(last.right() - px(0.1), last.top() + px(10.));
1294        cx.simulate_mouse_down(start, MouseButton::Left, Modifiers::default());
1295        cx.update(|window, cx| window.draw(cx).clear(cx));
1296        cx.simulate_mouse_move(end, MouseButton::Left, Modifiers::default());
1297        cx.update(|window, cx| window.draw(cx).clear(cx));
1298        cx.simulate_mouse_up(end, MouseButton::Left, Modifiers::default());
1299        cx.update(|window, cx| window.draw(cx).clear(cx));
1300        assert_eq!(
1301            root.read_with(cx, |root, cx| root.text_view.read(cx).selected_text())
1302                .trim(),
1303            "中文 x² y² English"
1304        );
1305        prepared_size.store(100, Ordering::Relaxed);
1306        root.update(cx, |root, cx| {
1307            root.text_view
1308                .update(cx, |state, cx| state.invalidate_inline_layout(cx))
1309        });
1310        cx.run_until_parked();
1311        cx.update(|window, cx| window.draw(cx).clear(cx));
1312        let after = root.read_with(cx, |root, cx| root.text_view.read(cx).bounds());
1313        assert!(after.size.height > before.size.height * 2.);
1314        assert_eq!(
1315            root.read_with(cx, |root, cx| root.text_view.read(cx).selected_text())
1316                .trim(),
1317            "中文 x² y² English"
1318        );
1319    }
1320
1321    #[gpui::test]
1322    fn triple_click_on_formula_selects_its_entire_mixed_line(cx: &mut TestAppContext) {
1323        cx.update(crate::init);
1324        let (root, cx) = cx.add_window_view(|_, cx| InlinePluginTestRoot {
1325            text_view: cx.new(|cx| TextViewState::markdown("before $x$ after", cx)),
1326            width: px(420.),
1327            font_size: px(16.),
1328            source_format: false,
1329            prepared_size: None,
1330        });
1331        let cx: &mut VisualTestContext = cx;
1332        cx.run_until_parked();
1333        cx.update(|window, cx| window.draw(cx).clear(cx));
1334        let regions = root.read_with(cx, |root, cx| {
1335            root.text_view.read(cx).selection_adapter.text_bounds()
1336        });
1337        let formula = regions[1];
1338        let position = formula.center();
1339        cx.simulate_event(MouseDownEvent {
1340            position,
1341            modifiers: Modifiers::default(),
1342            button: MouseButton::Left,
1343            click_count: 3,
1344            first_mouse: false,
1345        });
1346        cx.simulate_event(MouseUpEvent {
1347            position,
1348            modifiers: Modifiers::default(),
1349            button: MouseButton::Left,
1350            click_count: 3,
1351        });
1352        cx.update(|window, cx| window.draw(cx).clear(cx));
1353        assert_eq!(
1354            root.read_with(cx, |root, cx| root.text_view.read(cx).selected_text())
1355                .trim(),
1356            "before x² after"
1357        );
1358        root.update(cx, |root, cx| {
1359            root.source_format = true;
1360            cx.notify();
1361        });
1362        cx.update(|window, cx| window.draw(cx).clear(cx));
1363        assert_eq!(
1364            root.read_with(cx, |root, cx| root.text_view.read(cx).selected_text())
1365                .trim(),
1366            "before $x$ after"
1367        );
1368    }
1369
1370    #[gpui::test]
1371    fn double_click_on_formula_selects_only_that_object(cx: &mut TestAppContext) {
1372        cx.update(crate::init);
1373        let (root, cx) = cx.add_window_view(|_, cx| InlinePluginTestRoot {
1374            text_view: cx.new(|cx| TextViewState::markdown("before $x$ after", cx)),
1375            width: px(420.),
1376            font_size: px(16.),
1377            source_format: false,
1378            prepared_size: None,
1379        });
1380        let cx: &mut VisualTestContext = cx;
1381        cx.run_until_parked();
1382        cx.update(|window, cx| window.draw(cx).clear(cx));
1383        let regions = root.read_with(cx, |root, cx| {
1384            root.text_view.read(cx).selection_adapter.text_bounds()
1385        });
1386        let position = regions[1].center();
1387        cx.simulate_event(MouseDownEvent {
1388            position,
1389            modifiers: Modifiers::default(),
1390            button: MouseButton::Left,
1391            click_count: 2,
1392            first_mouse: false,
1393        });
1394        cx.simulate_event(MouseUpEvent {
1395            position,
1396            modifiers: Modifiers::default(),
1397            button: MouseButton::Left,
1398            click_count: 2,
1399        });
1400        cx.update(|window, cx| window.draw(cx).clear(cx));
1401        // Two clicks stop at the object; only three take the whole line.
1402        assert_eq!(
1403            root.read_with(cx, |root, cx| root.text_view.read(cx).selected_text())
1404                .trim(),
1405            "x²"
1406        );
1407    }
1408
1409    /// A scrollable viewport, so the list has a bounded height to measure
1410    /// against and `max_offset_for_scrollbar` reports a real scroll extent.
1411    struct ScrollExtentTestRoot {
1412        text_view: Entity<TextViewState>,
1413    }
1414
1415    impl Render for ScrollExtentTestRoot {
1416        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1417            div()
1418                .w(px(400.))
1419                .h(px(200.))
1420                .overflow_hidden()
1421                .child(TextView::new(&self.text_view).scrollable(true))
1422        }
1423    }
1424
1425    /// `count` paragraphs, each `words` words long, so two documents can share
1426    /// a block count while differing wildly in height.
1427    fn document_of(count: usize, words: usize) -> String {
1428        (1..=count)
1429            .map(|i| format!("Block {i}: {}", "lorem ipsum dolor ".repeat(words)))
1430            .collect::<Vec<_>>()
1431            .join("\n\n")
1432    }
1433
1434    /// Replacing a document with one that happens to have the *same* block
1435    /// count must still re-measure. `Document::render_root` only resets the
1436    /// list when the count changes, so without an explicit re-measure every
1437    /// cached height stays with the previous document and the scroll extent
1438    /// keeps describing it.
1439    #[gpui::test]
1440    fn replacing_a_document_with_an_equal_block_count_remeasures(cx: &mut TestAppContext) {
1441        cx.update(crate::init);
1442
1443        const BLOCKS: usize = 24;
1444        let short = document_of(BLOCKS, 1);
1445        let tall = document_of(BLOCKS, 60);
1446
1447        let (root, cx) = cx.add_window_view(|_, cx| ScrollExtentTestRoot {
1448            text_view: cx.new(|cx| TextViewState::markdown(&short, cx)),
1449        });
1450        let cx: &mut VisualTestContext = cx;
1451
1452        // The list is populated and measured during layout, so every
1453        // assertion below has to follow a real frame.
1454        let settle = |cx: &mut VisualTestContext| {
1455            cx.run_until_parked();
1456            cx.update(|window, cx| window.draw(cx).clear(cx));
1457            cx.run_until_parked();
1458        };
1459        settle(cx);
1460
1461        let scroll_extent = |cx: &mut VisualTestContext| {
1462            root.read_with(cx, |root, cx| {
1463                root.text_view
1464                    .read(cx)
1465                    .list_state()
1466                    .max_offset_for_scrollbar()
1467                    .y
1468            })
1469        };
1470
1471        let short_extent = scroll_extent(cx);
1472
1473        root.update(cx, |root, cx| {
1474            root.text_view
1475                .update(cx, |state, cx| state.set_text(&tall, cx));
1476        });
1477        settle(cx);
1478
1479        root.read_with(cx, |root, cx| {
1480            assert_eq!(
1481                root.text_view.read(cx).list_state().item_count(),
1482                BLOCKS,
1483                "the replacement must keep the block count, or the list resets and the bug cannot occur"
1484            );
1485        });
1486
1487        let tall_extent = scroll_extent(cx);
1488        assert!(
1489            tall_extent > short_extent * 5.,
1490            "a much taller document must grow the scroll extent, but it went from \
1491             {short_extent:?} to {tall_extent:?}"
1492        );
1493    }
1494
1495    struct StatelessMarkdownRoot {
1496        renders: Arc<AtomicUsize>,
1497    }
1498
1499    impl Render for StatelessMarkdownRoot {
1500        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1501            self.renders.fetch_add(1, Ordering::Relaxed);
1502            div().child(
1503                TextView::markdown("stateless-markdown", include_str!("../../../../README.md"))
1504                    .markdown_block_parser(|_, _| None),
1505            )
1506        }
1507    }
1508
1509    struct DummyTextViewPlugin;
1510
1511    impl TextViewPlugin for DummyTextViewPlugin {
1512        fn setup(self, mut text_view: TextView) -> TextView {
1513            text_view.selectable = true;
1514            text_view
1515        }
1516    }
1517
1518    #[gpui::test]
1519    fn text_view_constructors_are_selectable_by_default(cx: &mut TestAppContext) {
1520        cx.update(crate::init);
1521        let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("state", cx)));
1522
1523        assert!(TextView::new(&state).selectable);
1524        assert!(TextView::markdown("markdown", "text").selectable);
1525        assert!(TextView::html("html", "<p>text</p>").selectable);
1526    }
1527
1528    #[gpui::test]
1529    fn stateless_markdown_with_rebuilt_parser_settles(cx: &mut TestAppContext) {
1530        cx.update(crate::init);
1531        let renders = Arc::new(AtomicUsize::new(0));
1532        let (_, cx) = cx.add_window_view({
1533            let renders = renders.clone();
1534            move |_, _| StatelessMarkdownRoot { renders }
1535        });
1536        let cx: &mut VisualTestContext = cx;
1537
1538        cx.run_until_parked();
1539        cx.update(|window, cx| window.draw(cx).clear(cx));
1540        let renders_after_redraw = renders.load(Ordering::Relaxed);
1541        cx.run_until_parked();
1542        assert_eq!(
1543            renders.load(Ordering::Relaxed),
1544            renders_after_redraw,
1545            "an unchanged TextView must not schedule another render after its parser is rebuilt",
1546        );
1547    }
1548
1549    #[gpui::test]
1550    fn markdown_data_url_image_is_decoded_inline(cx: &mut TestAppContext) {
1551        use gpui::{Image, ImageFormat, ImageSource};
1552
1553        // A 1x1 red PNG.
1554        const PNG_BASE64: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==";
1555
1556        cx.update(crate::init);
1557        let markdown = format!("Inline ![dot](data:image/png;base64,{PNG_BASE64}) image");
1558        let (_, cx) = cx.add_window_view(|_, cx| TextViewTestRoot::new(&markdown, cx));
1559        let cx: &mut VisualTestContext = cx;
1560
1561        cx.run_until_parked();
1562        cx.update(|window, cx| window.draw(cx).clear(cx));
1563
1564        // `Image` keys the asset system by a hash of its bytes, so rebuilding it
1565        // from the same body finds the entry the text view's `img` registered
1566        // when it rendered — proof the body was decoded in place instead of
1567        // being fetched over HTTP.
1568        let bytes = data_url::DataUrl::process(&format!("data:image/png;base64,{PNG_BASE64}"))
1569            .unwrap()
1570            .decode_to_vec()
1571            .unwrap()
1572            .0;
1573        let image = Arc::new(Image::from_bytes(ImageFormat::Png, bytes));
1574        assert!(
1575            cx.update(|_, cx| ImageSource::Image(image).is_asset_cached(cx)),
1576            "the data URL image must be handed to GPUI as decoded bytes",
1577        );
1578    }
1579
1580    #[gpui::test]
1581    fn unstyled_text_view_uses_base_tokens_for_link_and_input_selection(cx: &mut TestAppContext) {
1582        cx.update(crate::init);
1583        cx.update(|cx| {
1584            let colors = &mut crate::Theme::global_mut(cx).tokens.colors;
1585            colors.primary = gpui::rgb(0x55aaff).into();
1586            colors.selection = gpui::rgb(0x335577).into();
1587        });
1588        let (root, cx) = cx.add_window_view(|_, cx| TextViewTestRoot::new("[link](url)", cx));
1589        let cx: &mut VisualTestContext = cx;
1590
1591        cx.run_until_parked();
1592        root.read_with(cx, |root, cx| {
1593            let style = &root.text_view.read(cx).text_view_style;
1594            assert_eq!(style.link(), gpui::rgb(0x55aaff).into());
1595            assert_eq!(style.selection(), gpui::rgb(0x335577).into());
1596        });
1597    }
1598
1599    impl TextViewTestRoot {
1600        fn new(text: &str, cx: &mut Context<Self>) -> Self {
1601            let text = text.to_string();
1602            let text_view = cx.new(|cx| TextViewState::markdown(&text, cx));
1603            Self { text_view }
1604        }
1605    }
1606
1607    impl Render for TextViewTestRoot {
1608        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1609            div()
1610                .w(px(160.))
1611                .child(
1612                    div()
1613                        .h(px(24.))
1614                        .overflow_hidden()
1615                        .child(TextView::new(&self.text_view).selectable(true)),
1616                )
1617                .child(div().h(px(40.)).child("footer"))
1618        }
1619    }
1620
1621    struct TableSelectionTestRoot {
1622        text_view: Entity<TextViewState>,
1623    }
1624
1625    impl Render for TableSelectionTestRoot {
1626        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1627            div()
1628                .debug_selector(|| "table-selection-root".into())
1629                .w(px(520.))
1630                .child(crate::TextSelectionLayer)
1631                .child(TextView::new(&self.text_view))
1632        }
1633    }
1634
1635    #[gpui::test]
1636    fn table_drag_selection_settles_without_requesting_idle_frames(cx: &mut TestAppContext) {
1637        cx.update(crate::init);
1638        let (_, cx) = cx.add_window_view(|_, cx| TableSelectionTestRoot {
1639            text_view: cx.new(|cx| {
1640                TextViewState::markdown(
1641                    "| Header 1 | Header 2 |\n| --- | --- |\n| Cell A | Cell B |\n| Cell C | Cell D |",
1642                    cx,
1643                )
1644            }),
1645        });
1646        let cx: &mut VisualTestContext = cx;
1647
1648        cx.run_until_parked();
1649        let bounds = cx
1650            .debug_bounds("table-selection-root")
1651            .expect("table bounds");
1652        let start = point(bounds.left() + px(24.), bounds.top() + px(16.));
1653        let end = point(bounds.right() - px(24.), bounds.bottom() - px(16.));
1654        cx.simulate_mouse_down(start, MouseButton::Left, Modifiers::default());
1655        cx.simulate_mouse_move(end, MouseButton::Left, Modifiers::default());
1656        cx.simulate_mouse_up(end, MouseButton::Left, Modifiers::default());
1657
1658        assert!(cx.update(|window, cx| crate::TextSelection::has_selection(window, cx)));
1659        assert_eq!(
1660            cx.update(|window, cx| window.simulate_next_frame(cx)),
1661            0,
1662            "finished table selection must not continuously request frames"
1663        );
1664    }
1665
1666    struct InlineImageTextViewTestRoot {
1667        text_view: Entity<TextViewState>,
1668    }
1669
1670    impl InlineImageTextViewTestRoot {
1671        fn new(cx: &mut Context<Self>) -> Self {
1672            let text_view = cx.new(|cx| {
1673                TextViewState::markdown(
1674                    "Build Status ![inline image](https://example.com/image.svg) after",
1675                    cx,
1676                )
1677            });
1678            Self { text_view }
1679        }
1680    }
1681
1682    impl Render for InlineImageTextViewTestRoot {
1683        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1684            div()
1685                .w(px(420.))
1686                .child(TextView::new(&self.text_view).selectable(true))
1687        }
1688    }
1689
1690    #[gpui::test]
1691    fn inline_image_keeps_surrounding_text_on_same_line(cx: &mut TestAppContext) {
1692        cx.update(crate::init);
1693        let (content, cx) = cx.add_window_view(|_, cx| InlineImageTextViewTestRoot::new(cx));
1694        let cx: &mut VisualTestContext = cx;
1695
1696        cx.run_until_parked();
1697        cx.update(|window, cx| {
1698            let _ = window.draw(cx);
1699        });
1700
1701        let inline_bounds = content.read_with(cx, |content, cx| {
1702            content.text_view.read(cx).selection_adapter.text_bounds()
1703        });
1704
1705        assert_eq!(inline_bounds.len(), 2);
1706        assert_eq!(
1707            inline_bounds[0].top(),
1708            inline_bounds[1].top(),
1709            "text before and after an inline image should share a rendered line"
1710        );
1711        assert!(
1712            inline_bounds[1].left() - inline_bounds[0].right() > px(8.),
1713            "inline image should reserve horizontal space in the text layout"
1714        );
1715        assert!(
1716            inline_bounds[1].left() - inline_bounds[0].right() < px(40.),
1717            "unloaded inline image fallback should stay generic and compact"
1718        );
1719    }
1720
1721    #[gpui::test]
1722    fn inline_html_image_after_newline_does_not_panic(cx: &mut TestAppContext) {
1723        cx.update(crate::init);
1724        let (_, cx) = cx.add_window_view(|_, cx| {
1725            TextViewTestRoot::new(
1726                "Hi\n[<img src=\"https://example.com/image.svg\">](https://google.com/)",
1727                cx,
1728            )
1729        });
1730        let cx: &mut VisualTestContext = cx;
1731
1732        cx.run_until_parked();
1733        cx.update(|window, cx| {
1734            let _ = window.draw(cx);
1735        });
1736    }
1737
1738    #[gpui::test]
1739    fn list_item_renders_fenced_code_block_at_document_width(cx: &mut TestAppContext) {
1740        struct ListItemBlockRoot;
1741
1742        impl Render for ListItemBlockRoot {
1743            fn render(
1744                &mut self,
1745                _window: &mut Window,
1746                _cx: &mut Context<Self>,
1747            ) -> impl IntoElement {
1748                div().w(px(840.)).h(px(400.)).child(
1749                    crate::h_resizable("markdown-width-test")
1750                        .child(crate::resizable_panel().child(div()))
1751                        .child(crate::resizable_panel().child(
1752                            TextView::markdown(
1753                                "list-with-code",
1754                                "1. List item\n   ```rust\n   nested code\n   ```\n\n```rust\ntop-level code\n```",
1755                            )
1756                            .code_block_actions(|code_block, _, _| {
1757                                let selector = if code_block.code().contains("nested") {
1758                                    "nested-code-action"
1759                                } else {
1760                                    "top-level-code-action"
1761                                };
1762                                div()
1763                                    .debug_selector(move || selector.into())
1764                                    .child("Copy")
1765                            })
1766                            .scrollable(true)
1767                            .p_5()
1768                            .flex_none(),
1769                        )),
1770                )
1771            }
1772        }
1773
1774        cx.update(crate::init);
1775        let (_, cx) = cx.add_window_view(|_, _| ListItemBlockRoot);
1776        let cx: &mut VisualTestContext = cx;
1777
1778        cx.run_until_parked();
1779        cx.update(|window, cx| {
1780            let _ = window.draw(cx);
1781        });
1782
1783        let nested_action = cx.debug_bounds("nested-code-action").unwrap();
1784        let top_level_action = cx.debug_bounds("top-level-code-action").unwrap();
1785        assert!(
1786            top_level_action.right() - nested_action.right() < px(32.),
1787            "nested code block should fill the list item's available width"
1788        );
1789    }
1790
1791    /// Height cap of the scrolling code block in [`ScrollingCodeRoot`].
1792    const CODE_HEIGHT: Pixels = px(60.);
1793
1794    struct ScrollingCodeRoot {
1795        text_view: Entity<TextViewState>,
1796        actions: bool,
1797    }
1798
1799    impl Render for ScrollingCodeRoot {
1800        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1801            let mut code_block = StyleRefinement::default().max_h(CODE_HEIGHT);
1802            code_block.overflow.y = Some(Overflow::Scroll);
1803
1804            div().w(px(320.)).h(px(200.)).child(
1805                TextView::new(&self.text_view)
1806                    .style(TextViewStyle::default().with_code_block(code_block))
1807                    .when(self.actions, |this| {
1808                        this.code_block_actions(|_, _, _| {
1809                            div().debug_selector(|| "code-action".into()).child("Copy")
1810                        })
1811                    })
1812                    .scrollable(true),
1813            )
1814        }
1815    }
1816
1817    #[gpui::test]
1818    fn scrolling_code_block_keeps_wheel_from_the_list(cx: &mut TestAppContext) {
1819        cx.update(crate::init);
1820        let code = (0..50).map(|i| format!("line {i}\n")).collect::<String>();
1821        let tail = "Paragraph after the code.\n\n".repeat(20);
1822        let markdown = format!("```\n{code}```\n\n{tail}");
1823
1824        // Scrolling must not depend on `code_block_actions`, which is what
1825        // gives a non-scrolling block its id.
1826        for actions in [false, true] {
1827            let (root, cx) = cx.add_window_view(|_, cx| ScrollingCodeRoot {
1828                text_view: cx.new(|cx| TextViewState::markdown(&markdown, cx)),
1829                actions,
1830            });
1831            let cx: &mut VisualTestContext = cx;
1832            let draw = |cx: &mut VisualTestContext| {
1833                cx.run_until_parked();
1834                cx.update(|window, cx| window.draw(cx).clear(cx));
1835            };
1836            let step = px(20.);
1837            let wheel_at = |cx: &mut VisualTestContext, y: Pixels| {
1838                cx.simulate_event(gpui::ScrollWheelEvent {
1839                    position: point(px(20.), y),
1840                    delta: gpui::ScrollDelta::Pixels(point(px(0.), -step)),
1841                    ..Default::default()
1842                });
1843            };
1844            // The code block is first in the view, and painted lines are
1845            // clipped to its viewport: the first code line's bottom moves with
1846            // the code, the first line below the viewport moves with the list.
1847            let edges = |cx: &mut VisualTestContext| {
1848                let lines = root.read_with(cx, |root, cx| {
1849                    root.text_view.read(cx).selection_adapter.text_bounds()
1850                });
1851                let paragraph = lines.iter().find(|line| line.top() >= CODE_HEIGHT).unwrap();
1852                (lines[0].bottom(), paragraph.top())
1853            };
1854            draw(cx);
1855            let (code_bottom, paragraph_top) = edges(cx);
1856            let action = cx.debug_bounds("code-action");
1857
1858            // Over the code block: the code scrolls, the list and the pinned
1859            // actions stay put.
1860            wheel_at(cx, CODE_HEIGHT / 2.);
1861            draw(cx);
1862            assert_eq!(
1863                edges(cx),
1864                (code_bottom - step, paragraph_top),
1865                "actions: {actions}"
1866            );
1867            assert_eq!(cx.debug_bounds("code-action"), action);
1868
1869            // Below the code block the list still scrolls. Only inequality
1870            // holds: once the paragraph moves into the code's band, `edges`
1871            // picks the next one down.
1872            wheel_at(cx, CODE_HEIGHT * 2.5);
1873            draw(cx);
1874            assert_ne!(edges(cx).1, paragraph_top, "actions: {actions}");
1875        }
1876    }
1877
1878    /// Draw a Markdown table with a `table_actions` hook installed, and return
1879    /// the painted bounds of the actions element plus the data it received.
1880    /// `scroll` opts into the horizontally scrollable table layout.
1881    fn draw_table_with_actions(
1882        cx: &mut TestAppContext,
1883        scroll: bool,
1884    ) -> (Bounds<Pixels>, TableData) {
1885        use std::sync::{Arc, Mutex};
1886
1887        struct TableRoot {
1888            scroll: bool,
1889            captured: Arc<Mutex<Vec<TableData>>>,
1890        }
1891
1892        impl Render for TableRoot {
1893            fn render(
1894                &mut self,
1895                _window: &mut Window,
1896                _cx: &mut Context<Self>,
1897            ) -> impl IntoElement {
1898                let captured = self.captured.clone();
1899                let mut table_style = StyleRefinement::default();
1900                if self.scroll {
1901                    table_style.overflow.x = Some(Overflow::Scroll);
1902                }
1903
1904                div().w(px(320.)).child(
1905                    TextView::markdown(
1906                        "table-actions",
1907                        "| Name | Age |\n|:--|--:|\n| Alice | 30 |\n| Bob | 41 |",
1908                    )
1909                    .style(TextViewStyle::default().with_table(table_style))
1910                    .table_actions(move |table, _, _| {
1911                        if let Ok(mut captured) = captured.lock() {
1912                            captured.push(table.clone());
1913                        }
1914                        div().debug_selector(|| "table-action".into()).child("Copy")
1915                    }),
1916                )
1917            }
1918        }
1919
1920        cx.update(crate::init);
1921        let captured = Arc::new(Mutex::new(Vec::new()));
1922        let (_, cx) = cx.add_window_view({
1923            let captured = captured.clone();
1924            move |_, _| TableRoot { scroll, captured }
1925        });
1926        let cx: &mut VisualTestContext = cx;
1927
1928        cx.run_until_parked();
1929        cx.update(|window, cx| {
1930            let _ = window.draw(cx);
1931        });
1932
1933        let bounds = cx
1934            .debug_bounds("table-action")
1935            .expect("table actions should be painted");
1936        let data = captured
1937            .lock()
1938            .expect("captured table data")
1939            .last()
1940            .cloned()
1941            .expect("table actions hook should receive the table");
1942
1943        (bounds, data)
1944    }
1945
1946    #[gpui::test]
1947    fn table_actions_render_below_the_table(cx: &mut TestAppContext) {
1948        for scroll in [false, true] {
1949            let (bounds, data) = draw_table_with_actions(cx, scroll);
1950
1951            // Header plus two data rows are painted above the actions row.
1952            assert!(
1953                bounds.top() > px(40.),
1954                "actions should sit below the table (scroll: {scroll}), got {:?}",
1955                bounds.top()
1956            );
1957            assert_eq!(data.headers, vec!["Name", "Age"]);
1958            assert_eq!(data.rows, vec![vec!["Alice", "30"], vec!["Bob", "41"]]);
1959            assert_eq!(
1960                data.markdown,
1961                "| Name | Age |\n| :-- | --: |\n| Alice | 30 |\n| Bob | 41 |"
1962            );
1963            assert_eq!(data.span, Some(0..52));
1964        }
1965    }
1966
1967    #[test]
1968    fn plugin_accepts_text_view_plugins_beyond_markdown() {
1969        let view = TextView::markdown("plugin-test", "").plugin(DummyTextViewPlugin);
1970
1971        assert!(view.selectable);
1972    }
1973
1974    #[test]
1975    fn syntax_highlighting_is_opt_in() {
1976        let default_view = TextView::markdown("default-code", "```rust\nfn main() {}\n```");
1977        assert!(default_view.code_block_highlighter.is_none());
1978
1979        let view = default_view.code_block_highlighter(|block| {
1980            vec![(
1981                0..block.code().len(),
1982                gpui::HighlightStyle {
1983                    color: Some(gpui::rgb(0x3366ff).into()),
1984                    ..Default::default()
1985                },
1986            )]
1987        });
1988        assert!(view.code_block_highlighter.is_some());
1989    }
1990
1991    #[gpui::test]
1992    fn clipped_markdown_link_does_not_open(cx: &mut TestAppContext) {
1993        cx.update(crate::init);
1994        let (_, cx) = cx.add_window_view(|_, cx| {
1995            TextViewTestRoot::new("visible\n\n[hidden](https://example.com)", cx)
1996        });
1997        let cx: &mut VisualTestContext = cx;
1998
1999        cx.simulate_click(point(px(10.), px(34.)), Modifiers::default());
2000
2001        assert_eq!(cx.opened_url(), None);
2002    }
2003
2004    struct MaxLinesTestRoot {
2005        text_view: Entity<TextViewState>,
2006        max_lines: usize,
2007    }
2008
2009    impl MaxLinesTestRoot {
2010        fn new(text: &str, max_lines: usize, cx: &mut Context<Self>) -> Self {
2011            let text_view = cx.new(|cx| TextViewState::markdown(text, cx));
2012            Self {
2013                text_view,
2014                max_lines,
2015            }
2016        }
2017    }
2018
2019    impl Render for MaxLinesTestRoot {
2020        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
2021            div()
2022                .w(px(200.))
2023                .child(TextView::new(&self.text_view).max_lines(self.max_lines))
2024        }
2025    }
2026
2027    #[test]
2028    fn the_clip_only_moves_for_a_straddling_glyph_line() {
2029        use super::line_safe_clip_bottom;
2030        use crate::text::state::LineSpan;
2031
2032        let spans = [
2033            // Lines end at 20 / 40 / 60.
2034            LineSpan {
2035                top: px(0.),
2036                bottom: px(60.),
2037                line_height: px(20.),
2038            },
2039            // A second block after an 8px gap; lines end at 88 / 108 / 128.
2040            LineSpan {
2041                top: px(68.),
2042                bottom: px(128.),
2043                line_height: px(20.),
2044            },
2045        ];
2046
2047        // Content continues well past the box in every case but the last.
2048        let below = px(400.);
2049
2050        // A box ending inside the line 88..108 leaves that line out whole.
2051        assert_eq!(
2052            line_safe_clip_bottom(&spans, px(100.), below),
2053            Some(px(88.))
2054        );
2055
2056        // A box ending on a line boundary has nothing to pull the clip up for.
2057        assert_eq!(line_safe_clip_bottom(&spans, px(88.), below), None);
2058
2059        // A strip below the last line shorter than a line — the border and
2060        // padding a block leads with — is not worth showing.
2061        assert_eq!(line_safe_clip_bottom(&spans, px(64.), below), Some(px(60.)));
2062
2063        // One taller than a line is: whatever crosses the edge keeps the part
2064        // that fits rather than leaving the box half empty.
2065        let one_block = [LineSpan {
2066            top: px(0.),
2067            bottom: px(60.),
2068            line_height: px(20.),
2069        }];
2070        assert_eq!(line_safe_clip_bottom(&one_block, px(200.), below), None);
2071
2072        // Nothing crosses the edge at all: the space under the last line is
2073        // the box's own, not a scrap of something below.
2074        assert_eq!(line_safe_clip_bottom(&spans, px(130.), px(128.)), None);
2075    }
2076
2077    #[test]
2078    fn a_line_taller_than_the_budget_keeps_the_part_that_fits() {
2079        use super::line_safe_clip_bottom;
2080        use crate::text::state::LineSpan;
2081
2082        // A heading line of 28px, in a box capped at one 26px body line.
2083        let heading = [LineSpan {
2084            top: px(70.),
2085            bottom: px(98.),
2086            line_height: px(28.),
2087        }];
2088
2089        assert_eq!(line_safe_clip_bottom(&heading, px(96.), px(400.)), None);
2090    }
2091
2092    #[test]
2093    fn the_clip_does_not_stop_on_a_row_of_border_and_padding() {
2094        use super::line_safe_clip_bottom;
2095        use crate::text::state::LineSpan;
2096
2097        // Two table rows, each one line of text, 9px of border and padding
2098        // between them.
2099        let rows = [
2100            LineSpan {
2101                top: px(100.),
2102                bottom: px(126.),
2103                line_height: px(26.),
2104            },
2105            LineSpan {
2106                top: px(135.),
2107                bottom: px(161.),
2108                line_height: px(26.),
2109            },
2110        ];
2111
2112        // Leaving out the second row's text would strand the 9px it leads
2113        // with, so the clip goes back to the row above it.
2114        assert_eq!(
2115            line_safe_clip_bottom(&rows, px(148.), px(400.)),
2116            Some(px(126.))
2117        );
2118    }
2119
2120    /// A clamped view nested the way an application nests one: inside a card,
2121    /// inside a region that fills a window of a known height. The height an
2122    /// ancestor hands down must not reach the clamped content and hide the
2123    /// overflow the clamp measures — with the content stretched to the capped
2124    /// box, nothing looks clipped and lines get cut in half.
2125    struct ClampedPageRoot {
2126        text_view: Entity<TextViewState>,
2127        max_lines: usize,
2128    }
2129
2130    impl Render for ClampedPageRoot {
2131        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
2132            use crate::{h_flex, v_flex};
2133
2134            v_flex()
2135                .size_full()
2136                .p_4()
2137                .gap_4()
2138                .child(h_flex().max_w(px(480.)).gap_3().child("header"))
2139                .child(
2140                    v_flex()
2141                        .flex_1()
2142                        .min_h_0()
2143                        .gap_4()
2144                        .id("clamped-page-scroll")
2145                        .child(
2146                            v_flex()
2147                                .max_w(px(480.))
2148                                .p_3()
2149                                .gap_2()
2150                                .child(TextView::new(&self.text_view).max_lines(self.max_lines)),
2151                        )
2152                        .overflow_y_scroll(),
2153                )
2154        }
2155    }
2156
2157    #[gpui::test]
2158    fn max_lines_measures_overflow_inside_a_sized_page(cx: &mut TestAppContext) {
2159        cx.update(crate::init);
2160        let (root, cx) = cx.add_window_view(|_, cx| {
2161            let text_view = cx.new(|cx| {
2162                TextViewState::markdown(
2163                    "first\n\nsecond\n\nthird\n\nfourth\n\nfifth\n\nsixth\n\nseventh",
2164                    cx,
2165                )
2166            });
2167            ClampedPageRoot {
2168                text_view,
2169                max_lines: 3,
2170            }
2171        });
2172        let cx: &mut VisualTestContext = cx;
2173
2174        assert!(root.read_with(cx, |root, cx| root.text_view.read(cx).is_clamped()));
2175    }
2176
2177    #[gpui::test]
2178    fn max_lines_clamps_overflowing_content(cx: &mut TestAppContext) {
2179        cx.update(crate::init);
2180        let (root, cx) = cx.add_window_view(|_, cx| {
2181            MaxLinesTestRoot::new(
2182                "first\n\nsecond\n\nthird\n\nfourth\n\nfifth\n\nsixth",
2183                2,
2184                cx,
2185            )
2186        });
2187        let cx: &mut VisualTestContext = cx;
2188
2189        assert!(root.read_with(cx, |root, cx| root.text_view.read(cx).is_clamped()));
2190    }
2191
2192    #[gpui::test]
2193    fn max_lines_leaves_short_content_unclamped(cx: &mut TestAppContext) {
2194        cx.update(crate::init);
2195        let (root, cx) = cx.add_window_view(|_, cx| MaxLinesTestRoot::new("only line", 3, cx));
2196        let cx: &mut VisualTestContext = cx;
2197
2198        assert!(!root.read_with(cx, |root, cx| root.text_view.read(cx).is_clamped()));
2199    }
2200
2201    #[gpui::test]
2202    fn heading_refinement_changes_rendered_heading_geometry(cx: &mut TestAppContext) {
2203        struct HeadingStyleRoot;
2204
2205        impl Render for HeadingStyleRoot {
2206            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2207                div()
2208                    .flex()
2209                    .items_start()
2210                    .child(
2211                        div()
2212                            .debug_selector(|| "default-h1".into())
2213                            .child(TextView::markdown("default-h1-view", "# Heading")),
2214                    )
2215                    .child(div().debug_selector(|| "custom-h1".into()).child(
2216                        TextView::markdown("custom-heading-view", "# Heading").style(
2217                            TextViewStyle::default().with_heading(|level| {
2218                                if level == 1 {
2219                                    StyleRefinement::default().pb(rems(2.))
2220                                } else {
2221                                    StyleRefinement::default()
2222                                }
2223                            }),
2224                        ),
2225                    ))
2226                    .child(
2227                        div()
2228                            .debug_selector(|| "default-h2".into())
2229                            .child(TextView::markdown("default-h2-view", "## Heading")),
2230                    )
2231                    .child(div().debug_selector(|| "custom-h2".into()).child(
2232                        TextView::markdown("custom-h2-view", "## Heading").style(
2233                            TextViewStyle::default().with_heading(|level| {
2234                                if level == 1 {
2235                                    StyleRefinement::default().pb(rems(2.))
2236                                } else {
2237                                    StyleRefinement::default()
2238                                }
2239                            }),
2240                        ),
2241                    ))
2242            }
2243        }
2244
2245        cx.update(crate::init);
2246        let (_, cx) = cx.add_window_view(|_, _| HeadingStyleRoot);
2247        cx.update(|window, cx| window.draw(cx).clear(cx));
2248
2249        let default_h1 = cx.debug_bounds("default-h1").unwrap();
2250        let custom_h1 = cx.debug_bounds("custom-h1").unwrap();
2251        let default_h2 = cx.debug_bounds("default-h2").unwrap();
2252        let custom_h2 = cx.debug_bounds("custom-h2").unwrap();
2253        assert!(custom_h1.size.height > default_h1.size.height);
2254        assert_eq!(custom_h2.size.height, default_h2.size.height);
2255    }
2256
2257    #[gpui::test]
2258    fn max_lines_disables_links_hidden_by_the_clamp(cx: &mut TestAppContext) {
2259        cx.update(crate::init);
2260        let (_, cx) = cx.add_window_view(|_, cx| {
2261            MaxLinesTestRoot::new(
2262                "first\n\nsecond\n\nthird\n\n[hidden](https://example.com)",
2263                2,
2264                cx,
2265            )
2266        });
2267        let cx: &mut VisualTestContext = cx;
2268
2269        // Click far below the clamped box, where the link would sit unclamped.
2270        cx.simulate_click(point(px(10.), px(150.)), Modifiers::default());
2271
2272        assert_eq!(cx.opened_url(), None);
2273    }
2274
2275    #[gpui::test]
2276    fn scaled_inline_code_keeps_links_and_drag_selection(cx: &mut TestAppContext) {
2277        struct SelectionRoot {
2278            text_view: Entity<TextViewState>,
2279            format: crate::text::SelectionFormat,
2280        }
2281        impl Render for SelectionRoot {
2282            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2283                div()
2284                    .w(px(160.))
2285                    .child(crate::TextSelectionLayer)
2286                    .child(TextView::new(&self.text_view).selection_format(self.format))
2287            }
2288        }
2289        cx.update(crate::init);
2290        let (view, cx) = cx.add_window_view(|_, cx| SelectionRoot {
2291            format: crate::text::SelectionFormat::Plain,
2292            text_view: cx
2293                .new(|cx| TextViewState::markdown("[`code`](https://example.com) after", cx)),
2294        });
2295        let cx: &mut VisualTestContext = cx;
2296        cx.run_until_parked();
2297        cx.simulate_click(point(px(10.), px(10.)), Modifiers::default());
2298        assert_eq!(cx.opened_url(), Some("https://example.com".to_string()));
2299        // Just past the code span's leading padding, on its first glyph.
2300        cx.simulate_mouse_down(
2301            point(px(5.), px(8.)),
2302            MouseButton::Left,
2303            Modifiers::default(),
2304        );
2305        cx.update(|window, cx| {
2306            let _ = window.draw(cx);
2307        });
2308        cx.simulate_mouse_move(
2309            point(px(155.), px(20.)),
2310            Some(MouseButton::Left),
2311            Modifiers::default(),
2312        );
2313        cx.update(|window, cx| {
2314            let _ = window.draw(cx);
2315        });
2316        cx.simulate_mouse_up(
2317            point(px(155.), px(20.)),
2318            MouseButton::Left,
2319            Modifiers::default(),
2320        );
2321        cx.update(|window, cx| {
2322            let _ = window.draw(cx);
2323        });
2324        let selected = view.read_with(cx, |view, cx| view.text_view.read(cx).selected_text());
2325        assert_eq!(selected.trim(), "code after");
2326        view.update(cx, |view, cx| {
2327            view.format = crate::text::SelectionFormat::Source;
2328            cx.notify();
2329        });
2330        cx.update(|window, cx| {
2331            let _ = window.draw(cx);
2332        });
2333        let selected = view.read_with(cx, |view, cx| view.text_view.read(cx).selected_text());
2334        assert_eq!(selected.trim(), "[`code`](https://example.com) after");
2335    }
2336
2337    /// Inline-code Markdown takes the deferred `InlineFlow` path. Its layout
2338    /// must use the heading's resolved typography rather than the ambient body
2339    /// style that remains after the heading's style stack has been popped.
2340    ///
2341    /// This is a real request-layout → measured-layout → prepaint → paint test:
2342    /// the deterministic text system makes bold glyphs wider than body glyphs.
2343    /// Before the fix, the code heading was allocated using normal body metrics
2344    /// but its fragments painted bold, so the wrapped heading text crossed into
2345    /// the following paragraph.
2346    #[test]
2347    fn inline_code_heading_reserves_painted_wrapped_lines_and_list_baseline() {
2348        use crate::text::inline::test_fonts::{MONO, WideMonoTextSystem};
2349        use gpui::{TestApp, rems};
2350
2351        const MARKDOWN: &str = "The same words with inline code.\n\nThe same words with `inline code`.\n\n- The same words with inline code.\n- The same words with `inline code`.\n\n# Heading with inline code\n# Heading with `inline code`\n\nthis is a test";
2352
2353        struct MarkdownRoot {
2354            text_view: Entity<TextViewState>,
2355            width: Pixels,
2356            preview_zoom: f32,
2357        }
2358
2359        impl Render for MarkdownRoot {
2360            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2361                div()
2362                    .w(self.width)
2363                    // Match example-markdown: preview zoom changes TextView's
2364                    // inherited text size, not the window rem size.
2365                    .text_size(rems(self.preview_zoom))
2366                    .child(crate::TextSelectionLayer)
2367                    .child(TextView::new(&self.text_view).selectable(true))
2368            }
2369        }
2370
2371        fn draw(
2372            app: &mut TestApp,
2373            width: Pixels,
2374            preview_zoom: f32,
2375        ) -> (Bounds<Pixels>, Vec<Bounds<Pixels>>, String) {
2376            let mut window = app.open_window(|_, cx| MarkdownRoot {
2377                text_view: cx.new(|cx| TextViewState::markdown(MARKDOWN, cx)),
2378                width,
2379                preview_zoom,
2380            });
2381            window.draw();
2382            app.run_until_parked();
2383            window.draw();
2384            window.update(|root, _, cx| {
2385                root.text_view.update(cx, |state, cx| state.select_all(cx));
2386            });
2387            window.draw();
2388            window.read(|root, cx| {
2389                let state = root.text_view.read(cx);
2390                (
2391                    state.bounds(),
2392                    state.selection_adapter.text_bounds(),
2393                    state.selected_text(),
2394                )
2395            })
2396        }
2397
2398        let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
2399        app.update(|cx| {
2400            crate::init(cx);
2401            crate::Theme::global_mut(cx).tokens.typography.mono = MONO.into();
2402        });
2403
2404        for (case, width, preview_zoom) in [
2405            // At the root 16px size the plain heading fits, while bold inline
2406            // fragments need their own measured widths.
2407            ("default-16px", px(600.), 1.),
2408            // A narrow view exercises intentional heading wrapping.
2409            ("narrow-16px", px(320.), 1.),
2410            // Preview zoom applies through `.text_size(rems(zoom))`.
2411            ("zoom-1.25", px(600.), 1.25),
2412        ] {
2413            let (view_bounds, text_bounds, selected_text) = draw(&mut app, width, preview_zoom);
2414            assert!(
2415                text_bounds.len() > 8,
2416                "{case}: expected all markdown fragments"
2417            );
2418            assert_eq!(
2419                selected_text.trim(),
2420                "The same words with inline code.\nThe same words with inline code.\nThe same words with inline code.\nThe same words with inline code.\nHeading with inline code\nHeading with inline code\nthis is a test",
2421                "{case}: wrapped inline-code text was lost or duplicated"
2422            );
2423            let following = text_bounds.last().expect("following paragraph must paint");
2424            let painted_bottom = text_bounds
2425                .iter()
2426                .map(|bounds| bounds.bottom())
2427                .max()
2428                .expect("markdown must paint text");
2429            // This is intentionally not a fragment-count or selection-only
2430            // assertion: `text_bounds` are the shaped Inline layouts produced
2431            // during paint. No painted wrapped line may reach the following
2432            // paragraph's origin, and the TextView allocation must contain the
2433            // complete painted document.
2434            assert!(
2435                text_bounds[..text_bounds.len() - 1]
2436                    .iter()
2437                    .all(|bounds| bounds.bottom() <= following.top()),
2438                "{case}: heading/list text painted through the following paragraph;                  following={following:?}, text_bounds={text_bounds:?}, view={view_bounds:?}"
2439            );
2440            assert!(
2441                painted_bottom <= view_bounds.bottom(),
2442                "{case}: TextView height did not reserve its painted text;                  painted_bottom={painted_bottom:?}, view={view_bounds:?}"
2443            );
2444        }
2445    }
2446
2447    #[test]
2448    fn ordered_markdown_list_start_reaches_layout_marker() {
2449        use crate::text::inline::test_fonts::{WideMonoTextSystem, record_shaped_lines};
2450        use gpui::TestApp;
2451
2452        struct MarkdownRoot {
2453            text_view: Entity<TextViewState>,
2454        }
2455
2456        impl Render for MarkdownRoot {
2457            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2458                div().w(px(400.)).child(TextView::new(&self.text_view))
2459            }
2460        }
2461
2462        #[derive(Clone, Copy)]
2463        enum Format {
2464            Markdown,
2465            Html,
2466        }
2467
2468        fn shaped_markers(format: Format, source: &str) -> Vec<String> {
2469            let (_, shaped_lines) = record_shaped_lines(|| {
2470                let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
2471                app.update(crate::init);
2472                let mut window = app.open_window(|_, cx| MarkdownRoot {
2473                    text_view: cx.new(|cx| match format {
2474                        Format::Markdown => TextViewState::markdown(source, cx),
2475                        Format::Html => TextViewState::html(source, cx),
2476                    }),
2477                });
2478                window.draw();
2479                app.run_until_parked();
2480                window.draw();
2481            });
2482
2483            let mut markers = shaped_lines
2484                .into_iter()
2485                .filter(|line| line.len() <= 3 && line.ends_with('.'))
2486                .collect::<Vec<_>>();
2487            markers.dedup();
2488            markers
2489        }
2490
2491        let starts_at_one = "1. one\n2. two";
2492        assert_eq!(
2493            shaped_markers(Format::Markdown, starts_at_one),
2494            ["1.", "2."]
2495        );
2496        assert_eq!(
2497            shaped_markers(Format::Html, "<ol><li>one</li><li>two</li></ol>"),
2498            ["1.", "2."]
2499        );
2500
2501        assert_eq!(
2502            shaped_markers(Format::Markdown, "3. hello\n4. world"),
2503            ["3.", "4."]
2504        );
2505
2506        let nested_starts_at_four = "1. outer\n\n   4. nested\n   5. again";
2507        assert_eq!(
2508            shaped_markers(Format::Markdown, nested_starts_at_four),
2509            ["1.", "D.", "E."]
2510        );
2511
2512        let nested_starts_at_zero = "1. outer\n\n   0. zero";
2513        assert_eq!(
2514            shaped_markers(Format::Markdown, nested_starts_at_zero),
2515            ["1.", "0."]
2516        );
2517    }
2518
2519    /// A line with inline code takes `InlineFlow` and a plain line takes the
2520    /// ordinary text path; both must be the same height, or a list with one
2521    /// code item is unevenly spaced (#3162). The fractional scale factor and
2522    /// zooms give line heights that are not whole logical pixels.
2523    #[test]
2524    fn inline_code_line_is_as_tall_as_a_plain_line() {
2525        use crate::text::inline::test_fonts::{MONO, WideMonoTextSystem};
2526        use gpui::{TestApp, rems};
2527
2528        struct LineRoot {
2529            plain: Entity<TextViewState>,
2530            code: Entity<TextViewState>,
2531            preview_zoom: f32,
2532        }
2533
2534        impl Render for LineRoot {
2535            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2536                div()
2537                    .w(px(600.))
2538                    .text_size(rems(self.preview_zoom))
2539                    .child(TextView::new(&self.plain))
2540                    .child(TextView::new(&self.code))
2541            }
2542        }
2543
2544        let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
2545        app.update(|cx| {
2546            crate::init(cx);
2547            crate::Theme::global_mut(cx).tokens.typography.mono = MONO.into();
2548        });
2549        for scale_factor in [1.6, 2.] {
2550            for preview_zoom in [1., 1.25] {
2551                let mut window = app.open_window(|_, cx| LineRoot {
2552                    plain: cx.new(|cx| TextViewState::markdown("plain body words", cx)),
2553                    code: cx.new(|cx| TextViewState::markdown("plain `code` words", cx)),
2554                    preview_zoom,
2555                });
2556                window.simulate_scale_factor_change(scale_factor);
2557                window.draw();
2558                app.run_until_parked();
2559                window.draw();
2560                let (plain, code) = window.read(|root, cx| {
2561                    (
2562                        root.plain.read(cx).bounds().size.height,
2563                        root.code.read(cx).bounds().size.height,
2564                    )
2565                });
2566                assert_eq!(code, plain, "scale {scale_factor}, zoom {preview_zoom}");
2567            }
2568        }
2569    }
2570
2571    /// The code-bearing list item takes `InlineFlow`; the plain item takes the
2572    /// ordinary text path. Their first body glyphs must begin at the same row
2573    /// position relative to their own TextView origins.
2574    #[test]
2575    fn inline_code_list_body_paint_origin_matches_plain_list_item() {
2576        use crate::text::inline::test_fonts::{MONO, WideMonoTextSystem};
2577        use gpui::{TestApp, rems};
2578
2579        struct ListRoot {
2580            plain: Entity<TextViewState>,
2581            code: Entity<TextViewState>,
2582            preview_zoom: f32,
2583        }
2584
2585        impl Render for ListRoot {
2586            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2587                div()
2588                    .w(px(600.))
2589                    .text_size(rems(self.preview_zoom))
2590                    .child(crate::TextSelectionLayer)
2591                    .child(TextView::new(&self.plain).selectable(true))
2592                    .child(TextView::new(&self.code).selectable(true))
2593            }
2594        }
2595
2596        fn draw(app: &mut TestApp, preview_zoom: f32) -> (Pixels, Pixels) {
2597            let mut window = app.open_window(|_, cx| ListRoot {
2598                plain: cx.new(|cx| TextViewState::markdown("- plain body words", cx)),
2599                code: cx.new(|cx| TextViewState::markdown("- plain `code` words", cx)),
2600                preview_zoom,
2601            });
2602            window.draw();
2603            app.run_until_parked();
2604            window.draw();
2605            window.read(|root, cx| {
2606                let first_body_line_top = |view: &Entity<TextViewState>| {
2607                    let view = view.read(cx);
2608                    let painted_line = view
2609                        .selection_adapter
2610                        .text_bounds()
2611                        .into_iter()
2612                        .next()
2613                        .expect("list item should paint its first body line");
2614                    painted_line.top() - view.bounds().top()
2615                };
2616                (
2617                    first_body_line_top(&root.plain),
2618                    first_body_line_top(&root.code),
2619                )
2620            })
2621        }
2622
2623        let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
2624        app.update(|cx| {
2625            crate::init(cx);
2626            crate::Theme::global_mut(cx).tokens.typography.mono = MONO.into();
2627        });
2628        for (case, preview_zoom) in [("16px", 1.), ("zoom-1.25", 1.25), ("zoom-1.5", 1.5)] {
2629            let (plain_origin, code_origin) = draw(&mut app, preview_zoom);
2630            assert_eq!(
2631                code_origin, plain_origin,
2632                "{case}: code list body paint origin {code_origin:?} must match plain {plain_origin:?}"
2633            );
2634        }
2635    }
2636
2637    #[test]
2638    fn inline_code_fragment_does_not_paint_past_its_reserved_row() {
2639        use crate::text::inline::test_fonts::{MONO, WideMonoTextSystem};
2640        use gpui::TestApp;
2641
2642        const TEXT_BACKGROUND: u32 = 0x20f0b0;
2643
2644        struct MarkdownRoot {
2645            text_view: Entity<TextViewState>,
2646        }
2647
2648        impl Render for MarkdownRoot {
2649            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2650                div()
2651                    .w(px(640.))
2652                    .text_size(px(17.9))
2653                    .text_bg(gpui::rgb(TEXT_BACKGROUND))
2654                    .child(TextView::new(&self.text_view))
2655            }
2656        }
2657
2658        let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
2659        app.update(|cx| {
2660            crate::init(cx);
2661            crate::Theme::global_mut(cx).tokens.typography.mono = MONO.into();
2662        });
2663        let mut window = app.open_window(|_, cx| MarkdownRoot {
2664            text_view: cx.new(|cx| TextViewState::markdown("`main` starts the paragraph", cx)),
2665        });
2666        window.draw();
2667        app.run_until_parked();
2668        window.draw();
2669
2670        let (view_bounds, painted) = window.update(|root, window, cx| {
2671            let view_bounds = root
2672                .text_view
2673                .read(cx)
2674                .bounds()
2675                .scale(window.scale_factor());
2676            let text_background: gpui::Background = gpui::rgb(TEXT_BACKGROUND).into();
2677            let painted = window
2678                .painted_quads()
2679                .into_iter()
2680                .filter(|quad| quad.background == text_background)
2681                .map(|quad| quad.bounds)
2682                .collect::<Vec<_>>();
2683            (view_bounds, painted)
2684        });
2685
2686        assert!(
2687            !painted.is_empty(),
2688            "the inherited text background must make actual text paint observable"
2689        );
2690        assert!(
2691            painted
2692                .iter()
2693                .all(|bounds| bounds.bottom() <= view_bounds.bottom()),
2694            "an inline-code fragment wrapped a second time after InlineFlow reserved one row; \
2695             text background quads={painted:?}, reserved TextView bounds={view_bounds:?}"
2696        );
2697    }
2698
2699    #[test]
2700    fn table_row_backgrounds_follow_the_frame_corner_radius() {
2701        use gpui::TestApp;
2702
2703        const HEAD_BACKGROUND: u32 = 0x11aa77;
2704        const RADIUS: f32 = 8.;
2705
2706        struct RoundedTableRoot {
2707            text_view: Entity<TextViewState>,
2708            scroll: bool,
2709        }
2710
2711        impl Render for RoundedTableRoot {
2712            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2713                let mut table = StyleRefinement::default();
2714                table.corner_radii.top_left = Some(px(RADIUS).into());
2715                table.corner_radii.top_right = Some(px(RADIUS).into());
2716                table.corner_radii.bottom_left = Some(px(RADIUS).into());
2717                table.corner_radii.bottom_right = Some(px(RADIUS).into());
2718                if self.scroll {
2719                    table.overflow.x = Some(gpui::Overflow::Scroll);
2720                }
2721                let style = TextViewStyle::default()
2722                    .with_code_background(gpui::rgb(HEAD_BACKGROUND).into())
2723                    .with_table(table);
2724                div()
2725                    .w(px(400.))
2726                    .child(TextView::new(&self.text_view).style(style))
2727            }
2728        }
2729
2730        for scroll in [false, true] {
2731            let mode = if scroll { "scroll" } else { "wrap" };
2732            let mut app = TestApp::new();
2733            app.update(crate::init);
2734            let mut window = app.open_window(|_, cx| RoundedTableRoot {
2735                text_view: cx
2736                    .new(|cx| TextViewState::markdown("| a | b |\n| --- | --- |\n| 1 | 2 |", cx)),
2737                scroll,
2738            });
2739            window.draw();
2740            app.run_until_parked();
2741            window.draw();
2742
2743            let (quad, scale) = window.update(|_, window, _| {
2744                let head: gpui::Background = gpui::rgb(HEAD_BACKGROUND).into();
2745                let quad = window
2746                    .painted_quads()
2747                    .into_iter()
2748                    .find(|quad| quad.background == head)
2749                    .unwrap_or_else(|| panic!("{mode}: the header background quad is painted"));
2750                (quad, window.scale_factor())
2751            });
2752
2753            // GPUI clips children with a rectangular mask, so the header fill
2754            // must carry the frame radius itself, inset by the 1px border.
2755            let expected = px(RADIUS - 1.).scale(scale);
2756            assert_eq!(quad.corner_radii.top_left, expected, "{mode}: top left");
2757            assert_eq!(quad.corner_radii.top_right, expected, "{mode}: top right");
2758            let square = px(0.).scale(scale);
2759            assert_eq!(quad.corner_radii.bottom_left, square, "{mode}: bottom left");
2760            assert_eq!(
2761                quad.corner_radii.bottom_right, square,
2762                "{mode}: bottom right"
2763            );
2764        }
2765    }
2766
2767    /// A table wider than its frame shrinks its widest column. A narrower
2768    /// column keeps its content width rather than shrinking by the same
2769    /// ratio and wrapping the end of its text onto a line of its own, in the
2770    /// header and the body alike, and with the cell and frame refinements
2771    /// the text renders with.
2772    #[test]
2773    fn table_keeps_narrow_columns_on_one_line_while_wide_ones_wrap() {
2774        use crate::text::inline::test_fonts::WideMonoTextSystem;
2775        use gpui::TestApp;
2776        use std::sync::Arc;
2777
2778        const CELL_BACKGROUND: u32 = 0x11aa77;
2779
2780        struct TableRoot {
2781            text_view: Entity<TextViewState>,
2782            width: Pixels,
2783            style: TextViewStyle,
2784        }
2785
2786        impl Render for TableRoot {
2787            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2788                div()
2789                    .w(self.width)
2790                    .child(TextView::new(&self.text_view).style(self.style.clone()))
2791            }
2792        }
2793
2794        let short = "Operating revenue (USD)";
2795        let source = format!(
2796            "| {short} | Note |\n| --- | --- |\n| {short} | x |\n| 1 | {} |",
2797            "word ".repeat(30)
2798        );
2799        // Heights of the first column's cells, top to bottom.
2800        let first_column = |width: f32, style: TextViewStyle| {
2801            let mut app = TestApp::with_text_system(Arc::new(WideMonoTextSystem));
2802            app.update(crate::init);
2803            let cell = style.table_cell().clone().bg(gpui::rgb(CELL_BACKGROUND));
2804            let style = style.with_table_cell(cell);
2805            let mut window = app.open_window(|_, cx| TableRoot {
2806                text_view: cx.new(|cx| TextViewState::markdown(&source, cx)),
2807                width: px(width),
2808                style,
2809            });
2810            window.draw();
2811            app.run_until_parked();
2812            window.draw();
2813            window.update(|_, window, _| {
2814                let background: gpui::Background = gpui::rgb(CELL_BACKGROUND).into();
2815                let cells: Vec<_> = window
2816                    .painted_quads()
2817                    .into_iter()
2818                    .filter(|quad| quad.background == background)
2819                    .map(|quad| quad.bounds)
2820                    .collect();
2821                let left = cells.iter().map(|bounds| bounds.origin.x).min().unwrap();
2822                let mut column: Vec<_> = cells
2823                    .into_iter()
2824                    .filter(|bounds| bounds.origin.x == left)
2825                    .collect();
2826                column.sort_by_key(|bounds| bounds.origin.y);
2827                column
2828                    .iter()
2829                    .map(|bounds| bounds.size.height)
2830                    .collect::<Vec<_>>()
2831            })
2832        };
2833        let scroll = || {
2834            let mut table = StyleRefinement::default();
2835            table.overflow.x = Some(gpui::Overflow::Scroll);
2836            TextViewStyle::default().with_table(table)
2837        };
2838
2839        for (layout, style) in [("wrap", TextViewStyle::default()), ("scroll", scroll())] {
2840            let fits = first_column(4000., style.clone());
2841            let narrow = first_column(700., style);
2842            assert_eq!(fits.len(), 3, "{layout}: three rows");
2843            assert_eq!(fits[0], fits[2], "{layout}: the header fits on one line");
2844            assert_eq!(narrow[..2], fits[..2], "{layout}: the short column wraps");
2845        }
2846
2847        let single_line = first_column(4000., TextViewStyle::default())[0];
2848        let padded = TextViewStyle::default().with_table_cell(StyleRefinement::default().px_4());
2849        assert_eq!(
2850            first_column(4000., padded)[0],
2851            single_line,
2852            "the cell padding refinement leaves the text its measured width"
2853        );
2854        let bold = TextViewStyle::default()
2855            .with_table(StyleRefinement::default().font_weight(gpui::FontWeight::BOLD));
2856        assert_eq!(
2857            first_column(4000., bold)[0],
2858            single_line,
2859            "the table text refinement is measured"
2860        );
2861    }
2862
2863    #[gpui::test]
2864    fn markdown_link_opens_url_without_handler(cx: &mut TestAppContext) {
2865        cx.update(crate::init);
2866        let (_, cx) =
2867            cx.add_window_view(|_, cx| TextViewTestRoot::new("[example](https://example.com)", cx));
2868        let cx: &mut VisualTestContext = cx;
2869
2870        cx.simulate_click(point(px(10.), px(10.)), Modifiers::default());
2871
2872        assert_eq!(cx.opened_url(), Some("https://example.com".to_string()));
2873    }
2874
2875    #[gpui::test]
2876    fn right_click_does_not_open_url_without_handler(cx: &mut TestAppContext) {
2877        cx.update(crate::init);
2878        let (_, cx) =
2879            cx.add_window_view(|_, cx| TextViewTestRoot::new("[example](https://example.com)", cx));
2880        let cx: &mut VisualTestContext = cx;
2881
2882        cx.simulate_mouse_down(
2883            point(px(10.), px(10.)),
2884            MouseButton::Right,
2885            Modifiers::default(),
2886        );
2887        cx.simulate_mouse_up(
2888            point(px(10.), px(10.)),
2889            MouseButton::Right,
2890            Modifiers::default(),
2891        );
2892
2893        assert_eq!(cx.opened_url(), None);
2894    }
2895
2896    #[gpui::test]
2897    fn link_handler_receives_button_and_modifiers(cx: &mut TestAppContext) {
2898        use std::sync::{Arc, Mutex};
2899
2900        struct LinkRoot {
2901            text_view: Entity<TextViewState>,
2902            clicks: Arc<Mutex<Vec<(SharedString, ClickEvent)>>>,
2903        }
2904
2905        impl Render for LinkRoot {
2906            fn render(
2907                &mut self,
2908                _window: &mut Window,
2909                _cx: &mut Context<Self>,
2910            ) -> impl IntoElement {
2911                let clicks = self.clicks.clone();
2912                div()
2913                    .w(px(240.))
2914                    .child(
2915                        TextView::new(&self.text_view).on_link_click(move |url, event, _, _| {
2916                            clicks.lock().unwrap().push((url.clone(), event.clone()));
2917                        }),
2918                    )
2919            }
2920        }
2921
2922        cx.update(crate::init);
2923        let clicks = Arc::new(Mutex::new(Vec::new()));
2924        let captured = clicks.clone();
2925        let (_, cx) = cx.add_window_view(move |_, cx| LinkRoot {
2926            text_view: cx.new(|cx| TextViewState::markdown("[example](https://example.com)", cx)),
2927            clicks,
2928        });
2929        let cx: &mut VisualTestContext = cx;
2930
2931        let mut modifiers = Modifiers::default();
2932        modifiers.control = true;
2933        cx.simulate_click(point(px(10.), px(10.)), modifiers);
2934        cx.simulate_mouse_down(
2935            point(px(10.), px(10.)),
2936            MouseButton::Middle,
2937            Modifiers::default(),
2938        );
2939        cx.simulate_mouse_up(
2940            point(px(10.), px(10.)),
2941            MouseButton::Middle,
2942            Modifiers::default(),
2943        );
2944        cx.simulate_mouse_down(
2945            point(px(10.), px(10.)),
2946            MouseButton::Right,
2947            Modifiers::default(),
2948        );
2949        cx.simulate_mouse_up(
2950            point(px(10.), px(10.)),
2951            MouseButton::Right,
2952            Modifiers::default(),
2953        );
2954
2955        let clicks = captured.lock().unwrap();
2956        assert_eq!(clicks.len(), 3);
2957        assert_eq!(clicks[0].0, "https://example.com");
2958        assert!(!clicks[0].1.is_right_click() && !clicks[0].1.is_middle_click());
2959        assert!(clicks[0].1.modifiers().control);
2960        assert!(clicks[1].1.is_middle_click());
2961        assert!(clicks[2].1.is_right_click());
2962        assert_eq!(cx.opened_url(), None);
2963    }
2964
2965    #[gpui::test]
2966    fn inline_object_inherits_bold_and_link_clicks_without_opening_on_drag(
2967        cx: &mut TestAppContext,
2968    ) {
2969        use std::sync::{Arc, Mutex};
2970        struct Root {
2971            state: Entity<TextViewState>,
2972            clicks: Arc<Mutex<Vec<SharedString>>>,
2973        }
2974        impl Render for Root {
2975            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2976                let clicks = self.clicks.clone();
2977                let extensions = crate::text::MarkdownExtensions::default().plugin(
2978                    crate::text::markdown_ext::TestInlinePlugin::new("math")
2979                        .parse_with(|node, _| {
2980                            matches!(node, markdown::mdast::Node::InlineMath(_)).then(|| {
2981                                super::super::MarkdownNode::new("math", ()).text("formula")
2982                            })
2983                        })
2984                        .render_with(|_, context, _, _| {
2985                            assert_eq!(context.text_style().font_weight, gpui::FontWeight::BOLD);
2986                            Some(super::super::InlineElement::new(div().child("formula")))
2987                        }),
2988                );
2989                div().w(px(300.)).child(crate::TextSelectionLayer).child(
2990                    TextView::new(&self.state)
2991                        .markdown_extensions(extensions)
2992                        .on_link_click(move |url, _, _, _| {
2993                            clicks.lock().unwrap().push(url.clone())
2994                        }),
2995                )
2996            }
2997        }
2998        cx.update(crate::init);
2999        let clicks = Arc::new(Mutex::new(Vec::new()));
3000        let captured = clicks.clone();
3001        let (root, cx) = cx.add_window_view(move |_, cx| Root {
3002            state: cx.new(|cx| TextViewState::markdown("**[$x$](https://example.com)**", cx)),
3003            clicks,
3004        });
3005        let cx: &mut VisualTestContext = cx;
3006        cx.run_until_parked();
3007        cx.update(|window, cx| {
3008            let _ = window.draw(cx);
3009        });
3010        let bounds = root.read_with(cx, |root, cx| {
3011            root.state.read(cx).selection_adapter.text_bounds()[0]
3012        });
3013        for button in [MouseButton::Left, MouseButton::Middle, MouseButton::Right] {
3014            cx.simulate_mouse_down(bounds.center(), button, Modifiers::default());
3015            cx.simulate_mouse_up(bounds.center(), button, Modifiers::default());
3016        }
3017        assert_eq!(captured.lock().unwrap().len(), 3);
3018        let start = point(bounds.left() + px(1.), bounds.center().y);
3019        let end = point(bounds.right() - px(1.), bounds.center().y);
3020        cx.simulate_mouse_down(start, MouseButton::Left, Modifiers::default());
3021        cx.update(|window, cx| {
3022            let _ = window.draw(cx);
3023        });
3024        cx.simulate_mouse_move(end, Some(MouseButton::Left), Modifiers::default());
3025        cx.update(|window, cx| {
3026            let _ = window.draw(cx);
3027        });
3028        cx.simulate_mouse_up(end, MouseButton::Left, Modifiers::default());
3029        assert_eq!(captured.lock().unwrap().len(), 3);
3030    }
3031
3032    #[gpui::test]
3033    fn linked_image_handler_receives_left_middle_and_right_clicks(cx: &mut TestAppContext) {
3034        use std::sync::{Arc, Mutex};
3035
3036        struct LinkedImageRoot {
3037            text_view: Entity<TextViewState>,
3038            clicks: Arc<Mutex<Vec<(SharedString, ClickEvent)>>>,
3039        }
3040
3041        impl Render for LinkedImageRoot {
3042            fn render(
3043                &mut self,
3044                _window: &mut Window,
3045                _cx: &mut Context<Self>,
3046            ) -> impl IntoElement {
3047                let clicks = self.clicks.clone();
3048                div().w(px(160.)).child(
3049                    TextView::new(&self.text_view)
3050                        .selectable(true)
3051                        .on_link_click(move |url, event, _, _| {
3052                            clicks.lock().unwrap().push((url.clone(), event.clone()));
3053                        }),
3054                )
3055            }
3056        }
3057
3058        cx.update(crate::init);
3059        let clicks = Arc::new(Mutex::new(Vec::new()));
3060        let captured = clicks.clone();
3061        let (content, cx) = cx.add_window_view(move |_, cx| LinkedImageRoot {
3062                text_view: cx.new(|cx| {
3063                    TextViewState::markdown(
3064                        r#"Before [<img src="https://example.com/image.svg" width="32" height="32">](https://example.com/image-link) after."#,
3065                        cx,
3066                    )
3067                }),
3068                clicks,
3069            }
3070        );
3071        let cx: &mut VisualTestContext = cx;
3072        cx.run_until_parked();
3073        cx.update(|window, cx| {
3074            let _ = window.draw(cx);
3075        });
3076
3077        let inline_bounds = content.read_with(cx, |content, cx| {
3078            content.text_view.read(cx).selection_adapter.text_bounds()
3079        });
3080        assert!(
3081            inline_bounds.len() >= 2,
3082            "linked image needs text bounds on both sides: {inline_bounds:?}"
3083        );
3084        assert!(
3085            inline_bounds[1].left() - inline_bounds[0].right() >= px(24.),
3086            "linked image did not reserve the expected click target: {inline_bounds:?}"
3087        );
3088        let position = point(
3089            inline_bounds[0].right() + (inline_bounds[1].left() - inline_bounds[0].right()) * 0.5,
3090            inline_bounds[0].top() + px(8.),
3091        );
3092        for button in [MouseButton::Left, MouseButton::Middle, MouseButton::Right] {
3093            cx.simulate_mouse_down(position, button, Modifiers::default());
3094            cx.simulate_mouse_up(position, button, Modifiers::default());
3095        }
3096
3097        let clicks = captured.lock().unwrap();
3098        assert_eq!(clicks.len(), 3);
3099        assert!(
3100            clicks
3101                .iter()
3102                .all(|(url, _)| url == "https://example.com/image-link")
3103        );
3104        assert!(!clicks[0].1.is_right_click() && !clicks[0].1.is_middle_click());
3105        assert!(clicks[1].1.is_middle_click());
3106        assert!(clicks[2].1.is_right_click());
3107        assert_eq!(cx.opened_url(), None);
3108    }
3109
3110    #[gpui::test]
3111    fn clipped_markdown_cannot_start_selection(cx: &mut TestAppContext) {
3112        cx.update(crate::init);
3113        let (view, cx) = cx
3114            .add_window_view(|_, cx| TextViewTestRoot::new("visible\n\nhidden selection text", cx));
3115        let cx: &mut VisualTestContext = cx;
3116
3117        cx.simulate_mouse_down(
3118            point(px(10.), px(34.)),
3119            MouseButton::Left,
3120            Modifiers::default(),
3121        );
3122        cx.simulate_mouse_move(
3123            point(px(90.), px(34.)),
3124            Some(MouseButton::Left),
3125            Modifiers::default(),
3126        );
3127        cx.simulate_mouse_up(
3128            point(px(90.), px(34.)),
3129            MouseButton::Left,
3130            Modifiers::default(),
3131        );
3132
3133        let selected_text = view.read_with(cx, |root, cx| root.text_view.read(cx).selected_text());
3134        assert!(
3135            selected_text.is_empty(),
3136            "unexpected selection: {selected_text:?}"
3137        );
3138    }
3139
3140    /// A tall selectable TextView clipped by a short `overflow_hidden` viewport,
3141    /// with a large blank footer below so a drag can extend the selection band
3142    /// past the bottom of the clip while still proxy-anchoring to the view.
3143    struct ClippedTallTextViewTestRoot {
3144        text_view: Entity<TextViewState>,
3145    }
3146
3147    impl ClippedTallTextViewTestRoot {
3148        fn new(cx: &mut Context<Self>) -> Self {
3149            // Four separate blocks; only the first (and maybe part of the
3150            // second) fit inside the 40px clip. "charlie"/"delta" render well
3151            // below it.
3152            let text_view =
3153                cx.new(|cx| TextViewState::markdown("alpha\n\nbravo\n\ncharlie\n\ndelta", cx));
3154            Self { text_view }
3155        }
3156    }
3157
3158    impl Render for ClippedTallTextViewTestRoot {
3159        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
3160            div()
3161                .w(px(200.))
3162                .child(crate::TextSelectionLayer)
3163                .child(
3164                    div()
3165                        .h(px(40.))
3166                        .overflow_hidden()
3167                        .child(TextView::new(&self.text_view).selectable(true)),
3168                )
3169                // A tall blank footer so a drag can reach a y below the clipped
3170                // text; a press there proxy-anchors to the TextView above.
3171                .child(div().h(px(160.)))
3172        }
3173    }
3174
3175    /// Regression for copying a selection taller than the visible viewport.
3176    ///
3177    /// The selection band runs from visible text at the top down to a point
3178    /// far below the clip. Every glyph of the painted TextView is laid out even
3179    /// though the lower ones are clipped away, so the copied text must include
3180    /// the clipped-out "charlie"/"delta" — not just what is on screen. This
3181    /// guards against re-adding a `content_mask` gate in
3182    /// `Inline::layout_selections`.
3183    #[gpui::test]
3184    fn selection_band_beyond_clip_copies_offscreen_text(cx: &mut TestAppContext) {
3185        cx.update(crate::init);
3186        let (content, cx) = cx.add_window_view(|_, cx| ClippedTallTextViewTestRoot::new(cx));
3187        let cx: &mut VisualTestContext = cx;
3188
3189        cx.run_until_parked();
3190        cx.update(|window, cx| {
3191            let _ = window.draw(cx);
3192        });
3193
3194        // Anchor on visible text near the top, then drag to a point well below
3195        // the 40px clip (into the blank footer) and to the far right so the
3196        // last line is fully covered.
3197        cx.simulate_mouse_down(
3198            point(px(2.), px(8.)),
3199            MouseButton::Left,
3200            Modifiers::default(),
3201        );
3202        cx.update(|window, cx| {
3203            let _ = window.draw(cx);
3204        });
3205        cx.simulate_mouse_move(
3206            point(px(180.), px(150.)),
3207            Some(MouseButton::Left),
3208            Modifiers::default(),
3209        );
3210        cx.update(|window, cx| {
3211            let _ = window.draw(cx);
3212        });
3213        cx.simulate_mouse_up(
3214            point(px(180.), px(150.)),
3215            MouseButton::Left,
3216            Modifiers::default(),
3217        );
3218        cx.update(|window, cx| {
3219            let _ = window.draw(cx);
3220        });
3221
3222        let selected_text =
3223            content.read_with(cx, |root, cx| root.text_view.read(cx).selected_text());
3224        assert!(
3225            selected_text.contains("delta"),
3226            "clipped-out text was not copied: {selected_text:?}"
3227        );
3228        assert!(
3229            selected_text.contains("charlie"),
3230            "clipped-out text was not copied: {selected_text:?}"
3231        );
3232    }
3233
3234    #[gpui::test]
3235    fn double_click_selects_word(cx: &mut TestAppContext) {
3236        cx.update(crate::init);
3237        let (view, cx) =
3238            cx.add_window_view(|_, cx| TextViewTestRoot::new("quick select value", cx));
3239
3240        let cx: &mut VisualTestContext = cx;
3241        cx.run_until_parked();
3242        cx.update(|window, cx| {
3243            let _ = window.draw(cx);
3244        });
3245        let position = point(px(10.), px(16.));
3246        cx.simulate_event(MouseDownEvent {
3247            position,
3248            modifiers: Modifiers::default(),
3249            button: MouseButton::Left,
3250            click_count: 2,
3251            first_mouse: false,
3252        });
3253        cx.simulate_event(MouseUpEvent {
3254            position,
3255            modifiers: Modifiers::default(),
3256            button: MouseButton::Left,
3257            click_count: 2,
3258        });
3259        cx.update(|window, cx| {
3260            let _ = window.draw(cx);
3261        });
3262
3263        let selected_text = view.read_with(cx, |root, cx| root.text_view.read(cx).selected_text());
3264        assert_eq!(selected_text.trim(), "quick");
3265    }
3266
3267    #[gpui::test]
3268    fn long_press_selects_word_then_drag_extends_selection(cx: &mut TestAppContext) {
3269        struct TouchRoot {
3270            text_view: Entity<TextViewState>,
3271        }
3272        impl Render for TouchRoot {
3273            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3274                div()
3275                    .w(px(300.))
3276                    .child(crate::TextSelectionLayer)
3277                    .child(TextView::new(&self.text_view).selectable(true))
3278            }
3279        }
3280        cx.update(crate::init);
3281        let (view, cx) = cx.add_window_view(|_, cx| TouchRoot {
3282            text_view: cx.new(|cx| TextViewState::markdown("quick select value", cx)),
3283        });
3284        cx.run_until_parked();
3285        cx.update(|window, cx| {
3286            let _ = window.draw(cx);
3287        });
3288        let start_position = point(px(10.), px(16.));
3289        cx.simulate_event(gpui::LongPressEvent {
3290            phase: gpui::TouchPhase::Started,
3291            start_position,
3292            position: start_position,
3293        });
3294        cx.update(|window, cx| {
3295            let _ = window.draw(cx);
3296        });
3297        assert_eq!(
3298            view.read_with(cx, |root, cx| root.text_view.read(cx).selected_text())
3299                .trim(),
3300            "quick"
3301        );
3302        for phase in [gpui::TouchPhase::Moved, gpui::TouchPhase::Ended] {
3303            cx.simulate_event(gpui::LongPressEvent {
3304                phase,
3305                start_position,
3306                position: point(px(220.), px(16.)),
3307            });
3308        }
3309        cx.update(|window, cx| {
3310            let _ = window.draw(cx);
3311        });
3312        assert_eq!(
3313            view.read_with(cx, |root, cx| root.text_view.read(cx).selected_text())
3314                .trim(),
3315            "quick select value"
3316        );
3317    }
3318
3319    #[gpui::test]
3320    fn long_press_release_keeps_handles_which_drag_the_selection(cx: &mut TestAppContext) {
3321        use crate::{SelectionEdge, TextSelection};
3322
3323        struct TouchRoot {
3324            text_view: Entity<TextViewState>,
3325        }
3326        impl Render for TouchRoot {
3327            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3328                div()
3329                    .w(px(300.))
3330                    .child(crate::TextSelectionLayer)
3331                    .child(TextView::new(&self.text_view).selectable(true))
3332            }
3333        }
3334        cx.update(crate::init);
3335        let (view, cx) = cx.add_window_view(|_, cx| TouchRoot {
3336            text_view: cx.new(|cx| TextViewState::markdown("quick select value", cx)),
3337        });
3338        let draw = |cx: &mut VisualTestContext| {
3339            cx.update(|window, cx| {
3340                let _ = window.draw(cx);
3341            });
3342        };
3343        let selected = |cx: &mut VisualTestContext| {
3344            view.read_with(cx, |root, cx| root.text_view.read(cx).selected_text())
3345                .trim()
3346                .to_string()
3347        };
3348        cx.run_until_parked();
3349        draw(cx);
3350
3351        let start_position = point(px(70.), px(16.));
3352        for phase in [gpui::TouchPhase::Started, gpui::TouchPhase::Ended] {
3353            cx.simulate_event(gpui::LongPressEvent {
3354                phase,
3355                start_position,
3356                position: start_position,
3357            });
3358            draw(cx);
3359        }
3360        assert_eq!(selected(cx), "select");
3361        let snapshot = cx
3362            .update(|window, cx| TextSelection::touch_selection(window, cx))
3363            .expect("a released long press keeps its handles");
3364        assert!(snapshot.is_menu_open());
3365        assert!(!snapshot.is_empty());
3366        assert!(snapshot.start().left() < snapshot.end().left());
3367
3368        // Drag the end handle to the end of the line. The finger holds the
3369        // knob below the line, the selection follows along the line.
3370        let end = snapshot.end();
3371        let finger = point(end.left(), end.bottom() + px(20.));
3372        cx.update(|window, cx| {
3373            TextSelection::begin_edge_drag(SelectionEdge::End, finger, window, cx);
3374        });
3375        draw(cx);
3376        let snapshot = cx
3377            .update(|window, cx| TextSelection::touch_selection(window, cx))
3378            .unwrap();
3379        assert_eq!(snapshot.dragging(), Some(SelectionEdge::End));
3380        assert!(!snapshot.is_menu_open());
3381        cx.update(|window, cx| {
3382            TextSelection::update_edge_drag(point(px(290.), finger.y), window, cx);
3383        });
3384        draw(cx);
3385        assert_eq!(selected(cx), "select value");
3386        cx.update(|window, cx| TextSelection::end_edge_drag(window, cx));
3387        draw(cx);
3388        let snapshot = cx
3389            .update(|window, cx| TextSelection::touch_selection(window, cx))
3390            .unwrap();
3391        assert!(snapshot.is_menu_open());
3392        assert_eq!(snapshot.dragging(), None);
3393        let select_start = snapshot.start().left();
3394
3395        // Select All from the menu is a view-local selection; its handles
3396        // still drag, turning it back into a point selection.
3397        cx.update(|_, cx| {
3398            view.update(cx, |root, cx| {
3399                root.text_view.update(cx, |state, cx| state.select_all(cx));
3400            });
3401        });
3402        draw(cx);
3403        assert_eq!(selected(cx), "quick select value");
3404        let snapshot = cx
3405            .update(|window, cx| TextSelection::touch_selection(window, cx))
3406            .expect("select all keeps the touch selection");
3407        let start = snapshot.start();
3408        cx.update(|window, cx| {
3409            TextSelection::begin_edge_drag(SelectionEdge::Start, start.origin, window, cx);
3410            TextSelection::update_edge_drag(point(select_start, start.origin.y), window, cx);
3411            TextSelection::end_edge_drag(window, cx);
3412        });
3413        draw(cx);
3414        assert_eq!(selected(cx), "select value");
3415
3416        // A press on the menu leaves the selection alone; one on the text
3417        // clears it.
3418        let menu = gpui::Bounds::new(point(px(0.), px(200.)), gpui::size(px(120.), px(32.)));
3419        cx.update(|window, cx| TextSelection::register_touch_ui(menu, window, cx));
3420        cx.simulate_event(MouseDownEvent {
3421            position: point(px(10.), px(210.)),
3422            modifiers: Modifiers::default(),
3423            button: MouseButton::Left,
3424            click_count: 1,
3425            first_mouse: false,
3426        });
3427        assert_eq!(selected(cx), "select value");
3428        cx.simulate_event(MouseDownEvent {
3429            position: point(px(10.), px(16.)),
3430            modifiers: Modifiers::default(),
3431            button: MouseButton::Left,
3432            click_count: 1,
3433            first_mouse: false,
3434        });
3435        cx.simulate_event(MouseUpEvent {
3436            position: point(px(10.), px(16.)),
3437            modifiers: Modifiers::default(),
3438            button: MouseButton::Left,
3439            click_count: 1,
3440        });
3441        draw(cx);
3442        assert!(
3443            cx.update(|window, cx| TextSelection::touch_selection(window, cx))
3444                .is_none()
3445        );
3446    }
3447
3448    #[gpui::test]
3449    fn triple_click_selects_paragraph(cx: &mut TestAppContext) {
3450        cx.update(crate::init);
3451        let (view, cx) =
3452            cx.add_window_view(|_, cx| TextViewTestRoot::new("quick select value", cx));
3453
3454        let cx: &mut VisualTestContext = cx;
3455        cx.run_until_parked();
3456        cx.update(|window, cx| {
3457            let _ = window.draw(cx);
3458        });
3459
3460        let position = point(px(10.), px(10.));
3461        cx.simulate_event(MouseDownEvent {
3462            position,
3463            modifiers: Modifiers::default(),
3464            button: MouseButton::Left,
3465            click_count: 3,
3466            first_mouse: false,
3467        });
3468        cx.simulate_event(MouseUpEvent {
3469            position,
3470            modifiers: Modifiers::default(),
3471            button: MouseButton::Left,
3472            click_count: 3,
3473        });
3474        cx.update(|window, cx| {
3475            let _ = window.draw(cx);
3476        });
3477
3478        let selected_text = view.read_with(cx, |root, cx| root.text_view.read(cx).selected_text());
3479        assert_eq!(selected_text.trim(), "quick select value");
3480    }
3481
3482    mod copy_before_paint {
3483        use super::*;
3484        use crate::{TextSelection, text::SelectionFormat};
3485
3486        const MARKDOWN: &str = "中文 **café**\n\nβeta `\u{2009}code\u{2009}` 👩‍💻\n\n終わり";
3487        const PLAIN: &str = "中文 café\nβeta \u{2009}code\u{2009} 👩‍💻\n終わり\n";
3488
3489        struct Root {
3490            state: Entity<TextViewState>,
3491            format: SelectionFormat,
3492            width: Pixels,
3493            scrollable: bool,
3494        }
3495
3496        impl Render for Root {
3497            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3498                div()
3499                    .w(self.width)
3500                    .when(self.scrollable, |el| el.h(px(160.)))
3501                    .child(crate::TextSelectionLayer)
3502                    .child(
3503                        TextView::new(&self.state)
3504                            .scrollable(self.scrollable)
3505                            .selection_format(self.format),
3506                    )
3507            }
3508        }
3509
3510        fn setup(
3511            format: SelectionFormat,
3512            cx: &mut TestAppContext,
3513        ) -> (Entity<TextViewState>, &mut VisualTestContext) {
3514            cx.update(crate::init);
3515            let (root, cx) = cx.add_window_view(|_, cx| Root {
3516                state: cx.new(|cx| TextViewState::markdown(MARKDOWN, cx)),
3517                format,
3518                width: px(520.),
3519                scrollable: false,
3520            });
3521            cx.run_until_parked();
3522            cx.update(|window, cx| window.draw(cx).clear(cx));
3523            (root.read_with(cx, |root, _| root.state.clone()), cx)
3524        }
3525
3526        fn document<'a>(
3527            markdown: &str,
3528            format: SelectionFormat,
3529            scrollable: bool,
3530            cx: &'a mut TestAppContext,
3531        ) -> (Entity<Root>, &'a mut VisualTestContext) {
3532            cx.update(crate::init);
3533            let (root, cx) = cx.add_window_view(|_, cx| Root {
3534                state: cx.new(|cx| TextViewState::markdown(markdown, cx)),
3535                format,
3536                width: px(520.),
3537                scrollable,
3538            });
3539            cx.run_until_parked();
3540            cx.update(|window, cx| window.draw(cx).clear(cx));
3541            (root, cx)
3542        }
3543
3544        fn scroll_to(state: &Entity<TextViewState>, ix: usize, cx: &mut VisualTestContext) {
3545            state.read_with(cx, |state, _| {
3546                state.list_state.scroll_to(gpui::ListOffset {
3547                    item_ix: ix,
3548                    offset_in_item: px(0.),
3549                })
3550            });
3551            cx.update(|window, cx| window.draw(cx).clear(cx));
3552        }
3553
3554        fn endpoints(
3555            state: &Entity<TextViewState>,
3556            cx: &mut VisualTestContext,
3557        ) -> (
3558            gpui::Point<Pixels>,
3559            gpui::Point<Pixels>,
3560            gpui::Point<Pixels>,
3561        ) {
3562            let rows = state.read_with(cx, |state, _| state.selection_adapter.text_bounds());
3563            let first = rows.first().unwrap();
3564            let last = rows.last().unwrap();
3565            (
3566                point(first.left() + px(0.1), first.center().y),
3567                point(first.right() - px(0.1), first.center().y),
3568                point(last.right() - px(0.1), last.center().y),
3569            )
3570        }
3571
3572        // Raw dispatch is intentional: simulate_event may draw after each
3573        // packet, concealing the exact native before-paint race.
3574        fn press(position: gpui::Point<Pixels>, window: &mut Window, cx: &mut gpui::App) {
3575            window.dispatch_event(
3576                gpui::PlatformInput::MouseDown(MouseDownEvent {
3577                    position,
3578                    button: MouseButton::Left,
3579                    click_count: 1,
3580                    ..Default::default()
3581                }),
3582                cx,
3583            );
3584        }
3585
3586        fn extend(position: gpui::Point<Pixels>, window: &mut Window, cx: &mut gpui::App) {
3587            window.dispatch_event(
3588                gpui::PlatformInput::MouseMove(gpui::MouseMoveEvent {
3589                    position,
3590                    pressed_button: Some(MouseButton::Left),
3591                    ..Default::default()
3592                }),
3593                cx,
3594            );
3595        }
3596
3597        fn release(position: gpui::Point<Pixels>, window: &mut Window, cx: &mut gpui::App) {
3598            window.dispatch_event(
3599                gpui::PlatformInput::MouseUp(MouseUpEvent {
3600                    position,
3601                    button: MouseButton::Left,
3602                    modifiers: Modifiers::default(),
3603                    click_count: 1,
3604                }),
3605                cx,
3606            );
3607        }
3608
3609        fn assert_copy(expected: &str, window: &mut Window, cx: &mut gpui::App) {
3610            // Dispatch synchronously on the native current focus, without
3611            // focusing a different entity or using deferred window dispatch.
3612            let focus = window.focused(cx).expect("gesture focuses TextView");
3613            focus.dispatch_action(&crate::input::Copy, window, cx);
3614            assert_eq!(
3615                cx.read_from_clipboard().and_then(|item| item.text()),
3616                Some(expected.trim().to_string())
3617            );
3618            assert!(focus.is_focused(window));
3619            assert_eq!(TextSelection::selected_text(window, cx), expected);
3620        }
3621
3622        #[gpui::test]
3623        fn custom_inline_and_block_copy_project_with_the_text(cx: &mut TestAppContext) {
3624            struct CustomRoot {
3625                state: Entity<TextViewState>,
3626                format: SelectionFormat,
3627                extensions: crate::text::MarkdownExtensions,
3628            }
3629            impl Render for CustomRoot {
3630                fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
3631                    div().w(px(520.)).child(crate::TextSelectionLayer).child(
3632                        TextView::new(&self.state)
3633                            .selection_format(self.format)
3634                            .markdown_extensions(self.extensions.clone()),
3635                    )
3636                }
3637            }
3638            for format in [SelectionFormat::Plain, SelectionFormat::Source] {
3639                cx.update(crate::init);
3640                let extensions = crate::text::MarkdownExtensions::default()
3641                    .plugin(
3642                        crate::text::markdown_ext::TestInlinePlugin::new("math").parse_with(
3643                            |node, _| {
3644                                let markdown::mdast::Node::InlineMath(math) = node else {
3645                                    return None;
3646                                };
3647                                Some(
3648                                    crate::text::MarkdownNode::new("math", ())
3649                                        .text(format!("{}²", math.value)),
3650                                )
3651                            },
3652                        ),
3653                    )
3654                    .block_parser(|node, _| {
3655                        matches!(node, markdown::mdast::Node::ThematicBreak(_)).then(|| {
3656                            crate::text::MarkdownNode::new("block", ())
3657                                .text("BLOCK")
3658                                .markdown("---")
3659                        })
3660                    })
3661                    .block_renderer("block", |_, _, _| {
3662                        div()
3663                            .debug_selector(|| "copy-custom-block".into())
3664                            .w(px(520.))
3665                            .h(px(40.))
3666                    });
3667                let (root, cx) = cx.add_window_view(|_, cx| CustomRoot {
3668                    state: cx
3669                        .new(|cx| TextViewState::markdown("before $x$ after\n\n---\n\nlast", cx)),
3670                    format,
3671                    extensions,
3672                });
3673                cx.run_until_parked();
3674                cx.update(|window, cx| window.draw(cx).clear(cx));
3675                let state = root.read_with(cx, |root, _| root.state.clone());
3676                let (start, _, end) = endpoints(&state, cx);
3677                let expected = if format == SelectionFormat::Plain {
3678                    "before x² after\nBLOCK\nlast\n"
3679                } else {
3680                    "before $x$ after\n\n---\n\nlast"
3681                };
3682                cx.update(|window, cx| {
3683                    press(start, window, cx);
3684                    extend(end, window, cx);
3685                    assert_copy(expected, window, cx);
3686                    release(end, window, cx);
3687                });
3688                cx.update(|window, cx| window.draw(cx).clear(cx));
3689                cx.update(|window, cx| assert_copy(expected, window, cx));
3690                let block = cx.debug_bounds("copy-custom-block").unwrap();
3691                cx.update(|window, cx| {
3692                    press(block.center(), window, cx);
3693                    extend(point(block.center().x, block.bottom() + px(1.)), window, cx);
3694                    assert_copy(
3695                        if format == SelectionFormat::Plain {
3696                            "BLOCK\n"
3697                        } else {
3698                            "---"
3699                        },
3700                        window,
3701                        cx,
3702                    );
3703                });
3704            }
3705        }
3706
3707        #[gpui::test]
3708        fn unicode_180_paragraph_copy_extends_and_retracts_before_paint(cx: &mut TestAppContext) {
3709            let markdown = (0..180)
3710                .map(|ix| format!("p{ix:03} **café** 中文 👩‍💻 `\u{2009}code\u{2009}`"))
3711                .collect::<Vec<_>>()
3712                .join("\n\n");
3713            // Expected bytes are constructed independently of rendered_text(),
3714            // selected_text(), and the document's selection serializer.
3715            let expected = (0..180)
3716                .map(|ix| format!("p{ix:03} café 中文 👩‍💻 \u{2009}code\u{2009}\n"))
3717                .collect::<String>();
3718            let (root, cx) = document(&markdown, SelectionFormat::Plain, false, cx);
3719            cx.simulate_resize(gpui::size(px(800.), px(12000.)));
3720            cx.update(|window, cx| window.draw(cx).clear(cx));
3721            let state = root.read_with(cx, |root, _| root.state.clone());
3722            let rows = state.read_with(cx, |state, _| state.selection_adapter.text_bounds());
3723            let first = rows.first().unwrap();
3724            let first_end = rows
3725                .iter()
3726                .take_while(|row| row.top() < first.bottom())
3727                .last()
3728                .unwrap();
3729            let last = rows.last().unwrap();
3730            assert!(last.top() > first.top() + px(3000.));
3731            let start = point(first.left() + px(0.1), first.center().y);
3732            let short_end = point(first_end.right() - px(0.1), first_end.center().y);
3733            let end = point(last.right() - px(0.1), last.center().y);
3734            let short = "p000 café 中文 👩‍💻 \u{2009}code\u{2009}\n";
3735            cx.update(|window, cx| {
3736                press(start, window, cx);
3737                extend(short_end, window, cx);
3738                assert_copy(short, window, cx);
3739            });
3740            cx.update(|window, cx| window.draw(cx).clear(cx));
3741            cx.update(|window, cx| {
3742                extend(end, window, cx);
3743                assert_copy(&expected, window, cx);
3744                extend(short_end, window, cx);
3745                assert_copy(short, window, cx);
3746            });
3747        }
3748
3749        #[gpui::test]
3750        fn scrolling_before_paint_keeps_the_completed_frame_coordinates(cx: &mut TestAppContext) {
3751            let markdown = format!(
3752                "{}\n\nlast",
3753                "alpha beta gamma delta epsilon zeta ".repeat(30)
3754            );
3755            let (root, cx) = document(&markdown, SelectionFormat::Plain, true, cx);
3756            root.update(cx, |root, cx| {
3757                root.width = px(180.);
3758                cx.notify();
3759            });
3760            cx.update(|window, cx| window.draw(cx).clear(cx));
3761            let state = root.read_with(cx, |root, _| root.state.clone());
3762            let rows = state.read_with(cx, |state, _| state.selection_adapter.text_bounds());
3763            let first = rows[0];
3764            let (start, end) = state.read_with(cx, |state, _| {
3765                (
3766                    state.selection_adapter.first_text_position(0),
3767                    state.selection_adapter.first_text_position("alpha".len()),
3768                )
3769            });
3770            cx.update(|window, cx| {
3771                press(start, window, cx);
3772                extend(end, window, cx);
3773            });
3774            cx.update(|window, cx| window.draw(cx).clear(cx));
3775            cx.update(|window, cx| {
3776                state.read(cx).list_state.scroll_by(first.size.height);
3777                // The logical offset changed, registration and native pixels
3778                // have not. This must not select the next wrapped row.
3779                assert_copy("alpha\n", window, cx);
3780            });
3781        }
3782
3783        #[gpui::test]
3784        fn offscreen_remeasurement_is_not_a_copy_projection(cx: &mut TestAppContext) {
3785            let markdown = (0..180)
3786                .map(|ix| format!("row {ix:03} café 中文"))
3787                .collect::<Vec<_>>()
3788                .join("\n\n");
3789            let (root, cx) = document(&markdown, SelectionFormat::Plain, true, cx);
3790            let state = root.read_with(cx, |root, _| root.state.clone());
3791            scroll_to(&state, 90, cx);
3792            root.update(cx, |root, cx| {
3793                root.width = px(260.);
3794                cx.notify();
3795            });
3796            cx.update(|window, cx| window.draw(cx).clear(cx));
3797            scroll_to(&state, 90, cx);
3798            // measure_all has remeasured earlier, unpainted paragraphs at the
3799            // new width. Only this completed paint supplies Copy geometry.
3800            let (start, end, _) = endpoints(&state, cx);
3801            cx.update(|window, cx| {
3802                press(start, window, cx);
3803                extend(end, window, cx);
3804                assert_copy("row 090 café 中文\n", window, cx);
3805            });
3806        }
3807
3808        #[gpui::test]
3809        fn virtualized_flow_retains_its_source_owned_partial_anchor(cx: &mut TestAppContext) {
3810            for format in [SelectionFormat::Plain, SelectionFormat::Source] {
3811                let tail = (1..180)
3812                    .map(|ix| format!("row {ix:03} café 中文"))
3813                    .collect::<Vec<_>>();
3814                let markdown = format!("PREFIX `code` suffix\n\n{}", tail.join("\n\n"));
3815                let expected = if format == SelectionFormat::Plain {
3816                    format!("code suffix\n{}\n", tail.join("\n"))
3817                } else {
3818                    format!("`code` suffix\n\n{}", tail.join("\n\n"))
3819                };
3820                let (root, cx) = document(&markdown, format, true, cx);
3821                let state = root.read_with(cx, |root, _| root.state.clone());
3822                let rows = state.read_with(cx, |state, _| state.selection_adapter.text_bounds());
3823                let code = rows[1];
3824                let suffix = rows[2];
3825                assert!(code.left() < suffix.left());
3826                cx.update(|window, cx| {
3827                    press(point(code.left() + px(0.1), code.center().y), window, cx);
3828                    extend(
3829                        point(suffix.right() - px(0.1), suffix.center().y),
3830                        window,
3831                        cx,
3832                    );
3833                });
3834                cx.update(|window, cx| window.draw(cx).clear(cx));
3835                scroll_to(&state, 179, cx);
3836                // Two completed frames let GPUI retire temporary flow fragments.
3837                cx.update(|window, cx| window.draw(cx).clear(cx));
3838                let (_, _, end) = endpoints(&state, cx);
3839                cx.update(|window, cx| {
3840                    extend(end, window, cx);
3841                    assert_copy(&expected, window, cx);
3842                });
3843            }
3844        }
3845
3846        #[gpui::test]
3847        fn a_new_range_copies_before_its_first_selection_paint(cx: &mut TestAppContext) {
3848            let (state, cx) = setup(SelectionFormat::Plain, cx);
3849            let (start, _, end) = endpoints(&state, cx);
3850            cx.update(|window, cx| {
3851                press(start, window, cx);
3852                extend(end, window, cx);
3853                assert_copy(PLAIN, window, cx);
3854            });
3855        }
3856
3857        #[gpui::test]
3858        fn an_established_range_extends_and_retracts_without_paint(cx: &mut TestAppContext) {
3859            let (state, cx) = setup(SelectionFormat::Plain, cx);
3860            let (start, first_end, end) = endpoints(&state, cx);
3861            cx.update(|window, cx| {
3862                press(start, window, cx);
3863                extend(first_end, window, cx);
3864            });
3865            cx.update(|window, cx| window.draw(cx).clear(cx));
3866            cx.update(|window, cx| {
3867                assert_copy("中文 café\n", window, cx);
3868                extend(end, window, cx);
3869                assert_copy(PLAIN, window, cx);
3870                extend(first_end, window, cx);
3871                assert_copy("中文 café\n", window, cx);
3872            });
3873        }
3874
3875        #[gpui::test]
3876        fn source_copy_projects_the_current_range_without_paint(cx: &mut TestAppContext) {
3877            let (state, cx) = setup(SelectionFormat::Source, cx);
3878            let (start, first_end, end) = endpoints(&state, cx);
3879            cx.update(|window, cx| {
3880                press(start, window, cx);
3881                extend(first_end, window, cx);
3882                assert_copy("中文 **café**", window, cx);
3883                extend(end, window, cx);
3884                assert_copy(MARKDOWN, window, cx);
3885                extend(first_end, window, cx);
3886                assert_eq!(TextSelection::selected_text(window, cx), "中文 **café**");
3887            });
3888        }
3889
3890        #[gpui::test]
3891        fn a_blank_gesture_does_not_reuse_the_previous_range(cx: &mut TestAppContext) {
3892            let (state, cx) = setup(SelectionFormat::Plain, cx);
3893            let (start, first_end, end) = endpoints(&state, cx);
3894            cx.update(|window, cx| {
3895                press(start, window, cx);
3896                extend(end, window, cx);
3897                assert_eq!(TextSelection::selected_text(window, cx), PLAIN);
3898                release(end, window, cx);
3899                press(first_end + point(px(20.), px(0.)), window, cx);
3900                extend(first_end + point(px(60.), px(0.)), window, cx);
3901                assert_eq!(TextSelection::selected_text(window, cx), "");
3902            });
3903        }
3904
3905        #[gpui::test]
3906        fn replacing_source_cannot_reproject_old_content(cx: &mut TestAppContext) {
3907            for format in [SelectionFormat::Plain, SelectionFormat::Source] {
3908                for replacement in ["replacement".to_string(), "replacement ".repeat(500)] {
3909                    let (state, cx) = setup(format, cx);
3910                    let (start, _, end) = endpoints(&state, cx);
3911                    cx.update(|window, cx| {
3912                        press(start, window, cx);
3913                        extend(end, window, cx);
3914                        assert_eq!(
3915                            TextSelection::selected_text(window, cx),
3916                            if format == SelectionFormat::Plain {
3917                                PLAIN
3918                            } else {
3919                                MARKDOWN
3920                            }
3921                        );
3922                        // Large replacements leave the previous parsed document
3923                        // in place while parsing; neither size may reuse it.
3924                        state.update(cx, |state, cx| state.set_text(&replacement, cx));
3925                        assert_eq!(TextSelection::selected_text(window, cx), "");
3926                    });
3927                }
3928            }
3929        }
3930    }
3931
3932    // Regression: markdown `TextView` items inside an outer `gpui::list` with
3933    // `measure_all` must keep a stable total content height while scrolling.
3934    // Before synchronous full-replace parsing, off-screen markdown views were
3935    // first measured with empty content and the scrollbar thumb jittered as the
3936    // total height grew during scrolling.
3937    #[gpui::test]
3938    fn outer_list_content_total_stable_while_scrolling(cx: &mut TestAppContext) {
3939        use gpui::{ListAlignment, ListState, list};
3940
3941        const ITEMS: &[&str] = &[
3942            "# Heading\n\nA paragraph long enough to wrap across several lines and produce a non-trivial height.",
3943            "Short.",
3944            "Paragraph A\n\nParagraph B\n\nParagraph C with more words to increase the height.",
3945            "## Subheading\n\n- One\n- Two\n- Three\n\nClosing paragraph.",
3946            "Only one line.",
3947            "**Bold**: medium length text with `code` mixed with regular words.",
3948            "1. First\n2. Second\n3. Third\n\nA short closing paragraph.",
3949            "A long message with enough words to wrap across multiple lines, create a taller item, and verify that off-screen measurement matches visible measurement.",
3950        ];
3951        let n = 40usize;
3952
3953        struct ListRoot {
3954            state: ListState,
3955        }
3956        impl Render for ListRoot {
3957            fn render(&mut self, _w: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
3958                div().w(px(360.)).h(px(500.)).child(
3959                    list(self.state.clone(), |ix, _w, _cx| {
3960                        div()
3961                            .w_full()
3962                            .child(TextView::markdown(
3963                                ("md", ix as u64),
3964                                ITEMS[ix % ITEMS.len()],
3965                            ))
3966                            .into_any_element()
3967                    })
3968                    .size_full(),
3969                )
3970            }
3971        }
3972
3973        cx.update(crate::init);
3974        let state = ListState::new(n, ListAlignment::Top, px(2048.)).measure_all();
3975        let probe = state.clone();
3976        let (_view, cx) = cx.add_window_view(|_w, _cx| ListRoot { state });
3977        let cx: &mut VisualTestContext = cx;
3978
3979        cx.run_until_parked();
3980        cx.update(|w, cx| {
3981            let _ = w.draw(cx);
3982        });
3983        cx.run_until_parked();
3984        cx.update(|w, cx| {
3985            let _ = w.draw(cx);
3986        });
3987
3988        let total = |p: &ListState| {
3989            f32::from(p.max_offset_for_scrollbar().y + p.viewport_bounds().size.height)
3990        };
3991        let mut totals = vec![total(&probe)];
3992        for _ in 0..20 {
3993            probe.scroll_by(px(150.));
3994            cx.update(|w, cx| {
3995                let _ = w.draw(cx);
3996            });
3997            cx.run_until_parked();
3998            totals.push(total(&probe));
3999        }
4000        let min = totals.iter().cloned().fold(f32::INFINITY, f32::min);
4001        let max = totals.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
4002        println!(
4003            "OUTER_LIST_PROBE min={min:.1} max={max:.1} delta={:.1}",
4004            max - min
4005        );
4006        assert!(
4007            (max - min) < 2.0,
4008            "list content total jittered while scrolling: min={min} max={max} totals={totals:?}"
4009        );
4010    }
4011}