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}