Skip to main content

mathtex_editor_core/
selection.rs

1//! Selection extension with common-ancestor promotion across slot boundaries.
2
3use crate::model::{Cursor, SeqId, Selection, Tree};
4
5/// Extend one node left or right, promoting across sequence boundaries.
6pub fn extend(tree: &Tree, sel: Selection, right: bool) -> Selection {
7    if right {
8        if sel.focus < tree.len(sel.seq) {
9            Selection {
10                seq: sel.seq,
11                anchor: sel.anchor,
12                focus: sel.focus + 1,
13            }
14        } else if let Some(parent) = tree.seq_parent(sel.seq) {
15            // promote: bracket the whole parent node in the grandparent
16            if let Some((gseq, gi)) = tree.index_in_parent(parent) {
17                Selection {
18                    seq: gseq,
19                    anchor: gi,
20                    focus: gi + 1,
21                }
22            } else {
23                sel
24            }
25        } else {
26            sel // root edge: clamp
27        }
28    } else if sel.focus > 0 {
29        Selection {
30            seq: sel.seq,
31            anchor: sel.anchor,
32            focus: sel.focus - 1,
33        }
34    } else if let Some(parent) = tree.seq_parent(sel.seq) {
35        if let Some((gseq, gi)) = tree.index_in_parent(parent) {
36            Selection {
37                seq: gseq,
38                anchor: gi + 1,
39                focus: gi,
40            }
41        } else {
42            sel
43        }
44    } else {
45        sel
46    }
47}
48
49/// Resolve a drag target against an anchor, promoting nested sides to their common sequence.
50pub fn extend_to(tree: &Tree, anchor: Cursor, target: Cursor) -> Selection {
51    if anchor.seq == target.seq {
52        return Selection {
53            seq: anchor.seq,
54            anchor: anchor.index,
55            focus: target.index,
56        };
57    }
58
59    // Root-to-leaf chain of seqs leading down to `leaf` (leaf last).
60    fn seq_chain(tree: &Tree, leaf: SeqId) -> Vec<SeqId> {
61        let mut chain = vec![leaf];
62        let mut seq = leaf;
63        while let Some(node) = tree.seq_parent(seq) {
64            let Some((pseq, _)) = tree.index_in_parent(node) else {
65                break;
66            };
67            chain.push(pseq);
68            seq = pseq;
69        }
70        chain.reverse();
71        chain
72    }
73    let chain_a = seq_chain(tree, anchor.seq);
74    let chain_b = seq_chain(tree, target.seq);
75    // Both chains start at `tree.root()`, so this is always >= 1.
76    let common_depth = chain_a.iter().zip(&chain_b).take_while(|(a, b)| a == b).count();
77    let common_seq = chain_a[common_depth - 1];
78
79    // At the common sequence, exact gaps stay cursors and nested sides promote to whole nodes.
80    let side = |cur: Cursor, chain: &[SeqId]| -> (usize, usize) {
81        if cur.seq == common_seq {
82            (cur.index, cur.index)
83        } else {
84            let next = chain[common_depth];
85            let node = tree.seq_parent(next).expect("non-root seq has a parent node");
86            let (_, idx) = tree.index_in_parent(node).expect("node is placed in its parent seq");
87            (idx, idx + 1)
88        }
89    };
90    let (lo_a, hi_a) = side(anchor, &chain_a);
91    let (lo_b, hi_b) = side(target, &chain_b);
92    let lo = lo_a.min(lo_b);
93    let hi = hi_a.max(hi_b);
94    // The focus tracks the side the drag currently approaches from.
95    let (anchor_idx, focus_idx) = if lo_a <= lo_b { (lo, hi) } else { (hi, lo) };
96    Selection {
97        seq: common_seq,
98        anchor: anchor_idx,
99        focus: focus_idx,
100    }
101}
102
103/// Select the whole expression (the root sequence).
104pub fn select_all(tree: &Tree) -> Selection {
105    let root = tree.root();
106    Selection {
107        seq: root,
108        anchor: 0,
109        focus: tree.len(root),
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use crate::model::{FracStyle, Kind, MathClass, ScriptSlot, Symbol};
117
118    fn atom(c: &str) -> Symbol {
119        Symbol { latex: c.into(), class: MathClass::Ord }
120    }
121
122    /// `x^2+2`: a `Script` (item 0) holding "x" as its base, then "+" and "2".
123    fn x_squared_plus_2(t: &mut Tree) -> (SeqId, SeqId) {
124        let root = t.root();
125        t.insert_atom(Cursor { seq: root, index: 0 }, atom("x"));
126        let c = t.attach_script(Cursor { seq: root, index: 1 }, ScriptSlot::Sup);
127        t.insert_atom(c, atom("2"));
128        t.insert_atom(Cursor { seq: root, index: 1 }, atom("+"));
129        t.insert_atom(Cursor { seq: root, index: 2 }, atom("2"));
130        let script = t.items(root)[0];
131        let base = match t.kind(script) {
132            Some(Kind::Script { base, .. }) => *base,
133            _ => panic!("expected a Script"),
134        };
135        (root, base)
136    }
137
138    /// Same-seq drag is the literal range, unaffected by promotion.
139    #[test]
140    fn extend_to_same_seq_is_literal() {
141        let mut t = Tree::new();
142        let root = t.root();
143        t.insert_atom(Cursor { seq: root, index: 0 }, atom("a"));
144        t.insert_atom(Cursor { seq: root, index: 1 }, atom("b"));
145        let sel = extend_to(&t, Cursor { seq: root, index: 0 }, Cursor { seq: root, index: 2 });
146        assert_eq!((sel.seq, sel.anchor, sel.focus), (root, 0, 2));
147    }
148
149    /// Dragging from inside `x^2`'s base past the final `2` selects the whole root sequence.
150    #[test]
151    fn extend_to_from_inside_a_script_base_past_the_end_selects_everything() {
152        let mut t = Tree::new();
153        let (root, base) = x_squared_plus_2(&mut t);
154        let end = t.len(root);
155        let sel = extend_to(&t, Cursor { seq: base, index: 1 }, Cursor { seq: root, index: end });
156        assert_eq!(sel.seq, root);
157        assert_eq!((sel.anchor.min(sel.focus), sel.anchor.max(sel.focus)), (0, end));
158        // The focus (caret) tracks the target side, the end of the document.
159        assert_eq!(sel.focus, end);
160    }
161
162    /// Reverse drag keeps the exact anchor and promotes only the focus side.
163    #[test]
164    fn extend_to_reversed_direction_keeps_the_true_anchor_fixed() {
165        let mut t = Tree::new();
166        let (root, base) = x_squared_plus_2(&mut t);
167        let end = t.len(root);
168        let sel = extend_to(&t, Cursor { seq: root, index: end }, Cursor { seq: base, index: 1 });
169        assert_eq!(sel.seq, root);
170        assert_eq!(sel.anchor, end); // the true anchor, untouched
171        assert_eq!(sel.focus, 0); // promoted to "before the whole Script"
172    }
173
174    /// Selecting between sibling structures promotes both and includes both in full.
175    #[test]
176    fn extend_to_between_two_sibling_structures() {
177        let mut t = Tree::new();
178        let root = t.root();
179        let c = t.insert_fraction(Cursor { seq: root, index: 0 }, FracStyle::Bar, None);
180        let num_a = c.seq;
181        let fa = t.items(root)[0];
182        let den_a = t.child_seqs(fa)[1];
183        let c = t.insert_fraction(Cursor { seq: root, index: 1 }, FracStyle::Bar, None);
184        let num_b = c.seq;
185        let _ = (num_a, den_a);
186
187        let sel = extend_to(&t, Cursor { seq: den_a, index: 0 }, Cursor { seq: num_b, index: 0 });
188        assert_eq!((sel.seq, sel.anchor.min(sel.focus), sel.anchor.max(sel.focus)), (root, 0, 2));
189    }
190}