Skip to main content

gpui_base/text/
state.rs

1use futures::Stream as _;
2#[cfg(not(target_family = "wasm"))]
3use std::time::Instant;
4use std::{
5    ops::{Range, RangeInclusive},
6    pin::Pin,
7    sync::{Arc, Mutex, OnceLock},
8    task::Poll,
9    time::Duration,
10};
11#[cfg(target_family = "wasm")]
12use web_time::Instant;
13
14use gpui::{
15    App, AppContext as _, Bounds, Context, EntityId, FocusHandle, IntoElement, KeyBinding,
16    ListState, ParentElement as _, Pixels, Point, Render, SharedString, Styled as _, Task, Window,
17    prelude::FluentBuilder as _, px,
18};
19
20use crate::{
21    AutoScroll, ElementExt, TextSelection,
22    async_util::{Receiver, Sender, unbounded},
23    input::{self, SelectAll},
24    text::{
25        CodeBlockActionsFn, CodeBlockHighlighterFn, LinkClickHandlerFn, MarkdownExtensions,
26        RangeHighlight, RangeHighlightError, RenderedText, TableActionsFn, TextViewStyle,
27        document::ParsedDocument,
28        format,
29        node::{self, NodeContext},
30        range_highlight::{
31            LeafRemap, PendingReveal, RangeHighlightFrame, RenderedIndex, RevealRequest,
32        },
33        selection_adapter::TextViewSelectionAdapter,
34        stream_fade::{StreamFadeTracker, TextViewMotion},
35    },
36    v_flex,
37};
38
39const CONTEXT: &'static str = "TextView";
40// Keep coalescing bounded so sustained streams still render intermediate updates.
41const MAX_COALESCED_UPDATES_PER_PARSE: usize = 64;
42// Preserve exact first-layout height for small documents while bounding the
43// amount of source parsed synchronously on the UI thread.
44const MAX_SYNC_FULL_REPLACE_BYTES: usize = 4 * 1024;
45// Repaint a streamed fade at about 30 fps, not at the display refresh rate.
46const STREAM_FADE_TICK: Duration = Duration::from_millis(33);
47
48pub(crate) fn init(cx: &mut App) {
49    cx.bind_keys(vec![
50        #[cfg(target_os = "macos")]
51        KeyBinding::new("cmd-c", input::Copy, Some(CONTEXT)),
52        #[cfg(not(target_os = "macos"))]
53        KeyBinding::new("ctrl-c", input::Copy, Some(CONTEXT)),
54        #[cfg(target_os = "macos")]
55        KeyBinding::new("cmd-a", input::SelectAll, Some(CONTEXT)),
56        #[cfg(not(target_os = "macos"))]
57        KeyBinding::new("ctrl-a", input::SelectAll, Some(CONTEXT)),
58    ]);
59}
60
61/// The content format of the text view.
62#[derive(Clone, Copy, PartialEq, Eq)]
63pub(super) enum TextViewFormat {
64    /// Markdown view
65    Markdown,
66    /// HTML view
67    Html,
68}
69
70/// The format of the text returned by
71/// [`TextViewState::selected_text`], which is also what copy writes to the
72/// clipboard.
73#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
74pub enum SelectionFormat {
75    /// The rendered text, without any markup.
76    #[default]
77    Plain,
78    /// The source of the selection.
79    ///
80    /// Select-all returns the original source verbatim, a partial selection is
81    /// reconstructed as Markdown from the parsed nodes (e.g. selecting inside
82    /// a `**bold**` run yields `**bold**`).
83    Source,
84}
85
86/// One text element's laid-out vertical extent, reported by `Inline` during
87/// prepaint so `TextView` can snap its `max_lines` clip to a whole-line
88/// boundary.
89#[derive(Clone, Copy)]
90pub(super) struct LineSpan {
91    pub(super) top: Pixels,
92    pub(super) bottom: Pixels,
93    pub(super) line_height: Pixels,
94}
95
96/// The state of a TextView.
97pub struct TextViewState {
98    entity_id: EntityId,
99    pub(super) focus_handle: FocusHandle,
100    pub(super) list_state: ListState,
101
102    /// The bounds of the text view
103    bounds: Bounds<Pixels>,
104
105    pub(super) selectable: bool,
106    pub(super) selection_format: SelectionFormat,
107    pub(super) scrollable: bool,
108    pub(super) max_lines: Option<usize>,
109    /// Line spans reported by `Inline` during prepaint (collected only while
110    /// [`Self::max_lines`] is set); cleared by `TextView` at each frame start.
111    pub(super) line_spans: Arc<Mutex<Vec<LineSpan>>>,
112    /// Whether the last painted frame clipped content due to `max_lines`.
113    pub(super) clamped: bool,
114    pub(super) text_view_style: Arc<TextViewStyle>,
115    pub(super) code_block_actions: Option<std::sync::Arc<CodeBlockActionsFn>>,
116    pub(super) code_block_highlighter: Option<std::sync::Arc<CodeBlockHighlighterFn>>,
117    pub(super) table_actions: Option<std::sync::Arc<TableActionsFn>>,
118    pub(super) image_source: Option<std::sync::Arc<super::text_view::ImageSourceFn>>,
119    pub(super) link_click_handler: Option<std::sync::Arc<LinkClickHandlerFn>>,
120    pub(super) markdown_extensions: Arc<MarkdownExtensions>,
121
122    pub(super) is_selecting: bool,
123    /// Logical ranges retained across an explicitly requested resource reflow.
124    pub(super) preserve_inline_selection: bool,
125    multi_click_selection: Option<TextViewMultiClickSelection>,
126    selected_text_override: Option<String>,
127    select_all: bool,
128    pub(super) auto_scroll: AutoScroll,
129    pub(super) selection_adapter: TextViewSelectionAdapter,
130
131    pub(super) parsed_content: ParsedContent,
132    pub(super) stream_fade: StreamFadeTracker,
133    fade_tick: Option<Task<()>>,
134    /// Content format (markdown / html), used for bounded synchronous parsing
135    /// of small full-replace updates.
136    format: TextViewFormat,
137    text: String,
138    /// The text a `TextView` element last handed over, to recognize the same
139    /// string next frame without comparing its bytes.
140    element_text: Option<SharedString>,
141    revision: usize,
142    /// The revision of the update `parsed_content` was committed from.
143    committed_revision: usize,
144    /// The revision of the last update that replaced the text rather than
145    /// appending to it.
146    full_update_revision: usize,
147    /// The rendered text of `parsed_content`, built when first read.
148    rendered_index: Arc<OnceLock<RenderedIndex>>,
149    range_highlights: Option<Arc<RangeHighlightFrame>>,
150    pub(super) pending_reveal: Option<PendingReveal>,
151    pub(super) selection_revision: usize,
152    compatible_layout_update: bool,
153    layout_text_style: Option<(gpui::TextStyle, Pixels)>,
154    parsed_error: Option<SharedString>,
155    tx: Sender<UpdateOptions>,
156    _parse_task: Task<()>,
157    _receive_task: Task<()>,
158}
159
160impl TextViewState {
161    /// Create a Markdown TextViewState.
162    pub fn markdown(text: &str, cx: &mut Context<Self>) -> Self {
163        Self::new(TextViewFormat::Markdown, text, cx)
164    }
165
166    /// Create a HTML TextViewState.
167    pub fn html(text: &str, cx: &mut Context<Self>) -> Self {
168        Self::new(TextViewFormat::Html, text, cx)
169    }
170
171    /// Create a new TextViewState.
172    fn new(format: TextViewFormat, text: &str, cx: &mut Context<Self>) -> Self {
173        let focus_handle = cx.focus_handle();
174        let selection_adapter = TextViewSelectionAdapter::new(cx.entity().downgrade(), cx);
175
176        let (tx, rx) = unbounded::<UpdateOptions>();
177        let (tx_result, rx_result) = unbounded::<ParsedUpdate>();
178        let _receive_task = cx.spawn({
179            async move |weak_self, cx| {
180                while let Ok(parsed_update) = rx_result.recv().await {
181                    _ = weak_self.update(cx, |state, cx| {
182                        if parsed_update.revision != state.revision {
183                            return;
184                        }
185                        if parsed_update.baseline_ack {
186                            debug_assert!(parsed_update.full_parse);
187                            return;
188                        }
189
190                        match parsed_update.result {
191                            Ok(content) => {
192                                let append =
193                                    parsed_update.selection_compatible && !parsed_update.full_parse;
194                                if append && state.full_update_revision <= state.committed_revision
195                                {
196                                    state.splice_appended_blocks(&content.document);
197                                }
198                                state.reconcile_range_highlights(
199                                    &content.document,
200                                    parsed_update.revision,
201                                    append,
202                                );
203                                state.stream_fade.record(
204                                    &state.parsed_content.document,
205                                    &content.document,
206                                    Instant::now(),
207                                );
208                                state.parsed_content = content;
209                                state.parsed_error = None;
210                                state.compatible_layout_update = parsed_update.selection_compatible;
211                                if parsed_update.full_parse {
212                                    state.invalidate_measured_heights();
213                                }
214                            }
215                            Err(err) => {
216                                state.stream_fade.discard_pending();
217                                state.parsed_error = Some(err);
218                            }
219                        }
220                        // Don't interrupt an active drag-selection; the stored
221                        // positions remain valid for append-only updates and will
222                        // self-correct on the next mouse-move event.
223                        if !parsed_update.selection_compatible && !state.is_selecting {
224                            state.reset_selection_and_adapter(cx);
225                        }
226                        cx.notify();
227                    });
228                }
229            }
230        });
231
232        let _parse_task = cx.background_spawn(UpdateFuture::new(format, rx, tx_result));
233
234        let mut this = Self {
235            entity_id: cx.entity_id(),
236            focus_handle,
237            bounds: Bounds::default(),
238            multi_click_selection: None,
239            selected_text_override: None,
240            select_all: false,
241            selectable: false,
242            selection_format: SelectionFormat::default(),
243            scrollable: false,
244            max_lines: None,
245            line_spans: Arc::default(),
246            clamped: false,
247            // Measure all blocks (not just visible ones) so the scrollbar
248            // thumb size stays stable. Without this, off-screen blocks count
249            // as zero height until scrolled into view, which makes the
250            // scrollbar jitter as more blocks get measured during scrolling.
251            list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)).measure_all(),
252            text_view_style: Arc::default(),
253            code_block_actions: None,
254            code_block_highlighter: None,
255            table_actions: None,
256            link_click_handler: None,
257            image_source: None,
258            markdown_extensions: Arc::default(),
259            is_selecting: false,
260            preserve_inline_selection: false,
261            auto_scroll: AutoScroll::default(),
262            selection_adapter,
263            parsed_content: Default::default(),
264            stream_fade: StreamFadeTracker::default(),
265            fade_tick: None,
266            format,
267            parsed_error: None,
268            text: text.to_string(),
269            element_text: None,
270            revision: 0,
271            committed_revision: 0,
272            full_update_revision: 0,
273            rendered_index: Arc::default(),
274            range_highlights: None,
275            pending_reveal: None,
276            selection_revision: 0,
277            compatible_layout_update: false,
278            layout_text_style: None,
279            tx,
280            _parse_task,
281            _receive_task,
282        };
283        this.increment_update(&text, false, cx);
284        this
285    }
286
287    /// Get the text content.
288    pub(crate) fn source(&self) -> SharedString {
289        self.parsed_content.document.source.clone()
290    }
291
292    /// Set whether the text is selectable, default false.
293    pub fn selectable(mut self, selectable: bool) -> Self {
294        self.selectable = selectable;
295        self
296    }
297
298    /// Set whether the text is selectable, default false.
299    pub fn set_selectable(&mut self, selectable: bool, cx: &mut Context<Self>) {
300        self.selectable = selectable;
301        cx.notify();
302    }
303
304    /// Set the [`SelectionFormat`], default is [`SelectionFormat::Plain`].
305    pub fn selection_format(mut self, selection_format: SelectionFormat) -> Self {
306        self.selection_format = selection_format;
307        self
308    }
309
310    /// Set the [`SelectionFormat`], default is [`SelectionFormat::Plain`].
311    pub fn set_selection_format(
312        &mut self,
313        selection_format: SelectionFormat,
314        cx: &mut Context<Self>,
315    ) {
316        self.selection_format = selection_format;
317        cx.notify();
318    }
319
320    /// Set whether the text view scrolls internally, default false.
321    pub fn scrollable(mut self, scrollable: bool) -> Self {
322        self.scrollable = scrollable;
323        self
324    }
325
326    /// Set whether the text view scrolls internally, default false.
327    pub fn set_scrollable(&mut self, scrollable: bool, cx: &mut Context<Self>) {
328        if !scrollable {
329            self.reset_selection_and_adapter(cx);
330        }
331        self.scrollable = scrollable;
332        cx.notify();
333    }
334
335    /// Whether the last painted frame clipped content because of
336    /// [`TextView::max_lines`](crate::text::TextView::max_lines).
337    pub fn is_clamped(&self) -> bool {
338        self.clamped
339    }
340
341    /// Set the text content.
342    ///
343    /// With a streamed fade-in enabled, text that extends the current text
344    /// fades in like [`Self::push_str`] would; any other replacement shows at
345    /// once.
346    pub fn set_text(&mut self, text: &str, cx: &mut Context<Self>) {
347        if self.text.as_str() == text {
348            return;
349        }
350        if self.stream_fade.is_enabled() {
351            if text.starts_with(self.text.as_str()) {
352                self.stream_fade.note_extend(self.text.len());
353            } else {
354                self.stream_fade.note_replace();
355            }
356        }
357
358        self.text.clear();
359        self.text.push_str(text);
360        self.parsed_error = None;
361        self.increment_update(text, false, cx);
362    }
363
364    /// [`Self::set_text`] for the text a `TextView` element hands over every
365    /// frame: the string it handed over last time (the same allocation, not
366    /// merely equal bytes) is recognized without a comparison, as long as the
367    /// state still holds a text of its length.
368    pub(super) fn set_element_text(&mut self, text: &SharedString, cx: &mut Context<Self>) {
369        let same_string = self.element_text.as_ref().is_some_and(|current| {
370            current.as_ptr() == text.as_ptr() && current.len() == text.len()
371        });
372        if same_string && self.text.len() == text.len() {
373            return;
374        }
375        self.element_text = Some(text.clone());
376        self.set_text(text, cx);
377    }
378
379    /// Append partial text content to the existing text.
380    pub fn push_str(&mut self, new_text: &str, cx: &mut Context<Self>) {
381        if new_text.is_empty() {
382            return;
383        }
384        self.stream_fade.note_extend(self.text.len());
385        self.text.push_str(new_text);
386        self.increment_update(new_text, true, cx);
387    }
388
389    /// Set the motion policy; see [`TextViewMotion`].
390    pub fn motion(mut self, motion: TextViewMotion) -> Self {
391        self.set_motion(motion);
392        self
393    }
394
395    /// Set the motion policy; see [`TextViewMotion`].
396    pub fn set_motion(&mut self, motion: TextViewMotion) {
397        self.stream_fade.set_motion(motion);
398    }
399
400    pub(crate) fn set_markdown_extensions(
401        &mut self,
402        markdown_extensions: Arc<MarkdownExtensions>,
403        cx: &mut Context<Self>,
404    ) {
405        if self.markdown_extensions.revision() == markdown_extensions.revision() {
406            return;
407        }
408
409        let parser_configuration_changed = !self
410            .markdown_extensions
411            .has_same_parser_configuration(&markdown_extensions);
412        self.markdown_extensions = markdown_extensions;
413        if parser_configuration_changed && self.format == TextViewFormat::Markdown {
414            let text = self.text.clone();
415            self.increment_update(&text, false, cx);
416        }
417    }
418
419    /// Return the selected text, in the view's [`SelectionFormat`].
420    pub fn selected_text(&self) -> String {
421        self.selected_text_in(None)
422    }
423
424    /// Return the original Markdown source byte range corresponding to the
425    /// rendered selection.
426    ///
427    /// The range addresses the source passed to this Markdown TextView. It is
428    /// derived from parser positions retained by the rendered inline nodes, so
429    /// identical rendered text maps to the occurrence that was actually
430    /// selected. The result is one contiguous source range, so it includes any
431    /// Markdown delimiters between the selected rendered endpoints. HTML views
432    /// and selections without an exact source mapping return `None`. Select-all
433    /// in a Markdown view returns the full source range.
434    pub fn selected_source_range(&self) -> Option<Range<usize>> {
435        if self.format != TextViewFormat::Markdown {
436            return None;
437        }
438        if self.select_all {
439            return Some(0..self.source().len());
440        }
441        self.parsed_content.document.selected_source_range()
442    }
443
444    /// The format to copy in, which is [`SelectionFormat::Plain`] whenever the
445    /// requested one cannot be produced.
446    ///
447    /// Only a Markdown view can return source. Reconstructing HTML would mean
448    /// spelling every attribute back out — a mark's color, an image's
449    /// dimensions, a cell's alignment — with a new way to lose one at each
450    /// step, and html5ever records no source offsets to fall back on (it
451    /// reports only line numbers), so there is no original text to copy from
452    /// either.
453    fn effective_format(&self) -> SelectionFormat {
454        match self.format {
455            TextViewFormat::Markdown => self.selection_format,
456            TextViewFormat::Html => SelectionFormat::Plain,
457        }
458    }
459
460    /// Return the selected text, with `blocks` bounding which top-level blocks
461    /// the selection covers.
462    ///
463    /// The range comes from the selection endpoints, which know their block
464    /// even after it scrolls out of view; see
465    /// [`ParsedDocument::selected_text`](crate::text::document::ParsedDocument).
466    pub(super) fn selected_text_in(&self, blocks: Option<RangeInclusive<usize>>) -> String {
467        let format = self.effective_format();
468
469        if self.select_all {
470            if format == SelectionFormat::Source {
471                return self.source().to_string();
472            }
473
474            return self.parsed_content.document.text();
475        }
476
477        // A multi-click stores the plain text it selected, which is a shortcut
478        // past the block walk. Source mode cannot take it: the word it stored
479        // has lost its markup. The click also set the inline selection it came
480        // from, so the walk reconstructs the same range with the markup intact.
481        if format != SelectionFormat::Source
482            && let Some(text) = &self.selected_text_override
483        {
484            return text.clone();
485        }
486
487        self.parsed_content.document.selected_text(format, blocks)
488    }
489
490    /// Force a full re-measure of the block list after the document has been
491    /// replaced.
492    ///
493    /// `Document::render_root` only calls `ListState::reset` when the block
494    /// *count* changes, and the full-measure pass enabled by `measure_all` is
495    /// a one-shot latch that only `reset`, `remeasure_items`, or a width
496    /// change re-arms. Replacing a document with one that happens to have the
497    /// same number of blocks therefore leaves every cached height belonging to
498    /// the *previous* document. The list summary height stays wrong, and since
499    /// the wheel clamps against that summary, the blocks past the false bottom
500    /// can never scroll into view to be re-measured -- the clamp seals itself.
501    ///
502    /// `remeasure_items` re-arms the latch and marks the items unmeasured
503    /// while keeping their old sizes as hints, so the scrollbar does not
504    /// collapse in the frame before the next layout measures the real heights.
505    fn invalidate_measured_heights(&self) {
506        let count = self.list_state.item_count();
507        if count > 0 {
508            self.list_state.remeasure_items(0..count);
509        }
510    }
511
512    /// Remeasure inline resources after their prepared content or metrics change.
513    ///
514    /// Call from the resource owner's completion handler, through a weak entity
515    /// when the task can outlive this view. This does not reparse the document.
516    /// Existing logical selection is retained until the next selection gesture.
517    pub fn invalidate_inline_layout(&mut self, cx: &mut Context<Self>) {
518        self.preserve_inline_selection = true;
519        self.compatible_layout_update = true;
520        self.invalidate_measured_heights();
521        cx.notify();
522    }
523
524    fn increment_update(&mut self, text: &str, append: bool, cx: &mut Context<Self>) {
525        self.revision += 1;
526        if !append {
527            self.full_update_revision = self.revision;
528            self.selection_revision = self.selection_revision.wrapping_add(1);
529        }
530        let parse_synchronously = !append && text.len() <= MAX_SYNC_FULL_REPLACE_BYTES;
531        let update_options = UpdateOptions {
532            revision: self.revision,
533            append,
534            mode: if append {
535                ParseMode::Compatible
536            } else if parse_synchronously {
537                ParseMode::BaselineAck
538            } else {
539                ParseMode::Replace
540            },
541            pending_text: text.to_string(),
542            markdown_extensions: self.markdown_extensions.clone(),
543        };
544
545        // Keep small full replacements synchronous so their first layout has
546        // the exact content height. Larger replacements use the existing
547        // background parser, bounding synchronous parser input on the UI thread.
548        if parse_synchronously {
549            match parse_content(self.format, ParsedContent::default(), &update_options) {
550                Ok(content) => {
551                    self.reconcile_range_highlights(&content.document, self.revision, false);
552                    self.stream_fade.record(
553                        &self.parsed_content.document,
554                        &content.document,
555                        Instant::now(),
556                    );
557                    self.parsed_content = content;
558                    self.parsed_error = None;
559                    self.invalidate_measured_heights();
560                    if !self.is_selecting {
561                        self.reset_selection_and_adapter(cx);
562                    }
563                }
564                Err(err) => {
565                    self.stream_fade.discard_pending();
566                    self.parsed_error = Some(err);
567                }
568            }
569            // Keep the background parser's accumulated document in sync so a
570            // later append extends this baseline instead of parsing the delta
571            // as a standalone document.
572            _ = self.tx.try_send(update_options);
573            cx.notify();
574            return;
575        }
576
577        _ = self.tx.try_send(update_options);
578    }
579
580    /// The text this view renders, which [`RangeHighlight`] ranges index.
581    ///
582    /// This is the string plain copy produces, as of the last parse that
583    /// landed; text set since then is not in it until its parse lands.
584    pub fn rendered_text(&self) -> RenderedText {
585        RenderedText::new(
586            self.entity_id,
587            self.committed_revision,
588            self.parsed_content.document.clone(),
589            self.rendered_index.clone(),
590        )
591    }
592
593    /// Replace the range highlights, whose ranges index the current rendered text.
594    ///
595    /// Compute ranges from the current [`Self::rendered_text`] and set them in
596    /// the same update. Search the new text again after its content changes.
597    /// A range crossing blocks paints in both, skipping their separator.
598    /// Text outside every block (separators, custom blocks, HTML blocks, and
599    /// inline objects) is left unpainted. Any invalid range rejects the set.
600    ///
601    /// Highlights follow unchanged text through updates. Text appended while
602    /// streaming keeps earlier highlights; after a table edit, cells in and
603    /// after the edited row lose theirs because cells are known by position.
604    /// Text backgrounds such as `<mark>` paint over a highlight; inline code
605    /// backgrounds paint under it.
606    pub fn set_range_highlights(
607        &mut self,
608        highlights: impl IntoIterator<Item = RangeHighlight>,
609        cx: &mut Context<Self>,
610    ) -> Result<(), RangeHighlightError> {
611        if self.format != TextViewFormat::Markdown {
612            return Err(RangeHighlightError::Unsupported);
613        }
614        let text = self.rendered_text();
615        self.range_highlights = RangeHighlightFrame::new(&text, highlights)?.map(Arc::new);
616        cx.notify();
617        Ok(())
618    }
619
620    /// Scroll the line `range` starts on into view, `range` indexing the
621    /// current rendered text, as with [`Self::set_range_highlights`].
622    ///
623    /// A [`Self::scrollable`] view scrolls itself. Otherwise the nearest
624    /// enclosing `gpui::list` scrolls, as long as the row holding the view is
625    /// laid out, so scroll to that row first when it may be off screen. Any
626    /// other scroll container scrolls through
627    /// [`TextView::on_reveal`](crate::text::TextView::on_reveal). An empty
628    /// range reveals the line of its position; a range in text that belongs
629    /// to no block's text, such as a custom block's, scrolls its whole block
630    /// into a scrollable view.
631    ///
632    /// Only the latest reveal is carried out. It follows the content as
633    /// [range highlights](Self::set_range_highlights) do, and it is dropped
634    /// when its text changes, when the view clamps its lines, or when it
635    /// cannot be shown within a second, so it never scrolls long after it
636    /// was asked for. A whole block taller than the view, scrolled to from
637    /// below, shows its end.
638    ///
639    /// Revealing is best effort: `Ok(())` means the range is valid for the
640    /// current text and the request was taken, not that the view has
641    /// scrolled. A dropped request is not reported.
642    pub fn reveal_range(
643        &mut self,
644        range: Range<usize>,
645        cx: &mut Context<Self>,
646    ) -> Result<(), RangeHighlightError> {
647        if self.format != TextViewFormat::Markdown {
648            return Err(RangeHighlightError::Unsupported);
649        }
650        let text = self.rendered_text();
651        if text.is_empty() && range == (0..0) {
652            // Nothing to reveal in an empty view.
653            self.pending_reveal = None;
654            return Ok(());
655        }
656        let now = cx.background_executor().now();
657        self.pending_reveal = Some(
658            PendingReveal::new(&text, &range, now).ok_or(RangeHighlightError::InvalidRange(0))?,
659        );
660        cx.notify();
661        Ok(())
662    }
663
664    /// Remove all range highlights.
665    pub fn clear_range_highlights(&mut self, cx: &mut Context<Self>) {
666        if self.range_highlights.take().is_some() {
667            cx.notify();
668        }
669    }
670
671    /// Carry the range highlights over to `new`, the document the update of
672    /// `revision` parsed, before it replaces the current one.
673    ///
674    /// `append` is whether that update parsed only appended text onto the
675    /// document before it. Only when no update has replaced the text since
676    /// the current document was committed was that document the current one,
677    /// so only then do the blocks before its last stay unchanged.
678    fn reconcile_range_highlights(&mut self, new: &ParsedDocument, revision: usize, append: bool) {
679        let tail_only = append && self.full_update_revision <= self.committed_revision;
680        self.committed_revision = revision;
681        self.rendered_index = Arc::default();
682        if self.range_highlights.is_some() || self.pending_reveal.is_some() {
683            let remap = LeafRemap::new(&self.parsed_content.document, new, tail_only);
684            self.range_highlights = self
685                .range_highlights
686                .take()
687                .and_then(|highlights| highlights.remap(&remap))
688                .map(Arc::new);
689            self.pending_reveal = self
690                .pending_reveal
691                .take()
692                .and_then(|reveal| reveal.remap(&remap));
693        }
694    }
695
696    /// Tell the scrollable list which blocks an append replaced, before `new`,
697    /// the appended document, replaces the current one.
698    ///
699    /// An append parses only the current document's last block again, so the
700    /// blocks before it keep their spans and their measured heights. Without
701    /// this, `render_root` resets the list whenever the block count changes,
702    /// which scrolled a streaming view back to the top and measured every
703    /// block again on each new block. Only a grown block count is handled
704    /// here; anything else is left to that reset.
705    fn splice_appended_blocks(&self, new: &ParsedDocument) {
706        let old = &self.parsed_content.document.blocks;
707        let (old_count, new_count) = (old.len(), new.blocks.len());
708        if old_count == 0 || new_count <= old_count || self.list_state.item_count() != old_count {
709            return;
710        }
711
712        // The last block is always measured again: it was parsed again, and
713        // it is no longer the last one.
714        let unchanged = old
715            .iter()
716            .zip(new.blocks.iter())
717            .take(old_count - 1)
718            .take_while(|(old_block, new_block)| {
719                old_block
720                    .span()
721                    .is_some_and(|span| new_block.span() == Some(span))
722            })
723            .count();
724        // Keeps the replaced blocks' heights as hints and the scroll offset
725        // inside them, and has the next layout measure only the blocks that
726        // are not measured.
727        self.list_state.remeasure_items(unchanged..old_count);
728        self.list_state
729            .splice(old_count..old_count, new_count - old_count);
730    }
731
732    /// Save bounds and unselect if bounds changed.
733    pub(super) fn update_bounds(&mut self, bounds: Bounds<Pixels>, _cx: &mut App) {
734        self.bounds = bounds;
735    }
736
737    /// The index of the top-level block at `content_y`, in this view's content
738    /// coordinates (the same space the base selection endpoint stores its point in).
739    ///
740    /// Only laid-out blocks can be located, which is enough for a selection
741    /// endpoint: the user can only put one where they can see it. Returns
742    /// `None` for a view that is not virtualized, where every block paints and
743    /// the range is not needed.
744    pub(super) fn block_ix_at(&self, content_y: Pixels) -> Option<usize> {
745        if !self.scrollable {
746            return None;
747        }
748
749        let origin = self.bounds.origin.y + self.scroll_offset().y;
750        let count = self.list_state.item_count();
751        let mut ix = self.list_state.logical_scroll_top().item_ix;
752        while ix < count {
753            let bounds = self.list_state.bounds_for_item(ix)?;
754            if content_y < bounds.bottom() - origin {
755                return Some(ix);
756            }
757            ix += 1;
758        }
759
760        count.checked_sub(1)
761    }
762
763    #[doc(hidden)]
764    pub fn bounds(&self) -> Bounds<Pixels> {
765        self.bounds
766    }
767
768    #[doc(hidden)]
769    pub fn list_state(&self) -> &ListState {
770        &self.list_state
771    }
772
773    #[doc(hidden)]
774    pub fn is_selecting(&self) -> bool {
775        self.is_selecting
776    }
777
778    #[doc(hidden)]
779    pub fn focus_handle(&self) -> &FocusHandle {
780        &self.focus_handle
781    }
782
783    /// Whether this view has a view-local selection (select-all, multi-click, or override),
784    /// independent of the window-level selection.
785    pub(super) fn has_view_selection(&self) -> bool {
786        self.select_all
787            || self.multi_click_selection.is_some()
788            || self.selected_text_override.is_some()
789    }
790
791    pub(super) fn stop_auto_scroll(&mut self) {
792        self.auto_scroll.stop();
793    }
794
795    pub(super) fn reset_selection(&mut self) {
796        self.preserve_inline_selection = false;
797        self.multi_click_selection = None;
798        self.selected_text_override = None;
799        self.select_all = false;
800        self.is_selecting = false;
801        self.auto_scroll.stop();
802        // Clear the inline selection state synchronously, so offscreen
803        // (virtualized) views that won't repaint don't leak stale selection
804        // text into a new cross-view copy.
805        self.parsed_content.document.clear_selection();
806    }
807
808    fn reset_selection_and_adapter(&mut self, cx: &mut App) {
809        self.reset_selection();
810        self.selection_adapter.set_local_selection(false, cx);
811    }
812
813    /// Clear the current text selection.
814    pub fn clear_selection(&mut self, cx: &mut Context<Self>) {
815        self.reset_selection_and_adapter(cx);
816        cx.notify();
817    }
818
819    pub(super) fn scroll_offset(&self) -> Point<Pixels> {
820        if self.scrollable {
821            self.list_state.scroll_px_offset_for_scrollbar()
822        } else {
823            Point::default()
824        }
825    }
826
827    /// Select all rendered text in this view.
828    pub fn select_all(&mut self, cx: &mut Context<Self>) {
829        self.multi_click_selection = None;
830        self.selected_text_override = None;
831        self.select_all = true;
832        self.is_selecting = false;
833        self.auto_scroll.stop();
834        self.selection_adapter.set_local_selection(true, cx);
835        cx.notify();
836    }
837
838    pub(crate) fn set_multi_click_selection(
839        &mut self,
840        pos: Point<Pixels>,
841        kind: TextViewMultiClickKind,
842        selected_text: String,
843        cx: &mut App,
844    ) {
845        self.preserve_inline_selection = false;
846        let scroll_offset = self.scroll_offset();
847        let pos = pos - self.bounds.origin - scroll_offset;
848        self.multi_click_selection = Some(TextViewMultiClickSelection {
849            pos,
850            kind,
851            line_bounds: None,
852        });
853        self.selected_text_override = Some(selected_text);
854        self.select_all = false;
855        self.is_selecting = false;
856        self.auto_scroll.stop();
857        self.selection_adapter.set_local_selection(true, cx);
858    }
859
860    pub(crate) fn set_multi_click_line(&mut self, bounds: Bounds<Pixels>, cx: &mut App) {
861        self.set_multi_click_selection(
862            bounds.center(),
863            TextViewMultiClickKind::Line,
864            String::new(),
865            cx,
866        );
867        self.selected_text_override = None;
868        let offset = self.bounds.origin + self.scroll_offset();
869        if let Some(selection) = self.multi_click_selection.as_mut() {
870            selection.line_bounds = Some(Bounds::new(bounds.origin - offset, bounds.size));
871        }
872    }
873
874    pub(super) fn set_auto_scroll(&mut self, delta: Option<Pixels>, cx: &mut Context<Self>) {
875        self.auto_scroll.set(delta, cx, |delta, state, cx| {
876            state.list_state.scroll_by(delta);
877            cx.notify();
878        });
879    }
880
881    /// Return the window selection (anchor, cursor) in window coordinates if
882    /// this view participates in it.
883    ///
884    /// Single-view fast path: when both endpoints are anchored inside one
885    /// TextView, only that view participates (identical to the previous
886    /// per-view behavior).
887    pub(crate) fn selection_points(&self, cx: &App) -> Option<(Point<Pixels>, Point<Pixels>)> {
888        if !self.selectable {
889            return None;
890        }
891        self.selection_adapter.selection_points(cx)
892    }
893
894    pub(crate) fn has_selection(&self, cx: &App) -> bool {
895        self.has_view_selection() || self.selection_points(cx).is_some()
896    }
897
898    pub(super) fn on_action_select_all(
899        &mut self,
900        _: &SelectAll,
901        _: &mut Window,
902        cx: &mut Context<Self>,
903    ) {
904        if !self.selectable {
905            cx.propagate();
906            return;
907        }
908
909        self.select_all(cx);
910    }
911
912    pub(crate) fn is_selectable(&self) -> bool {
913        self.selectable
914    }
915
916    pub(crate) fn is_all_selected(&self) -> bool {
917        self.select_all
918    }
919
920    pub(crate) fn multi_click_selection(&self) -> Option<TextViewMultiClickSelection> {
921        let scroll_offset = self.scroll_offset();
922        self.multi_click_selection.map(|selection| {
923            let pos = selection.pos + scroll_offset + self.bounds.origin;
924            let line_bounds = selection.line_bounds.map(|bounds| {
925                Bounds::new(
926                    bounds.origin + scroll_offset + self.bounds.origin,
927                    bounds.size,
928                )
929            });
930            TextViewMultiClickSelection {
931                pos,
932                line_bounds,
933                ..selection
934            }
935        })
936    }
937}
938
939#[derive(Clone, Copy, Debug, PartialEq)]
940pub(crate) struct TextViewMultiClickSelection {
941    pub(crate) pos: Point<Pixels>,
942    pub(crate) kind: TextViewMultiClickKind,
943    pub(crate) line_bounds: Option<Bounds<Pixels>>,
944}
945
946#[derive(Clone, Copy, Debug, PartialEq, Eq)]
947pub(crate) enum TextViewMultiClickKind {
948    Word,
949    Paragraph,
950    Line,
951}
952
953impl TextViewState {
954    /// Starts a frame of the pending reveal: the line to hand to rendering,
955    /// and the block a scrollable view has to scroll to first, because it is
956    /// off screen or the reveal is of a whole block.
957    fn reveal_frame(
958        &mut self,
959        now: Instant,
960        window: &mut Window,
961    ) -> (Option<RevealRequest>, Option<usize>) {
962        if self.max_lines.is_some()
963            || self
964                .pending_reveal
965                .as_ref()
966                .is_some_and(|reveal| reveal.is_expired(now))
967        {
968            self.pending_reveal = None;
969        }
970        let Some(pending) = &self.pending_reveal else {
971            return (None, None);
972        };
973
974        let block_ix = pending.block_ix(&self.parsed_content.document);
975        let block_off_screen = |ix: usize| {
976            let viewport = self.list_state.viewport_bounds();
977            self.list_state.bounds_for_item(ix).is_none_or(|item| {
978                item.bottom() <= viewport.top() || item.top() >= viewport.bottom()
979            })
980        };
981        let reveal_block = block_ix.filter(|ix| {
982            self.scrollable
983                && (pending.is_block() || !pending.was_laid_out())
984                && block_off_screen(*ix)
985        });
986        let request = pending.request();
987        if pending.is_block() {
988            // A block has no line to wait for.
989            self.pending_reveal = None;
990        } else {
991            window.request_animation_frame();
992        }
993        (request, reveal_block)
994    }
995}
996
997impl Render for TextViewState {
998    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
999        let typography = (window.text_style(), window.rem_size());
1000        if self
1001            .layout_text_style
1002            .as_ref()
1003            .is_some_and(|previous| previous != &typography)
1004        {
1005            // ListState invalidates its cached rows for width changes, but does
1006            // not know that an inherited font or rem change affects offscreen
1007            // inline metrics. Retain logical selections just as for resources.
1008            self.preserve_inline_selection = true;
1009            self.compatible_layout_update = true;
1010            self.invalidate_measured_heights();
1011        }
1012        self.layout_text_style = Some(typography);
1013        let state = cx.entity();
1014        let stream_fade = self.stream_fade.frame(Instant::now(), cx.reduce_motion());
1015        if stream_fade.is_some() && self.fade_tick.is_none() {
1016            self.fade_tick = Some(cx.spawn(async move |this, cx| {
1017                cx.background_executor().timer(STREAM_FADE_TICK).await;
1018                _ = this.update(cx, |state, cx| {
1019                    state.fade_tick = None;
1020                    cx.notify();
1021                });
1022            }));
1023        }
1024        let (reveal, reveal_block) = self.reveal_frame(cx.background_executor().now(), window);
1025        // Built every frame, so everything in it is shared, not copied.
1026        let node_cx = NodeContext {
1027            offset: self.parsed_content.node_cx.offset,
1028            link_refs: self.parsed_content.node_cx.link_refs.clone(),
1029            style: self.text_view_style.clone(),
1030            code_block_actions: self.code_block_actions.clone(),
1031            code_block_highlighter: self.code_block_highlighter.clone(),
1032            table_actions: self.table_actions.clone(),
1033            link_click_handler: self.link_click_handler.clone(),
1034            image_source: self.image_source.clone(),
1035            markdown_extensions: self.markdown_extensions.clone(),
1036            stream_fade,
1037            range_highlights: self.range_highlights.clone(),
1038            reveal,
1039        };
1040
1041        let content = v_flex()
1042            .w_full()
1043            // Clamped content must keep its natural height: stretching it to
1044            // the capped box would hide the overflow the clamp has to measure.
1045            .when(self.max_lines.is_none(), |this| this.h_full())
1046            .map(|this| match &self.parsed_error {
1047                None => this.child(self.parsed_content.document.render_root(
1048                    if self.scrollable {
1049                        Some(self.list_state.clone())
1050                    } else {
1051                        None
1052                    },
1053                    &node_cx,
1054                    window,
1055                    cx,
1056                )),
1057                Some(err) => this.child(
1058                    v_flex()
1059                        .gap_1()
1060                        .child("Failed to parse content")
1061                        .child(err.to_string()),
1062                ),
1063            })
1064            .on_prepaint(move |bounds, window, cx| {
1065                let (
1066                    size_changed,
1067                    selection_involves_view,
1068                    has_selection_snapshot,
1069                    is_selecting,
1070                    compatible_layout_update,
1071                ) = {
1072                    let state = state.read(cx);
1073                    (
1074                        state.bounds().size != bounds.size,
1075                        state.selection_adapter.is_part_of_window_selection(cx),
1076                        state.selection_adapter.has_selection_snapshot(cx),
1077                        state.is_selecting,
1078                        state.compatible_layout_update,
1079                    )
1080                };
1081                let mut revision_changed = false;
1082                state.update(cx, |state, cx| {
1083                    revision_changed = state
1084                        .selection_adapter
1085                        .update_layout_revision(state.selection_revision, state.is_selecting);
1086                    state.update_bounds(bounds, cx);
1087                    state.compatible_layout_update = false;
1088                });
1089                if !is_selecting
1090                    && ((size_changed && selection_involves_view && !compatible_layout_update)
1091                        || (revision_changed && has_selection_snapshot))
1092                {
1093                    TextSelection::clear(window, cx);
1094                }
1095            });
1096        // After `render_root`, which resets the list when the block count
1097        // changed other than by an append, and with it the scroll position.
1098        if let Some(block_ix) = reveal_block {
1099            self.list_state.scroll_to_reveal_item(block_ix);
1100        }
1101        content
1102    }
1103}
1104
1105#[derive(Clone, PartialEq, Default)]
1106pub(crate) struct ParsedContent {
1107    pub(crate) document: ParsedDocument,
1108    pub(crate) node_cx: node::NodeContext,
1109}
1110
1111struct UpdateFuture {
1112    format: TextViewFormat,
1113    content: ParsedContent,
1114    rx: Pin<Box<Receiver<UpdateOptions>>>,
1115    tx_result: Sender<ParsedUpdate>,
1116}
1117
1118impl UpdateFuture {
1119    fn new(
1120        format: TextViewFormat,
1121        rx: Receiver<UpdateOptions>,
1122        tx_result: Sender<ParsedUpdate>,
1123    ) -> Self {
1124        Self {
1125            format,
1126            content: Default::default(),
1127            rx: Box::pin(rx),
1128            tx_result,
1129        }
1130    }
1131}
1132
1133impl Future for UpdateFuture {
1134    type Output = ();
1135
1136    fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
1137        loop {
1138            match self.rx.as_mut().poll_next(cx) {
1139                Poll::Ready(Some(mut options)) => {
1140                    let hit_coalesce_budget =
1141                        merge_pending_options(&mut options, self.rx.as_ref().get_ref());
1142
1143                    let res = parse_content(self.format, self.content.clone(), &options);
1144                    if let Ok(content) = &res {
1145                        self.content = content.clone();
1146                    }
1147                    _ = self.tx_result.try_send(ParsedUpdate {
1148                        revision: options.revision,
1149                        full_parse: !options.append,
1150                        selection_compatible: options.mode == ParseMode::Compatible,
1151                        baseline_ack: options.mode == ParseMode::BaselineAck,
1152                        result: res,
1153                    });
1154                    if hit_coalesce_budget {
1155                        cx.waker().wake_by_ref();
1156                        return Poll::Pending;
1157                    }
1158                    continue;
1159                }
1160                Poll::Ready(None) => return Poll::Ready(()),
1161                Poll::Pending => return Poll::Pending,
1162            }
1163        }
1164    }
1165}
1166
1167#[derive(Clone)]
1168struct UpdateOptions {
1169    revision: usize,
1170    pending_text: String,
1171    append: bool,
1172    mode: ParseMode,
1173    markdown_extensions: Arc<MarkdownExtensions>,
1174}
1175
1176impl UpdateOptions {
1177    fn merge(&mut self, next: UpdateOptions) {
1178        if next.append {
1179            self.pending_text.push_str(&next.pending_text);
1180            self.revision = next.revision;
1181            if self.mode != ParseMode::Replace {
1182                self.mode = ParseMode::Compatible;
1183            }
1184        } else {
1185            *self = next;
1186        }
1187    }
1188}
1189
1190struct ParsedUpdate {
1191    revision: usize,
1192    full_parse: bool,
1193    selection_compatible: bool,
1194    baseline_ack: bool,
1195    result: Result<ParsedContent, SharedString>,
1196}
1197
1198#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1199enum ParseMode {
1200    BaselineAck,
1201    Replace,
1202    Compatible,
1203}
1204
1205fn merge_pending_options(options: &mut UpdateOptions, rx: &Receiver<UpdateOptions>) -> bool {
1206    let mut update_count = 1;
1207
1208    while update_count < MAX_COALESCED_UPDATES_PER_PARSE {
1209        match rx.try_recv() {
1210            Ok(next_options) => {
1211                options.merge(next_options);
1212                update_count += 1;
1213            }
1214            Err(_) => return false,
1215        }
1216    }
1217
1218    true
1219}
1220
1221fn parse_content(
1222    format: TextViewFormat,
1223    mut content: ParsedContent,
1224    options: &UpdateOptions,
1225) -> Result<ParsedContent, SharedString> {
1226    let mut node_cx = NodeContext {
1227        markdown_extensions: options.markdown_extensions.clone(),
1228        ..NodeContext::default()
1229    };
1230
1231    // Re-parse the last block together with the appended text, so a block the
1232    // new text continues (an unclosed list, a fenced code block) is not split
1233    // in two. A block without a span cannot be located in `source` — the HTML
1234    // parser never records spans — so it is left in place and only the
1235    // appended text is parsed, positioned at the end of the current source.
1236    let last_span = options
1237        .append
1238        .then(|| {
1239            content
1240                .document
1241                .blocks
1242                .last()
1243                .and_then(|block| block.span())
1244        })
1245        .flatten();
1246
1247    let mut source = String::new();
1248    if let Some(span) = last_span {
1249        Arc::make_mut(&mut content.document.blocks).pop();
1250        node_cx.offset = span.start;
1251        source.push_str(&content.document.source[span.start..]);
1252        source.push_str(&options.pending_text);
1253    } else {
1254        if options.append {
1255            node_cx.offset = content.document.source.len();
1256        }
1257        source.push_str(&options.pending_text);
1258    }
1259
1260    let new_document = match format {
1261        TextViewFormat::Markdown => format::markdown::parse(&source, &mut node_cx),
1262        TextViewFormat::Html => format::html::parse(&source, &mut node_cx),
1263    }?;
1264
1265    if options.append {
1266        content.document.source =
1267            format!("{}{}", content.document.source, options.pending_text).into();
1268        Arc::make_mut(&mut content.document.blocks)
1269            .extend(Arc::unwrap_or_clone(new_document.blocks));
1270    } else {
1271        content.document = new_document;
1272    }
1273
1274    Ok(content)
1275}
1276
1277#[cfg(test)]
1278mod tests {
1279    use super::*;
1280    use crate::text::{MarkdownNode, node::BlockNode};
1281    use gpui::TestAppContext;
1282
1283    mod stream_fade {
1284        use std::{cell::Cell, ops::Range, rc::Rc, time::Duration};
1285
1286        use gpui::{Entity, TestAppContext};
1287
1288        use super::super::*;
1289        use crate::{motion::Easing, text::stream_fade::TextLeafKey};
1290
1291        /// Long enough that a debug test cannot outrun the fade before it
1292        /// samples the first frame.
1293        const FADE: Duration = Duration::from_secs(10);
1294
1295        fn fading_state(markdown: &str, cx: &mut TestAppContext) -> Entity<TextViewState> {
1296            cx.update(crate::init);
1297            let state = cx.update(|cx| {
1298                cx.new(|cx| {
1299                    TextViewState::markdown(markdown, cx).motion(
1300                        TextViewMotion::default()
1301                            .with_stream_fade(FADE)
1302                            .with_stream_fade_easing(Easing::Linear),
1303                    )
1304                })
1305            });
1306            cx.run_until_parked();
1307            state
1308        }
1309
1310        /// The fade ranges of `key` sampled right now, or `None` when the
1311        /// state has nothing fading.
1312        fn fades(
1313            state: &Entity<TextViewState>,
1314            key: TextLeafKey,
1315            cx: &mut TestAppContext,
1316        ) -> Option<Vec<Range<usize>>> {
1317            state.update(cx, |state, _| {
1318                let frame = state.stream_fade.frame(Instant::now(), false)?;
1319                let fades = frame.fades(key)?;
1320                assert!(
1321                    fades.iter().all(|(_, fade_out)| *fade_out > 0.9),
1322                    "a fade sampled right after it starts is still transparent: {fades:?}"
1323                );
1324                Some(fades.iter().map(|(range, _)| range.clone()).collect())
1325            })
1326        }
1327
1328        #[gpui::test]
1329        fn push_str_fades_only_the_appended_text(cx: &mut TestAppContext) {
1330            let state = fading_state("hello", cx);
1331            state.update(cx, |state, cx| state.push_str(" world", cx));
1332            cx.run_until_parked();
1333
1334            assert_eq!(fades(&state, TextLeafKey::block(0), cx), Some(vec![5..11]));
1335
1336            state.update(cx, |state, _| {
1337                let later = Instant::now() + FADE + Duration::from_secs(1);
1338                assert!(state.stream_fade.frame(later, false).is_none());
1339                assert!(state.stream_fade.frame(Instant::now(), false).is_none());
1340            });
1341        }
1342
1343        #[gpui::test]
1344        fn set_text_extending_the_text_fades_like_push_str(cx: &mut TestAppContext) {
1345            let state = fading_state("hello", cx);
1346            state.update(cx, |state, cx| state.set_text("hello world", cx));
1347            cx.run_until_parked();
1348
1349            assert_eq!(fades(&state, TextLeafKey::block(0), cx), Some(vec![5..11]));
1350        }
1351
1352        #[gpui::test]
1353        fn set_text_replacing_the_text_shows_it_at_once(cx: &mut TestAppContext) {
1354            let state = fading_state("hello", cx);
1355            state.update(cx, |state, cx| state.push_str(" world", cx));
1356            cx.run_until_parked();
1357            assert!(fades(&state, TextLeafKey::block(0), cx).is_some());
1358
1359            state.update(cx, |state, cx| state.set_text("other", cx));
1360            cx.run_until_parked();
1361
1362            assert_eq!(fades(&state, TextLeafKey::block(0), cx), None);
1363        }
1364
1365        #[gpui::test]
1366        fn completed_markup_refades_from_the_divergence(cx: &mut TestAppContext) {
1367            // The paragraph renders `text` (trailing space trimmed), then
1368            // `text **bo` literally.
1369            let state = fading_state("text ", cx);
1370            state.update(cx, |state, cx| state.push_str("**bo", cx));
1371            cx.run_until_parked();
1372            assert_eq!(fades(&state, TextLeafKey::block(0), cx), Some(vec![4..9]));
1373
1374            // `text **bold**` renders `text bold`: the space keeps its fade,
1375            // the glyphs from byte 5 on changed and fade again as one run.
1376            state.update(cx, |state, cx| state.push_str("ld**", cx));
1377            cx.run_until_parked();
1378            assert_eq!(
1379                fades(&state, TextLeafKey::block(0), cx),
1380                Some(vec![4..5, 5..9])
1381            );
1382        }
1383
1384        #[gpui::test]
1385        fn without_stagger_an_update_fades_as_one_chunk(cx: &mut TestAppContext) {
1386            let state = fading_state("hello", cx);
1387            state.update(cx, |state, cx| state.push_str(" one two three", cx));
1388            cx.run_until_parked();
1389
1390            assert_eq!(fades(&state, TextLeafKey::block(0), cx), Some(vec![5..19]));
1391        }
1392
1393        #[gpui::test]
1394        fn words_of_one_update_start_one_after_another(cx: &mut TestAppContext) {
1395            let stagger = Duration::from_millis(100);
1396            let state = fading_state("hello", cx);
1397            state.update(cx, |state, _| {
1398                state.set_motion(
1399                    TextViewMotion::default()
1400                        .with_stream_fade(FADE)
1401                        .with_stream_fade_stagger(stagger)
1402                        .with_stream_fade_easing(Easing::Linear),
1403                )
1404            });
1405            state.update(cx, |state, cx| state.push_str(" one two three", cx));
1406            cx.run_until_parked();
1407
1408            state.update(cx, |state, _| {
1409                let frame = state
1410                    .stream_fade
1411                    .frame(Instant::now() + stagger * 3, false)
1412                    .expect("words still fading");
1413                let fades = frame.fades(TextLeafKey::block(0)).expect("paragraph fades");
1414                let ranges: Vec<_> = fades.iter().map(|(range, _)| range.clone()).collect();
1415                assert_eq!(ranges, vec![5..10, 10..14, 14..19]);
1416                // A later word has faded less, so it is still more transparent.
1417                assert!(
1418                    fades[0].1 < fades[1].1 && fades[1].1 < fades[2].1,
1419                    "{fades:?}"
1420                );
1421            });
1422        }
1423
1424        #[gpui::test]
1425        fn a_new_paragraph_fades_as_a_whole(cx: &mut TestAppContext) {
1426            let state = fading_state("first", cx);
1427            state.update(cx, |state, cx| state.push_str("\n\nsecond", cx));
1428            cx.run_until_parked();
1429
1430            assert_eq!(fades(&state, TextLeafKey::block(0), cx), None);
1431            assert_eq!(fades(&state, TextLeafKey::block(7), cx), Some(vec![0..6]));
1432        }
1433
1434        #[gpui::test]
1435        fn code_block_text_fades_by_block(cx: &mut TestAppContext) {
1436            let state = fading_state("```rs\nlet", cx);
1437            state.update(cx, |state, cx| state.push_str(" x", cx));
1438            cx.run_until_parked();
1439
1440            assert_eq!(fades(&state, TextLeafKey::block(0), cx), Some(vec![3..5]));
1441        }
1442
1443        #[gpui::test]
1444        fn table_cells_fade_by_ordinal(cx: &mut TestAppContext) {
1445            let state = fading_state("| a | b |\n|---|---|\n| c | d", cx);
1446            state.update(cx, |state, cx| state.push_str("e |", cx));
1447            cx.run_until_parked();
1448
1449            assert_eq!(fades(&state, TextLeafKey::table_cell(0, 2), cx), None);
1450            assert_eq!(
1451                fades(&state, TextLeafKey::table_cell(0, 3), cx),
1452                Some(vec![1..2])
1453            );
1454        }
1455
1456        #[gpui::test]
1457        fn zero_duration_records_nothing(cx: &mut TestAppContext) {
1458            cx.update(crate::init);
1459            let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("hello", cx)));
1460            cx.run_until_parked();
1461            state.update(cx, |state, cx| state.push_str(" world", cx));
1462            cx.run_until_parked();
1463
1464            state.update(cx, |state, _| {
1465                assert!(state.stream_fade.frame(Instant::now(), false).is_none());
1466            });
1467        }
1468
1469        #[gpui::test]
1470        fn reduced_motion_drops_the_fade(cx: &mut TestAppContext) {
1471            let state = fading_state("hello", cx);
1472            state.update(cx, |state, cx| state.push_str(" world", cx));
1473            cx.run_until_parked();
1474
1475            state.update(cx, |state, _| {
1476                assert!(state.stream_fade.frame(Instant::now(), true).is_none());
1477                assert!(state.stream_fade.frame(Instant::now(), false).is_none());
1478            });
1479        }
1480
1481        #[gpui::test]
1482        fn a_fade_repaints_on_a_timer_until_nothing_fades(cx: &mut TestAppContext) {
1483            struct FadeRoot(Entity<TextViewState>);
1484            impl Render for FadeRoot {
1485                fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1486                    crate::text::TextView::new(&self.0)
1487                }
1488            }
1489
1490            let state = fading_state("hello", cx);
1491            let (_, cx) = cx.add_window_view(|_, _| FadeRoot(state.clone()));
1492            let notifies = Rc::new(Cell::new(0));
1493            let _subscription = cx.update(|_, cx| {
1494                let notifies = notifies.clone();
1495                cx.observe(&state, move |_, _| notifies.set(notifies.get() + 1))
1496            });
1497
1498            state.update(cx, |state, cx| state.push_str(" world", cx));
1499            cx.run_until_parked();
1500            cx.update(|window, cx| window.draw(cx).clear(cx));
1501            let before = notifies.get();
1502
1503            cx.executor().advance_clock(STREAM_FADE_TICK / 2);
1504            cx.run_until_parked();
1505            assert_eq!(notifies.get(), before);
1506
1507            // Each tick repaints once, and that frame schedules the next tick.
1508            for tick in 1..=3 {
1509                cx.executor().advance_clock(STREAM_FADE_TICK);
1510                cx.run_until_parked();
1511                assert_eq!(notifies.get(), before + tick);
1512            }
1513
1514            // Replacing the text drops the fade, so the ticks stop.
1515            state.update(cx, |state, cx| state.set_text("other", cx));
1516            cx.run_until_parked();
1517            cx.executor().advance_clock(STREAM_FADE_TICK);
1518            cx.run_until_parked();
1519            let after = notifies.get();
1520            cx.executor().advance_clock(STREAM_FADE_TICK * 10);
1521            cx.run_until_parked();
1522            assert_eq!(notifies.get(), after);
1523            assert!(state.read_with(cx, |state, _| state.fade_tick.is_none()));
1524        }
1525    }
1526
1527    #[gpui::test]
1528    fn small_full_replace_parses_before_background_executor_runs(cx: &mut TestAppContext) {
1529        cx.update(crate::init);
1530        let markdown = "# ready";
1531        let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown(markdown, cx)));
1532
1533        state.read_with(cx, |state, _| {
1534            assert_eq!(state.source().as_str(), markdown);
1535            assert_eq!(state.parsed_content.document.blocks.len(), 1);
1536        });
1537    }
1538
1539    #[gpui::test]
1540    fn large_markdown_and_html_full_replacements_wait_for_background_executor(
1541        cx: &mut TestAppContext,
1542    ) {
1543        cx.update(crate::init);
1544        let markdown = "# x\n\n".repeat(MAX_SYNC_FULL_REPLACE_BYTES / 5 + 1);
1545        let html = format!("<p>{}</p>", "x".repeat(MAX_SYNC_FULL_REPLACE_BYTES + 1));
1546        assert!(markdown.len() > MAX_SYNC_FULL_REPLACE_BYTES);
1547        assert!(html.len() > MAX_SYNC_FULL_REPLACE_BYTES);
1548
1549        let (markdown_state, html_state) = cx.update(|cx| {
1550            (
1551                cx.new(|cx| TextViewState::markdown(&markdown, cx)),
1552                cx.new(|cx| TextViewState::html(&html, cx)),
1553            )
1554        });
1555
1556        markdown_state.read_with(cx, |state, _| {
1557            assert_eq!(state.text.as_str(), markdown.as_str());
1558            assert!(state.source().as_str().is_empty());
1559            assert!(state.parsed_content.document.blocks.is_empty());
1560        });
1561        html_state.read_with(cx, |state, _| {
1562            assert_eq!(state.text.as_str(), html.as_str());
1563            assert!(state.source().as_str().is_empty());
1564            assert!(state.parsed_content.document.blocks.is_empty());
1565        });
1566
1567        cx.run_until_parked();
1568
1569        markdown_state.read_with(cx, |state, _| {
1570            assert_eq!(state.source().as_str(), markdown.as_str());
1571            assert!(!state.parsed_content.document.blocks.is_empty());
1572        });
1573        html_state.read_with(cx, |state, _| {
1574            assert_eq!(state.source().as_str(), html.as_str());
1575            assert!(!state.parsed_content.document.blocks.is_empty());
1576        });
1577    }
1578
1579    #[gpui::test]
1580    fn inline_source_ranges_follow_streamed_tail_reparsing(cx: &mut TestAppContext) {
1581        cx.update(crate::init);
1582        let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("中文 $a$\n\n尾 $x", cx)));
1583        state.update(cx, |state, cx| {
1584            let extensions = MarkdownExtensions::default().plugin(
1585                crate::text::markdown_ext::TestInlinePlugin::new("test").parse_with(|node, _| {
1586                    let markdown::mdast::Node::InlineMath(math) = node else {
1587                        return None;
1588                    };
1589                    Some(super::super::MarkdownNode::new("formula", ()).text(math.value.clone()))
1590                }),
1591            );
1592            state.set_markdown_extensions(Arc::new(extensions), cx);
1593        });
1594        cx.run_until_parked();
1595        state.update(cx, |state, cx| state.push_str("^2$ 后", cx));
1596        cx.run_until_parked();
1597        state.read_with(cx, |state, _| {
1598            let source = "中文 $a$\n\n尾 $x^2$ 后";
1599            assert_eq!(state.source().as_str(), source);
1600            let objects: Vec<_> = state
1601                .parsed_content
1602                .document
1603                .blocks
1604                .iter()
1605                .flat_map(|block| {
1606                    let super::super::node::BlockNode::Paragraph(paragraph) = block else {
1607                        panic!()
1608                    };
1609                    paragraph
1610                        .children
1611                        .iter()
1612                        .filter_map(|node| node.custom.as_ref())
1613                })
1614                .collect();
1615            assert_eq!(objects.len(), 2);
1616            for (object, markdown) in objects.iter().zip(["$a$", "$x^2$"]) {
1617                let start = source.find(markdown).unwrap();
1618                assert_eq!(object.source_range(), Some(start..start + markdown.len()));
1619                assert_eq!(object.as_markdown(), markdown);
1620            }
1621        });
1622    }
1623
1624    #[gpui::test]
1625    fn async_full_replace_then_push_str_preserves_complete_source(cx: &mut TestAppContext) {
1626        cx.update(crate::init);
1627        let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("old", cx)));
1628        cx.run_until_parked();
1629
1630        let replacement = "x".repeat(MAX_SYNC_FULL_REPLACE_BYTES + 1);
1631        let expected = format!("{replacement} tail");
1632        state.update(cx, |state, cx| {
1633            state.set_text(&replacement, cx);
1634            state.push_str(" tail", cx);
1635        });
1636        cx.run_until_parked();
1637
1638        state.read_with(cx, |state, _| {
1639            assert_eq!(state.text.as_str(), expected.as_str());
1640            assert_eq!(state.source().as_str(), expected.as_str());
1641        });
1642    }
1643
1644    #[gpui::test]
1645    fn html_push_str_keeps_earlier_blocks(cx: &mut TestAppContext) {
1646        cx.update(crate::init);
1647        let state = cx.update(|cx| cx.new(|cx| TextViewState::html("<p>first</p>", cx)));
1648        cx.run_until_parked();
1649
1650        state.update(cx, |state, cx| {
1651            state.push_str("<p>second</p>", cx);
1652        });
1653        cx.run_until_parked();
1654
1655        state.read_with(cx, |state, _| {
1656            assert_eq!(state.source().as_str(), "<p>first</p><p>second</p>");
1657            let text = state
1658                .parsed_content
1659                .document
1660                .blocks
1661                .iter()
1662                .map(|block| block.text())
1663                .collect::<String>();
1664            assert!(text.contains("first"), "lost the first block: {text:?}");
1665            assert!(text.contains("second"), "lost the appended block: {text:?}");
1666        });
1667    }
1668
1669    #[gpui::test]
1670    fn element_text_of_the_same_string_is_not_compared_again(cx: &mut TestAppContext) {
1671        cx.update(crate::init);
1672        let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("", cx)));
1673        let revision = |cx: &mut TestAppContext| state.read_with(cx, |state, _| state.revision);
1674        let text = SharedString::from("hello");
1675
1676        state.update(cx, |state, cx| state.set_element_text(&text, cx));
1677        let parsed = revision(cx);
1678        assert!(parsed > 0);
1679
1680        // The same allocation again, and equal bytes in another allocation,
1681        // both leave the parsed text alone.
1682        state.update(cx, |state, cx| state.set_element_text(&text, cx));
1683        assert_eq!(revision(cx), parsed);
1684        let equal = SharedString::from("hello".to_string());
1685        state.update(cx, |state, cx| state.set_element_text(&equal, cx));
1686        assert_eq!(revision(cx), parsed);
1687
1688        // Once the state's text moved on, the element's string is set again.
1689        state.update(cx, |state, cx| {
1690            state.push_str(" world", cx);
1691            state.set_element_text(&equal, cx);
1692        });
1693        state.read_with(cx, |state, _| assert_eq!(state.text.as_str(), "hello"));
1694    }
1695
1696    #[gpui::test]
1697    fn set_text_then_push_str_appends_to_replaced_content(cx: &mut TestAppContext) {
1698        cx.update(crate::init);
1699        let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("old", cx)));
1700        cx.run_until_parked();
1701
1702        state.update(cx, |state, cx| {
1703            state.set_text("", cx);
1704            state.push_str("new", cx);
1705            state.push_str(" text", cx);
1706        });
1707        cx.run_until_parked();
1708
1709        state.read_with(cx, |state, _| {
1710            assert_eq!(state.text.as_str(), "new text");
1711            assert_eq!(state.source().as_str(), "new text");
1712        });
1713
1714        state.update(cx, |state, cx| {
1715            state.set_text("", cx);
1716        });
1717        cx.run_until_parked();
1718
1719        state.read_with(cx, |state, _| {
1720            assert_eq!(state.text.as_str(), "");
1721            assert_eq!(state.source().as_str(), "");
1722        });
1723    }
1724
1725    #[gpui::test]
1726    fn full_parse_coalesced_with_append_preserves_new_select_all(cx: &mut TestAppContext) {
1727        cx.update(crate::init);
1728        let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("old", cx)));
1729        cx.run_until_parked();
1730
1731        state.update(cx, |state, cx| {
1732            state.set_text("new", cx);
1733            state.push_str(" text", cx);
1734            state.select_all(cx);
1735        });
1736        cx.run_until_parked();
1737
1738        state.read_with(cx, |state, _| {
1739            assert!(state.select_all);
1740            assert_eq!(state.selected_text().trim(), "new text");
1741        });
1742    }
1743
1744    #[test]
1745    fn update_options_merge_keeps_latest_full_text() {
1746        let mut options = UpdateOptions {
1747            revision: 1,
1748            pending_text: "old".to_string(),
1749            append: true,
1750            mode: ParseMode::Compatible,
1751            markdown_extensions: Arc::default(),
1752        };
1753
1754        options.merge(UpdateOptions {
1755            revision: 2,
1756            pending_text: "new".to_string(),
1757            append: false,
1758            mode: ParseMode::BaselineAck,
1759            markdown_extensions: Arc::default(),
1760        });
1761        options.merge(UpdateOptions {
1762            revision: 3,
1763            pending_text: " text".to_string(),
1764            append: true,
1765            mode: ParseMode::Compatible,
1766            markdown_extensions: Arc::default(),
1767        });
1768
1769        assert_eq!(options.revision, 3);
1770        assert_eq!(options.pending_text, "new text");
1771        assert!(!options.append);
1772    }
1773
1774    #[test]
1775    fn append_merged_into_async_replace_remains_a_replacement() {
1776        let mut options = UpdateOptions {
1777            revision: 1,
1778            pending_text: "new".to_string(),
1779            append: false,
1780            mode: ParseMode::Replace,
1781            markdown_extensions: Arc::default(),
1782        };
1783
1784        options.merge(UpdateOptions {
1785            revision: 2,
1786            pending_text: " text".to_string(),
1787            append: true,
1788            mode: ParseMode::Compatible,
1789            markdown_extensions: Arc::default(),
1790        });
1791
1792        assert_eq!(options.revision, 2);
1793        assert_eq!(options.pending_text, "new text");
1794        assert!(!options.append);
1795        assert_eq!(options.mode, ParseMode::Replace);
1796    }
1797
1798    #[test]
1799    fn update_future_yields_before_coalescing_all_queued_updates() {
1800        let (tx, rx) = unbounded::<UpdateOptions>();
1801        let (tx_result, rx_result) = unbounded::<ParsedUpdate>();
1802        let total_updates = 128;
1803
1804        for revision in 1..=total_updates {
1805            tx.try_send(UpdateOptions {
1806                revision,
1807                pending_text: format!("{revision}\n"),
1808                append: revision != 1,
1809                mode: if revision == 1 {
1810                    ParseMode::BaselineAck
1811                } else {
1812                    ParseMode::Compatible
1813                },
1814                markdown_extensions: Arc::default(),
1815            })
1816            .unwrap();
1817        }
1818
1819        let mut future = Box::pin(UpdateFuture::new(TextViewFormat::Markdown, rx, tx_result));
1820        let waker = futures::task::noop_waker();
1821        let mut task_cx = std::task::Context::from_waker(&waker);
1822
1823        assert!(matches!(
1824            std::future::Future::poll(future.as_mut(), &mut task_cx),
1825            Poll::Pending
1826        ));
1827        let parsed_update = rx_result.try_recv().expect("parse result");
1828
1829        assert!(
1830            parsed_update.revision < total_updates,
1831            "single poll coalesced every queued update through revision {}",
1832            parsed_update.revision
1833        );
1834
1835        assert!(matches!(
1836            std::future::Future::poll(future.as_mut(), &mut task_cx),
1837            Poll::Pending
1838        ));
1839        let parsed_update = rx_result.try_recv().expect("next parse result");
1840        assert_eq!(parsed_update.revision, total_updates);
1841    }
1842
1843    #[gpui::test]
1844    fn select_all_returns_rendered_text(cx: &mut TestAppContext) {
1845        cx.update(crate::init);
1846        let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("**quick** value", cx)));
1847        cx.run_until_parked();
1848
1849        state.update(cx, |state, cx| {
1850            state.select_all(cx);
1851        });
1852
1853        state.read_with(cx, |state, _| {
1854            assert!(state.has_view_selection());
1855            assert_eq!(state.selected_text().trim(), "quick value");
1856        });
1857
1858        state.update(cx, |state, cx| {
1859            state.clear_selection(cx);
1860        });
1861
1862        state.read_with(cx, |state, _| {
1863            assert!(!state.has_view_selection());
1864            assert_eq!(state.selected_text(), "");
1865        });
1866    }
1867
1868    #[gpui::test]
1869    fn select_all_in_source_format_returns_source(cx: &mut TestAppContext) {
1870        cx.update(crate::init);
1871        let markdown = "**quick** value";
1872        let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown(markdown, cx)));
1873        cx.run_until_parked();
1874
1875        state.update(cx, |state, cx| state.select_all(cx));
1876
1877        // The default (plain) mode strips the markup.
1878        state.read_with(cx, |state, _| {
1879            assert_eq!(state.selected_text().trim(), "quick value");
1880        });
1881
1882        state.update(cx, |state, cx| {
1883            state.set_selection_format(SelectionFormat::Source, cx)
1884        });
1885
1886        // Source mode yields the whole source verbatim.
1887        state.read_with(cx, |state, _| {
1888            assert_eq!(state.selected_text().trim(), markdown);
1889        });
1890    }
1891
1892    #[gpui::test]
1893    fn selected_source_range_returns_full_markdown_source_for_select_all(cx: &mut TestAppContext) {
1894        cx.update(crate::init);
1895        let markdown = "**quick** value";
1896        let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown(markdown, cx)));
1897        cx.run_until_parked();
1898
1899        state.update(cx, |state, cx| state.select_all(cx));
1900
1901        state.read_with(cx, |state, _| {
1902            assert_eq!(state.selected_source_range(), Some(0..markdown.len()));
1903        });
1904    }
1905
1906    #[gpui::test]
1907    fn selected_source_range_returns_none_for_html(cx: &mut TestAppContext) {
1908        cx.update(crate::init);
1909        let state = cx.update(|cx| cx.new(|cx| TextViewState::html("<b>quick</b>", cx)));
1910        cx.run_until_parked();
1911
1912        state.update(cx, |state, cx| state.select_all(cx));
1913
1914        state.read_with(cx, |state, _| {
1915            assert_eq!(state.selected_source_range(), None);
1916        });
1917    }
1918
1919    #[test]
1920    fn selected_source_range_keeps_global_offsets_after_incremental_tail_parse() {
1921        let options = UpdateOptions {
1922            revision: 1,
1923            pending_text: "first\n\nsecond".to_string(),
1924            append: false,
1925            mode: ParseMode::Replace,
1926            markdown_extensions: Arc::default(),
1927        };
1928        let content = parse_content(TextViewFormat::Markdown, ParsedContent::default(), &options)
1929            .expect("initial parse");
1930        let content = parse_content(
1931            TextViewFormat::Markdown,
1932            content,
1933            &UpdateOptions {
1934                revision: 2,
1935                pending_text: "\n\n**écho**".to_string(),
1936                append: true,
1937                mode: ParseMode::Compatible,
1938                markdown_extensions: Arc::default(),
1939            },
1940        )
1941        .expect("incremental parse");
1942        let BlockNode::Paragraph(paragraph) = &content.document.blocks[2] else {
1943            panic!("expected appended paragraph");
1944        };
1945        let mut state = paragraph.state.lock().unwrap();
1946        state.set_text(paragraph.text().into());
1947        state.selection = Some((0.."écho".len()).into());
1948        drop(state);
1949
1950        assert_eq!(content.document.selected_source_range(), Some(17..22));
1951    }
1952
1953    #[test]
1954    fn streamed_ordered_list_continuation_preserves_start() {
1955        let initial = parse_content(
1956            TextViewFormat::Markdown,
1957            ParsedContent::default(),
1958            &UpdateOptions {
1959                revision: 1,
1960                pending_text: "3. three".to_string(),
1961                append: false,
1962                mode: ParseMode::Replace,
1963                markdown_extensions: Arc::default(),
1964            },
1965        )
1966        .expect("initial list parse");
1967        let continued = parse_content(
1968            TextViewFormat::Markdown,
1969            initial,
1970            &UpdateOptions {
1971                revision: 2,
1972                pending_text: "\n4. four".to_string(),
1973                append: true,
1974                mode: ParseMode::Compatible,
1975                markdown_extensions: Arc::default(),
1976            },
1977        )
1978        .expect("streamed list continuation parse");
1979
1980        let BlockNode::List {
1981            start, children, ..
1982        } = &continued.document.blocks[0]
1983        else {
1984            panic!("expected streamed ordered list");
1985        };
1986        assert_eq!(*start, Some(3));
1987        assert_eq!(children.len(), 2);
1988    }
1989
1990    #[gpui::test]
1991    fn parser_revision_reparses_same_name_inline_configuration(cx: &mut TestAppContext) {
1992        cx.update(crate::init);
1993        let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("@member", cx)));
1994        for (revision, label) in [(1, "Alice"), (2, "Bob")] {
1995            let extensions = MarkdownExtensions::default()
1996                .parser_revision(revision)
1997                .plugin(
1998                    crate::text::markdown_ext::TestInlinePlugin::new("test").parse_with(
1999                        move |node, _| {
2000                            matches!(node, markdown::mdast::Node::Text(_))
2001                                .then(|| MarkdownNode::new("mention", label).text(label))
2002                        },
2003                    ),
2004                );
2005            state.update(cx, |state, cx| {
2006                state.set_markdown_extensions(Arc::new(extensions), cx)
2007            });
2008            cx.run_until_parked();
2009            state.read_with(cx, |state, _| {
2010                let node::BlockNode::Paragraph(paragraph) =
2011                    &state.parsed_content.document.blocks[0]
2012                else {
2013                    panic!()
2014                };
2015                assert_eq!(
2016                    paragraph.children[0].custom.as_ref().unwrap().as_text(),
2017                    label
2018                );
2019            });
2020        }
2021    }
2022
2023    #[gpui::test]
2024    fn set_markdown_extensions_reparses_existing_text(cx: &mut TestAppContext) {
2025        cx.update(crate::init);
2026        let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("$TSLA.US", cx)));
2027        cx.run_until_parked();
2028
2029        let extensions = MarkdownExtensions::default().block_parser(|node, cx| {
2030            let markdown::mdast::Node::Paragraph(paragraph) = node else {
2031                return None;
2032            };
2033            let [markdown::mdast::Node::Text(text)] = paragraph.children.as_slice() else {
2034                return None;
2035            };
2036            let symbol = text.value.strip_prefix('$')?.to_string();
2037            let node_text = format!("${symbol}");
2038
2039            Some(
2040                MarkdownNode::new("ticker", symbol)
2041                    .text(node_text)
2042                    .markdown(cx.node_source(node).unwrap_or_default()),
2043            )
2044        });
2045
2046        state.update(cx, |state, cx| {
2047            state.set_markdown_extensions(Arc::new(extensions), cx);
2048        });
2049        cx.run_until_parked();
2050
2051        state.read_with(cx, |state, _| {
2052            let node::BlockNode::Custom(node) = &state.parsed_content.document.blocks[0] else {
2053                panic!("expected custom markdown node");
2054            };
2055            assert_eq!(node.name(), "ticker");
2056            assert_eq!(node.data::<String>().map(String::as_str), Some("TSLA.US"));
2057        });
2058    }
2059
2060    mod range_highlights {
2061        use std::ops::Range;
2062
2063        use gpui::{Entity, TestAppContext};
2064
2065        use super::super::*;
2066        use crate::text::stream_fade::TextLeafKey;
2067
2068        fn state(markdown: &str, cx: &mut TestAppContext) -> Entity<TextViewState> {
2069            cx.update(crate::init);
2070            let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown(markdown, cx)));
2071            cx.run_until_parked();
2072            state
2073        }
2074
2075        fn highlight(range: Range<usize>) -> RangeHighlight {
2076            RangeHighlight::new(range, gpui::hsla(0.15, 1., 0.5, 0.4))
2077        }
2078
2079        /// Highlights `ranges` of the current rendered text.
2080        fn set(
2081            state: &Entity<TextViewState>,
2082            ranges: impl IntoIterator<Item = Range<usize>>,
2083            cx: &mut TestAppContext,
2084        ) -> Result<(), RangeHighlightError> {
2085            state.update(cx, |state, cx| {
2086                state.set_range_highlights(ranges.into_iter().map(highlight), cx)
2087            })
2088        }
2089
2090        /// The ranges leaf `key` paints, in its own byte space.
2091        fn painted(
2092            state: &Entity<TextViewState>,
2093            key: TextLeafKey,
2094            cx: &mut TestAppContext,
2095        ) -> Vec<Range<usize>> {
2096            state.read_with(cx, |state, _| {
2097                state.range_highlights.as_ref().map_or(Vec::new(), |frame| {
2098                    frame
2099                        .backgrounds(key)
2100                        .iter()
2101                        .map(|(range, _)| range.clone())
2102                        .collect()
2103                })
2104            })
2105        }
2106
2107        fn has_highlights(state: &Entity<TextViewState>, cx: &mut TestAppContext) -> bool {
2108            state.read_with(cx, |state, _| state.range_highlights.is_some())
2109        }
2110
2111        #[gpui::test]
2112        fn rendered_text_is_the_plain_copy_text(cx: &mut TestAppContext) {
2113            let state = state(
2114                "# Title\n\nhello **world** \\*\n\n- item\n\n| a | b |\n|---|---|\n| c | d |\n\n```\nlet x\n```",
2115                cx,
2116            );
2117            state.read_with(cx, |state, _| {
2118                assert_eq!(
2119                    state.rendered_text().as_str(),
2120                    "Title\nhello world *\nitem\na b\nc d\n\nlet x\n"
2121                );
2122            });
2123        }
2124
2125        #[gpui::test]
2126        fn a_match_across_marks_paints_in_its_paragraph(cx: &mut TestAppContext) {
2127            let state = state("hello **world**", cx);
2128            set(&state, [0.."hello world".len()], cx).unwrap();
2129            assert_eq!(painted(&state, TextLeafKey::block(0), cx), [0..11]);
2130        }
2131
2132        #[gpui::test]
2133        fn repeated_text_maps_to_the_occurrence_addressed(cx: &mut TestAppContext) {
2134            let state = state("foo\n\nfoo", cx);
2135            let second = state.read_with(cx, |state, _| {
2136                state.rendered_text().as_str().rfind("foo").unwrap()
2137            });
2138            set(&state, [second..second + 3], cx).unwrap();
2139            assert!(painted(&state, TextLeafKey::block(0), cx).is_empty());
2140            assert_eq!(painted(&state, TextLeafKey::block(5), cx), [0..3]);
2141        }
2142
2143        #[gpui::test]
2144        fn a_range_across_blocks_skips_the_separator(cx: &mut TestAppContext) {
2145            let state = state("ab\n\ncd", cx);
2146            // "ab\ncd\n": 1..4 is "b\nc".
2147            set(&state, [1..4], cx).unwrap();
2148            assert_eq!(painted(&state, TextLeafKey::block(0), cx), [1..2]);
2149            assert_eq!(painted(&state, TextLeafKey::block(4), cx), [0..1]);
2150        }
2151
2152        #[gpui::test]
2153        fn table_cells_and_code_blocks_are_leaves(cx: &mut TestAppContext) {
2154            let state = state("| a | b |\n|---|---|\n| c | d |\n\n```\nlet x\n```", cx);
2155            // "a b\nc d\n\nlet x\n"
2156            set(&state, [0..3, 6..7, 9..12], cx).unwrap();
2157            assert_eq!(painted(&state, TextLeafKey::table_cell(0, 0), cx), [0..1]);
2158            assert_eq!(painted(&state, TextLeafKey::table_cell(0, 1), cx), [0..1]);
2159            assert_eq!(painted(&state, TextLeafKey::table_cell(0, 3), cx), [0..1]);
2160            let code_start = "| a | b |\n|---|---|\n| c | d |\n\n".len();
2161            assert_eq!(painted(&state, TextLeafKey::block(code_start), cx), [0..3]);
2162        }
2163
2164        #[gpui::test]
2165        fn invalid_ranges_reject_the_whole_set(cx: &mut TestAppContext) {
2166            let state = state("中文\n\nab", cx);
2167            // "中文\nab\n"
2168            set(&state, [0.."中".len()], cx).unwrap();
2169            let invalid = [Range { start: 6, end: 3 }, 0..1, 0..100];
2170            for range in invalid {
2171                assert_eq!(
2172                    set(&state, [0..3, range.clone()], cx),
2173                    Err(RangeHighlightError::InvalidRange(1)),
2174                    "{range:?}"
2175                );
2176            }
2177            // The rejected sets left the earlier highlight in place.
2178            assert_eq!(painted(&state, TextLeafKey::block(0), cx), [0..3]);
2179        }
2180
2181        #[gpui::test]
2182        fn text_outside_every_block_is_left_unpainted(cx: &mut TestAppContext) {
2183            let source = "foo one\n\n<div>foo two</div>\n\n| foo | x |\n|---|---|\n\nfoo three";
2184            let state = state(source, cx);
2185            let text = state.read_with(cx, |state, _| state.rendered_text().as_str().to_string());
2186            // Every "foo" and " ", an empty range, and the separator after the
2187            // first block: the HTML block's text and the separators are not
2188            // any block's text, so they paint nothing, but nothing fails.
2189            let ranges = text
2190                .match_indices("foo")
2191                .chain(text.match_indices(' '))
2192                .map(|(start, found)| start..start + found.len())
2193                .chain([3..3, 7..8])
2194                .collect::<Vec<_>>();
2195            set(&state, ranges, cx).unwrap();
2196            assert_eq!(painted(&state, TextLeafKey::block(0), cx), [0..3, 3..4]);
2197            let table = source.find("| foo").unwrap();
2198            assert_eq!(
2199                painted(&state, TextLeafKey::table_cell(table, 0), cx),
2200                [0..3]
2201            );
2202            let last = source.find("foo three").unwrap();
2203            assert_eq!(painted(&state, TextLeafKey::block(last), cx), [0..3, 3..4]);
2204        }
2205
2206        #[gpui::test]
2207        fn a_later_highlight_paints_over_an_earlier_one(cx: &mut TestAppContext) {
2208            let state = state("abcdef", cx);
2209            set(&state, [0..6, 2..3], cx).unwrap();
2210            assert_eq!(painted(&state, TextLeafKey::block(0), cx), [0..6, 2..3]);
2211        }
2212
2213        #[gpui::test]
2214        fn html_text_is_unsupported(cx: &mut TestAppContext) {
2215            let html = cx.update(|cx| cx.new(|cx| TextViewState::html("<p>one</p>", cx)));
2216            cx.run_until_parked();
2217            html.update(cx, |html, cx| {
2218                assert_eq!(
2219                    html.set_range_highlights([highlight(0..3)], cx),
2220                    Err(RangeHighlightError::Unsupported)
2221                );
2222            });
2223        }
2224
2225        #[gpui::test]
2226        fn inline_objects_are_skipped(cx: &mut TestAppContext) {
2227            let state = state("x $a$ y", cx);
2228            state.update(cx, |state, cx| {
2229                let extensions = MarkdownExtensions::default().plugin(
2230                    crate::text::markdown_ext::TestInlinePlugin::new("test").parse_with(
2231                        |node, _| {
2232                            let markdown::mdast::Node::InlineMath(math) = node else {
2233                                return None;
2234                            };
2235                            Some(
2236                                crate::text::MarkdownNode::new("formula", ())
2237                                    .text(math.value.clone()),
2238                            )
2239                        },
2240                    ),
2241                );
2242                state.set_markdown_extensions(Arc::new(extensions), cx);
2243            });
2244            cx.run_until_parked();
2245            // "x a y\n", where "a" is the formula.
2246            set(&state, [2..3], cx).unwrap();
2247            assert!(painted(&state, TextLeafKey::block(0), cx).is_empty());
2248            set(&state, [0..5], cx).unwrap();
2249            assert_eq!(painted(&state, TextLeafKey::block(0), cx), [0..2, 3..5]);
2250        }
2251
2252        #[gpui::test]
2253        fn push_str_keeps_earlier_blocks_and_clips_the_changed_tail(cx: &mut TestAppContext) {
2254            let state = state("first\n\na **b", cx);
2255            // "first\na **b\n"
2256            set(&state, [0..5, 6..11], cx).unwrap();
2257
2258            // Closing the emphasis renders the tail as "a bc".
2259            state.update(cx, |state, cx| state.push_str("c**", cx));
2260            cx.run_until_parked();
2261
2262            assert_eq!(painted(&state, TextLeafKey::block(0), cx), [0..5]);
2263            assert_eq!(painted(&state, TextLeafKey::block(7), cx), [0..2]);
2264        }
2265
2266        #[gpui::test]
2267        fn push_str_keeps_a_block_whose_text_is_unchanged(cx: &mut TestAppContext) {
2268            let state = state("first\n\nsecond", cx);
2269            set(&state, [6..12], cx).unwrap();
2270            // A setext underline turns the paragraph into a heading at the
2271            // same source start, rendering the same text.
2272            state.update(cx, |state, cx| state.push_str("\n===", cx));
2273            cx.run_until_parked();
2274            assert_eq!(painted(&state, TextLeafKey::block(7), cx), [0..6]);
2275        }
2276
2277        #[gpui::test]
2278        fn push_str_drops_highlights_of_a_leaf_that_is_gone(cx: &mut TestAppContext) {
2279            let state = state("first\n\n| a |", cx);
2280            set(&state, [0..5, 6..11], cx).unwrap();
2281            // A delimiter row turns the paragraph into a table, whose text
2282            // lives in cells instead.
2283            state.update(cx, |state, cx| state.push_str("\n|---|", cx));
2284            cx.run_until_parked();
2285            assert_eq!(painted(&state, TextLeafKey::block(0), cx), [0..5]);
2286            assert!(painted(&state, TextLeafKey::block(7), cx).is_empty());
2287            assert!(painted(&state, TextLeafKey::table_cell(7, 0), cx).is_empty());
2288        }
2289
2290        #[gpui::test]
2291        fn replacing_text_drops_highlights_only_where_it_changed(cx: &mut TestAppContext) {
2292            let state = state("first\n\nsecond", cx);
2293            set(&state, [0..5, 6..12], cx).unwrap();
2294            state.update(cx, |state, cx| state.set_text("first\n\nchanged", cx));
2295            cx.run_until_parked();
2296            assert_eq!(painted(&state, TextLeafKey::block(0), cx), [0..5]);
2297            assert!(painted(&state, TextLeafKey::block(7), cx).is_empty());
2298
2299            let large = "x".repeat(MAX_SYNC_FULL_REPLACE_BYTES + 1);
2300            state.update(cx, |state, cx| state.set_text(&large, cx));
2301            cx.run_until_parked();
2302            assert!(!has_highlights(&state, cx));
2303        }
2304
2305        #[gpui::test]
2306        fn a_highlight_follows_its_block_past_an_earlier_edit(cx: &mut TestAppContext) {
2307            let state = state("foo\n\nbar\n\nbar", cx);
2308            // "foo\nbar\nbar\n": the first "bar", in the block at source 5.
2309            set(&state, [4..7], cx).unwrap();
2310            // Deleting "foo" moves that "bar" to 0, and the second one to 5.
2311            state.update(cx, |state, cx| state.set_text("bar\n\nbar", cx));
2312            cx.run_until_parked();
2313            assert_eq!(painted(&state, TextLeafKey::block(0), cx), [0..3]);
2314            assert!(painted(&state, TextLeafKey::block(5), cx).is_empty());
2315
2316            let state = self::state("ERROR a\n\nERROR b\n\nERROR c", cx);
2317            set(&state, [8..15], cx).unwrap();
2318            state.update(cx, |state, cx| state.set_text("ERROR b\n\nERROR c", cx));
2319            cx.run_until_parked();
2320            assert_eq!(painted(&state, TextLeafKey::block(0), cx), [0..7]);
2321            assert!(painted(&state, TextLeafKey::block(9), cx).is_empty());
2322        }
2323
2324        #[gpui::test]
2325        fn appending_a_copy_of_the_last_block_keeps_the_highlight_on_it(cx: &mut TestAppContext) {
2326            let state = state("a\n\nfoo", cx);
2327            // "a\nfoo\n"
2328            set(&state, [2..5], cx).unwrap();
2329            state.update(cx, |state, cx| state.set_text("a\n\nfoo\n\nfoo", cx));
2330            cx.run_until_parked();
2331            assert_eq!(painted(&state, TextLeafKey::block(3), cx), [0..3]);
2332            assert!(painted(&state, TextLeafKey::block(8), cx).is_empty());
2333        }
2334
2335        #[gpui::test]
2336        fn deleting_a_table_row_drops_highlights_in_the_rows_it_moves(cx: &mut TestAppContext) {
2337            let table = "| a | b |\n|---|---|\n| x | 1 |\n| x | 2 |";
2338            let without_middle_row = "| a | b |\n|---|---|\n| x | 2 |";
2339            // "a b\nx 1\nx 2\n\n": the last row's cells, then the middle
2340            // row's "x".
2341            for ranges in [vec![8..9, 10..11], vec![4..5]] {
2342                let state = state(table, cx);
2343                set(&state, [0..1].into_iter().chain(ranges), cx).unwrap();
2344                state.update(cx, |state, cx| state.set_text(without_middle_row, cx));
2345                cx.run_until_parked();
2346                assert_eq!(painted(&state, TextLeafKey::table_cell(0, 0), cx), [0..1]);
2347                for cell in 1..4 {
2348                    assert!(
2349                        painted(&state, TextLeafKey::table_cell(0, cell), cx).is_empty(),
2350                        "cell {cell}"
2351                    );
2352                }
2353            }
2354        }
2355
2356        #[gpui::test]
2357        fn streaming_table_rows_keeps_the_highlights_of_earlier_rows(cx: &mut TestAppContext) {
2358            let state = state("| a | b |\n|---|---|\n| x | 1 |", cx);
2359            // "a b\nx 1\n\n"
2360            set(&state, [0..1, 4..5], cx).unwrap();
2361            state.update(cx, |state, cx| {
2362                state.set_text("| a | b |\n|---|---|\n| x | 1 |\n| y | 2 |", cx)
2363            });
2364            cx.run_until_parked();
2365            assert_eq!(painted(&state, TextLeafKey::table_cell(0, 0), cx), [0..1]);
2366            assert_eq!(painted(&state, TextLeafKey::table_cell(0, 2), cx), [0..1]);
2367        }
2368
2369        #[gpui::test]
2370        fn an_edit_in_an_earlier_block_keeps_later_highlights(cx: &mut TestAppContext) {
2371            let state = state("one\n\ntwo", cx);
2372            set(&state, [0..3, 4..7], cx).unwrap();
2373            state.update(cx, |state, cx| state.set_text("one!\n\ntwo", cx));
2374            cx.run_until_parked();
2375            assert_eq!(painted(&state, TextLeafKey::block(0), cx), [0..3]);
2376            assert_eq!(painted(&state, TextLeafKey::block(6), cx), [0..3]);
2377        }
2378
2379        #[gpui::test]
2380        fn an_append_after_a_full_update_compares_every_block(cx: &mut TestAppContext) {
2381            let state = state("first\n\nsecond", cx);
2382            set(&state, [0..5], cx).unwrap();
2383            // An append result whose baseline was a full update that has not
2384            // been committed: its earlier blocks are not the current ones.
2385            let options = UpdateOptions {
2386                revision: 0,
2387                pending_text: "FIRST\n\nsecond".to_string(),
2388                append: false,
2389                mode: ParseMode::Replace,
2390                markdown_extensions: Arc::default(),
2391            };
2392            let new = parse_content(TextViewFormat::Markdown, ParsedContent::default(), &options)
2393                .unwrap()
2394                .document;
2395            state.update(cx, |state, _| {
2396                state.full_update_revision = state.committed_revision + 1;
2397                let revision = state.committed_revision + 2;
2398                state.reconcile_range_highlights(&new, revision, true);
2399            });
2400            assert!(!has_highlights(&state, cx));
2401        }
2402
2403        #[gpui::test]
2404        fn streaming_through_set_text_keeps_highlights(cx: &mut TestAppContext) {
2405            let state = state("first\n\nsec", cx);
2406            // "first\nsec\n"
2407            set(&state, [0..5, 6..9], cx).unwrap();
2408            state.update(cx, |state, cx| state.set_text("first\n\nsecond", cx));
2409            cx.run_until_parked();
2410            assert_eq!(painted(&state, TextLeafKey::block(0), cx), [0..5]);
2411            assert_eq!(painted(&state, TextLeafKey::block(7), cx), [0..3]);
2412        }
2413
2414        #[gpui::test]
2415        fn a_full_parse_merged_with_an_append_compares_every_block(cx: &mut TestAppContext) {
2416            let state = state("", cx);
2417            // The small text is committed at once, and its background parse
2418            // merges with the append into one full parse, in which the
2419            // definition turns the earlier `[foo]` into the link text `foo`.
2420            state.update(cx, |state, cx| {
2421                state.set_text("[foo] and some text\n\nmore", cx);
2422                let text = state.rendered_text();
2423                let some = text.as_str().find("some").unwrap();
2424                state
2425                    .set_range_highlights([highlight(some..some + 4)], cx)
2426                    .unwrap();
2427                state.push_str("\n\n[foo]: https://example.com", cx);
2428            });
2429            cx.run_until_parked();
2430            state.read_with(cx, |state, _| {
2431                assert!(state.rendered_text().as_str().starts_with("foo and some"));
2432            });
2433            assert!(painted(&state, TextLeafKey::block(0), cx).is_empty());
2434        }
2435
2436        #[gpui::test]
2437        fn a_full_update_before_an_append_drops_replaced_highlights(cx: &mut TestAppContext) {
2438            let state = state("first", cx);
2439            set(&state, [0..5], cx).unwrap();
2440            let large = "x".repeat(MAX_SYNC_FULL_REPLACE_BYTES + 1);
2441            state.update(cx, |state, cx| {
2442                state.set_text(&large, cx);
2443                state.push_str(" tail", cx);
2444            });
2445            cx.run_until_parked();
2446            assert!(!has_highlights(&state, cx));
2447        }
2448
2449        #[gpui::test]
2450        fn reparsing_unchanged_text_keeps_highlights(cx: &mut TestAppContext) {
2451            let state = state("first", cx);
2452            set(&state, [0..5], cx).unwrap();
2453            state.update(cx, |state, cx| {
2454                state.set_markdown_extensions(
2455                    Arc::new(MarkdownExtensions::default().parser_revision(1)),
2456                    cx,
2457                )
2458            });
2459            cx.run_until_parked();
2460            assert_eq!(painted(&state, TextLeafKey::block(0), cx), [0..5]);
2461        }
2462
2463        pub(super) struct Root {
2464            pub(super) state: Entity<TextViewState>,
2465        }
2466
2467        impl Render for Root {
2468            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2469                gpui::div()
2470                    .w(px(120.))
2471                    .child(crate::text::TextView::new(&self.state).selectable(true))
2472            }
2473        }
2474
2475        #[gpui::test]
2476        fn highlights_paint_across_inline_code_tables_and_code_blocks(cx: &mut TestAppContext) {
2477            cx.update(crate::init);
2478            let markdown = "wrapping text with `inline code` and a [link](https://x.y) \
2479                            that wraps\n\n| a | b |\n|---|---|\n| c | d |\n\n```\nlet x = 1;\n```";
2480            let (root, cx) = cx.add_window_view(|_, cx| Root {
2481                state: cx.new(|cx| TextViewState::markdown(markdown, cx)),
2482            });
2483            cx.run_until_parked();
2484            let state = root.read_with(cx, |root, _| root.state.clone());
2485            let text = state.read_with(cx, |state, _| state.rendered_text().as_str().to_string());
2486            let code = text.find("code").unwrap();
2487            let len = text.len();
2488            // Part of the inline code, every character, and the whole text.
2489            let ranges = [code..code + 2]
2490                .into_iter()
2491                .chain(text.char_indices().map(|(ix, c)| ix..ix + c.len_utf8()))
2492                .chain([0..len])
2493                .filter(|range| !text[range.clone()].trim().is_empty());
2494            state.update(cx, |state, cx| {
2495                state
2496                    .set_range_highlights(ranges.map(highlight), cx)
2497                    .unwrap();
2498            });
2499            cx.update(|window, cx| window.draw(cx).clear(cx));
2500            assert_eq!(
2501                painted(&state, TextLeafKey::block(0), cx)[0],
2502                code..code + 2
2503            );
2504        }
2505
2506        #[gpui::test]
2507        fn clear_range_highlights_removes_them(cx: &mut TestAppContext) {
2508            let state = state("first", cx);
2509            set(&state, [0..5], cx).unwrap();
2510            state.update(cx, |state, cx| state.clear_range_highlights(cx));
2511            assert!(!has_highlights(&state, cx));
2512        }
2513    }
2514
2515    mod reveal_range {
2516        use gpui::{
2517            Entity, InteractiveElement as _, ListAlignment, ListState, ScrollHandle,
2518            StatefulInteractiveElement as _, TestAppContext, VisualTestContext, div, list,
2519        };
2520
2521        use super::super::*;
2522        use crate::text::TextView;
2523
2524        /// Where the view sits in a 200 × 100 window.
2525        #[derive(Clone)]
2526        enum Container {
2527            /// A scrollable view.
2528            Scrollable,
2529            /// A fit-content view, second row of an application list.
2530            List(ListState),
2531            /// A fit-content view in a scrolling `div`, which follows reveals
2532            /// through `on_reveal`.
2533            Div(ScrollHandle),
2534            /// A fit-content view clamped to two lines, second row of a list.
2535            Clamped(ListState),
2536            /// A fit-content view in nothing that scrolls.
2537            Fixed,
2538        }
2539
2540        struct Root {
2541            state: Entity<TextViewState>,
2542            container: Container,
2543        }
2544
2545        impl Render for Root {
2546            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2547                let state = self.state.clone();
2548                let frame = div().w(px(200.)).h(px(100.));
2549                let row = |list_state: &ListState, clamp: bool| {
2550                    let state = state.clone();
2551                    list(list_state.clone(), move |ix, _, _| match ix {
2552                        0 => div().h(px(40.)).into_any_element(),
2553                        _ => TextView::new(&state)
2554                            .when(clamp, |view| view.max_lines(2))
2555                            .into_any_element(),
2556                    })
2557                    .size_full()
2558                };
2559                match &self.container {
2560                    Container::Scrollable => frame
2561                        .child(TextView::new(&state).scrollable(true))
2562                        .into_any_element(),
2563                    Container::List(list_state) => {
2564                        frame.child(row(list_state, false)).into_any_element()
2565                    }
2566                    Container::Clamped(list_state) => {
2567                        frame.child(row(list_state, true)).into_any_element()
2568                    }
2569                    Container::Div(handle) => {
2570                        let scroll = handle.clone();
2571                        frame
2572                            .child(
2573                                div()
2574                                    .id("scroll")
2575                                    .size_full()
2576                                    .overflow_y_scroll()
2577                                    .track_scroll(handle)
2578                                    .child(TextView::new(&state).on_reveal(move |line, _, _| {
2579                                        let viewport = scroll.bounds();
2580                                        let mut offset = scroll.offset();
2581                                        if line.bottom() > viewport.bottom() {
2582                                            offset.y -= line.bottom() - viewport.bottom();
2583                                        } else if line.top() < viewport.top() {
2584                                            offset.y += viewport.top() - line.top();
2585                                        }
2586                                        scroll.set_offset(offset);
2587                                    })),
2588                            )
2589                            .into_any_element()
2590                    }
2591                    Container::Fixed => frame.child(TextView::new(&state)).into_any_element(),
2592                }
2593            }
2594        }
2595
2596        fn window<'a>(
2597            markdown: &str,
2598            container: Container,
2599            cx: &'a mut TestAppContext,
2600        ) -> (Entity<TextViewState>, &'a mut VisualTestContext) {
2601            cx.update(crate::init);
2602            let (root, cx) = cx.add_window_view(|_, cx| Root {
2603                state: cx.new(|cx| TextViewState::markdown(markdown, cx)),
2604                container,
2605            });
2606            cx.run_until_parked();
2607            draw(cx);
2608            let state = root.read_with(cx, |root, _| root.state.clone());
2609            (state, cx)
2610        }
2611
2612        fn draw(cx: &mut VisualTestContext) {
2613            cx.update(|window, cx| window.draw(cx).clear(cx));
2614        }
2615
2616        /// Asks to reveal the first occurrence of `needle`, then runs `then`
2617        /// before anything is drawn.
2618        fn request(
2619            state: &Entity<TextViewState>,
2620            needle: &str,
2621            cx: &mut VisualTestContext,
2622            then: impl FnOnce(&mut TextViewState, &mut Context<TextViewState>),
2623        ) {
2624            state.update(cx, |state, cx| {
2625                let text = state.rendered_text();
2626                let start = text.as_str().find(needle).unwrap();
2627                state.reveal_range(start..start + needle.len(), cx).unwrap();
2628                then(state, cx);
2629            });
2630        }
2631
2632        /// Reveals the first occurrence of `needle` and draws a few frames.
2633        fn reveal(state: &Entity<TextViewState>, needle: &str, cx: &mut VisualTestContext) {
2634            request(state, needle, cx, |_, _| {});
2635            for _ in 0..3 {
2636                draw(cx);
2637            }
2638        }
2639
2640        fn is_pending(state: &Entity<TextViewState>, cx: &mut VisualTestContext) -> bool {
2641            state.read_with(cx, |state, _| state.pending_reveal.is_some())
2642        }
2643
2644        fn scroll_top(
2645            state: &Entity<TextViewState>,
2646            cx: &mut VisualTestContext,
2647        ) -> gpui::ListOffset {
2648            state.read_with(cx, |state, _| state.list_state.logical_scroll_top())
2649        }
2650
2651        fn paragraphs(count: usize) -> String {
2652            (0..count)
2653                .map(|ix| format!("paragraph {ix}"))
2654                .collect::<Vec<_>>()
2655                .join("\n\n")
2656        }
2657
2658        fn words(count: usize) -> String {
2659            (0..count)
2660                .map(|ix| format!("w{ix}"))
2661                .collect::<Vec<_>>()
2662                .join(" ")
2663        }
2664
2665        #[gpui::test]
2666        fn a_scrollable_view_scrolls_to_an_offscreen_block(cx: &mut TestAppContext) {
2667            let (state, cx) = window(&paragraphs(200), Container::Scrollable, cx);
2668            reveal(&state, "paragraph 150", cx);
2669            assert!(!is_pending(&state, cx));
2670            let top = scroll_top(&state, cx);
2671            assert!((140..=150).contains(&top.item_ix), "{top:?}");
2672
2673            reveal(&state, "paragraph 3", cx);
2674            assert!(!is_pending(&state, cx));
2675            assert!(scroll_top(&state, cx).item_ix <= 3);
2676        }
2677
2678        #[gpui::test]
2679        fn an_append_adding_blocks_keeps_the_scroll_position(cx: &mut TestAppContext) {
2680            let (state, cx) = window(&paragraphs(200), Container::Scrollable, cx);
2681            reveal(&state, "paragraph 100", cx);
2682            let top = scroll_top(&state, cx);
2683            assert!(top.item_ix > 0, "{top:?}");
2684
2685            state.update(cx, |state, cx| state.push_str("\n\nparagraph 200", cx));
2686            cx.run_until_parked();
2687            draw(cx);
2688
2689            state.read_with(cx, |state, _| {
2690                assert_eq!(state.list_state.item_count(), 201);
2691            });
2692            assert_unmoved(&state, top, cx);
2693        }
2694
2695        #[gpui::test]
2696        fn a_scrollable_view_scrolls_to_a_line_inside_a_long_paragraph(cx: &mut TestAppContext) {
2697            let (state, cx) = window(&words(400), Container::Scrollable, cx);
2698            reveal(&state, "w390", cx);
2699            assert!(!is_pending(&state, cx));
2700            let top = scroll_top(&state, cx);
2701            assert_eq!(top.item_ix, 0);
2702            assert!(top.offset_in_item > px(100.), "{top:?}");
2703        }
2704
2705        #[gpui::test]
2706        fn revealing_a_visible_line_does_not_scroll(cx: &mut TestAppContext) {
2707            let (state, cx) = window(&words(400), Container::Scrollable, cx);
2708            reveal(&state, "w200", cx);
2709            let top = scroll_top(&state, cx);
2710            assert!(top.offset_in_item > px(0.), "{top:?}");
2711            for word in ["w199", "w198", "w197", "w196"] {
2712                reveal(&state, word, cx);
2713                assert!(!is_pending(&state, cx));
2714                let now = scroll_top(&state, cx);
2715                assert_eq!(
2716                    (now.item_ix, now.offset_in_item),
2717                    (top.item_ix, top.offset_in_item),
2718                    "{word}"
2719                );
2720            }
2721        }
2722
2723        #[gpui::test]
2724        fn an_enclosing_list_scrolls_to_a_line_of_a_fit_content_view(cx: &mut TestAppContext) {
2725            let outer = ListState::new(2, ListAlignment::Top, px(1000.));
2726            let (state, cx) = window(&words(400), Container::List(outer.clone()), cx);
2727            reveal(&state, "w390", cx);
2728            assert!(!is_pending(&state, cx));
2729            let top = outer.logical_scroll_top();
2730            assert_eq!(top.item_ix, 1, "{top:?}");
2731            assert!(top.offset_in_item > px(100.), "{top:?}");
2732        }
2733
2734        #[gpui::test]
2735        fn on_reveal_scrolls_a_container_that_ignores_scroll_requests(cx: &mut TestAppContext) {
2736            let handle = ScrollHandle::new();
2737            let (state, cx) = window(&words(400), Container::Div(handle.clone()), cx);
2738            reveal(&state, "w390", cx);
2739            assert!(!is_pending(&state, cx));
2740            assert!(handle.offset().y < px(-100.), "{:?}", handle.offset());
2741        }
2742
2743        #[gpui::test]
2744        fn a_reveal_that_cannot_be_shown_gives_up(cx: &mut TestAppContext) {
2745            let (state, cx) = window(&words(4000), Container::Fixed, cx);
2746            reveal(&state, "w3990", cx);
2747            assert!(is_pending(&state, cx));
2748            for _ in 0..10 {
2749                draw(cx);
2750            }
2751            assert!(!is_pending(&state, cx));
2752        }
2753
2754        #[gpui::test]
2755        fn a_reveal_not_carried_out_in_time_is_dropped(cx: &mut TestAppContext) {
2756            let (state, cx) = window(&paragraphs(200), Container::Scrollable, cx);
2757            request(&state, "paragraph 150", cx, |_, cx| {
2758                cx.background_executor()
2759                    .advance_clock(std::time::Duration::from_secs(2))
2760            });
2761            draw(cx);
2762            assert!(!is_pending(&state, cx));
2763            assert_eq!(scroll_top(&state, cx).item_ix, 0);
2764        }
2765
2766        #[gpui::test]
2767        fn an_empty_range_reveals_its_line(cx: &mut TestAppContext) {
2768            let (state, cx) = window(&words(400), Container::Scrollable, cx);
2769            reveal(&state, "w390", cx);
2770            let top = scroll_top(&state, cx);
2771            // A position on a visible line leaves the view where it is.
2772            state.update(cx, |state, cx| {
2773                let text = state.rendered_text();
2774                let start = text.as_str().find("w391").unwrap();
2775                state.reveal_range(start..start, cx).unwrap();
2776            });
2777            for _ in 0..3 {
2778                draw(cx);
2779            }
2780            assert!(!is_pending(&state, cx));
2781            let now = scroll_top(&state, cx);
2782            assert_eq!(
2783                (now.item_ix, now.offset_in_item),
2784                (top.item_ix, top.offset_in_item)
2785            );
2786        }
2787
2788        #[gpui::test]
2789        fn a_position_after_the_last_character_reveals_its_line(cx: &mut TestAppContext) {
2790            let (state, cx) = window(&words(400), Container::Scrollable, cx);
2791            reveal(&state, "w399", cx);
2792            let top = scroll_top(&state, cx);
2793            assert!(top.offset_in_item > px(1000.), "{top:?}");
2794            // The end of the text, on the visible last line.
2795            state.update(cx, |state, cx| {
2796                let text = state.rendered_text();
2797                let end = text.as_str().find("w399").unwrap() + "w399".len();
2798                state.reveal_range(end..end, cx).unwrap();
2799            });
2800            for _ in 0..3 {
2801                draw(cx);
2802            }
2803            assert!(!is_pending(&state, cx));
2804            let now = scroll_top(&state, cx);
2805            assert_eq!(
2806                (now.item_ix, now.offset_in_item),
2807                (top.item_ix, top.offset_in_item)
2808            );
2809        }
2810
2811        /// Reveals `range` of the current text and draws a few frames.
2812        fn reveal_at(
2813            state: &Entity<TextViewState>,
2814            range: impl FnOnce(&str) -> std::ops::Range<usize>,
2815            cx: &mut VisualTestContext,
2816        ) {
2817            state.update(cx, |state, cx| {
2818                let text = state.rendered_text();
2819                let range = range(text.as_str());
2820                state.reveal_range(range, cx).unwrap();
2821            });
2822            for _ in 0..3 {
2823                draw(cx);
2824            }
2825        }
2826
2827        fn assert_unmoved(
2828            state: &Entity<TextViewState>,
2829            top: gpui::ListOffset,
2830            cx: &mut VisualTestContext,
2831        ) {
2832            assert!(!is_pending(state, cx));
2833            let now = scroll_top(state, cx);
2834            assert_eq!(
2835                (now.item_ix, now.offset_in_item),
2836                (top.item_ix, top.offset_in_item)
2837            );
2838        }
2839
2840        #[gpui::test]
2841        fn the_end_of_the_text_and_its_separators_reveal_the_last_line(cx: &mut TestAppContext) {
2842            let (state, cx) = window(&words(400), Container::Scrollable, cx);
2843            reveal(&state, "w399", cx);
2844            let top = scroll_top(&state, cx);
2845            assert!(top.offset_in_item > px(1000.), "{top:?}");
2846            // The end of the text, after the separator that ends the block.
2847            reveal_at(&state, |text| text.len()..text.len(), cx);
2848            assert_unmoved(&state, top, cx);
2849            // Only that separator.
2850            reveal_at(&state, |text| text.len() - 1..text.len(), cx);
2851            assert_unmoved(&state, top, cx);
2852
2853            let code = (0..200)
2854                .map(|ix| format!("line {ix}"))
2855                .collect::<Vec<_>>()
2856                .join("\n");
2857            state.update(cx, |state, cx| {
2858                state.set_text(&format!("```\n{code}\n```"), cx)
2859            });
2860            cx.run_until_parked();
2861            reveal(&state, "line 199", cx);
2862            let top = scroll_top(&state, cx);
2863            assert!(top.offset_in_item > px(1000.), "{top:?}");
2864            reveal_at(&state, |text| text.len()..text.len(), cx);
2865            assert_unmoved(&state, top, cx);
2866        }
2867
2868        #[gpui::test]
2869        fn the_end_of_a_block_above_reveals_its_last_line(cx: &mut TestAppContext) {
2870            let second = (0..400)
2871                .map(|ix| format!("v{ix}"))
2872                .collect::<Vec<_>>()
2873                .join(" ");
2874            let markdown = format!("{}\n\n{second}", words(400));
2875            let (state, cx) = window(&markdown, Container::Scrollable, cx);
2876            reveal(&state, "v399", cx);
2877            assert_eq!(scroll_top(&state, cx).item_ix, 1);
2878            // The end of the first paragraph, on the separator after it.
2879            reveal_at(
2880                &state,
2881                |text| {
2882                    let end = text.find("w399").unwrap() + "w399".len();
2883                    end..end
2884                },
2885                cx,
2886            );
2887            assert!(!is_pending(&state, cx));
2888            let top = scroll_top(&state, cx);
2889            assert_eq!(top.item_ix, 0, "{top:?}");
2890            assert!(top.offset_in_item > px(1000.), "{top:?}");
2891        }
2892
2893        #[gpui::test]
2894        fn an_empty_view_has_nothing_to_reveal(cx: &mut TestAppContext) {
2895            cx.update(crate::init);
2896            let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("", cx)));
2897            cx.run_until_parked();
2898            state.update(cx, |state, cx| {
2899                assert_eq!(state.reveal_range(0..0, cx), Ok(()));
2900                assert!(state.pending_reveal.is_none());
2901            });
2902        }
2903
2904        #[gpui::test]
2905        fn revealing_a_visible_block_does_not_scroll(cx: &mut TestAppContext) {
2906            let markdown = format!("{}\n\n<div>html</div>\n\n{}", words(400), words(400));
2907            let (state, cx) = window(&markdown, Container::Scrollable, cx);
2908            reveal(&state, "html", cx);
2909            // Still in view a little further down.
2910            state.update(cx, |state, _| state.list_state.scroll_by(px(30.)));
2911            draw(cx);
2912            let top = scroll_top(&state, cx);
2913            reveal(&state, "html", cx);
2914            assert_unmoved(&state, top, cx);
2915        }
2916
2917        #[gpui::test]
2918        fn a_line_of_an_inline_flow_counts_as_shown_once_scrolled_to(cx: &mut TestAppContext) {
2919            // Inline code every tenth word, so the rows are taller than the
2920            // body line and land between pixels.
2921            let markdown = (0..400)
2922                .map(|ix| {
2923                    if ix % 10 == 0 {
2924                        format!("`c{ix}`")
2925                    } else {
2926                        format!("w{ix}")
2927                    }
2928                })
2929                .collect::<Vec<_>>()
2930                .join(" ");
2931            let (state, cx) = window(&markdown, Container::Scrollable, cx);
2932            reveal(&state, "c390", cx);
2933            assert!(!is_pending(&state, cx));
2934            assert!(scroll_top(&state, cx).offset_in_item > px(1000.));
2935        }
2936
2937        #[gpui::test]
2938        fn a_range_starting_on_a_line_break_reveals_the_next_line(cx: &mut TestAppContext) {
2939            let code = (0..200)
2940                .map(|ix| format!("line {ix}"))
2941                .collect::<Vec<_>>()
2942                .join("\n");
2943            let (state, cx) = window(&format!("```\n{code}\n```"), Container::Scrollable, cx);
2944            reveal(&state, "\nline 190", cx);
2945            assert!(!is_pending(&state, cx));
2946            let top = scroll_top(&state, cx);
2947            assert!(top.offset_in_item > px(1000.), "{top:?}");
2948        }
2949
2950        #[gpui::test]
2951        fn a_range_starting_on_a_line_break_in_an_inline_flow_reveals_the_next_line(
2952            cx: &mut TestAppContext,
2953        ) {
2954            // Inline code lays the paragraph out as an inline flow.
2955            let lines = (0..200)
2956                .map(|ix| format!("line {ix} `code`"))
2957                .collect::<Vec<_>>()
2958                .join("\\\n");
2959            let (state, cx) = window(&lines, Container::Scrollable, cx);
2960            reveal(&state, "\nline 190", cx);
2961            assert!(!is_pending(&state, cx));
2962            let top = scroll_top(&state, cx);
2963            assert!(top.offset_in_item > px(1000.), "{top:?}");
2964        }
2965
2966        #[gpui::test]
2967        fn a_reveal_is_dropped_when_its_text_before_it_changes(cx: &mut TestAppContext) {
2968            let (state, cx) = window(&paragraphs(200), Container::Scrollable, cx);
2969            let target = "first words then the target";
2970            let markdown = format!("{target}\n\n{}", paragraphs(200));
2971            state.update(cx, |state, cx| state.set_text(&markdown, cx));
2972            cx.run_until_parked();
2973
2974            // Appending to its paragraph keeps it.
2975            request(&state, "target", cx, |state, cx| {
2976                state.set_text(&markdown.replacen("target", "target and more", 1), cx);
2977                assert!(state.pending_reveal.is_some());
2978            });
2979            // An edit before it in its paragraph drops it.
2980            request(&state, "target", cx, |state, cx| {
2981                state.set_text(&markdown.replacen("words", "WORDS", 1), cx);
2982                assert!(state.pending_reveal.is_none());
2983            });
2984        }
2985
2986        #[gpui::test]
2987        fn a_clamped_view_does_not_reveal(cx: &mut TestAppContext) {
2988            let outer = ListState::new(2, ListAlignment::Top, px(1000.));
2989            let (state, cx) = window(&words(400), Container::Clamped(outer.clone()), cx);
2990            reveal(&state, "w390", cx);
2991            assert!(!is_pending(&state, cx));
2992            assert_eq!(outer.logical_scroll_top().item_ix, 0);
2993        }
2994
2995        #[gpui::test]
2996        fn text_outside_every_block_reveals_its_block(cx: &mut TestAppContext) {
2997            let markdown = format!("{}\n\n<div>html text</div>", paragraphs(100));
2998            let (state, cx) = window(&markdown, Container::Scrollable, cx);
2999            reveal(&state, "html text", cx);
3000            assert!(!is_pending(&state, cx));
3001            assert!(
3002                scroll_top(&state, cx).item_ix >= 90,
3003                "{:?}",
3004                scroll_top(&state, cx)
3005            );
3006        }
3007
3008        #[gpui::test]
3009        fn a_reveal_follows_its_text_past_an_edit_before_it(cx: &mut TestAppContext) {
3010            let (state, cx) = window(&paragraphs(200), Container::Scrollable, cx);
3011            // An inserted paragraph moves the target down one block.
3012            request(&state, "paragraph 150", cx, |state, cx| {
3013                state.set_text(&format!("inserted\n\n{}", paragraphs(200)), cx);
3014                assert!(state.pending_reveal.is_some());
3015            });
3016            for _ in 0..3 {
3017                draw(cx);
3018            }
3019            assert!(!is_pending(&state, cx));
3020            let top = scroll_top(&state, cx);
3021            assert!((141..=151).contains(&top.item_ix), "{top:?}");
3022
3023            // A reveal whose text changed is dropped.
3024            request(&state, "paragraph 150", cx, |state, cx| {
3025                state.set_text(&paragraphs(200).replace("paragraph 150", "changed"), cx);
3026                assert!(state.pending_reveal.is_none());
3027            });
3028        }
3029
3030        #[gpui::test]
3031        fn malformed_ranges_and_html_views_are_rejected(cx: &mut TestAppContext) {
3032            cx.update(crate::init);
3033            let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("first\n\nsecond", cx)));
3034            cx.run_until_parked();
3035            state.update(cx, |state, cx| {
3036                assert_eq!(
3037                    state.reveal_range(std::ops::Range { start: 5, end: 3 }, cx),
3038                    Err(RangeHighlightError::InvalidRange(0))
3039                );
3040                assert_eq!(
3041                    state.reveal_range(0..100, cx),
3042                    Err(RangeHighlightError::InvalidRange(0))
3043                );
3044                state.reveal_range(0..5, cx).unwrap();
3045            });
3046
3047            let html = cx.update(|cx| cx.new(|cx| TextViewState::html("<p>one</p>", cx)));
3048            cx.run_until_parked();
3049            html.update(cx, |html, cx| {
3050                assert_eq!(
3051                    html.reveal_range(0..3, cx),
3052                    Err(RangeHighlightError::Unsupported)
3053                );
3054            });
3055        }
3056    }
3057}