Skip to main content

gpui_base/text/
state.rs

1use futures::Stream as _;
2use std::{
3    ops::RangeInclusive,
4    pin::Pin,
5    sync::{Arc, Mutex},
6    task::Poll,
7};
8
9use gpui::{
10    App, AppContext as _, Bounds, Context, FocusHandle, IntoElement, KeyBinding, ListState,
11    ParentElement as _, Pixels, Point, Render, SharedString, Styled as _, Task, Window,
12    prelude::FluentBuilder as _, px,
13};
14
15use crate::{
16    AutoScroll, ElementExt, TextSelection,
17    async_util::{Receiver, Sender, unbounded},
18    input::{self, SelectAll},
19    text::{
20        CodeBlockActionsFn, CodeBlockHighlighterFn, LinkClickHandlerFn, MarkdownExtensions,
21        TableActionsFn, TextViewStyle,
22        document::ParsedDocument,
23        format,
24        node::{self, NodeContext},
25        selection_adapter::TextViewSelectionAdapter,
26    },
27    v_flex,
28};
29
30const CONTEXT: &'static str = "TextView";
31// Keep coalescing bounded so sustained streams still render intermediate updates.
32const MAX_COALESCED_UPDATES_PER_PARSE: usize = 64;
33// Preserve exact first-layout height for small documents while bounding the
34// amount of source parsed synchronously on the UI thread.
35const MAX_SYNC_FULL_REPLACE_BYTES: usize = 4 * 1024;
36
37pub(crate) fn init(cx: &mut App) {
38    cx.bind_keys(vec![
39        #[cfg(target_os = "macos")]
40        KeyBinding::new("cmd-c", input::Copy, Some(CONTEXT)),
41        #[cfg(not(target_os = "macos"))]
42        KeyBinding::new("ctrl-c", input::Copy, Some(CONTEXT)),
43        #[cfg(target_os = "macos")]
44        KeyBinding::new("cmd-a", input::SelectAll, Some(CONTEXT)),
45        #[cfg(not(target_os = "macos"))]
46        KeyBinding::new("ctrl-a", input::SelectAll, Some(CONTEXT)),
47    ]);
48}
49
50/// The content format of the text view.
51#[derive(Clone, Copy, PartialEq, Eq)]
52pub(super) enum TextViewFormat {
53    /// Markdown view
54    Markdown,
55    /// HTML view
56    Html,
57}
58
59/// The format of the text returned by
60/// [`TextViewState::selected_text`], which is also what copy writes to the
61/// clipboard.
62#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
63pub enum SelectionFormat {
64    /// The rendered text, without any markup.
65    #[default]
66    Plain,
67    /// The source of the selection.
68    ///
69    /// Select-all returns the original source verbatim, a partial selection is
70    /// reconstructed as Markdown from the parsed nodes (e.g. selecting inside
71    /// a `**bold**` run yields `**bold**`).
72    Source,
73}
74
75/// One text element's laid-out vertical extent, reported by `Inline` during
76/// prepaint so `TextView` can snap its `max_lines` clip to a whole-line
77/// boundary.
78#[derive(Clone, Copy)]
79pub(super) struct LineSpan {
80    pub(super) top: Pixels,
81    pub(super) bottom: Pixels,
82    pub(super) line_height: Pixels,
83}
84
85/// The state of a TextView.
86pub struct TextViewState {
87    pub(super) focus_handle: FocusHandle,
88    pub(super) list_state: ListState,
89
90    /// The bounds of the text view
91    bounds: Bounds<Pixels>,
92
93    pub(super) selectable: bool,
94    pub(super) selection_format: SelectionFormat,
95    pub(super) scrollable: bool,
96    pub(super) max_lines: Option<usize>,
97    /// Line spans reported by `Inline` during prepaint (collected only while
98    /// [`Self::max_lines`] is set); cleared by `TextView` at each frame start.
99    pub(super) line_spans: Arc<Mutex<Vec<LineSpan>>>,
100    /// Whether the last painted frame clipped content due to `max_lines`.
101    pub(super) clamped: bool,
102    pub(super) text_view_style: TextViewStyle,
103    pub(super) code_block_actions: Option<std::sync::Arc<CodeBlockActionsFn>>,
104    pub(super) code_block_highlighter: Option<std::sync::Arc<CodeBlockHighlighterFn>>,
105    pub(super) table_actions: Option<std::sync::Arc<TableActionsFn>>,
106    pub(super) link_click_handler: Option<std::sync::Arc<LinkClickHandlerFn>>,
107    pub(super) markdown_extensions: Arc<MarkdownExtensions>,
108
109    pub(super) is_selecting: bool,
110    multi_click_selection: Option<TextViewMultiClickSelection>,
111    selected_text_override: Option<String>,
112    select_all: bool,
113    pub(super) auto_scroll: AutoScroll,
114    pub(super) selection_adapter: TextViewSelectionAdapter,
115
116    pub(super) parsed_content: ParsedContent,
117    /// Content format (markdown / html), used for bounded synchronous parsing
118    /// of small full-replace updates.
119    format: TextViewFormat,
120    text: String,
121    revision: usize,
122    pub(super) selection_revision: usize,
123    compatible_layout_update: bool,
124    parsed_error: Option<SharedString>,
125    tx: Sender<UpdateOptions>,
126    _parse_task: Task<()>,
127    _receive_task: Task<()>,
128}
129
130impl TextViewState {
131    /// Create a Markdown TextViewState.
132    pub fn markdown(text: &str, cx: &mut Context<Self>) -> Self {
133        Self::new(TextViewFormat::Markdown, text, cx)
134    }
135
136    /// Create a HTML TextViewState.
137    pub fn html(text: &str, cx: &mut Context<Self>) -> Self {
138        Self::new(TextViewFormat::Html, text, cx)
139    }
140
141    /// Create a new TextViewState.
142    fn new(format: TextViewFormat, text: &str, cx: &mut Context<Self>) -> Self {
143        let focus_handle = cx.focus_handle();
144        let selection_adapter = TextViewSelectionAdapter::new(cx.entity().downgrade(), cx);
145
146        let (tx, rx) = unbounded::<UpdateOptions>();
147        let (tx_result, rx_result) = unbounded::<ParsedUpdate>();
148        let _receive_task = cx.spawn({
149            async move |weak_self, cx| {
150                while let Ok(parsed_update) = rx_result.recv().await {
151                    _ = weak_self.update(cx, |state, cx| {
152                        if parsed_update.revision != state.revision {
153                            return;
154                        }
155                        if parsed_update.baseline_ack {
156                            debug_assert!(parsed_update.full_parse);
157                            return;
158                        }
159
160                        match parsed_update.result {
161                            Ok(content) => {
162                                state.parsed_content = content;
163                                state.parsed_error = None;
164                                state.compatible_layout_update = parsed_update.selection_compatible;
165                                if parsed_update.full_parse {
166                                    state.invalidate_measured_heights();
167                                }
168                            }
169                            Err(err) => {
170                                state.parsed_error = Some(err);
171                            }
172                        }
173                        // Don't interrupt an active drag-selection; the stored
174                        // positions remain valid for append-only updates and will
175                        // self-correct on the next mouse-move event.
176                        if !parsed_update.selection_compatible && !state.is_selecting {
177                            state.reset_selection_and_adapter(cx);
178                        }
179                        cx.notify();
180                    });
181                }
182            }
183        });
184
185        let _parse_task = cx.background_spawn(UpdateFuture::new(format, rx, tx_result));
186
187        let mut this = Self {
188            focus_handle,
189            bounds: Bounds::default(),
190            multi_click_selection: None,
191            selected_text_override: None,
192            select_all: false,
193            selectable: false,
194            selection_format: SelectionFormat::default(),
195            scrollable: false,
196            max_lines: None,
197            line_spans: Arc::default(),
198            clamped: false,
199            // Measure all blocks (not just visible ones) so the scrollbar
200            // thumb size stays stable. Without this, off-screen blocks count
201            // as zero height until scrolled into view, which makes the
202            // scrollbar jitter as more blocks get measured during scrolling.
203            list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)).measure_all(),
204            text_view_style: TextViewStyle::default(),
205            code_block_actions: None,
206            code_block_highlighter: None,
207            table_actions: None,
208            link_click_handler: None,
209            markdown_extensions: Arc::default(),
210            is_selecting: false,
211            auto_scroll: AutoScroll::default(),
212            selection_adapter,
213            parsed_content: Default::default(),
214            format,
215            parsed_error: None,
216            text: text.to_string(),
217            revision: 0,
218            selection_revision: 0,
219            compatible_layout_update: false,
220            tx,
221            _parse_task,
222            _receive_task,
223        };
224        this.increment_update(&text, false, cx);
225        this
226    }
227
228    /// Get the text content.
229    pub(crate) fn source(&self) -> SharedString {
230        self.parsed_content.document.source.clone()
231    }
232
233    /// Set whether the text is selectable, default false.
234    pub fn selectable(mut self, selectable: bool) -> Self {
235        self.selectable = selectable;
236        self
237    }
238
239    /// Set whether the text is selectable, default false.
240    pub fn set_selectable(&mut self, selectable: bool, cx: &mut Context<Self>) {
241        self.selectable = selectable;
242        cx.notify();
243    }
244
245    /// Set the [`SelectionFormat`], default is [`SelectionFormat::Plain`].
246    pub fn selection_format(mut self, selection_format: SelectionFormat) -> Self {
247        self.selection_format = selection_format;
248        self
249    }
250
251    /// Set the [`SelectionFormat`], default is [`SelectionFormat::Plain`].
252    pub fn set_selection_format(
253        &mut self,
254        selection_format: SelectionFormat,
255        cx: &mut Context<Self>,
256    ) {
257        self.selection_format = selection_format;
258        cx.notify();
259    }
260
261    /// Set whether the text view scrolls internally, default false.
262    pub fn scrollable(mut self, scrollable: bool) -> Self {
263        self.scrollable = scrollable;
264        self
265    }
266
267    /// Set whether the text view scrolls internally, default false.
268    pub fn set_scrollable(&mut self, scrollable: bool, cx: &mut Context<Self>) {
269        if !scrollable {
270            self.reset_selection_and_adapter(cx);
271        }
272        self.scrollable = scrollable;
273        cx.notify();
274    }
275
276    /// Whether the last painted frame clipped content because of
277    /// [`TextView::max_lines`](crate::text::TextView::max_lines).
278    pub fn is_clamped(&self) -> bool {
279        self.clamped
280    }
281
282    /// Set the text content.
283    pub fn set_text(&mut self, text: &str, cx: &mut Context<Self>) {
284        if self.text.as_str() == text {
285            return;
286        }
287
288        self.text.clear();
289        self.text.push_str(text);
290        self.parsed_error = None;
291        self.increment_update(text, false, cx);
292    }
293
294    /// Append partial text content to the existing text.
295    pub fn push_str(&mut self, new_text: &str, cx: &mut Context<Self>) {
296        if new_text.is_empty() {
297            return;
298        }
299        self.text.push_str(new_text);
300        self.increment_update(new_text, true, cx);
301    }
302
303    pub(crate) fn set_markdown_extensions(
304        &mut self,
305        markdown_extensions: Arc<MarkdownExtensions>,
306        cx: &mut Context<Self>,
307    ) {
308        if self.markdown_extensions.revision() == markdown_extensions.revision() {
309            return;
310        }
311
312        let parser_configuration_changed = !self
313            .markdown_extensions
314            .has_same_parser_configuration(&markdown_extensions);
315        self.markdown_extensions = markdown_extensions;
316        if parser_configuration_changed && self.format == TextViewFormat::Markdown {
317            let text = self.text.clone();
318            self.increment_update(&text, false, cx);
319        }
320    }
321
322    /// Return the selected text, in the view's [`SelectionFormat`].
323    pub fn selected_text(&self) -> String {
324        self.selected_text_in(None)
325    }
326
327    /// The format to copy in, which is [`SelectionFormat::Plain`] whenever the
328    /// requested one cannot be produced.
329    ///
330    /// Only a Markdown view can return source. Reconstructing HTML would mean
331    /// spelling every attribute back out — a mark's color, an image's
332    /// dimensions, a cell's alignment — with a new way to lose one at each
333    /// step, and html5ever records no source offsets to fall back on (it
334    /// reports only line numbers), so there is no original text to copy from
335    /// either.
336    fn effective_format(&self) -> SelectionFormat {
337        match self.format {
338            TextViewFormat::Markdown => self.selection_format,
339            TextViewFormat::Html => SelectionFormat::Plain,
340        }
341    }
342
343    /// Return the selected text, with `blocks` bounding which top-level blocks
344    /// the selection covers.
345    ///
346    /// The range comes from the selection endpoints, which know their block
347    /// even after it scrolls out of view; see
348    /// [`ParsedDocument::selected_text`](crate::text::document::ParsedDocument).
349    pub(super) fn selected_text_in(&self, blocks: Option<RangeInclusive<usize>>) -> String {
350        let format = self.effective_format();
351
352        if self.select_all {
353            if format == SelectionFormat::Source {
354                return self.source().to_string();
355            }
356
357            return self.parsed_content.document.text();
358        }
359
360        // A multi-click stores the plain text it selected, which is a shortcut
361        // past the block walk. Source mode cannot take it: the word it stored
362        // has lost its markup. The click also set the inline selection it came
363        // from, so the walk reconstructs the same range with the markup intact.
364        if format != SelectionFormat::Source
365            && let Some(text) = &self.selected_text_override
366        {
367            return text.clone();
368        }
369
370        self.parsed_content.document.selected_text(format, blocks)
371    }
372
373    /// Force a full re-measure of the block list after the document has been
374    /// replaced.
375    ///
376    /// `Document::render_root` only calls `ListState::reset` when the block
377    /// *count* changes, and the full-measure pass enabled by `measure_all` is
378    /// a one-shot latch that only `reset`, `remeasure_items`, or a width
379    /// change re-arms. Replacing a document with one that happens to have the
380    /// same number of blocks therefore leaves every cached height belonging to
381    /// the *previous* document. The list summary height stays wrong, and since
382    /// the wheel clamps against that summary, the blocks past the false bottom
383    /// can never scroll into view to be re-measured -- the clamp seals itself.
384    ///
385    /// `remeasure_items` re-arms the latch and marks the items unmeasured
386    /// while keeping their old sizes as hints, so the scrollbar does not
387    /// collapse in the frame before the next layout measures the real heights.
388    fn invalidate_measured_heights(&self) {
389        let count = self.list_state.item_count();
390        if count > 0 {
391            self.list_state.remeasure_items(0..count);
392        }
393    }
394
395    fn increment_update(&mut self, text: &str, append: bool, cx: &mut Context<Self>) {
396        self.revision += 1;
397        if !append {
398            self.selection_revision = self.selection_revision.wrapping_add(1);
399        }
400        let parse_synchronously = !append && text.len() <= MAX_SYNC_FULL_REPLACE_BYTES;
401        let update_options = UpdateOptions {
402            revision: self.revision,
403            append,
404            mode: if append {
405                ParseMode::Compatible
406            } else if parse_synchronously {
407                ParseMode::BaselineAck
408            } else {
409                ParseMode::Replace
410            },
411            pending_text: text.to_string(),
412            markdown_extensions: self.markdown_extensions.clone(),
413        };
414
415        // Keep small full replacements synchronous so their first layout has
416        // the exact content height. Larger replacements use the existing
417        // background parser, bounding synchronous parser input on the UI thread.
418        if parse_synchronously {
419            match parse_content(self.format, ParsedContent::default(), &update_options) {
420                Ok(content) => {
421                    self.parsed_content = content;
422                    self.parsed_error = None;
423                    self.invalidate_measured_heights();
424                    if !self.is_selecting {
425                        self.reset_selection_and_adapter(cx);
426                    }
427                }
428                Err(err) => {
429                    self.parsed_error = Some(err);
430                }
431            }
432            // Keep the background parser's accumulated document in sync so a
433            // later append extends this baseline instead of parsing the delta
434            // as a standalone document.
435            _ = self.tx.try_send(update_options);
436            cx.notify();
437            return;
438        }
439
440        _ = self.tx.try_send(update_options);
441    }
442
443    /// Save bounds and unselect if bounds changed.
444    pub(super) fn update_bounds(&mut self, bounds: Bounds<Pixels>, _cx: &mut App) {
445        self.bounds = bounds;
446    }
447
448    /// The index of the top-level block at `content_y`, in this view's content
449    /// coordinates (the same space the base selection endpoint stores its point in).
450    ///
451    /// Only laid-out blocks can be located, which is enough for a selection
452    /// endpoint: the user can only put one where they can see it. Returns
453    /// `None` for a view that is not virtualized, where every block paints and
454    /// the range is not needed.
455    pub(super) fn block_ix_at(&self, content_y: Pixels) -> Option<usize> {
456        if !self.scrollable {
457            return None;
458        }
459
460        let origin = self.bounds.origin.y + self.scroll_offset().y;
461        let count = self.list_state.item_count();
462        let mut ix = self.list_state.logical_scroll_top().item_ix;
463        while ix < count {
464            let bounds = self.list_state.bounds_for_item(ix)?;
465            if content_y < bounds.bottom() - origin {
466                return Some(ix);
467            }
468            ix += 1;
469        }
470
471        count.checked_sub(1)
472    }
473
474    #[doc(hidden)]
475    pub fn bounds(&self) -> Bounds<Pixels> {
476        self.bounds
477    }
478
479    #[doc(hidden)]
480    pub fn list_state(&self) -> &ListState {
481        &self.list_state
482    }
483
484    #[doc(hidden)]
485    pub fn is_selecting(&self) -> bool {
486        self.is_selecting
487    }
488
489    #[doc(hidden)]
490    pub fn focus_handle(&self) -> &FocusHandle {
491        &self.focus_handle
492    }
493
494    /// Whether this view has a view-local selection (select-all, multi-click, or override),
495    /// independent of the window-level selection.
496    pub(super) fn has_view_selection(&self) -> bool {
497        self.select_all
498            || self.multi_click_selection.is_some()
499            || self.selected_text_override.is_some()
500    }
501
502    pub(super) fn stop_auto_scroll(&mut self) {
503        self.auto_scroll.stop();
504    }
505
506    pub(super) fn reset_selection(&mut self) {
507        self.multi_click_selection = None;
508        self.selected_text_override = None;
509        self.select_all = false;
510        self.is_selecting = false;
511        self.auto_scroll.stop();
512        // Clear the inline selection state synchronously, so offscreen
513        // (virtualized) views that won't repaint don't leak stale selection
514        // text into a new cross-view copy.
515        self.parsed_content.document.clear_selection();
516    }
517
518    fn reset_selection_and_adapter(&mut self, cx: &mut App) {
519        self.reset_selection();
520        self.selection_adapter.set_local_selection(false, cx);
521    }
522
523    /// Clear the current text selection.
524    pub fn clear_selection(&mut self, cx: &mut Context<Self>) {
525        self.reset_selection_and_adapter(cx);
526        cx.notify();
527    }
528
529    pub(super) fn scroll_offset(&self) -> Point<Pixels> {
530        if self.scrollable {
531            self.list_state.scroll_px_offset_for_scrollbar()
532        } else {
533            Point::default()
534        }
535    }
536
537    /// Select all rendered text in this view.
538    pub fn select_all(&mut self, cx: &mut Context<Self>) {
539        self.multi_click_selection = None;
540        self.selected_text_override = None;
541        self.select_all = true;
542        self.is_selecting = false;
543        self.auto_scroll.stop();
544        self.selection_adapter.set_local_selection(true, cx);
545        cx.notify();
546    }
547
548    pub(crate) fn set_multi_click_selection(
549        &mut self,
550        pos: Point<Pixels>,
551        kind: TextViewMultiClickKind,
552        selected_text: String,
553        cx: &mut App,
554    ) {
555        let scroll_offset = self.scroll_offset();
556        let pos = pos - self.bounds.origin - scroll_offset;
557        self.multi_click_selection = Some(TextViewMultiClickSelection { pos, kind });
558        self.selected_text_override = Some(selected_text);
559        self.select_all = false;
560        self.is_selecting = false;
561        self.auto_scroll.stop();
562        self.selection_adapter.set_local_selection(true, cx);
563    }
564
565    pub(super) fn set_auto_scroll(&mut self, delta: Option<Pixels>, cx: &mut Context<Self>) {
566        self.auto_scroll.set(delta, cx, |delta, state, cx| {
567            state.list_state.scroll_by(delta);
568            cx.notify();
569        });
570    }
571
572    /// Return the window selection (anchor, cursor) in window coordinates if
573    /// this view participates in it.
574    ///
575    /// Single-view fast path: when both endpoints are anchored inside one
576    /// TextView, only that view participates (identical to the previous
577    /// per-view behavior).
578    pub(crate) fn selection_points(&self, cx: &App) -> Option<(Point<Pixels>, Point<Pixels>)> {
579        if !self.selectable {
580            return None;
581        }
582        self.selection_adapter.selection_points(cx)
583    }
584
585    pub(crate) fn has_selection(&self, cx: &App) -> bool {
586        self.has_view_selection() || self.selection_points(cx).is_some()
587    }
588
589    pub(super) fn on_action_select_all(
590        &mut self,
591        _: &SelectAll,
592        _: &mut Window,
593        cx: &mut Context<Self>,
594    ) {
595        if !self.selectable {
596            cx.propagate();
597            return;
598        }
599
600        self.select_all(cx);
601    }
602
603    pub(crate) fn is_selectable(&self) -> bool {
604        self.selectable
605    }
606
607    pub(crate) fn is_all_selected(&self) -> bool {
608        self.select_all
609    }
610
611    pub(crate) fn multi_click_selection(&self) -> Option<TextViewMultiClickSelection> {
612        let scroll_offset = self.scroll_offset();
613        self.multi_click_selection.map(|selection| {
614            let pos = selection.pos + scroll_offset + self.bounds.origin;
615            TextViewMultiClickSelection { pos, ..selection }
616        })
617    }
618}
619
620#[derive(Clone, Copy, Debug, PartialEq)]
621pub(crate) struct TextViewMultiClickSelection {
622    pub(crate) pos: Point<Pixels>,
623    pub(crate) kind: TextViewMultiClickKind,
624}
625
626#[derive(Clone, Copy, Debug, PartialEq, Eq)]
627pub(crate) enum TextViewMultiClickKind {
628    Word,
629    Paragraph,
630}
631
632impl Render for TextViewState {
633    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
634        let state = cx.entity();
635        let document = self.parsed_content.document.clone();
636        let mut node_cx = self.parsed_content.node_cx.clone();
637
638        node_cx.code_block_actions = self.code_block_actions.clone();
639        node_cx.code_block_highlighter = self.code_block_highlighter.clone();
640        node_cx.table_actions = self.table_actions.clone();
641        node_cx.link_click_handler = self.link_click_handler.clone();
642        node_cx.markdown_extensions = self.markdown_extensions.clone();
643        node_cx.style = self.text_view_style.clone();
644
645        v_flex()
646            .w_full()
647            // Clamped content must keep its natural height: stretching it to
648            // the capped box would hide the overflow the clamp has to measure.
649            .when(self.max_lines.is_none(), |this| this.h_full())
650            .map(|this| match &mut self.parsed_error {
651                None => this.child(document.render_root(
652                    if self.scrollable {
653                        Some(self.list_state.clone())
654                    } else {
655                        None
656                    },
657                    &node_cx,
658                    window,
659                    cx,
660                )),
661                Some(err) => this.child(
662                    v_flex()
663                        .gap_1()
664                        .child("Failed to parse content")
665                        .child(err.to_string()),
666                ),
667            })
668            .on_prepaint(move |bounds, window, cx| {
669                let (
670                    size_changed,
671                    selection_involves_view,
672                    has_selection_snapshot,
673                    is_selecting,
674                    compatible_layout_update,
675                ) = {
676                    let state = state.read(cx);
677                    (
678                        state.bounds().size != bounds.size,
679                        state.selection_adapter.is_part_of_window_selection(cx),
680                        state.selection_adapter.has_selection_snapshot(cx),
681                        state.is_selecting,
682                        state.compatible_layout_update,
683                    )
684                };
685                let mut revision_changed = false;
686                state.update(cx, |state, cx| {
687                    revision_changed = state
688                        .selection_adapter
689                        .update_layout_revision(state.selection_revision, state.is_selecting);
690                    state.update_bounds(bounds, cx);
691                    state.compatible_layout_update = false;
692                });
693                if !is_selecting
694                    && ((size_changed && selection_involves_view && !compatible_layout_update)
695                        || (revision_changed && has_selection_snapshot))
696                {
697                    TextSelection::clear(window, cx);
698                }
699            })
700    }
701}
702
703#[derive(Clone, PartialEq, Default)]
704pub(crate) struct ParsedContent {
705    pub(crate) document: ParsedDocument,
706    pub(crate) node_cx: node::NodeContext,
707}
708
709struct UpdateFuture {
710    format: TextViewFormat,
711    content: ParsedContent,
712    rx: Pin<Box<Receiver<UpdateOptions>>>,
713    tx_result: Sender<ParsedUpdate>,
714}
715
716impl UpdateFuture {
717    fn new(
718        format: TextViewFormat,
719        rx: Receiver<UpdateOptions>,
720        tx_result: Sender<ParsedUpdate>,
721    ) -> Self {
722        Self {
723            format,
724            content: Default::default(),
725            rx: Box::pin(rx),
726            tx_result,
727        }
728    }
729}
730
731impl Future for UpdateFuture {
732    type Output = ();
733
734    fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
735        loop {
736            match self.rx.as_mut().poll_next(cx) {
737                Poll::Ready(Some(mut options)) => {
738                    let hit_coalesce_budget =
739                        merge_pending_options(&mut options, self.rx.as_ref().get_ref());
740
741                    let res = parse_content(self.format, self.content.clone(), &options);
742                    if let Ok(content) = &res {
743                        self.content = content.clone();
744                    }
745                    _ = self.tx_result.try_send(ParsedUpdate {
746                        revision: options.revision,
747                        full_parse: !options.append,
748                        selection_compatible: options.mode == ParseMode::Compatible,
749                        baseline_ack: options.mode == ParseMode::BaselineAck,
750                        result: res,
751                    });
752                    if hit_coalesce_budget {
753                        cx.waker().wake_by_ref();
754                        return Poll::Pending;
755                    }
756                    continue;
757                }
758                Poll::Ready(None) => return Poll::Ready(()),
759                Poll::Pending => return Poll::Pending,
760            }
761        }
762    }
763}
764
765#[derive(Clone)]
766struct UpdateOptions {
767    revision: usize,
768    pending_text: String,
769    append: bool,
770    mode: ParseMode,
771    markdown_extensions: Arc<MarkdownExtensions>,
772}
773
774impl UpdateOptions {
775    fn merge(&mut self, next: UpdateOptions) {
776        if next.append {
777            self.pending_text.push_str(&next.pending_text);
778            self.revision = next.revision;
779            if self.mode != ParseMode::Replace {
780                self.mode = ParseMode::Compatible;
781            }
782        } else {
783            *self = next;
784        }
785    }
786}
787
788struct ParsedUpdate {
789    revision: usize,
790    full_parse: bool,
791    selection_compatible: bool,
792    baseline_ack: bool,
793    result: Result<ParsedContent, SharedString>,
794}
795
796#[derive(Clone, Copy, Debug, PartialEq, Eq)]
797enum ParseMode {
798    BaselineAck,
799    Replace,
800    Compatible,
801}
802
803fn merge_pending_options(options: &mut UpdateOptions, rx: &Receiver<UpdateOptions>) -> bool {
804    let mut update_count = 1;
805
806    while update_count < MAX_COALESCED_UPDATES_PER_PARSE {
807        match rx.try_recv() {
808            Ok(next_options) => {
809                options.merge(next_options);
810                update_count += 1;
811            }
812            Err(_) => return false,
813        }
814    }
815
816    true
817}
818
819fn parse_content(
820    format: TextViewFormat,
821    mut content: ParsedContent,
822    options: &UpdateOptions,
823) -> Result<ParsedContent, SharedString> {
824    let mut node_cx = NodeContext {
825        markdown_extensions: options.markdown_extensions.clone(),
826        ..NodeContext::default()
827    };
828
829    // Re-parse the last block together with the appended text, so a block the
830    // new text continues (an unclosed list, a fenced code block) is not split
831    // in two. A block without a span cannot be located in `source` — the HTML
832    // parser never records spans — so it is left in place and only the
833    // appended text is parsed, positioned at the end of the current source.
834    let last_span = options
835        .append
836        .then(|| {
837            content
838                .document
839                .blocks
840                .last()
841                .and_then(|block| block.span())
842        })
843        .flatten();
844
845    let mut source = String::new();
846    if let Some(span) = last_span {
847        Arc::make_mut(&mut content.document.blocks).pop();
848        node_cx.offset = span.start;
849        source.push_str(&content.document.source[span.start..]);
850        source.push_str(&options.pending_text);
851    } else {
852        if options.append {
853            node_cx.offset = content.document.source.len();
854        }
855        source.push_str(&options.pending_text);
856    }
857
858    let new_document = match format {
859        TextViewFormat::Markdown => format::markdown::parse(&source, &mut node_cx),
860        TextViewFormat::Html => format::html::parse(&source, &mut node_cx),
861    }?;
862
863    if options.append {
864        content.document.source =
865            format!("{}{}", content.document.source, options.pending_text).into();
866        Arc::make_mut(&mut content.document.blocks)
867            .extend(Arc::unwrap_or_clone(new_document.blocks));
868    } else {
869        content.document = new_document;
870    }
871
872    Ok(content)
873}
874
875#[cfg(test)]
876mod tests {
877    use super::*;
878    use crate::text::MarkdownNode;
879    use gpui::TestAppContext;
880
881    #[gpui::test]
882    fn small_full_replace_parses_before_background_executor_runs(cx: &mut TestAppContext) {
883        cx.update(crate::init);
884        let markdown = "# ready";
885        let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown(markdown, cx)));
886
887        state.read_with(cx, |state, _| {
888            assert_eq!(state.source().as_str(), markdown);
889            assert_eq!(state.parsed_content.document.blocks.len(), 1);
890        });
891    }
892
893    #[gpui::test]
894    fn large_markdown_and_html_full_replacements_wait_for_background_executor(
895        cx: &mut TestAppContext,
896    ) {
897        cx.update(crate::init);
898        let markdown = "# x\n\n".repeat(MAX_SYNC_FULL_REPLACE_BYTES / 5 + 1);
899        let html = format!("<p>{}</p>", "x".repeat(MAX_SYNC_FULL_REPLACE_BYTES + 1));
900        assert!(markdown.len() > MAX_SYNC_FULL_REPLACE_BYTES);
901        assert!(html.len() > MAX_SYNC_FULL_REPLACE_BYTES);
902
903        let (markdown_state, html_state) = cx.update(|cx| {
904            (
905                cx.new(|cx| TextViewState::markdown(&markdown, cx)),
906                cx.new(|cx| TextViewState::html(&html, cx)),
907            )
908        });
909
910        markdown_state.read_with(cx, |state, _| {
911            assert_eq!(state.text.as_str(), markdown.as_str());
912            assert!(state.source().as_str().is_empty());
913            assert!(state.parsed_content.document.blocks.is_empty());
914        });
915        html_state.read_with(cx, |state, _| {
916            assert_eq!(state.text.as_str(), html.as_str());
917            assert!(state.source().as_str().is_empty());
918            assert!(state.parsed_content.document.blocks.is_empty());
919        });
920
921        cx.run_until_parked();
922
923        markdown_state.read_with(cx, |state, _| {
924            assert_eq!(state.source().as_str(), markdown.as_str());
925            assert!(!state.parsed_content.document.blocks.is_empty());
926        });
927        html_state.read_with(cx, |state, _| {
928            assert_eq!(state.source().as_str(), html.as_str());
929            assert!(!state.parsed_content.document.blocks.is_empty());
930        });
931    }
932
933    #[gpui::test]
934    fn async_full_replace_then_push_str_preserves_complete_source(cx: &mut TestAppContext) {
935        cx.update(crate::init);
936        let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("old", cx)));
937        cx.run_until_parked();
938
939        let replacement = "x".repeat(MAX_SYNC_FULL_REPLACE_BYTES + 1);
940        let expected = format!("{replacement} tail");
941        state.update(cx, |state, cx| {
942            state.set_text(&replacement, cx);
943            state.push_str(" tail", cx);
944        });
945        cx.run_until_parked();
946
947        state.read_with(cx, |state, _| {
948            assert_eq!(state.text.as_str(), expected.as_str());
949            assert_eq!(state.source().as_str(), expected.as_str());
950        });
951    }
952
953    #[gpui::test]
954    fn html_push_str_keeps_earlier_blocks(cx: &mut TestAppContext) {
955        cx.update(crate::init);
956        let state = cx.update(|cx| cx.new(|cx| TextViewState::html("<p>first</p>", cx)));
957        cx.run_until_parked();
958
959        state.update(cx, |state, cx| {
960            state.push_str("<p>second</p>", cx);
961        });
962        cx.run_until_parked();
963
964        state.read_with(cx, |state, _| {
965            assert_eq!(state.source().as_str(), "<p>first</p><p>second</p>");
966            let text = state
967                .parsed_content
968                .document
969                .blocks
970                .iter()
971                .map(|block| block.text())
972                .collect::<String>();
973            assert!(text.contains("first"), "lost the first block: {text:?}");
974            assert!(text.contains("second"), "lost the appended block: {text:?}");
975        });
976    }
977
978    #[gpui::test]
979    fn set_text_then_push_str_appends_to_replaced_content(cx: &mut TestAppContext) {
980        cx.update(crate::init);
981        let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("old", cx)));
982        cx.run_until_parked();
983
984        state.update(cx, |state, cx| {
985            state.set_text("", cx);
986            state.push_str("new", cx);
987            state.push_str(" text", cx);
988        });
989        cx.run_until_parked();
990
991        state.read_with(cx, |state, _| {
992            assert_eq!(state.text.as_str(), "new text");
993            assert_eq!(state.source().as_str(), "new text");
994        });
995
996        state.update(cx, |state, cx| {
997            state.set_text("", cx);
998        });
999        cx.run_until_parked();
1000
1001        state.read_with(cx, |state, _| {
1002            assert_eq!(state.text.as_str(), "");
1003            assert_eq!(state.source().as_str(), "");
1004        });
1005    }
1006
1007    #[gpui::test]
1008    fn full_parse_coalesced_with_append_preserves_new_select_all(cx: &mut TestAppContext) {
1009        cx.update(crate::init);
1010        let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("old", cx)));
1011        cx.run_until_parked();
1012
1013        state.update(cx, |state, cx| {
1014            state.set_text("new", cx);
1015            state.push_str(" text", cx);
1016            state.select_all(cx);
1017        });
1018        cx.run_until_parked();
1019
1020        state.read_with(cx, |state, _| {
1021            assert!(state.select_all);
1022            assert_eq!(state.selected_text().trim(), "new text");
1023        });
1024    }
1025
1026    #[test]
1027    fn update_options_merge_keeps_latest_full_text() {
1028        let mut options = UpdateOptions {
1029            revision: 1,
1030            pending_text: "old".to_string(),
1031            append: true,
1032            mode: ParseMode::Compatible,
1033            markdown_extensions: Arc::default(),
1034        };
1035
1036        options.merge(UpdateOptions {
1037            revision: 2,
1038            pending_text: "new".to_string(),
1039            append: false,
1040            mode: ParseMode::BaselineAck,
1041            markdown_extensions: Arc::default(),
1042        });
1043        options.merge(UpdateOptions {
1044            revision: 3,
1045            pending_text: " text".to_string(),
1046            append: true,
1047            mode: ParseMode::Compatible,
1048            markdown_extensions: Arc::default(),
1049        });
1050
1051        assert_eq!(options.revision, 3);
1052        assert_eq!(options.pending_text, "new text");
1053        assert!(!options.append);
1054    }
1055
1056    #[test]
1057    fn append_merged_into_async_replace_remains_a_replacement() {
1058        let mut options = UpdateOptions {
1059            revision: 1,
1060            pending_text: "new".to_string(),
1061            append: false,
1062            mode: ParseMode::Replace,
1063            markdown_extensions: Arc::default(),
1064        };
1065
1066        options.merge(UpdateOptions {
1067            revision: 2,
1068            pending_text: " text".to_string(),
1069            append: true,
1070            mode: ParseMode::Compatible,
1071            markdown_extensions: Arc::default(),
1072        });
1073
1074        assert_eq!(options.revision, 2);
1075        assert_eq!(options.pending_text, "new text");
1076        assert!(!options.append);
1077        assert_eq!(options.mode, ParseMode::Replace);
1078    }
1079
1080    #[test]
1081    fn update_future_yields_before_coalescing_all_queued_updates() {
1082        let (tx, rx) = unbounded::<UpdateOptions>();
1083        let (tx_result, rx_result) = unbounded::<ParsedUpdate>();
1084        let total_updates = 128;
1085
1086        for revision in 1..=total_updates {
1087            tx.try_send(UpdateOptions {
1088                revision,
1089                pending_text: format!("{revision}\n"),
1090                append: revision != 1,
1091                mode: if revision == 1 {
1092                    ParseMode::BaselineAck
1093                } else {
1094                    ParseMode::Compatible
1095                },
1096                markdown_extensions: Arc::default(),
1097            })
1098            .unwrap();
1099        }
1100
1101        let mut future = Box::pin(UpdateFuture::new(TextViewFormat::Markdown, rx, tx_result));
1102        let waker = futures::task::noop_waker();
1103        let mut task_cx = std::task::Context::from_waker(&waker);
1104
1105        assert!(matches!(
1106            std::future::Future::poll(future.as_mut(), &mut task_cx),
1107            Poll::Pending
1108        ));
1109        let parsed_update = rx_result.try_recv().expect("parse result");
1110
1111        assert!(
1112            parsed_update.revision < total_updates,
1113            "single poll coalesced every queued update through revision {}",
1114            parsed_update.revision
1115        );
1116
1117        assert!(matches!(
1118            std::future::Future::poll(future.as_mut(), &mut task_cx),
1119            Poll::Pending
1120        ));
1121        let parsed_update = rx_result.try_recv().expect("next parse result");
1122        assert_eq!(parsed_update.revision, total_updates);
1123    }
1124
1125    #[gpui::test]
1126    fn select_all_returns_rendered_text(cx: &mut TestAppContext) {
1127        cx.update(crate::init);
1128        let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("**quick** value", cx)));
1129        cx.run_until_parked();
1130
1131        state.update(cx, |state, cx| {
1132            state.select_all(cx);
1133        });
1134
1135        state.read_with(cx, |state, _| {
1136            assert!(state.has_view_selection());
1137            assert_eq!(state.selected_text().trim(), "quick value");
1138        });
1139
1140        state.update(cx, |state, cx| {
1141            state.clear_selection(cx);
1142        });
1143
1144        state.read_with(cx, |state, _| {
1145            assert!(!state.has_view_selection());
1146            assert_eq!(state.selected_text(), "");
1147        });
1148    }
1149
1150    #[gpui::test]
1151    fn select_all_in_source_format_returns_source(cx: &mut TestAppContext) {
1152        cx.update(crate::init);
1153        let markdown = "**quick** value";
1154        let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown(markdown, cx)));
1155        cx.run_until_parked();
1156
1157        state.update(cx, |state, cx| state.select_all(cx));
1158
1159        // The default (plain) mode strips the markup.
1160        state.read_with(cx, |state, _| {
1161            assert_eq!(state.selected_text().trim(), "quick value");
1162        });
1163
1164        state.update(cx, |state, cx| {
1165            state.set_selection_format(SelectionFormat::Source, cx)
1166        });
1167
1168        // Source mode yields the whole source verbatim.
1169        state.read_with(cx, |state, _| {
1170            assert_eq!(state.selected_text().trim(), markdown);
1171        });
1172    }
1173
1174    #[gpui::test]
1175    fn set_markdown_extensions_reparses_existing_text(cx: &mut TestAppContext) {
1176        cx.update(crate::init);
1177        let state = cx.update(|cx| cx.new(|cx| TextViewState::markdown("$TSLA.US", cx)));
1178        cx.run_until_parked();
1179
1180        let extensions = MarkdownExtensions::default().block_parser(|node, cx| {
1181            let markdown::mdast::Node::Paragraph(paragraph) = node else {
1182                return None;
1183            };
1184            let [markdown::mdast::Node::Text(text)] = paragraph.children.as_slice() else {
1185                return None;
1186            };
1187            let symbol = text.value.strip_prefix('$')?.to_string();
1188            let node_text = format!("${symbol}");
1189
1190            Some(
1191                MarkdownNode::new("ticker", symbol)
1192                    .text(node_text)
1193                    .markdown(cx.node_source(node).unwrap_or_default()),
1194            )
1195        });
1196
1197        state.update(cx, |state, cx| {
1198            state.set_markdown_extensions(Arc::new(extensions), cx);
1199        });
1200        cx.run_until_parked();
1201
1202        state.read_with(cx, |state, _| {
1203            let node::BlockNode::Custom(node) = &state.parsed_content.document.blocks[0] else {
1204                panic!("expected custom markdown node");
1205            };
1206            assert_eq!(node.name(), "ticker");
1207            assert_eq!(node.data::<String>().map(String::as_str), Some("TSLA.US"));
1208        });
1209    }
1210}