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