Skip to main content

gpui_base/text/
state.rs

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