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::RangeInclusive,
6    pin::Pin,
7    sync::{Arc, Mutex},
8    task::Poll,
9};
10#[cfg(target_family = "wasm")]
11use web_time::Instant;
12
13use gpui::{
14    App, AppContext as _, Bounds, Context, FocusHandle, IntoElement, KeyBinding, ListState,
15    ParentElement as _, Pixels, Point, Render, SharedString, Styled as _, Task, Window,
16    prelude::FluentBuilder as _, px,
17};
18
19use crate::{
20    AutoScroll, ElementExt, TextSelection,
21    async_util::{Receiver, Sender, unbounded},
22    input::{self, SelectAll},
23    text::{
24        CodeBlockActionsFn, CodeBlockHighlighterFn, LinkClickHandlerFn, MarkdownExtensions,
25        TableActionsFn, TextViewStyle,
26        document::ParsedDocument,
27        format,
28        node::{self, NodeContext},
29        selection_adapter::TextViewSelectionAdapter,
30        stream_fade::{StreamFadeTracker, TextViewMotion},
31    },
32    v_flex,
33};
34
35const CONTEXT: &'static str = "TextView";
36// Keep coalescing bounded so sustained streams still render intermediate updates.
37const MAX_COALESCED_UPDATES_PER_PARSE: usize = 64;
38// Preserve exact first-layout height for small documents while bounding the
39// amount of source parsed synchronously on the UI thread.
40const MAX_SYNC_FULL_REPLACE_BYTES: usize = 4 * 1024;
41
42pub(crate) fn init(cx: &mut App) {
43    cx.bind_keys(vec![
44        #[cfg(target_os = "macos")]
45        KeyBinding::new("cmd-c", input::Copy, Some(CONTEXT)),
46        #[cfg(not(target_os = "macos"))]
47        KeyBinding::new("ctrl-c", input::Copy, Some(CONTEXT)),
48        #[cfg(target_os = "macos")]
49        KeyBinding::new("cmd-a", input::SelectAll, Some(CONTEXT)),
50        #[cfg(not(target_os = "macos"))]
51        KeyBinding::new("ctrl-a", input::SelectAll, Some(CONTEXT)),
52    ]);
53}
54
55/// The content format of the text view.
56#[derive(Clone, Copy, PartialEq, Eq)]
57pub(super) enum TextViewFormat {
58    /// Markdown view
59    Markdown,
60    /// HTML view
61    Html,
62}
63
64/// The format of the text returned by
65/// [`TextViewState::selected_text`], which is also what copy writes to the
66/// clipboard.
67#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
68pub enum SelectionFormat {
69    /// The rendered text, without any markup.
70    #[default]
71    Plain,
72    /// The source of the selection.
73    ///
74    /// Select-all returns the original source verbatim, a partial selection is
75    /// reconstructed as Markdown from the parsed nodes (e.g. selecting inside
76    /// a `**bold**` run yields `**bold**`).
77    Source,
78}
79
80/// One text element's laid-out vertical extent, reported by `Inline` during
81/// prepaint so `TextView` can snap its `max_lines` clip to a whole-line
82/// boundary.
83#[derive(Clone, Copy)]
84pub(super) struct LineSpan {
85    pub(super) top: Pixels,
86    pub(super) bottom: Pixels,
87    pub(super) line_height: Pixels,
88}
89
90/// The state of a TextView.
91pub struct TextViewState {
92    pub(super) focus_handle: FocusHandle,
93    pub(super) list_state: ListState,
94
95    /// The bounds of the text view
96    bounds: Bounds<Pixels>,
97
98    pub(super) selectable: bool,
99    pub(super) selection_format: SelectionFormat,
100    pub(super) scrollable: bool,
101    pub(super) max_lines: Option<usize>,
102    /// Line spans reported by `Inline` during prepaint (collected only while
103    /// [`Self::max_lines`] is set); cleared by `TextView` at each frame start.
104    pub(super) line_spans: Arc<Mutex<Vec<LineSpan>>>,
105    /// Whether the last painted frame clipped content due to `max_lines`.
106    pub(super) clamped: bool,
107    pub(super) text_view_style: Arc<TextViewStyle>,
108    pub(super) code_block_actions: Option<std::sync::Arc<CodeBlockActionsFn>>,
109    pub(super) code_block_highlighter: Option<std::sync::Arc<CodeBlockHighlighterFn>>,
110    pub(super) table_actions: Option<std::sync::Arc<TableActionsFn>>,
111    pub(super) link_click_handler: Option<std::sync::Arc<LinkClickHandlerFn>>,
112    pub(super) markdown_extensions: Arc<MarkdownExtensions>,
113
114    pub(super) is_selecting: bool,
115    /// Logical ranges retained across an explicitly requested resource reflow.
116    pub(super) preserve_inline_selection: bool,
117    multi_click_selection: Option<TextViewMultiClickSelection>,
118    selected_text_override: Option<String>,
119    select_all: bool,
120    pub(super) auto_scroll: AutoScroll,
121    pub(super) selection_adapter: TextViewSelectionAdapter,
122
123    pub(super) parsed_content: ParsedContent,
124    pub(super) stream_fade: StreamFadeTracker,
125    /// Content format (markdown / html), used for bounded synchronous parsing
126    /// of small full-replace updates.
127    format: TextViewFormat,
128    text: String,
129    /// The text a `TextView` element last handed over, to recognize the same
130    /// string next frame without comparing its bytes.
131    element_text: Option<SharedString>,
132    revision: usize,
133    pub(super) selection_revision: usize,
134    compatible_layout_update: bool,
135    layout_text_style: Option<(gpui::TextStyle, Pixels)>,
136    parsed_error: Option<SharedString>,
137    tx: Sender<UpdateOptions>,
138    _parse_task: Task<()>,
139    _receive_task: Task<()>,
140}
141
142impl TextViewState {
143    /// Create a Markdown TextViewState.
144    pub fn markdown(text: &str, cx: &mut Context<Self>) -> Self {
145        Self::new(TextViewFormat::Markdown, text, cx)
146    }
147
148    /// Create a HTML TextViewState.
149    pub fn html(text: &str, cx: &mut Context<Self>) -> Self {
150        Self::new(TextViewFormat::Html, text, cx)
151    }
152
153    /// Create a new TextViewState.
154    fn new(format: TextViewFormat, text: &str, cx: &mut Context<Self>) -> Self {
155        let focus_handle = cx.focus_handle();
156        let selection_adapter = TextViewSelectionAdapter::new(cx.entity().downgrade(), cx);
157
158        let (tx, rx) = unbounded::<UpdateOptions>();
159        let (tx_result, rx_result) = unbounded::<ParsedUpdate>();
160        let _receive_task = cx.spawn({
161            async move |weak_self, cx| {
162                while let Ok(parsed_update) = rx_result.recv().await {
163                    _ = weak_self.update(cx, |state, cx| {
164                        if parsed_update.revision != state.revision {
165                            return;
166                        }
167                        if parsed_update.baseline_ack {
168                            debug_assert!(parsed_update.full_parse);
169                            return;
170                        }
171
172                        match parsed_update.result {
173                            Ok(content) => {
174                                state.stream_fade.record(
175                                    &state.parsed_content.document,
176                                    &content.document,
177                                    Instant::now(),
178                                );
179                                state.parsed_content = content;
180                                state.parsed_error = None;
181                                state.compatible_layout_update = parsed_update.selection_compatible;
182                                if parsed_update.full_parse {
183                                    state.invalidate_measured_heights();
184                                }
185                            }
186                            Err(err) => {
187                                state.stream_fade.discard_pending();
188                                state.parsed_error = Some(err);
189                            }
190                        }
191                        // Don't interrupt an active drag-selection; the stored
192                        // positions remain valid for append-only updates and will
193                        // self-correct on the next mouse-move event.
194                        if !parsed_update.selection_compatible && !state.is_selecting {
195                            state.reset_selection_and_adapter(cx);
196                        }
197                        cx.notify();
198                    });
199                }
200            }
201        });
202
203        let _parse_task = cx.background_spawn(UpdateFuture::new(format, rx, tx_result));
204
205        let mut this = Self {
206            focus_handle,
207            bounds: Bounds::default(),
208            multi_click_selection: None,
209            selected_text_override: None,
210            select_all: false,
211            selectable: false,
212            selection_format: SelectionFormat::default(),
213            scrollable: false,
214            max_lines: None,
215            line_spans: Arc::default(),
216            clamped: false,
217            // Measure all blocks (not just visible ones) so the scrollbar
218            // thumb size stays stable. Without this, off-screen blocks count
219            // as zero height until scrolled into view, which makes the
220            // scrollbar jitter as more blocks get measured during scrolling.
221            list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)).measure_all(),
222            text_view_style: Arc::default(),
223            code_block_actions: None,
224            code_block_highlighter: None,
225            table_actions: None,
226            link_click_handler: None,
227            markdown_extensions: Arc::default(),
228            is_selecting: false,
229            preserve_inline_selection: false,
230            auto_scroll: AutoScroll::default(),
231            selection_adapter,
232            parsed_content: Default::default(),
233            stream_fade: StreamFadeTracker::default(),
234            format,
235            parsed_error: None,
236            text: text.to_string(),
237            element_text: None,
238            revision: 0,
239            selection_revision: 0,
240            compatible_layout_update: false,
241            layout_text_style: None,
242            tx,
243            _parse_task,
244            _receive_task,
245        };
246        this.increment_update(&text, false, cx);
247        this
248    }
249
250    /// Get the text content.
251    pub(crate) fn source(&self) -> SharedString {
252        self.parsed_content.document.source.clone()
253    }
254
255    /// Set whether the text is selectable, default false.
256    pub fn selectable(mut self, selectable: bool) -> Self {
257        self.selectable = selectable;
258        self
259    }
260
261    /// Set whether the text is selectable, default false.
262    pub fn set_selectable(&mut self, selectable: bool, cx: &mut Context<Self>) {
263        self.selectable = selectable;
264        cx.notify();
265    }
266
267    /// Set the [`SelectionFormat`], default is [`SelectionFormat::Plain`].
268    pub fn selection_format(mut self, selection_format: SelectionFormat) -> Self {
269        self.selection_format = selection_format;
270        self
271    }
272
273    /// Set the [`SelectionFormat`], default is [`SelectionFormat::Plain`].
274    pub fn set_selection_format(
275        &mut self,
276        selection_format: SelectionFormat,
277        cx: &mut Context<Self>,
278    ) {
279        self.selection_format = selection_format;
280        cx.notify();
281    }
282
283    /// Set whether the text view scrolls internally, default false.
284    pub fn scrollable(mut self, scrollable: bool) -> Self {
285        self.scrollable = scrollable;
286        self
287    }
288
289    /// Set whether the text view scrolls internally, default false.
290    pub fn set_scrollable(&mut self, scrollable: bool, cx: &mut Context<Self>) {
291        if !scrollable {
292            self.reset_selection_and_adapter(cx);
293        }
294        self.scrollable = scrollable;
295        cx.notify();
296    }
297
298    /// Whether the last painted frame clipped content because of
299    /// [`TextView::max_lines`](crate::text::TextView::max_lines).
300    pub fn is_clamped(&self) -> bool {
301        self.clamped
302    }
303
304    /// Set the text content.
305    ///
306    /// With a streamed fade-in enabled, text that extends the current text
307    /// fades in like [`Self::push_str`] would; any other replacement shows at
308    /// once.
309    pub fn set_text(&mut self, text: &str, cx: &mut Context<Self>) {
310        if self.text.as_str() == text {
311            return;
312        }
313        if self.stream_fade.is_enabled() {
314            if text.starts_with(self.text.as_str()) {
315                self.stream_fade.note_extend(self.text.len());
316            } else {
317                self.stream_fade.note_replace();
318            }
319        }
320
321        self.text.clear();
322        self.text.push_str(text);
323        self.parsed_error = None;
324        self.increment_update(text, false, cx);
325    }
326
327    /// [`Self::set_text`] for the text a `TextView` element hands over every
328    /// frame: the string it handed over last time (the same allocation, not
329    /// merely equal bytes) is recognized without a comparison, as long as the
330    /// state still holds a text of its length.
331    pub(super) fn set_element_text(&mut self, text: &SharedString, cx: &mut Context<Self>) {
332        let same_string = self.element_text.as_ref().is_some_and(|current| {
333            current.as_ptr() == text.as_ptr() && current.len() == text.len()
334        });
335        if same_string && self.text.len() == text.len() {
336            return;
337        }
338        self.element_text = Some(text.clone());
339        self.set_text(text, cx);
340    }
341
342    /// Append partial text content to the existing text.
343    pub fn push_str(&mut self, new_text: &str, cx: &mut Context<Self>) {
344        if new_text.is_empty() {
345            return;
346        }
347        self.stream_fade.note_extend(self.text.len());
348        self.text.push_str(new_text);
349        self.increment_update(new_text, true, cx);
350    }
351
352    /// Set the motion policy; see [`TextViewMotion`].
353    pub fn motion(mut self, motion: TextViewMotion) -> Self {
354        self.set_motion(motion);
355        self
356    }
357
358    /// Set the motion policy; see [`TextViewMotion`].
359    pub fn set_motion(&mut self, motion: TextViewMotion) {
360        self.stream_fade.set_motion(motion);
361    }
362
363    pub(crate) fn set_markdown_extensions(
364        &mut self,
365        markdown_extensions: Arc<MarkdownExtensions>,
366        cx: &mut Context<Self>,
367    ) {
368        if self.markdown_extensions.revision() == markdown_extensions.revision() {
369            return;
370        }
371
372        let parser_configuration_changed = !self
373            .markdown_extensions
374            .has_same_parser_configuration(&markdown_extensions);
375        self.markdown_extensions = markdown_extensions;
376        if parser_configuration_changed && self.format == TextViewFormat::Markdown {
377            let text = self.text.clone();
378            self.increment_update(&text, false, cx);
379        }
380    }
381
382    /// Return the selected text, in the view's [`SelectionFormat`].
383    pub fn selected_text(&self) -> String {
384        self.selected_text_in(None)
385    }
386
387    /// The format to copy in, which is [`SelectionFormat::Plain`] whenever the
388    /// requested one cannot be produced.
389    ///
390    /// Only a Markdown view can return source. Reconstructing HTML would mean
391    /// spelling every attribute back out — a mark's color, an image's
392    /// dimensions, a cell's alignment — with a new way to lose one at each
393    /// step, and html5ever records no source offsets to fall back on (it
394    /// reports only line numbers), so there is no original text to copy from
395    /// either.
396    fn effective_format(&self) -> SelectionFormat {
397        match self.format {
398            TextViewFormat::Markdown => self.selection_format,
399            TextViewFormat::Html => SelectionFormat::Plain,
400        }
401    }
402
403    /// Return the selected text, with `blocks` bounding which top-level blocks
404    /// the selection covers.
405    ///
406    /// The range comes from the selection endpoints, which know their block
407    /// even after it scrolls out of view; see
408    /// [`ParsedDocument::selected_text`](crate::text::document::ParsedDocument).
409    pub(super) fn selected_text_in(&self, blocks: Option<RangeInclusive<usize>>) -> String {
410        let format = self.effective_format();
411
412        if self.select_all {
413            if format == SelectionFormat::Source {
414                return self.source().to_string();
415            }
416
417            return self.parsed_content.document.text();
418        }
419
420        // A multi-click stores the plain text it selected, which is a shortcut
421        // past the block walk. Source mode cannot take it: the word it stored
422        // has lost its markup. The click also set the inline selection it came
423        // from, so the walk reconstructs the same range with the markup intact.
424        if format != SelectionFormat::Source
425            && let Some(text) = &self.selected_text_override
426        {
427            return text.clone();
428        }
429
430        self.parsed_content.document.selected_text(format, blocks)
431    }
432
433    /// Force a full re-measure of the block list after the document has been
434    /// replaced.
435    ///
436    /// `Document::render_root` only calls `ListState::reset` when the block
437    /// *count* changes, and the full-measure pass enabled by `measure_all` is
438    /// a one-shot latch that only `reset`, `remeasure_items`, or a width
439    /// change re-arms. Replacing a document with one that happens to have the
440    /// same number of blocks therefore leaves every cached height belonging to
441    /// the *previous* document. The list summary height stays wrong, and since
442    /// the wheel clamps against that summary, the blocks past the false bottom
443    /// can never scroll into view to be re-measured -- the clamp seals itself.
444    ///
445    /// `remeasure_items` re-arms the latch and marks the items unmeasured
446    /// while keeping their old sizes as hints, so the scrollbar does not
447    /// collapse in the frame before the next layout measures the real heights.
448    fn invalidate_measured_heights(&self) {
449        let count = self.list_state.item_count();
450        if count > 0 {
451            self.list_state.remeasure_items(0..count);
452        }
453    }
454
455    /// Remeasure inline resources after their prepared content or metrics change.
456    ///
457    /// Call from the resource owner's completion handler, through a weak entity
458    /// when the task can outlive this view. This does not reparse the document.
459    /// Existing logical selection is retained until the next selection gesture.
460    pub fn invalidate_inline_layout(&mut self, cx: &mut Context<Self>) {
461        self.preserve_inline_selection = true;
462        self.compatible_layout_update = true;
463        self.invalidate_measured_heights();
464        cx.notify();
465    }
466
467    fn increment_update(&mut self, text: &str, append: bool, cx: &mut Context<Self>) {
468        self.revision += 1;
469        if !append {
470            self.selection_revision = self.selection_revision.wrapping_add(1);
471        }
472        let parse_synchronously = !append && text.len() <= MAX_SYNC_FULL_REPLACE_BYTES;
473        let update_options = UpdateOptions {
474            revision: self.revision,
475            append,
476            mode: if append {
477                ParseMode::Compatible
478            } else if parse_synchronously {
479                ParseMode::BaselineAck
480            } else {
481                ParseMode::Replace
482            },
483            pending_text: text.to_string(),
484            markdown_extensions: self.markdown_extensions.clone(),
485        };
486
487        // Keep small full replacements synchronous so their first layout has
488        // the exact content height. Larger replacements use the existing
489        // background parser, bounding synchronous parser input on the UI thread.
490        if parse_synchronously {
491            match parse_content(self.format, ParsedContent::default(), &update_options) {
492                Ok(content) => {
493                    self.stream_fade.record(
494                        &self.parsed_content.document,
495                        &content.document,
496                        Instant::now(),
497                    );
498                    self.parsed_content = content;
499                    self.parsed_error = None;
500                    self.invalidate_measured_heights();
501                    if !self.is_selecting {
502                        self.reset_selection_and_adapter(cx);
503                    }
504                }
505                Err(err) => {
506                    self.stream_fade.discard_pending();
507                    self.parsed_error = Some(err);
508                }
509            }
510            // Keep the background parser's accumulated document in sync so a
511            // later append extends this baseline instead of parsing the delta
512            // as a standalone document.
513            _ = self.tx.try_send(update_options);
514            cx.notify();
515            return;
516        }
517
518        _ = self.tx.try_send(update_options);
519    }
520
521    /// Save bounds and unselect if bounds changed.
522    pub(super) fn update_bounds(&mut self, bounds: Bounds<Pixels>, _cx: &mut App) {
523        self.bounds = bounds;
524    }
525
526    /// The index of the top-level block at `content_y`, in this view's content
527    /// coordinates (the same space the base selection endpoint stores its point in).
528    ///
529    /// Only laid-out blocks can be located, which is enough for a selection
530    /// endpoint: the user can only put one where they can see it. Returns
531    /// `None` for a view that is not virtualized, where every block paints and
532    /// the range is not needed.
533    pub(super) fn block_ix_at(&self, content_y: Pixels) -> Option<usize> {
534        if !self.scrollable {
535            return None;
536        }
537
538        let origin = self.bounds.origin.y + self.scroll_offset().y;
539        let count = self.list_state.item_count();
540        let mut ix = self.list_state.logical_scroll_top().item_ix;
541        while ix < count {
542            let bounds = self.list_state.bounds_for_item(ix)?;
543            if content_y < bounds.bottom() - origin {
544                return Some(ix);
545            }
546            ix += 1;
547        }
548
549        count.checked_sub(1)
550    }
551
552    #[doc(hidden)]
553    pub fn bounds(&self) -> Bounds<Pixels> {
554        self.bounds
555    }
556
557    #[doc(hidden)]
558    pub fn list_state(&self) -> &ListState {
559        &self.list_state
560    }
561
562    #[doc(hidden)]
563    pub fn is_selecting(&self) -> bool {
564        self.is_selecting
565    }
566
567    #[doc(hidden)]
568    pub fn focus_handle(&self) -> &FocusHandle {
569        &self.focus_handle
570    }
571
572    /// Whether this view has a view-local selection (select-all, multi-click, or override),
573    /// independent of the window-level selection.
574    pub(super) fn has_view_selection(&self) -> bool {
575        self.select_all
576            || self.multi_click_selection.is_some()
577            || self.selected_text_override.is_some()
578    }
579
580    pub(super) fn stop_auto_scroll(&mut self) {
581        self.auto_scroll.stop();
582    }
583
584    pub(super) fn reset_selection(&mut self) {
585        self.preserve_inline_selection = false;
586        self.multi_click_selection = None;
587        self.selected_text_override = None;
588        self.select_all = false;
589        self.is_selecting = false;
590        self.auto_scroll.stop();
591        // Clear the inline selection state synchronously, so offscreen
592        // (virtualized) views that won't repaint don't leak stale selection
593        // text into a new cross-view copy.
594        self.parsed_content.document.clear_selection();
595    }
596
597    fn reset_selection_and_adapter(&mut self, cx: &mut App) {
598        self.reset_selection();
599        self.selection_adapter.set_local_selection(false, cx);
600    }
601
602    /// Clear the current text selection.
603    pub fn clear_selection(&mut self, cx: &mut Context<Self>) {
604        self.reset_selection_and_adapter(cx);
605        cx.notify();
606    }
607
608    pub(super) fn scroll_offset(&self) -> Point<Pixels> {
609        if self.scrollable {
610            self.list_state.scroll_px_offset_for_scrollbar()
611        } else {
612            Point::default()
613        }
614    }
615
616    /// Select all rendered text in this view.
617    pub fn select_all(&mut self, cx: &mut Context<Self>) {
618        self.multi_click_selection = None;
619        self.selected_text_override = None;
620        self.select_all = true;
621        self.is_selecting = false;
622        self.auto_scroll.stop();
623        self.selection_adapter.set_local_selection(true, cx);
624        cx.notify();
625    }
626
627    pub(crate) fn set_multi_click_selection(
628        &mut self,
629        pos: Point<Pixels>,
630        kind: TextViewMultiClickKind,
631        selected_text: String,
632        cx: &mut App,
633    ) {
634        self.preserve_inline_selection = false;
635        let scroll_offset = self.scroll_offset();
636        let pos = pos - self.bounds.origin - scroll_offset;
637        self.multi_click_selection = Some(TextViewMultiClickSelection {
638            pos,
639            kind,
640            line_bounds: None,
641        });
642        self.selected_text_override = Some(selected_text);
643        self.select_all = false;
644        self.is_selecting = false;
645        self.auto_scroll.stop();
646        self.selection_adapter.set_local_selection(true, cx);
647    }
648
649    pub(crate) fn set_multi_click_line(&mut self, bounds: Bounds<Pixels>, cx: &mut App) {
650        self.set_multi_click_selection(
651            bounds.center(),
652            TextViewMultiClickKind::Line,
653            String::new(),
654            cx,
655        );
656        self.selected_text_override = None;
657        let offset = self.bounds.origin + self.scroll_offset();
658        if let Some(selection) = self.multi_click_selection.as_mut() {
659            selection.line_bounds = Some(Bounds::new(bounds.origin - offset, bounds.size));
660        }
661    }
662
663    pub(super) fn set_auto_scroll(&mut self, delta: Option<Pixels>, cx: &mut Context<Self>) {
664        self.auto_scroll.set(delta, cx, |delta, state, cx| {
665            state.list_state.scroll_by(delta);
666            cx.notify();
667        });
668    }
669
670    /// Return the window selection (anchor, cursor) in window coordinates if
671    /// this view participates in it.
672    ///
673    /// Single-view fast path: when both endpoints are anchored inside one
674    /// TextView, only that view participates (identical to the previous
675    /// per-view behavior).
676    pub(crate) fn selection_points(&self, cx: &App) -> Option<(Point<Pixels>, Point<Pixels>)> {
677        if !self.selectable {
678            return None;
679        }
680        self.selection_adapter.selection_points(cx)
681    }
682
683    pub(crate) fn has_selection(&self, cx: &App) -> bool {
684        self.has_view_selection() || self.selection_points(cx).is_some()
685    }
686
687    pub(super) fn on_action_select_all(
688        &mut self,
689        _: &SelectAll,
690        _: &mut Window,
691        cx: &mut Context<Self>,
692    ) {
693        if !self.selectable {
694            cx.propagate();
695            return;
696        }
697
698        self.select_all(cx);
699    }
700
701    pub(crate) fn is_selectable(&self) -> bool {
702        self.selectable
703    }
704
705    pub(crate) fn is_all_selected(&self) -> bool {
706        self.select_all
707    }
708
709    pub(crate) fn multi_click_selection(&self) -> Option<TextViewMultiClickSelection> {
710        let scroll_offset = self.scroll_offset();
711        self.multi_click_selection.map(|selection| {
712            let pos = selection.pos + scroll_offset + self.bounds.origin;
713            let line_bounds = selection.line_bounds.map(|bounds| {
714                Bounds::new(
715                    bounds.origin + scroll_offset + self.bounds.origin,
716                    bounds.size,
717                )
718            });
719            TextViewMultiClickSelection {
720                pos,
721                line_bounds,
722                ..selection
723            }
724        })
725    }
726}
727
728#[derive(Clone, Copy, Debug, PartialEq)]
729pub(crate) struct TextViewMultiClickSelection {
730    pub(crate) pos: Point<Pixels>,
731    pub(crate) kind: TextViewMultiClickKind,
732    pub(crate) line_bounds: Option<Bounds<Pixels>>,
733}
734
735#[derive(Clone, Copy, Debug, PartialEq, Eq)]
736pub(crate) enum TextViewMultiClickKind {
737    Word,
738    Paragraph,
739    Line,
740}
741
742impl Render for TextViewState {
743    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
744        let typography = (window.text_style(), window.rem_size());
745        if self
746            .layout_text_style
747            .as_ref()
748            .is_some_and(|previous| previous != &typography)
749        {
750            // ListState invalidates its cached rows for width changes, but does
751            // not know that an inherited font or rem change affects offscreen
752            // inline metrics. Retain logical selections just as for resources.
753            self.preserve_inline_selection = true;
754            self.compatible_layout_update = true;
755            self.invalidate_measured_heights();
756        }
757        self.layout_text_style = Some(typography);
758        let state = cx.entity();
759        let stream_fade = self.stream_fade.frame(Instant::now(), cx.reduce_motion());
760        if stream_fade.is_some() {
761            window.request_animation_frame();
762        }
763        // Built every frame, so everything in it is shared, not copied.
764        let node_cx = NodeContext {
765            offset: self.parsed_content.node_cx.offset,
766            link_refs: self.parsed_content.node_cx.link_refs.clone(),
767            style: self.text_view_style.clone(),
768            code_block_actions: self.code_block_actions.clone(),
769            code_block_highlighter: self.code_block_highlighter.clone(),
770            table_actions: self.table_actions.clone(),
771            link_click_handler: self.link_click_handler.clone(),
772            markdown_extensions: self.markdown_extensions.clone(),
773            stream_fade,
774        };
775
776        v_flex()
777            .w_full()
778            // Clamped content must keep its natural height: stretching it to
779            // the capped box would hide the overflow the clamp has to measure.
780            .when(self.max_lines.is_none(), |this| this.h_full())
781            .map(|this| match &self.parsed_error {
782                None => this.child(self.parsed_content.document.render_root(
783                    if self.scrollable {
784                        Some(self.list_state.clone())
785                    } else {
786                        None
787                    },
788                    &node_cx,
789                    window,
790                    cx,
791                )),
792                Some(err) => this.child(
793                    v_flex()
794                        .gap_1()
795                        .child("Failed to parse content")
796                        .child(err.to_string()),
797                ),
798            })
799            .on_prepaint(move |bounds, window, cx| {
800                let (
801                    size_changed,
802                    selection_involves_view,
803                    has_selection_snapshot,
804                    is_selecting,
805                    compatible_layout_update,
806                ) = {
807                    let state = state.read(cx);
808                    (
809                        state.bounds().size != bounds.size,
810                        state.selection_adapter.is_part_of_window_selection(cx),
811                        state.selection_adapter.has_selection_snapshot(cx),
812                        state.is_selecting,
813                        state.compatible_layout_update,
814                    )
815                };
816                let mut revision_changed = false;
817                state.update(cx, |state, cx| {
818                    revision_changed = state
819                        .selection_adapter
820                        .update_layout_revision(state.selection_revision, state.is_selecting);
821                    state.update_bounds(bounds, cx);
822                    state.compatible_layout_update = false;
823                });
824                if !is_selecting
825                    && ((size_changed && selection_involves_view && !compatible_layout_update)
826                        || (revision_changed && has_selection_snapshot))
827                {
828                    TextSelection::clear(window, cx);
829                }
830            })
831    }
832}
833
834#[derive(Clone, PartialEq, Default)]
835pub(crate) struct ParsedContent {
836    pub(crate) document: ParsedDocument,
837    pub(crate) node_cx: node::NodeContext,
838}
839
840struct UpdateFuture {
841    format: TextViewFormat,
842    content: ParsedContent,
843    rx: Pin<Box<Receiver<UpdateOptions>>>,
844    tx_result: Sender<ParsedUpdate>,
845}
846
847impl UpdateFuture {
848    fn new(
849        format: TextViewFormat,
850        rx: Receiver<UpdateOptions>,
851        tx_result: Sender<ParsedUpdate>,
852    ) -> Self {
853        Self {
854            format,
855            content: Default::default(),
856            rx: Box::pin(rx),
857            tx_result,
858        }
859    }
860}
861
862impl Future for UpdateFuture {
863    type Output = ();
864
865    fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
866        loop {
867            match self.rx.as_mut().poll_next(cx) {
868                Poll::Ready(Some(mut options)) => {
869                    let hit_coalesce_budget =
870                        merge_pending_options(&mut options, self.rx.as_ref().get_ref());
871
872                    let res = parse_content(self.format, self.content.clone(), &options);
873                    if let Ok(content) = &res {
874                        self.content = content.clone();
875                    }
876                    _ = self.tx_result.try_send(ParsedUpdate {
877                        revision: options.revision,
878                        full_parse: !options.append,
879                        selection_compatible: options.mode == ParseMode::Compatible,
880                        baseline_ack: options.mode == ParseMode::BaselineAck,
881                        result: res,
882                    });
883                    if hit_coalesce_budget {
884                        cx.waker().wake_by_ref();
885                        return Poll::Pending;
886                    }
887                    continue;
888                }
889                Poll::Ready(None) => return Poll::Ready(()),
890                Poll::Pending => return Poll::Pending,
891            }
892        }
893    }
894}
895
896#[derive(Clone)]
897struct UpdateOptions {
898    revision: usize,
899    pending_text: String,
900    append: bool,
901    mode: ParseMode,
902    markdown_extensions: Arc<MarkdownExtensions>,
903}
904
905impl UpdateOptions {
906    fn merge(&mut self, next: UpdateOptions) {
907        if next.append {
908            self.pending_text.push_str(&next.pending_text);
909            self.revision = next.revision;
910            if self.mode != ParseMode::Replace {
911                self.mode = ParseMode::Compatible;
912            }
913        } else {
914            *self = next;
915        }
916    }
917}
918
919struct ParsedUpdate {
920    revision: usize,
921    full_parse: bool,
922    selection_compatible: bool,
923    baseline_ack: bool,
924    result: Result<ParsedContent, SharedString>,
925}
926
927#[derive(Clone, Copy, Debug, PartialEq, Eq)]
928enum ParseMode {
929    BaselineAck,
930    Replace,
931    Compatible,
932}
933
934fn merge_pending_options(options: &mut UpdateOptions, rx: &Receiver<UpdateOptions>) -> bool {
935    let mut update_count = 1;
936
937    while update_count < MAX_COALESCED_UPDATES_PER_PARSE {
938        match rx.try_recv() {
939            Ok(next_options) => {
940                options.merge(next_options);
941                update_count += 1;
942            }
943            Err(_) => return false,
944        }
945    }
946
947    true
948}
949
950fn parse_content(
951    format: TextViewFormat,
952    mut content: ParsedContent,
953    options: &UpdateOptions,
954) -> Result<ParsedContent, SharedString> {
955    let mut node_cx = NodeContext {
956        markdown_extensions: options.markdown_extensions.clone(),
957        ..NodeContext::default()
958    };
959
960    // Re-parse the last block together with the appended text, so a block the
961    // new text continues (an unclosed list, a fenced code block) is not split
962    // in two. A block without a span cannot be located in `source` — the HTML
963    // parser never records spans — so it is left in place and only the
964    // appended text is parsed, positioned at the end of the current source.
965    let last_span = options
966        .append
967        .then(|| {
968            content
969                .document
970                .blocks
971                .last()
972                .and_then(|block| block.span())
973        })
974        .flatten();
975
976    let mut source = String::new();
977    if let Some(span) = last_span {
978        Arc::make_mut(&mut content.document.blocks).pop();
979        node_cx.offset = span.start;
980        source.push_str(&content.document.source[span.start..]);
981        source.push_str(&options.pending_text);
982    } else {
983        if options.append {
984            node_cx.offset = content.document.source.len();
985        }
986        source.push_str(&options.pending_text);
987    }
988
989    let new_document = match format {
990        TextViewFormat::Markdown => format::markdown::parse(&source, &mut node_cx),
991        TextViewFormat::Html => format::html::parse(&source, &mut node_cx),
992    }?;
993
994    if options.append {
995        content.document.source =
996            format!("{}{}", content.document.source, options.pending_text).into();
997        Arc::make_mut(&mut content.document.blocks)
998            .extend(Arc::unwrap_or_clone(new_document.blocks));
999    } else {
1000        content.document = new_document;
1001    }
1002
1003    Ok(content)
1004}
1005
1006#[cfg(test)]
1007mod tests {
1008    use super::*;
1009    use crate::text::MarkdownNode;
1010    use gpui::TestAppContext;
1011
1012    mod stream_fade {
1013        use std::{ops::Range, time::Duration};
1014
1015        use gpui::{Entity, TestAppContext};
1016
1017        use super::super::*;
1018        use crate::{motion::Easing, text::stream_fade::TextLeafKey};
1019
1020        /// Long enough that a debug test cannot outrun the fade before it
1021        /// samples the first frame.
1022        const FADE: Duration = Duration::from_secs(10);
1023
1024        fn fading_state(markdown: &str, cx: &mut TestAppContext) -> Entity<TextViewState> {
1025            cx.update(crate::init);
1026            let state = cx.update(|cx| {
1027                cx.new(|cx| {
1028                    TextViewState::markdown(markdown, cx).motion(
1029                        TextViewMotion::default()
1030                            .with_stream_fade(FADE)
1031                            .with_stream_fade_easing(Easing::Linear),
1032                    )
1033                })
1034            });
1035            cx.run_until_parked();
1036            state
1037        }
1038
1039        /// The fade ranges of `key` sampled right now, or `None` when the
1040        /// state has nothing fading.
1041        fn fades(
1042            state: &Entity<TextViewState>,
1043            key: TextLeafKey,
1044            cx: &mut TestAppContext,
1045        ) -> Option<Vec<Range<usize>>> {
1046            state.update(cx, |state, _| {
1047                let frame = state.stream_fade.frame(Instant::now(), false)?;
1048                let fades = frame.fades(key)?;
1049                assert!(
1050                    fades.iter().all(|(_, fade_out)| *fade_out > 0.9),
1051                    "a fade sampled right after it starts is still transparent: {fades:?}"
1052                );
1053                Some(fades.iter().map(|(range, _)| range.clone()).collect())
1054            })
1055        }
1056
1057        #[gpui::test]
1058        fn push_str_fades_only_the_appended_text(cx: &mut TestAppContext) {
1059            let state = fading_state("hello", cx);
1060            state.update(cx, |state, cx| state.push_str(" world", cx));
1061            cx.run_until_parked();
1062
1063            assert_eq!(fades(&state, TextLeafKey::block(0), cx), Some(vec![5..11]));
1064
1065            state.update(cx, |state, _| {
1066                let later = Instant::now() + FADE + Duration::from_secs(1);
1067                assert!(state.stream_fade.frame(later, false).is_none());
1068                assert!(state.stream_fade.frame(Instant::now(), false).is_none());
1069            });
1070        }
1071
1072        #[gpui::test]
1073        fn set_text_extending_the_text_fades_like_push_str(cx: &mut TestAppContext) {
1074            let state = fading_state("hello", cx);
1075            state.update(cx, |state, cx| state.set_text("hello world", cx));
1076            cx.run_until_parked();
1077
1078            assert_eq!(fades(&state, TextLeafKey::block(0), cx), Some(vec![5..11]));
1079        }
1080
1081        #[gpui::test]
1082        fn set_text_replacing_the_text_shows_it_at_once(cx: &mut TestAppContext) {
1083            let state = fading_state("hello", cx);
1084            state.update(cx, |state, cx| state.push_str(" world", cx));
1085            cx.run_until_parked();
1086            assert!(fades(&state, TextLeafKey::block(0), cx).is_some());
1087
1088            state.update(cx, |state, cx| state.set_text("other", cx));
1089            cx.run_until_parked();
1090
1091            assert_eq!(fades(&state, TextLeafKey::block(0), cx), None);
1092        }
1093
1094        #[gpui::test]
1095        fn completed_markup_refades_from_the_divergence(cx: &mut TestAppContext) {
1096            // The paragraph renders `text` (trailing space trimmed), then
1097            // `text **bo` literally.
1098            let state = fading_state("text ", cx);
1099            state.update(cx, |state, cx| state.push_str("**bo", cx));
1100            cx.run_until_parked();
1101            assert_eq!(fades(&state, TextLeafKey::block(0), cx), Some(vec![4..9]));
1102
1103            // `text **bold**` renders `text bold`: the space keeps its fade,
1104            // the glyphs from byte 5 on changed and fade again as one run.
1105            state.update(cx, |state, cx| state.push_str("ld**", cx));
1106            cx.run_until_parked();
1107            assert_eq!(
1108                fades(&state, TextLeafKey::block(0), cx),
1109                Some(vec![4..5, 5..9])
1110            );
1111        }
1112
1113        #[gpui::test]
1114        fn without_stagger_an_update_fades_as_one_chunk(cx: &mut TestAppContext) {
1115            let state = fading_state("hello", cx);
1116            state.update(cx, |state, cx| state.push_str(" one two three", cx));
1117            cx.run_until_parked();
1118
1119            assert_eq!(fades(&state, TextLeafKey::block(0), cx), Some(vec![5..19]));
1120        }
1121
1122        #[gpui::test]
1123        fn words_of_one_update_start_one_after_another(cx: &mut TestAppContext) {
1124            let stagger = Duration::from_millis(100);
1125            let state = fading_state("hello", cx);
1126            state.update(cx, |state, _| {
1127                state.set_motion(
1128                    TextViewMotion::default()
1129                        .with_stream_fade(FADE)
1130                        .with_stream_fade_stagger(stagger)
1131                        .with_stream_fade_easing(Easing::Linear),
1132                )
1133            });
1134            state.update(cx, |state, cx| state.push_str(" one two three", cx));
1135            cx.run_until_parked();
1136
1137            state.update(cx, |state, _| {
1138                let frame = state
1139                    .stream_fade
1140                    .frame(Instant::now() + stagger * 3, false)
1141                    .expect("words still fading");
1142                let fades = frame.fades(TextLeafKey::block(0)).expect("paragraph fades");
1143                let ranges: Vec<_> = fades.iter().map(|(range, _)| range.clone()).collect();
1144                assert_eq!(ranges, vec![5..10, 10..14, 14..19]);
1145                // A later word has faded less, so it is still more transparent.
1146                assert!(
1147                    fades[0].1 < fades[1].1 && fades[1].1 < fades[2].1,
1148                    "{fades:?}"
1149                );
1150            });
1151        }
1152
1153        #[gpui::test]
1154        fn a_new_paragraph_fades_as_a_whole(cx: &mut TestAppContext) {
1155            let state = fading_state("first", cx);
1156            state.update(cx, |state, cx| state.push_str("\n\nsecond", cx));
1157            cx.run_until_parked();
1158
1159            assert_eq!(fades(&state, TextLeafKey::block(0), cx), None);
1160            assert_eq!(fades(&state, TextLeafKey::block(7), cx), Some(vec![0..6]));
1161        }
1162
1163        #[gpui::test]
1164        fn code_block_text_fades_by_block(cx: &mut TestAppContext) {
1165            let state = fading_state("```rs\nlet", cx);
1166            state.update(cx, |state, cx| state.push_str(" x", cx));
1167            cx.run_until_parked();
1168
1169            assert_eq!(fades(&state, TextLeafKey::block(0), cx), Some(vec![3..5]));
1170        }
1171
1172        #[gpui::test]
1173        fn table_cells_fade_by_ordinal(cx: &mut TestAppContext) {
1174            let state = fading_state("| a | b |\n|---|---|\n| c | d", cx);
1175            state.update(cx, |state, cx| state.push_str("e |", cx));
1176            cx.run_until_parked();
1177
1178            assert_eq!(fades(&state, TextLeafKey::table_cell(0, 2), cx), None);
1179            assert_eq!(
1180                fades(&state, TextLeafKey::table_cell(0, 3), cx),
1181                Some(vec![1..2])
1182            );
1183        }
1184
1185        #[gpui::test]
1186        fn zero_duration_records_nothing(cx: &mut TestAppContext) {
1187            cx.update(crate::init);
1188            let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("hello", cx)));
1189            cx.run_until_parked();
1190            state.update(cx, |state, cx| state.push_str(" world", cx));
1191            cx.run_until_parked();
1192
1193            state.update(cx, |state, _| {
1194                assert!(state.stream_fade.frame(Instant::now(), false).is_none());
1195            });
1196        }
1197
1198        #[gpui::test]
1199        fn reduced_motion_drops_the_fade(cx: &mut TestAppContext) {
1200            let state = fading_state("hello", cx);
1201            state.update(cx, |state, cx| state.push_str(" world", cx));
1202            cx.run_until_parked();
1203
1204            state.update(cx, |state, _| {
1205                assert!(state.stream_fade.frame(Instant::now(), true).is_none());
1206                assert!(state.stream_fade.frame(Instant::now(), false).is_none());
1207            });
1208        }
1209    }
1210
1211    #[gpui::test]
1212    fn small_full_replace_parses_before_background_executor_runs(cx: &mut TestAppContext) {
1213        cx.update(crate::init);
1214        let markdown = "# ready";
1215        let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown(markdown, cx)));
1216
1217        state.read_with(cx, |state, _| {
1218            assert_eq!(state.source().as_str(), markdown);
1219            assert_eq!(state.parsed_content.document.blocks.len(), 1);
1220        });
1221    }
1222
1223    #[gpui::test]
1224    fn large_markdown_and_html_full_replacements_wait_for_background_executor(
1225        cx: &mut TestAppContext,
1226    ) {
1227        cx.update(crate::init);
1228        let markdown = "# x\n\n".repeat(MAX_SYNC_FULL_REPLACE_BYTES / 5 + 1);
1229        let html = format!("<p>{}</p>", "x".repeat(MAX_SYNC_FULL_REPLACE_BYTES + 1));
1230        assert!(markdown.len() > MAX_SYNC_FULL_REPLACE_BYTES);
1231        assert!(html.len() > MAX_SYNC_FULL_REPLACE_BYTES);
1232
1233        let (markdown_state, html_state) = cx.update(|cx| {
1234            (
1235                cx.new(|cx| TextViewState::markdown(&markdown, cx)),
1236                cx.new(|cx| TextViewState::html(&html, cx)),
1237            )
1238        });
1239
1240        markdown_state.read_with(cx, |state, _| {
1241            assert_eq!(state.text.as_str(), markdown.as_str());
1242            assert!(state.source().as_str().is_empty());
1243            assert!(state.parsed_content.document.blocks.is_empty());
1244        });
1245        html_state.read_with(cx, |state, _| {
1246            assert_eq!(state.text.as_str(), html.as_str());
1247            assert!(state.source().as_str().is_empty());
1248            assert!(state.parsed_content.document.blocks.is_empty());
1249        });
1250
1251        cx.run_until_parked();
1252
1253        markdown_state.read_with(cx, |state, _| {
1254            assert_eq!(state.source().as_str(), markdown.as_str());
1255            assert!(!state.parsed_content.document.blocks.is_empty());
1256        });
1257        html_state.read_with(cx, |state, _| {
1258            assert_eq!(state.source().as_str(), html.as_str());
1259            assert!(!state.parsed_content.document.blocks.is_empty());
1260        });
1261    }
1262
1263    #[gpui::test]
1264    fn inline_source_ranges_follow_streamed_tail_reparsing(cx: &mut TestAppContext) {
1265        cx.update(crate::init);
1266        let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("中文 $a$\n\n尾 $x", cx)));
1267        state.update(cx, |state, cx| {
1268            let extensions = MarkdownExtensions::default().plugin(
1269                crate::text::markdown_ext::TestInlinePlugin::new("test").parse_with(|node, _| {
1270                    let markdown::mdast::Node::InlineMath(math) = node else {
1271                        return None;
1272                    };
1273                    Some(super::super::MarkdownNode::new("formula", ()).text(math.value.clone()))
1274                }),
1275            );
1276            state.set_markdown_extensions(Arc::new(extensions), cx);
1277        });
1278        cx.run_until_parked();
1279        state.update(cx, |state, cx| state.push_str("^2$ 后", cx));
1280        cx.run_until_parked();
1281        state.read_with(cx, |state, _| {
1282            let source = "中文 $a$\n\n尾 $x^2$ 后";
1283            assert_eq!(state.source().as_str(), source);
1284            let objects: Vec<_> = state
1285                .parsed_content
1286                .document
1287                .blocks
1288                .iter()
1289                .flat_map(|block| {
1290                    let super::super::node::BlockNode::Paragraph(paragraph) = block else {
1291                        panic!()
1292                    };
1293                    paragraph
1294                        .children
1295                        .iter()
1296                        .filter_map(|node| node.custom.as_ref())
1297                })
1298                .collect();
1299            assert_eq!(objects.len(), 2);
1300            for (object, markdown) in objects.iter().zip(["$a$", "$x^2$"]) {
1301                let start = source.find(markdown).unwrap();
1302                assert_eq!(object.source_range(), Some(start..start + markdown.len()));
1303                assert_eq!(object.as_markdown(), markdown);
1304            }
1305        });
1306    }
1307
1308    #[gpui::test]
1309    fn async_full_replace_then_push_str_preserves_complete_source(cx: &mut TestAppContext) {
1310        cx.update(crate::init);
1311        let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("old", cx)));
1312        cx.run_until_parked();
1313
1314        let replacement = "x".repeat(MAX_SYNC_FULL_REPLACE_BYTES + 1);
1315        let expected = format!("{replacement} tail");
1316        state.update(cx, |state, cx| {
1317            state.set_text(&replacement, cx);
1318            state.push_str(" tail", cx);
1319        });
1320        cx.run_until_parked();
1321
1322        state.read_with(cx, |state, _| {
1323            assert_eq!(state.text.as_str(), expected.as_str());
1324            assert_eq!(state.source().as_str(), expected.as_str());
1325        });
1326    }
1327
1328    #[gpui::test]
1329    fn html_push_str_keeps_earlier_blocks(cx: &mut TestAppContext) {
1330        cx.update(crate::init);
1331        let state = cx.update(|cx| cx.new(|cx| TextViewState::html("<p>first</p>", cx)));
1332        cx.run_until_parked();
1333
1334        state.update(cx, |state, cx| {
1335            state.push_str("<p>second</p>", cx);
1336        });
1337        cx.run_until_parked();
1338
1339        state.read_with(cx, |state, _| {
1340            assert_eq!(state.source().as_str(), "<p>first</p><p>second</p>");
1341            let text = state
1342                .parsed_content
1343                .document
1344                .blocks
1345                .iter()
1346                .map(|block| block.text())
1347                .collect::<String>();
1348            assert!(text.contains("first"), "lost the first block: {text:?}");
1349            assert!(text.contains("second"), "lost the appended block: {text:?}");
1350        });
1351    }
1352
1353    #[gpui::test]
1354    fn element_text_of_the_same_string_is_not_compared_again(cx: &mut TestAppContext) {
1355        cx.update(crate::init);
1356        let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("", cx)));
1357        let revision = |cx: &mut TestAppContext| state.read_with(cx, |state, _| state.revision);
1358        let text = SharedString::from("hello");
1359
1360        state.update(cx, |state, cx| state.set_element_text(&text, cx));
1361        let parsed = revision(cx);
1362        assert!(parsed > 0);
1363
1364        // The same allocation again, and equal bytes in another allocation,
1365        // both leave the parsed text alone.
1366        state.update(cx, |state, cx| state.set_element_text(&text, cx));
1367        assert_eq!(revision(cx), parsed);
1368        let equal = SharedString::from("hello".to_string());
1369        state.update(cx, |state, cx| state.set_element_text(&equal, cx));
1370        assert_eq!(revision(cx), parsed);
1371
1372        // Once the state's text moved on, the element's string is set again.
1373        state.update(cx, |state, cx| {
1374            state.push_str(" world", cx);
1375            state.set_element_text(&equal, cx);
1376        });
1377        state.read_with(cx, |state, _| assert_eq!(state.text.as_str(), "hello"));
1378    }
1379
1380    #[gpui::test]
1381    fn set_text_then_push_str_appends_to_replaced_content(cx: &mut TestAppContext) {
1382        cx.update(crate::init);
1383        let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("old", cx)));
1384        cx.run_until_parked();
1385
1386        state.update(cx, |state, cx| {
1387            state.set_text("", cx);
1388            state.push_str("new", cx);
1389            state.push_str(" text", cx);
1390        });
1391        cx.run_until_parked();
1392
1393        state.read_with(cx, |state, _| {
1394            assert_eq!(state.text.as_str(), "new text");
1395            assert_eq!(state.source().as_str(), "new text");
1396        });
1397
1398        state.update(cx, |state, cx| {
1399            state.set_text("", cx);
1400        });
1401        cx.run_until_parked();
1402
1403        state.read_with(cx, |state, _| {
1404            assert_eq!(state.text.as_str(), "");
1405            assert_eq!(state.source().as_str(), "");
1406        });
1407    }
1408
1409    #[gpui::test]
1410    fn full_parse_coalesced_with_append_preserves_new_select_all(cx: &mut TestAppContext) {
1411        cx.update(crate::init);
1412        let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("old", cx)));
1413        cx.run_until_parked();
1414
1415        state.update(cx, |state, cx| {
1416            state.set_text("new", cx);
1417            state.push_str(" text", cx);
1418            state.select_all(cx);
1419        });
1420        cx.run_until_parked();
1421
1422        state.read_with(cx, |state, _| {
1423            assert!(state.select_all);
1424            assert_eq!(state.selected_text().trim(), "new text");
1425        });
1426    }
1427
1428    #[test]
1429    fn update_options_merge_keeps_latest_full_text() {
1430        let mut options = UpdateOptions {
1431            revision: 1,
1432            pending_text: "old".to_string(),
1433            append: true,
1434            mode: ParseMode::Compatible,
1435            markdown_extensions: Arc::default(),
1436        };
1437
1438        options.merge(UpdateOptions {
1439            revision: 2,
1440            pending_text: "new".to_string(),
1441            append: false,
1442            mode: ParseMode::BaselineAck,
1443            markdown_extensions: Arc::default(),
1444        });
1445        options.merge(UpdateOptions {
1446            revision: 3,
1447            pending_text: " text".to_string(),
1448            append: true,
1449            mode: ParseMode::Compatible,
1450            markdown_extensions: Arc::default(),
1451        });
1452
1453        assert_eq!(options.revision, 3);
1454        assert_eq!(options.pending_text, "new text");
1455        assert!(!options.append);
1456    }
1457
1458    #[test]
1459    fn append_merged_into_async_replace_remains_a_replacement() {
1460        let mut options = UpdateOptions {
1461            revision: 1,
1462            pending_text: "new".to_string(),
1463            append: false,
1464            mode: ParseMode::Replace,
1465            markdown_extensions: Arc::default(),
1466        };
1467
1468        options.merge(UpdateOptions {
1469            revision: 2,
1470            pending_text: " text".to_string(),
1471            append: true,
1472            mode: ParseMode::Compatible,
1473            markdown_extensions: Arc::default(),
1474        });
1475
1476        assert_eq!(options.revision, 2);
1477        assert_eq!(options.pending_text, "new text");
1478        assert!(!options.append);
1479        assert_eq!(options.mode, ParseMode::Replace);
1480    }
1481
1482    #[test]
1483    fn update_future_yields_before_coalescing_all_queued_updates() {
1484        let (tx, rx) = unbounded::<UpdateOptions>();
1485        let (tx_result, rx_result) = unbounded::<ParsedUpdate>();
1486        let total_updates = 128;
1487
1488        for revision in 1..=total_updates {
1489            tx.try_send(UpdateOptions {
1490                revision,
1491                pending_text: format!("{revision}\n"),
1492                append: revision != 1,
1493                mode: if revision == 1 {
1494                    ParseMode::BaselineAck
1495                } else {
1496                    ParseMode::Compatible
1497                },
1498                markdown_extensions: Arc::default(),
1499            })
1500            .unwrap();
1501        }
1502
1503        let mut future = Box::pin(UpdateFuture::new(TextViewFormat::Markdown, rx, tx_result));
1504        let waker = futures::task::noop_waker();
1505        let mut task_cx = std::task::Context::from_waker(&waker);
1506
1507        assert!(matches!(
1508            std::future::Future::poll(future.as_mut(), &mut task_cx),
1509            Poll::Pending
1510        ));
1511        let parsed_update = rx_result.try_recv().expect("parse result");
1512
1513        assert!(
1514            parsed_update.revision < total_updates,
1515            "single poll coalesced every queued update through revision {}",
1516            parsed_update.revision
1517        );
1518
1519        assert!(matches!(
1520            std::future::Future::poll(future.as_mut(), &mut task_cx),
1521            Poll::Pending
1522        ));
1523        let parsed_update = rx_result.try_recv().expect("next parse result");
1524        assert_eq!(parsed_update.revision, total_updates);
1525    }
1526
1527    #[gpui::test]
1528    fn select_all_returns_rendered_text(cx: &mut TestAppContext) {
1529        cx.update(crate::init);
1530        let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("**quick** value", cx)));
1531        cx.run_until_parked();
1532
1533        state.update(cx, |state, cx| {
1534            state.select_all(cx);
1535        });
1536
1537        state.read_with(cx, |state, _| {
1538            assert!(state.has_view_selection());
1539            assert_eq!(state.selected_text().trim(), "quick value");
1540        });
1541
1542        state.update(cx, |state, cx| {
1543            state.clear_selection(cx);
1544        });
1545
1546        state.read_with(cx, |state, _| {
1547            assert!(!state.has_view_selection());
1548            assert_eq!(state.selected_text(), "");
1549        });
1550    }
1551
1552    #[gpui::test]
1553    fn select_all_in_source_format_returns_source(cx: &mut TestAppContext) {
1554        cx.update(crate::init);
1555        let markdown = "**quick** value";
1556        let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown(markdown, cx)));
1557        cx.run_until_parked();
1558
1559        state.update(cx, |state, cx| state.select_all(cx));
1560
1561        // The default (plain) mode strips the markup.
1562        state.read_with(cx, |state, _| {
1563            assert_eq!(state.selected_text().trim(), "quick value");
1564        });
1565
1566        state.update(cx, |state, cx| {
1567            state.set_selection_format(SelectionFormat::Source, cx)
1568        });
1569
1570        // Source mode yields the whole source verbatim.
1571        state.read_with(cx, |state, _| {
1572            assert_eq!(state.selected_text().trim(), markdown);
1573        });
1574    }
1575
1576    #[gpui::test]
1577    fn parser_revision_reparses_same_name_inline_configuration(cx: &mut TestAppContext) {
1578        cx.update(crate::init);
1579        let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("@member", cx)));
1580        for (revision, label) in [(1, "Alice"), (2, "Bob")] {
1581            let extensions = MarkdownExtensions::default()
1582                .parser_revision(revision)
1583                .plugin(
1584                    crate::text::markdown_ext::TestInlinePlugin::new("test").parse_with(
1585                        move |node, _| {
1586                            matches!(node, markdown::mdast::Node::Text(_))
1587                                .then(|| MarkdownNode::new("mention", label).text(label))
1588                        },
1589                    ),
1590                );
1591            state.update(cx, |state, cx| {
1592                state.set_markdown_extensions(Arc::new(extensions), cx)
1593            });
1594            cx.run_until_parked();
1595            state.read_with(cx, |state, _| {
1596                let node::BlockNode::Paragraph(paragraph) =
1597                    &state.parsed_content.document.blocks[0]
1598                else {
1599                    panic!()
1600                };
1601                assert_eq!(
1602                    paragraph.children[0].custom.as_ref().unwrap().as_text(),
1603                    label
1604                );
1605            });
1606        }
1607    }
1608
1609    #[gpui::test]
1610    fn set_markdown_extensions_reparses_existing_text(cx: &mut TestAppContext) {
1611        cx.update(crate::init);
1612        let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("$TSLA.US", cx)));
1613        cx.run_until_parked();
1614
1615        let extensions = MarkdownExtensions::default().block_parser(|node, cx| {
1616            let markdown::mdast::Node::Paragraph(paragraph) = node else {
1617                return None;
1618            };
1619            let [markdown::mdast::Node::Text(text)] = paragraph.children.as_slice() else {
1620                return None;
1621            };
1622            let symbol = text.value.strip_prefix('$')?.to_string();
1623            let node_text = format!("${symbol}");
1624
1625            Some(
1626                MarkdownNode::new("ticker", symbol)
1627                    .text(node_text)
1628                    .markdown(cx.node_source(node).unwrap_or_default()),
1629            )
1630        });
1631
1632        state.update(cx, |state, cx| {
1633            state.set_markdown_extensions(Arc::new(extensions), cx);
1634        });
1635        cx.run_until_parked();
1636
1637        state.read_with(cx, |state, _| {
1638            let node::BlockNode::Custom(node) = &state.parsed_content.document.blocks[0] else {
1639                panic!("expected custom markdown node");
1640            };
1641            assert_eq!(node.name(), "ticker");
1642            assert_eq!(node.data::<String>().map(String::as_str), Some("TSLA.US"));
1643        });
1644    }
1645}