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