use std::ops::Range;
use crate::command::Side;
use crate::model::{
Cursor, Deco, FracStyle, Kind, Mark, MatrixEnv, Node, NodeId, ScriptSlot, Selection, Seq,
SeqId, Symbol, Tree, UnderOverSpec, Variant,
};
use crate::nav;
pub(crate) enum NewNode {
Atom(Symbol),
Frac(FracStyle),
Script(ScriptSlot),
BigOp(Symbol),
Sqrt,
Delim { open: char, close: char },
Accent(Mark),
UnderOver(UnderOverSpec),
Styled(Variant),
Matrix { env: MatrixEnv, rows: usize, cols: usize },
}
impl Tree {
pub(crate) fn alloc_seq(&mut self, parent: Option<NodeId>) -> SeqId {
self.seqs.insert(Seq {
parent,
items: Vec::new(),
})
}
fn build_kind(&mut self, spec: NewNode) -> Kind {
match spec {
NewNode::Atom(sym) => Kind::Atom(sym),
NewNode::Frac(style) => Kind::Frac {
num: self.alloc_seq(None),
den: self.alloc_seq(None),
style,
},
NewNode::Script(slot) => {
let base = self.alloc_seq(None);
let (sub, sup) = match slot {
ScriptSlot::Sub => (Some(self.alloc_seq(None)), None),
ScriptSlot::Sup => (None, Some(self.alloc_seq(None))),
};
Kind::Script { base, sub, sup }
}
NewNode::BigOp(op) => Kind::BigOp {
op,
lower: self.alloc_seq(None),
upper: self.alloc_seq(None),
},
NewNode::Sqrt => Kind::Sqrt {
index: self.alloc_seq(None),
radicand: self.alloc_seq(None),
},
NewNode::Delim { open, close } => Kind::Delim {
open,
close,
body: self.alloc_seq(None),
},
NewNode::Accent(mark) => Kind::Accent {
mark,
base: self.alloc_seq(None),
},
NewNode::UnderOver(spec) => Kind::UnderOver {
base: self.alloc_seq(None),
over: spec.over.then(|| self.alloc_seq(None)),
under: spec.under.then(|| self.alloc_seq(None)),
over_deco: spec.over_deco,
under_deco: spec.under_deco,
},
NewNode::Styled(variant) => Kind::Styled {
variant,
content: self.alloc_seq(None),
},
NewNode::Matrix { env, rows, cols } => {
let grid = (0..rows)
.map(|_| (0..cols).map(|_| self.alloc_seq(None)).collect())
.collect();
Kind::Matrix { env, rows: grid }
}
}
}
pub(crate) fn insert_new(&mut self, seq: SeqId, index: usize, spec: NewNode) -> NodeId {
let kind = self.build_kind(spec);
let node_id = self.nodes.insert(Node { parent: seq, kind });
for s in self.child_seqs(node_id) {
if let Some(sq) = self.seqs.get_mut(s) {
sq.parent = Some(node_id);
}
}
if let Some(sq) = self.seqs.get_mut(seq) {
let i = index.min(sq.items.len());
sq.items.insert(i, node_id);
}
node_id
}
pub(crate) fn detach(&mut self, node: NodeId) -> NodeId {
if let Some((parent, idx)) = self.index_in_parent(node) {
if let Some(sq) = self.seqs.get_mut(parent) {
sq.items.remove(idx);
}
}
node
}
pub(crate) fn drop_node(&mut self, node: NodeId) {
self.detach(node);
self.free_node(node);
}
fn free_node(&mut self, node: NodeId) {
for s in self.child_seqs(node) {
self.free_seq(s);
}
self.nodes.remove(node);
}
fn free_seq(&mut self, seq: SeqId) {
let items: Vec<NodeId> = self
.seqs
.get(seq)
.map(|s| s.items.clone())
.unwrap_or_default();
for n in items {
self.free_node(n);
}
self.seqs.remove(seq);
}
pub(crate) fn move_range(&mut self, src: SeqId, range: Range<usize>, dst: SeqId, at: usize) {
let moved: Vec<NodeId> = self
.seqs
.get_mut(src)
.map(|s| s.items.drain(range).collect())
.unwrap_or_default();
for &n in &moved {
if let Some(nd) = self.nodes.get_mut(n) {
nd.parent = dst;
}
}
if let Some(d) = self.seqs.get_mut(dst) {
let at = at.min(d.items.len());
d.items.splice(at..at, moved);
}
}
}
impl Tree {
fn make_room_in_script_base(&mut self, at: Cursor) -> Cursor {
if !self.is_empty(at.seq) {
if let Some(script) = self.script_base_node(at.seq) {
if let Some((pseq, pidx)) = self.index_in_parent(script) {
let n = self.len(at.seq);
self.move_range(at.seq, 0..n, pseq, pidx);
return Cursor {
seq: at.seq,
index: 0,
};
}
}
}
at
}
pub fn insert_atom(&mut self, at: Cursor, sym: Symbol) -> Cursor {
let at = self.make_room_in_script_base(at);
self.insert_new(at.seq, at.index, NewNode::Atom(sym));
Cursor {
seq: at.seq,
index: at.index + 1,
}
}
fn insert_struct(
&mut self,
at: Cursor,
sel: Option<Selection>,
spec: NewNode,
wrap_slot: usize,
) -> (NodeId, bool) {
match sel {
None => {
let at = self.make_room_in_script_base(at);
(self.insert_new(at.seq, at.index, spec), false)
}
Some(s) => {
let lo = s.anchor.min(s.focus);
let hi = s.anchor.max(s.focus);
let node = self.insert_new(s.seq, lo, spec);
let slot = self.child_seqs(node)[wrap_slot];
self.move_range(s.seq, lo + 1..hi + 1, slot, 0);
(node, true)
}
}
}
pub fn insert_fraction(&mut self, at: Cursor, style: FracStyle, sel: Option<Selection>) -> Cursor {
let (node, wrapped) = self.insert_struct(at, sel, NewNode::Frac(style), 0);
let slots = self.child_seqs(node);
let (num, den) = (slots[0], slots[1]);
if wrapped {
Cursor { seq: den, index: 0 }
} else {
Cursor { seq: num, index: 0 }
}
}
pub fn insert_sqrt(&mut self, at: Cursor, sel: Option<Selection>) -> Cursor {
let (node, _wrapped) = self.insert_struct(at, sel, NewNode::Sqrt, 1);
let radicand = self.child_seqs(node)[1];
Cursor {
seq: radicand,
index: 0,
}
}
pub fn insert_delimiters(
&mut self,
at: Cursor,
open: char,
close: char,
sel: Option<Selection>,
) -> Cursor {
let (node, wrapped) = self.insert_struct(at, sel, NewNode::Delim { open, close }, 0);
let body = self.child_seqs(node)[0];
Cursor {
seq: body,
index: if wrapped { self.len(body) } else { 0 },
}
}
pub fn insert_accent(&mut self, at: Cursor, mark: Mark, sel: Option<Selection>) -> Cursor {
let (node, wrapped) = self.insert_struct(at, sel, NewNode::Accent(mark), 0);
let base = self.child_seqs(node)[0];
Cursor {
seq: base,
index: if wrapped { self.len(base) } else { 0 },
}
}
pub fn insert_styled(&mut self, at: Cursor, variant: Variant, sel: Option<Selection>) -> Cursor {
let (node, wrapped) = self.insert_struct(at, sel, NewNode::Styled(variant), 0);
let content = self.child_seqs(node)[0];
Cursor {
seq: content,
index: if wrapped { self.len(content) } else { 0 },
}
}
pub fn insert_under_over(&mut self, at: Cursor, spec: UnderOverSpec, sel: Option<Selection>) -> Cursor {
let wrap_slot = if spec.over { 1 } else { 0 };
let (node, _wrapped) = self.insert_struct(at, sel, NewNode::UnderOver(spec), wrap_slot);
let base = self.child_seqs(node)[wrap_slot];
Cursor { seq: base, index: 0 }
}
pub fn insert_matrix(&mut self, at: Cursor, env: MatrixEnv, rows: usize, cols: usize) -> Cursor {
let rows = rows.max(1);
let cols = cols.max(1);
let node = self.insert_new(at.seq, at.index, NewNode::Matrix { env, rows, cols });
let first = self.child_seqs(node)[0];
Cursor {
seq: first,
index: 0,
}
}
pub fn insert_big_op(&mut self, at: Cursor, op: Symbol) -> Cursor {
self.insert_new(at.seq, at.index, NewNode::BigOp(op));
Cursor {
seq: at.seq,
index: at.index + 1,
}
}
pub fn bigop_limit_target(&self, at: Cursor, which: ScriptSlot) -> Option<Cursor> {
if at.index == 0 {
return None;
}
let prev = self.items(at.seq)[at.index - 1];
if let Some(Kind::BigOp { lower, upper, .. }) = self.kind(prev) {
let target = match which {
ScriptSlot::Sub => *lower,
ScriptSlot::Sup => *upper,
};
return Some(Cursor {
seq: target,
index: self.len(target),
});
}
None
}
pub fn attach_script(&mut self, at: Cursor, which: ScriptSlot) -> Cursor {
if at.index > 0 {
let prev = self.items(at.seq)[at.index - 1];
if matches!(self.kind(prev), Some(Kind::Script { .. })) {
let slot = self.script_slot(prev, which);
return Cursor {
seq: slot,
index: self.len(slot),
};
}
let script = self.insert_new(at.seq, at.index, NewNode::Script(which));
let base = match self.kind(script) {
Some(Kind::Script { base, .. }) => *base,
_ => unreachable!(),
};
self.move_range(at.seq, at.index - 1..at.index, base, 0);
let slot = self.script_slot(script, which);
return Cursor { seq: slot, index: 0 };
}
let script = self.insert_new(at.seq, at.index, NewNode::Script(which));
let slot = self.script_slot(script, which);
Cursor { seq: slot, index: 0 }
}
fn script_slot(&mut self, node: NodeId, which: ScriptSlot) -> SeqId {
let existing = match self.kind(node) {
Some(Kind::Script { sub, sup, .. }) => match which {
ScriptSlot::Sub => *sub,
ScriptSlot::Sup => *sup,
},
_ => None,
};
if let Some(s) = existing {
return s;
}
let s = self.alloc_seq(Some(node));
if let Some(Node {
kind: Kind::Script { sub, sup, .. },
..
}) = self.nodes.get_mut(node)
{
match which {
ScriptSlot::Sub => *sub = Some(s),
ScriptSlot::Sup => *sup = Some(s),
}
}
s
}
pub fn delete_backward(&mut self, at: Cursor) -> Cursor {
if at.index > 0 {
let prev = self.items(at.seq)[at.index - 1];
if self.child_seqs(prev).is_empty() {
self.drop_node(prev);
return Cursor {
seq: at.seq,
index: at.index - 1,
};
}
return nav::move_left(self, at).unwrap_or(at);
}
if self.is_empty(at.seq) {
if let Some(parent) = self.seq_parent(at.seq) {
return self.escalate_delete(parent, at.seq, at);
}
return at;
}
nav::move_left(self, at).unwrap_or(at)
}
pub fn delete_forward(&mut self, at: Cursor) -> Cursor {
if at.index < self.len(at.seq) {
let next = self.items(at.seq)[at.index];
if self.child_seqs(next).is_empty() {
self.drop_node(next);
return Cursor {
seq: at.seq,
index: at.index,
};
}
return nav::move_right(self, at).unwrap_or(at);
}
if self.is_empty(at.seq) {
if let Some(parent) = self.seq_parent(at.seq) {
return self.escalate_delete(parent, at.seq, at);
}
return at;
}
nav::move_right(self, at).unwrap_or(at)
}
pub fn delete_selection(&mut self, sel: Selection) -> Cursor {
let lo = sel.anchor.min(sel.focus);
let hi = sel.anchor.max(sel.focus);
let victims: Vec<NodeId> = self.items(sel.seq)[lo..hi.min(self.len(sel.seq))].to_vec();
for n in victims {
self.drop_node(n);
}
Cursor {
seq: sel.seq,
index: lo,
}
}
fn drop_and_step_out(&mut self, parent: NodeId, at_parent: Cursor) -> Cursor {
self.drop_node(parent);
at_parent
}
fn promote_and_drop(&mut self, parent: NodeId, from: SeqId, into: SeqId, at: usize) -> Cursor {
let n = self.len(from);
self.move_range(from, 0..n, into, at);
self.drop_node(parent);
Cursor { seq: into, index: at + n }
}
fn delete_empty_script_base(
&mut self,
parent: NodeId,
base: SeqId,
sub: Option<SeqId>,
sup: Option<SeqId>,
pseq: SeqId,
pidx: usize,
at_parent: Cursor,
) -> Cursor {
if pidx > 0 {
self.move_range(pseq, pidx - 1..pidx, base, 0);
return Cursor { seq: base, index: self.len(base) };
}
let mut offset = 0;
for s in [Some(base), sub, sup].into_iter().flatten() {
let n = self.len(s);
if n > 0 {
self.move_range(s, 0..n, pseq, pidx + offset);
offset += n;
}
}
self.drop_and_step_out(parent, at_parent)
}
fn collapse_to_base_or_stay(
&mut self,
parent: NodeId,
base: SeqId,
opt_a: Option<SeqId>,
opt_b: Option<SeqId>,
pseq: SeqId,
pidx: usize,
) -> Cursor {
if opt_a.is_none() && opt_b.is_none() {
self.promote_and_drop(parent, base, pseq, pidx)
} else {
Cursor { seq: base, index: self.len(base) }
}
}
fn escalate_delete(&mut self, parent: NodeId, slot: SeqId, fallback: Cursor) -> Cursor {
let Some((pseq, pidx)) = self.index_in_parent(parent) else {
return fallback;
};
let Some(kind) = self.nodes.get(parent).map(|n| n.kind.clone()) else {
return fallback;
};
let at_parent = Cursor {
seq: pseq,
index: pidx,
};
match kind {
Kind::Frac { num, den, .. } => {
let other = if slot == num { den } else { num };
if self.is_empty(other) {
self.drop_and_step_out(parent, at_parent)
} else {
self.promote_and_drop(parent, other, pseq, pidx)
}
}
Kind::Sqrt { radicand, .. } => {
if slot == radicand {
self.drop_and_step_out(parent, at_parent)
} else {
at_parent
}
}
Kind::Delim { .. } | Kind::Accent { .. } | Kind::Styled { .. } => {
self.drop_and_step_out(parent, at_parent)
}
Kind::Script { base, sub, sup } => {
if slot == base {
self.delete_empty_script_base(parent, base, sub, sup, pseq, pidx, at_parent)
} else {
if let Some(Node {
kind: Kind::Script { sub, sup, .. },
..
}) = self.nodes.get_mut(parent)
{
if *sub == Some(slot) {
*sub = None;
} else if *sup == Some(slot) {
*sup = None;
}
}
self.free_seq(slot);
let (rsub, rsup) = match self.kind(parent) {
Some(Kind::Script { sub, sup, .. }) => (*sub, *sup),
_ => (None, None),
};
self.collapse_to_base_or_stay(parent, base, rsub, rsup, pseq, pidx)
}
}
Kind::BigOp { lower, upper, .. } => {
let other = if slot == lower { upper } else { lower };
if self.is_empty(other) {
self.drop_and_step_out(parent, at_parent)
} else {
Cursor {
seq: other,
index: self.len(other),
}
}
}
Kind::UnderOver {
base, over, under, ..
} => {
if slot == base {
self.drop_and_step_out(parent, at_parent)
} else {
if let Some(Node {
kind: Kind::UnderOver { over, under, .. },
..
}) = self.nodes.get_mut(parent)
{
if *over == Some(slot) {
*over = None;
} else if *under == Some(slot) {
*under = None;
}
}
self.free_seq(slot);
let (rover, runder) = match self.kind(parent) {
Some(Kind::UnderOver { over, under, .. }) => (*over, *under),
_ => (None, None),
};
self.collapse_to_base_or_stay(parent, base, rover, runder, pseq, pidx)
}
}
Kind::Matrix { rows, .. } => {
let all_empty = rows.iter().flatten().all(|&c| self.is_empty(c));
if all_empty {
self.drop_and_step_out(parent, at_parent)
} else {
nav::move_left(self, Cursor { seq: slot, index: 0 }).unwrap_or(fallback)
}
}
Kind::Atom(_) => fallback,
}
}
fn matrix_of(&self, at: Cursor) -> Option<(NodeId, usize, usize)> {
let m = self.seq_parent(at.seq)?;
if let Some(Kind::Matrix { rows, .. }) = self.kind(m) {
for (r, row) in rows.iter().enumerate() {
if let Some(c) = row.iter().position(|&s| s == at.seq) {
return Some((m, r, c));
}
}
}
None
}
fn matrix_cols(&self, m: NodeId) -> usize {
match self.kind(m) {
Some(Kind::Matrix { rows, .. }) => rows.first().map_or(0, |r| r.len()),
_ => 0,
}
}
pub fn matrix_shape_at(&self, at: Cursor) -> Option<(usize, usize)> {
let (m, _, _) = self.matrix_of(at)?;
let Some(Kind::Matrix { rows, .. }) = self.kind(m) else {
return None;
};
Some((rows.len(), self.matrix_cols(m)))
}
pub fn matrix_insert_row(&mut self, at: Cursor, side: Side) -> Cursor {
let Some((m, r, _c)) = self.matrix_of(at) else {
return at;
};
let cols = self.matrix_cols(m);
let new_row: Vec<SeqId> = (0..cols).map(|_| self.alloc_seq(Some(m))).collect();
let first = new_row.first().copied();
let idx = match side {
Side::Before => r,
Side::After => r + 1,
};
if let Some(Node {
kind: Kind::Matrix { rows, .. },
..
}) = self.nodes.get_mut(m)
{
let idx = idx.min(rows.len());
rows.insert(idx, new_row);
}
first.map_or(at, |s| Cursor { seq: s, index: 0 })
}
pub fn matrix_delete_row(&mut self, at: Cursor) -> Cursor {
let Some((m, r, _c)) = self.matrix_of(at) else {
return at;
};
let row_cells: Vec<SeqId> = match self.kind(m) {
Some(Kind::Matrix { rows, .. }) if rows.len() > 1 => rows[r].clone(),
_ => return at,
};
if let Some(Node {
kind: Kind::Matrix { rows, .. },
..
}) = self.nodes.get_mut(m)
{
rows.remove(r);
}
for c in row_cells {
self.free_seq(c);
}
let target = match self.kind(m) {
Some(Kind::Matrix { rows, .. }) => rows.get(r.min(rows.len().saturating_sub(1))).and_then(|row| row.first().copied()),
_ => None,
};
target.map_or(at, |s| Cursor { seq: s, index: 0 })
}
pub fn matrix_insert_col(&mut self, at: Cursor, side: Side) -> Cursor {
let Some((m, _r, c)) = self.matrix_of(at) else {
return at;
};
let nrows = match self.kind(m) {
Some(Kind::Matrix { rows, .. }) => rows.len(),
_ => 0,
};
let new_cells: Vec<SeqId> = (0..nrows).map(|_| self.alloc_seq(Some(m))).collect();
let idx = match side {
Side::Before => c,
Side::After => c + 1,
};
let first = new_cells.first().copied();
if let Some(Node {
kind: Kind::Matrix { rows, .. },
..
}) = self.nodes.get_mut(m)
{
for (row, cell) in rows.iter_mut().zip(new_cells) {
let i = idx.min(row.len());
row.insert(i, cell);
}
}
first.map_or(at, |s| Cursor { seq: s, index: 0 })
}
pub fn matrix_delete_col(&mut self, at: Cursor) -> Cursor {
let Some((m, _r, c)) = self.matrix_of(at) else {
return at;
};
if self.matrix_cols(m) <= 1 {
return at;
}
let mut removed = Vec::new();
if let Some(Node {
kind: Kind::Matrix { rows, .. },
..
}) = self.nodes.get_mut(m)
{
for row in rows.iter_mut() {
if c < row.len() {
removed.push(row.remove(c));
}
}
}
for cell in removed {
self.free_seq(cell);
}
let target = match self.kind(m) {
Some(Kind::Matrix { rows, .. }) => rows.first().and_then(|row| {
row.get(c.min(row.len().saturating_sub(1))).copied()
}),
_ => None,
};
target.map_or(at, |s| Cursor { seq: s, index: 0 })
}
pub fn swap_variants(&self, node: NodeId) -> Option<Vec<SwapVariant>> {
match self.kind(node)? {
Kind::Atom(_) | Kind::Frac { .. } | Kind::Script { .. } | Kind::Sqrt { .. } => None,
Kind::Delim { open, close, .. } => {
let (open, close) = (*open, *close);
Some(
DELIM_PAIRS
.iter()
.filter(|&&(o, c, _)| o != open || c != close)
.map(|&(open, close, label)| SwapVariant::Delim { open, close, label })
.collect(),
)
}
Kind::BigOp { op, .. } => {
let current = op.latex.as_str();
Some(
BIGOP_ALTS
.iter()
.filter(|&&(latex, _)| latex != current)
.map(|&(latex, label)| SwapVariant::BigOp { latex, label })
.collect(),
)
}
Kind::Accent { mark, .. } => {
let mark = *mark;
Some(
ACCENT_ALTS
.iter()
.filter(|&&(m, _)| m != mark)
.map(|&(mark, label)| SwapVariant::Accent { mark, label })
.collect(),
)
}
Kind::UnderOver { over, under, over_deco, under_deco, .. } => {
let mut alts = Vec::new();
if over.is_some() {
alts.extend(
DECO_ALTS
.iter()
.filter(|&&(d, _)| d != *over_deco)
.map(|&(deco, label)| SwapVariant::UnderOverDeco {
over: deco,
under: *under_deco,
label,
}),
);
}
if under.is_some() {
alts.extend(
DECO_ALTS
.iter()
.filter(|&&(d, _)| d != *under_deco)
.map(|&(deco, label)| SwapVariant::UnderOverDeco {
over: *over_deco,
under: deco,
label,
}),
);
}
Some(alts)
}
Kind::Styled { variant, .. } if *variant == Variant::Text => None,
Kind::Styled { variant, .. } => {
let current = *variant;
Some(
STYLED_ALTS
.iter()
.filter(|&&(v, _)| v != current)
.map(|&(variant, label)| SwapVariant::Styled { variant, label })
.collect(),
)
}
Kind::Matrix { env, .. } => {
let current = *env;
Some(
MATRIX_ENV_ALTS
.iter()
.filter(|&&(e, _)| e != current)
.map(|&(env, label)| SwapVariant::Matrix { env, label })
.collect(),
)
}
}
}
pub fn apply_swap(&mut self, node: NodeId, variant: &SwapVariant) {
let Some(n) = self.nodes.get_mut(node) else {
return;
};
match (&mut n.kind, variant) {
(Kind::Delim { open, close, .. }, SwapVariant::Delim { open: o, close: c, .. }) => {
*open = *o;
*close = *c;
}
(Kind::BigOp { op, .. }, SwapVariant::BigOp { latex, .. }) => {
op.latex = (*latex).to_string();
}
(Kind::Accent { mark, .. }, SwapVariant::Accent { mark: m, .. }) => {
*mark = *m;
}
(
Kind::UnderOver { over_deco, under_deco, .. },
SwapVariant::UnderOverDeco { over, under, .. },
) => {
*over_deco = *over;
*under_deco = *under;
}
(Kind::Styled { variant: v, .. }, SwapVariant::Styled { variant: nv, .. }) => {
*v = *nv;
}
(Kind::Matrix { env, .. }, SwapVariant::Matrix { env: e, .. }) => {
*env = *e;
}
_ => {}
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum SwapVariant {
Delim {
open: char,
close: char,
label: &'static str,
},
BigOp {
latex: &'static str,
label: &'static str,
},
Accent {
mark: Mark,
label: &'static str,
},
UnderOverDeco {
over: Deco,
under: Deco,
label: &'static str,
},
Styled {
variant: Variant,
label: &'static str,
},
Matrix {
env: MatrixEnv,
label: &'static str,
},
}
impl SwapVariant {
pub fn label(&self) -> &'static str {
match self {
SwapVariant::Delim { label, .. }
| SwapVariant::BigOp { label, .. }
| SwapVariant::Accent { label, .. }
| SwapVariant::UnderOverDeco { label, .. }
| SwapVariant::Styled { label, .. }
| SwapVariant::Matrix { label, .. } => label,
}
}
}
const DELIM_PAIRS: &[(char, char, &str)] = &[
('(', ')', "( ) parentheses"),
('[', ']', "[ ] brackets"),
('{', '}', "{ } braces"),
('|', '|', "| | bars"),
('\u{2308}', '\u{2309}', "⌈ ⌉ ceiling"),
('\u{230A}', '\u{230B}', "⌊ ⌋ floor"),
('\u{27E8}', '\u{27E9}', "⟨ ⟩ angle"),
];
const BIGOP_ALTS: &[(&str, &str)] = &[
("\\sum", "∑ sum"),
("\\prod", "∏ product"),
("\\coprod", "∐ coproduct"),
("\\int", "∫ integral"),
("\\iint", "∬ double integral"),
("\\iiint", "∭ triple integral"),
("\\oint", "∮ contour integral"),
("\\bigcup", "⋃ union"),
("\\bigcap", "⋂ intersection"),
("\\bigsqcup", "⊔ disjoint union"),
("\\biguplus", "⊎ uplus"),
("\\bigoplus", "⊕ oplus"),
("\\bigotimes", "⊗ otimes"),
("\\bigodot", "⊙ odot"),
("\\bigvee", "⋁ vee"),
("\\bigwedge", "⋀ wedge"),
];
const ACCENT_ALTS: &[(Mark, &str)] = &[
(Mark::Hat, "hat"),
(Mark::Vec, "vec"),
(Mark::Bar, "bar"),
(Mark::Tilde, "tilde"),
(Mark::Dot, "dot"),
(Mark::Ddot, "double dot"),
(Mark::Widehat, "wide hat"),
(Mark::Widetilde, "wide tilde"),
(Mark::Overline, "overline"),
(Mark::Underline, "underline"),
(Mark::Check, "check"),
(Mark::Breve, "breve"),
];
const DECO_ALTS: &[(Deco, &str)] = &[
(Deco::None, "plain"),
(Deco::Brace, "brace"),
(Deco::Arrow, "arrow"),
(Deco::Line, "line"),
];
const STYLED_ALTS: &[(Variant, &str)] = &[
(Variant::Normal, "normal"),
(Variant::Bold, "bold"),
(Variant::Blackboard, "blackboard"),
(Variant::Calligraphic, "calligraphic"),
(Variant::Fraktur, "fraktur"),
(Variant::Roman, "roman"),
(Variant::SansSerif, "sans serif"),
(Variant::Typewriter, "typewriter"),
(Variant::OperatorName, "operator name"),
];
const MATRIX_ENV_ALTS: &[(MatrixEnv, &str)] = &[
(MatrixEnv::Matrix, "matrix"),
(MatrixEnv::Pmatrix, "( matrix )"),
(MatrixEnv::Bmatrix, "[ matrix ]"),
(MatrixEnv::Vmatrix, "| matrix |"),
(MatrixEnv::Cases, "cases"),
(MatrixEnv::Aligned, "aligned"),
(MatrixEnv::Array, "array"),
];
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{Deco, MathClass};
fn atom(c: &str) -> Symbol {
Symbol {
latex: c.into(),
class: MathClass::Ord,
}
}
fn fill(t: &mut Tree, seq: SeqId, s: &str) {
let mut c = Cursor { seq, index: t.len(seq) };
for ch in s.chars() {
c = t.insert_atom(c, atom(&ch.to_string()));
}
}
#[test]
fn insert_wires_parents_and_splices() {
let mut t = Tree::new();
let root = t.root();
t.insert_new(root, 0, NewNode::Atom(atom("a")));
let frac = t.insert_new(root, 1, NewNode::Frac(FracStyle::Bar));
assert_eq!(t.child_seqs(frac).len(), 2);
assert_eq!(t.seq_parent(t.child_seqs(frac)[0]), Some(frac));
assert_eq!(t.index_in_parent(frac), Some((root, 1)));
}
#[test]
fn move_right_walks_source_order_through_a_fraction() {
let mut t = Tree::new();
let root = t.root();
let frac = t.insert_new(root, 0, NewNode::Frac(FracStyle::Bar));
let slots = t.child_seqs(frac);
let (num, den) = (slots[0], slots[1]);
let c = nav::move_right(&t, Cursor { seq: root, index: 0 }).unwrap();
assert_eq!((c.seq, c.index), (num, 0));
let c = nav::move_right(&t, c).unwrap();
assert_eq!((c.seq, c.index), (den, 0));
let c = nav::move_right(&t, c).unwrap();
assert_eq!((c.seq, c.index), (root, 1));
assert_eq!(nav::move_right(&t, c), None);
}
#[test]
fn drop_node_frees_the_whole_subtree() {
let mut t = Tree::new();
let root = t.root();
let frac = t.insert_new(root, 0, NewNode::Frac(FracStyle::Bar));
assert!(t.seqs.len() >= 3);
t.drop_node(frac);
assert_eq!(t.len(root), 0);
assert_eq!(t.seqs.len(), 1);
}
#[test]
fn backspace_deletes_an_atom() {
let mut t = Tree::new();
let root = t.root();
fill(&mut t, root, "ab");
let c = t.delete_backward(Cursor { seq: root, index: 2 });
assert_eq!((c.seq, c.index), (root, 1));
assert_eq!(t.len(root), 1);
}
#[test]
fn backspace_in_empty_fraction_removes_it() {
let mut t = Tree::new();
let root = t.root();
let c = t.insert_fraction(Cursor { seq: root, index: 0 }, FracStyle::Bar, None);
let c = t.delete_backward(c);
assert_eq!((c.seq, c.index), (root, 0));
assert_eq!(t.len(root), 0);
assert_eq!(t.seqs.len(), 1);
}
#[test]
fn backspace_in_partial_fraction_unwraps_and_promotes() {
let mut t = Tree::new();
let root = t.root();
let c = t.insert_fraction(Cursor { seq: root, index: 0 }, FracStyle::Bar, None);
let num = c.seq;
fill(&mut t, num, "xy");
let frac = t.items(root)[0];
let den = t.child_seqs(frac)[1];
let c = t.delete_backward(Cursor { seq: den, index: 0 });
assert_eq!(t.len(root), 2);
assert_eq!(c.seq, root);
}
#[test]
fn backspace_at_nonempty_slot_start_exits_without_deleting() {
let mut t = Tree::new();
let root = t.root();
let c = t.insert_fraction(Cursor { seq: root, index: 0 }, FracStyle::Bar, None);
let num = c.seq;
fill(&mut t, num, "x");
let before = t.len(num);
let c = t.delete_backward(Cursor { seq: num, index: 0 });
assert_eq!(t.len(num), before);
assert_eq!(c.seq, root);
}
#[test]
fn fraction_wraps_a_selection() {
let mut t = Tree::new();
let root = t.root();
fill(&mut t, root, "ab");
let sel = Selection {
seq: root,
anchor: 0,
focus: 2,
};
let c = t.insert_fraction(Cursor { seq: root, index: 0 }, FracStyle::Bar, Some(sel));
let frac = t.items(root)[0];
let num = t.child_seqs(frac)[0];
assert_eq!(t.len(num), 2);
assert_eq!(t.len(root), 1);
assert_eq!(c.seq, t.child_seqs(frac)[1]);
}
#[test]
fn script_attaches_to_preceding_atom() {
let mut t = Tree::new();
let root = t.root();
fill(&mut t, root, "x");
let c = t.attach_script(Cursor { seq: root, index: 1 }, ScriptSlot::Sup);
let script = t.items(root)[0];
assert!(matches!(t.kind(script), Some(Kind::Script { .. })));
if let Some(Kind::Script { base, sup, .. }) = t.kind(script) {
assert_eq!(t.len(*base), 1);
assert_eq!(Some(c.seq), *sup);
} else {
panic!();
}
}
#[test]
fn deleting_empty_script_unwraps_with_cursor_after_base() {
let mut t = Tree::new();
let root = t.root();
t.insert_atom(Cursor { seq: root, index: 0 }, atom("x"));
let c = t.attach_script(Cursor { seq: root, index: 1 }, ScriptSlot::Sup);
let c = t.insert_atom(c, atom("2"));
let c = t.delete_backward(c);
let c = t.delete_backward(c);
assert_eq!(t.len(root), 1);
assert!(matches!(t.kind(t.items(root)[0]), Some(Kind::Atom(_))));
assert_eq!((c.seq, c.index), (root, 1));
}
#[test]
fn typing_into_script_base_evicts_old_base_left() {
let mut t = Tree::new();
let root = t.root();
t.insert_atom(Cursor { seq: root, index: 0 }, atom("x"));
let c = t.attach_script(Cursor { seq: root, index: 1 }, ScriptSlot::Sup);
t.insert_atom(c, atom("2"));
let script = t.items(root)[0];
let base = match t.kind(script) {
Some(Kind::Script { base, .. }) => *base,
_ => panic!(),
};
let c = t.insert_atom(Cursor { seq: base, index: 1 }, atom("y"));
assert_eq!(t.len(base), 1);
assert_eq!((c.seq, c.index), (base, 1));
assert_eq!(t.len(root), 2);
assert!(matches!(t.kind(t.items(root)[0]), Some(Kind::Atom(_))));
assert_eq!(t.items(root)[1], script);
}
#[test]
fn deleting_lone_script_base_dissolves_into_sequence() {
let mut t = Tree::new();
let root = t.root();
t.insert_atom(Cursor { seq: root, index: 0 }, atom("x"));
let c = t.attach_script(Cursor { seq: root, index: 1 }, ScriptSlot::Sup);
t.insert_atom(c, atom("2"));
let script = t.items(root)[0];
let base = match t.kind(script) {
Some(Kind::Script { base, .. }) => *base,
_ => panic!(),
};
let c = t.delete_backward(Cursor { seq: base, index: 1 });
assert_eq!((c.seq, c.index), (base, 0));
assert!(t.is_empty(base));
let c = t.delete_backward(c);
assert_eq!(t.len(root), 1);
assert!(matches!(t.kind(t.items(root)[0]), Some(Kind::Atom(_))));
assert_eq!((c.seq, c.index), (root, 0));
}
#[test]
fn deleting_empty_base_pulls_left_sibling_in() {
let mut t = Tree::new();
let root = t.root();
t.insert_atom(Cursor { seq: root, index: 0 }, atom("a"));
let c = t.attach_script(Cursor { seq: root, index: 1 }, ScriptSlot::Sup);
let script = t.items(root)[0];
let base = match t.kind(script) {
Some(Kind::Script { base, .. }) => *base,
_ => panic!(),
};
t.insert_atom(c, atom("2"));
t.move_range(base, 0..1, root, 0);
let c = t.delete_backward(Cursor { seq: base, index: 0 });
assert_eq!(t.len(root), 1);
assert_eq!(t.len(base), 1);
assert_eq!((c.seq, c.index), (base, 1));
}
#[test]
fn backspace_in_empty_sqrt_radicand_drops_radical() {
let mut t = Tree::new();
let root = t.root();
let c = t.insert_sqrt(Cursor { seq: root, index: 0 }, None);
let c = t.delete_backward(c);
assert_eq!(t.len(root), 0);
assert_eq!((c.seq, c.index), (root, 0));
}
#[test]
fn backspace_in_empty_delim_drops_it() {
let mut t = Tree::new();
let root = t.root();
let c = t.insert_delimiters(Cursor { seq: root, index: 0 }, '(', ')', None);
let c = t.delete_backward(c);
assert_eq!(t.len(root), 0);
assert_eq!((c.seq, c.index), (root, 0));
}
#[test]
fn backspace_in_empty_bigop_limit_steps_into_nonempty_other() {
let mut t = Tree::new();
let root = t.root();
let op = Symbol { latex: "\\sum".into(), class: MathClass::Op };
t.insert_big_op(Cursor { seq: root, index: 0 }, op);
let bigop = t.items(root)[0];
let cs = t.child_seqs(bigop);
let (upper, lower) = (cs[0], cs[1]);
fill(&mut t, lower, "2");
let c = t.delete_backward(Cursor { seq: upper, index: 0 });
assert_eq!(t.len(root), 1);
assert_eq!((c.seq, c.index), (lower, 1));
}
#[test]
fn backspace_in_empty_bigop_both_limits_empty_drops_op() {
let mut t = Tree::new();
let root = t.root();
let op = Symbol { latex: "\\sum".into(), class: MathClass::Op };
t.insert_big_op(Cursor { seq: root, index: 0 }, op);
let bigop = t.items(root)[0];
let lower = t.child_seqs(bigop)[1];
let c = t.delete_backward(Cursor { seq: lower, index: 0 });
assert_eq!(t.len(root), 0);
assert_eq!((c.seq, c.index), (root, 0));
}
#[test]
fn backspace_in_underover_over_collapses_to_base() {
let mut t = Tree::new();
let root = t.root();
let spec = UnderOverSpec { over: true, under: false, over_deco: Deco::None, under_deco: Deco::None };
let c = t.insert_under_over(Cursor { seq: root, index: 0 }, spec, None);
let node = t.items(root)[0];
let over = match t.kind(node) {
Some(Kind::UnderOver { over, .. }) => over.unwrap(),
_ => panic!(),
};
fill(&mut t, c.seq, "x");
let c = t.delete_backward(Cursor { seq: over, index: 0 });
assert_eq!(t.len(root), 1);
assert!(matches!(t.kind(t.items(root)[0]), Some(Kind::Atom(_))));
assert_eq!((c.seq, c.index), (root, 1));
}
#[test]
fn backspace_in_all_empty_matrix_drops_it() {
let mut t = Tree::new();
let root = t.root();
let c = t.insert_matrix(Cursor { seq: root, index: 0 }, MatrixEnv::Pmatrix, 2, 2);
let c = t.delete_backward(c);
assert_eq!(t.len(root), 0);
assert_eq!((c.seq, c.index), (root, 0));
}
#[test]
fn vertical_nav_switches_fraction_slots() {
let mut t = Tree::new();
let root = t.root();
let c = t.insert_fraction(Cursor { seq: root, index: 0 }, FracStyle::Bar, None);
let num = c.seq;
let frac = t.items(root)[0];
let den = t.child_seqs(frac)[1];
let down = nav::vertical(&t, Cursor { seq: num, index: 0 }, false).unwrap();
assert_eq!(down.seq, den);
let up = nav::vertical(&t, Cursor { seq: den, index: 0 }, true).unwrap();
assert_eq!(up.seq, num);
}
#[test]
fn matrix_row_col_ops() {
let mut t = Tree::new();
let root = t.root();
let c = t.insert_matrix(Cursor { seq: root, index: 0 }, MatrixEnv::Pmatrix, 2, 2);
let m = t.items(root)[0];
let count = |t: &Tree| match t.kind(m) {
Some(Kind::Matrix { rows, .. }) => (rows.len(), rows[0].len()),
_ => (0, 0),
};
assert_eq!(count(&t), (2, 2));
let c = t.matrix_insert_row(c, Side::After);
assert_eq!(count(&t), (3, 2));
let c = t.matrix_insert_col(c, Side::Before);
assert_eq!(count(&t), (3, 3));
let c = t.matrix_delete_row(c);
assert_eq!(count(&t), (2, 3));
let _c = t.matrix_delete_col(c);
assert_eq!(count(&t), (2, 2));
}
#[test]
fn matrix_shape_at_reports_current_grid_size_and_none_outside_a_matrix() {
let mut t = Tree::new();
let root = t.root();
let c = t.insert_matrix(Cursor { seq: root, index: 0 }, MatrixEnv::Pmatrix, 2, 2);
assert_eq!(t.matrix_shape_at(c), Some((2, 2)));
let c = t.matrix_insert_row(c, Side::After);
assert_eq!(t.matrix_shape_at(c), Some((3, 2)));
assert_eq!(t.matrix_shape_at(Cursor { seq: root, index: 0 }), None);
}
#[test]
fn swap_variants_for_delim_excludes_the_current_pair() {
let mut t = Tree::new();
let root = t.root();
t.insert_delimiters(Cursor { seq: root, index: 0 }, '(', ')', None);
let delim = t.items(root)[0];
let variants = t.swap_variants(delim).expect("Delim should be swappable");
assert!(variants.iter().all(|v| !matches!(
v,
SwapVariant::Delim { open: '(', close: ')', .. }
)));
assert!(variants
.iter()
.any(|v| matches!(v, SwapVariant::Delim { open: '[', close: ']', .. })));
}
#[test]
fn apply_swap_changes_only_the_tag_field() {
let mut t = Tree::new();
let root = t.root();
let c = t.insert_delimiters(Cursor { seq: root, index: 0 }, '(', ')', None);
fill(&mut t, c.seq, "x");
let delim = t.items(root)[0];
let body_before = t.child_seqs(delim)[0];
t.apply_swap(delim, &SwapVariant::Delim { open: '[', close: ']', label: "[ ]" });
match t.kind(delim) {
Some(Kind::Delim { open, close, body }) => {
assert_eq!((*open, *close), ('[', ']'));
assert_eq!(*body, body_before);
assert_eq!(t.len(*body), 1);
}
_ => panic!("expected Delim"),
}
}
#[test]
fn swap_variants_none_for_non_swappable_kinds() {
let mut t = Tree::new();
let root = t.root();
t.insert_atom(Cursor { seq: root, index: 0 }, atom("x"));
let atom_node = t.items(root)[0];
assert!(t.swap_variants(atom_node).is_none());
let c = t.insert_fraction(Cursor { seq: root, index: 1 }, FracStyle::Bar, None);
let _ = c;
let frac = t.items(root)[1];
assert!(t.swap_variants(frac).is_none());
let c = t.insert_sqrt(Cursor { seq: root, index: 2 }, None);
let _ = c;
let sqrt = t.items(root)[2];
assert!(t.swap_variants(sqrt).is_none());
}
#[test]
fn swap_variants_none_for_text_carve_out() {
let mut t = Tree::new();
let root = t.root();
t.insert_styled(Cursor { seq: root, index: 0 }, Variant::Text, None);
let text = t.items(root)[0];
assert!(t.swap_variants(text).is_none());
}
#[test]
fn swap_variants_for_styled_font_excludes_current_and_text() {
let mut t = Tree::new();
let root = t.root();
t.insert_styled(Cursor { seq: root, index: 0 }, Variant::Bold, None);
let styled = t.items(root)[0];
let variants = t.swap_variants(styled).expect("Styled(Bold) should be swappable");
assert!(variants
.iter()
.all(|v| !matches!(v, SwapVariant::Styled { variant: Variant::Bold, .. })));
assert!(variants
.iter()
.all(|v| !matches!(v, SwapVariant::Styled { variant: Variant::Text, .. })));
}
#[test]
fn swap_variants_for_underover_only_varies_present_slots() {
let mut t = Tree::new();
let root = t.root();
let spec = UnderOverSpec {
over: true,
under: false,
over_deco: Deco::Brace,
under_deco: Deco::None,
};
t.insert_under_over(Cursor { seq: root, index: 0 }, spec, None);
let node = t.items(root)[0];
let variants = t.swap_variants(node).expect("UnderOver should be swappable");
assert!(!variants.is_empty());
for v in &variants {
match v {
SwapVariant::UnderOverDeco { over, under, .. } => {
assert_ne!(*over, Deco::Brace);
assert_eq!(*under, Deco::None);
}
_ => panic!("expected UnderOverDeco"),
}
}
}
}