mathtex-editor-core 0.3.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, SeqRange, Tree};

/// Extend one node left or right, promoting to the whole parent node at a slot edge.
pub(crate) fn extend(tree: &Tree, sel: SeqRange, right: bool) -> SeqRange {
    let len = tree.len(sel.seq);
    if right && sel.focus < len {
        return SeqRange { focus: sel.focus + 1, ..sel };
    }
    if !right && sel.focus > 0 {
        return SeqRange { focus: sel.focus - 1, ..sel };
    }
    match tree.before_parent(sel.seq) {
        Some(p) if right => SeqRange { seq: p.seq, anchor: p.index, focus: p.index + 1 },
        Some(p) => SeqRange { seq: p.seq, anchor: p.index + 1, focus: p.index },
        None => sel,
    }
}

/// Resolve a drag target against an anchor, promoting nested sides to their common sequence.
pub(crate) fn extend_to(tree: &Tree, anchor: Cursor, target: Cursor) -> SeqRange {
    if anchor.seq == target.seq {
        return SeqRange { seq: anchor.seq, anchor: anchor.index, focus: target.index };
    }
    let chain_a = seq_chain(tree, anchor.seq);
    let chain_b = seq_chain(tree, target.seq);
    // Both chains start at the root, so the common prefix is never empty.
    let common_depth = chain_a.iter().zip(&chain_b).take_while(|(a, b)| a == b).count().max(1);
    let common = chain_a[common_depth - 1];
    // At the common sequence an exact gap stays a gap and a nested side covers its whole node.
    let side = |cur: Cursor, chain: &[SeqId]| -> (usize, usize) {
        if cur.seq == common {
            return (cur.index, cur.index);
        }
        match chain.get(common_depth).and_then(|&s| tree.before_parent(s)) {
            Some(p) => (p.index, p.index + 1),
            None => (0, 0),
        }
    };
    let (lo_a, hi_a) = side(anchor, &chain_a);
    let (lo_b, hi_b) = side(target, &chain_b);
    let (lo, hi) = (lo_a.min(lo_b), hi_a.max(hi_b));
    // Midpoints order the two sides, so a gap just left of the anchor's node puts the focus left.
    let target_left = lo_b + hi_b < lo_a + hi_a;
    let (anchor_idx, focus_idx) = if target_left { (hi, lo) } else { (lo, hi) };
    SeqRange { seq: common, anchor: anchor_idx, focus: focus_idx }
}

/// 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(p) = tree.before_parent(seq) {
        chain.push(p.seq);
        seq = p.seq;
    }
    chain.reverse();
    chain
}

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

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

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

    /// `x^2+2` with the Script at item 0, returns `(root, base)`.
    fn x_squared_plus_2(t: &mut Tree) -> (SeqId, SeqId) {
        let root = t.root();
        t.insert_atom(Cursor { seq: root, index: 0 }, None, atom("x")).unwrap();
        let c = t.attach_script(Cursor { seq: root, index: 1 }, ScriptSlot::Sup, None).unwrap();
        t.insert_atom(c, None, atom("2")).unwrap();
        t.insert_atom(Cursor { seq: root, index: 1 }, None, atom("+")).unwrap();
        t.insert_atom(Cursor { seq: root, index: 2 }, None, atom("2")).unwrap();
        let base = t.child_seqs(t.items(root)[0])[0];
        (root, base)
    }

    #[test]
    fn same_seq_drag_is_literal() {
        let mut t = Tree::new();
        let (root, _) = x_squared_plus_2(&mut t);
        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));
    }

    #[test]
    fn drag_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, sel.anchor, sel.focus), (root, 0, end));
    }

    #[test]
    fn reverse_drag_keeps_the_true_anchor() {
        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, sel.anchor, sel.focus), (root, end, 0));
    }

    /// On a tie the focus follows the target, here the gap just left of the anchor's node.
    #[test]
    fn tie_puts_the_focus_on_the_target_side() {
        let mut t = Tree::new();
        let (root, base) = x_squared_plus_2(&mut t);
        let sel = extend_to(&t, Cursor { seq: base, index: 1 }, Cursor { seq: root, index: 0 });
        assert_eq!((sel.seq, sel.anchor, sel.focus), (root, 1, 0));
        let sel = extend_to(&t, Cursor { seq: base, index: 1 }, Cursor { seq: root, index: 1 });
        assert_eq!((sel.seq, sel.anchor, sel.focus), (root, 0, 1));
    }

    #[test]
    fn drag_between_sibling_structures_covers_both() {
        let mut t = Tree::new();
        let root = t.root();
        t.insert_fraction(Cursor { seq: root, index: 0 }, FracStyle::Bar, None).unwrap();
        let num_b = t.insert_fraction(Cursor { seq: root, index: 1 }, FracStyle::Bar, None).unwrap();
        let den_a = t.child_seqs(t.items(root)[0])[1];
        let sel = extend_to(&t, Cursor { seq: den_a, index: 0 }, num_b);
        assert_eq!((sel.seq, sel.anchor, sel.focus), (root, 0, 2));
    }
}