Skip to main content

taino_edit_core/
selection.rs

1//! Editor [`Selection`]. v0.1 covers the three ProseMirror selection shapes;
2//! mapping is positional (sufficient for the linear, single-user editing
3//! v0.1 targets — richer "find a valid selection nearby" behaviour is a
4//! v0.2 refinement).
5
6use crate::map::Mapping;
7use crate::node::Node;
8
9/// What is currently selected.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum Selection {
12    /// A (possibly empty) text range. `anchor` is the fixed side, `head` the
13    /// moving side; they may be in either order.
14    Text {
15        /// Fixed end of the selection.
16        anchor: usize,
17        /// Moving end (where the caret is).
18        head: usize,
19    },
20    /// A single node selected as a unit, starting at `pos`.
21    Node {
22        /// Position directly before the selected node.
23        pos: usize,
24    },
25    /// A rectangular range of table cells. `anchor` and `head` are the
26    /// positions directly before the anchor and head cell nodes; the
27    /// covered rectangle is whatever table-aware code derives from them.
28    /// `core` treats this generically (for `from`/`to`/`map`); the table
29    /// extension interprets the rectangle.
30    Cell {
31        /// Position before the anchor (fixed) cell.
32        anchor: usize,
33        /// Position before the head (moving) cell.
34        head: usize,
35    },
36    /// The whole document.
37    All,
38}
39
40impl Selection {
41    /// A collapsed text caret at `pos`.
42    pub fn caret(pos: usize) -> Selection {
43        Selection::Text {
44            anchor: pos,
45            head: pos,
46        }
47    }
48
49    /// Lowest selected position.
50    pub fn from(&self) -> usize {
51        match self {
52            Selection::Text { anchor, head } => (*anchor).min(*head),
53            Selection::Node { pos } => *pos,
54            Selection::Cell { anchor, head } => (*anchor).min(*head),
55            Selection::All => 0,
56        }
57    }
58
59    /// Highest selected position (relative to `doc` for `All`/`Node`/`Cell`).
60    pub fn to(&self, doc: &Node) -> usize {
61        match self {
62            Selection::Text { anchor, head } => (*anchor).max(*head),
63            Selection::Node { pos } => doc.node_at(*pos).map_or(*pos, |n| pos + n.node_size()),
64            Selection::Cell { anchor, head } => {
65                // End just past the later of the two cells.
66                let later = (*anchor).max(*head);
67                doc.node_at(later).map_or(later, |n| later + n.node_size())
68            }
69            Selection::All => doc.content().size(),
70        }
71    }
72
73    /// Whether the selection is an empty caret.
74    pub fn is_empty(&self) -> bool {
75        matches!(self, Selection::Text { anchor, head } if anchor == head)
76    }
77
78    /// Map this selection forward through `mapping`, clamping into `doc`.
79    /// A node or cell selection whose node was touched degrades to a caret.
80    pub fn map(&self, doc: &Node, mapping: &Mapping) -> Selection {
81        let max = doc.content().size();
82        let clamp = |p: usize| p.min(max);
83        match self {
84            Selection::Text { anchor, head } => Selection::Text {
85                anchor: clamp(mapping.map(*anchor, 1)),
86                head: clamp(mapping.map(*head, 1)),
87            },
88            Selection::Node { pos } => {
89                let r = mapping.map_result(*pos, 1);
90                if r.deleted_after() {
91                    Selection::caret(clamp(r.pos))
92                } else {
93                    Selection::Node { pos: clamp(r.pos) }
94                }
95            }
96            Selection::Cell { anchor, head } => {
97                let a = mapping.map_result(*anchor, 1);
98                let h = mapping.map_result(*head, 1);
99                if a.deleted_after() || h.deleted_after() {
100                    // A spanned cell was removed (merge/delete) — collapse.
101                    Selection::caret(clamp(a.pos))
102                } else {
103                    Selection::Cell {
104                        anchor: clamp(a.pos),
105                        head: clamp(h.pos),
106                    }
107                }
108            }
109            Selection::All => Selection::All,
110        }
111    }
112}