use crate::ast::{Field, Node, Row, row_at, row_at_mut};
use crate::glyphs::Mark;
pub use crate::symbols::{GRID_ENVS, GridWrap};
pub type CursorPos = (Vec<(usize, Field)>, usize);
pub type BlockRef = (Vec<(usize, Field)>, std::ops::Range<usize>);
use crate::symbols::{Accent, ColDelim, Delim, is_bigop, is_func_name, symbol_by_name};
mod command;
mod modes;
use modes::gap_shift_cell as modes_gap_shift;
pub use command::{Edit, MODE_COMMANDS, ModeCmd, mode_command, preview_row, resolve};
type Snapshot = (Row, Vec<(usize, Field)>, usize);
#[derive(Clone)]
pub struct Editor {
pub root: Row,
pub path: Vec<(usize, Field)>,
pub col: usize,
pub minibuffer: Option<String>,
pub op_entry: Option<(BoxKind, String)>,
pub(crate) executing: Option<String>,
pub(crate) op_cmd: Option<String>,
pub copy_flash: bool,
pub(crate) mid_armed: Option<(Vec<(usize, Field)>, usize)>,
pub op_cursor: usize,
pub(crate) op_escape: bool,
pub grid: Option<GridSel>,
undo: Vec<Snapshot>,
redo: Vec<Snapshot>,
pub message: String,
pub message_error: bool,
pub free: Option<FreeCursor>,
pub block: Option<Vec<BlockRef>>,
pub block_sel: usize,
pub ghost: Vec<Vec<(usize, Field)>>,
pub(crate) select_anchor: Option<usize>,
select_path: Vec<(usize, Field)>,
select_whole: bool,
clip: Clip,
pub completion: Option<crate::complete::Completion>,
pub(crate) unwrap_armed: Option<Vec<(usize, Field)>>,
pub ask: Option<Ask>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Ask {
Path(String),
SaveFirst,
}
const JUMP_W_Y: usize = 3;
const FREE_EXPAND_IN: usize = 3;
const FREE_EXPAND_OUT: usize = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BoxKind {
Op,
OpStar,
Rm,
Text,
Tex,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GridSel {
Cells { anchor: Option<usize> },
Lanes {
cols: bool,
pos: usize,
ext: Option<usize>,
},
}
#[derive(Debug, Clone, PartialEq)]
pub enum Clip {
Nodes(Row),
Cells {
rows: usize,
cols: usize,
cells: Vec<Row>,
},
}
impl Clip {
fn is_empty(&self) -> bool {
match self {
Clip::Nodes(r) => r.is_empty(),
Clip::Cells { cells, .. } => cells.is_empty(),
}
}
}
#[derive(Clone, Debug)]
pub struct FreeCursor {
pub at: (usize, usize),
pub snap: CursorPos,
pub snap_at: (usize, usize),
}
pub(crate) struct GapSplice {
pub k: usize,
pub i: usize,
pub cmode: bool,
pub g: usize,
pub rows: usize,
pub cols: usize,
pub parent: Vec<(usize, Field)>,
}
struct Frame {
gap: Option<GapSplice>,
ghosts: Vec<Vec<(usize, Field)>>,
}
impl Frame {
fn map(&self, p: &[(usize, Field)], c: usize) -> (Vec<(usize, Field)>, usize) {
let (mut p2, c2) = ghost_adjust(&self.ghosts, p, c);
self.gap_fix(p, &mut p2);
(p2, c2)
}
fn gap_fix(&self, raw: &[(usize, Field)], p: &mut [(usize, Field)]) {
let Some(gs) = &self.gap else { return };
if raw.len() > gs.k
&& raw[..gs.k] == gs.parent[..]
&& raw[gs.k].0 == gs.i
&& let Field::Cell(cell) = raw[gs.k].1
{
p[gs.k].1 = Field::Cell(modes_gap_shift(cell, gs.cmode, gs.g, gs.rows, gs.cols));
}
}
}
fn ghost_adjust(
ghost: &[Vec<(usize, Field)>],
p: &[(usize, Field)],
c: usize,
) -> (Vec<(usize, Field)>, usize) {
let mut p2 = p.to_vec();
let mut c2 = c;
for g in ghost {
if p.len() > g.len() && p[..g.len()] == g[..] {
p2[g.len()].0 += 1;
} else if p == &g[..] {
c2 += 1;
}
}
(p2, c2)
}
#[derive(Clone, Debug)]
pub struct JumpCand {
pub pos: CursorPos,
pub empty: bool,
pub cell_end: bool,
pub interior: bool,
pub bound: bool,
pub is_cursor: bool,
}
impl Default for Editor {
fn default() -> Self {
Self::new()
}
}
fn script_cmd(cmd: &str) -> Option<(bool, crate::ast::Row)> {
let lead = cmd.chars().next().filter(|c| matches!(c, '^' | '_'));
let trail = if cmd.chars().count() > 1 {
cmd.chars().last().filter(|c| matches!(c, '^' | '_'))
} else {
None
};
let marker = match (lead, trail) {
(Some(a), Some(b)) if a == b => a,
(Some(a), None) => a,
(None, Some(b)) => b,
_ => return None,
};
let mut rest = cmd;
if lead.is_some() {
rest = &rest[1..];
}
if trail.is_some() {
rest = &rest[..rest.len() - 1];
}
if rest.is_empty() {
return None;
}
let arg: crate::ast::Row = if let Some(c) = symbol_by_name(rest) {
vec![Node::Sym(c)]
} else if rest.chars().all(|c| c.is_ascii_alphanumeric()) {
rest.chars().map(Node::Sym).collect()
} else {
return None;
};
Some((marker == '^', arg))
}
fn lr_tokens(spec: &str) -> Option<Vec<char>> {
let mut tokens = Vec::new();
let mut it = spec.chars().peekable();
while let Some(c) = it.next() {
let tok = match c {
'\\' => {
let mut name = String::new();
while it.peek().is_some_and(|c| c.is_ascii_alphabetic()) {
name.push(it.next().unwrap());
}
*crate::symbols::DELIM_NAMES.get(name.as_str())?
}
'<' => '⟨',
'>' => '⟩',
c => c,
};
tokens.push(tok);
}
Some(tokens)
}
pub(crate) fn lr_split(spec: &str) -> Option<(&str, &str)> {
let mut it = spec.char_indices().peekable();
while let Some((at, c)) = it.next() {
if c != '\\' {
continue;
}
let mut name = String::new();
while let Some(&(_, ch)) = it.peek() {
if !ch.is_ascii_alphabetic() {
break;
}
name.push(ch);
it.next();
}
if crate::symbols::DELIM_NAMES.contains_key(name.as_str()) {
continue;
}
return it.peek().is_none().then(|| (&spec[..at], &spec[at + 1..]));
}
Some((spec, ""))
}
pub(crate) fn lr_spec_more(spec: &str) -> Option<bool> {
let tokens = lr_tokens(spec)?;
let Some((first, rest)) = tokens.split_first() else {
return Some(true);
};
(Delim::of_spec_side(*first, true).is_some() && rest.iter().all(|&c| c == '|')).then_some(false)
}
fn lr_spec(cmd: &str) -> Option<(Delim, Delim, usize)> {
let spec = cmd
.strip_prefix("delim")
.or_else(|| cmd.strip_prefix("lr"))?;
let tokens = lr_tokens(spec)?;
if tokens.len() < 2 {
return None;
}
let left = Delim::of_spec_side(tokens[0], true)?;
let right = Delim::of_spec_side(*tokens.last().unwrap(), false)?;
let mids = &tokens[1..tokens.len() - 1];
if !mids.iter().all(|&c| c == '|') {
return None;
}
Some((left, right, mids.len()))
}
pub fn grid_env_name(left: Delim, right: Delim, cols: usize) -> Option<&'static str> {
GRID_ENVS
.entries()
.find(|(name, w)| {
**w == GridWrap::Pair(left, right)
&& (cols <= 2 || !matches!(**name, "cases" | "rcases"))
})
.map(|(&name, _)| name)
}
fn grid_command(cmd: &str) -> Option<(GridWrap, usize, usize)> {
for (name, &delims) in GRID_ENVS.entries() {
if *name == "smallmatrix" {
continue;
}
let Some(rest) = cmd.strip_prefix(name) else {
continue;
};
match rest.as_bytes() {
[r, c] if r.is_ascii_digit() && c.is_ascii_digit() => {
let (rows, cols) = ((r - b'0') as usize, (c - b'0') as usize);
if rows >= 1 && cols >= 1 {
return Some((delims, rows, cols));
}
}
_ => {}
}
}
None
}
fn unwrap_contents(node: &Node) -> Option<(Field, &Row)> {
match node {
Node::Delim { mids: 0, segs, .. }
if segs.len() == 1 && !matches!(segs[0][..], [Node::Array { .. }]) =>
{
Some((Field::Seg(0), &segs[0]))
}
Node::Norm { arg } => Some((Field::Seg(0), arg)),
Node::Sqrt { arg, .. } => Some((Field::SqrtArg, arg)),
Node::WideAccent { base, .. } => Some((Field::WideBase, base)),
_ => None,
}
}
impl Editor {
pub fn new() -> Self {
Editor {
root: Vec::new(),
path: Vec::new(),
col: 0,
minibuffer: None,
op_entry: None,
executing: None,
op_cmd: None,
copy_flash: false,
mid_armed: None,
op_cursor: 0,
op_escape: false,
grid: None,
undo: Vec::new(),
redo: Vec::new(),
message: String::new(),
message_error: false,
free: None,
block: None,
block_sel: 0,
ghost: Vec::new(),
select_anchor: None,
select_path: Vec::new(),
select_whole: false,
clip: Clip::Nodes(Vec::new()),
completion: None,
unwrap_armed: None,
ask: None,
}
}
pub fn ask_path(&mut self, at: &str) {
self.clear_message();
self.ask = Some(Ask::Path(at.to_string()));
}
pub fn ask_save_first(&mut self) {
self.clear_message();
self.ask = Some(Ask::SaveFirst);
}
pub fn info(&mut self, m: impl Into<String>) {
self.message = m.into();
self.message_error = false;
}
pub fn error(&mut self, m: impl Into<String>) {
self.message = m.into();
self.message_error = true;
}
pub fn clear_message(&mut self) {
self.message.clear();
self.message_error = false;
}
pub fn cur_row(&self) -> &Row {
row_at(&self.root, &self.path)
}
fn cur_row_mut(&mut self) -> &mut Row {
row_at_mut(&mut self.root, &self.path)
}
pub fn insert_sym(&mut self, c: char) {
if self.selection().is_some() {
self.take_selection();
}
let col = self.col;
self.cur_row_mut().insert(col, Node::Sym(c));
self.col += 1;
}
pub fn slash(&mut self) {
if self.col > 0 && matches!(self.cur_row()[self.col - 1], Node::Sym('/')) {
self.col -= 1;
let col = self.col;
self.cur_row_mut().remove(col);
self.insert_and_enter(Node::Frac {
num: vec![],
den: vec![],
});
} else {
self.insert_sym('/');
}
}
pub fn click(&mut self, x: usize, y: usize) {
self.free = None;
self.block = None;
self.select_anchor = None;
self.completion = None;
self.unwrap_armed = None;
self.mid_armed = None;
if self.op_entry.is_some() {
let before = (self.root.clone(), self.path.clone(), self.col);
self.op_commit();
if self.root != before.0 {
self.push_undo(before);
}
}
if self.minibuffer.is_some() {
self.minibuffer = None;
self.clear_message();
}
if let Some((pos, _)) = self.nearest_position(x, y) {
self.path = pos.0;
self.col = pos.1;
}
if let Some(GridSel::Cells { .. }) = self.grid {
self.grid = Some(GridSel::Cells { anchor: None });
}
self.reclamp_grid();
}
fn nearest_position(&self, x: usize, y: usize) -> Option<(CursorPos, (usize, usize))> {
let cands = self.jump_candidates();
let coords = self.coords_displayed(&cands);
(0..cands.len())
.filter_map(|i| coords[i].map(|xy| (i, xy)))
.min_by_key(|&(_, (cy, cx))| cy.abs_diff(y) * 1000 + cx.abs_diff(x))
.map(|(i, xy)| (cands[i].pos.clone(), xy))
}
fn display_visible(&self, pos: &CursorPos) -> bool {
use crate::render::is_inline_script_row;
let mut row: &Row = &self.root;
let mut prefix: Vec<(usize, Field)> = Vec::new();
for &(i, f) in &pos.0 {
prefix.push((i, f));
let child: &Row = row[i].field(f);
let ghosted = self.ghost.iter().any(|g| g == &prefix);
let d = prefix.len() - 1;
let editing = self.path.len() > d
&& self.path[..d] == prefix[..d]
&& self.path[d].0 == prefix[d].0;
let visible = match f {
Field::SupArg => ghosted || editing || !is_inline_script_row(child, true),
Field::SubArg => ghosted || editing || !is_inline_script_row(child, false),
Field::OpLower
| Field::OpUpper
| Field::ArrowOver
| Field::ArrowUnder
| Field::BraceLabel => ghosted || editing || !child.is_empty(),
_ => true,
};
if !visible {
return false;
}
row = child;
}
true
}
pub(crate) fn gap_splice(&self) -> Option<GapSplice> {
match self.grid {
Some(GridSel::Lanes {
cols: cmode, pos, ..
}) if pos.is_multiple_of(2) => {
self.grid_info().map(|(k, i, rows, cols, _)| GapSplice {
k,
i,
cmode,
g: pos / 2,
rows,
cols,
parent: self.path[..k].to_vec(),
})
}
_ => None,
}
}
fn displayed_frame(&self, root: &mut Row) -> Frame {
let gap = self.gap_splice();
if let Some(gs) = &gap {
let Node::Array {
rows: nr,
cols: nc,
cells,
} = &mut row_at_mut(root, &gs.parent)[gs.i]
else {
unreachable!()
};
(*nr, *nc) = modes::splice_lane(cells, gs.rows, gs.cols, gs.cmode, gs.g, || {
vec![Node::Spacer]
});
}
let frame = Frame {
gap,
ghosts: self.ghost.clone(),
};
let mut ghosts: Vec<&Vec<(usize, Field)>> = self.ghost.iter().collect();
ghosts.sort_by_key(|p| std::cmp::Reverse(p.len()));
for p in ghosts {
let raw = p.clone();
let mut p = p.clone();
frame.gap_fix(&raw, &mut p);
row_at_mut(root, &p).insert(0, Node::Sym(Mark::SlotGhost.ch()));
}
frame
}
fn coords_displayed(&self, cands: &[JumpCand]) -> Vec<Option<(usize, usize)>> {
let mut root = self.root.clone();
let frame = self.displayed_frame(&mut root);
self.probe(root, cands, Some(&frame))
}
fn probe(
&self,
mut root: Row,
cands: &[JumpCand],
frame: Option<&Frame>,
) -> Vec<Option<(usize, usize)>> {
use crate::render::{RenderCtx, render_root};
let n = cands.len().min(crate::glyphs::PROBE_MAX);
for (idx, cand) in cands.iter().take(n).enumerate().rev() {
if frame.is_some() && !self.display_visible(&cand.pos) {
continue;
}
let (p, c) = &cand.pos;
let (p2, c2) = match frame {
Some(f) => f.map(p, *c),
None => (p.clone(), *c),
};
let mark = Mark::Probe { index: idx }.ch();
row_at_mut(&mut root, &p2).insert(c2, Node::Sym(mark));
}
let b = render_root(&root, None, &RenderCtx::canonical());
let mut out = vec![None; cands.len()];
for &(y, x, ch) in &b.marks {
if let Some(Mark::Probe { index: i }) = Mark::decode(ch)
&& i < out.len()
{
out[i] = Some((y, x));
}
}
out
}
pub fn right(&mut self) {
let row = self.cur_row();
if self.col < row.len() {
if let Some(&f) = row[self.col].fields().first() {
self.path.push((self.col, f));
self.col = 0;
} else {
self.col += 1;
}
} else if let Some((i, f)) = self.path.pop() {
let parent = self.cur_row();
let fields = parent[i].fields();
let k = fields.iter().position(|&x| x == f).unwrap();
if k + 1 < fields.len() {
self.path.push((i, fields[k + 1]));
self.col = 0;
} else {
self.col = i + 1;
}
}
}
pub fn left(&mut self) {
let row = self.cur_row();
if self.col > 0 {
if let Some(&f) = row[self.col - 1].fields().last() {
let end = row[self.col - 1].field(f).len();
self.path.push((self.col - 1, f));
self.col = end;
} else {
self.col -= 1;
}
} else if let Some((i, f)) = self.path.pop() {
let parent = self.cur_row();
let fields = parent[i].fields();
let k = fields.iter().position(|&x| x == f).unwrap();
if k > 0 {
let prev = fields[k - 1];
let end = parent[i].field(prev).len();
self.path.push((i, prev));
self.col = end;
} else {
self.col = i;
}
}
}
pub fn vertical(&mut self, up: bool) {
if let Some(&(i, f)) = self.path.last() {
let parent_path = &self.path[..self.path.len() - 1];
let node = &row_at(&self.root, parent_path)[i];
let target = match (f, up) {
(Field::FracNum, false) => Some(Field::FracDen),
(Field::FracDen, true) => Some(Field::FracNum),
(Field::OpLower, true) => Some(Field::OpUpper),
(Field::OpUpper, false) => Some(Field::OpLower),
(Field::ArrowUnder, true) => Some(Field::ArrowOver),
(Field::ArrowOver, false) => Some(Field::ArrowUnder),
(Field::BraceArg, dir) => match node {
Node::Brace { over, .. } if *over == dir => Some(Field::BraceLabel),
_ => None,
},
(Field::BraceLabel, dir) => match node {
Node::Brace { over, .. } if *over != dir => Some(Field::BraceArg),
_ => None,
},
(Field::Cell(c), up) => match node {
Node::Array { cols, cells, .. } => {
if up && c >= *cols {
Some(Field::Cell(c - cols))
} else if !up && c + cols < cells.len() {
Some(Field::Cell(c + cols))
} else {
None
}
}
_ => None,
},
_ => None,
};
if let Some(t) = target {
self.path.pop();
let len = {
let parent = self.cur_row();
parent[i].field(t).len()
};
self.path.push((i, t));
self.col = self.col.min(len);
return;
}
}
if let Some(&(i, Field::Cell(c))) = self.path.last() {
let parent_path = &self.path[..self.path.len() - 1];
if let Node::Array { cols, cells, .. } = &row_at(&self.root, parent_path)[i] {
let at_edge = if up {
c < *cols
} else {
c + cols >= cells.len()
};
if at_edge {
self.path.pop();
let mut idx = i;
if let Some(&(d, Field::Seg(_))) = self.path.last() {
self.path.pop();
idx = d;
}
self.col = if up { idx } else { idx + 1 };
return;
}
}
}
if self.col > 0 {
let col = self.col - 1;
let promotable = match &self.cur_row()[col] {
Node::Sym(c) => is_bigop(*c),
Node::Func(name) => crate::symbols::func_takes_limits(name),
_ => false,
};
if promotable {
let row = self.cur_row_mut();
row[col] = match &row[col] {
Node::Sym(c) => Node::BigOpSym {
op: *c,
lower: vec![],
upper: vec![],
},
Node::Func(name) => Node::BigOp {
name: name.clone(),
lower: vec![],
upper: vec![],
},
_ => unreachable!(),
};
let f = if up { Field::OpUpper } else { Field::OpLower };
self.path.push((col, f));
self.col = 0;
return;
}
}
if self.root.iter().any(|n| matches!(n, Node::Break)) {
let seg_of = |steps: &[Node]| steps.iter().filter(|n| matches!(n, Node::Break)).count();
let top = self.path.first().map_or(self.col, |&(i, _)| i);
let cur_seg = seg_of(&self.root[..top]);
let max_seg = seg_of(&self.root);
let target = if up {
cur_seg.checked_sub(1)
} else {
(cur_seg < max_seg).then_some(cur_seg + 1)
};
let Some(target) = target else { return };
let cands = self.jump_candidates();
let coords = self.coords_displayed(&cands);
let Some(my) = cands
.iter()
.position(|c| c.is_cursor)
.and_then(|i| coords[i])
else {
return;
};
let seg_of_pos = |pos: &CursorPos| {
let top = pos.0.first().map_or(pos.1, |&(i, _)| i);
self.root[..top]
.iter()
.filter(|n| matches!(n, Node::Break))
.count()
};
let best = (0..cands.len())
.filter(|&i| !cands[i].is_cursor && seg_of_pos(&cands[i].pos) == target)
.filter_map(|i| coords[i].map(|xy| (i, xy)))
.min_by_key(|&(_, (y, x))| x.abs_diff(my.1) * 10 + y.abs_diff(my.0));
if let Some((i, _)) = best {
let (p, c) = cands[i].pos.clone();
self.path = p;
self.col = c;
}
}
}
pub fn exit_inset(&mut self) {
if let Some((i, _)) = self.path.pop() {
self.col = i + 1;
self.select_anchor = None;
}
}
pub fn home(&mut self) {
let row = self.cur_row();
self.col = row[..self.col]
.iter()
.rposition(|n| matches!(n, Node::Break))
.map_or(0, |i| i + 1);
self.select_anchor = None;
}
pub fn end(&mut self) {
let row = self.cur_row();
self.col = row[self.col..]
.iter()
.position(|n| matches!(n, Node::Break))
.map_or(row.len(), |i| self.col + i);
self.select_anchor = None;
}
pub fn negate_prev(&mut self) {
let Some(col) = self.col.checked_sub(1) else {
self.error("negation needs a symbol before the cursor");
return;
};
match self.cur_row()[col] {
Node::Sym(c) => {
match crate::symbols::negated(c).or_else(|| crate::symbols::unnegated(c)) {
Some(flipped) => self.cur_row_mut()[col] = Node::Sym(flipped),
None => self.error(format!("{} has no slashed negation", c)),
}
}
_ => self.error("negation needs a symbol before the cursor"),
}
}
pub fn backspace(&mut self) {
if self.col > 0 {
let col = self.col - 1;
if let Node::Accent {
overs,
unders,
base,
} = &mut self.cur_row_mut()[col]
{
if overs.pop().is_none() {
unders.pop();
}
if overs.is_empty() && unders.is_empty() {
let base = *base;
self.cur_row_mut()[col] = Node::Sym(base);
}
return;
}
let target = &self.cur_row()[self.col - 1];
if !target.fields().is_empty() && !target.is_empty_structure() {
self.select_anchor = Some(self.col - 1);
self.select_path = self.path.clone();
self.select_whole = true;
} else {
self.col -= 1;
let col = self.col;
self.cur_row_mut().remove(col);
}
} else if let Some(&(i, _)) = self.path.last() {
let parent_path = &self.path[..self.path.len() - 1];
let node = &row_at(&self.root, parent_path)[i];
if node.is_empty_structure() {
self.path.pop();
let col = i;
self.cur_row_mut().remove(col);
self.col = col;
} else {
self.left();
}
}
}
fn unwrappable(&self, at_start: bool) -> Option<usize> {
let stop = if at_start { 0 } else { self.cur_row().len() };
if self.col != stop {
return None;
}
let &(i, field) = self.path.last()?;
let parent = &self.path[..self.path.len() - 1];
let node = row_at(&self.root, parent).get(i)?;
let (slot, contents) = unwrap_contents(node)?;
(field == slot && !contents.is_empty() && (at_start || !matches!(node, Node::Sqrt { .. })))
.then_some(i)
}
fn mid_beside(&self, right: bool) -> Option<usize> {
let &(i, Field::Seg(k)) = self.path.last()? else {
return None;
};
let parent = &self.path[..self.path.len() - 1];
let Node::Delim { mids, .. } = row_at(&self.root, parent).get(i)? else {
return None;
};
if right {
(k < *mids).then_some(k)
} else {
k.checked_sub(1)
}
}
pub(crate) fn merge_mid(&mut self, m: usize) {
let Some(&(i, Field::Seg(_))) = self.path.last() else {
return;
};
self.path.pop();
let row = self.cur_row_mut();
let Some(Node::Delim { mids, segs, .. }) = row.get_mut(i) else {
return;
};
if m + 1 >= segs.len() {
return;
}
let tail = segs.remove(m + 1);
let junction = segs[m].len();
segs[m].extend(tail);
*mids -= 1;
self.path.push((i, Field::Seg(m)));
self.col = junction;
self.select_anchor = None;
}
fn unwrap_delim(&mut self, i: usize) {
self.path.pop();
self.unwrap_at(i, true);
}
fn unwrap_at(&mut self, i: usize, select: bool) {
let node = self.cur_row_mut().remove(i);
let content: Row = match node {
Node::Delim { segs, .. } => segs.into_iter().next().unwrap_or_default(),
Node::Norm { arg } | Node::Sqrt { arg, .. } => arg,
Node::WideAccent { base, .. } => base,
other => {
self.cur_row_mut().insert(i, other);
return;
}
};
let n = content.len();
let from_left = self.col <= i;
self.cur_row_mut().splice(i..i, content);
self.select_whole = false;
if select {
self.col = i + n;
self.select_anchor = (n > 0).then_some(i);
self.select_path = self.path.clone();
} else {
self.col = if from_left { i } else { i + n };
self.select_anchor = None;
}
}
pub fn delete_toward_delim(&mut self, at_start: bool, armed: bool) -> bool {
let Some(i) = self
.unwrappable(at_start)
.or_else(|| armed.then(|| self.unwrappable(!at_start)).flatten())
else {
return false;
};
if armed {
self.unwrap_delim(i);
} else {
self.unwrap_armed = Some(self.path.clone());
}
true
}
pub fn select_arm(&mut self, right: bool, armed: &Option<Vec<(usize, Field)>>) -> bool {
if self.selection().is_some() {
return false;
}
let edge = if right {
self.col == self.cur_row().len()
} else {
self.col == 0
};
if edge {
if let Some(m) = self.mid_beside(right) {
self.mid_armed = Some((self.path.clone(), m));
return true;
}
if self.unwrappable(!right).is_none() {
return false;
}
self.unwrap_armed = Some(self.path.clone());
return true;
}
let Some(crossed) = (if right {
Some(self.col)
} else {
self.col.checked_sub(1)
}) else {
return false;
};
let Some(node) = self.cur_row().get(crossed) else {
return false;
};
let Some((slot, contents)) = unwrap_contents(node) else {
return false;
};
if contents.is_empty() {
return false;
}
let mut p = self.path.clone();
p.push((crossed, slot));
if armed.as_deref() == Some(&p[..]) {
return false;
}
self.unwrap_armed = Some(p);
true
}
pub fn unwrap_armed_outside(&mut self, armed: &Option<Vec<(usize, Field)>>) -> bool {
let Some((&(i, _), parent)) = armed.as_deref().and_then(|p| p.split_last()) else {
return false;
};
if parent != &self.path[..] || (self.col != i && self.col != i + 1) {
return false;
}
if self.cur_row().get(i).and_then(unwrap_contents).is_none() {
return false;
}
self.unwrap_at(i, false);
true
}
pub fn delete_forward(&mut self, armed: bool) {
if !self.delete_selection() && !self.delete_toward_delim(false, armed) {
self.delete();
}
}
pub fn delete(&mut self) {
let row = self.cur_row();
if self.col < row.len() {
let target = &row[self.col];
if !target.fields().is_empty() && !target.is_empty_structure() {
self.select_anchor = Some(self.col + 1);
self.select_path = self.path.clone();
self.select_whole = true;
} else {
let col = self.col;
self.cur_row_mut().remove(col);
}
}
}
pub fn insert_delim(&mut self, left: Delim, right: Delim, mids: usize) {
let segs = vec![vec![]; mids + 1];
self.insert_and_enter(Node::Delim {
left,
right,
mids,
segs,
});
}
pub fn close_delim(&mut self, right: Delim) -> bool {
for k in (0..self.path.len()).rev() {
let (i, f) = self.path[k];
if !matches!(f, Field::Seg(_)) {
continue;
}
let node = &row_at(&self.root, &self.path[..k])[i];
if matches!(node, Node::Delim { right: r, .. } if *r == right) {
self.path.truncate(k);
self.col = i + 1;
self.select_anchor = None;
return true;
}
}
false
}
pub fn close_paren(&mut self) {
if !self.close_delim(Delim::Col(ColDelim::Paren)) {
self.info("not inside a ( ) pair (( inserts one)");
}
}
pub fn close_bracket(&mut self) {
if self.close_delim(Delim::Col(ColDelim::Bracket)) {
return;
}
if let Some((k, i, _)) = self.enclosing_array() {
self.path.truncate(k);
self.col = i + 1;
self.select_anchor = None;
} else {
self.info("not inside a [ ] pair ([ inserts one)");
}
}
pub fn close_brace(&mut self) {
if !self.close_delim(Delim::Col(ColDelim::Brace)) {
self.info("not inside a { } pair ({ inserts one)");
}
}
pub fn insert_grid(&mut self, left: Delim, right: Delim, rows: usize, cols: usize) {
let array = Node::Array {
rows,
cols,
cells: vec![vec![]; rows * cols],
};
let node = Node::Delim {
left,
right,
mids: 0,
segs: vec![vec![array]],
};
let col = self.col;
self.cur_row_mut().insert(col, node);
self.path.push((col, Field::Seg(0)));
self.path.push((0, Field::Cell(0)));
self.col = 0;
}
pub fn insert_norm_grid(&mut self, rows: usize, cols: usize) {
let array = Node::Array {
rows,
cols,
cells: vec![vec![]; rows * cols],
};
let node = Node::Norm { arg: vec![array] };
let col = self.col;
self.cur_row_mut().insert(col, node);
self.path.push((col, Field::Seg(0)));
self.path.push((0, Field::Cell(0)));
self.col = 0;
}
pub fn in_grid(&self) -> bool {
self.enclosing_array().is_some()
}
pub(crate) fn enclosing_array(&self) -> Option<(usize, usize, usize)> {
self.path
.iter()
.rposition(|&(_, f)| matches!(f, Field::Cell(_)))
.map(|k| {
let (i, Field::Cell(c)) = self.path[k] else {
unreachable!()
};
(k, i, c)
})
}
fn edit_array(
&mut self,
op: impl FnOnce(usize, usize, &mut Vec<Row>, usize) -> Option<(usize, usize, usize)>,
) {
let Some((k, i, c)) = self.enclosing_array() else {
self.info("not inside a grid");
return;
};
let parent_path = self.path[..k].to_vec();
let Node::Array { rows, cols, cells } = &mut row_at_mut(&mut self.root, &parent_path)[i]
else {
unreachable!()
};
let Some((nr, nc, ncell)) = op(*rows, *cols, cells, c) else {
self.error("cannot remove the last row/column");
return;
};
*rows = nr;
*cols = nc;
self.path.truncate(k);
self.path.push((i, Field::Cell(ncell)));
self.col = 0;
}
pub fn add_lane(&mut self, cols_mode: bool) {
let Some((_, _, _, cols, c)) = self.grid_info() else {
self.info("not inside a grid");
return;
};
let after = if cols_mode { c % cols } else { c / cols };
self.lane_insert(cols_mode, after + 1);
}
pub fn del_lane(&mut self, cols_mode: bool) {
let Some((_, _, _, cols, c)) = self.grid_info() else {
self.info("not inside a grid");
return;
};
let at = if cols_mode { c % cols } else { c / cols };
self.lane_delete(cols_mode, at, at);
}
pub fn lane_insert(&mut self, cols_mode: bool, g: usize) {
self.edit_array(|rows, cols, cells, c| {
let (nr, nc) = modes::splice_lane(cells, rows, cols, cols_mode, g, Vec::new);
let at = if cols_mode {
(c / cols) * nc + g.min(cols)
} else {
g.min(rows) * cols + c % cols
};
Some((nr, nc, at))
});
}
pub fn lane_delete(&mut self, cols_mode: bool, lo: usize, hi: usize) {
self.edit_array(|rows, cols, cells, c| {
let n = hi - lo + 1;
if cols_mode {
if n >= cols || hi >= cols {
return None;
}
for r in (0..rows).rev() {
cells.drain(r * cols + lo..r * cols + hi + 1);
}
let j = c % cols;
let j = if j > hi { j - n } else { j.min(cols - n - 1) };
Some((rows, cols - n, (c / cols) * (cols - n) + j))
} else {
if n >= rows || hi >= rows {
return None;
}
cells.drain(lo * cols..(hi + 1) * cols);
let r = c / cols;
let r = if r > hi { r - n } else { r.min(rows - n - 1) };
Some((rows - n, cols, r * cols + c % cols))
}
});
}
pub fn grid_move(&mut self, dr: isize, dc: isize) {
let Some((k, i, c)) = self.enclosing_array() else {
return;
};
let parent_path = self.path[..k].to_vec();
let Node::Array { rows, cols, .. } = &row_at(&self.root, &parent_path)[i] else {
unreachable!()
};
let (rows, cols) = (*rows, *cols);
let r = (c / cols).saturating_add_signed(dr).min(rows - 1);
let j = (c % cols).saturating_add_signed(dc).min(cols - 1);
self.path.truncate(k);
self.path.push((i, Field::Cell(r * cols + j)));
self.col = self.cur_row().len();
}
pub(crate) fn grid_info(&self) -> Option<(usize, usize, usize, usize, usize)> {
let (k, i, c) = self.enclosing_array()?;
let Node::Array { rows, cols, .. } = &row_at(&self.root, &self.path[..k])[i] else {
unreachable!()
};
Some((k, i, *rows, *cols, c))
}
pub(crate) fn reclamp_grid(&mut self) {
let Some(gs) = self.grid else { return };
let Some((_, _, rows, cols, _)) = self.grid_info() else {
self.grid = None;
return;
};
self.grid = Some(match gs {
GridSel::Cells { anchor } => GridSel::Cells {
anchor: anchor.filter(|&a| a < rows * cols),
},
GridSel::Lanes { cols: cm, pos, ext } => {
let n = if cm { cols } else { rows };
GridSel::Lanes {
cols: cm,
pos: pos.min(2 * n),
ext: ext.filter(|&e| e < n),
}
}
});
}
pub fn grid_rect(&self) -> Option<(usize, usize, usize, usize)> {
let (_, _, rows, cols, c) = self.grid_info()?;
match self.grid? {
GridSel::Cells { anchor } => {
let a = anchor.unwrap_or(c).min(rows * cols - 1);
let c = c.min(rows * cols - 1);
let (r0, r1) = ((a / cols).min(c / cols), (a / cols).max(c / cols));
let (j0, j1) = ((a % cols).min(c % cols), (a % cols).max(c % cols));
Some((r0, j0, r1, j1))
}
GridSel::Lanes { cols: cm, pos, ext } if pos % 2 == 1 => {
let n = if cm { cols } else { rows };
let lane = (pos / 2).min(n - 1);
let end = ext.unwrap_or(lane).min(n - 1);
let (lo, hi) = (lane.min(end), lane.max(end));
Some(if cm {
(0, lo, rows - 1, hi)
} else {
(lo, 0, hi, cols - 1)
})
}
_ => None,
}
}
pub fn grid_clear_cells(&mut self) {
let Some((r0, j0, r1, j1)) = self.grid_rect() else {
return;
};
self.edit_array(|rows, cols, cells, c| {
for r in r0..=r1.min(rows - 1) {
for j in j0..=j1.min(cols - 1) {
cells[r * cols + j].clear();
}
}
Some((rows, cols, c))
});
self.grid = Some(GridSel::Cells { anchor: None });
}
pub fn grid_copy_cells(&mut self) {
let Some((r0, j0, r1, j1)) = self.grid_rect() else {
return;
};
let Some((k, i, rows, cols, _)) = self.grid_info() else {
return;
};
let (r1, j1) = (r1.min(rows - 1), j1.min(cols - 1));
let Node::Array { cells, .. } = &row_at(&self.root, &self.path[..k])[i] else {
unreachable!()
};
let (ch, cw) = (r1 - r0 + 1, j1 - j0 + 1);
let mut out = Vec::with_capacity(ch * cw);
for r in r0..=r1 {
for j in j0..=j1 {
out.push(cells[r * cols + j].clone());
}
}
self.clip = Clip::Cells {
rows: ch,
cols: cw,
cells: out,
};
self.copy_flash = true;
}
pub fn grid_cut_cells(&mut self) {
self.grid_copy_cells();
if matches!(self.clip, Clip::Cells { .. }) {
self.grid_clear_cells();
}
}
pub fn grid_paste_cells(&mut self, ch: usize, cw: usize, clip: Vec<Row>) {
self.edit_array(|rows, cols, cells, c| {
let (r0, j0) = (c / cols, c % cols);
let (nr, nc) = ((r0 + ch).max(rows), (j0 + cw).max(cols));
let mut grown = vec![Vec::new(); nr * nc];
for r in 0..rows {
for j in 0..cols {
grown[r * nc + j] = std::mem::take(&mut cells[r * cols + j]);
}
}
for r in 0..ch {
for j in 0..cw {
grown[(r0 + r) * nc + (j0 + j)] = clip[r * cw + j].clone();
}
}
*cells = grown;
Some((nr, nc, r0 * nc + j0))
});
if self.grid.is_some() {
self.grid = Some(GridSel::Cells { anchor: None });
}
}
pub fn insert_mid(&mut self) {
let Some(&(i, Field::Seg(k))) = self.path.last() else {
self.insert_sym('∣');
return;
};
let col = self.col;
let parent_path = self.path[..self.path.len() - 1].to_vec();
let Node::Delim { mids, segs, .. } = &mut row_at_mut(&mut self.root, &parent_path)[i]
else {
self.insert_sym('∣');
return;
};
let tail: Row = segs[k].split_off(col);
segs.insert(k + 1, tail);
*mids += 1;
*self.path.last_mut().unwrap() = (i, Field::Seg(k + 1));
self.col = 0;
}
fn apply_accent(&mut self, mark: Accent) {
if self.selection().is_some() {
if let Some(content) = self.take_selection() {
let content: Row = content.into_iter().filter(|n| *n != Node::Break).collect();
let under = mark.under();
let (overs, unders) = if under {
(vec![], vec![mark])
} else {
(vec![mark], vec![])
};
let node = Node::WideAccent {
overs,
unders,
base: content,
};
let col = self.col;
self.cur_row_mut().insert(col, node);
self.col += 1;
}
return;
}
if self.col == 0 {
self.info("accent needs a base character before the cursor");
return;
}
let col = self.col;
let row = self.cur_row_mut();
let under = mark.under();
match &mut row[col - 1] {
Node::Sym(c) => {
let (mut overs, mut unders) = (vec![], vec![]);
if under {
unders.push(mark)
} else {
overs.push(mark)
}
row[col - 1] = Node::Accent {
overs,
unders,
base: *c,
};
}
Node::Accent { overs, unders, .. } => {
if under {
unders.push(mark)
} else {
overs.push(mark)
}
}
Node::WideAccent { overs, unders, .. } => if under { unders } else { overs }.push(mark),
_ => self.info("accents apply to a single character"),
}
}
pub fn select_move(&mut self, right: bool) {
let crossed = if right {
Some(self.col)
} else {
self.col.checked_sub(1)
};
if crossed.is_some_and(|i| matches!(self.cur_row().get(i), Some(Node::Break))) {
return;
}
if self.select_anchor.is_none() || self.select_path != self.path {
self.select_anchor = Some(self.col);
self.select_path = self.path.clone();
}
if self.select_whole
&& let Some(a) = self.select_anchor
&& a != self.col
{
let next = if right {
self.col + 1
} else {
self.col.wrapping_sub(1)
};
if next == a {
self.select_anchor = Some(self.col);
self.col = a;
}
}
self.select_whole = false;
if right {
self.col = (self.col + 1).min(self.cur_row().len());
} else {
self.col = self.col.saturating_sub(1);
}
if self.select_anchor == Some(self.col) {
self.select_anchor = None;
}
}
pub fn selection(&self) -> Option<(usize, usize)> {
let a = self.select_anchor?;
if a == self.col || self.select_path != self.path {
return None;
}
let (lo, hi) = (a.min(self.col), a.max(self.col));
if hi > self.cur_row().len() {
return None;
}
Some((lo, hi))
}
pub fn take_selection(&mut self) -> Option<Row> {
let (lo, hi) = self.selection()?;
self.select_anchor = None;
self.col = lo;
Some(self.cur_row_mut().drain(lo..hi).collect())
}
pub fn delete_selection(&mut self) -> bool {
self.take_selection().is_some()
}
pub fn copy_selection(&mut self) {
if let Some((lo, hi)) = self.selection() {
self.clip = Clip::Nodes(self.cur_row()[lo..hi].to_vec());
self.copy_flash = true;
}
}
pub fn cut_selection(&mut self) {
if let Some(content) = self.take_selection() {
self.clip = Clip::Nodes(content);
}
}
pub fn paste(&mut self) {
if self.clip.is_empty() {
return;
}
self.select_anchor = None;
match self.clip.clone() {
Clip::Nodes(clip) => {
let clip: Row = if self.path.is_empty() {
clip
} else {
clip.into_iter().filter(|n| *n != Node::Break).collect()
};
let col = self.col;
let row = self.cur_row_mut();
row.splice(col..col, clip.iter().cloned());
self.col += clip.len();
}
Clip::Cells { rows, cols, cells } => {
if self.enclosing_array().is_some() {
self.grid_paste_cells(rows, cols, cells);
} else {
let node = Node::Array { rows, cols, cells };
let col = self.col;
self.cur_row_mut().insert(col, node);
self.col += 1;
}
}
}
}
pub fn select_parent(&mut self) {
if let Some((i, _)) = self.path.pop() {
self.select_anchor = Some(i);
self.select_path = self.path.clone();
self.col = i + 1;
self.select_whole = true;
} else if !self.cur_row().is_empty() {
self.select_anchor = Some(0);
self.select_path = self.path.clone();
self.col = self.cur_row().len();
self.select_whole = true;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::latex::row_to_latex;
#[test]
fn typing_a_formula() {
let mut ed = Editor::new();
ed.insert_sym('x');
ed.insert_and_enter(Node::Sup { arg: vec![] });
ed.insert_sym('2');
ed.exit_inset();
ed.insert_sym('+');
ed.execute("frac");
ed.insert_sym('1');
ed.vertical(false); ed.insert_sym('2');
ed.exit_inset();
assert_eq!(row_to_latex(&ed.root), "x^{2}+\\frac{1}{2}");
}
#[test]
fn arrow_navigation_enters_and_leaves_structures() {
let mut ed = Editor::new();
ed.execute("frac");
ed.insert_sym('a');
ed.right();
assert_eq!(ed.path.last().unwrap().1, Field::FracDen);
ed.insert_sym('b');
ed.right();
assert!(ed.path.is_empty());
assert_eq!(ed.col, 1);
ed.left();
assert_eq!(ed.path.last().unwrap().1, Field::FracDen);
assert_eq!(ed.col, 1);
}
#[test]
fn delim_mids_are_pipe_only() {
let mut ed = Editor::new();
ed.execute("delim([]");
assert!(
ed.root.is_empty(),
"\\delim([] (bracket mid) must be rejected, got {:?}",
ed.root
);
ed.execute("delim(|]");
assert!(matches!(
ed.root[0],
Node::Delim {
left: Delim::Col(ColDelim::Paren),
right: Delim::Col(ColDelim::Bracket),
mids: 1,
..
}
));
}
#[test]
fn lr_spec_reads_visually_and_accepts_names() {
let mut ed = Editor::new();
ed.execute("lr(]");
assert!(matches!(
ed.root[0],
Node::Delim {
left: Delim::Col(ColDelim::Paren),
right: Delim::Col(ColDelim::Bracket),
..
}
));
let mut ed = Editor::new();
ed.execute("lr\\langle||\\rangle");
assert!(matches!(ed.root[0],
Node::Delim { left: Delim::Angle, right: Delim::Angle, mids: 2, ref segs }
if segs.len() == 3));
let mut ed = Editor::new();
ed.execute("lr");
assert!(ed.root.is_empty());
assert!(ed.message.contains("takes delimiters"), "{:?}", ed.message);
assert_eq!(lr_spec("lrfoo"), None);
assert_eq!(lr_spec("lr]["), None);
assert_eq!(lr_spec("lr)("), None);
assert!(lr_spec("lr|.").is_some());
}
#[test]
fn angle_is_the_symbol_not_the_delimiter() {
let mut ed = Editor::new();
ed.execute("angle");
assert_eq!(ed.root, vec![Node::Sym('∠')]);
}
#[test]
fn op_box() {
let type_name = |ed: &mut Editor, s: &str| {
for c in s.chars() {
ed.op_type(c);
}
ed.op_commit();
};
let mut ed = Editor::new();
ed.execute("op");
assert!(ed.op_entry.is_some());
type_name(&mut ed, "vol");
assert_eq!(row_to_latex(&ed.root), "\\operatorname{vol}");
let mut ed = Editor::new();
ed.execute("op");
type_name(&mut ed, "sin");
assert_eq!(ed.root, vec![Node::Func("sin".into())]);
let mut ed = Editor::new();
ed.execute("op");
type_name(&mut ed, "arg blah");
assert_eq!(
ed.root,
vec![
Node::Func("arg".into()),
Node::Sym('␣'),
Node::Func("blah".into())
]
);
let mut ed = Editor::new();
ed.execute("op*");
type_name(&mut ed, "ess sup");
assert_eq!(ed.path.last().unwrap().1, Field::OpLower);
ed.insert_sym('x');
ed.exit_inset();
assert!(matches!(&ed.root[0], Node::BigOp { name, .. } if name == "esssup"));
assert_eq!(row_to_latex(&ed.root), "\\operatorname*{esssup}_{x}");
let mut ed = Editor::new();
ed.execute("op");
ed.op_commit();
assert!(ed.root.is_empty() && ed.op_entry.is_none());
let mut ed = Editor::new();
ed.execute("op");
ed.op_type('a');
ed.op_backspace();
ed.op_backspace();
assert!(ed.op_entry.is_none() && ed.root.is_empty());
}
#[test]
fn rm_and_op_arguments_are_alphanumeric_only() {
for cmd in ["op*)", "rm*", "opα", "opvol"] {
let mut ed = Editor::new();
ed.execute(cmd);
assert!(
ed.root.is_empty(),
"\\{} must be rejected, got {:?}",
cmd,
ed.root
);
assert!(!ed.message.is_empty());
}
let mut ed = Editor::new();
ed.execute("text(a)");
assert!(ed.root.is_empty(), "brackets rejected: {:?}", ed.root);
let mut ed = Editor::new();
ed.execute("texta+b");
assert_eq!(row_to_latex(&ed.root), "\\text{a+b}");
}
#[test]
fn argmax_makes_a_named_band() {
let mut ed = Editor::new();
ed.execute("argmax");
assert_eq!(ed.path.last().unwrap().1, Field::OpLower);
ed.insert_sym('x');
ed.exit_inset();
assert_eq!(row_to_latex(&ed.root), "\\operatorname*{arg\\,max}_{x}");
}
#[test]
fn backspace_deletes_empty_structure() {
let mut ed = Editor::new();
ed.execute("sqrt");
ed.backspace();
assert!(ed.root.is_empty());
assert_eq!(ed.col, 0);
}
#[test]
fn copy_cut_paste_and_parent_selection() {
let mut ed = Editor::new();
for c in "ab".chars() {
ed.insert_sym(c);
}
ed.execute("frac");
ed.insert_sym('x');
ed.select_parent();
assert_eq!(ed.selection(), Some((2, 3)));
ed.copy_selection();
ed.paste(); assert_eq!(ed.root.len(), 4);
assert!(matches!(ed.root[3], Node::Frac { .. }));
ed.select_move(false);
ed.cut_selection();
assert_eq!(ed.root.len(), 3);
ed.home();
ed.paste();
assert!(matches!(ed.root[0], Node::Frac { .. }));
assert_eq!(ed.col, 1);
ed.select_parent();
assert_eq!(ed.selection(), Some((0, 4)));
}
#[test]
fn selection_wrap_and_delete() {
let mut ed = Editor::new();
for c in "abc".chars() {
ed.insert_sym(c);
}
ed.select_move(false);
ed.select_move(false);
assert_eq!(ed.selection(), Some((1, 3)));
assert!(ed.delete_selection());
assert_eq!(ed.root, vec![Node::Sym('a')]);
ed.insert_sym('b');
ed.select_move(false);
ed.select_move(false);
ed.execute("frac");
ed.insert_sym('2');
assert_eq!(
ed.root,
vec![Node::Frac {
num: vec![Node::Sym('a'), Node::Sym('b')],
den: vec![Node::Sym('2')],
}]
);
}
#[test]
fn close_paren_exits_inset() {
let mut ed = Editor::new();
ed.insert_delim(Delim::Col(ColDelim::Paren), Delim::Col(ColDelim::Paren), 0);
ed.insert_sym('x');
ed.close_paren();
assert!(ed.path.is_empty());
assert_eq!(ed.col, 1);
assert_eq!(row_to_latex(&ed.root), "\\left(x\\right)");
}
#[test]
fn vertical_promotes_a_bare_big_operator() {
let mut ed = Editor::new();
ed.root = vec![Node::Sym('∑')];
ed.col = 1;
ed.vertical(false); assert_eq!(ed.path.last().unwrap().1, Field::OpLower);
ed.insert_sym('n');
ed.exit_inset();
assert_eq!(row_to_latex(&ed.root), "\\sum_{n}");
let mut ed = Editor::new();
ed.root = vec![Node::Func("lim".into())];
ed.col = 1;
ed.vertical(true); assert_eq!(ed.path.last().unwrap().1, Field::OpUpper);
let mut ed = Editor::new();
ed.root = vec![Node::Sym('x')];
ed.col = 1;
ed.vertical(false);
assert!(ed.path.is_empty());
}
#[test]
fn limit_functions_enter_lower() {
let mut ed = Editor::new();
ed.execute("lim");
for c in "x→0".chars() {
ed.insert_sym(c);
}
ed.exit_inset();
ed.insert_sym('f');
assert_eq!(row_to_latex(&ed.root), "\\operatorname*{lim}_{x\\to 0}f");
}
#[test]
fn arrows_and_text_runs() {
let mut ed = Editor::new();
ed.insert_sym('A');
ed.execute("xto");
ed.insert_sym('f');
ed.exit_inset();
ed.insert_sym('B');
ed.execute("rmdx");
assert_eq!(
row_to_latex(&ed.root),
"A\\xrightarrow{f}B\\operatorname{dx}"
);
}
#[test]
fn accent_stacking() {
let mut ed = Editor::new();
ed.insert_sym('a');
ed.execute("vec");
ed.execute("hat");
ed.execute("underline");
assert_eq!(row_to_latex(&ed.root), "\\hat{\\vec{\\underline{a}}}");
}
#[test]
fn grid_size_suffix() {
let mut ed = Editor::new();
ed.execute("bmatrix13"); ed.insert_sym('a');
assert_eq!(
row_to_latex(&ed.root),
"\\begin{bmatrix} a & & \\end{bmatrix}"
);
let mut ed = Editor::new();
ed.execute("matrix13");
ed.insert_sym('a');
assert_eq!(
row_to_latex(&ed.root),
"\\begin{matrix} a & & \\end{matrix}"
);
let mut ed = Editor::new();
ed.execute("cases32");
let Node::Delim { segs, .. } = &ed.root[0] else {
panic!()
};
let [
Node::Array {
rows: 3, cols: 2, ..
},
] = &segs[0][..]
else {
panic!()
};
}
#[test]
fn grid_row_col_editing() {
let mut ed = Editor::new();
ed.execute("bmatrix22"); ed.insert_sym('a');
ed.execute("addcol"); ed.insert_sym('x');
ed.execute("addrow"); ed.insert_sym('y');
assert_eq!(
row_to_latex(&ed.root),
"\\begin{bmatrix} a & x & \\\\ & y & \\\\ & & \\end{bmatrix}"
);
ed.execute("delcol"); ed.execute("delrow");
assert_eq!(
row_to_latex(&ed.root),
"\\begin{bmatrix} a & \\\\ & \\end{bmatrix}"
);
ed.execute("delrow");
ed.execute("delrow");
assert_eq!(ed.message, "cannot remove the last row/column");
}
#[test]
fn grid_and_mid_editing() {
let mut ed = Editor::new();
ed.execute("bmatrix22");
ed.insert_sym('a');
assert_eq!(ed.path.len(), 2);
ed.close_bracket();
assert!(ed.path.is_empty());
assert_eq!(
row_to_latex(&ed.root),
"\\begin{bmatrix} a & \\\\ & \\end{bmatrix}"
);
let mut ed = Editor::new();
ed.execute("braket");
ed.insert_sym('x');
ed.right(); ed.insert_sym('y');
assert_eq!(
row_to_latex(&ed.root),
"\\left\\langle x\\middle|y\\right\\rangle "
);
ed.execute("mid"); ed.insert_sym('z');
assert_eq!(
row_to_latex(&ed.root),
"\\left\\langle x\\middle|y\\middle|z\\right\\rangle "
);
}
}