Skip to main content

gpui_base/text/
range_highlight.rs

1//! Application-supplied highlights over the text a [`TextViewState`] renders,
2//! and scrolling one of its ranges into view.
3//!
4//! Ranges address the rendered text, the string plain copy produces: an
5//! application searches [`TextViewState::rendered_text`] and hands the ranges
6//! it found back. Each range is split into the text leaves it covers (a
7//! paragraph, a heading, a code block, a table cell), which paint it as a
8//! background behind their glyphs, so a highlight never changes layout. Text
9//! outside every leaf (the separators between blocks and cells, custom blocks,
10//! HTML blocks, inline objects) is left unpainted.
11//!
12//! [`TextViewState`]: super::TextViewState
13//! [`TextViewState::rendered_text`]: super::TextViewState::rendered_text
14
15#[cfg(not(target_family = "wasm"))]
16use std::time::Instant;
17use std::{
18    ops::Range,
19    sync::{Arc, Mutex, OnceLock},
20    time::Duration,
21};
22#[cfg(target_family = "wasm")]
23use web_time::Instant;
24
25use gpui::{Bounds, EntityId, Hsla, Pixels, SharedString};
26
27use super::{
28    document::ParsedDocument,
29    node::{BlockNode, Paragraph},
30    stream_fade::{TextLeaf, TextLeafKey, text_leaves},
31};
32
33/// A snapshot of the text a [`TextViewState`](super::TextViewState) renders,
34/// as of one parse of its content.
35///
36/// Offsets into it are UTF-8 byte offsets. It is the string plain copy
37/// produces: `hello **world**` renders as `hello world`, escapes are
38/// resolved, and heading markers and list markers are left out. Blocks end
39/// with a newline and table cells are joined with a space; those separators
40/// belong to no block, so no highlight paints them.
41///
42/// Two snapshots are equal when they come from the same view and the same
43/// parse. Comparing the current [`rendered_text`] with the one last searched
44/// tells an observer of the view whether its content changed, so setting
45/// highlights, which notifies the view too, does not start another search.
46/// The text itself is only built when it is first read, from the parsed
47/// document the snapshot holds on to, so drop a snapshot that is no longer
48/// needed rather than keeping it past many changes.
49///
50/// [`rendered_text`]: super::TextViewState::rendered_text
51#[derive(Clone)]
52pub struct RenderedText {
53    owner: EntityId,
54    revision: usize,
55    document: ParsedDocument,
56    index: Arc<OnceLock<RenderedIndex>>,
57}
58
59impl std::fmt::Debug for RenderedText {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        f.debug_struct("RenderedText")
62            .field("owner", &self.owner)
63            .field("revision", &self.revision)
64            .finish_non_exhaustive()
65    }
66}
67
68impl RenderedText {
69    /// The text of `document`, whose index `index` holds once built.
70    pub(super) fn new(
71        owner: EntityId,
72        revision: usize,
73        document: ParsedDocument,
74        index: Arc<OnceLock<RenderedIndex>>,
75    ) -> Self {
76        Self {
77            owner,
78            revision,
79            document,
80            index,
81        }
82    }
83
84    /// The rendered text.
85    pub fn as_str(&self) -> &str {
86        &self.index().text
87    }
88
89    /// The length of the rendered text, in bytes.
90    pub fn len(&self) -> usize {
91        self.index().text.len()
92    }
93
94    /// Whether the view renders no text.
95    pub fn is_empty(&self) -> bool {
96        self.index().text.is_empty()
97    }
98
99    pub(super) fn index(&self) -> &RenderedIndex {
100        self.index
101            .get_or_init(|| RenderedIndex::new(&self.document))
102    }
103}
104
105impl PartialEq for RenderedText {
106    fn eq(&self, other: &Self) -> bool {
107        self.owner == other.owner && self.revision == other.revision
108    }
109}
110
111impl Eq for RenderedText {}
112
113/// A background painted behind one range of a [`RenderedText`].
114///
115/// It is painted under the text and under the selection, and never changes
116/// layout. Where highlights overlap, the later one paints over the earlier.
117#[derive(Clone, Debug, PartialEq)]
118pub struct RangeHighlight {
119    range: Range<usize>,
120    background: Hsla,
121}
122
123impl RangeHighlight {
124    /// A highlight over `range`, in byte offsets of a [`RenderedText`].
125    pub fn new(range: Range<usize>, background: impl Into<Hsla>) -> Self {
126        Self {
127            range,
128            background: background.into(),
129        }
130    }
131
132    pub fn range(&self) -> Range<usize> {
133        self.range.clone()
134    }
135
136    pub fn background(&self) -> Hsla {
137        self.background
138    }
139}
140
141/// Why setting range highlights or revealing a range was rejected. Existing
142/// highlights and reveals stay unchanged.
143#[derive(Clone, Copy, Debug, Eq, PartialEq)]
144#[non_exhaustive]
145pub enum RangeHighlightError {
146    /// The view renders HTML, which records no source positions to address
147    /// its text by.
148    Unsupported,
149    /// The range at this index, the highlight's or the one revealed, is
150    /// reversed, out of bounds, or not on a character boundary.
151    InvalidRange(usize),
152}
153
154impl std::fmt::Display for RangeHighlightError {
155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        match self {
157            Self::Unsupported => f.write_str("HTML views do not support ranges of their text"),
158            Self::InvalidRange(ix) => write!(f, "range {ix} is not a range of the text"),
159        }
160    }
161}
162
163impl std::error::Error for RangeHighlightError {}
164
165/// The rendered text of one parsed document, and where each text leaf sits
166/// in it.
167#[derive(Debug, Default)]
168pub(super) struct RenderedIndex {
169    text: SharedString,
170    /// In document order, so by their position in `text`.
171    leaves: Vec<LeafSpan>,
172    /// Where the text of each top-level block sits, in document order.
173    blocks: Vec<Range<usize>>,
174}
175
176#[derive(Debug)]
177struct LeafSpan {
178    /// Where the leaf's text sits in the rendered text.
179    range: Range<usize>,
180    key: TextLeafKey,
181    /// Inline objects in the leaf's text, in leaf offsets. They paint as
182    /// objects rather than as text, so no highlight paints them.
183    objects: Vec<Range<usize>>,
184}
185
186impl LeafSpan {
187    /// `offset` in the leaf's text, moved out of an inline object onto the
188    /// text after it, or before it at the end of the leaf. `None` when the
189    /// leaf has no text outside its objects.
190    fn text_offset_near(&self, offset: usize) -> Option<usize> {
191        let object_at = |offset: usize| self.objects.iter().find(|object| object.contains(&offset));
192        let mut after = offset;
193        while let Some(object) = object_at(after) {
194            after = object.end;
195        }
196        if after < self.range.len() {
197            return Some(after);
198        }
199        let mut before = offset;
200        while let Some(object) = object_at(before) {
201            before = object.start.checked_sub(1)?;
202        }
203        Some(before)
204    }
205}
206
207impl RenderedIndex {
208    pub(super) fn new(document: &ParsedDocument) -> Self {
209        let mut builder = IndexBuilder::default();
210        let mut blocks = Vec::with_capacity(document.blocks.len());
211        for block in document.blocks.iter() {
212            let start = builder.text.len();
213            builder.push_block(block);
214            blocks.push(start..builder.text.len());
215        }
216        let index = Self {
217            text: builder.text.into(),
218            leaves: builder.leaves,
219            blocks,
220        };
221        debug_assert_eq!(index.text.as_ref(), document.text());
222        index
223    }
224
225    /// The leaf ranges `range` paints over, which are none when it covers no
226    /// leaf text, or `None` when it is not a range of the text.
227    fn resolve(&self, range: &Range<usize>) -> Option<Vec<(TextLeafKey, Range<usize>)>> {
228        if range.start > range.end
229            || range.end > self.text.len()
230            || !self.text.is_char_boundary(range.start)
231            || !self.text.is_char_boundary(range.end)
232        {
233            return None;
234        }
235
236        let first = self
237            .leaves
238            .partition_point(|leaf| leaf.range.end <= range.start);
239        let mut pieces = Vec::new();
240        for leaf in &self.leaves[first..] {
241            if leaf.range.start >= range.end {
242                break;
243            }
244            let end = range.end.min(leaf.range.end) - leaf.range.start;
245            let mut cursor = range.start.max(leaf.range.start) - leaf.range.start;
246            for object in &leaf.objects {
247                if object.start >= end {
248                    break;
249                }
250                if object.end <= cursor {
251                    continue;
252                }
253                if object.start > cursor {
254                    pieces.push((leaf.key, cursor..object.start));
255                }
256                cursor = object.end;
257            }
258            if cursor < end {
259                pieces.push((leaf.key, cursor..end));
260            }
261        }
262        Some(pieces)
263    }
264
265    /// Where `range` starts: the line of the first leaf text it covers, or,
266    /// when it covers none, as an empty range does, of the leaf text at its
267    /// start or last before it in its top-level block, or else that whole
268    /// block. `None` when it is not a range of the text, or there is none.
269    fn locate(&self, range: &Range<usize>) -> Option<RevealTarget> {
270        if let Some((key, leaf_range)) = self.resolve(range)?.into_iter().next() {
271            return Some(RevealTarget::Line {
272                key,
273                offset: leaf_range.start,
274            });
275        }
276        let block_ix = self
277            .blocks
278            .partition_point(|block| block.end <= range.start)
279            .min(self.blocks.len().checked_sub(1)?);
280        let block_start = self.blocks[block_ix].start;
281        let ix = self
282            .leaves
283            .partition_point(|leaf| leaf.range.end <= range.start);
284        let leaf_offset = match self.leaves.get(ix) {
285            Some(leaf) if leaf.range.contains(&range.start) => {
286                Some((leaf, range.start - leaf.range.start))
287            }
288            // A position after the text of a leaf, on the separators after
289            // it or at the end of the text, is on the line of the last
290            // character before it in its block.
291            _ => ix
292                .checked_sub(1)
293                .and_then(|ix| self.leaves.get(ix))
294                .filter(|leaf| leaf.range.start >= block_start)
295                .and_then(|leaf| {
296                    let (last, _) = self.text[leaf.range.clone()].char_indices().last()?;
297                    Some((leaf, last))
298                }),
299        };
300        if let Some((leaf, offset)) = leaf_offset
301            && let Some(offset) = leaf.text_offset_near(offset)
302        {
303            return Some(RevealTarget::Line {
304                key: leaf.key,
305                offset,
306            });
307        }
308        Some(RevealTarget::Block { ix: block_ix })
309    }
310}
311
312/// Builds the rendered text the way `BlockNode::text` does, recording each
313/// leaf as it goes.
314#[derive(Default)]
315struct IndexBuilder {
316    text: String,
317    leaves: Vec<LeafSpan>,
318}
319
320impl IndexBuilder {
321    fn push_block(&mut self, block: &BlockNode) {
322        let start = self.text.len();
323        match block {
324            BlockNode::Root { children, .. } | BlockNode::Blockquote { children, .. } => {
325                for child in children {
326                    self.push_block(child);
327                }
328            }
329            BlockNode::List { children, .. } | BlockNode::ListItem { children, .. } => {
330                for child in children {
331                    self.push_block(child);
332                }
333                return;
334            }
335            BlockNode::Paragraph(paragraph) => {
336                self.push_paragraph(
337                    paragraph,
338                    paragraph.span.map(|span| TextLeafKey::block(span.start)),
339                );
340            }
341            BlockNode::Heading { children, span, .. } => {
342                self.push_paragraph(children, span.map(|span| TextLeafKey::block(span.start)));
343            }
344            BlockNode::Table(table) => {
345                let mut ordinal = 0;
346                for row in table.children.iter().filter(|row| !row.children.is_empty()) {
347                    for (ix, cell) in row.children.iter().enumerate() {
348                        if ix > 0 {
349                            self.text.push(' ');
350                        }
351                        self.push_paragraph(
352                            &cell.children,
353                            table
354                                .span
355                                .map(|span| TextLeafKey::table_cell(span.start, ordinal)),
356                        );
357                        ordinal += 1;
358                    }
359                    self.text.push('\n');
360                }
361            }
362            BlockNode::CodeBlock(code_block) => {
363                self.push_leaf(
364                    &code_block.code(),
365                    code_block.span.map(|span| TextLeafKey::block(span.start)),
366                    Vec::new(),
367                );
368            }
369            BlockNode::Custom(node) => self.text.push_str(node.as_text()),
370            BlockNode::Definition { .. }
371            | BlockNode::Break { .. }
372            | BlockNode::HorizontalRule { .. }
373            | BlockNode::Unknown => {}
374        }
375        if self.text.len() > start {
376            self.text.push('\n');
377        }
378    }
379
380    fn push_paragraph(&mut self, paragraph: &Paragraph, key: Option<TextLeafKey>) {
381        let mut text = String::new();
382        let mut objects = Vec::new();
383        for child in &paragraph.children {
384            if child.custom.is_some() {
385                objects.push(text.len()..text.len() + child.text.len());
386            }
387            text.push_str(&child.text);
388        }
389        self.push_leaf(&text, key, objects);
390    }
391
392    fn push_leaf(&mut self, text: &str, key: Option<TextLeafKey>, objects: Vec<Range<usize>>) {
393        let start = self.text.len();
394        self.text.push_str(text);
395        if let Some(key) = key
396            && !text.is_empty()
397        {
398            self.leaves.push(LeafSpan {
399                range: start..self.text.len(),
400                key,
401                objects,
402            });
403        }
404    }
405}
406
407/// The last cell and source end of each table row. Collect them once so
408/// remapping cells neither rescans a table nor recomputes a row's end.
409fn table_row_source_ends(blocks: &[BlockNode], rows: &mut Vec<(TextLeafKey, Option<usize>)>) {
410    for block in blocks {
411        match block {
412            BlockNode::Table(table) => {
413                let Some(span) = table.span else {
414                    continue;
415                };
416                let mut cell_count = 0;
417                for row in &table.children {
418                    if row.children.is_empty() {
419                        continue;
420                    }
421                    cell_count += row.children.len();
422                    let end = row
423                        .children
424                        .iter()
425                        .filter_map(|cell| paragraph_source_end(&cell.children))
426                        .max();
427                    rows.push((TextLeafKey::table_cell(span.start, cell_count - 1), end));
428                }
429            }
430            BlockNode::Root { children, .. }
431            | BlockNode::Blockquote { children, .. }
432            | BlockNode::List { children, .. }
433            | BlockNode::ListItem { children, .. } => table_row_source_ends(children, rows),
434            _ => {}
435        }
436    }
437}
438
439/// Where the source of `paragraph`'s text ends, when the parser recorded it.
440fn paragraph_source_end(paragraph: &Paragraph) -> Option<usize> {
441    paragraph
442        .children
443        .iter()
444        .flat_map(|node| {
445            node.source_segments
446                .iter()
447                .map(|segment| segment.source.end)
448                .chain(
449                    node.custom
450                        .as_ref()
451                        .and_then(|custom| custom.source_range())
452                        .map(|range| range.end),
453                )
454        })
455        .max()
456}
457
458/// The highlights each leaf paints, resolved once when they change so
459/// rendering only looks up its leaf.
460#[derive(Debug, Default)]
461pub(crate) struct RangeHighlightFrame {
462    /// Sorted by key. A leaf's backgrounds keep the order the application
463    /// gave them in, so a later one paints over an earlier one.
464    leaves: Vec<(TextLeafKey, Vec<(Range<usize>, Hsla)>)>,
465}
466
467impl RangeHighlightFrame {
468    /// Validates `highlights` against `text` and resolves them to leaves.
469    pub(super) fn new(
470        text: &RenderedText,
471        highlights: impl IntoIterator<Item = RangeHighlight>,
472    ) -> Result<Option<Self>, RangeHighlightError> {
473        let mut pieces = Vec::new();
474        for (ix, highlight) in highlights.into_iter().enumerate() {
475            let leaf_ranges = text
476                .index()
477                .resolve(&highlight.range)
478                .ok_or(RangeHighlightError::InvalidRange(ix))?;
479            pieces.extend(
480                leaf_ranges
481                    .into_iter()
482                    .map(|(key, range)| (key, range, highlight.background)),
483            );
484        }
485
486        // Stable, so each leaf keeps the application's order.
487        pieces.sort_by_key(|(key, _, _)| *key);
488        let mut leaves: Vec<(TextLeafKey, Vec<(Range<usize>, Hsla)>)> = Vec::new();
489        for (key, range, background) in pieces {
490            match leaves.last_mut() {
491                Some((last, backgrounds)) if *last == key => backgrounds.push((range, background)),
492                _ => leaves.push((key, vec![(range, background)])),
493            }
494        }
495        Ok((!leaves.is_empty()).then_some(Self { leaves }))
496    }
497
498    /// The backgrounds of leaf `key`, in its rendered byte space.
499    pub(crate) fn backgrounds(&self, key: TextLeafKey) -> &[(Range<usize>, Hsla)] {
500        self.leaves
501            .binary_search_by_key(&key, |(leaf, _)| *leaf)
502            .map_or(&[], |ix| self.leaves[ix].1.as_slice())
503    }
504
505    /// The highlights that still describe `new`, the document `remap` maps
506    /// the old one to: each follows its leaf as far as the leaf's text is
507    /// unchanged, and is dropped with a leaf that is gone.
508    pub(super) fn remap(&self, remap: &LeafRemap) -> Option<Self> {
509        let mut leaves = self
510            .leaves
511            .iter()
512            .filter_map(|(key, backgrounds)| {
513                let (new_key, unchanged) = remap.leaf(*key)?;
514                let clipped = backgrounds
515                    .iter()
516                    .filter(|(range, _)| range.start < unchanged)
517                    .map(|(range, background)| (range.start..range.end.min(unchanged), *background))
518                    .collect::<Vec<_>>();
519                (!clipped.is_empty()).then_some((new_key, clipped))
520            })
521            .collect::<Vec<_>>();
522        // Moving keys keeps their order, but stay safe for the binary search.
523        leaves.sort_by_key(|(key, _)| *key);
524        (!leaves.is_empty()).then_some(Self { leaves })
525    }
526}
527
528/// Where the text leaves of one parsed document are found in the document
529/// parsed after it.
530///
531/// A block that starts before the first change of the source is found at the
532/// same offset, and one after the last change at an offset moved by the
533/// change in length; a block that starts between them is gone. A leaf keeps
534/// its text up to where it first differs from before.
535pub(super) struct LeafRemap<'a> {
536    old_len: usize,
537    new_len: usize,
538    /// With an append, where the block it parsed again starts: every leaf
539    /// before it is unchanged.
540    tail_start: Option<usize>,
541    unchanged_prefix: usize,
542    unchanged_suffix: usize,
543    old_leaves: Vec<(TextLeafKey, TextLeaf<'a>)>,
544    new_leaves: Vec<(TextLeafKey, TextLeaf<'a>)>,
545    /// Sorted by each row's last cell. Unneeded for append-only remapping.
546    table_rows: Vec<(TextLeafKey, Option<usize>)>,
547}
548
549impl<'a> LeafRemap<'a> {
550    /// With `tail_only`, `new` was parsed by appending to `old`, which parses
551    /// only the last block of `old` again and keeps the others as they were.
552    pub(super) fn new(old: &'a ParsedDocument, new: &'a ParsedDocument, tail_only: bool) -> Self {
553        let (old_len, new_len) = (old.source.len(), new.source.len());
554        // An append starts after the old source, and parses its last block
555        // again, or only the new text when that block has no span.
556        let tail_start = tail_only.then(|| {
557            old.blocks
558                .last()
559                .and_then(BlockNode::span)
560                .map_or(old_len, |span| span.start)
561        });
562        let (unchanged_prefix, unchanged_suffix) = if tail_only {
563            (old_len, 0)
564        } else {
565            let prefix = old
566                .source
567                .bytes()
568                .zip(new.source.bytes())
569                .take_while(|(old, new)| old == new)
570                .count();
571            let shorter = old_len.min(new_len);
572            if prefix == shorter {
573                // One source extends the other, as when text is appended.
574                (prefix, 0)
575            } else {
576                // Where the two overlap, as when a deleted block starts like
577                // the block after it, the end wins: the blocks after a change
578                // keep following their text rather than their offset.
579                let suffix = old
580                    .source
581                    .bytes()
582                    .rev()
583                    .zip(new.source.bytes().rev())
584                    .take_while(|(old, new)| old == new)
585                    .count()
586                    .min(shorter);
587                (prefix.min(shorter - suffix), suffix)
588            }
589        };
590
591        fn leaves_from(
592            document: &ParsedDocument,
593            tail_start: Option<usize>,
594        ) -> Vec<(TextLeafKey, TextLeaf<'_>)> {
595            let mut leaves = Vec::new();
596            for block in document.blocks.iter().rev() {
597                if let Some(tail_start) = tail_start
598                    && block.span().is_none_or(|span| span.start < tail_start)
599                {
600                    break;
601                }
602                text_leaves(block, &mut leaves);
603            }
604            leaves.sort_by_key(|(key, _)| *key);
605            leaves
606        }
607
608        let mut table_rows = Vec::new();
609        if !tail_only {
610            table_row_source_ends(&old.blocks, &mut table_rows);
611            table_rows.sort_by_key(|(key, _)| *key);
612        }
613
614        Self {
615            old_len,
616            new_len,
617            tail_start,
618            unchanged_prefix,
619            unchanged_suffix,
620            old_leaves: leaves_from(old, tail_start),
621            new_leaves: leaves_from(new, tail_start),
622            table_rows,
623        }
624    }
625
626    /// Where leaf `key` is in the new document, and how much of its text is
627    /// unchanged, or `None` when it is gone.
628    pub(super) fn leaf(&self, key: TextLeafKey) -> Option<(TextLeafKey, usize)> {
629        if self
630            .tail_start
631            .is_some_and(|tail_start| key.block_start() < tail_start)
632        {
633            return Some((key, usize::MAX));
634        }
635        let new_key = key.moved_to(self.moved(key.block_start())?);
636        let old_leaf = Self::find(&self.old_leaves, key)?;
637        // A table's cells are only known by their place in it, so after a
638        // change inside the table a cell is the same one only when the source
639        // of its whole row ends before that change.
640        if key.cell_ix().is_some()
641            && key.block_start() < self.unchanged_prefix
642            && self.tail_start.is_none()
643            && self
644                .row_source_end(key)
645                .is_none_or(|end| end > self.unchanged_prefix)
646        {
647            return None;
648        }
649        let new_leaf = Self::find(&self.new_leaves, new_key)?;
650        Some((new_key, new_leaf.common_prefix_len(old_leaf)))
651    }
652
653    fn row_source_end(&self, key: TextLeafKey) -> Option<usize> {
654        let ix = self.table_rows.partition_point(|(last, _)| *last < key);
655        let (last, end) = self.table_rows.get(ix)?;
656        if last.block_start() != key.block_start() {
657            return None;
658        }
659        *end
660    }
661
662    /// Where the block starting at `start` in the old document starts in the
663    /// new one.
664    fn moved(&self, start: usize) -> Option<usize> {
665        if start < self.unchanged_prefix {
666            Some(start)
667        } else if start >= self.old_len - self.unchanged_suffix {
668            Some(start + self.new_len - self.old_len)
669        } else {
670            None
671        }
672    }
673
674    fn find<'b>(
675        leaves: &'b [(TextLeafKey, TextLeaf<'a>)],
676        key: TextLeafKey,
677    ) -> Option<&'b TextLeaf<'a>> {
678        let ix = leaves.binary_search_by_key(&key, |(leaf, _)| *leaf).ok()?;
679        Some(&leaves[ix].1)
680    }
681}
682
683#[cfg(test)]
684mod tests {
685    use gpui::hsla;
686
687    use super::{LeafRemap, RangeHighlight, RangeHighlightFrame, TextLeafKey};
688    use crate::text::{document::ParsedDocument, format::markdown, node::NodeContext};
689
690    fn parse(source: &str) -> ParsedDocument {
691        markdown::parse(source, &mut NodeContext::default()).unwrap()
692    }
693
694    #[test]
695    fn table_row_index_keeps_empty_cells_in_their_row() {
696        let source = "| a | b |\n|---|---|\n| é |   |\n|   |   |\n| c | d |\n";
697        let document = parse(source);
698        let remap = LeafRemap::new(&document, &document, false);
699        assert_eq!(remap.table_rows.len(), 4);
700        let ends = [Some("b"), Some("é"), None, Some("d")];
701        for (row, text) in ends.into_iter().enumerate() {
702            let expected = text.map(|text| source.find(text).unwrap() + text.len());
703            for column in 0..2 {
704                let key = TextLeafKey::table_cell(0, row * 2 + column);
705                assert_eq!(remap.row_source_end(key), expected, "{key:?}");
706            }
707        }
708        assert_eq!(remap.row_source_end(TextLeafKey::table_cell(0, 8)), None);
709    }
710
711    #[test]
712    fn table_row_index_finds_nested_tables_without_crossing_between_them() {
713        let source = concat!(
714            "| a |\n|---|\n| b |\n\n",
715            "> | c |\n> |---|\n> | d |\n\n",
716            "- | e |\n  |---|\n  | f |\n",
717        );
718        let document = parse(source);
719        let remap = LeafRemap::new(&document, &document, false);
720        assert_eq!(remap.table_rows.len(), 6);
721        for (header, body) in [("a", "b"), ("c", "d"), ("e", "f")] {
722            let start = source.find(&format!("| {header} |")).unwrap();
723            for (cell, text) in [header, body].into_iter().enumerate() {
724                let key = TextLeafKey::table_cell(start, cell);
725                assert_eq!(
726                    remap.row_source_end(key),
727                    Some(source.find(text).unwrap() + text.len()),
728                );
729            }
730            assert_eq!(
731                remap.row_source_end(TextLeafKey::table_cell(start, 2)),
732                None
733            );
734            assert_eq!(
735                remap.row_source_end(TextLeafKey::table_cell(start + 1, 0)),
736                None
737            );
738        }
739    }
740
741    #[test]
742    fn long_and_wide_tables_remap_highlights_by_whole_rows() {
743        for (rows, columns) in [(4096, 1), (2, 1024), (64, 16)] {
744            let row = format!("|{}\n", " x |".repeat(columns));
745            let separator = format!("|{}\n", "---|".repeat(columns));
746            let source = format!("{row}{separator}{}", row.repeat(rows));
747            let old = parse(&source);
748            let mut changed = source.clone();
749            // Even unchanged cells earlier in the edited row must lose their
750            // highlights, as must the unchanged rows that follow it.
751            let edit = row.len() + separator.len() + row.rfind('x').unwrap();
752            changed.replace_range(edit..edit + 1, "y");
753            let new = parse(&changed);
754            let remap = LeafRemap::new(&old, &new, false);
755            assert_eq!(remap.table_rows.len(), rows + 1);
756            let frame = RangeHighlightFrame {
757                leaves: remap
758                    .old_leaves
759                    .iter()
760                    .map(|(key, _)| (*key, vec![(0..1, hsla(0.15, 1., 0.5, 0.4))]))
761                    .collect(),
762            };
763            assert_eq!(frame.leaves.len(), (rows + 1) * columns);
764            let kept = frame.remap(&remap).unwrap();
765            assert_eq!(kept.leaves.len(), columns);
766            for column in 0..columns {
767                assert_eq!(
768                    kept.backgrounds(TextLeafKey::table_cell(0, column)),
769                    &[(0..1, hsla(0.15, 1., 0.5, 0.4))],
770                );
771            }
772        }
773    }
774
775    #[test]
776    fn append_remapping_does_not_build_a_table_row_index() {
777        let source = "| a |\n|---|\n| b |\n\n| c |\n|---|\n| d |\n";
778        let old = parse(source);
779        let new = parse(&format!("{source}| e |\n"));
780        let remap = LeafRemap::new(&old, &new, true);
781        assert!(remap.table_rows.is_empty());
782        let first = TextLeafKey::table_cell(0, 0);
783        assert_eq!(remap.leaf(first), Some((first, usize::MAX)));
784        let last_start = source.find("| c |").unwrap();
785        for cell in 0..2 {
786            let key = TextLeafKey::table_cell(last_start, cell);
787            assert_eq!(remap.leaf(key), Some((key, 1)));
788        }
789    }
790
791    #[test]
792    fn a_position_in_an_inline_object_moves_onto_text() {
793        use super::{LeafSpan, TextLeafKey};
794        // "ab" then two objects of 2 bytes each, then "cd".
795        let leaf = |len: usize| LeafSpan {
796            range: 10..10 + len,
797            key: TextLeafKey::block(0),
798            objects: vec![2..4, 4..6],
799        };
800        assert_eq!(leaf(8).text_offset_near(1), Some(1));
801        // Onto the text after the objects.
802        assert_eq!(leaf(8).text_offset_near(3), Some(6));
803        assert_eq!(leaf(8).text_offset_near(5), Some(6));
804        // At the end of the leaf, onto the text before them.
805        assert_eq!(leaf(6).text_offset_near(5), Some(1));
806    }
807
808    #[test]
809    fn range_highlight_requires_a_background() {
810        let color = hsla(0.15, 1., 0.5, 0.4);
811        let highlight = RangeHighlight::new(2..5, color);
812        assert_eq!(highlight.range(), 2..5);
813        assert_eq!(highlight.background(), color);
814    }
815}
816
817/// Where a range to reveal starts.
818#[derive(Clone, Copy, Debug, PartialEq)]
819enum RevealTarget {
820    /// A line of a text leaf: the leaf, and the offset in its text.
821    Line { key: TextLeafKey, offset: usize },
822    /// A whole top-level block, for text that belongs to no leaf.
823    Block { ix: usize },
824}
825
826/// How long a reveal keeps trying. One that has not been carried out by
827/// then, e.g. because its view was not painted, is dropped rather than
828/// scrolling long after it was asked for.
829const REVEAL_TIMEOUT: Duration = Duration::from_secs(1);
830
831/// How many frames a reveal whose line was laid out but not visible keeps
832/// trying, e.g. while an enclosing container scrolls to it.
833const REVEAL_ATTEMPTS: usize = 8;
834
835/// Where the line a reveal starts on was laid out in one frame, in window
836/// coordinates, and whether it was inside the visible area.
837#[derive(Clone, Copy, Debug)]
838struct RevealReport {
839    line: Bounds<Pixels>,
840    visible: bool,
841}
842
843/// A range [`TextViewState::reveal_range`](super::TextViewState::reveal_range)
844/// is scrolling into view.
845///
846/// The `Inline` that lays out the start of the range asks the enclosing list
847/// to scroll its line into view during prepaint, and reports where the line
848/// ended up. The view reads the report once painted, after any list has
849/// scrolled, and is done once the line is visible.
850#[derive(Debug)]
851pub(super) struct PendingReveal {
852    target: RevealTarget,
853    requested_at: Instant,
854    /// What the target's `Inline` reported this frame; `None` when it was not
855    /// laid out.
856    report: Arc<Mutex<Option<RevealReport>>>,
857    attempts: usize,
858}
859
860/// How a pending reveal went in one frame.
861pub(super) enum RevealProgress {
862    /// The line is visible, so the reveal is done.
863    Shown,
864    /// The line was laid out at these window bounds without being visible.
865    Hidden(Bounds<Pixels>),
866    /// The line was not laid out.
867    NotLaidOut,
868}
869
870impl PendingReveal {
871    /// The start of `range` in `text`, asked for at `now`, or `None` when
872    /// the range is not a range of it.
873    pub(super) fn new(text: &RenderedText, range: &Range<usize>, now: Instant) -> Option<Self> {
874        Some(Self {
875            target: text.index().locate(range)?,
876            requested_at: now,
877            report: Arc::default(),
878            attempts: 0,
879        })
880    }
881
882    pub(super) fn is_expired(&self, now: Instant) -> bool {
883        now.saturating_duration_since(self.requested_at) > REVEAL_TIMEOUT
884            || self.attempts >= REVEAL_ATTEMPTS
885    }
886
887    /// Whether the reveal is of a whole block rather than a line.
888    pub(super) fn is_block(&self) -> bool {
889        matches!(self.target, RevealTarget::Block { .. })
890    }
891
892    /// The index of the top-level block of `document` the reveal starts in.
893    pub(super) fn block_ix(&self, document: &ParsedDocument) -> Option<usize> {
894        match self.target {
895            RevealTarget::Line { key, .. } => document.blocks.iter().rposition(|block| {
896                block
897                    .span()
898                    .is_some_and(|span| span.start <= key.block_start())
899            }),
900            RevealTarget::Block { ix } => (ix < document.blocks.len()).then_some(ix),
901        }
902    }
903
904    /// Whether the line was laid out in the previous frame.
905    pub(super) fn was_laid_out(&self) -> bool {
906        self.report.lock().is_ok_and(|report| report.is_some())
907    }
908
909    /// Starts a frame: forgets the previous report and hands the line to
910    /// rendering. A block has no line.
911    pub(super) fn request(&self) -> Option<RevealRequest> {
912        if let Ok(mut report) = self.report.lock() {
913            *report = None;
914        }
915        let RevealTarget::Line { key, offset } = self.target else {
916            return None;
917        };
918        Some(RevealRequest {
919            key,
920            offset,
921            report: self.report.clone(),
922        })
923    }
924
925    /// Ends a frame with what the line reported, counting a frame in which
926    /// it was laid out but hidden as an attempt.
927    pub(super) fn progress(&mut self) -> RevealProgress {
928        let report = self.report.lock().ok().and_then(|report| *report);
929        match report {
930            Some(report) if report.visible => RevealProgress::Shown,
931            Some(report) => {
932                self.attempts += 1;
933                RevealProgress::Hidden(report.line)
934            }
935            None => RevealProgress::NotLaidOut,
936        }
937    }
938
939    /// The reveal in the document `remap` maps the old one to, as long as
940    /// the text it starts at is unchanged.
941    pub(super) fn remap(mut self, remap: &LeafRemap) -> Option<Self> {
942        let RevealTarget::Line { key, offset } = self.target else {
943            return None;
944        };
945        let (key, unchanged) = remap.leaf(key)?;
946        (offset < unchanged).then_some(())?;
947        self.target = RevealTarget::Line { key, offset };
948        Some(self)
949    }
950}
951
952/// The start of a pending reveal, as rendering hands it to the `Inline`
953/// that lays that text out.
954#[derive(Clone, Debug)]
955pub(crate) struct RevealRequest {
956    key: TextLeafKey,
957    offset: usize,
958    report: Arc<Mutex<Option<RevealReport>>>,
959}
960
961impl RevealRequest {
962    /// The start of the reveal, when it is in `key`'s text between `start`
963    /// and `end`, rebased to `start`.
964    pub(crate) fn at(
965        &self,
966        key: Option<TextLeafKey>,
967        start: usize,
968        end: usize,
969    ) -> Option<RevealAt> {
970        if key != Some(self.key) {
971            return None;
972        }
973        RevealAt {
974            offset: self.offset,
975            report: self.report.clone(),
976        }
977        .rebase(start, end)
978    }
979}
980
981/// The start of a pending reveal, in the byte space of one run of text.
982#[derive(Clone, Debug)]
983pub(crate) struct RevealAt {
984    offset: usize,
985    report: Arc<Mutex<Option<RevealReport>>>,
986}
987
988impl RevealAt {
989    pub(crate) fn offset(&self) -> usize {
990        self.offset
991    }
992
993    /// The reveal in the text between `start` and `end`, rebased to `start`,
994    /// or `None` when it starts outside it.
995    pub(crate) fn rebase(&self, start: usize, end: usize) -> Option<Self> {
996        (start..end).contains(&self.offset).then(|| Self {
997            offset: self.offset - start,
998            report: self.report.clone(),
999        })
1000    }
1001
1002    /// The reveal moved into the text between `start` and `end` and rebased
1003    /// to `start`: one before it moves to its first character, one after it
1004    /// to its end.
1005    pub(crate) fn clamp(&self, start: usize, end: usize) -> Self {
1006        Self {
1007            offset: self.offset.clamp(start, end) - start,
1008            report: self.report.clone(),
1009        }
1010    }
1011
1012    /// Report where the line the reveal starts on was laid out, in window
1013    /// coordinates, and whether it was inside the visible area.
1014    pub(crate) fn report(&self, line: Bounds<Pixels>, visible: bool) {
1015        if let Ok(mut report) = self.report.lock() {
1016            *report = Some(RevealReport { line, visible });
1017        }
1018    }
1019}