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