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_markdown_extensions(mut self, extensions: Arc<MarkdownExtensions>) -> Self {
146 self.node.markdown_extensions = extensions;
147 self
148 }
149
150 pub fn with_body_font_size(mut self, size: Pixels) -> Self {
153 self.node.body_font_size = size;
154 self
155 }
156
157 pub fn style(&self) -> &TextViewStyle {
159 &self.node.style
160 }
161}
162
163#[derive(Clone, Copy, Debug)]
167pub struct FlowPosition {
168 is_first: bool,
169 is_last: bool,
170 prev: PrevBlock,
171}
172
173impl FlowPosition {
174 pub fn at(blocks: &[&BlockNode], ix: usize, is_last: bool) -> Self {
178 let (is_first, prev) = flow_position(blocks, ix);
179 Self {
180 is_first,
181 is_last,
182 prev,
183 }
184 }
185
186 pub fn first(mut self, is_first: bool) -> Self {
189 self.is_first = is_first;
190 self
191 }
192
193 pub fn last(mut self, is_last: bool) -> Self {
195 self.is_last = is_last;
196 self
197 }
198}
199
200impl BlockNode {
201 pub fn render_with_id(
205 &self,
206 id: impl Into<ElementId>,
207 flow: FlowPosition,
208 render_context: &RenderContext,
209 window: &mut Window,
210 cx: &mut App,
211 ) -> AnyElement {
212 div()
213 .id(id)
214 .child(self.render(0, flow, render_context, window, cx))
215 .into_any_element()
216 }
217
218 pub fn render(
221 &self,
222 ix: usize,
223 flow: FlowPosition,
224 cx: &RenderContext,
225 window: &mut Window,
226 app: &mut App,
227 ) -> AnyElement {
228 self.render_block(
229 NodeRenderOptions {
230 ix,
231 is_first: flow.is_first,
232 is_last: flow.is_last,
233 prev: flow.prev,
234 ..Default::default()
235 },
236 &cx.node,
237 window,
238 app,
239 )
240 }
241}
242
243pub fn collect_leaf_geometry<R>(
260 decorations: HashMap<u64, Vec<(Range<usize>, Hsla)>>,
261 f: impl FnOnce() -> R,
262) -> (R, HashMap<u64, LeafGeometry>) {
263 let collector = Rc::new(Collector {
264 pieces: RefCell::default(),
265 decorations,
266 });
267 let result = editor_seam::with_sink(collector.clone(), f);
268 let mut pieces = collector.pieces.take();
269 pieces.retain(|_, pieces| !pieces.is_empty());
270 let leaves = pieces
271 .into_iter()
272 .map(|(key, pieces)| (key, LeafGeometry { pieces }))
273 .collect();
274 (result, leaves)
275}
276
277struct Collector {
278 pieces: RefCell<HashMap<u64, Vec<Piece>>>,
279 decorations: HashMap<u64, Vec<(Range<usize>, Hsla)>>,
280}
281
282impl EditorSink for Collector {
283 fn report(&self, key: u64, layout: &TextLayout) {
284 self.pieces
285 .borrow_mut()
286 .insert(key, vec![Piece::Whole(LayoutSnapshot::new(layout))]);
287 }
288
289 fn begin_pieces(&self, key: u64) {
290 self.pieces.borrow_mut().insert(key, Vec::new());
291 }
292
293 fn report_text_piece(&self, key: u64, start: usize, layout: &TextLayout) {
294 self.pieces
295 .borrow_mut()
296 .entry(key)
297 .or_default()
298 .push(Piece::Text {
299 start,
300 layout: LayoutSnapshot::new(layout),
301 });
302 }
303
304 fn report_box_piece(&self, key: u64, range: Range<usize>, bounds: Bounds<Pixels>) {
305 self.pieces
306 .borrow_mut()
307 .entry(key)
308 .or_default()
309 .push(Piece::Atom { range, bounds });
310 }
311
312 fn report_empty_piece(&self, key: u64, start: usize, bounds: Bounds<Pixels>) {
313 self.pieces
314 .borrow_mut()
315 .entry(key)
316 .or_default()
317 .push(Piece::Empty { start, bounds });
318 }
319
320 fn backgrounds(&self, key: u64) -> Vec<(Range<usize>, Hsla)> {
321 self.decorations.get(&key).cloned().unwrap_or_default()
322 }
323}
324
325#[derive(Clone)]
327struct LayoutSnapshot {
328 bounds: Bounds<Pixels>,
329 len: usize,
330 line_height: Pixels,
331 text: SharedString,
332 lines: Vec<Arc<WrappedLineLayout>>,
333}
334
335impl LayoutSnapshot {
336 fn new(layout: &TextLayout) -> Self {
337 Self {
338 bounds: layout.bounds(),
339 len: layout.len(),
340 line_height: layout.line_height(),
341 text: layout.text().into(),
342 lines: layout.line_layouts().into_iter().collect(),
343 }
344 }
345
346 fn bounds(&self) -> Bounds<Pixels> {
347 self.bounds
348 }
349 fn len(&self) -> usize {
350 self.len
351 }
352 fn line_height(&self) -> Pixels {
353 self.line_height
354 }
355 fn text(&self) -> &str {
356 &self.text
357 }
358 fn line_layouts(&self) -> &[Arc<WrappedLineLayout>] {
359 &self.lines
360 }
361
362 fn position_for_index(&self, index: usize) -> Option<Point<Pixels>> {
363 let mut origin = self.bounds.origin;
364 let mut start = 0;
365 for line in &self.lines {
366 if index <= start + line.len() {
367 return line
368 .position_for_index(index - start, self.line_height)
369 .map(|p| origin + p);
370 }
371 start += line.len() + 1;
372 origin.y += line.size(self.line_height).height;
373 }
374 None
375 }
376}
377
378#[derive(Clone)]
382enum Piece {
383 Whole(LayoutSnapshot),
384 Text {
385 start: usize,
386 layout: LayoutSnapshot,
387 },
388 Empty {
389 start: usize,
390 bounds: Bounds<Pixels>,
391 },
392 Atom {
393 range: Range<usize>,
394 bounds: Bounds<Pixels>,
395 },
396}
397
398impl Piece {
399 fn bounds(&self) -> Bounds<Pixels> {
400 match self {
401 Piece::Whole(layout) | Piece::Text { layout, .. } => layout.bounds(),
402 Piece::Atom { bounds, .. } | Piece::Empty { bounds, .. } => *bounds,
403 }
404 }
405
406 fn range(&self) -> Range<usize> {
407 match self {
408 Piece::Whole(layout) => 0..layout.len(),
409 Piece::Text { start, layout } => *start..start + layout.len(),
410 Piece::Atom { range, .. } => range.clone(),
411 Piece::Empty { start, .. } => *start..*start,
412 }
413 }
414}
415
416#[derive(Clone)]
425pub struct LeafGeometry {
426 pieces: Vec<Piece>,
427}
428
429impl LeafGeometry {
430 fn whole(&self) -> Option<&LayoutSnapshot> {
431 match self.pieces.as_slice() {
432 [Piece::Whole(layout)] => Some(layout),
433 _ => None,
434 }
435 }
436
437 pub fn bounds(&self) -> Bounds<Pixels> {
439 self.pieces
440 .iter()
441 .map(Piece::bounds)
442 .reduce(|a, b| a.union(&b))
443 .unwrap_or_default()
444 }
445
446 pub fn len(&self) -> usize {
448 self.pieces
449 .iter()
450 .map(|piece| piece.range().end)
451 .max()
452 .unwrap_or(0)
453 }
454
455 pub fn is_empty(&self) -> bool {
457 self.len() == 0
458 }
459
460 pub fn line_height(&self) -> Pixels {
462 self.pieces
463 .iter()
464 .find_map(|piece| match piece {
465 Piece::Whole(layout) | Piece::Text { layout, .. } => Some(layout.line_height()),
466 Piece::Empty { bounds, .. } => Some(bounds.size.height),
467 Piece::Atom { .. } => None,
468 })
469 .unwrap_or_else(|| self.bounds().size.height)
470 }
471
472 pub fn caret_height(&self) -> Pixels {
475 let line_height = self.line_height();
476 self.pieces
477 .iter()
478 .find_map(|piece| match piece {
479 Piece::Whole(layout) | Piece::Text { layout, .. } => {
480 let line = layout.lines.first()?;
481 let h = line.unwrapped_layout.ascent + line.unwrapped_layout.descent;
482 (h > Pixels::ZERO).then_some(h)
483 }
484 Piece::Atom { .. } | Piece::Empty { .. } => None,
485 })
486 .unwrap_or(line_height * 0.85)
487 .min(line_height)
488 }
489
490 pub fn position_for_index(&self, index: usize) -> Point<Pixels> {
492 if let Some(layout) = self.whole() {
493 return layout
494 .position_for_index(index.min(layout.len()))
495 .unwrap_or_else(|| self.bounds().origin);
496 }
497 let piece = self
500 .pieces
501 .iter()
502 .find(|p| p.range().contains(&index))
503 .or_else(|| self.pieces.iter().rev().find(|p| p.range().end == index))
504 .or(self.pieces.last());
505 let Some(piece) = piece else {
506 return self.bounds().origin;
507 };
508 match piece {
509 Piece::Whole(layout) | Piece::Text { layout, .. } => {
510 let local = index.saturating_sub(piece.range().start).min(layout.len());
511 layout
512 .position_for_index(local)
513 .unwrap_or_else(|| layout.bounds().origin)
514 }
515 Piece::Empty { bounds, .. } => bounds.origin,
516 Piece::Atom { range, bounds } => {
517 let x = if index >= range.end {
518 bounds.right()
519 } else {
520 bounds.left()
521 };
522 let y = (bounds.bottom() - self.line_height()).max(bounds.top());
525 point(x, y)
526 }
527 }
528 }
529
530 pub fn index_for_position(&self, at: Point<Pixels>) -> usize {
533 if let Some(layout) = self.whole() {
534 return whole_index_at(layout, at);
535 }
536 let dy = |p: &Piece| {
539 let b = p.bounds();
540 if at.y < b.top() {
541 b.top() - at.y
542 } else if at.y > b.bottom() {
543 at.y - b.bottom()
544 } else {
545 px(0.)
546 }
547 };
548 let dx = |p: &Piece| {
549 let b = p.bounds();
550 if at.x < b.left() {
551 b.left() - at.x
552 } else if at.x > b.right() {
553 at.x - b.right()
554 } else {
555 px(0.)
556 }
557 };
558 let Some(piece) = self.pieces.iter().min_by(|a, b| {
559 (dy(a), dx(a))
560 .partial_cmp(&(dy(b), dx(b)))
561 .unwrap_or(std::cmp::Ordering::Equal)
562 }) else {
563 return 0;
564 };
565 let index = match piece {
566 Piece::Whole(layout) => whole_index_at(layout, at),
567 Piece::Text { start, layout } => {
574 let text = layout.text();
575 let nearest = text
576 .grapheme_indices(true)
577 .map(|(i, _)| i)
578 .chain(std::iter::once(text.len()))
579 .filter_map(|i| {
580 layout
581 .position_for_index(i)
582 .map(|p| (i, (p.x - at.x).abs()))
583 })
584 .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
585 .map_or(0, |(i, _)| i);
586 start + nearest
587 }
588 Piece::Empty { start, .. } => *start,
589 Piece::Atom { range, bounds } => {
590 if at.x < bounds.center().x {
591 range.start
592 } else {
593 range.end
594 }
595 }
596 };
597 index.min(self.len())
598 }
599
600 pub fn atom_at(&self, at: Point<Pixels>) -> Option<Range<usize>> {
602 self.pieces.iter().find_map(|piece| match piece {
603 Piece::Atom { range, bounds } if bounds.contains(&at) => Some(range.clone()),
604 _ => None,
605 })
606 }
607}
608
609fn whole_index_at(layout: &LayoutSnapshot, at: Point<Pixels>) -> usize {
611 let bounds = layout.bounds();
612 let line_height = layout.line_height();
613 let mut top = bounds.top();
614 let mut start = 0;
615 let lines = layout.line_layouts();
616 for (ix, line) in lines.iter().enumerate() {
617 let height = line.size(line_height).height.max(line_height);
618 let is_last = ix + 1 == lines.len();
619 if at.y < top + height || is_last {
620 let local = point(
621 (at.x - bounds.left()).max(px(0.)),
622 (at.y - top).clamp(px(0.), (height - px(1.)).max(px(0.))),
623 );
624 let index = match line.closest_index_for_position(local, line_height) {
625 Ok(ix) | Err(ix) => ix,
626 };
627 return start + index;
628 }
629 top += height;
630 start += line.len() + 1;
631 }
632 0
633}
634
635#[cfg(test)]
636mod tests {
637 use std::cell::Cell;
638
639 use gpui::{
640 Context, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, LayoutId,
641 Render, Styled as _, TestAppContext, VisualTestContext, div, rgb, rgba,
642 };
643
644 use super::*;
645 use crate::text::{InlineElement, MarkdownNode, markdown_ext::TestInlinePlugin};
646
647 type Geometries = Rc<RefCell<HashMap<u64, LeafGeometry>>>;
648
649 struct Probe {
652 child: Option<AnyElement>,
653 decorations: HashMap<u64, Vec<(Range<usize>, Hsla)>>,
654 out: Geometries,
655 sink: Option<Rc<dyn EditorSink>>,
657 }
658
659 impl IntoElement for Probe {
660 type Element = Self;
661 fn into_element(self) -> Self::Element {
662 self
663 }
664 }
665
666 impl Element for Probe {
667 type RequestLayoutState = AnyElement;
668 type PrepaintState = ();
669
670 fn id(&self) -> Option<ElementId> {
671 None
672 }
673
674 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
675 None
676 }
677
678 fn request_layout(
679 &mut self,
680 _: Option<&GlobalElementId>,
681 _: Option<&InspectorElementId>,
682 window: &mut Window,
683 cx: &mut App,
684 ) -> (LayoutId, AnyElement) {
685 let mut child = self.child.take().unwrap();
686 (child.request_layout(window, cx), child)
687 }
688
689 fn prepaint(
690 &mut self,
691 _: Option<&GlobalElementId>,
692 _: Option<&InspectorElementId>,
693 _: Bounds<Pixels>,
694 child: &mut AnyElement,
695 window: &mut Window,
696 cx: &mut App,
697 ) {
698 if let Some(sink) = self.sink.take() {
699 editor_seam::with_sink(sink, || child.prepaint(window, cx));
700 return;
701 }
702 let decorations = std::mem::take(&mut self.decorations);
703 let (_, geometries) = collect_leaf_geometry(decorations, || child.prepaint(window, cx));
704 *self.out.borrow_mut() = geometries;
705 }
706
707 fn paint(
708 &mut self,
709 _: Option<&GlobalElementId>,
710 _: Option<&InspectorElementId>,
711 _: Bounds<Pixels>,
712 child: &mut AnyElement,
713 _: &mut (),
714 window: &mut Window,
715 cx: &mut App,
716 ) {
717 child.paint(window, cx);
718 }
719 }
720
721 struct Host {
722 blocks: Vec<BlockNode>,
723 extensions: Arc<MarkdownExtensions>,
724 decorations: HashMap<u64, Vec<(Range<usize>, Hsla)>>,
725 out: Geometries,
726 width: Pixels,
727 sink: Option<Rc<dyn EditorSink>>,
728 }
729
730 impl Render for Host {
731 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
732 let rcx = RenderContext::from_app(cx)
733 .with_markdown_extensions(self.extensions.clone())
734 .with_body_font_size(px(16.));
735 let refs: Vec<&BlockNode> = self.blocks.iter().collect();
736 let children: Vec<AnyElement> = refs
737 .iter()
738 .enumerate()
739 .map(|(ix, block)| {
740 let flow = FlowPosition::at(&refs, ix, ix + 1 == refs.len());
741 block.render(ix, flow, &rcx, window, cx)
742 })
743 .collect();
744 Probe {
745 child: Some(div().w(self.width).children(children).into_any_element()),
746 decorations: self.decorations.clone(),
747 out: self.out.clone(),
748 sink: self.sink.clone(),
749 }
750 }
751 }
752
753 fn draw(
754 cx: &mut TestAppContext,
755 blocks: Vec<BlockNode>,
756 decorations: HashMap<u64, Vec<(Range<usize>, Hsla)>>,
757 extensions: Arc<MarkdownExtensions>,
758 width: Pixels,
759 ) -> HashMap<u64, LeafGeometry> {
760 draw_and_inspect(cx, blocks, decorations, extensions, width, |_| ()).0
761 }
762
763 fn draw_and_inspect<R>(
764 cx: &mut TestAppContext,
765 blocks: Vec<BlockNode>,
766 decorations: HashMap<u64, Vec<(Range<usize>, Hsla)>>,
767 extensions: Arc<MarkdownExtensions>,
768 width: Pixels,
769 inspect: impl FnOnce(&mut Window) -> R,
770 ) -> (HashMap<u64, LeafGeometry>, R) {
771 cx.update(crate::init);
772 let out = Geometries::default();
773 let (_, vcx) = cx.add_window_view({
774 let out = out.clone();
775 move |_, _| Host {
776 blocks,
777 extensions,
778 decorations,
779 out,
780 width,
781 sink: None,
782 }
783 });
784 settle(vcx);
785 let inspected = vcx.update(|window, _| inspect(window));
786 (out.take(), inspected)
787 }
788
789 fn settle(cx: &mut VisualTestContext) {
790 cx.run_until_parked();
791 cx.update(|window, cx| window.draw(cx).clear(cx));
792 cx.run_until_parked();
793 }
794
795 fn plain(text: &str) -> Paragraph {
796 let mut paragraph = Paragraph::default();
797 paragraph.push(InlineNode::new(text.to_string()));
798 paragraph
799 }
800
801 fn atom_plugin() -> Arc<MarkdownExtensions> {
802 Arc::new(
803 MarkdownExtensions::default().plugin(TestInlinePlugin::new("atom").render_with(
804 |_, _, _, _| {
805 Some(InlineElement::new(div().w(px(24.)).h(px(16.))).with_baseline(px(12.)))
806 },
807 )),
808 )
809 }
810
811 fn atom() -> InlineNode {
812 InlineNode::custom(MarkdownNode::new("atom", ()).text("\u{FFFC}"))
813 }
814
815 #[gpui::test]
816 fn keyed_leaves_report_geometry_and_unkeyed_leaves_do_not(cx: &mut TestAppContext) {
817 let blocks = vec![
818 BlockNode::Heading {
819 level: 1,
820 children: plain("Title").with_leaf_key(1),
821 span: None,
822 },
823 BlockNode::Paragraph(plain("first").with_leaf_key(2)),
824 BlockNode::Paragraph(plain("not an editor leaf")),
825 BlockNode::CodeBlock(
826 CodeBlock::new("let x = 1;".into(), Some("rust".into()), None::<Span>)
827 .with_leaf_key(3),
828 ),
829 BlockNode::Table(Table::new(
830 vec![
831 vec![plain("a").with_leaf_key(4), plain("b").with_leaf_key(5)],
832 vec![plain("c").with_leaf_key(6), plain("d").with_leaf_key(7)],
833 ],
834 vec![ColumnAlign::None, ColumnAlign::Right],
835 )),
836 BlockNode::List {
837 children: vec![BlockNode::ListItem {
838 children: vec![BlockNode::Paragraph(plain("item").with_leaf_key(8))],
839 spread: false,
840 checked: None,
841 span: None,
842 }],
843 ordered: false,
844 start: None,
845 span: None,
846 },
847 ];
848 let geometries = draw(cx, blocks, HashMap::new(), Default::default(), px(300.));
849 let mut keys: Vec<u64> = geometries.keys().copied().collect();
850 keys.sort_unstable();
851 assert_eq!(keys, (1..=8).collect::<Vec<_>>());
852 for key in [1, 2, 3, 8] {
853 assert!(geometries[&key].bounds().size.height > px(0.), "leaf {key}");
854 }
855 assert_eq!(geometries[&1].len(), "Title".len());
856 assert_eq!(geometries[&3].len(), "let x = 1;".len());
857 let top = |key: u64| geometries[&key].bounds().top();
859 assert!(top(1) < top(2) && top(2) < top(3) && top(3) < top(4) && top(7) < top(8));
860 assert_eq!(top(4), top(5));
862 assert!(geometries[&5].bounds().left() > geometries[&4].bounds().right() - px(1.));
863 }
864
865 #[gpui::test]
866 fn wrapped_leaf_maps_indices_and_positions(cx: &mut TestAppContext) {
867 let text = "the quick brown fox jumps over the lazy dog and keeps running far away";
868 let blocks = vec![BlockNode::Paragraph(plain(text).with_leaf_key(1))];
869 let geometries = draw(cx, blocks, HashMap::new(), Default::default(), px(120.));
870 let leaf = &geometries[&1];
871 assert_eq!(leaf.len(), text.len());
872 assert!(!leaf.is_empty());
873 let line = leaf.line_height();
874 assert!(line > px(0.) && leaf.caret_height() <= line);
875 let mut last_y = px(-1.);
876 let mut lines = std::collections::BTreeSet::new();
877 for index in 0..=text.len() {
878 let caret = leaf.position_for_index(index);
879 assert!(caret.y >= last_y, "carets never move up");
880 last_y = caret.y;
881 lines.insert((caret.y.as_f32() * 100.) as i64);
882 let hit = leaf.index_for_position(point(caret.x, caret.y + line / 2.));
885 assert_eq!(
886 leaf.position_for_index(hit),
887 caret,
888 "index {index} hit {hit}"
889 );
890 }
891 assert!(lines.len() >= 3, "the text wraps onto several lines");
892 assert_eq!(leaf.index_for_position(point(px(-500.), px(-500.))), 0);
894 assert_eq!(
895 leaf.index_for_position(point(px(5000.), px(5000.))),
896 text.len()
897 );
898 }
899
900 #[gpui::test]
901 fn mixed_text_and_atoms_report_boxes(cx: &mut TestAppContext) {
902 let mut paragraph = Paragraph::default();
903 paragraph.push(InlineNode::new("ab"));
904 paragraph.push(atom());
905 paragraph.push(InlineNode::new("cd"));
906 let atom_len = '\u{FFFC}'.len_utf8();
907 let blocks = vec![BlockNode::Paragraph(paragraph.with_leaf_key(1))];
908 let geometries = draw(cx, blocks, HashMap::new(), atom_plugin(), px(300.));
909 let leaf = &geometries[&1];
910 assert_eq!(leaf.len(), 2 + atom_len + 2);
911 let range = 2..2 + atom_len;
913 let before = leaf.position_for_index(2);
914 let after = leaf.position_for_index(2 + atom_len);
915 assert!(after.x > before.x, "the caret moves across the atom");
916 let inside = point(
917 (before.x + after.x) / 2.,
918 before.y + leaf.line_height() / 2.,
919 );
920 assert_eq!(leaf.atom_at(inside), Some(range.clone()));
921 assert_eq!(leaf.atom_at(point(before.x - px(8.), inside.y)), None);
922 let quarter = |t: f32| point(before.x + (after.x - before.x) * t, inside.y);
924 assert_eq!(leaf.index_for_position(quarter(0.25)), range.start);
925 assert_eq!(leaf.index_for_position(quarter(0.75)), range.end);
926 let end = leaf.position_for_index(leaf.len());
928 assert_eq!(
929 leaf.index_for_position(point(end.x + px(40.), inside.y)),
930 leaf.len()
931 );
932 assert_eq!(leaf.index_for_position(point(px(-40.), inside.y)), 0);
933 }
934
935 #[gpui::test]
936 fn mixed_flow_hits_land_on_grapheme_boundaries(cx: &mut TestAppContext) {
937 let parts = ["éa", "\u{FFFC}", "中文", "\u{FFFC}", "👍🏽x"];
940 let mut paragraph = Paragraph::default();
941 for part in parts {
942 if part == "\u{FFFC}" {
943 paragraph.push(atom());
944 } else {
945 paragraph.push(InlineNode::new(part));
946 }
947 }
948 let text = parts.concat();
949 let blocks = vec![BlockNode::Paragraph(paragraph.with_leaf_key(1))];
950 let geometries = draw(cx, blocks, HashMap::new(), atom_plugin(), px(400.));
951 let leaf = &geometries[&1];
952 assert_eq!(leaf.len(), text.len());
953 let boundaries: Vec<usize> = text
954 .grapheme_indices(true)
955 .map(|(i, _)| i)
956 .chain(std::iter::once(text.len()))
957 .collect();
958
959 let bounds = leaf.bounds();
962 let y = bounds.top() + leaf.line_height() / 2.;
963 let mut x = bounds.left() - px(4.);
964 while x <= bounds.right() + px(4.) {
965 let index = leaf.index_for_position(point(x, y));
966 assert!(
967 boundaries.contains(&index),
968 "hit at {x:?} returned {index}, inside a grapheme of {text:?}"
969 );
970 let mut edited = text.clone();
971 edited.insert(index, '|');
972 x += px(0.5);
973 }
974
975 for &index in &boundaries {
978 let at = leaf.position_for_index(index);
979 assert_eq!(
980 leaf.index_for_position(point(at.x, y)),
981 index,
982 "caret position of {index} in {text:?}"
983 );
984 }
985 }
986
987 #[gpui::test]
988 fn mixed_flow_keeps_empty_lines_and_trailing_breaks(cx: &mut TestAppContext) {
989 let mut paragraph = Paragraph::default();
990 paragraph.push(atom());
991 paragraph.push(InlineNode::new("a\n\n"));
992 let mut breaks = Paragraph::default();
993 breaks.push(atom());
994 breaks.push(InlineNode::new("\n\n"));
995 let leaves = draw(
996 cx,
997 vec![
998 BlockNode::Paragraph(paragraph.with_leaf_key(1)),
999 BlockNode::Paragraph(breaks.with_leaf_key(2)),
1000 ],
1001 HashMap::new(),
1002 atom_plugin(),
1003 px(300.),
1004 );
1005 let leaf = &leaves[&1];
1006 assert_eq!(leaf.len(), 6);
1007 let a = leaf.position_for_index(3);
1008 let empty = leaf.position_for_index(5);
1009 let end = leaf.position_for_index(6);
1010 assert!(a.y < empty.y && empty.y < end.y);
1011 for index in [5, 6] {
1012 let caret = leaf.position_for_index(index);
1013 assert_eq!(
1014 leaf.index_for_position(point(
1015 caret.x + px(20.),
1016 caret.y + leaf.line_height() / 2.
1017 )),
1018 index
1019 );
1020 }
1021 assert!(leaf.bounds().bottom() >= end.y + leaf.line_height());
1022 let breaks = &leaves[&2];
1023 assert_eq!(breaks.len(), 5);
1024 assert!(breaks.line_height() < breaks.bounds().size.height);
1025 assert!(breaks.position_for_index(5).y > breaks.position_for_index(4).y);
1026 }
1027
1028 #[gpui::test]
1029 fn atom_decorations_are_painted_for_custom_nodes_and_images(cx: &mut TestAppContext) {
1030 let color: Hsla = rgba(0x12345680).into();
1031 let mut custom = Paragraph::default();
1032 custom.push(atom());
1033 let mut image = Paragraph::default();
1034 image.push(InlineNode::image(ImageNode {
1035 url: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20'/>".into(),
1036 width: Some(px(20.).into()), height: Some(px(20.).into()),
1037 ..Default::default()
1038 }));
1039 let blocks = vec![
1040 BlockNode::Paragraph(custom.with_leaf_key(1)),
1041 BlockNode::Paragraph(image.with_leaf_key(2)),
1042 ];
1043 let decorations = HashMap::from([
1044 (1, vec![(0..3, color), (1..1, color)]),
1045 (2, vec![(0..3, color), (3..6, color)]),
1046 ]);
1047 let (_, painted) =
1048 draw_and_inspect(cx, blocks, decorations, atom_plugin(), px(300.), |window| {
1049 let background: gpui::Background = color.into();
1050 window
1051 .painted_quads()
1052 .into_iter()
1053 .filter(|quad| quad.background == background)
1054 .count()
1055 });
1056 assert_eq!(
1057 painted, 2,
1058 "one overlay per atom; empty and adjacent ranges paint nothing"
1059 );
1060 }
1061
1062 #[gpui::test]
1063 fn rekeying_cloned_leaves_preserves_each_identity(cx: &mut TestAppContext) {
1064 let original = plain("shared source").with_leaf_key(1);
1065 let copy = original.clone().with_leaf_key(2);
1066 let code = CodeBlock::new("let x = 1;".into(), None, None::<Span>).with_leaf_key(3);
1067 let copy_code = code.clone().with_leaf_key(4);
1068 let leaves = draw(
1069 cx,
1070 vec![
1071 BlockNode::Paragraph(original),
1072 BlockNode::Paragraph(copy),
1073 BlockNode::CodeBlock(code),
1074 BlockNode::CodeBlock(copy_code),
1075 ],
1076 HashMap::new(),
1077 Default::default(),
1078 px(300.),
1079 );
1080 assert_eq!(leaves.len(), 4);
1081 assert_eq!(leaves[&1].len(), "shared source".len());
1082 assert_eq!(leaves[&3].len(), "let x = 1;".len());
1083 assert!(leaves[&1].bounds().top() < leaves[&2].bounds().top());
1084 assert!(leaves[&3].bounds().top() < leaves[&4].bounds().top());
1085 }
1086
1087 #[gpui::test]
1088 fn geometry_is_an_immutable_snapshot(cx: &mut TestAppContext) {
1089 cx.update(crate::init);
1090 let out = Geometries::default();
1091 let (host, vcx) = cx.add_window_view({
1092 let out = out.clone();
1093 move |_, _| Host {
1094 blocks: vec![BlockNode::Paragraph(
1095 plain("a long sentence that wraps when the viewport gets narrower")
1096 .with_leaf_key(1),
1097 )],
1098 extensions: Default::default(),
1099 decorations: HashMap::new(),
1100 out,
1101 width: px(600.),
1102 sink: None,
1103 }
1104 });
1105 settle(vcx);
1106 let old = out.borrow()[&1].clone();
1107 let before = old.bounds();
1108 vcx.update(|_, cx| {
1109 host.update(cx, |host, cx| {
1110 host.width = px(80.);
1111 cx.notify();
1112 })
1113 });
1114 settle(vcx);
1115 assert!(out.borrow()[&1].bounds().size.height > before.size.height);
1116 assert_eq!(old.bounds(), before);
1117 assert_eq!(old.position_for_index(0), before.origin);
1118 }
1119
1120 #[gpui::test]
1121 fn empty_keyed_leaf_has_a_line_box_and_empty_unkeyed_leaf_stays_empty(cx: &mut TestAppContext) {
1122 let blocks = vec![
1123 BlockNode::Paragraph(Paragraph::default()),
1124 BlockNode::Paragraph(Paragraph::default().with_leaf_key(9)),
1125 ];
1126 let geometries = draw(cx, blocks, HashMap::new(), Default::default(), px(300.));
1127 assert_eq!(geometries.len(), 1);
1128 let leaf = &geometries[&9];
1129 assert!(leaf.is_empty());
1130 assert!(leaf.bounds().size.height > px(0.));
1131 assert_eq!(leaf.position_for_index(0).x, leaf.bounds().left());
1132 assert_eq!(
1133 leaf.index_for_position(point(px(50.), leaf.bounds().top())),
1134 0
1135 );
1136 }
1137
1138 #[gpui::test]
1139 fn image_only_leaf_reports_its_box(cx: &mut TestAppContext) {
1140 let image = ImageNode {
1141 url: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20'/>".into(),
1142 width: Some(px(20.).into()),
1143 height: Some(px(20.).into()),
1144 ..Default::default()
1145 };
1146 let mut paragraph = Paragraph::default();
1147 paragraph.push(InlineNode::image(image));
1148 let blocks = vec![BlockNode::Paragraph(paragraph.with_leaf_key(1))];
1149 let geometries = draw(cx, blocks, HashMap::new(), Default::default(), px(300.));
1150 let leaf = &geometries[&1];
1151 let atom_len = '\u{FFFC}'.len_utf8();
1152 assert_eq!(leaf.len(), atom_len);
1153 let inside = leaf.bounds().center();
1154 assert_eq!(leaf.atom_at(inside), Some(0..atom_len));
1155 }
1156
1157 #[derive(Default)]
1160 struct Recorder {
1161 asked: RefCell<Vec<u64>>,
1162 }
1163
1164 impl EditorSink for Recorder {
1165 fn report(&self, _: u64, _: &TextLayout) {}
1166 fn begin_pieces(&self, _: u64) {}
1167 fn report_text_piece(&self, _: u64, _: usize, _: &TextLayout) {}
1168 fn report_box_piece(&self, _: u64, _: Range<usize>, _: Bounds<Pixels>) {}
1169 fn report_empty_piece(&self, _: u64, _: usize, _: Bounds<Pixels>) {}
1170 fn backgrounds(&self, key: u64) -> Vec<(Range<usize>, Hsla)> {
1171 self.asked.borrow_mut().push(key);
1172 vec![(0..1, rgb(0xff0000).into())]
1173 }
1174 }
1175
1176 #[gpui::test]
1177 fn decorations_are_requested_per_keyed_leaf(cx: &mut TestAppContext) {
1178 cx.update(crate::init);
1179 let recorder = Rc::new(Recorder::default());
1180 let mut mixed = Paragraph::default();
1181 mixed.push(InlineNode::new("ab"));
1182 mixed.push(atom());
1183 let blocks = vec![
1184 BlockNode::Paragraph(plain("one").with_leaf_key(1)),
1185 BlockNode::Paragraph(plain("unkeyed")),
1186 BlockNode::Paragraph(mixed.with_leaf_key(2)),
1187 ];
1188 let (_, vcx) = cx.add_window_view({
1189 let sink: Rc<dyn EditorSink> = recorder.clone();
1190 move |_, _| Host {
1191 blocks,
1192 extensions: atom_plugin(),
1193 decorations: HashMap::new(),
1194 out: Geometries::default(),
1195 width: px(300.),
1196 sink: Some(sink),
1197 }
1198 });
1199 settle(vcx);
1200 let asked = recorder.asked.borrow();
1201 assert!(asked.contains(&1) && asked.contains(&2), "{asked:?}");
1202 assert!(asked.iter().all(|key| *key == 1 || *key == 2), "{asked:?}");
1203 }
1204
1205 #[gpui::test]
1206 fn decorated_leaves_still_report_geometry(cx: &mut TestAppContext) {
1207 let blocks = vec![BlockNode::Paragraph(plain("hello world").with_leaf_key(1))];
1208 let decorations = HashMap::from([(
1209 1,
1210 vec![
1211 (0..5, rgba(0x3366ff80).into()),
1212 (6..11, rgb(0xffcc00).into()),
1213 ],
1214 )]);
1215 let plain_geometry = draw(
1216 cx,
1217 blocks.clone(),
1218 HashMap::new(),
1219 Default::default(),
1220 px(300.),
1221 );
1222 let decorated = draw(cx, blocks, decorations, Default::default(), px(300.));
1223 assert_eq!(
1224 decorated[&1].bounds().size,
1225 plain_geometry[&1].bounds().size,
1226 "decorations paint behind the text and never change layout"
1227 );
1228 }
1229
1230 #[test]
1231 fn collect_leaf_geometry_nests_and_restores_on_panic() {
1232 let seen = Cell::new(0);
1233 let (inner_ran, geometries) = collect_leaf_geometry(HashMap::new(), || {
1234 assert!(editor_seam::current().is_some());
1235 let outer = editor_seam::current().unwrap();
1236 collect_leaf_geometry(HashMap::new(), || seen.set(seen.get() + 1));
1237 let after = editor_seam::current().unwrap();
1238 assert!(Rc::ptr_eq(&outer, &after), "the outer scope is restored");
1239 true
1240 });
1241 assert!(inner_ran && geometries.is_empty() && seen.get() == 1);
1242 assert!(editor_seam::current().is_none());
1243
1244 let result = std::panic::catch_unwind(|| {
1245 collect_leaf_geometry(HashMap::new(), || panic!("boom"));
1246 });
1247 assert!(result.is_err());
1248 assert!(editor_seam::current().is_none(), "cleared after a panic");
1249 }
1250
1251 #[test]
1252 fn parse_markdown_reads_what_an_editor_round_trips() {
1253 let source = "```rust title=\"x\"\nfn main() {}\n```\n\n\
1254 | a | b | c | d |\n|---|:--|:-:|--:|\n| 1 | 2 | 3 | 4 |\n\n\
1255 [ref]: https://example.com \"Title\"\n";
1256 let blocks = parse_markdown(source, Default::default()).unwrap();
1257 let code = blocks
1258 .iter()
1259 .find_map(|block| match block {
1260 BlockNode::CodeBlock(code) => Some(code),
1261 _ => None,
1262 })
1263 .expect("code block");
1264 assert_eq!(code.lang().as_deref(), Some("rust"));
1265 assert_eq!(code.meta().as_deref(), Some("title=\"x\""));
1266 let table = blocks
1267 .iter()
1268 .find_map(|block| match block {
1269 BlockNode::Table(table) => Some(table),
1270 _ => None,
1271 })
1272 .expect("table");
1273 assert_eq!(
1274 table.column_aligns(),
1275 [
1276 ColumnAlign::None,
1277 ColumnAlign::Left,
1278 ColumnAlign::Center,
1279 ColumnAlign::Right
1280 ]
1281 );
1282 let cells: Vec<Vec<String>> = table
1283 .rows()
1284 .map(|row| {
1285 row.map(|cell| cell.children().iter().map(|c| c.text()).collect())
1286 .collect()
1287 })
1288 .collect();
1289 assert_eq!(cells, [["a", "b", "c", "d"], ["1", "2", "3", "4"]]);
1290 assert!(blocks.iter().any(|block| matches!(
1291 block,
1292 BlockNode::Definition { identifier, .. } if identifier.as_ref() == "ref"
1293 )));
1294 assert!(
1295 blocks
1296 .iter()
1297 .all(|block| block.span().is_some() || matches!(block, BlockNode::Unknown))
1298 );
1299 }
1300
1301 #[test]
1302 fn parse_markdown_matches_text_view_parse() {
1303 let source = "# Title\n\nSome **bold** and `code` and ==mark==.\n\n- one\n- two\n";
1304 let mut cx = NodeContext::default();
1305 let expected = markdown::parse(source, &mut cx).unwrap();
1306 let blocks = parse_markdown(source, Default::default()).unwrap();
1307 assert_eq!(blocks, *expected.blocks);
1308 }
1309
1310 #[test]
1311 fn table_and_paragraph_builders_read_back() {
1312 let mut paragraph = Paragraph::default();
1313 paragraph.push(InlineNode::new("hi").marks(vec![(0..2, TextMark::default().bold())]));
1314 assert_eq!(paragraph.children()[0].text(), "hi");
1315 assert!(paragraph.children()[0].mark_runs()[0].1.bold);
1316 assert!(paragraph.children()[0].as_image().is_none());
1317 assert!(paragraph.children()[0].as_custom().is_none());
1318 let table = Table::new(vec![vec![paragraph]], vec![ColumnAlign::Center]);
1319 assert_eq!(table.rows().count(), 1);
1320 assert_eq!(table.column_aligns(), [ColumnAlign::Center]);
1321 }
1322}