mathtex-editor-core 0.1.0

Headless core of the mathtex structural math editor: model, operations, navigation, selection, IR matching
Documentation
//! Selection extension with common-ancestor promotion across slot boundaries.

use crate::model::{Cursor, SeqId, Selection, Tree};

/// Extend one node left or right, promoting across sequence boundaries.
pub fn extend(tree: &Tree, sel: Selection, right: bool) -> Selection {
    if right {
        if sel.focus < tree.len(sel.seq) {
            Selection {
                seq: sel.seq,
                anchor: sel.anchor,
                focus: sel.focus + 1,
            }
        } else if let Some(parent) = tree.seq_parent(sel.seq) {
            // promote: bracket the whole parent node in the grandparent
            if let Some((gseq, gi)) = tree.index_in_parent(parent) {
                Selection {
                    seq: gseq,
                    anchor: gi,
                    focus: gi + 1,
                }
            } else {
                sel
            }
        } else {
            sel // root edge: clamp
        }
    } else if sel.focus > 0 {
        Selection {
            seq: sel.seq,
            anchor: sel.anchor,
            focus: sel.focus - 1,
        }
    } else if let Some(parent) = tree.seq_parent(sel.seq) {
        if let Some((gseq, gi)) = tree.index_in_parent(parent) {
            Selection {
                seq: gseq,
                anchor: gi + 1,
                focus: gi,
            }
        } else {
            sel
        }
    } else {
        sel
    }
}

/// Resolve a drag target against an anchor, promoting nested sides to their common sequence.
pub fn extend_to(tree: &Tree, anchor: Cursor, target: Cursor) -> Selection {
    if anchor.seq == target.seq {
        return Selection {
            seq: anchor.seq,
            anchor: anchor.index,
            focus: target.index,
        };
    }

    // Root-to-leaf chain of seqs leading down to `leaf` (leaf last).
    fn seq_chain(tree: &Tree, leaf: SeqId) -> Vec<SeqId> {
        let mut chain = vec![leaf];
        let mut seq = leaf;
        while let Some(node) = tree.seq_parent(seq) {
            let Some((pseq, _)) = tree.index_in_parent(node) else {
                break;
            };
            chain.push(pseq);
            seq = pseq;
        }
        chain.reverse();
        chain
    }
    let chain_a = seq_chain(tree, anchor.seq);
    let chain_b = seq_chain(tree, target.seq);
    // Both chains start at `tree.root()`, so this is always >= 1.
    let common_depth = chain_a.iter().zip(&chain_b).take_while(|(a, b)| a == b).count();
    let common_seq = chain_a[common_depth - 1];

    // At the common sequence, exact gaps stay cursors and nested sides promote to whole nodes.
    let side = |cur: Cursor, chain: &[SeqId]| -> (usize, usize) {
        if cur.seq == common_seq {
            (cur.index, cur.index)
        } else {
            let next = chain[common_depth];
            let node = tree.seq_parent(next).expect("non-root seq has a parent node");
            let (_, idx) = tree.index_in_parent(node).expect("node is placed in its parent seq");
            (idx, idx + 1)
        }
    };
    let (lo_a, hi_a) = side(anchor, &chain_a);
    let (lo_b, hi_b) = side(target, &chain_b);
    let lo = lo_a.min(lo_b);
    let hi = hi_a.max(hi_b);
    // The focus tracks the side the drag currently approaches from.
    let (anchor_idx, focus_idx) = if lo_a <= lo_b { (lo, hi) } else { (hi, lo) };
    Selection {
        seq: common_seq,
        anchor: anchor_idx,
        focus: focus_idx,
    }
}

/// Select the whole expression (the root sequence).
pub fn select_all(tree: &Tree) -> Selection {
    let root = tree.root();
    Selection {
        seq: root,
        anchor: 0,
        focus: tree.len(root),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::model::{FracStyle, Kind, MathClass, ScriptSlot, Symbol};

    fn atom(c: &str) -> Symbol {
        Symbol { latex: c.into(), class: MathClass::Ord }
    }

    /// `x^2+2`: a `Script` (item 0) holding "x" as its base, then "+" and "2".
    fn x_squared_plus_2(t: &mut Tree) -> (SeqId, SeqId) {
        let root = t.root();
        t.insert_atom(Cursor { seq: root, index: 0 }, atom("x"));
        let c = t.attach_script(Cursor { seq: root, index: 1 }, ScriptSlot::Sup);
        t.insert_atom(c, atom("2"));
        t.insert_atom(Cursor { seq: root, index: 1 }, atom("+"));
        t.insert_atom(Cursor { seq: root, index: 2 }, atom("2"));
        let script = t.items(root)[0];
        let base = match t.kind(script) {
            Some(Kind::Script { base, .. }) => *base,
            _ => panic!("expected a Script"),
        };
        (root, base)
    }

    /// Same-seq drag is the literal range, unaffected by promotion.
    #[test]
    fn extend_to_same_seq_is_literal() {
        let mut t = Tree::new();
        let root = t.root();
        t.insert_atom(Cursor { seq: root, index: 0 }, atom("a"));
        t.insert_atom(Cursor { seq: root, index: 1 }, atom("b"));
        let sel = extend_to(&t, Cursor { seq: root, index: 0 }, Cursor { seq: root, index: 2 });
        assert_eq!((sel.seq, sel.anchor, sel.focus), (root, 0, 2));
    }

    /// Dragging from inside `x^2`'s base past the final `2` selects the whole root sequence.
    #[test]
    fn extend_to_from_inside_a_script_base_past_the_end_selects_everything() {
        let mut t = Tree::new();
        let (root, base) = x_squared_plus_2(&mut t);
        let end = t.len(root);
        let sel = extend_to(&t, Cursor { seq: base, index: 1 }, Cursor { seq: root, index: end });
        assert_eq!(sel.seq, root);
        assert_eq!((sel.anchor.min(sel.focus), sel.anchor.max(sel.focus)), (0, end));
        // The focus (caret) tracks the target side, the end of the document.
        assert_eq!(sel.focus, end);
    }

    /// Reverse drag keeps the exact anchor and promotes only the focus side.
    #[test]
    fn extend_to_reversed_direction_keeps_the_true_anchor_fixed() {
        let mut t = Tree::new();
        let (root, base) = x_squared_plus_2(&mut t);
        let end = t.len(root);
        let sel = extend_to(&t, Cursor { seq: root, index: end }, Cursor { seq: base, index: 1 });
        assert_eq!(sel.seq, root);
        assert_eq!(sel.anchor, end); // the true anchor, untouched
        assert_eq!(sel.focus, 0); // promoted to "before the whole Script"
    }

    /// Selecting between sibling structures promotes both and includes both in full.
    #[test]
    fn extend_to_between_two_sibling_structures() {
        let mut t = Tree::new();
        let root = t.root();
        let c = t.insert_fraction(Cursor { seq: root, index: 0 }, FracStyle::Bar, None);
        let num_a = c.seq;
        let fa = t.items(root)[0];
        let den_a = t.child_seqs(fa)[1];
        let c = t.insert_fraction(Cursor { seq: root, index: 1 }, FracStyle::Bar, None);
        let num_b = c.seq;
        let _ = (num_a, den_a);

        let sel = extend_to(&t, Cursor { seq: den_a, index: 0 }, Cursor { seq: num_b, index: 0 });
        assert_eq!((sel.seq, sel.anchor.min(sel.focus), sel.anchor.max(sel.focus)), (root, 0, 2));
    }
}