Skip to main content

mathtex_editor_core/
ops.rs

1//! Edit operations preserve slotmap integrity and keep semantic edits built from primitive tree changes.
2
3use std::ops::Range;
4
5use crate::command::Side;
6use crate::model::{
7    Cursor, Deco, FracStyle, Kind, Mark, MatrixEnv, Node, NodeId, ScriptSlot, Selection, Seq,
8    SeqId, Symbol, Tree, UnderOverSpec, Variant,
9};
10use crate::nav;
11
12/// Describes a node to create before `insert_new` allocates slot seqs and parent links.
13pub(crate) enum NewNode {
14    Atom(Symbol),
15    Frac(FracStyle),
16    Script(ScriptSlot),
17    BigOp(Symbol),
18    Sqrt,
19    Delim { open: char, close: char },
20    Accent(Mark),
21    UnderOver(UnderOverSpec),
22    Styled(Variant),
23    Matrix { env: MatrixEnv, rows: usize, cols: usize },
24}
25
26impl Tree {
27    pub(crate) fn alloc_seq(&mut self, parent: Option<NodeId>) -> SeqId {
28        self.seqs.insert(Seq {
29            parent,
30            items: Vec::new(),
31        })
32    }
33
34    /// Builds a node kind with empty slot seqs whose parent is set by [`Tree::insert_new`].
35    fn build_kind(&mut self, spec: NewNode) -> Kind {
36        match spec {
37            NewNode::Atom(sym) => Kind::Atom(sym),
38            NewNode::Frac(style) => Kind::Frac {
39                num: self.alloc_seq(None),
40                den: self.alloc_seq(None),
41                style,
42            },
43            NewNode::Script(slot) => {
44                let base = self.alloc_seq(None);
45                let (sub, sup) = match slot {
46                    ScriptSlot::Sub => (Some(self.alloc_seq(None)), None),
47                    ScriptSlot::Sup => (None, Some(self.alloc_seq(None))),
48                };
49                Kind::Script { base, sub, sup }
50            }
51            NewNode::BigOp(op) => Kind::BigOp {
52                op,
53                lower: self.alloc_seq(None),
54                upper: self.alloc_seq(None),
55            },
56            NewNode::Sqrt => Kind::Sqrt {
57                // Both slots always exist, an empty degree is just a placeholder.
58                index: self.alloc_seq(None),
59                radicand: self.alloc_seq(None),
60            },
61            NewNode::Delim { open, close } => Kind::Delim {
62                open,
63                close,
64                body: self.alloc_seq(None),
65            },
66            NewNode::Accent(mark) => Kind::Accent {
67                mark,
68                base: self.alloc_seq(None),
69            },
70            NewNode::UnderOver(spec) => Kind::UnderOver {
71                base: self.alloc_seq(None),
72                over: spec.over.then(|| self.alloc_seq(None)),
73                under: spec.under.then(|| self.alloc_seq(None)),
74                over_deco: spec.over_deco,
75                under_deco: spec.under_deco,
76            },
77            NewNode::Styled(variant) => Kind::Styled {
78                variant,
79                content: self.alloc_seq(None),
80            },
81            NewNode::Matrix { env, rows, cols } => {
82                let grid = (0..rows)
83                    .map(|_| (0..cols).map(|_| self.alloc_seq(None)).collect())
84                    .collect();
85                Kind::Matrix { env, rows: grid }
86            }
87        }
88    }
89
90    /// Creates a node with empty slot seqs, splices it into `seq`, and wires parent links.
91    pub(crate) fn insert_new(&mut self, seq: SeqId, index: usize, spec: NewNode) -> NodeId {
92        let kind = self.build_kind(spec);
93        let node_id = self.nodes.insert(Node { parent: seq, kind });
94        for s in self.child_seqs(node_id) {
95            if let Some(sq) = self.seqs.get_mut(s) {
96                sq.parent = Some(node_id);
97            }
98        }
99        if let Some(sq) = self.seqs.get_mut(seq) {
100            let i = index.min(sq.items.len());
101            sq.items.insert(i, node_id);
102        }
103        node_id
104    }
105
106    /// Unlinks a node from its parent seq while keeping its subtree alive.
107    pub(crate) fn detach(&mut self, node: NodeId) -> NodeId {
108        if let Some((parent, idx)) = self.index_in_parent(node) {
109            if let Some(sq) = self.seqs.get_mut(parent) {
110                sq.items.remove(idx);
111            }
112        }
113        node
114    }
115
116    /// Recursively free a node, its slot seqs, and their contents.
117    pub(crate) fn drop_node(&mut self, node: NodeId) {
118        self.detach(node);
119        self.free_node(node);
120    }
121
122    fn free_node(&mut self, node: NodeId) {
123        for s in self.child_seqs(node) {
124            self.free_seq(s);
125        }
126        self.nodes.remove(node);
127    }
128
129    fn free_seq(&mut self, seq: SeqId) {
130        let items: Vec<NodeId> = self
131            .seqs
132            .get(seq)
133            .map(|s| s.items.clone())
134            .unwrap_or_default();
135        for n in items {
136            self.free_node(n);
137        }
138        self.seqs.remove(seq);
139    }
140
141    /// Moves nodes from `src[range]` into `dst` at `at`, the caller ensures the seqs differ.
142    pub(crate) fn move_range(&mut self, src: SeqId, range: Range<usize>, dst: SeqId, at: usize) {
143        let moved: Vec<NodeId> = self
144            .seqs
145            .get_mut(src)
146            .map(|s| s.items.drain(range).collect())
147            .unwrap_or_default();
148        for &n in &moved {
149            if let Some(nd) = self.nodes.get_mut(n) {
150                nd.parent = dst;
151            }
152        }
153        if let Some(d) = self.seqs.get_mut(dst) {
154            let at = at.min(d.items.len());
155            d.items.splice(at..at, moved);
156        }
157    }
158}
159
160impl Tree {
161    /// Evicts a nonempty Script base so the next insertion replaces it.
162    fn make_room_in_script_base(&mut self, at: Cursor) -> Cursor {
163        if !self.is_empty(at.seq) {
164            if let Some(script) = self.script_base_node(at.seq) {
165                if let Some((pseq, pidx)) = self.index_in_parent(script) {
166                    let n = self.len(at.seq);
167                    self.move_range(at.seq, 0..n, pseq, pidx);
168                    return Cursor {
169                        seq: at.seq,
170                        index: 0,
171                    };
172                }
173            }
174        }
175        at
176    }
177
178    /// Inserts an atom and returns the cursor after it.
179    pub fn insert_atom(&mut self, at: Cursor, sym: Symbol) -> Cursor {
180        let at = self.make_room_in_script_base(at);
181        self.insert_new(at.seq, at.index, NewNode::Atom(sym));
182        Cursor {
183            seq: at.seq,
184            index: at.index + 1,
185        }
186    }
187
188    /// Inserts a structure and optionally wraps `sel` into the chosen slot.
189    fn insert_struct(
190        &mut self,
191        at: Cursor,
192        sel: Option<Selection>,
193        spec: NewNode,
194        wrap_slot: usize,
195    ) -> (NodeId, bool) {
196        match sel {
197            None => {
198                let at = self.make_room_in_script_base(at);
199                (self.insert_new(at.seq, at.index, spec), false)
200            }
201            Some(s) => {
202                let lo = s.anchor.min(s.focus);
203                let hi = s.anchor.max(s.focus);
204                let node = self.insert_new(s.seq, lo, spec);
205                let slot = self.child_seqs(node)[wrap_slot];
206                self.move_range(s.seq, lo + 1..hi + 1, slot, 0);
207                (node, true)
208            }
209        }
210    }
211
212    /// Inserts a fraction and returns a cursor in the numerator or denominator.
213    pub fn insert_fraction(&mut self, at: Cursor, style: FracStyle, sel: Option<Selection>) -> Cursor {
214        let (node, wrapped) = self.insert_struct(at, sel, NewNode::Frac(style), 0);
215        let slots = self.child_seqs(node);
216        let (num, den) = (slots[0], slots[1]);
217        if wrapped {
218            Cursor { seq: den, index: 0 }
219        } else {
220            Cursor { seq: num, index: 0 }
221        }
222    }
223
224    /// Inserts a radical and returns a cursor in the radicand.
225    pub fn insert_sqrt(&mut self, at: Cursor, sel: Option<Selection>) -> Cursor {
226        // child_seqs is [index, radicand], wrapped content goes into the radicand.
227        let (node, _wrapped) = self.insert_struct(at, sel, NewNode::Sqrt, 1);
228        let radicand = self.child_seqs(node)[1];
229        Cursor {
230            seq: radicand,
231            index: 0,
232        }
233    }
234
235    /// Inserts delimiters and returns a cursor in their body.
236    pub fn insert_delimiters(
237        &mut self,
238        at: Cursor,
239        open: char,
240        close: char,
241        sel: Option<Selection>,
242    ) -> Cursor {
243        let (node, wrapped) = self.insert_struct(at, sel, NewNode::Delim { open, close }, 0);
244        let body = self.child_seqs(node)[0];
245        Cursor {
246            seq: body,
247            index: if wrapped { self.len(body) } else { 0 },
248        }
249    }
250
251    /// Inserts an accent and returns a cursor in its base.
252    pub fn insert_accent(&mut self, at: Cursor, mark: Mark, sel: Option<Selection>) -> Cursor {
253        let (node, wrapped) = self.insert_struct(at, sel, NewNode::Accent(mark), 0);
254        let base = self.child_seqs(node)[0];
255        Cursor {
256            seq: base,
257            index: if wrapped { self.len(base) } else { 0 },
258        }
259    }
260
261    /// Inserts a styled wrapper and returns a cursor in its content.
262    pub fn insert_styled(&mut self, at: Cursor, variant: Variant, sel: Option<Selection>) -> Cursor {
263        let (node, wrapped) = self.insert_struct(at, sel, NewNode::Styled(variant), 0);
264        let content = self.child_seqs(node)[0];
265        Cursor {
266            seq: content,
267            index: if wrapped { self.len(content) } else { 0 },
268        }
269    }
270
271    /// Inserts an under or over structure and returns a cursor in its base.
272    pub fn insert_under_over(&mut self, at: Cursor, spec: UnderOverSpec, sel: Option<Selection>) -> Cursor {
273        // base is the first slot when there's no `over`, else index 1.
274        let wrap_slot = if spec.over { 1 } else { 0 };
275        let (node, _wrapped) = self.insert_struct(at, sel, NewNode::UnderOver(spec), wrap_slot);
276        let base = self.child_seqs(node)[wrap_slot];
277        Cursor { seq: base, index: 0 }
278    }
279
280    /// Inserts a matrix and returns a cursor in the first cell.
281    pub fn insert_matrix(&mut self, at: Cursor, env: MatrixEnv, rows: usize, cols: usize) -> Cursor {
282        let rows = rows.max(1);
283        let cols = cols.max(1);
284        let node = self.insert_new(at.seq, at.index, NewNode::Matrix { env, rows, cols });
285        let first = self.child_seqs(node)[0];
286        Cursor {
287            seq: first,
288            index: 0,
289        }
290    }
291
292    /// Inserts a big operator with empty limits and returns a cursor after it.
293    pub fn insert_big_op(&mut self, at: Cursor, op: Symbol) -> Cursor {
294        self.insert_new(at.seq, at.index, NewNode::BigOp(op));
295        Cursor {
296            seq: at.seq,
297            index: at.index + 1,
298        }
299    }
300
301    /// Returns the `BigOp` limit targeted by `_` or `^` when the cursor is after the operator.
302    pub fn bigop_limit_target(&self, at: Cursor, which: ScriptSlot) -> Option<Cursor> {
303        if at.index == 0 {
304            return None;
305        }
306        let prev = self.items(at.seq)[at.index - 1];
307        if let Some(Kind::BigOp { lower, upper, .. }) = self.kind(prev) {
308            let target = match which {
309                ScriptSlot::Sub => *lower,
310                ScriptSlot::Sup => *upper,
311            };
312            return Some(Cursor {
313                seq: target,
314                index: self.len(target),
315            });
316        }
317        None
318    }
319
320    /// Attaches or moves into a subscript or superscript on the node before the cursor.
321    pub fn attach_script(&mut self, at: Cursor, which: ScriptSlot) -> Cursor {
322        if at.index > 0 {
323            let prev = self.items(at.seq)[at.index - 1];
324            if matches!(self.kind(prev), Some(Kind::Script { .. })) {
325                let slot = self.script_slot(prev, which);
326                return Cursor {
327                    seq: slot,
328                    index: self.len(slot),
329                };
330            }
331            // Wrap the preceding node as the base of a new Script.
332            let script = self.insert_new(at.seq, at.index, NewNode::Script(which));
333            let base = match self.kind(script) {
334                Some(Kind::Script { base, .. }) => *base,
335                _ => unreachable!(),
336            };
337            self.move_range(at.seq, at.index - 1..at.index, base, 0);
338            let slot = self.script_slot(script, which);
339            return Cursor { seq: slot, index: 0 };
340        }
341        // No base to the left means an empty base Script with the cursor in the script slot.
342        let script = self.insert_new(at.seq, at.index, NewNode::Script(which));
343        let slot = self.script_slot(script, which);
344        Cursor { seq: slot, index: 0 }
345    }
346
347    /// Returns the existing or newly created subscript or superscript seq of a Script node.
348    fn script_slot(&mut self, node: NodeId, which: ScriptSlot) -> SeqId {
349        let existing = match self.kind(node) {
350            Some(Kind::Script { sub, sup, .. }) => match which {
351                ScriptSlot::Sub => *sub,
352                ScriptSlot::Sup => *sup,
353            },
354            _ => None,
355        };
356        if let Some(s) = existing {
357            return s;
358        }
359        let s = self.alloc_seq(Some(node));
360        if let Some(Node {
361            kind: Kind::Script { sub, sup, .. },
362            ..
363        }) = self.nodes.get_mut(node)
364        {
365            match which {
366                ScriptSlot::Sub => *sub = Some(s),
367                ScriptSlot::Sup => *sup = Some(s),
368            }
369        }
370        s
371    }
372
373    /// Deletes backward, escalating empty placeholders and otherwise moving left when no leaf is removed.
374    pub fn delete_backward(&mut self, at: Cursor) -> Cursor {
375        if at.index > 0 {
376            let prev = self.items(at.seq)[at.index - 1];
377            if self.child_seqs(prev).is_empty() {
378                self.drop_node(prev);
379                return Cursor {
380                    seq: at.seq,
381                    index: at.index - 1,
382                };
383            }
384            return nav::move_left(self, at).unwrap_or(at);
385        }
386        if self.is_empty(at.seq) {
387            if let Some(parent) = self.seq_parent(at.seq) {
388                return self.escalate_delete(parent, at.seq, at);
389            }
390            return at;
391        }
392        nav::move_left(self, at).unwrap_or(at)
393    }
394
395    /// Deletes forward as the mirror of [`Tree::delete_backward`].
396    pub fn delete_forward(&mut self, at: Cursor) -> Cursor {
397        if at.index < self.len(at.seq) {
398            let next = self.items(at.seq)[at.index];
399            if self.child_seqs(next).is_empty() {
400                self.drop_node(next);
401                return Cursor {
402                    seq: at.seq,
403                    index: at.index,
404                };
405            }
406            return nav::move_right(self, at).unwrap_or(at);
407        }
408        if self.is_empty(at.seq) {
409            if let Some(parent) = self.seq_parent(at.seq) {
410                return self.escalate_delete(parent, at.seq, at);
411            }
412            return at;
413        }
414        nav::move_right(self, at).unwrap_or(at)
415    }
416
417    /// Deletes a contiguous selection run within one seq.
418    pub fn delete_selection(&mut self, sel: Selection) -> Cursor {
419        let lo = sel.anchor.min(sel.focus);
420        let hi = sel.anchor.max(sel.focus);
421        let victims: Vec<NodeId> = self.items(sel.seq)[lo..hi.min(self.len(sel.seq))].to_vec();
422        for n in victims {
423            self.drop_node(n);
424        }
425        Cursor {
426            seq: sel.seq,
427            index: lo,
428        }
429    }
430
431    /// Drops `parent` and leaves the cursor in the grandparent seq.
432    fn drop_and_step_out(&mut self, parent: NodeId, at_parent: Cursor) -> Cursor {
433        self.drop_node(parent);
434        at_parent
435    }
436
437    /// Promotes `from` into `into`, drops `parent`, and returns a cursor after the promoted run.
438    fn promote_and_drop(&mut self, parent: NodeId, from: SeqId, into: SeqId, at: usize) -> Cursor {
439        let n = self.len(from);
440        self.move_range(from, 0..n, into, at);
441        self.drop_node(parent);
442        Cursor { seq: into, index: at + n }
443    }
444
445    /// Handles deletion from an empty Script base by reattaching or dissolving the script.
446    fn delete_empty_script_base(
447        &mut self,
448        parent: NodeId,
449        base: SeqId,
450        sub: Option<SeqId>,
451        sup: Option<SeqId>,
452        pseq: SeqId,
453        pidx: usize,
454        at_parent: Cursor,
455    ) -> Cursor {
456        if pidx > 0 {
457            // The preceding sibling becomes the base.
458            self.move_range(pseq, pidx - 1..pidx, base, 0);
459            return Cursor { seq: base, index: self.len(base) };
460        }
461        let mut offset = 0;
462        for s in [Some(base), sub, sup].into_iter().flatten() {
463            let n = self.len(s);
464            if n > 0 {
465                self.move_range(s, 0..n, pseq, pidx + offset);
466                offset += n;
467            }
468        }
469        self.drop_and_step_out(parent, at_parent)
470    }
471
472    /// Promotes the base when no optional slots remain, or keeps the cursor at the base end.
473    fn collapse_to_base_or_stay(
474        &mut self,
475        parent: NodeId,
476        base: SeqId,
477        opt_a: Option<SeqId>,
478        opt_b: Option<SeqId>,
479        pseq: SeqId,
480        pidx: usize,
481    ) -> Cursor {
482        if opt_a.is_none() && opt_b.is_none() {
483            self.promote_and_drop(parent, base, pseq, pidx)
484        } else {
485            Cursor { seq: base, index: self.len(base) }
486        }
487    }
488
489    /// Handles structure deletion when the cursor is at the start of an empty slot.
490    fn escalate_delete(&mut self, parent: NodeId, slot: SeqId, fallback: Cursor) -> Cursor {
491        let Some((pseq, pidx)) = self.index_in_parent(parent) else {
492            return fallback;
493        };
494        let Some(kind) = self.nodes.get(parent).map(|n| n.kind.clone()) else {
495            return fallback;
496        };
497        let at_parent = Cursor {
498            seq: pseq,
499            index: pidx,
500        };
501        match kind {
502            // Empty Frac slots drop the frac or promote the sibling content.
503            Kind::Frac { num, den, .. } => {
504                let other = if slot == num { den } else { num };
505                if self.is_empty(other) {
506                    self.drop_and_step_out(parent, at_parent)
507                } else {
508                    self.promote_and_drop(parent, other, pseq, pidx)
509                }
510            }
511
512            // An empty radicand drops the radical, while the permanent empty degree steps out left.
513            Kind::Sqrt { radicand, .. } => {
514                if slot == radicand {
515                    self.drop_and_step_out(parent, at_parent)
516                } else {
517                    at_parent
518                }
519            }
520
521            // Empty single body wrappers are removed.
522            Kind::Delim { .. } | Kind::Accent { .. } | Kind::Styled { .. } => {
523                self.drop_and_step_out(parent, at_parent)
524            }
525
526            Kind::Script { base, sub, sup } => {
527                if slot == base {
528                    self.delete_empty_script_base(parent, base, sub, sup, pseq, pidx, at_parent)
529                } else {
530                    // Empty subscript or superscript slots are dropped before collapse checks.
531                    if let Some(Node {
532                        kind: Kind::Script { sub, sup, .. },
533                        ..
534                    }) = self.nodes.get_mut(parent)
535                    {
536                        if *sub == Some(slot) {
537                            *sub = None;
538                        } else if *sup == Some(slot) {
539                            *sup = None;
540                        }
541                    }
542                    self.free_seq(slot);
543                    let (rsub, rsup) = match self.kind(parent) {
544                        Some(Kind::Script { sub, sup, .. }) => (*sub, *sup),
545                        _ => (None, None),
546                    };
547                    self.collapse_to_base_or_stay(parent, base, rsub, rsup, pseq, pidx)
548                }
549            }
550
551            // Empty BigOp limits drop the operator or step into the nonempty other limit.
552            Kind::BigOp { lower, upper, .. } => {
553                let other = if slot == lower { upper } else { lower };
554                if self.is_empty(other) {
555                    self.drop_and_step_out(parent, at_parent)
556                } else {
557                    Cursor {
558                        seq: other,
559                        index: self.len(other),
560                    }
561                }
562            }
563
564            Kind::UnderOver {
565                base, over, under, ..
566            } => {
567                if slot == base {
568                    self.drop_and_step_out(parent, at_parent)
569                } else {
570                    // Empty over or under slots are dropped before collapse checks.
571                    if let Some(Node {
572                        kind: Kind::UnderOver { over, under, .. },
573                        ..
574                    }) = self.nodes.get_mut(parent)
575                    {
576                        if *over == Some(slot) {
577                            *over = None;
578                        } else if *under == Some(slot) {
579                            *under = None;
580                        }
581                    }
582                    self.free_seq(slot);
583                    let (rover, runder) = match self.kind(parent) {
584                        Some(Kind::UnderOver { over, under, .. }) => (*over, *under),
585                        _ => (None, None),
586                    };
587                    self.collapse_to_base_or_stay(parent, base, rover, runder, pseq, pidx)
588                }
589            }
590
591            // Empty matrix cells are valid, so the matrix is removed only when every cell is empty.
592            Kind::Matrix { rows, .. } => {
593                let all_empty = rows.iter().flatten().all(|&c| self.is_empty(c));
594                if all_empty {
595                    self.drop_and_step_out(parent, at_parent)
596                } else {
597                    nav::move_left(self, Cursor { seq: slot, index: 0 }).unwrap_or(fallback)
598                }
599            }
600
601            Kind::Atom(_) => fallback,
602        }
603    }
604
605    /// Returns the matrix node, row, and column for the cursor's cell when it is in a matrix.
606    fn matrix_of(&self, at: Cursor) -> Option<(NodeId, usize, usize)> {
607        let m = self.seq_parent(at.seq)?;
608        if let Some(Kind::Matrix { rows, .. }) = self.kind(m) {
609            for (r, row) in rows.iter().enumerate() {
610                if let Some(c) = row.iter().position(|&s| s == at.seq) {
611                    return Some((m, r, c));
612                }
613            }
614        }
615        None
616    }
617
618    fn matrix_cols(&self, m: NodeId) -> usize {
619        match self.kind(m) {
620            Some(Kind::Matrix { rows, .. }) => rows.first().map_or(0, |r| r.len()),
621            _ => 0,
622        }
623    }
624
625    /// Returns the shape of the matrix containing the cursor.
626    pub fn matrix_shape_at(&self, at: Cursor) -> Option<(usize, usize)> {
627        let (m, _, _) = self.matrix_of(at)?;
628        let Some(Kind::Matrix { rows, .. }) = self.kind(m) else {
629            return None;
630        };
631        Some((rows.len(), self.matrix_cols(m)))
632    }
633
634    /// Inserts a matrix row before or after the current row.
635    pub fn matrix_insert_row(&mut self, at: Cursor, side: Side) -> Cursor {
636        let Some((m, r, _c)) = self.matrix_of(at) else {
637            return at;
638        };
639        let cols = self.matrix_cols(m);
640        let new_row: Vec<SeqId> = (0..cols).map(|_| self.alloc_seq(Some(m))).collect();
641        let first = new_row.first().copied();
642        let idx = match side {
643            Side::Before => r,
644            Side::After => r + 1,
645        };
646        if let Some(Node {
647            kind: Kind::Matrix { rows, .. },
648            ..
649        }) = self.nodes.get_mut(m)
650        {
651            let idx = idx.min(rows.len());
652            rows.insert(idx, new_row);
653        }
654        first.map_or(at, |s| Cursor { seq: s, index: 0 })
655    }
656
657    /// Deletes the current matrix row when more than one row remains.
658    pub fn matrix_delete_row(&mut self, at: Cursor) -> Cursor {
659        let Some((m, r, _c)) = self.matrix_of(at) else {
660            return at;
661        };
662        let row_cells: Vec<SeqId> = match self.kind(m) {
663            Some(Kind::Matrix { rows, .. }) if rows.len() > 1 => rows[r].clone(),
664            _ => return at,
665        };
666        if let Some(Node {
667            kind: Kind::Matrix { rows, .. },
668            ..
669        }) = self.nodes.get_mut(m)
670        {
671            rows.remove(r);
672        }
673        for c in row_cells {
674            self.free_seq(c);
675        }
676        let target = match self.kind(m) {
677            Some(Kind::Matrix { rows, .. }) => rows.get(r.min(rows.len().saturating_sub(1))).and_then(|row| row.first().copied()),
678            _ => None,
679        };
680        target.map_or(at, |s| Cursor { seq: s, index: 0 })
681    }
682
683    /// Inserts a matrix column before or after the current column.
684    pub fn matrix_insert_col(&mut self, at: Cursor, side: Side) -> Cursor {
685        let Some((m, _r, c)) = self.matrix_of(at) else {
686            return at;
687        };
688        let nrows = match self.kind(m) {
689            Some(Kind::Matrix { rows, .. }) => rows.len(),
690            _ => 0,
691        };
692        let new_cells: Vec<SeqId> = (0..nrows).map(|_| self.alloc_seq(Some(m))).collect();
693        let idx = match side {
694            Side::Before => c,
695            Side::After => c + 1,
696        };
697        let first = new_cells.first().copied();
698        if let Some(Node {
699            kind: Kind::Matrix { rows, .. },
700            ..
701        }) = self.nodes.get_mut(m)
702        {
703            for (row, cell) in rows.iter_mut().zip(new_cells) {
704                let i = idx.min(row.len());
705                row.insert(i, cell);
706            }
707        }
708        first.map_or(at, |s| Cursor { seq: s, index: 0 })
709    }
710
711    /// Deletes the current matrix column when more than one column remains.
712    pub fn matrix_delete_col(&mut self, at: Cursor) -> Cursor {
713        let Some((m, _r, c)) = self.matrix_of(at) else {
714            return at;
715        };
716        if self.matrix_cols(m) <= 1 {
717            return at;
718        }
719        let mut removed = Vec::new();
720        if let Some(Node {
721            kind: Kind::Matrix { rows, .. },
722            ..
723        }) = self.nodes.get_mut(m)
724        {
725            for row in rows.iter_mut() {
726                if c < row.len() {
727                    removed.push(row.remove(c));
728                }
729            }
730        }
731        for cell in removed {
732            self.free_seq(cell);
733        }
734        let target = match self.kind(m) {
735            Some(Kind::Matrix { rows, .. }) => rows.first().and_then(|row| {
736                row.get(c.min(row.len().saturating_sub(1))).copied()
737            }),
738            _ => None,
739        };
740        target.map_or(at, |s| Cursor { seq: s, index: 0 })
741    }
742
743    /// Returns swap menu alternatives for `node`, excluding its current value.
744    pub fn swap_variants(&self, node: NodeId) -> Option<Vec<SwapVariant>> {
745        match self.kind(node)? {
746            Kind::Atom(_) | Kind::Frac { .. } | Kind::Script { .. } | Kind::Sqrt { .. } => None,
747            Kind::Delim { open, close, .. } => {
748                let (open, close) = (*open, *close);
749                Some(
750                    DELIM_PAIRS
751                        .iter()
752                        .filter(|&&(o, c, _)| o != open || c != close)
753                        .map(|&(open, close, label)| SwapVariant::Delim { open, close, label })
754                        .collect(),
755                )
756            }
757            Kind::BigOp { op, .. } => {
758                let current = op.latex.as_str();
759                Some(
760                    BIGOP_ALTS
761                        .iter()
762                        .filter(|&&(latex, _)| latex != current)
763                        .map(|&(latex, label)| SwapVariant::BigOp { latex, label })
764                        .collect(),
765                )
766            }
767            Kind::Accent { mark, .. } => {
768                let mark = *mark;
769                Some(
770                    ACCENT_ALTS
771                        .iter()
772                        .filter(|&&(m, _)| m != mark)
773                        .map(|&(mark, label)| SwapVariant::Accent { mark, label })
774                        .collect(),
775                )
776            }
777            // Only decoration for slots that are present varies.
778            Kind::UnderOver { over, under, over_deco, under_deco, .. } => {
779                let mut alts = Vec::new();
780                if over.is_some() {
781                    alts.extend(
782                        DECO_ALTS
783                            .iter()
784                            .filter(|&&(d, _)| d != *over_deco)
785                            .map(|&(deco, label)| SwapVariant::UnderOverDeco {
786                                over: deco,
787                                under: *under_deco,
788                                label,
789                            }),
790                    );
791                }
792                if under.is_some() {
793                    alts.extend(
794                        DECO_ALTS
795                            .iter()
796                            .filter(|&&(d, _)| d != *under_deco)
797                            .map(|&(deco, label)| SwapVariant::UnderOverDeco {
798                                over: *over_deco,
799                                under: deco,
800                                label,
801                            }),
802                    );
803                }
804                Some(alts)
805            }
806            // `\text{}` is a carve out where the caret enters instead of swapping.
807            Kind::Styled { variant, .. } if *variant == Variant::Text => None,
808            Kind::Styled { variant, .. } => {
809                let current = *variant;
810                Some(
811                    STYLED_ALTS
812                        .iter()
813                        .filter(|&&(v, _)| v != current)
814                        .map(|&(variant, label)| SwapVariant::Styled { variant, label })
815                        .collect(),
816                )
817            }
818            Kind::Matrix { env, .. } => {
819                let current = *env;
820                Some(
821                    MATRIX_ENV_ALTS
822                        .iter()
823                        .filter(|&&(e, _)| e != current)
824                        .map(|&(env, label)| SwapVariant::Matrix { env, label })
825                        .collect(),
826                )
827            }
828        }
829    }
830
831    /// Applies a chosen alternative by changing only the named tag fields.
832    pub fn apply_swap(&mut self, node: NodeId, variant: &SwapVariant) {
833        let Some(n) = self.nodes.get_mut(node) else {
834            return;
835        };
836        match (&mut n.kind, variant) {
837            (Kind::Delim { open, close, .. }, SwapVariant::Delim { open: o, close: c, .. }) => {
838                *open = *o;
839                *close = *c;
840            }
841            (Kind::BigOp { op, .. }, SwapVariant::BigOp { latex, .. }) => {
842                op.latex = (*latex).to_string();
843            }
844            (Kind::Accent { mark, .. }, SwapVariant::Accent { mark: m, .. }) => {
845                *mark = *m;
846            }
847            (
848                Kind::UnderOver { over_deco, under_deco, .. },
849                SwapVariant::UnderOverDeco { over, under, .. },
850            ) => {
851                *over_deco = *over;
852                *under_deco = *under;
853            }
854            (Kind::Styled { variant: v, .. }, SwapVariant::Styled { variant: nv, .. }) => {
855                *v = *nv;
856            }
857            (Kind::Matrix { env, .. }, SwapVariant::Matrix { env: e, .. }) => {
858                *env = *e;
859            }
860            _ => {}
861        }
862    }
863}
864
865/// Describes one swap menu alternative for a swappable structure.
866#[derive(Debug, Clone, PartialEq)]
867pub enum SwapVariant {
868    /// Delimiter pair replacement.
869    Delim {
870        /// Opening delimiter.
871        open: char,
872        /// Closing delimiter.
873        close: char,
874        /// Menu label.
875        label: &'static str,
876    },
877    /// Big operator replacement.
878    BigOp {
879        /// TeX command for the operator.
880        latex: &'static str,
881        /// Menu label.
882        label: &'static str,
883    },
884    /// Accent mark replacement.
885    Accent {
886        /// Accent mark.
887        mark: Mark,
888        /// Menu label.
889        label: &'static str,
890    },
891    /// Under or over decoration replacement.
892    UnderOverDeco {
893        /// Over slot decoration.
894        over: Deco,
895        /// Under slot decoration.
896        under: Deco,
897        /// Menu label.
898        label: &'static str,
899    },
900    /// Styled variant replacement.
901    Styled {
902        /// Math variant.
903        variant: Variant,
904        /// Menu label.
905        label: &'static str,
906    },
907    /// Matrix environment replacement.
908    Matrix {
909        /// Matrix environment.
910        env: MatrixEnv,
911        /// Menu label.
912        label: &'static str,
913    },
914}
915
916impl SwapVariant {
917    /// Returns the menu label for this swap alternative.
918    pub fn label(&self) -> &'static str {
919        match self {
920            SwapVariant::Delim { label, .. }
921            | SwapVariant::BigOp { label, .. }
922            | SwapVariant::Accent { label, .. }
923            | SwapVariant::UnderOverDeco { label, .. }
924            | SwapVariant::Styled { label, .. }
925            | SwapVariant::Matrix { label, .. } => label,
926        }
927    }
928}
929
930const DELIM_PAIRS: &[(char, char, &str)] = &[
931    ('(', ')', "( ) parentheses"),
932    ('[', ']', "[ ] brackets"),
933    ('{', '}', "{ } braces"),
934    ('|', '|', "| | bars"),
935    ('\u{2308}', '\u{2309}', "⌈ ⌉ ceiling"),
936    ('\u{230A}', '\u{230B}', "⌊ ⌋ floor"),
937    ('\u{27E8}', '\u{27E9}', "⟨ ⟩ angle"),
938];
939
940const BIGOP_ALTS: &[(&str, &str)] = &[
941    ("\\sum", "∑ sum"),
942    ("\\prod", "∏ product"),
943    ("\\coprod", "∐ coproduct"),
944    ("\\int", "∫ integral"),
945    ("\\iint", "∬ double integral"),
946    ("\\iiint", "∭ triple integral"),
947    ("\\oint", "∮ contour integral"),
948    ("\\bigcup", "⋃ union"),
949    ("\\bigcap", "⋂ intersection"),
950    ("\\bigsqcup", "⊔ disjoint union"),
951    ("\\biguplus", "⊎ uplus"),
952    ("\\bigoplus", "⊕ oplus"),
953    ("\\bigotimes", "⊗ otimes"),
954    ("\\bigodot", "⊙ odot"),
955    ("\\bigvee", "⋁ vee"),
956    ("\\bigwedge", "⋀ wedge"),
957];
958
959const ACCENT_ALTS: &[(Mark, &str)] = &[
960    (Mark::Hat, "hat"),
961    (Mark::Vec, "vec"),
962    (Mark::Bar, "bar"),
963    (Mark::Tilde, "tilde"),
964    (Mark::Dot, "dot"),
965    (Mark::Ddot, "double dot"),
966    (Mark::Widehat, "wide hat"),
967    (Mark::Widetilde, "wide tilde"),
968    (Mark::Overline, "overline"),
969    (Mark::Underline, "underline"),
970    (Mark::Check, "check"),
971    (Mark::Breve, "breve"),
972];
973
974const DECO_ALTS: &[(Deco, &str)] = &[
975    (Deco::None, "plain"),
976    (Deco::Brace, "brace"),
977    (Deco::Arrow, "arrow"),
978    (Deco::Line, "line"),
979];
980
981const STYLED_ALTS: &[(Variant, &str)] = &[
982    (Variant::Normal, "normal"),
983    (Variant::Bold, "bold"),
984    (Variant::Blackboard, "blackboard"),
985    (Variant::Calligraphic, "calligraphic"),
986    (Variant::Fraktur, "fraktur"),
987    (Variant::Roman, "roman"),
988    (Variant::SansSerif, "sans serif"),
989    (Variant::Typewriter, "typewriter"),
990    (Variant::OperatorName, "operator name"),
991];
992
993const MATRIX_ENV_ALTS: &[(MatrixEnv, &str)] = &[
994    (MatrixEnv::Matrix, "matrix"),
995    (MatrixEnv::Pmatrix, "( matrix )"),
996    (MatrixEnv::Bmatrix, "[ matrix ]"),
997    (MatrixEnv::Vmatrix, "| matrix |"),
998    (MatrixEnv::Cases, "cases"),
999    (MatrixEnv::Aligned, "aligned"),
1000    (MatrixEnv::Array, "array"),
1001];
1002
1003#[cfg(test)]
1004mod tests {
1005    use super::*;
1006    use crate::model::{Deco, MathClass};
1007
1008    fn atom(c: &str) -> Symbol {
1009        Symbol {
1010            latex: c.into(),
1011            class: MathClass::Ord,
1012        }
1013    }
1014
1015    fn fill(t: &mut Tree, seq: SeqId, s: &str) {
1016        let mut c = Cursor { seq, index: t.len(seq) };
1017        for ch in s.chars() {
1018            c = t.insert_atom(c, atom(&ch.to_string()));
1019        }
1020    }
1021
1022    #[test]
1023    fn insert_wires_parents_and_splices() {
1024        let mut t = Tree::new();
1025        let root = t.root();
1026        t.insert_new(root, 0, NewNode::Atom(atom("a")));
1027        let frac = t.insert_new(root, 1, NewNode::Frac(FracStyle::Bar));
1028        assert_eq!(t.child_seqs(frac).len(), 2);
1029        assert_eq!(t.seq_parent(t.child_seqs(frac)[0]), Some(frac));
1030        assert_eq!(t.index_in_parent(frac), Some((root, 1)));
1031    }
1032
1033    #[test]
1034    fn move_right_walks_source_order_through_a_fraction() {
1035        let mut t = Tree::new();
1036        let root = t.root();
1037        let frac = t.insert_new(root, 0, NewNode::Frac(FracStyle::Bar));
1038        let slots = t.child_seqs(frac);
1039        let (num, den) = (slots[0], slots[1]);
1040        let c = nav::move_right(&t, Cursor { seq: root, index: 0 }).unwrap();
1041        assert_eq!((c.seq, c.index), (num, 0));
1042        let c = nav::move_right(&t, c).unwrap();
1043        assert_eq!((c.seq, c.index), (den, 0));
1044        let c = nav::move_right(&t, c).unwrap();
1045        assert_eq!((c.seq, c.index), (root, 1));
1046        assert_eq!(nav::move_right(&t, c), None);
1047    }
1048
1049    #[test]
1050    fn drop_node_frees_the_whole_subtree() {
1051        let mut t = Tree::new();
1052        let root = t.root();
1053        let frac = t.insert_new(root, 0, NewNode::Frac(FracStyle::Bar));
1054        assert!(t.seqs.len() >= 3);
1055        t.drop_node(frac);
1056        assert_eq!(t.len(root), 0);
1057        assert_eq!(t.seqs.len(), 1);
1058    }
1059
1060    #[test]
1061    fn backspace_deletes_an_atom() {
1062        let mut t = Tree::new();
1063        let root = t.root();
1064        fill(&mut t, root, "ab");
1065        let c = t.delete_backward(Cursor { seq: root, index: 2 });
1066        assert_eq!((c.seq, c.index), (root, 1));
1067        assert_eq!(t.len(root), 1);
1068    }
1069
1070    #[test]
1071    fn backspace_in_empty_fraction_removes_it() {
1072        let mut t = Tree::new();
1073        let root = t.root();
1074        let c = t.insert_fraction(Cursor { seq: root, index: 0 }, FracStyle::Bar, None);
1075        // The empty numerator escalates to removing the frac.
1076        let c = t.delete_backward(c);
1077        assert_eq!((c.seq, c.index), (root, 0));
1078        assert_eq!(t.len(root), 0);
1079        assert_eq!(t.seqs.len(), 1);
1080    }
1081
1082    #[test]
1083    fn backspace_in_partial_fraction_unwraps_and_promotes() {
1084        let mut t = Tree::new();
1085        let root = t.root();
1086        let c = t.insert_fraction(Cursor { seq: root, index: 0 }, FracStyle::Bar, None);
1087        let num = c.seq;
1088        fill(&mut t, num, "xy");
1089        // Backspace from the empty denominator start.
1090        let frac = t.items(root)[0];
1091        let den = t.child_seqs(frac)[1];
1092        let c = t.delete_backward(Cursor { seq: den, index: 0 });
1093        // The numerator content is promoted to root.
1094        assert_eq!(t.len(root), 2);
1095        assert_eq!(c.seq, root);
1096    }
1097
1098    #[test]
1099    fn backspace_at_nonempty_slot_start_exits_without_deleting() {
1100        let mut t = Tree::new();
1101        let root = t.root();
1102        let c = t.insert_fraction(Cursor { seq: root, index: 0 }, FracStyle::Bar, None);
1103        let num = c.seq;
1104        fill(&mut t, num, "x");
1105        let before = t.len(num);
1106        let c = t.delete_backward(Cursor { seq: num, index: 0 });
1107        assert_eq!(t.len(num), before);
1108        assert_eq!(c.seq, root);
1109    }
1110
1111    #[test]
1112    fn fraction_wraps_a_selection() {
1113        let mut t = Tree::new();
1114        let root = t.root();
1115        fill(&mut t, root, "ab");
1116        let sel = Selection {
1117            seq: root,
1118            anchor: 0,
1119            focus: 2,
1120        };
1121        let c = t.insert_fraction(Cursor { seq: root, index: 0 }, FracStyle::Bar, Some(sel));
1122        let frac = t.items(root)[0];
1123        let num = t.child_seqs(frac)[0];
1124        assert_eq!(t.len(num), 2);
1125        assert_eq!(t.len(root), 1);
1126        assert_eq!(c.seq, t.child_seqs(frac)[1]);
1127    }
1128
1129    #[test]
1130    fn script_attaches_to_preceding_atom() {
1131        let mut t = Tree::new();
1132        let root = t.root();
1133        fill(&mut t, root, "x");
1134        let c = t.attach_script(Cursor { seq: root, index: 1 }, ScriptSlot::Sup);
1135        let script = t.items(root)[0];
1136        assert!(matches!(t.kind(script), Some(Kind::Script { .. })));
1137        // The base holds "x" and the cursor is in the empty sup.
1138        if let Some(Kind::Script { base, sup, .. }) = t.kind(script) {
1139            assert_eq!(t.len(*base), 1);
1140            assert_eq!(Some(c.seq), *sup);
1141        } else {
1142            panic!();
1143        }
1144    }
1145
1146    #[test]
1147    fn deleting_empty_script_unwraps_with_cursor_after_base() {
1148        let mut t = Tree::new();
1149        let root = t.root();
1150        t.insert_atom(Cursor { seq: root, index: 0 }, atom("x"));
1151        let c = t.attach_script(Cursor { seq: root, index: 1 }, ScriptSlot::Sup);
1152        let c = t.insert_atom(c, atom("2"));
1153        let c = t.delete_backward(c);
1154        let c = t.delete_backward(c);
1155        assert_eq!(t.len(root), 1);
1156        assert!(matches!(t.kind(t.items(root)[0]), Some(Kind::Atom(_))));
1157        assert_eq!((c.seq, c.index), (root, 1));
1158    }
1159
1160    #[test]
1161    fn typing_into_script_base_evicts_old_base_left() {
1162        let mut t = Tree::new();
1163        let root = t.root();
1164        t.insert_atom(Cursor { seq: root, index: 0 }, atom("x"));
1165        let c = t.attach_script(Cursor { seq: root, index: 1 }, ScriptSlot::Sup);
1166        t.insert_atom(c, atom("2"));
1167        let script = t.items(root)[0];
1168        let base = match t.kind(script) {
1169            Some(Kind::Script { base, .. }) => *base,
1170            _ => panic!(),
1171        };
1172        // Typing y at the base end evicts x to the left.
1173        let c = t.insert_atom(Cursor { seq: base, index: 1 }, atom("y"));
1174        assert_eq!(t.len(base), 1);
1175        assert_eq!((c.seq, c.index), (base, 1));
1176        assert_eq!(t.len(root), 2);
1177        assert!(matches!(t.kind(t.items(root)[0]), Some(Kind::Atom(_))));
1178        assert_eq!(t.items(root)[1], script);
1179    }
1180
1181    #[test]
1182    fn deleting_lone_script_base_dissolves_into_sequence() {
1183        let mut t = Tree::new();
1184        let root = t.root();
1185        t.insert_atom(Cursor { seq: root, index: 0 }, atom("x"));
1186        let c = t.attach_script(Cursor { seq: root, index: 1 }, ScriptSlot::Sup);
1187        t.insert_atom(c, atom("2"));
1188        let script = t.items(root)[0];
1189        let base = match t.kind(script) {
1190            Some(Kind::Script { base, .. }) => *base,
1191            _ => panic!(),
1192        };
1193        // Backspace at the base end deletes the base atom.
1194        let c = t.delete_backward(Cursor { seq: base, index: 1 });
1195        assert_eq!((c.seq, c.index), (base, 0));
1196        assert!(t.is_empty(base));
1197        // Backspace again with nothing to the left dissolves into a plain seq.
1198        let c = t.delete_backward(c);
1199        assert_eq!(t.len(root), 1);
1200        assert!(matches!(t.kind(t.items(root)[0]), Some(Kind::Atom(_))));
1201        assert_eq!((c.seq, c.index), (root, 0));
1202    }
1203
1204    #[test]
1205    fn deleting_empty_base_pulls_left_sibling_in() {
1206        let mut t = Tree::new();
1207        let root = t.root();
1208        // Backspace in the empty base pulls the left sibling in.
1209        t.insert_atom(Cursor { seq: root, index: 0 }, atom("a"));
1210        let c = t.attach_script(Cursor { seq: root, index: 1 }, ScriptSlot::Sup);
1211        // Pop the wrapped base back out to recreate `a` plus an empty base.
1212        let script = t.items(root)[0];
1213        let base = match t.kind(script) {
1214            Some(Kind::Script { base, .. }) => *base,
1215            _ => panic!(),
1216        };
1217        t.insert_atom(c, atom("2"));
1218        t.move_range(base, 0..1, root, 0);
1219        let c = t.delete_backward(Cursor { seq: base, index: 0 });
1220        assert_eq!(t.len(root), 1);
1221        assert_eq!(t.len(base), 1);
1222        assert_eq!((c.seq, c.index), (base, 1));
1223    }
1224
1225    #[test]
1226    fn backspace_in_empty_sqrt_radicand_drops_radical() {
1227        let mut t = Tree::new();
1228        let root = t.root();
1229        let c = t.insert_sqrt(Cursor { seq: root, index: 0 }, None);
1230        let c = t.delete_backward(c);
1231        assert_eq!(t.len(root), 0);
1232        assert_eq!((c.seq, c.index), (root, 0));
1233    }
1234
1235    #[test]
1236    fn backspace_in_empty_delim_drops_it() {
1237        let mut t = Tree::new();
1238        let root = t.root();
1239        let c = t.insert_delimiters(Cursor { seq: root, index: 0 }, '(', ')', None);
1240        let c = t.delete_backward(c);
1241        assert_eq!(t.len(root), 0);
1242        assert_eq!((c.seq, c.index), (root, 0));
1243    }
1244
1245    #[test]
1246    fn backspace_in_empty_bigop_limit_steps_into_nonempty_other() {
1247        let mut t = Tree::new();
1248        let root = t.root();
1249        let op = Symbol { latex: "\\sum".into(), class: MathClass::Op };
1250        t.insert_big_op(Cursor { seq: root, index: 0 }, op);
1251        let bigop = t.items(root)[0];
1252        let cs = t.child_seqs(bigop);
1253        let (upper, lower) = (cs[0], cs[1]);
1254        fill(&mut t, lower, "2");
1255        // Backspace from the empty upper limit steps into the nonempty lower.
1256        let c = t.delete_backward(Cursor { seq: upper, index: 0 });
1257        assert_eq!(t.len(root), 1);
1258        assert_eq!((c.seq, c.index), (lower, 1));
1259    }
1260
1261    #[test]
1262    fn backspace_in_empty_bigop_both_limits_empty_drops_op() {
1263        let mut t = Tree::new();
1264        let root = t.root();
1265        let op = Symbol { latex: "\\sum".into(), class: MathClass::Op };
1266        t.insert_big_op(Cursor { seq: root, index: 0 }, op);
1267        let bigop = t.items(root)[0];
1268        let lower = t.child_seqs(bigop)[1];
1269        let c = t.delete_backward(Cursor { seq: lower, index: 0 });
1270        assert_eq!(t.len(root), 0);
1271        assert_eq!((c.seq, c.index), (root, 0));
1272    }
1273
1274    #[test]
1275    fn backspace_in_underover_over_collapses_to_base() {
1276        let mut t = Tree::new();
1277        let root = t.root();
1278        let spec = UnderOverSpec { over: true, under: false, over_deco: Deco::None, under_deco: Deco::None };
1279        let c = t.insert_under_over(Cursor { seq: root, index: 0 }, spec, None);
1280        let node = t.items(root)[0];
1281        let over = match t.kind(node) {
1282            Some(Kind::UnderOver { over, .. }) => over.unwrap(),
1283            _ => panic!(),
1284        };
1285        fill(&mut t, c.seq, "x");
1286        // Backspace from the empty over slot drops over and promotes the base.
1287        let c = t.delete_backward(Cursor { seq: over, index: 0 });
1288        assert_eq!(t.len(root), 1);
1289        assert!(matches!(t.kind(t.items(root)[0]), Some(Kind::Atom(_))));
1290        assert_eq!((c.seq, c.index), (root, 1));
1291    }
1292
1293    #[test]
1294    fn backspace_in_all_empty_matrix_drops_it() {
1295        let mut t = Tree::new();
1296        let root = t.root();
1297        let c = t.insert_matrix(Cursor { seq: root, index: 0 }, MatrixEnv::Pmatrix, 2, 2);
1298        let c = t.delete_backward(c);
1299        assert_eq!(t.len(root), 0);
1300        assert_eq!((c.seq, c.index), (root, 0));
1301    }
1302
1303    #[test]
1304    fn vertical_nav_switches_fraction_slots() {
1305        let mut t = Tree::new();
1306        let root = t.root();
1307        let c = t.insert_fraction(Cursor { seq: root, index: 0 }, FracStyle::Bar, None);
1308        let num = c.seq;
1309        let frac = t.items(root)[0];
1310        let den = t.child_seqs(frac)[1];
1311        let down = nav::vertical(&t, Cursor { seq: num, index: 0 }, false).unwrap();
1312        assert_eq!(down.seq, den);
1313        let up = nav::vertical(&t, Cursor { seq: den, index: 0 }, true).unwrap();
1314        assert_eq!(up.seq, num);
1315    }
1316
1317    #[test]
1318    fn matrix_row_col_ops() {
1319        let mut t = Tree::new();
1320        let root = t.root();
1321        let c = t.insert_matrix(Cursor { seq: root, index: 0 }, MatrixEnv::Pmatrix, 2, 2);
1322        let m = t.items(root)[0];
1323        let count = |t: &Tree| match t.kind(m) {
1324            Some(Kind::Matrix { rows, .. }) => (rows.len(), rows[0].len()),
1325            _ => (0, 0),
1326        };
1327        assert_eq!(count(&t), (2, 2));
1328        let c = t.matrix_insert_row(c, Side::After);
1329        assert_eq!(count(&t), (3, 2));
1330        let c = t.matrix_insert_col(c, Side::Before);
1331        assert_eq!(count(&t), (3, 3));
1332        let c = t.matrix_delete_row(c);
1333        assert_eq!(count(&t), (2, 3));
1334        let _c = t.matrix_delete_col(c);
1335        assert_eq!(count(&t), (2, 2));
1336    }
1337
1338    #[test]
1339    fn matrix_shape_at_reports_current_grid_size_and_none_outside_a_matrix() {
1340        let mut t = Tree::new();
1341        let root = t.root();
1342        let c = t.insert_matrix(Cursor { seq: root, index: 0 }, MatrixEnv::Pmatrix, 2, 2);
1343        assert_eq!(t.matrix_shape_at(c), Some((2, 2)));
1344        let c = t.matrix_insert_row(c, Side::After);
1345        assert_eq!(t.matrix_shape_at(c), Some((3, 2)));
1346
1347        // The document root is outside any matrix.
1348        assert_eq!(t.matrix_shape_at(Cursor { seq: root, index: 0 }), None);
1349    }
1350
1351    #[test]
1352    fn swap_variants_for_delim_excludes_the_current_pair() {
1353        let mut t = Tree::new();
1354        let root = t.root();
1355        t.insert_delimiters(Cursor { seq: root, index: 0 }, '(', ')', None);
1356        let delim = t.items(root)[0];
1357        let variants = t.swap_variants(delim).expect("Delim should be swappable");
1358        assert!(variants.iter().all(|v| !matches!(
1359            v,
1360            SwapVariant::Delim { open: '(', close: ')', .. }
1361        )));
1362        assert!(variants
1363            .iter()
1364            .any(|v| matches!(v, SwapVariant::Delim { open: '[', close: ']', .. })));
1365    }
1366
1367    #[test]
1368    fn apply_swap_changes_only_the_tag_field() {
1369        let mut t = Tree::new();
1370        let root = t.root();
1371        let c = t.insert_delimiters(Cursor { seq: root, index: 0 }, '(', ')', None);
1372        fill(&mut t, c.seq, "x");
1373        let delim = t.items(root)[0];
1374        let body_before = t.child_seqs(delim)[0];
1375
1376        t.apply_swap(delim, &SwapVariant::Delim { open: '[', close: ']', label: "[ ]" });
1377        match t.kind(delim) {
1378            Some(Kind::Delim { open, close, body }) => {
1379                assert_eq!((*open, *close), ('[', ']'));
1380                assert_eq!(*body, body_before);
1381                assert_eq!(t.len(*body), 1);
1382            }
1383            _ => panic!("expected Delim"),
1384        }
1385    }
1386
1387    #[test]
1388    fn swap_variants_none_for_non_swappable_kinds() {
1389        let mut t = Tree::new();
1390        let root = t.root();
1391
1392        t.insert_atom(Cursor { seq: root, index: 0 }, atom("x"));
1393        let atom_node = t.items(root)[0];
1394        assert!(t.swap_variants(atom_node).is_none());
1395
1396        let c = t.insert_fraction(Cursor { seq: root, index: 1 }, FracStyle::Bar, None);
1397        let _ = c;
1398        let frac = t.items(root)[1];
1399        assert!(t.swap_variants(frac).is_none());
1400
1401        let c = t.insert_sqrt(Cursor { seq: root, index: 2 }, None);
1402        let _ = c;
1403        let sqrt = t.items(root)[2];
1404        assert!(t.swap_variants(sqrt).is_none());
1405    }
1406
1407    #[test]
1408    fn swap_variants_none_for_text_carve_out() {
1409        let mut t = Tree::new();
1410        let root = t.root();
1411        t.insert_styled(Cursor { seq: root, index: 0 }, Variant::Text, None);
1412        let text = t.items(root)[0];
1413        assert!(t.swap_variants(text).is_none());
1414    }
1415
1416    #[test]
1417    fn swap_variants_for_styled_font_excludes_current_and_text() {
1418        let mut t = Tree::new();
1419        let root = t.root();
1420        t.insert_styled(Cursor { seq: root, index: 0 }, Variant::Bold, None);
1421        let styled = t.items(root)[0];
1422        let variants = t.swap_variants(styled).expect("Styled(Bold) should be swappable");
1423        assert!(variants
1424            .iter()
1425            .all(|v| !matches!(v, SwapVariant::Styled { variant: Variant::Bold, .. })));
1426        assert!(variants
1427            .iter()
1428            .all(|v| !matches!(v, SwapVariant::Styled { variant: Variant::Text, .. })));
1429    }
1430
1431    #[test]
1432    fn swap_variants_for_underover_only_varies_present_slots() {
1433        let mut t = Tree::new();
1434        let root = t.root();
1435        // With over only, every alternative leaves `under` at its absent equivalent default.
1436        let spec = UnderOverSpec {
1437            over: true,
1438            under: false,
1439            over_deco: Deco::Brace,
1440            under_deco: Deco::None,
1441        };
1442        t.insert_under_over(Cursor { seq: root, index: 0 }, spec, None);
1443        let node = t.items(root)[0];
1444        let variants = t.swap_variants(node).expect("UnderOver should be swappable");
1445        assert!(!variants.is_empty());
1446        for v in &variants {
1447            match v {
1448                SwapVariant::UnderOverDeco { over, under, .. } => {
1449                    assert_ne!(*over, Deco::Brace);
1450                    assert_eq!(*under, Deco::None);
1451                }
1452                _ => panic!("expected UnderOverDeco"),
1453            }
1454        }
1455    }
1456}