1#[cfg(not(target_family = "wasm"))]
22use std::time::Instant;
23use std::{
24 ops::Range,
25 sync::{Arc, Mutex, OnceLock},
26 time::Duration,
27};
28#[cfg(target_family = "wasm")]
29use web_time::Instant;
30
31use gpui::{Bounds, EntityId, Hsla, Pixels, SharedString};
32
33use super::{
34 document::ParsedDocument,
35 node::{BlockNode, Paragraph, SourceSegment},
36 stream_fade::{TextLeaf, TextLeafKey, text_leaves},
37};
38
39#[derive(Clone)]
58pub struct RenderedText {
59 owner: EntityId,
60 revision: usize,
61 document: ParsedDocument,
62 index: Arc<OnceLock<RenderedIndex>>,
63}
64
65impl std::fmt::Debug for RenderedText {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 f.debug_struct("RenderedText")
68 .field("owner", &self.owner)
69 .field("revision", &self.revision)
70 .finish_non_exhaustive()
71 }
72}
73
74impl RenderedText {
75 pub(super) fn new(
77 owner: EntityId,
78 revision: usize,
79 document: ParsedDocument,
80 index: Arc<OnceLock<RenderedIndex>>,
81 ) -> Self {
82 Self {
83 owner,
84 revision,
85 document,
86 index,
87 }
88 }
89
90 pub fn as_str(&self) -> &str {
92 &self.index().text
93 }
94
95 pub fn len(&self) -> usize {
97 self.index().text.len()
98 }
99
100 pub fn is_empty(&self) -> bool {
102 self.index().text.is_empty()
103 }
104
105 pub fn source(&self) -> &str {
114 &self.document.source
115 }
116
117 pub fn range_for_source(&self, source: Range<usize>) -> Option<Range<usize>> {
149 let text = self.source();
150 if source.start >= source.end
151 || source.end > text.len()
152 || !text.is_char_boundary(source.start)
153 || !text.is_char_boundary(source.end)
154 {
155 return None;
156 }
157 self.index().range_for_source(&source)
158 }
159
160 pub(super) fn index(&self) -> &RenderedIndex {
161 self.index
162 .get_or_init(|| RenderedIndex::new(&self.document))
163 }
164}
165
166impl PartialEq for RenderedText {
167 fn eq(&self, other: &Self) -> bool {
168 self.owner == other.owner && self.revision == other.revision
169 }
170}
171
172impl Eq for RenderedText {}
173
174#[derive(Clone, Debug, PartialEq)]
179pub struct RangeHighlight {
180 range: Range<usize>,
181 background: Hsla,
182}
183
184impl RangeHighlight {
185 pub fn new(range: Range<usize>, background: impl Into<Hsla>) -> Self {
187 Self {
188 range,
189 background: background.into(),
190 }
191 }
192
193 pub fn range(&self) -> Range<usize> {
194 self.range.clone()
195 }
196
197 pub fn background(&self) -> Hsla {
198 self.background
199 }
200}
201
202#[derive(Clone, Copy, Debug, Eq, PartialEq)]
205#[non_exhaustive]
206pub enum RangeHighlightError {
207 Unsupported,
210 InvalidRange(usize),
213}
214
215impl std::fmt::Display for RangeHighlightError {
216 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217 match self {
218 Self::Unsupported => f.write_str("HTML views do not support ranges of their text"),
219 Self::InvalidRange(ix) => write!(f, "range {ix} is not a range of the text"),
220 }
221 }
222}
223
224impl std::error::Error for RangeHighlightError {}
225
226#[derive(Debug, Default)]
229pub(super) struct RenderedIndex {
230 text: SharedString,
231 leaves: Vec<LeafSpan>,
233 blocks: Vec<Range<usize>>,
235 source_map: Vec<SourceSegment>,
239}
240
241#[derive(Debug)]
242struct LeafSpan {
243 range: Range<usize>,
245 key: TextLeafKey,
246 objects: Vec<Range<usize>>,
249}
250
251impl LeafSpan {
252 fn text_offset_near(&self, offset: usize) -> Option<usize> {
256 let object_at = |offset: usize| self.objects.iter().find(|object| object.contains(&offset));
257 let mut after = offset;
258 while let Some(object) = object_at(after) {
259 after = object.end;
260 }
261 if after < self.range.len() {
262 return Some(after);
263 }
264 let mut before = offset;
265 while let Some(object) = object_at(before) {
266 before = object.start.checked_sub(1)?;
267 }
268 Some(before)
269 }
270}
271
272impl RenderedIndex {
273 pub(super) fn new(document: &ParsedDocument) -> Self {
274 let mut builder = IndexBuilder::default();
275 let mut blocks = Vec::with_capacity(document.blocks.len());
276 for block in document.blocks.iter() {
277 let start = builder.text.len();
278 builder.push_block(block);
279 blocks.push(start..builder.text.len());
280 }
281 let index = Self {
282 text: builder.text.into(),
283 leaves: builder.leaves,
284 blocks,
285 source_map: builder.source_map,
286 };
287 debug_assert_eq!(index.text.as_ref(), document.text());
288 index
289 }
290
291 fn resolve(&self, range: &Range<usize>) -> Option<Vec<(TextLeafKey, Range<usize>)>> {
294 if range.start > range.end
295 || range.end > self.text.len()
296 || !self.text.is_char_boundary(range.start)
297 || !self.text.is_char_boundary(range.end)
298 {
299 return None;
300 }
301
302 let first = self
303 .leaves
304 .partition_point(|leaf| leaf.range.end <= range.start);
305 let mut pieces = Vec::new();
306 for leaf in &self.leaves[first..] {
307 if leaf.range.start >= range.end {
308 break;
309 }
310 let end = range.end.min(leaf.range.end) - leaf.range.start;
311 let mut cursor = range.start.max(leaf.range.start) - leaf.range.start;
312 for object in &leaf.objects {
313 if object.start >= end {
314 break;
315 }
316 if object.end <= cursor {
317 continue;
318 }
319 if object.start > cursor {
320 pieces.push((leaf.key, cursor..object.start));
321 }
322 cursor = object.end;
323 }
324 if cursor < end {
325 pieces.push((leaf.key, cursor..end));
326 }
327 }
328 Some(pieces)
329 }
330
331 fn locate(&self, range: &Range<usize>) -> Option<RevealTarget> {
336 if let Some((key, leaf_range)) = self.resolve(range)?.into_iter().next() {
337 return Some(RevealTarget::Line {
338 key,
339 offset: leaf_range.start,
340 });
341 }
342 let block_ix = self
343 .blocks
344 .partition_point(|block| block.end <= range.start)
345 .min(self.blocks.len().checked_sub(1)?);
346 let block_start = self.blocks[block_ix].start;
347 let ix = self
348 .leaves
349 .partition_point(|leaf| leaf.range.end <= range.start);
350 let leaf_offset = match self.leaves.get(ix) {
351 Some(leaf) if leaf.range.contains(&range.start) => {
352 Some((leaf, range.start - leaf.range.start))
353 }
354 _ => ix
358 .checked_sub(1)
359 .and_then(|ix| self.leaves.get(ix))
360 .filter(|leaf| leaf.range.start >= block_start)
361 .and_then(|leaf| {
362 let (last, _) = self.text[leaf.range.clone()].char_indices().last()?;
363 Some((leaf, last))
364 }),
365 };
366 if let Some((leaf, offset)) = leaf_offset
367 && let Some(offset) = leaf.text_offset_near(offset)
368 {
369 return Some(RevealTarget::Line {
370 key: leaf.key,
371 offset,
372 });
373 }
374 Some(RevealTarget::Block { ix: block_ix })
375 }
376
377 fn range_for_source(&self, source: &Range<usize>) -> Option<Range<usize>> {
380 self.source_map
381 .iter()
382 .filter(|segment| {
383 segment.source.start < source.end && segment.source.end > source.start
384 })
385 .map(|segment| {
386 if !segment.linear {
387 return segment.rendered.clone();
388 }
389 let start = segment.source.start.max(source.start) - segment.source.start;
390 let end = segment.source.end.min(source.end) - segment.source.start;
391 self.text
392 .floor_char_boundary(segment.rendered.start + start)
393 ..self.text.ceil_char_boundary(segment.rendered.start + end)
394 })
395 .reduce(|found, range| found.start.min(range.start)..found.end.max(range.end))
396 }
397}
398
399#[derive(Default)]
402struct IndexBuilder {
403 text: String,
404 leaves: Vec<LeafSpan>,
405 source_map: Vec<SourceSegment>,
406}
407
408impl IndexBuilder {
409 fn push_block(&mut self, block: &BlockNode) {
410 let start = self.text.len();
411 match block {
412 BlockNode::Root { children, .. } | BlockNode::Blockquote { children, .. } => {
413 for child in children {
414 self.push_block(child);
415 }
416 }
417 BlockNode::List { children, .. } | BlockNode::ListItem { children, .. } => {
418 for child in children {
419 self.push_block(child);
420 }
421 return;
422 }
423 BlockNode::Paragraph(paragraph) => {
424 self.push_paragraph(
425 paragraph,
426 paragraph.span.map(|span| TextLeafKey::block(span.start)),
427 );
428 }
429 BlockNode::Heading { children, span, .. } => {
430 self.push_paragraph(children, span.map(|span| TextLeafKey::block(span.start)));
431 }
432 BlockNode::Table(table) => {
433 let mut ordinal = 0;
434 for row in table.children.iter().filter(|row| !row.children.is_empty()) {
435 for (ix, cell) in row.children.iter().enumerate() {
436 if ix > 0 {
437 self.text.push(' ');
438 }
439 self.push_paragraph(
440 &cell.children,
441 table
442 .span
443 .map(|span| TextLeafKey::table_cell(span.start, ordinal)),
444 );
445 ordinal += 1;
446 }
447 self.text.push('\n');
448 }
449 }
450 BlockNode::CodeBlock(code_block) => {
451 let code = code_block.code();
452 self.push_source_segments(self.text.len(), &code, &code_block.source_segments);
453 self.push_leaf(
454 &code,
455 code_block.span.map(|span| TextLeafKey::block(span.start)),
456 Vec::new(),
457 );
458 }
459 BlockNode::Custom(node) => {
460 self.push_object_source(self.text.len(), node.as_text(), node.source_range());
461 self.text.push_str(node.as_text());
462 }
463 BlockNode::Definition { .. }
464 | BlockNode::Break { .. }
465 | BlockNode::HorizontalRule { .. }
466 | BlockNode::Unknown => {}
467 }
468 if self.text.len() > start {
469 self.text.push('\n');
470 }
471 }
472
473 fn push_paragraph(&mut self, paragraph: &Paragraph, key: Option<TextLeafKey>) {
474 let start = self.text.len();
475 let mut text = String::new();
476 let mut objects = Vec::new();
477 for child in ¶graph.children {
478 let offset = start + text.len();
479 if let Some(custom) = &child.custom {
480 objects.push(text.len()..text.len() + child.text.len());
481 self.push_object_source(offset, &child.text, custom.source_range());
482 } else {
483 self.push_source_segments(offset, &child.text, &child.source_segments);
484 }
485 text.push_str(&child.text);
486 }
487 self.push_leaf(&text, key, objects);
488 }
489
490 fn push_source_segments(&mut self, offset: usize, text: &str, segments: &[SourceSegment]) {
495 self.source_map.extend(
496 segments
497 .iter()
498 .filter(|segment| {
499 !segment.rendered.is_empty()
500 && !segment.source.is_empty()
501 && segment.rendered.end <= text.len()
502 && text.is_char_boundary(segment.rendered.start)
503 && text.is_char_boundary(segment.rendered.end)
504 })
505 .map(|segment| SourceSegment {
506 rendered: offset + segment.rendered.start..offset + segment.rendered.end,
507 ..segment.clone()
508 }),
509 );
510 }
511
512 fn push_object_source(&mut self, offset: usize, text: &str, source: Option<Range<usize>>) {
515 if let Some(source) = source {
516 self.push_source_segments(
517 offset,
518 text,
519 &[SourceSegment {
520 rendered: 0..text.len(),
521 source,
522 linear: false,
523 }],
524 );
525 }
526 }
527
528 fn push_leaf(&mut self, text: &str, key: Option<TextLeafKey>, objects: Vec<Range<usize>>) {
529 let start = self.text.len();
530 self.text.push_str(text);
531 if let Some(key) = key
532 && !text.is_empty()
533 {
534 self.leaves.push(LeafSpan {
535 range: start..self.text.len(),
536 key,
537 objects,
538 });
539 }
540 }
541}
542
543fn table_row_source_ends(blocks: &[BlockNode], rows: &mut Vec<(TextLeafKey, Option<usize>)>) {
546 for block in blocks {
547 match block {
548 BlockNode::Table(table) => {
549 let Some(span) = table.span else {
550 continue;
551 };
552 let mut cell_count = 0;
553 for row in &table.children {
554 if row.children.is_empty() {
555 continue;
556 }
557 cell_count += row.children.len();
558 let end = row
559 .children
560 .iter()
561 .filter_map(|cell| paragraph_source_end(&cell.children))
562 .max();
563 rows.push((TextLeafKey::table_cell(span.start, cell_count - 1), end));
564 }
565 }
566 BlockNode::Root { children, .. }
567 | BlockNode::Blockquote { children, .. }
568 | BlockNode::List { children, .. }
569 | BlockNode::ListItem { children, .. } => table_row_source_ends(children, rows),
570 _ => {}
571 }
572 }
573}
574
575fn paragraph_source_end(paragraph: &Paragraph) -> Option<usize> {
577 paragraph
578 .children
579 .iter()
580 .flat_map(|node| {
581 node.source_segments
582 .iter()
583 .map(|segment| segment.source.end)
584 .chain(
585 node.custom
586 .as_ref()
587 .and_then(|custom| custom.source_range())
588 .map(|range| range.end),
589 )
590 })
591 .max()
592}
593
594#[derive(Debug, Default)]
597pub(crate) struct RangeHighlightFrame {
598 leaves: Vec<(TextLeafKey, Vec<(Range<usize>, Hsla)>)>,
601}
602
603impl RangeHighlightFrame {
604 pub(super) fn new(
606 text: &RenderedText,
607 highlights: impl IntoIterator<Item = RangeHighlight>,
608 ) -> Result<Option<Self>, RangeHighlightError> {
609 let mut pieces = Vec::new();
610 for (ix, highlight) in highlights.into_iter().enumerate() {
611 let leaf_ranges = text
612 .index()
613 .resolve(&highlight.range)
614 .ok_or(RangeHighlightError::InvalidRange(ix))?;
615 pieces.extend(
616 leaf_ranges
617 .into_iter()
618 .map(|(key, range)| (key, range, highlight.background)),
619 );
620 }
621
622 pieces.sort_by_key(|(key, _, _)| *key);
624 let mut leaves: Vec<(TextLeafKey, Vec<(Range<usize>, Hsla)>)> = Vec::new();
625 for (key, range, background) in pieces {
626 match leaves.last_mut() {
627 Some((last, backgrounds)) if *last == key => backgrounds.push((range, background)),
628 _ => leaves.push((key, vec![(range, background)])),
629 }
630 }
631 Ok((!leaves.is_empty()).then_some(Self { leaves }))
632 }
633
634 pub(crate) fn backgrounds(&self, key: TextLeafKey) -> &[(Range<usize>, Hsla)] {
636 self.leaves
637 .binary_search_by_key(&key, |(leaf, _)| *leaf)
638 .map_or(&[], |ix| self.leaves[ix].1.as_slice())
639 }
640
641 pub(super) fn remap(&self, remap: &LeafRemap) -> Option<Self> {
645 let mut leaves = self
646 .leaves
647 .iter()
648 .filter_map(|(key, backgrounds)| {
649 let (new_key, unchanged) = remap.leaf(*key)?;
650 let clipped = backgrounds
651 .iter()
652 .filter(|(range, _)| range.start < unchanged)
653 .map(|(range, background)| (range.start..range.end.min(unchanged), *background))
654 .collect::<Vec<_>>();
655 (!clipped.is_empty()).then_some((new_key, clipped))
656 })
657 .collect::<Vec<_>>();
658 leaves.sort_by_key(|(key, _)| *key);
660 (!leaves.is_empty()).then_some(Self { leaves })
661 }
662}
663
664pub(super) struct LeafRemap<'a> {
672 old_len: usize,
673 new_len: usize,
674 tail_start: Option<usize>,
677 unchanged_prefix: usize,
678 unchanged_suffix: usize,
679 old_leaves: Vec<(TextLeafKey, TextLeaf<'a>)>,
680 new_leaves: Vec<(TextLeafKey, TextLeaf<'a>)>,
681 table_rows: Vec<(TextLeafKey, Option<usize>)>,
683}
684
685impl<'a> LeafRemap<'a> {
686 pub(super) fn new(old: &'a ParsedDocument, new: &'a ParsedDocument, tail_only: bool) -> Self {
689 let (old_len, new_len) = (old.source.len(), new.source.len());
690 let tail_start = tail_only.then(|| {
693 old.blocks
694 .last()
695 .and_then(BlockNode::span)
696 .map_or(old_len, |span| span.start)
697 });
698 let (unchanged_prefix, unchanged_suffix) = if tail_only {
699 (old_len, 0)
700 } else {
701 let prefix = old
702 .source
703 .bytes()
704 .zip(new.source.bytes())
705 .take_while(|(old, new)| old == new)
706 .count();
707 let shorter = old_len.min(new_len);
708 if prefix == shorter {
709 (prefix, 0)
711 } else {
712 let suffix = old
716 .source
717 .bytes()
718 .rev()
719 .zip(new.source.bytes().rev())
720 .take_while(|(old, new)| old == new)
721 .count()
722 .min(shorter);
723 (prefix.min(shorter - suffix), suffix)
724 }
725 };
726
727 fn leaves_from(
728 document: &ParsedDocument,
729 tail_start: Option<usize>,
730 ) -> Vec<(TextLeafKey, TextLeaf<'_>)> {
731 let mut leaves = Vec::new();
732 for block in document.blocks.iter().rev() {
733 if let Some(tail_start) = tail_start
734 && block.span().is_none_or(|span| span.start < tail_start)
735 {
736 break;
737 }
738 text_leaves(block, &mut leaves);
739 }
740 leaves.sort_by_key(|(key, _)| *key);
741 leaves
742 }
743
744 let mut table_rows = Vec::new();
745 if !tail_only {
746 table_row_source_ends(&old.blocks, &mut table_rows);
747 table_rows.sort_by_key(|(key, _)| *key);
748 }
749
750 Self {
751 old_len,
752 new_len,
753 tail_start,
754 unchanged_prefix,
755 unchanged_suffix,
756 old_leaves: leaves_from(old, tail_start),
757 new_leaves: leaves_from(new, tail_start),
758 table_rows,
759 }
760 }
761
762 pub(super) fn leaf(&self, key: TextLeafKey) -> Option<(TextLeafKey, usize)> {
765 if self
766 .tail_start
767 .is_some_and(|tail_start| key.block_start() < tail_start)
768 {
769 return Some((key, usize::MAX));
770 }
771 let new_key = key.moved_to(self.moved(key.block_start())?);
772 let old_leaf = Self::find(&self.old_leaves, key)?;
773 if key.cell_ix().is_some()
777 && key.block_start() < self.unchanged_prefix
778 && self.tail_start.is_none()
779 && self
780 .row_source_end(key)
781 .is_none_or(|end| end > self.unchanged_prefix)
782 {
783 return None;
784 }
785 let new_leaf = Self::find(&self.new_leaves, new_key)?;
786 Some((new_key, new_leaf.common_prefix_len(old_leaf)))
787 }
788
789 fn row_source_end(&self, key: TextLeafKey) -> Option<usize> {
790 let ix = self.table_rows.partition_point(|(last, _)| *last < key);
791 let (last, end) = self.table_rows.get(ix)?;
792 if last.block_start() != key.block_start() {
793 return None;
794 }
795 *end
796 }
797
798 fn moved(&self, start: usize) -> Option<usize> {
801 if start < self.unchanged_prefix {
802 Some(start)
803 } else if start >= self.old_len - self.unchanged_suffix {
804 Some(start + self.new_len - self.old_len)
805 } else {
806 None
807 }
808 }
809
810 fn find<'b>(
811 leaves: &'b [(TextLeafKey, TextLeaf<'a>)],
812 key: TextLeafKey,
813 ) -> Option<&'b TextLeaf<'a>> {
814 let ix = leaves.binary_search_by_key(&key, |(leaf, _)| *leaf).ok()?;
815 Some(&leaves[ix].1)
816 }
817}
818
819#[cfg(test)]
820mod tests {
821 use std::{ops::Range, sync::Arc};
822
823 use gpui::{EntityId, hsla};
824
825 use super::{LeafRemap, RangeHighlight, RangeHighlightFrame, RenderedText, TextLeafKey};
826 use crate::text::{
827 document::ParsedDocument,
828 format,
829 node::{BlockNode, CodeBlock, NodeContext, Paragraph},
830 };
831
832 fn parse(markdown: &str) -> ParsedDocument {
833 format::markdown::parse(markdown, &mut NodeContext::default()).expect("parse Markdown")
834 }
835
836 #[test]
837 fn table_row_index_keeps_empty_cells_in_their_row() {
838 let source = "| a | b |\n|---|---|\n| é | |\n| | |\n| c | d |\n";
839 let document = parse(source);
840 let remap = LeafRemap::new(&document, &document, false);
841 assert_eq!(remap.table_rows.len(), 4);
842 let ends = [Some("b"), Some("é"), None, Some("d")];
843 for (row, text) in ends.into_iter().enumerate() {
844 let expected = text.map(|text| source.find(text).unwrap() + text.len());
845 for column in 0..2 {
846 let key = TextLeafKey::table_cell(0, row * 2 + column);
847 assert_eq!(remap.row_source_end(key), expected, "{key:?}");
848 }
849 }
850 assert_eq!(remap.row_source_end(TextLeafKey::table_cell(0, 8)), None);
851 }
852
853 #[test]
854 fn table_row_index_finds_nested_tables_without_crossing_between_them() {
855 let source = concat!(
856 "| a |\n|---|\n| b |\n\n",
857 "> | c |\n> |---|\n> | d |\n\n",
858 "- | e |\n |---|\n | f |\n",
859 );
860 let document = parse(source);
861 let remap = LeafRemap::new(&document, &document, false);
862 assert_eq!(remap.table_rows.len(), 6);
863 for (header, body) in [("a", "b"), ("c", "d"), ("e", "f")] {
864 let start = source.find(&format!("| {header} |")).unwrap();
865 for (cell, text) in [header, body].into_iter().enumerate() {
866 let key = TextLeafKey::table_cell(start, cell);
867 assert_eq!(
868 remap.row_source_end(key),
869 Some(source.find(text).unwrap() + text.len()),
870 );
871 }
872 assert_eq!(
873 remap.row_source_end(TextLeafKey::table_cell(start, 2)),
874 None
875 );
876 assert_eq!(
877 remap.row_source_end(TextLeafKey::table_cell(start + 1, 0)),
878 None
879 );
880 }
881 }
882
883 #[test]
884 fn long_and_wide_tables_remap_highlights_by_whole_rows() {
885 for (rows, columns) in [(4096, 1), (2, 1024), (64, 16)] {
886 let row = format!("|{}\n", " x |".repeat(columns));
887 let separator = format!("|{}\n", "---|".repeat(columns));
888 let source = format!("{row}{separator}{}", row.repeat(rows));
889 let old = parse(&source);
890 let mut changed = source.clone();
891 let edit = row.len() + separator.len() + row.rfind('x').unwrap();
894 changed.replace_range(edit..edit + 1, "y");
895 let new = parse(&changed);
896 let remap = LeafRemap::new(&old, &new, false);
897 assert_eq!(remap.table_rows.len(), rows + 1);
898 let frame = RangeHighlightFrame {
899 leaves: remap
900 .old_leaves
901 .iter()
902 .map(|(key, _)| (*key, vec![(0..1, hsla(0.15, 1., 0.5, 0.4))]))
903 .collect(),
904 };
905 assert_eq!(frame.leaves.len(), (rows + 1) * columns);
906 let kept = frame.remap(&remap).unwrap();
907 assert_eq!(kept.leaves.len(), columns);
908 for column in 0..columns {
909 assert_eq!(
910 kept.backgrounds(TextLeafKey::table_cell(0, column)),
911 &[(0..1, hsla(0.15, 1., 0.5, 0.4))],
912 );
913 }
914 }
915 }
916
917 #[test]
918 fn append_remapping_does_not_build_a_table_row_index() {
919 let source = "| a |\n|---|\n| b |\n\n| c |\n|---|\n| d |\n";
920 let old = parse(source);
921 let new = parse(&format!("{source}| e |\n"));
922 let remap = LeafRemap::new(&old, &new, true);
923 assert!(remap.table_rows.is_empty());
924 let first = TextLeafKey::table_cell(0, 0);
925 assert_eq!(remap.leaf(first), Some((first, usize::MAX)));
926 let last_start = source.find("| c |").unwrap();
927 for cell in 0..2 {
928 let key = TextLeafKey::table_cell(last_start, cell);
929 assert_eq!(remap.leaf(key), Some((key, 1)));
930 }
931 }
932
933 #[test]
934 fn a_position_in_an_inline_object_moves_onto_text() {
935 use super::{LeafSpan, TextLeafKey};
936 let leaf = |len: usize| LeafSpan {
938 range: 10..10 + len,
939 key: TextLeafKey::block(0),
940 objects: vec![2..4, 4..6],
941 };
942 assert_eq!(leaf(8).text_offset_near(1), Some(1));
943 assert_eq!(leaf(8).text_offset_near(3), Some(6));
945 assert_eq!(leaf(8).text_offset_near(5), Some(6));
946 assert_eq!(leaf(6).text_offset_near(5), Some(1));
948 }
949
950 #[test]
951 fn range_highlight_requires_a_background() {
952 let color = hsla(0.15, 1., 0.5, 0.4);
953 let highlight = RangeHighlight::new(2..5, color);
954 assert_eq!(highlight.range(), 2..5);
955 assert_eq!(highlight.background(), color);
956 }
957
958 fn rendered(document: ParsedDocument) -> RenderedText {
959 RenderedText::new(EntityId::from(1), 0, document, Arc::default())
960 }
961
962 fn nth(markdown: &str, needle: &str, nth: usize) -> Range<usize> {
965 let (start, _) = markdown
966 .match_indices(needle)
967 .nth(nth)
968 .unwrap_or_else(|| panic!("{needle:?} occurs {nth} times in {markdown:?}"));
969 start..start + needle.len()
970 }
971
972 fn converted(markdown: &str, source: Range<usize>) -> Option<String> {
974 let text = rendered(parse(markdown));
975 let range = text.range_for_source(source)?;
976 Some(text.as_str()[range].to_string())
977 }
978
979 fn converted_needle(markdown: &str, needle: &str) -> Option<String> {
981 converted(markdown, nth(markdown, needle, 0))
982 }
983
984 #[test]
985 fn source_rendering_nothing_converts_to_none() {
986 for (markdown, needle) in [
987 ("# Title", "# "),
988 ("hello **world**", "**"),
989 ("a ~~b~~ c", "~~"),
990 ("- item", "- "),
991 ("1. item", "1. "),
992 ("- [x] done", "[x] "),
993 ("> quote", "> "),
994 ("```rust\nlet x\n```", "```rust\n"),
995 ("```rust\nlet x\n```", "\n```"),
996 ("| a | b |\n|---|---|\n| c | d |", "|---|---|"),
997 ("| a | b |\n|---|---|\n| c | d |", " | "),
998 (
999 "see [docs](https://example.com) now",
1000 "(https://example.com)",
1001 ),
1002 ("a  b", ""),
1003 ("a\n\n---\n\nb", "---"),
1004 (
1005 "para\n\n[ref]: https://example.com",
1006 "[ref]: https://example.com",
1007 ),
1008 ] {
1009 assert_eq!(
1010 converted_needle(markdown, needle),
1011 None,
1012 "{needle:?} of {markdown:?}"
1013 );
1014 }
1015 }
1016
1017 #[test]
1018 fn delimiters_in_a_range_add_nothing() {
1019 let markdown = "hello **world** and `code`";
1020 assert_eq!(
1021 converted(markdown, 0..markdown.len()).as_deref(),
1022 Some("hello world and code")
1023 );
1024 assert_eq!(
1025 converted_needle(markdown, "**world**").as_deref(),
1026 Some("world")
1027 );
1028 assert_eq!(
1029 converted_needle(markdown, "o **wor").as_deref(),
1030 Some("o wor")
1031 );
1032 assert_eq!(
1033 converted_needle(markdown, "`code`").as_deref(),
1034 Some("code")
1035 );
1036 assert_eq!(
1037 converted_needle("# **Title**", "# **Title**").as_deref(),
1038 Some("Title")
1039 );
1040 assert_eq!(
1041 converted_needle("see [the docs](https://x.y) now", "[the docs](https://x.y)")
1042 .as_deref(),
1043 Some("the docs")
1044 );
1045 }
1046
1047 #[test]
1048 fn repeated_text_converts_to_the_occurrence_addressed() {
1049 let markdown = "foo **foo** foo";
1050 let text = rendered(parse(markdown));
1051 assert_eq!(text.as_str(), "foo foo foo\n");
1052 assert_eq!(text.range_for_source(nth(markdown, "foo", 0)), Some(0..3));
1053 assert_eq!(text.range_for_source(nth(markdown, "foo", 1)), Some(4..7));
1054 assert_eq!(text.range_for_source(nth(markdown, "foo", 2)), Some(8..11));
1055 }
1056
1057 #[test]
1058 fn fenced_code_backslashes_convert_individually() {
1059 let markdown = "```\na\\\\b\n```";
1060 let text = rendered(parse(markdown));
1061 assert_eq!(text.range_for_source(5..6), Some(1..2));
1062 assert_eq!(text.range_for_source(6..7), Some(2..3));
1063 assert_eq!(text.range_for_source(5..7), Some(1..3));
1064 }
1065
1066 #[test]
1067 fn literal_code_escapes_convert_individually() {
1068 for (markdown, first, second, rendered_start) in [
1069 ("`a\\\\b`", 2, 3, 1),
1070 (r"`a\*b`", 2, 3, 1),
1071 (" one\n a\\\\b", 13, 14, 5),
1072 ("- ```\n a\\\\b\n ```", 9, 10, 1),
1073 ("> ```\n> a\\\\b\n> ```", 9, 10, 1),
1074 ("```\na\\*b\n```", 5, 6, 1),
1075 ("```\na\\\\b\r\n```", 5, 6, 1),
1076 ("```\na\\\\\nb\n```", 5, 6, 1),
1077 ] {
1078 let text = rendered(parse(markdown));
1079 assert_eq!(
1080 text.range_for_source(first..first + 1),
1081 Some(rendered_start..rendered_start + 1),
1082 "{markdown:?}"
1083 );
1084 assert_eq!(
1085 text.range_for_source(second..second + 1),
1086 Some(rendered_start + 1..rendered_start + 2),
1087 "{markdown:?}"
1088 );
1089 }
1090 let text = rendered(parse(r"a\*b"));
1091 assert_eq!(text.range_for_source(1..2), Some(1..2));
1092 assert_eq!(text.range_for_source(2..3), Some(1..2));
1093 let text = rendered(parse("```\na\\\\\nb\n```"));
1094 assert_eq!(text.range_for_source(7..8), Some(3..4));
1095 assert_eq!(text.range_for_source(8..9), Some(4..5));
1096 }
1097
1098 #[test]
1099 fn a_character_converts_when_any_of_its_source_is_in_the_range() {
1100 assert_eq!(converted_needle("a & b", "amp").as_deref(), Some("&"));
1102 assert_eq!(converted_needle("a & b", "&").as_deref(), Some("&"));
1103 assert_eq!(
1104 converted_needle("AB", "B").as_deref(),
1105 Some("B")
1106 );
1107 assert_eq!(converted_needle(r"a \* b", r"\").as_deref(), Some("*"));
1109 assert_eq!(converted_needle(r"a \* b", "*").as_deref(), Some("*"));
1110 assert_eq!(converted_needle(r"\*abc", "b").as_deref(), Some("b"));
1113 assert_eq!(converted_needle(r"**\*abc**", "*a").as_deref(), Some("*a"));
1114 assert_eq!(converted_needle("soft\nbreak", "\n").as_deref(), Some(" "));
1116 assert_eq!(
1117 converted_needle("soft\r\nbreak", "\r\n").as_deref(),
1118 Some(" ")
1119 );
1120 assert_eq!(
1121 converted_needle("soft\r\nbreak", "\n").as_deref(),
1122 Some(" ")
1123 );
1124 }
1125
1126 #[test]
1127 fn a_decoded_entity_converts_only_as_a_whole() {
1128 let markdown = "a ∾̳ b";
1131 let text = rendered(parse(markdown));
1132 assert_eq!(text.as_str(), "a ∾̳ b\n");
1133 assert_eq!(converted_needle(markdown, "a").as_deref(), Some("a"));
1134 assert_eq!(converted_needle(markdown, "b").as_deref(), Some("b"));
1135 assert_eq!(converted_needle(markdown, "cE").as_deref(), Some("∾̳"));
1136 assert_eq!(converted_needle(markdown, "∾̳").as_deref(), Some("∾̳"));
1137 assert_eq!(converted_needle(markdown, "a &a").as_deref(), Some("a ∾̳"));
1138 }
1139
1140 #[test]
1141 fn multibyte_text_converts_on_character_boundaries() {
1142 let markdown = "中文 **粗体** 🎉 é";
1143 assert_eq!(converted_needle(markdown, "粗").as_deref(), Some("粗"));
1144 assert_eq!(
1145 converted_needle(markdown, "文 **粗").as_deref(),
1146 Some("文 粗")
1147 );
1148 assert_eq!(converted_needle(markdown, "🎉").as_deref(), Some("🎉"));
1149 assert_eq!(converted_needle(markdown, "é").as_deref(), Some("é"));
1150 let split = nth(markdown, "粗", 0);
1152 assert_eq!(converted(markdown, split.start..split.start + 1), None);
1153 assert_eq!(converted(markdown, split.start + 1..split.end), None);
1154 }
1155
1156 #[test]
1157 fn ranges_that_are_not_ranges_of_the_source_convert_to_none() {
1158 let text = rendered(parse("hello world"));
1159 assert_eq!(text.range_for_source(3..3), None);
1160 assert_eq!(text.range_for_source(Range { start: 5, end: 3 }), None);
1161 assert_eq!(text.range_for_source(0..12), None);
1162 assert_eq!(text.range_for_source(20..30), None);
1163 assert_eq!(text.range_for_source(0..11), Some(0..11));
1164
1165 let empty = rendered(parse(""));
1166 assert_eq!(empty.source(), "");
1167 assert_eq!(empty.range_for_source(0..0), None);
1168 }
1169
1170 #[test]
1171 fn a_range_across_blocks_holds_the_separators_between_them() {
1172 let markdown = "ab\n\ncd\n\n# ef";
1173 assert_eq!(
1175 converted_needle(markdown, "b\n\ncd\n\n# e").as_deref(),
1176 Some("b\ncd\ne")
1177 );
1178
1179 let table = "| a | b |\n|---|---|\n| c | d |";
1180 assert_eq!(
1181 converted_needle(table, "b |\n|---|---|\n| c").as_deref(),
1182 Some("b\nc")
1183 );
1184 assert_eq!(converted_needle(table, "c | d").as_deref(), Some("c d"));
1185
1186 let list = "- one\n- two\n - three";
1187 assert_eq!(
1188 converted_needle(list, "ne\n- two\n - th").as_deref(),
1189 Some("ne\ntwo\nth")
1190 );
1191 }
1192
1193 #[test]
1194 fn code_blocks_convert_their_body() {
1195 let fenced = "```rust\nlet x = 1;\nlet y = 2;\n```";
1196 assert_eq!(
1197 converted_needle(fenced, "x = 1;\nlet y").as_deref(),
1198 Some("x = 1;\nlet y")
1199 );
1200 assert_eq!(
1201 converted(fenced, 0..fenced.len()).as_deref(),
1202 Some("let x = 1;\nlet y = 2;")
1203 );
1204 let indented = " let x = 1;\n let y = 2;";
1205 assert_eq!(
1206 converted_needle(indented, "x = 1").as_deref(),
1207 Some("x = 1")
1208 );
1209 let tilde = "~~~\nwavy\n~~~";
1210 assert_eq!(converted(tilde, 0..tilde.len()).as_deref(), Some("wavy"));
1211 }
1212
1213 #[test]
1214 fn an_image_between_texts_keeps_the_range_contiguous() {
1215 let markdown = "a  b";
1216 let text = rendered(parse(markdown));
1217 let range = text.range_for_source(0..markdown.len()).unwrap();
1218 assert_eq!(&text.as_str()[range], "a b");
1219 }
1220
1221 #[test]
1222 fn html_text_converts_nothing() {
1223 let html = "<p>one <b>two</b></p>";
1224 let document = format::html::parse(html, &mut NodeContext::default()).expect("parse HTML");
1225 let text = rendered(document);
1226 assert_eq!(text.source(), html);
1227 assert!(!text.is_empty());
1228 for start in 0..html.len() {
1229 for end in start + 1..=html.len() {
1230 assert_eq!(text.range_for_source(start..end), None, "{start}..{end}");
1231 }
1232 }
1233 }
1234
1235 #[test]
1236 fn custom_blocks_convert_whole() {
1237 let extensions = crate::text::MarkdownExtensions::default().block_parser(|node, cx| {
1238 let markdown::mdast::Node::Paragraph(paragraph) = node else {
1239 return None;
1240 };
1241 let [markdown::mdast::Node::Text(text)] = paragraph.children.as_slice() else {
1242 return None;
1243 };
1244 text.value.starts_with('$').then(|| {
1245 crate::text::MarkdownNode::new("ticker", ())
1246 .text(text.value.clone())
1247 .markdown(cx.node_source(node).unwrap_or_default())
1248 })
1249 });
1250 let markdown = "before\n\n$TSLA.US\n\nafter";
1251 let mut cx = NodeContext {
1252 markdown_extensions: extensions.into(),
1253 ..NodeContext::default()
1254 };
1255 let text = rendered(format::markdown::parse(markdown, &mut cx).unwrap());
1256
1257 assert_eq!(text.as_str(), "before\n$TSLA.US\nafter\n");
1258 let ticker = nth(markdown, "$TSLA.US", 0);
1259 assert_eq!(text.range_for_source(ticker.clone()), Some(7..15));
1260 assert_eq!(
1262 text.range_for_source(ticker.start + 1..ticker.start + 3),
1263 Some(7..15)
1264 );
1265 assert_eq!(
1266 text.range_for_source(nth(markdown, "re\n\n$T", 0)),
1267 Some(4..15)
1268 );
1269 }
1270
1271 enum LeafNode<'a> {
1273 Paragraph(&'a Paragraph),
1274 Code(&'a CodeBlock),
1275 }
1276
1277 impl LeafNode<'_> {
1278 fn text(&self) -> String {
1279 match self {
1280 Self::Paragraph(paragraph) => paragraph
1281 .children
1282 .iter()
1283 .map(|child| child.text.as_ref())
1284 .collect(),
1285 Self::Code(code_block) => code_block.code().to_string(),
1286 }
1287 }
1288
1289 fn select(&self, range: Option<Range<usize>>) {
1295 match self {
1296 Self::Paragraph(paragraph) => {
1297 let text = self.text();
1298 let select_run = |state: &std::sync::Mutex<_>, run: Range<usize>| {
1299 let mut state: std::sync::MutexGuard<'_, crate::text::inline::InlineState> =
1300 state.lock().unwrap();
1301 state.set_text(text[run.clone()].to_string().into());
1302 state.selection = range
1303 .as_ref()
1304 .map(|range| range.start.max(run.start)..range.end.min(run.end))
1305 .filter(|selected| selected.start < selected.end)
1306 .map(|selected| {
1307 (selected.start - run.start..selected.end - run.start).into()
1308 });
1309 };
1310 let (mut run_start, mut offset) = (0, 0);
1311 for child in ¶graph.children {
1312 assert!(child.custom.is_none(), "inline objects select on their own");
1313 if child.image.is_some() {
1314 select_run(&child.state, run_start..offset);
1315 run_start = offset;
1316 }
1317 offset += child.text.len();
1318 }
1319 select_run(¶graph.state, run_start..offset);
1320 }
1321 Self::Code(code_block) => match range {
1322 Some(range) => code_block.set_selection(range),
1323 None => code_block.clear_selection(),
1324 },
1325 }
1326 }
1327 }
1328
1329 fn leaf_nodes<'a>(blocks: &'a [BlockNode], leaves: &mut Vec<LeafNode<'a>>) {
1331 for block in blocks {
1332 match block {
1333 BlockNode::Root { children, .. }
1334 | BlockNode::Blockquote { children, .. }
1335 | BlockNode::List { children, .. }
1336 | BlockNode::ListItem { children, .. } => leaf_nodes(children, leaves),
1337 BlockNode::Paragraph(paragraph) => leaves.push(LeafNode::Paragraph(paragraph)),
1338 BlockNode::Heading { children, .. } => leaves.push(LeafNode::Paragraph(children)),
1339 BlockNode::Table(table) => {
1340 for row in &table.children {
1341 for cell in &row.children {
1342 leaves.push(LeafNode::Paragraph(&cell.children));
1343 }
1344 }
1345 }
1346 BlockNode::CodeBlock(code_block) => leaves.push(LeafNode::Code(code_block)),
1347 _ => {}
1348 }
1349 }
1350 leaves.retain(|leaf| !leaf.text().is_empty());
1351 }
1352
1353 fn assert_selections_round_trip(markdown: &str) {
1360 let document = parse(markdown);
1361 let text = rendered(document.clone());
1362 for segment in text
1365 .index()
1366 .source_map
1367 .iter()
1368 .filter(|segment| !segment.linear)
1369 {
1370 let rendered = &text.as_str()[segment.rendered.clone()];
1371 let source = &markdown[segment.source.clone()];
1372 assert!(
1373 rendered.chars().count() == 1 || (source.starts_with('&') && source.ends_with(';')),
1374 "{markdown:?}: {rendered:?} maps from {source:?} only as a whole"
1375 );
1376 }
1377 let mut leaves = Vec::new();
1378 leaf_nodes(&document.blocks, &mut leaves);
1379 let spans = &text.index().leaves;
1380 assert_eq!(leaves.len(), spans.len(), "leaves of {markdown:?}");
1381 let leaves = leaves
1382 .into_iter()
1383 .zip(spans.iter().map(|span| span.range.clone()))
1384 .collect::<Vec<_>>();
1385 for (_, range) in &leaves {
1386 assert!(text.as_str().is_char_boundary(range.start));
1387 }
1388
1389 let boundaries = leaves
1391 .iter()
1392 .enumerate()
1393 .flat_map(|(ix, (_, range))| {
1394 let leaf_text = &text.as_str()[range.clone()];
1395 leaf_text
1396 .char_indices()
1397 .map(|(offset, _)| offset)
1398 .chain([leaf_text.len()])
1399 .map(move |offset| (ix, range.start + offset))
1400 })
1401 .collect::<Vec<_>>();
1402
1403 for (start_ix, &(first, start)) in boundaries.iter().enumerate() {
1404 if start == leaves[first].1.end {
1405 continue;
1406 }
1407 for &(last, end) in &boundaries[start_ix + 1..] {
1408 if end == leaves[last].1.start || end <= start {
1409 continue;
1410 }
1411 for (ix, (leaf, range)) in leaves.iter().enumerate() {
1412 let selected = (first..=last).contains(&ix).then(|| {
1413 start.max(range.start) - range.start..end.min(range.end) - range.start
1414 });
1415 leaf.select(selected);
1416 }
1417 let source = document.selected_source_range().unwrap_or_else(|| {
1418 panic!("{markdown:?}: selecting {start}..{end} maps to no source")
1419 });
1420 let expected = text
1423 .index()
1424 .source_map
1425 .iter()
1426 .filter(|segment| {
1427 !segment.linear
1428 && segment.rendered.start < end
1429 && segment.rendered.end > start
1430 })
1431 .fold(start..end, |range, segment| {
1432 range.start.min(segment.rendered.start)..range.end.max(segment.rendered.end)
1433 });
1434 assert_eq!(
1435 text.range_for_source(source.clone()),
1436 Some(expected),
1437 "{markdown:?}: selecting {:?} ({start}..{end}) reports source {:?} ({source:?})",
1438 &text.as_str()[start..end],
1439 &markdown[source.clone()],
1440 );
1441 }
1442 }
1443 for (leaf, _) in &leaves {
1444 leaf.select(None);
1445 }
1446 }
1447
1448 const ROUND_TRIP_CORPUS: &[&str] = &[
1449 "plain text",
1450 "hello **world** and *em* and ~~del~~ and `code`",
1451 "nested **bold *and italic* text** and ***both***",
1452 "`` code with ` backtick `` then text",
1453 "# Heading with **bold**",
1454 "Setext heading\n===",
1455 "> quoted **text**\n> continued\n>\n> second paragraph",
1456 "- item one\n- item **two**\n - nested *item*\n\n continued item",
1457 "3. third\n4. fourth",
1458 "- [x] done\n- [ ] todo",
1459 "| a | **b** |\n|---|:---:|\n| c `d` | e |\n| | f |",
1460 "```rust\nlet x = 1;\n\nlet y = 2;\n```",
1461 " indented code\n more",
1462 "~~~\ntilde fence\n~~~",
1463 "a [link](https://example.com \"title\") b",
1464 "a [reference] b\n\n[reference]: https://example.com",
1465 "<https://auto.example> and https://gfm.example",
1466 "soft\nbreak\nlines",
1467 "hard \nbreak\\\nagain",
1468 "trailing spaces \nnext line",
1469 "escapes \\* \\_ \\` \\\\ and \\[not a link\\]",
1470 "\\*starts escaped and **\\*bold** and *\\_em*",
1471 "entity ∾̳ as long as its characters",
1472 "entities & < A B © end",
1473 "中文 **粗体** 和 `代码` 🎉 é and e\u{301}",
1474 "crlf\r\nlines\r\n\r\nnext paragraph",
1475 "image  between",
1476 "one\n\ntwo\n\n---\n\nthree",
1477 "# Title\n\nIntro with **bold**.\n\n- first\n- second\n\n| a | b |\n|---|---|\n| c | d |\n\n```\ncode\n```\n\n> quote",
1478 ];
1479
1480 #[test]
1481 fn selections_round_trip_through_source_ranges() {
1482 for markdown in ROUND_TRIP_CORPUS {
1483 assert_selections_round_trip(markdown);
1484 }
1485 }
1486}
1487
1488#[derive(Clone, Copy, Debug, PartialEq)]
1490enum RevealTarget {
1491 Line { key: TextLeafKey, offset: usize },
1493 Block { ix: usize },
1495}
1496
1497const REVEAL_TIMEOUT: Duration = Duration::from_secs(1);
1501
1502const REVEAL_ATTEMPTS: usize = 8;
1505
1506#[derive(Clone, Copy, Debug)]
1509struct RevealReport {
1510 line: Bounds<Pixels>,
1511 visible: bool,
1512}
1513
1514#[derive(Debug)]
1522pub(super) struct PendingReveal {
1523 target: RevealTarget,
1524 requested_at: Instant,
1525 report: Arc<Mutex<Option<RevealReport>>>,
1528 attempts: usize,
1529}
1530
1531pub(super) enum RevealProgress {
1533 Shown,
1535 Hidden(Bounds<Pixels>),
1537 NotLaidOut,
1539}
1540
1541impl PendingReveal {
1542 pub(super) fn new(text: &RenderedText, range: &Range<usize>, now: Instant) -> Option<Self> {
1545 Some(Self {
1546 target: text.index().locate(range)?,
1547 requested_at: now,
1548 report: Arc::default(),
1549 attempts: 0,
1550 })
1551 }
1552
1553 pub(super) fn is_expired(&self, now: Instant) -> bool {
1554 now.saturating_duration_since(self.requested_at) > REVEAL_TIMEOUT
1555 || self.attempts >= REVEAL_ATTEMPTS
1556 }
1557
1558 pub(super) fn is_block(&self) -> bool {
1560 matches!(self.target, RevealTarget::Block { .. })
1561 }
1562
1563 pub(super) fn block_ix(&self, document: &ParsedDocument) -> Option<usize> {
1565 match self.target {
1566 RevealTarget::Line { key, .. } => document.blocks.iter().rposition(|block| {
1567 block
1568 .span()
1569 .is_some_and(|span| span.start <= key.block_start())
1570 }),
1571 RevealTarget::Block { ix } => (ix < document.blocks.len()).then_some(ix),
1572 }
1573 }
1574
1575 pub(super) fn was_laid_out(&self) -> bool {
1577 self.report.lock().is_ok_and(|report| report.is_some())
1578 }
1579
1580 pub(super) fn request(&self) -> Option<RevealRequest> {
1583 if let Ok(mut report) = self.report.lock() {
1584 *report = None;
1585 }
1586 let RevealTarget::Line { key, offset } = self.target else {
1587 return None;
1588 };
1589 Some(RevealRequest {
1590 key,
1591 offset,
1592 report: self.report.clone(),
1593 })
1594 }
1595
1596 pub(super) fn progress(&mut self) -> RevealProgress {
1599 let report = self.report.lock().ok().and_then(|report| *report);
1600 match report {
1601 Some(report) if report.visible => RevealProgress::Shown,
1602 Some(report) => {
1603 self.attempts += 1;
1604 RevealProgress::Hidden(report.line)
1605 }
1606 None => RevealProgress::NotLaidOut,
1607 }
1608 }
1609
1610 pub(super) fn remap(mut self, remap: &LeafRemap) -> Option<Self> {
1613 let RevealTarget::Line { key, offset } = self.target else {
1614 return None;
1615 };
1616 let (key, unchanged) = remap.leaf(key)?;
1617 (offset < unchanged).then_some(())?;
1618 self.target = RevealTarget::Line { key, offset };
1619 Some(self)
1620 }
1621}
1622
1623#[derive(Clone, Debug)]
1626pub(crate) struct RevealRequest {
1627 key: TextLeafKey,
1628 offset: usize,
1629 report: Arc<Mutex<Option<RevealReport>>>,
1630}
1631
1632impl RevealRequest {
1633 pub(crate) fn at(
1636 &self,
1637 key: Option<TextLeafKey>,
1638 start: usize,
1639 end: usize,
1640 ) -> Option<RevealAt> {
1641 if key != Some(self.key) {
1642 return None;
1643 }
1644 RevealAt {
1645 offset: self.offset,
1646 report: self.report.clone(),
1647 }
1648 .rebase(start, end)
1649 }
1650}
1651
1652#[derive(Clone, Debug)]
1654pub(crate) struct RevealAt {
1655 offset: usize,
1656 report: Arc<Mutex<Option<RevealReport>>>,
1657}
1658
1659impl RevealAt {
1660 pub(crate) fn offset(&self) -> usize {
1661 self.offset
1662 }
1663
1664 pub(crate) fn rebase(&self, start: usize, end: usize) -> Option<Self> {
1667 (start..end).contains(&self.offset).then(|| Self {
1668 offset: self.offset - start,
1669 report: self.report.clone(),
1670 })
1671 }
1672
1673 pub(crate) fn clamp(&self, start: usize, end: usize) -> Self {
1677 Self {
1678 offset: self.offset.clamp(start, end) - start,
1679 report: self.report.clone(),
1680 }
1681 }
1682
1683 pub(crate) fn report(&self, line: Bounds<Pixels>, visible: bool) {
1686 if let Ok(mut report) = self.report.lock() {
1687 *report = Some(RevealReport { line, visible });
1688 }
1689 }
1690}