Skip to main content

gpui_base/text/
editor_host.rs

1//! Host API for external editors.
2//!
3//! An editor that lives outside gpui-kit keeps its own document model and
4//! hands gpui-kit a [`BlockNode`] tree, the same tree [`TextView`] renders.
5//! gpui-kit renders it and gives back, per text leaf, the geometry the editor
6//! needs for hit-testing, caret placement and platform input (IME) bounds.
7//!
8//! The boundary is deliberately narrow:
9//!
10//! - **In:** a [`BlockNode`] tree whose text leaves carry a host-chosen `u64`
11//!   key (see [`Paragraph::with_leaf_key`] and [`CodeBlock::with_leaf_key`]),
12//!   a [`RenderContext`], and per-key decoration ranges.
13//! - **Out:** one opaque [`LeafGeometry`] per key from
14//!   [`collect_leaf_geometry`].
15//!
16//! Nothing lower is exposed: no `TextLayout`, no layout internals, no sink
17//! trait, no retained-layout or projection caches.
18//!
19//! The host owns editing semantics, document revisions, accessibility and native
20//! input integration. This module provides horizontal TextView block rendering;
21//! it is not a complete visual-caret navigation or pagination API. Extend the
22//! public boundary additively while keeping rendering internals private.
23//!
24//! [`TextView`]: super::TextView
25
26use 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
49/// Parses Markdown with the parser [`TextView`](super::TextView) uses,
50/// including the given extensions, into the block tree.
51pub 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/// What [`BlockNode::render`] needs besides the node: the text style, code
64/// highlighter and Markdown extensions.
65#[derive(Clone)]
66pub struct RenderContext {
67    node: NodeContext,
68}
69
70impl RenderContext {
71    /// Creates a local rendering context without reading application globals.
72    /// The host supplies body typography through its container or
73    /// `with_body_font_size`.
74    pub fn new(style: TextViewStyle) -> Self {
75        Self {
76            node: NodeContext {
77                style: Arc::new(style),
78                ..Default::default()
79            },
80        }
81    }
82
83    /// Replaces code highlighting for this context only.
84    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    /// Resolves document image URIs. The host owns resource access policy.
93    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    /// Handles link activation. Without a handler, links use App::open_url.
102    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    /// Registers a reference-link definition from the host's document.
111    /// Supply definitions for the entire document, including virtualized blocks.
112    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    /// The context TextView would start from: the global [`TextViewDefaults`]
122    /// style and code-block highlighter when set, else the theme's style.
123    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    /// Replaces the text style.
139    pub fn with_style(mut self, style: TextViewStyle) -> Self {
140        self.node.style = Arc::new(style);
141        self
142    }
143
144    /// Supplies this render's foreground ranges for a keyed paragraph leaf,
145    /// including a heading or table cell. Replaces any ranges for the same key.
146    ///
147    /// Ranges use the leaf's rendered UTF-8 bytes; images and custom inline
148    /// objects occupy one U+FFFC (three bytes), as in [`LeafGeometry`]. Text
149    /// ranges are clipped to each text fragment; non-character boundaries and
150    /// empty or reversed ranges are ignored. An object's foreground changes
151    /// only when a range covers its entire placeholder; images are unchanged.
152    ///
153    /// These colors override mark and link foregrounds; later ranges win
154    /// overlaps. Other mark properties and link activation are preserved.
155    /// Uncovered text inherits its ordinary foreground. Colors belong to this
156    /// rendering context, never the document tree: supply current theme/surface
157    /// colors on each render, or omit the ranges to restore inheritance.
158    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    /// Sets the Markdown extensions that render custom nodes.
165    pub fn with_markdown_extensions(mut self, extensions: Arc<MarkdownExtensions>) -> Self {
166        self.node.markdown_extensions = extensions;
167        self
168    }
169
170    /// Sets the font size body text inherits, which the block gaps and
171    /// heading sizes are relative to.
172    pub fn with_body_font_size(mut self, size: Pixels) -> Self {
173        self.node.body_font_size = size;
174        self
175    }
176
177    /// The text style blocks render with.
178    pub fn style(&self) -> &TextViewStyle {
179        &self.node.style
180    }
181}
182
183/// Where a block stands in the flow it is rendered in, which decides the gaps
184/// around it: the first block takes no gap above, the last none below, and a
185/// heading or rule counts the block before it.
186#[derive(Clone, Copy, Debug)]
187pub struct FlowPosition {
188    is_first: bool,
189    is_last: bool,
190    prev: PrevBlock,
191}
192
193impl FlowPosition {
194    /// The position of `blocks[ix]` among its siblings. Blocks that render
195    /// nothing (definitions, breaks) are skipped when looking for the block
196    /// before it.
197    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    /// Overrides whether the block opens its flow, e.g. for a chunk of a
207    /// long list rendered as a list of its own.
208    pub fn first(mut self, is_first: bool) -> Self {
209        self.is_first = is_first;
210        self
211    }
212
213    /// Overrides whether the block closes its flow.
214    pub fn last(mut self, is_last: bool) -> Self {
215        self.is_last = is_last;
216        self
217    }
218}
219
220impl BlockNode {
221    /// Renders under a stable host-owned identity. Use a domain ID, not a list
222    /// position, when inserting, reordering or virtualizing blocks. Identity
223    /// scopes GPUI element state independently of the leaf keys used for geometry.
224    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    /// Renders the block as TextView does. `ix` identifies the block among
239    /// its siblings (element ids derive from it).
240    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
263/// Runs `f`, which should prepaint rendered blocks, and returns its result
264/// with the geometry every keyed text leaf reported meanwhile.
265///
266/// `decorations` maps a leaf key to byte ranges of that leaf's text (atoms
267/// count as one `U+FFFC` each) to paint behind the text: the host's selection,
268/// find matches, anything. Leaves without a key, or without an entry, paint as
269/// usual. Atom decorations cover the whole atom and paint over its content
270/// so selections remain visible over opaque images. Supply translucent colors
271/// for overlays. Ranges are half-open; empty ranges paint nothing.
272/// Scopes nest, and the previous one is restored even if `f` panics.
273///
274/// Keys must be unique within one collection scope. Nested scopes are isolated:
275/// an outer scope does not collect the inner scope's leaves. Only prepainted
276/// leaves are returned; absence is not evidence that a document leaf was removed.
277/// The host owns document revisions, virtualization, focus, native input and
278/// UTF-16/UTF-8 conversion for platform input APIs.
279pub 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/// Copy mutable frame state, sharing only immutable shaped lines.
346#[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/// How a leaf was laid out: one layout for the whole leaf, or (for a mixed
399/// paragraph rendered as a flow) single-line text fragments and atom boxes in
400/// leaf byte order.
401#[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/// Which side of a soft wrap a caret sits on.
437///
438/// At a soft wrap one byte offset names two caret positions: the end of the
439/// upper visual line and the start of the lower one. Same terms as AppKit's
440/// `NSSelectionAffinity` and Flutter's `TextAffinity`.
441#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
442pub enum CaretAffinity {
443    /// The end of the visual line before the offset.
444    Upstream,
445    /// The start of the visual line after the offset.
446    #[default]
447    Downstream,
448}
449
450/// A caret position: a byte offset plus the side of a soft wrap it is on.
451#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
452pub struct Caret {
453    /// The byte offset into the leaf's text.
454    pub index: usize,
455    /// The visual line the caret is on when `index` is at a soft wrap.
456    pub affinity: CaretAffinity,
457}
458
459impl Caret {
460    /// A caret at `index`, at the start of a line at a soft wrap.
461    pub fn new(index: usize) -> Self {
462        Self {
463            index,
464            affinity: CaretAffinity::Downstream,
465        }
466    }
467
468    /// A caret at `index`, at the end of a line at a soft wrap.
469    pub fn upstream(index: usize) -> Self {
470        Self {
471            index,
472            affinity: CaretAffinity::Upstream,
473        }
474    }
475}
476
477/// Where one keyed text leaf was laid out in the last frame.
478///
479/// Indices are byte offsets into the leaf's text, where each inline atom
480/// (image, custom node) is one `U+FFFC` (3 bytes). Coordinates are window
481/// pixels and are valid for the frame that produced them. It is a value, not a
482/// handle: it owns its bounds and shares immutable shaped lines, never the
483/// renderer's mutable layout state. Later frames do not mutate this snapshot.
484/// The host must replace it after content, width, typography or scrolling changes.
485#[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    /// The bounds of the leaf: the union of its pieces.
499    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    /// The length of the leaf's text in bytes, atoms included.
508    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    /// Whether the leaf's text is empty (it still has a line box).
517    pub fn is_empty(&self) -> bool {
518        self.len() == 0
519    }
520
521    /// The height of a line.
522    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    /// The height of a caret: the font's ascent plus descent, not the whole
534    /// line box, which includes leading.
535    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    /// The top of a caret placed at byte `index`, in window coordinates.
552    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        // The piece the index starts, else the one it ends (a caret at a
559        // fragment boundary sits at the start of the next fragment).
560        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    /// The byte index of the caret position closest to `at`: glyph halves
573    /// round to the nearer side, unlike `TextLayout::index_for_position`.
574    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            // Fragments are one line: take the caret position nearest to the
584            // point (`closest_index_for_position` rounds up at an exact
585            // boundary inside scaled code text).
586            // Only grapheme boundaries are candidates: an interior byte of a
587            // multi-byte character maps to the next glyph's x, so it could
588            // tie with (and beat) the real boundary and split the character.
589            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    /// The caret closest to `at`, keeping the visual line that was hit: at a
617    /// soft wrap the end of the upper line is [`CaretAffinity::Upstream`].
618    /// Pass it to [`Self::position_for_caret`] to round-trip a click.
619    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        // The hit ends a piece (the right end of a line in the flow): stay on
625        // the line that was hit, not the next piece's.
626        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    /// The top of `caret` in window coordinates. Unlike
638    /// [`Self::position_for_index`], a caret at a soft wrap lands on the
639    /// visual line its affinity names.
640    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                // A caret at the start of a piece belongs to that piece.
648                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    /// The piece under `at`: the visual line under the point (pieces whose
692    /// vertical extent holds it, else the nearest), then the horizontally
693    /// nearest piece.
694    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    /// The byte range of the inline atom drawn under `at`, if any.
723    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
731/// The closest index in a whole-leaf layout for a window point.
732fn 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
757/// The byte offsets where each soft-wrapped row after the first starts.
758fn 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
764/// [`whole_index_at`] plus the affinity of the visual row that was hit.
765fn 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            // The hit row ends where the next row starts: an index there is
777            // the end of the hit row.
778            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
791/// The top of `caret` in a whole-leaf layout, on the row its affinity names.
792fn 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            // Row starts: 0, then each wrap. Pick the last row starting at or
802            // before `local`; upstream steps back off a row that starts there.
803            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    /// Stands in for an editor's root element: prepaints its child inside a
837    /// `collect_leaf_geometry` scope and keeps what the leaves reported.
838    struct Probe {
839        child: Option<AnyElement>,
840        decorations: HashMap<u64, Vec<(Range<usize>, Hsla)>>,
841        out: Geometries,
842        /// Replaces `collect_leaf_geometry` with a custom sink.
843        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        // Document order is top to bottom.
1117        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        // Cells of one row share a line; the second column sits to the right.
1120        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            // Hit-testing at a caret lands on that caret (at a soft wrap the
1142            // end of a line and the start of the next share a caret index).
1143            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        // Far outside the leaf clamps to its ends.
1152        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    /// Clicks the left and right edge of every visual line and checks the
1160    /// caret maps back to the clicked line (issue #3832).
1161    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        // Away from wraps both affinities agree with `position_for_index`.
1195        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        // The box is hit-testable, and only the box.
1228        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        // Clicking either half of the atom puts the caret on its near edge.
1239        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        // Text on both sides keeps its own indices.
1243        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        // Accented, CJK and emoji text (a skin-tone sequence is one grapheme
1254        // of two chars) on both sides of atoms, so the leaf is a mixed flow.
1255        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        // Sweep the whole line: every hit is a grapheme boundary that an
1276        // edit can use.
1277        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        // The caret x of each boundary hits that boundary, never a byte
1292        // inside the character before it (`é`: offsets 1 and 2 share an x).
1293        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    /// Records which leaves ask for backgrounds, to prove decorations are
1475    /// requested for keyed leaves only.
1476    #[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}