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