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