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