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