1use std::{cell::RefCell, collections::HashMap, ops::Range, rc::Rc, sync::Arc};
27
28use gpui::{
29 AnyElement, App, Bounds, ClickEvent, ElementId, HighlightStyle, Hsla, ImageSource,
30 InteractiveElement as _, IntoElement as _, ParentElement as _, Pixels, Point, SharedString,
31 SharedUri, TextLayout, Window, WrappedLineLayout, div, point, px,
32};
33use unicode_segmentation::UnicodeSegmentation as _;
34
35use super::{
36 TextViewDefaults, TextViewStyle,
37 document::{NodeRenderOptions, PrevBlock, flow_position},
38 editor_seam::{self, EditorSink},
39 format::markdown,
40 markdown_ext::MarkdownExtensions,
41 node::NodeContext,
42};
43
44pub use super::node::{
45 BlockNode, BlockState, CodeBlock, ColumnumnAlign as ColumnAlign, ImageNode, InlineNode,
46 LinkMark, Paragraph, Span, Table, TextMark,
47};
48
49pub fn parse_markdown(
52 source: &str,
53 extensions: Arc<MarkdownExtensions>,
54) -> Result<Vec<BlockNode>, SharedString> {
55 let mut cx = NodeContext {
56 markdown_extensions: extensions,
57 ..Default::default()
58 };
59 let parsed = markdown::parse(source, &mut cx)?;
60 Ok(Arc::unwrap_or_clone(parsed.blocks))
61}
62
63#[derive(Clone)]
66pub struct RenderContext {
67 node: NodeContext,
68}
69
70impl RenderContext {
71 pub fn new(style: TextViewStyle) -> Self {
75 Self {
76 node: NodeContext {
77 style: Arc::new(style),
78 ..Default::default()
79 },
80 }
81 }
82
83 pub fn with_code_block_highlighter<F>(mut self, highlighter: F) -> Self
85 where
86 F: Fn(&CodeBlock) -> Vec<(Range<usize>, HighlightStyle)> + Send + Sync + 'static,
87 {
88 self.node.code_block_highlighter = Some(Arc::new(highlighter));
89 self
90 }
91
92 pub fn with_image_source<F>(mut self, resolver: F) -> Self
94 where
95 F: Fn(&SharedUri) -> ImageSource + Send + Sync + 'static,
96 {
97 self.node.image_source = Some(Arc::new(resolver));
98 self
99 }
100
101 pub fn on_link_click<F>(mut self, handler: F) -> Self
103 where
104 F: Fn(&SharedString, &ClickEvent, &mut Window, &mut App) + Send + Sync + 'static,
105 {
106 self.node.link_click_handler = Some(Arc::new(handler));
107 self
108 }
109
110 pub fn with_link_reference(
113 mut self,
114 identifier: impl Into<SharedString>,
115 link: LinkMark,
116 ) -> Self {
117 self.node.link_refs.insert(identifier.into(), link);
118 self
119 }
120
121 pub fn from_app(cx: &App) -> Self {
124 let theme = crate::Theme::global(cx);
125 let defaults = cx.try_global::<TextViewDefaults>();
126 let style = defaults
127 .and_then(|defaults| defaults.style().cloned())
128 .unwrap_or_else(|| TextViewStyle::from_theme(&theme));
129 Self {
130 node: NodeContext {
131 style: Arc::new(style),
132 code_block_highlighter: defaults.and_then(|d| d.code_block_highlighter()),
133 ..Default::default()
134 },
135 }
136 }
137
138 pub fn with_style(mut self, style: TextViewStyle) -> Self {
140 self.node.style = Arc::new(style);
141 self
142 }
143
144 pub fn with_foreground_ranges(mut self, key: u64, ranges: Vec<(Range<usize>, Hsla)>) -> Self {
159 let foregrounds = self.node.foregrounds.get_or_insert_with(Default::default);
160 Arc::make_mut(foregrounds).insert(key, ranges);
161 self
162 }
163
164 pub fn with_markdown_extensions(mut self, extensions: Arc<MarkdownExtensions>) -> Self {
166 self.node.markdown_extensions = extensions;
167 self
168 }
169
170 pub fn with_body_font_size(mut self, size: Pixels) -> Self {
173 self.node.body_font_size = size;
174 self
175 }
176
177 pub fn style(&self) -> &TextViewStyle {
179 &self.node.style
180 }
181}
182
183#[derive(Clone, Copy, Debug)]
187pub struct FlowPosition {
188 is_first: bool,
189 is_last: bool,
190 prev: PrevBlock,
191}
192
193impl FlowPosition {
194 pub fn at(blocks: &[&BlockNode], ix: usize, is_last: bool) -> Self {
198 let (is_first, prev) = flow_position(blocks, ix);
199 Self {
200 is_first,
201 is_last,
202 prev,
203 }
204 }
205
206 pub fn first(mut self, is_first: bool) -> Self {
209 self.is_first = is_first;
210 self
211 }
212
213 pub fn last(mut self, is_last: bool) -> Self {
215 self.is_last = is_last;
216 self
217 }
218}
219
220impl BlockNode {
221 pub fn render_with_id(
225 &self,
226 id: impl Into<ElementId>,
227 flow: FlowPosition,
228 render_context: &RenderContext,
229 window: &mut Window,
230 cx: &mut App,
231 ) -> AnyElement {
232 div()
233 .id(id)
234 .child(self.render(0, flow, render_context, window, cx))
235 .into_any_element()
236 }
237
238 pub fn render(
241 &self,
242 ix: usize,
243 flow: FlowPosition,
244 cx: &RenderContext,
245 window: &mut Window,
246 app: &mut App,
247 ) -> AnyElement {
248 self.render_block(
249 NodeRenderOptions {
250 ix,
251 is_first: flow.is_first,
252 is_last: flow.is_last,
253 prev: flow.prev,
254 ..Default::default()
255 },
256 &cx.node,
257 window,
258 app,
259 )
260 }
261}
262
263pub fn collect_leaf_geometry<R>(
280 decorations: HashMap<u64, Vec<(Range<usize>, Hsla)>>,
281 f: impl FnOnce() -> R,
282) -> (R, HashMap<u64, LeafGeometry>) {
283 let collector = Rc::new(Collector {
284 pieces: RefCell::default(),
285 decorations,
286 });
287 let result = editor_seam::with_sink(collector.clone(), f);
288 let mut pieces = collector.pieces.take();
289 pieces.retain(|_, pieces| !pieces.is_empty());
290 let leaves = pieces
291 .into_iter()
292 .map(|(key, pieces)| (key, LeafGeometry { pieces }))
293 .collect();
294 (result, leaves)
295}
296
297struct Collector {
298 pieces: RefCell<HashMap<u64, Vec<Piece>>>,
299 decorations: HashMap<u64, Vec<(Range<usize>, Hsla)>>,
300}
301
302impl EditorSink for Collector {
303 fn report(&self, key: u64, layout: &TextLayout) {
304 self.pieces
305 .borrow_mut()
306 .insert(key, vec![Piece::Whole(LayoutSnapshot::new(layout))]);
307 }
308
309 fn begin_pieces(&self, key: u64) {
310 self.pieces.borrow_mut().insert(key, Vec::new());
311 }
312
313 fn report_text_piece(&self, key: u64, start: usize, layout: &TextLayout) {
314 self.pieces
315 .borrow_mut()
316 .entry(key)
317 .or_default()
318 .push(Piece::Text {
319 start,
320 layout: LayoutSnapshot::new(layout),
321 });
322 }
323
324 fn report_box_piece(&self, key: u64, range: Range<usize>, bounds: Bounds<Pixels>) {
325 self.pieces
326 .borrow_mut()
327 .entry(key)
328 .or_default()
329 .push(Piece::Atom { range, bounds });
330 }
331
332 fn report_empty_piece(&self, key: u64, start: usize, bounds: Bounds<Pixels>) {
333 self.pieces
334 .borrow_mut()
335 .entry(key)
336 .or_default()
337 .push(Piece::Empty { start, bounds });
338 }
339
340 fn backgrounds(&self, key: u64) -> Vec<(Range<usize>, Hsla)> {
341 self.decorations.get(&key).cloned().unwrap_or_default()
342 }
343}
344
345#[derive(Clone)]
347struct LayoutSnapshot {
348 bounds: Bounds<Pixels>,
349 len: usize,
350 line_height: Pixels,
351 text: SharedString,
352 lines: Vec<Arc<WrappedLineLayout>>,
353}
354
355impl LayoutSnapshot {
356 fn new(layout: &TextLayout) -> Self {
357 Self {
358 bounds: layout.bounds(),
359 len: layout.len(),
360 line_height: layout.line_height(),
361 text: layout.text().into(),
362 lines: layout.line_layouts().into_iter().collect(),
363 }
364 }
365
366 fn bounds(&self) -> Bounds<Pixels> {
367 self.bounds
368 }
369 fn len(&self) -> usize {
370 self.len
371 }
372 fn line_height(&self) -> Pixels {
373 self.line_height
374 }
375 fn text(&self) -> &str {
376 &self.text
377 }
378 fn line_layouts(&self) -> &[Arc<WrappedLineLayout>] {
379 &self.lines
380 }
381
382 fn position_for_index(&self, index: usize) -> Option<Point<Pixels>> {
383 let mut origin = self.bounds.origin;
384 let mut start = 0;
385 for line in &self.lines {
386 if index <= start + line.len() {
387 return line
388 .position_for_index(index - start, self.line_height)
389 .map(|p| origin + p);
390 }
391 start += line.len() + 1;
392 origin.y += line.size(self.line_height).height;
393 }
394 None
395 }
396}
397
398#[derive(Clone)]
402enum Piece {
403 Whole(LayoutSnapshot),
404 Text {
405 start: usize,
406 layout: LayoutSnapshot,
407 },
408 Empty {
409 start: usize,
410 bounds: Bounds<Pixels>,
411 },
412 Atom {
413 range: Range<usize>,
414 bounds: Bounds<Pixels>,
415 },
416}
417
418impl Piece {
419 fn bounds(&self) -> Bounds<Pixels> {
420 match self {
421 Piece::Whole(layout) | Piece::Text { layout, .. } => layout.bounds(),
422 Piece::Atom { bounds, .. } | Piece::Empty { bounds, .. } => *bounds,
423 }
424 }
425
426 fn range(&self) -> Range<usize> {
427 match self {
428 Piece::Whole(layout) => 0..layout.len(),
429 Piece::Text { start, layout } => *start..start + layout.len(),
430 Piece::Atom { range, .. } => range.clone(),
431 Piece::Empty { start, .. } => *start..*start,
432 }
433 }
434}
435
436#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
442pub enum CaretAffinity {
443 Upstream,
445 #[default]
447 Downstream,
448}
449
450#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
452pub struct Caret {
453 pub index: usize,
455 pub affinity: CaretAffinity,
457}
458
459impl Caret {
460 pub fn new(index: usize) -> Self {
462 Self {
463 index,
464 affinity: CaretAffinity::Downstream,
465 }
466 }
467
468 pub fn upstream(index: usize) -> Self {
470 Self {
471 index,
472 affinity: CaretAffinity::Upstream,
473 }
474 }
475}
476
477#[derive(Clone)]
486pub struct LeafGeometry {
487 pieces: Vec<Piece>,
488}
489
490impl LeafGeometry {
491 fn whole(&self) -> Option<&LayoutSnapshot> {
492 match self.pieces.as_slice() {
493 [Piece::Whole(layout)] => Some(layout),
494 _ => None,
495 }
496 }
497
498 pub fn bounds(&self) -> Bounds<Pixels> {
500 self.pieces
501 .iter()
502 .map(Piece::bounds)
503 .reduce(|a, b| a.union(&b))
504 .unwrap_or_default()
505 }
506
507 pub fn len(&self) -> usize {
509 self.pieces
510 .iter()
511 .map(|piece| piece.range().end)
512 .max()
513 .unwrap_or(0)
514 }
515
516 pub fn is_empty(&self) -> bool {
518 self.len() == 0
519 }
520
521 pub fn line_height(&self) -> Pixels {
523 self.pieces
524 .iter()
525 .find_map(|piece| match piece {
526 Piece::Whole(layout) | Piece::Text { layout, .. } => Some(layout.line_height()),
527 Piece::Empty { bounds, .. } => Some(bounds.size.height),
528 Piece::Atom { .. } => None,
529 })
530 .unwrap_or_else(|| self.bounds().size.height)
531 }
532
533 pub fn caret_height(&self) -> Pixels {
536 let line_height = self.line_height();
537 self.pieces
538 .iter()
539 .find_map(|piece| match piece {
540 Piece::Whole(layout) | Piece::Text { layout, .. } => {
541 let line = layout.lines.first()?;
542 let h = line.unwrapped_layout.ascent + line.unwrapped_layout.descent;
543 (h > Pixels::ZERO).then_some(h)
544 }
545 Piece::Atom { .. } | Piece::Empty { .. } => None,
546 })
547 .unwrap_or(line_height * 0.85)
548 .min(line_height)
549 }
550
551 pub fn position_for_index(&self, index: usize) -> Point<Pixels> {
553 if let Some(layout) = self.whole() {
554 return layout
555 .position_for_index(index.min(layout.len()))
556 .unwrap_or_else(|| self.bounds().origin);
557 }
558 let piece = self
561 .pieces
562 .iter()
563 .find(|p| p.range().contains(&index))
564 .or_else(|| self.pieces.iter().rev().find(|p| p.range().end == index))
565 .or(self.pieces.last());
566 match piece {
567 Some(piece) => self.position_in_piece(piece, index),
568 None => self.bounds().origin,
569 }
570 }
571
572 pub fn index_for_position(&self, at: Point<Pixels>) -> usize {
575 if let Some(layout) = self.whole() {
576 return whole_index_at(layout, at);
577 }
578 let Some(piece) = self.hit_piece(at) else {
579 return 0;
580 };
581 let index = match piece {
582 Piece::Whole(layout) => whole_index_at(layout, at),
583 Piece::Text { start, layout } => {
590 let text = layout.text();
591 let nearest = text
592 .grapheme_indices(true)
593 .map(|(i, _)| i)
594 .chain(std::iter::once(text.len()))
595 .filter_map(|i| {
596 layout
597 .position_for_index(i)
598 .map(|p| (i, (p.x - at.x).abs()))
599 })
600 .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
601 .map_or(0, |(i, _)| i);
602 start + nearest
603 }
604 Piece::Empty { start, .. } => *start,
605 Piece::Atom { range, bounds } => {
606 if at.x < bounds.center().x {
607 range.start
608 } else {
609 range.end
610 }
611 }
612 };
613 index.min(self.len())
614 }
615
616 pub fn caret_for_position(&self, at: Point<Pixels>) -> Caret {
620 if let Some(layout) = self.whole() {
621 return whole_caret_at(layout, at);
622 }
623 let index = self.index_for_position(at);
624 let ends_hit_line = self.hit_piece(at).is_some_and(|piece| {
627 let range = piece.range();
628 range.start < range.end && range.end == index
629 });
630 if ends_hit_line {
631 Caret::upstream(index)
632 } else {
633 Caret::new(index)
634 }
635 }
636
637 pub fn position_for_caret(&self, caret: Caret) -> Point<Pixels> {
641 let index = caret.index;
642 if let Some(layout) = self.whole() {
643 return whole_position_for_caret(layout, caret).unwrap_or_else(|| self.bounds().origin);
644 }
645 match caret.affinity {
646 CaretAffinity::Downstream => {
647 let starts = self
649 .pieces
650 .iter()
651 .find(|p| p.range().start == index && p.range().end > index);
652 match starts {
653 Some(piece) => self.position_in_piece(piece, index),
654 None => self.position_for_index(index),
655 }
656 }
657 CaretAffinity::Upstream => {
658 let ends = self
659 .pieces
660 .iter()
661 .find(|p| p.range().start < index && p.range().end == index);
662 match ends {
663 Some(piece) => self.position_in_piece(piece, index),
664 None => self.position_for_index(index),
665 }
666 }
667 }
668 }
669
670 fn position_in_piece(&self, piece: &Piece, index: usize) -> Point<Pixels> {
671 match piece {
672 Piece::Whole(layout) | Piece::Text { layout, .. } => {
673 let local = index.saturating_sub(piece.range().start).min(layout.len());
674 layout
675 .position_for_index(local)
676 .unwrap_or_else(|| layout.bounds().origin)
677 }
678 Piece::Empty { bounds, .. } => bounds.origin,
679 Piece::Atom { range, bounds } => {
680 let x = if index >= range.end {
681 bounds.right()
682 } else {
683 bounds.left()
684 };
685 let y = (bounds.bottom() - self.line_height()).max(bounds.top());
686 point(x, y)
687 }
688 }
689 }
690
691 fn hit_piece(&self, at: Point<Pixels>) -> Option<&Piece> {
695 let dy = |p: &Piece| {
696 let b = p.bounds();
697 if at.y < b.top() {
698 b.top() - at.y
699 } else if at.y > b.bottom() {
700 at.y - b.bottom()
701 } else {
702 px(0.)
703 }
704 };
705 let dx = |p: &Piece| {
706 let b = p.bounds();
707 if at.x < b.left() {
708 b.left() - at.x
709 } else if at.x > b.right() {
710 at.x - b.right()
711 } else {
712 px(0.)
713 }
714 };
715 self.pieces.iter().min_by(|a, b| {
716 (dy(a), dx(a))
717 .partial_cmp(&(dy(b), dx(b)))
718 .unwrap_or(std::cmp::Ordering::Equal)
719 })
720 }
721
722 pub fn atom_at(&self, at: Point<Pixels>) -> Option<Range<usize>> {
724 self.pieces.iter().find_map(|piece| match piece {
725 Piece::Atom { range, bounds } if bounds.contains(&at) => Some(range.clone()),
726 _ => None,
727 })
728 }
729}
730
731fn whole_index_at(layout: &LayoutSnapshot, at: Point<Pixels>) -> usize {
733 let bounds = layout.bounds();
734 let line_height = layout.line_height();
735 let mut top = bounds.top();
736 let mut start = 0;
737 let lines = layout.line_layouts();
738 for (ix, line) in lines.iter().enumerate() {
739 let height = line.size(line_height).height.max(line_height);
740 let is_last = ix + 1 == lines.len();
741 if at.y < top + height || is_last {
742 let local = point(
743 (at.x - bounds.left()).max(px(0.)),
744 (at.y - top).clamp(px(0.), (height - px(1.)).max(px(0.))),
745 );
746 let index = match line.closest_index_for_position(local, line_height) {
747 Ok(ix) | Err(ix) => ix,
748 };
749 return start + index;
750 }
751 top += height;
752 start += line.len() + 1;
753 }
754 0
755}
756
757fn wrap_starts(line: &WrappedLineLayout) -> impl Iterator<Item = usize> + '_ {
759 line.wrap_boundaries
760 .iter()
761 .map(|b| line.unwrapped_layout.runs[b.run_ix].glyphs[b.glyph_ix].index)
762}
763
764fn whole_caret_at(layout: &LayoutSnapshot, at: Point<Pixels>) -> Caret {
766 let index = whole_index_at(layout, at);
767 let bounds = layout.bounds();
768 let line_height = layout.line_height();
769 let mut top = bounds.top();
770 let mut start = 0;
771 let lines = layout.line_layouts();
772 for (ix, line) in lines.iter().enumerate() {
773 let height = line.size(line_height).height.max(line_height);
774 if at.y < top + height || ix + 1 == lines.len() {
775 let row = ((at.y - top).max(px(0.)) / line_height) as usize;
776 let row_end = wrap_starts(line).nth(row).map(|i| start + i);
779 return if row_end == Some(index) {
780 Caret::upstream(index)
781 } else {
782 Caret::new(index)
783 };
784 }
785 top += height;
786 start += line.len() + 1;
787 }
788 Caret::new(index)
789}
790
791fn whole_position_for_caret(layout: &LayoutSnapshot, caret: Caret) -> Option<Point<Pixels>> {
793 let index = caret.index.min(layout.len());
794 let line_height = layout.line_height();
795 let mut origin = layout.bounds().origin;
796 let mut start = 0;
797 for line in layout.line_layouts() {
798 if index <= start + line.len() {
799 let local = index - start;
800 let unwrapped = &line.unwrapped_layout;
801 let mut row = 0;
804 let mut row_start = 0;
805 for (ix, wrap) in wrap_starts(line).enumerate() {
806 if wrap < local || (wrap == local && caret.affinity == CaretAffinity::Downstream) {
807 row = ix + 1;
808 row_start = wrap;
809 } else {
810 break;
811 }
812 }
813 let x = unwrapped.x_for_index(local) - unwrapped.x_for_index(row_start);
814 return Some(origin + point(x, line_height * row as f32));
815 }
816 start += line.len() + 1;
817 origin.y += line.size(line_height).height;
818 }
819 None
820}
821
822#[cfg(test)]
823mod tests {
824 use std::cell::Cell;
825
826 use gpui::{
827 Context, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, LayoutId,
828 Render, Styled as _, TestAppContext, VisualTestContext, div, rgb, rgba,
829 };
830
831 use super::*;
832 use crate::text::{InlineElement, MarkdownNode, markdown_ext::TestInlinePlugin};
833
834 type Geometries = Rc<RefCell<HashMap<u64, LeafGeometry>>>;
835
836 struct Probe {
839 child: Option<AnyElement>,
840 decorations: HashMap<u64, Vec<(Range<usize>, Hsla)>>,
841 out: Geometries,
842 sink: Option<Rc<dyn EditorSink>>,
844 }
845
846 impl IntoElement for Probe {
847 type Element = Self;
848 fn into_element(self) -> Self::Element {
849 self
850 }
851 }
852
853 impl Element for Probe {
854 type RequestLayoutState = AnyElement;
855 type PrepaintState = ();
856
857 fn id(&self) -> Option<ElementId> {
858 None
859 }
860
861 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
862 None
863 }
864
865 fn request_layout(
866 &mut self,
867 _: Option<&GlobalElementId>,
868 _: Option<&InspectorElementId>,
869 window: &mut Window,
870 cx: &mut App,
871 ) -> (LayoutId, AnyElement) {
872 let mut child = self.child.take().unwrap();
873 (child.request_layout(window, cx), child)
874 }
875
876 fn prepaint(
877 &mut self,
878 _: Option<&GlobalElementId>,
879 _: Option<&InspectorElementId>,
880 _: Bounds<Pixels>,
881 child: &mut AnyElement,
882 window: &mut Window,
883 cx: &mut App,
884 ) {
885 if let Some(sink) = self.sink.take() {
886 editor_seam::with_sink(sink, || child.prepaint(window, cx));
887 return;
888 }
889 let decorations = std::mem::take(&mut self.decorations);
890 let (_, geometries) = collect_leaf_geometry(decorations, || child.prepaint(window, cx));
891 *self.out.borrow_mut() = geometries;
892 }
893
894 fn paint(
895 &mut self,
896 _: Option<&GlobalElementId>,
897 _: Option<&InspectorElementId>,
898 _: Bounds<Pixels>,
899 child: &mut AnyElement,
900 _: &mut (),
901 window: &mut Window,
902 cx: &mut App,
903 ) {
904 child.paint(window, cx);
905 }
906 }
907
908 struct Host {
909 blocks: Vec<BlockNode>,
910 extensions: Arc<MarkdownExtensions>,
911 decorations: HashMap<u64, Vec<(Range<usize>, Hsla)>>,
912 out: Geometries,
913 width: Pixels,
914 sink: Option<Rc<dyn EditorSink>>,
915 render_context: Option<RenderContext>,
916 }
917
918 impl Render for Host {
919 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
920 let rcx = self
921 .render_context
922 .clone()
923 .unwrap_or_else(|| RenderContext::from_app(cx))
924 .with_markdown_extensions(self.extensions.clone())
925 .with_body_font_size(px(16.));
926 let refs: Vec<&BlockNode> = self.blocks.iter().collect();
927 let children: Vec<AnyElement> = refs
928 .iter()
929 .enumerate()
930 .map(|(ix, block)| {
931 let flow = FlowPosition::at(&refs, ix, ix + 1 == refs.len());
932 block.render(ix, flow, &rcx, window, cx)
933 })
934 .collect();
935 Probe {
936 child: Some(div().w(self.width).children(children).into_any_element()),
937 decorations: self.decorations.clone(),
938 out: self.out.clone(),
939 sink: self.sink.clone(),
940 }
941 }
942 }
943
944 fn draw(
945 cx: &mut TestAppContext,
946 blocks: Vec<BlockNode>,
947 decorations: HashMap<u64, Vec<(Range<usize>, Hsla)>>,
948 extensions: Arc<MarkdownExtensions>,
949 width: Pixels,
950 ) -> HashMap<u64, LeafGeometry> {
951 draw_and_inspect(cx, blocks, decorations, extensions, width, |_| ()).0
952 }
953
954 fn draw_and_inspect<R>(
955 cx: &mut TestAppContext,
956 blocks: Vec<BlockNode>,
957 decorations: HashMap<u64, Vec<(Range<usize>, Hsla)>>,
958 extensions: Arc<MarkdownExtensions>,
959 width: Pixels,
960 inspect: impl FnOnce(&mut Window) -> R,
961 ) -> (HashMap<u64, LeafGeometry>, R) {
962 cx.update(crate::init);
963 let out = Geometries::default();
964 let (_, vcx) = cx.add_window_view({
965 let out = out.clone();
966 move |_, _| Host {
967 blocks,
968 extensions,
969 decorations,
970 out,
971 width,
972 sink: None,
973 render_context: None,
974 }
975 });
976 settle(vcx);
977 let inspected = vcx.update(|window, _| inspect(window));
978 (out.take(), inspected)
979 }
980
981 fn settle(cx: &mut VisualTestContext) {
982 cx.run_until_parked();
983 cx.update(|window, cx| window.draw(cx).clear(cx));
984 cx.run_until_parked();
985 }
986
987 fn plain(text: &str) -> Paragraph {
988 let mut paragraph = Paragraph::default();
989 paragraph.push(InlineNode::new(text.to_string()));
990 paragraph
991 }
992
993 fn atom_plugin() -> Arc<MarkdownExtensions> {
994 Arc::new(
995 MarkdownExtensions::default().plugin(TestInlinePlugin::new("atom").render_with(
996 |_, _, _, _| {
997 Some(InlineElement::new(div().w(px(24.)).h(px(16.))).with_baseline(px(12.)))
998 },
999 )),
1000 )
1001 }
1002
1003 #[gpui::test]
1004 fn render_local_foregrounds_keep_reused_leaf_geometry(cx: &mut TestAppContext) {
1005 cx.update(crate::init);
1006 let out = Geometries::default();
1007 let (host, vcx) = cx.add_window_view({
1008 let out = out.clone();
1009 move |_, cx| {
1010 let mut mixed = plain("a中😀").with_leaf_key(2);
1011 mixed.push(atom());
1012 mixed.push(InlineNode::new("tail"));
1013 Host {
1014 blocks: vec![
1015 BlockNode::Paragraph(plain("a中😀").with_leaf_key(1)),
1016 BlockNode::Paragraph(mixed),
1017 ],
1018 extensions: atom_plugin(),
1019 decorations: HashMap::new(),
1020 out,
1021 width: px(300.),
1022 sink: None,
1023 render_context: Some(
1024 RenderContext::from_app(cx)
1025 .with_foreground_ranges(1, vec![(0..8, gpui::red())])
1026 .with_foreground_ranges(2, vec![(0..15, gpui::red())]),
1027 ),
1028 }
1029 }
1030 });
1031 settle(vcx);
1032 let before = out.borrow().clone();
1033 for color in [Some(gpui::blue()), None] {
1034 host.update(vcx, |host, cx| {
1035 let ranges: Vec<_> = color.map(|color| (0..15, color)).into_iter().collect();
1036 host.render_context = Some(
1037 RenderContext::from_app(cx)
1038 .with_foreground_ranges(1, vec![(0..15, gpui::red())])
1039 .with_foreground_ranges(1, ranges.clone())
1040 .with_foreground_ranges(2, ranges.clone()),
1041 );
1042 assert_eq!(
1043 host.render_context
1044 .as_ref()
1045 .unwrap()
1046 .node
1047 .foregrounds
1048 .as_ref()
1049 .unwrap()[&1],
1050 ranges
1051 );
1052 cx.notify();
1053 });
1054 settle(vcx);
1055 let after = out.borrow();
1056 for key in [1, 2] {
1057 assert_eq!(after[&key].bounds(), before[&key].bounds());
1058 assert_eq!(after[&key].len(), before[&key].len());
1059 for at in [0, 1, 4, 8] {
1060 assert_eq!(
1061 after[&key].position_for_index(at),
1062 before[&key].position_for_index(at)
1063 );
1064 }
1065 }
1066 }
1067 }
1068
1069 fn atom() -> InlineNode {
1070 InlineNode::custom(MarkdownNode::new("atom", ()).text("\u{FFFC}"))
1071 }
1072
1073 #[gpui::test]
1074 fn keyed_leaves_report_geometry_and_unkeyed_leaves_do_not(cx: &mut TestAppContext) {
1075 let blocks = vec![
1076 BlockNode::Heading {
1077 level: 1,
1078 children: plain("Title").with_leaf_key(1),
1079 span: None,
1080 },
1081 BlockNode::Paragraph(plain("first").with_leaf_key(2)),
1082 BlockNode::Paragraph(plain("not an editor leaf")),
1083 BlockNode::CodeBlock(
1084 CodeBlock::new("let x = 1;".into(), Some("rust".into()), None::<Span>)
1085 .with_leaf_key(3),
1086 ),
1087 BlockNode::Table(Table::new(
1088 vec![
1089 vec![plain("a").with_leaf_key(4), plain("b").with_leaf_key(5)],
1090 vec![plain("c").with_leaf_key(6), plain("d").with_leaf_key(7)],
1091 ],
1092 vec![ColumnAlign::None, ColumnAlign::Right],
1093 )),
1094 BlockNode::List {
1095 children: vec![BlockNode::ListItem {
1096 children: vec![BlockNode::Paragraph(plain("item").with_leaf_key(8))],
1097 spread: false,
1098 checked: None,
1099 task_marker: None,
1100 span: None,
1101 }],
1102 ordered: false,
1103 start: None,
1104 span: None,
1105 },
1106 ];
1107 let geometries = draw(cx, blocks, HashMap::new(), Default::default(), px(300.));
1108 let mut keys: Vec<u64> = geometries.keys().copied().collect();
1109 keys.sort_unstable();
1110 assert_eq!(keys, (1..=8).collect::<Vec<_>>());
1111 for key in [1, 2, 3, 8] {
1112 assert!(geometries[&key].bounds().size.height > px(0.), "leaf {key}");
1113 }
1114 assert_eq!(geometries[&1].len(), "Title".len());
1115 assert_eq!(geometries[&3].len(), "let x = 1;".len());
1116 let top = |key: u64| geometries[&key].bounds().top();
1118 assert!(top(1) < top(2) && top(2) < top(3) && top(3) < top(4) && top(7) < top(8));
1119 assert_eq!(top(4), top(5));
1121 assert!(geometries[&5].bounds().left() > geometries[&4].bounds().right() - px(1.));
1122 }
1123
1124 #[gpui::test]
1125 fn wrapped_leaf_maps_indices_and_positions(cx: &mut TestAppContext) {
1126 let text = "the quick brown fox jumps over the lazy dog and keeps running far away";
1127 let blocks = vec![BlockNode::Paragraph(plain(text).with_leaf_key(1))];
1128 let geometries = draw(cx, blocks, HashMap::new(), Default::default(), px(120.));
1129 let leaf = &geometries[&1];
1130 assert_eq!(leaf.len(), text.len());
1131 assert!(!leaf.is_empty());
1132 let line = leaf.line_height();
1133 assert!(line > px(0.) && leaf.caret_height() <= line);
1134 let mut last_y = px(-1.);
1135 let mut lines = std::collections::BTreeSet::new();
1136 for index in 0..=text.len() {
1137 let caret = leaf.position_for_index(index);
1138 assert!(caret.y >= last_y, "carets never move up");
1139 last_y = caret.y;
1140 lines.insert((caret.y.as_f32() * 100.) as i64);
1141 let hit = leaf.index_for_position(point(caret.x, caret.y + line / 2.));
1144 assert_eq!(
1145 leaf.position_for_index(hit),
1146 caret,
1147 "index {index} hit {hit}"
1148 );
1149 }
1150 assert!(lines.len() >= 3, "the text wraps onto several lines");
1151 assert_eq!(leaf.index_for_position(point(px(-500.), px(-500.))), 0);
1153 assert_eq!(
1154 leaf.index_for_position(point(px(5000.), px(5000.))),
1155 text.len()
1156 );
1157 }
1158
1159 fn assert_carets_keep_their_line(leaf: &LeafGeometry) {
1162 let bounds = leaf.bounds();
1163 let line = leaf.line_height();
1164 let rows = (bounds.size.height / line).round() as usize;
1165 assert!(rows >= 3, "the text wraps onto several lines");
1166 let mut wraps = 0;
1167 for row in 0..rows {
1168 let row_top = bounds.top() + line * row as f32;
1169 let y = row_top + line / 2.;
1170 for x in [bounds.left() + px(1.), bounds.right() - px(1.)] {
1171 let caret = leaf.caret_for_position(point(x, y));
1172 assert_eq!(caret.index, leaf.index_for_position(point(x, y)));
1173 let pos = leaf.position_for_caret(caret);
1174 assert!(
1175 (pos.y - row_top).abs() <= line / 2.,
1176 "row {row} x {x:?}: {caret:?} at {pos:?}, row top {row_top:?}"
1177 );
1178 if leaf.position_for_index(caret.index).y != pos.y {
1179 wraps += 1;
1180 }
1181 }
1182 }
1183 assert!(wraps > 0, "some clicks land on a soft wrap");
1184 }
1185
1186 #[gpui::test]
1187 fn wrapped_whole_leaf_carets_keep_their_line(cx: &mut TestAppContext) {
1188 let text = "the quick brown fox jumps over the lazy dog and keeps running far away";
1189 let blocks = vec![BlockNode::Paragraph(plain(text).with_leaf_key(1))];
1190 let geometries = draw(cx, blocks, HashMap::new(), Default::default(), px(120.));
1191 let leaf = &geometries[&1];
1192 assert!(leaf.whole().is_some());
1193 assert_carets_keep_their_line(leaf);
1194 for index in [0, 1, text.len()] {
1196 let at = leaf.position_for_index(index);
1197 assert_eq!(leaf.position_for_caret(Caret::new(index)), at);
1198 assert_eq!(leaf.position_for_caret(Caret::upstream(index)), at);
1199 }
1200 }
1201
1202 #[gpui::test]
1203 fn wrapped_mixed_leaf_carets_keep_their_line(cx: &mut TestAppContext) {
1204 let words = "the quick brown fox jumps over the lazy dog and ";
1205 let mut paragraph = Paragraph::default();
1206 paragraph.push(InlineNode::new(words));
1207 paragraph.push(atom());
1208 paragraph.push(InlineNode::new(words));
1209 let blocks = vec![BlockNode::Paragraph(paragraph.with_leaf_key(1))];
1210 let geometries = draw(cx, blocks, HashMap::new(), atom_plugin(), px(120.));
1211 let leaf = &geometries[&1];
1212 assert!(leaf.whole().is_none());
1213 assert_carets_keep_their_line(leaf);
1214 }
1215
1216 #[gpui::test]
1217 fn mixed_text_and_atoms_report_boxes(cx: &mut TestAppContext) {
1218 let mut paragraph = Paragraph::default();
1219 paragraph.push(InlineNode::new("ab"));
1220 paragraph.push(atom());
1221 paragraph.push(InlineNode::new("cd"));
1222 let atom_len = '\u{FFFC}'.len_utf8();
1223 let blocks = vec![BlockNode::Paragraph(paragraph.with_leaf_key(1))];
1224 let geometries = draw(cx, blocks, HashMap::new(), atom_plugin(), px(300.));
1225 let leaf = &geometries[&1];
1226 assert_eq!(leaf.len(), 2 + atom_len + 2);
1227 let range = 2..2 + atom_len;
1229 let before = leaf.position_for_index(2);
1230 let after = leaf.position_for_index(2 + atom_len);
1231 assert!(after.x > before.x, "the caret moves across the atom");
1232 let inside = point(
1233 (before.x + after.x) / 2.,
1234 before.y + leaf.line_height() / 2.,
1235 );
1236 assert_eq!(leaf.atom_at(inside), Some(range.clone()));
1237 assert_eq!(leaf.atom_at(point(before.x - px(8.), inside.y)), None);
1238 let quarter = |t: f32| point(before.x + (after.x - before.x) * t, inside.y);
1240 assert_eq!(leaf.index_for_position(quarter(0.25)), range.start);
1241 assert_eq!(leaf.index_for_position(quarter(0.75)), range.end);
1242 let end = leaf.position_for_index(leaf.len());
1244 assert_eq!(
1245 leaf.index_for_position(point(end.x + px(40.), inside.y)),
1246 leaf.len()
1247 );
1248 assert_eq!(leaf.index_for_position(point(px(-40.), inside.y)), 0);
1249 }
1250
1251 #[gpui::test]
1252 fn mixed_flow_hits_land_on_grapheme_boundaries(cx: &mut TestAppContext) {
1253 let parts = ["éa", "\u{FFFC}", "中文", "\u{FFFC}", "👍🏽x"];
1256 let mut paragraph = Paragraph::default();
1257 for part in parts {
1258 if part == "\u{FFFC}" {
1259 paragraph.push(atom());
1260 } else {
1261 paragraph.push(InlineNode::new(part));
1262 }
1263 }
1264 let text = parts.concat();
1265 let blocks = vec![BlockNode::Paragraph(paragraph.with_leaf_key(1))];
1266 let geometries = draw(cx, blocks, HashMap::new(), atom_plugin(), px(400.));
1267 let leaf = &geometries[&1];
1268 assert_eq!(leaf.len(), text.len());
1269 let boundaries: Vec<usize> = text
1270 .grapheme_indices(true)
1271 .map(|(i, _)| i)
1272 .chain(std::iter::once(text.len()))
1273 .collect();
1274
1275 let bounds = leaf.bounds();
1278 let y = bounds.top() + leaf.line_height() / 2.;
1279 let mut x = bounds.left() - px(4.);
1280 while x <= bounds.right() + px(4.) {
1281 let index = leaf.index_for_position(point(x, y));
1282 assert!(
1283 boundaries.contains(&index),
1284 "hit at {x:?} returned {index}, inside a grapheme of {text:?}"
1285 );
1286 let mut edited = text.clone();
1287 edited.insert(index, '|');
1288 x += px(0.5);
1289 }
1290
1291 for &index in &boundaries {
1294 let at = leaf.position_for_index(index);
1295 assert_eq!(
1296 leaf.index_for_position(point(at.x, y)),
1297 index,
1298 "caret position of {index} in {text:?}"
1299 );
1300 }
1301 }
1302
1303 #[gpui::test]
1304 fn mixed_flow_keeps_empty_lines_and_trailing_breaks(cx: &mut TestAppContext) {
1305 let mut paragraph = Paragraph::default();
1306 paragraph.push(atom());
1307 paragraph.push(InlineNode::new("a\n\n"));
1308 let mut breaks = Paragraph::default();
1309 breaks.push(atom());
1310 breaks.push(InlineNode::new("\n\n"));
1311 let leaves = draw(
1312 cx,
1313 vec![
1314 BlockNode::Paragraph(paragraph.with_leaf_key(1)),
1315 BlockNode::Paragraph(breaks.with_leaf_key(2)),
1316 ],
1317 HashMap::new(),
1318 atom_plugin(),
1319 px(300.),
1320 );
1321 let leaf = &leaves[&1];
1322 assert_eq!(leaf.len(), 6);
1323 let a = leaf.position_for_index(3);
1324 let empty = leaf.position_for_index(5);
1325 let end = leaf.position_for_index(6);
1326 assert!(a.y < empty.y && empty.y < end.y);
1327 for index in [5, 6] {
1328 let caret = leaf.position_for_index(index);
1329 assert_eq!(
1330 leaf.index_for_position(point(
1331 caret.x + px(20.),
1332 caret.y + leaf.line_height() / 2.
1333 )),
1334 index
1335 );
1336 }
1337 assert!(leaf.bounds().bottom() >= end.y + leaf.line_height());
1338 let breaks = &leaves[&2];
1339 assert_eq!(breaks.len(), 5);
1340 assert!(breaks.line_height() < breaks.bounds().size.height);
1341 assert!(breaks.position_for_index(5).y > breaks.position_for_index(4).y);
1342 }
1343
1344 #[gpui::test]
1345 fn atom_decorations_are_painted_for_custom_nodes_and_images(cx: &mut TestAppContext) {
1346 let color: Hsla = rgba(0x12345680).into();
1347 let mut custom = Paragraph::default();
1348 custom.push(atom());
1349 let mut image = Paragraph::default();
1350 image.push(InlineNode::image(ImageNode {
1351 url: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20'/>".into(),
1352 width: Some(px(20.).into()), height: Some(px(20.).into()),
1353 ..Default::default()
1354 }));
1355 let blocks = vec![
1356 BlockNode::Paragraph(custom.with_leaf_key(1)),
1357 BlockNode::Paragraph(image.with_leaf_key(2)),
1358 ];
1359 let decorations = HashMap::from([
1360 (1, vec![(0..3, color), (1..1, color)]),
1361 (2, vec![(0..3, color), (3..6, color)]),
1362 ]);
1363 let (_, painted) =
1364 draw_and_inspect(cx, blocks, decorations, atom_plugin(), px(300.), |window| {
1365 let background: gpui::Background = color.into();
1366 window
1367 .painted_quads()
1368 .into_iter()
1369 .filter(|quad| quad.background == background)
1370 .count()
1371 });
1372 assert_eq!(
1373 painted, 2,
1374 "one overlay per atom; empty and adjacent ranges paint nothing"
1375 );
1376 }
1377
1378 #[gpui::test]
1379 fn rekeying_cloned_leaves_preserves_each_identity(cx: &mut TestAppContext) {
1380 let original = plain("shared source").with_leaf_key(1);
1381 let copy = original.clone().with_leaf_key(2);
1382 let code = CodeBlock::new("let x = 1;".into(), None, None::<Span>).with_leaf_key(3);
1383 let copy_code = code.clone().with_leaf_key(4);
1384 let leaves = draw(
1385 cx,
1386 vec![
1387 BlockNode::Paragraph(original),
1388 BlockNode::Paragraph(copy),
1389 BlockNode::CodeBlock(code),
1390 BlockNode::CodeBlock(copy_code),
1391 ],
1392 HashMap::new(),
1393 Default::default(),
1394 px(300.),
1395 );
1396 assert_eq!(leaves.len(), 4);
1397 assert_eq!(leaves[&1].len(), "shared source".len());
1398 assert_eq!(leaves[&3].len(), "let x = 1;".len());
1399 assert!(leaves[&1].bounds().top() < leaves[&2].bounds().top());
1400 assert!(leaves[&3].bounds().top() < leaves[&4].bounds().top());
1401 }
1402
1403 #[gpui::test]
1404 fn geometry_is_an_immutable_snapshot(cx: &mut TestAppContext) {
1405 cx.update(crate::init);
1406 let out = Geometries::default();
1407 let (host, vcx) = cx.add_window_view({
1408 let out = out.clone();
1409 move |_, _| Host {
1410 blocks: vec![BlockNode::Paragraph(
1411 plain("a long sentence that wraps when the viewport gets narrower")
1412 .with_leaf_key(1),
1413 )],
1414 extensions: Default::default(),
1415 decorations: HashMap::new(),
1416 out,
1417 width: px(600.),
1418 sink: None,
1419 render_context: None,
1420 }
1421 });
1422 settle(vcx);
1423 let old = out.borrow()[&1].clone();
1424 let before = old.bounds();
1425 vcx.update(|_, cx| {
1426 host.update(cx, |host, cx| {
1427 host.width = px(80.);
1428 cx.notify();
1429 })
1430 });
1431 settle(vcx);
1432 assert!(out.borrow()[&1].bounds().size.height > before.size.height);
1433 assert_eq!(old.bounds(), before);
1434 assert_eq!(old.position_for_index(0), before.origin);
1435 }
1436
1437 #[gpui::test]
1438 fn empty_keyed_leaf_has_a_line_box_and_empty_unkeyed_leaf_stays_empty(cx: &mut TestAppContext) {
1439 let blocks = vec![
1440 BlockNode::Paragraph(Paragraph::default()),
1441 BlockNode::Paragraph(Paragraph::default().with_leaf_key(9)),
1442 ];
1443 let geometries = draw(cx, blocks, HashMap::new(), Default::default(), px(300.));
1444 assert_eq!(geometries.len(), 1);
1445 let leaf = &geometries[&9];
1446 assert!(leaf.is_empty());
1447 assert!(leaf.bounds().size.height > px(0.));
1448 assert_eq!(leaf.position_for_index(0).x, leaf.bounds().left());
1449 assert_eq!(
1450 leaf.index_for_position(point(px(50.), leaf.bounds().top())),
1451 0
1452 );
1453 }
1454
1455 #[gpui::test]
1456 fn image_only_leaf_reports_its_box(cx: &mut TestAppContext) {
1457 let image = ImageNode {
1458 url: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20'/>".into(),
1459 width: Some(px(20.).into()),
1460 height: Some(px(20.).into()),
1461 ..Default::default()
1462 };
1463 let mut paragraph = Paragraph::default();
1464 paragraph.push(InlineNode::image(image));
1465 let blocks = vec![BlockNode::Paragraph(paragraph.with_leaf_key(1))];
1466 let geometries = draw(cx, blocks, HashMap::new(), Default::default(), px(300.));
1467 let leaf = &geometries[&1];
1468 let atom_len = '\u{FFFC}'.len_utf8();
1469 assert_eq!(leaf.len(), atom_len);
1470 let inside = leaf.bounds().center();
1471 assert_eq!(leaf.atom_at(inside), Some(0..atom_len));
1472 }
1473
1474 #[derive(Default)]
1477 struct Recorder {
1478 asked: RefCell<Vec<u64>>,
1479 }
1480
1481 impl EditorSink for Recorder {
1482 fn report(&self, _: u64, _: &TextLayout) {}
1483 fn begin_pieces(&self, _: u64) {}
1484 fn report_text_piece(&self, _: u64, _: usize, _: &TextLayout) {}
1485 fn report_box_piece(&self, _: u64, _: Range<usize>, _: Bounds<Pixels>) {}
1486 fn report_empty_piece(&self, _: u64, _: usize, _: Bounds<Pixels>) {}
1487 fn backgrounds(&self, key: u64) -> Vec<(Range<usize>, Hsla)> {
1488 self.asked.borrow_mut().push(key);
1489 vec![(0..1, rgb(0xff0000).into())]
1490 }
1491 }
1492
1493 #[gpui::test]
1494 fn decorations_are_requested_per_keyed_leaf(cx: &mut TestAppContext) {
1495 cx.update(crate::init);
1496 let recorder = Rc::new(Recorder::default());
1497 let mut mixed = Paragraph::default();
1498 mixed.push(InlineNode::new("ab"));
1499 mixed.push(atom());
1500 let blocks = vec![
1501 BlockNode::Paragraph(plain("one").with_leaf_key(1)),
1502 BlockNode::Paragraph(plain("unkeyed")),
1503 BlockNode::Paragraph(mixed.with_leaf_key(2)),
1504 ];
1505 let (_, vcx) = cx.add_window_view({
1506 let sink: Rc<dyn EditorSink> = recorder.clone();
1507 move |_, _| Host {
1508 blocks,
1509 extensions: atom_plugin(),
1510 decorations: HashMap::new(),
1511 out: Geometries::default(),
1512 width: px(300.),
1513 sink: Some(sink),
1514 render_context: None,
1515 }
1516 });
1517 settle(vcx);
1518 let asked = recorder.asked.borrow();
1519 assert!(asked.contains(&1) && asked.contains(&2), "{asked:?}");
1520 assert!(asked.iter().all(|key| *key == 1 || *key == 2), "{asked:?}");
1521 }
1522
1523 #[gpui::test]
1524 fn decorated_leaves_still_report_geometry(cx: &mut TestAppContext) {
1525 let blocks = vec![BlockNode::Paragraph(plain("hello world").with_leaf_key(1))];
1526 let decorations = HashMap::from([(
1527 1,
1528 vec![
1529 (0..5, rgba(0x3366ff80).into()),
1530 (6..11, rgb(0xffcc00).into()),
1531 ],
1532 )]);
1533 let plain_geometry = draw(
1534 cx,
1535 blocks.clone(),
1536 HashMap::new(),
1537 Default::default(),
1538 px(300.),
1539 );
1540 let decorated = draw(cx, blocks, decorations, Default::default(), px(300.));
1541 assert_eq!(
1542 decorated[&1].bounds().size,
1543 plain_geometry[&1].bounds().size,
1544 "decorations paint behind the text and never change layout"
1545 );
1546 }
1547
1548 #[test]
1549 fn collect_leaf_geometry_nests_and_restores_on_panic() {
1550 let seen = Cell::new(0);
1551 let (inner_ran, geometries) = collect_leaf_geometry(HashMap::new(), || {
1552 assert!(editor_seam::current().is_some());
1553 let outer = editor_seam::current().unwrap();
1554 collect_leaf_geometry(HashMap::new(), || seen.set(seen.get() + 1));
1555 let after = editor_seam::current().unwrap();
1556 assert!(Rc::ptr_eq(&outer, &after), "the outer scope is restored");
1557 true
1558 });
1559 assert!(inner_ran && geometries.is_empty() && seen.get() == 1);
1560 assert!(editor_seam::current().is_none());
1561
1562 let result = std::panic::catch_unwind(|| {
1563 collect_leaf_geometry(HashMap::new(), || panic!("boom"));
1564 });
1565 assert!(result.is_err());
1566 assert!(editor_seam::current().is_none(), "cleared after a panic");
1567 }
1568
1569 #[test]
1570 fn parse_markdown_reads_what_an_editor_round_trips() {
1571 let source = "```rust title=\"x\"\nfn main() {}\n```\n\n\
1572 | a | b | c | d |\n|---|:--|:-:|--:|\n| 1 | 2 | 3 | 4 |\n\n\
1573 [ref]: https://example.com \"Title\"\n";
1574 let blocks = parse_markdown(source, Default::default()).unwrap();
1575 let code = blocks
1576 .iter()
1577 .find_map(|block| match block {
1578 BlockNode::CodeBlock(code) => Some(code),
1579 _ => None,
1580 })
1581 .expect("code block");
1582 assert_eq!(code.lang().as_deref(), Some("rust"));
1583 assert_eq!(code.meta().as_deref(), Some("title=\"x\""));
1584 let table = blocks
1585 .iter()
1586 .find_map(|block| match block {
1587 BlockNode::Table(table) => Some(table),
1588 _ => None,
1589 })
1590 .expect("table");
1591 assert_eq!(
1592 table.column_aligns(),
1593 [
1594 ColumnAlign::None,
1595 ColumnAlign::Left,
1596 ColumnAlign::Center,
1597 ColumnAlign::Right
1598 ]
1599 );
1600 let cells: Vec<Vec<String>> = table
1601 .rows()
1602 .map(|row| {
1603 row.map(|cell| cell.children().iter().map(|c| c.text()).collect())
1604 .collect()
1605 })
1606 .collect();
1607 assert_eq!(cells, [["a", "b", "c", "d"], ["1", "2", "3", "4"]]);
1608 assert!(blocks.iter().any(|block| matches!(
1609 block,
1610 BlockNode::Definition { identifier, .. } if identifier.as_ref() == "ref"
1611 )));
1612 assert!(
1613 blocks
1614 .iter()
1615 .all(|block| block.span().is_some() || matches!(block, BlockNode::Unknown))
1616 );
1617 }
1618
1619 #[test]
1620 fn parse_markdown_matches_text_view_parse() {
1621 let source = "# Title\n\nSome **bold** and `code` and ==mark==.\n\n- one\n- two\n";
1622 let mut cx = NodeContext::default();
1623 let expected = markdown::parse(source, &mut cx).unwrap();
1624 let blocks = parse_markdown(source, Default::default()).unwrap();
1625 assert_eq!(blocks, *expected.blocks);
1626 }
1627
1628 #[test]
1629 fn table_and_paragraph_builders_read_back() {
1630 let mut paragraph = Paragraph::default();
1631 paragraph.push(InlineNode::new("hi").marks(vec![(0..2, TextMark::default().bold())]));
1632 assert_eq!(paragraph.children()[0].text(), "hi");
1633 assert!(paragraph.children()[0].mark_runs()[0].1.bold);
1634 assert!(paragraph.children()[0].as_image().is_none());
1635 assert!(paragraph.children()[0].as_custom().is_none());
1636 let table = Table::new(vec![vec![paragraph]], vec![ColumnAlign::Center]);
1637 assert_eq!(table.rows().count(), 1);
1638 assert_eq!(table.column_aligns(), [ColumnAlign::Center]);
1639 }
1640}