use formulaa::ast::Field;
use formulaa::ast::normalize;
use formulaa::editor::{Ask, Editor};
use formulaa::input::{Effect, Key};
use formulaa::latex::row_to_latex;
use formulaa::parse::parse;
use formulaa::render::{RenderCtx, render_root};
fn named(tok: &str) -> Option<Key> {
Some(match tok {
"Left" => Key::Left,
"Right" => Key::Right,
"Up" => Key::Up,
"Down" => Key::Down,
"Home" => Key::Home,
"End" => Key::End,
"Tab" => Key::Tab,
"Enter" => Key::Enter,
"Backspace" => Key::Backspace,
"Delete" => Key::Delete,
"Esc" => Key::Esc,
"Space" => Key::Char(' '),
_ => return None,
})
}
fn type_script(ed: &mut Editor, script: &str) -> Vec<Effect> {
let mut effects = Vec::new();
for tok in script.split_whitespace() {
let (tok, shift, ctrl) = match tok.strip_prefix("S-") {
Some(rest) => (rest, true, false),
None => match tok.strip_prefix("C-") {
Some(rest) => (rest, false, true),
None => (tok, false, false),
},
};
if let Some(key) = named(tok) {
effects.push(ed.input(key, shift, ctrl));
} else if let Some(cmd) = tok.strip_prefix('\\').filter(|c| !c.is_empty()) {
effects.push(ed.input(Key::Char('\\'), false, false));
for c in cmd.chars() {
effects.push(ed.input(Key::Char(c), false, false));
}
effects.push(ed.input(Key::Enter, false, false));
} else {
for c in tok.chars() {
effects.push(ed.input(Key::Char(c), shift, ctrl));
}
}
}
effects
}
fn latex(ed: &Editor) -> String {
row_to_latex(&normalize(&ed.root))
}
fn aa(ed: &Editor) -> String {
render_root(&normalize(&ed.root), None, &RenderCtx::canonical()).to_text()
}
#[test]
fn typing_a_formula_by_keys() {
let mut ed = Editor::new();
type_script(&mut ed, r"x ^ 2 Tab + \frac 1 Down 2");
assert_eq!(latex(&ed), "x^{2}+\\frac{1}{2}");
}
#[test]
fn minibuffer_esc_cancels_and_backspace_erases() {
let mut ed = Editor::new();
type_script(&mut ed, r"\ f r a Esc a");
assert_eq!(latex(&ed), "a");
let mut ed = Editor::new();
type_script(&mut ed, r"\ f Backspace Backspace b");
assert_eq!(latex(&ed), "b");
assert!(ed.minibuffer.is_none());
}
#[test]
fn selection_wraps_into_fraction() {
let mut ed = Editor::new();
type_script(&mut ed, r"a+b S-Left S-Left S-Left \frac 2");
assert_eq!(latex(&ed), "\\frac{a+b}{2}");
}
#[test]
fn wrap_keys_wrap_the_selection() {
let mut ed = Editor::new();
type_script(&mut ed, "y x S-Left ^");
assert_eq!(latex(&ed), "y^{x}");
let mut ed = Editor::new();
type_script(&mut ed, "a + b S-Left S-Left S-Left (");
assert_eq!(latex(&ed), "\\left(a+b\\right)");
}
#[test]
fn every_radical_wraps_the_selection() {
for (cmd, want) in [
("sqrt", "\\sqrt{a+b}"),
("cbrt", "\\sqrt[3]{a+b}"),
("qdrt", "\\sqrt[4]{a+b}"),
] {
let mut ed = Editor::new();
type_script(&mut ed, r"a + b S-Left S-Left S-Left");
ed.execute(cmd);
assert_eq!(latex(&ed), want, "\\{}", cmd);
}
}
#[test]
fn rm_box_space_commits() {
let mut ed = Editor::new();
type_script(&mut ed, r"\rm a Space b");
assert!(ed.op_entry.is_none());
assert_eq!(latex(&ed), "\\mathrm{a}b");
let mut ed = Editor::new();
ed.execute("text");
for c in "if x".chars() {
ed.input(Key::Char(c), false, false);
}
ed.input(Key::Enter, false, false);
assert_eq!(latex(&ed), "\\text{if x}");
}
#[test]
fn box_caret_moves_and_edge_commits() {
let mut ed = Editor::new();
type_script(&mut ed, r"\rm a c Left b Enter");
assert_eq!(latex(&ed), "\\operatorname{abc}");
let mut ed = Editor::new();
type_script(&mut ed, r"\rm a b Right");
assert!(ed.op_entry.is_none(), "right edge committed the box");
assert_eq!(latex(&ed), "\\operatorname{ab}");
let mut ed = Editor::new();
type_script(&mut ed, r"\rm a b Left Left Left");
assert!(ed.op_entry.is_none(), "left edge committed the box");
assert_eq!(latex(&ed), "\\operatorname{ab}");
assert_eq!(ed.col, 0, "the exiting ← still moved the cursor");
let mut ed = Editor::new();
type_script(&mut ed, r"\rm a b c Home Delete End d Enter");
assert_eq!(latex(&ed), "\\operatorname{bcd}");
}
#[test]
fn tex_box_reads_latex_in_place() {
let mut ed = Editor::new();
ed.execute("latex");
assert!(ed.op_entry.is_some());
for c in r"\frac{1}{2}+\alpha".chars() {
ed.op_type(c);
}
ed.op_commit();
assert_eq!(latex(&ed), "\\frac{1}{2}+\\alpha ");
ed.input(Key::Char('x'), false, false);
assert_eq!(latex(&ed), "\\frac{1}{2}+\\alpha x");
let mut ed = Editor::new();
ed.execute("latex");
for c in r"\begin{".chars() {
ed.op_type(c);
}
ed.op_commit();
assert_eq!(ed.root, vec![]);
let mut ed = Editor::new();
type_script(&mut ed, r"\ l a t e x Space");
assert!(ed.op_entry.is_some(), "{:?}", ed.op_entry);
for c in r"x ^ 2".chars() {
ed.input(Key::Char(c), false, false);
}
ed.input(Key::Enter, false, false);
assert_eq!(latex(&ed), "x^{2}");
}
#[test]
fn minibuffer_previews_the_commit() {
let mut ed = Editor::new();
type_script(&mut ed, r"\ a l p h a");
assert_eq!(ed.command_preview(), Some('α'));
assert_eq!(
ed.command_preview_row(),
Some(vec![formulaa::ast::Node::Sym('α')])
);
let mut ed = Editor::new();
type_script(&mut ed, r"\ s u m");
assert_eq!(ed.command_preview(), Some('∑'));
let mut ed = Editor::new();
type_script(&mut ed, r"\ f r a c");
assert_eq!(ed.command_preview(), None);
assert!(matches!(
ed.command_preview_row().as_deref(),
Some([formulaa::ast::Node::Frac { .. }])
));
let mut ed = Editor::new();
type_script(&mut ed, r"\ f r");
assert_eq!(ed.command_preview_row(), None);
let mut ed = Editor::new();
type_script(&mut ed, r"\ l a t e x");
assert_eq!(
ed.command_preview_row(),
Some(vec![formulaa::ast::Node::Sym('⬚')])
);
let mut ed = Editor::new();
type_script(&mut ed, r"\ a d d r o w");
assert_eq!(ed.command_preview_row(), None);
let mut ed = Editor::new();
type_script(&mut ed, r"x \ f r a c");
assert_eq!(latex(&ed), "x");
}
#[test]
fn ctrl_keys_return_host_effects() {
let mut ed = Editor::new();
assert_eq!(ed.input(Key::Char('q'), false, true), Effect::Quit);
assert_eq!(ed.input(Key::Char('y'), false, true), Effect::CopyAa);
assert_eq!(ed.input(Key::Char('o'), false, true), Effect::Write);
assert_eq!(ed.input(Key::Char('w'), false, true), Effect::WriteQuit);
assert_eq!(ed.input(Key::Char('q'), false, false), Effect::None);
}
#[test]
fn ctrl_h_deletes_left_everywhere() {
let mut ed = Editor::new();
type_script(&mut ed, "a b C-h");
assert_eq!(latex(&ed), "a");
type_script(&mut ed, r"\ a l C-h");
assert_eq!(ed.minibuffer.as_deref(), Some("a"));
let mut ed = Editor::new();
ed.ask_path("");
type_script(&mut ed, "x y C-h");
assert_eq!(ed.ask, Some(Ask::Path("x".into())));
}
#[test]
fn status_line_questions_answer_the_host() {
let mut ed = Editor::new();
type_script(&mut ed, "a");
ed.ask_path("");
let fx = type_script(&mut ed, "f o o . a a");
assert!(fx.iter().all(|e| *e == Effect::None), "{:?}", fx);
assert_eq!(ed.ask, Some(Ask::Path("foo.aa".into())), "not collected");
assert_eq!(latex(&ed), "a", "the name reached the formula");
assert_eq!(
ed.input(Key::Enter, false, false),
Effect::WriteTo("foo.aa".into())
);
assert_eq!(ed.ask, None, "the question outlived its answer");
let mut ed = Editor::new();
ed.ask_path("");
type_script(&mut ed, "x Esc");
assert_eq!(ed.ask, None);
assert_eq!(latex(&ed), "", "the typing leaked into the formula");
let mut ed = Editor::new();
ed.ask_path("");
assert_eq!(ed.input(Key::Char('q'), false, true), Effect::None);
assert_eq!(ed.ask, Some(Ask::Path(String::new())), "\\q leaked");
for (script, want) in [
("y", Effect::WriteQuit),
("n", Effect::Discard),
("Enter", Effect::WriteQuit),
] {
let mut ed = Editor::new();
ed.ask_save_first();
let fx = type_script(&mut ed, script);
assert!(fx.contains(&want), "{}: {:?}", script, fx);
assert_eq!(ed.ask, None);
}
let mut ed = Editor::new();
ed.ask_save_first();
let fx = type_script(&mut ed, "Esc");
assert!(fx.iter().all(|e| *e == Effect::None), "{:?}", fx);
assert_eq!(ed.ask, None);
let mut ed = Editor::new();
type_script(&mut ed, "a");
ed.ask_save_first();
let fx = type_script(&mut ed, "x Space Tab Down C-y");
assert!(fx.iter().all(|e| *e == Effect::None), "{:?}", fx);
assert_eq!(ed.ask, Some(Ask::SaveFirst), "the question was answered");
assert_eq!(latex(&ed), "a", "a key leaked into the formula");
}
#[test]
fn esc_cancels_modes_before_quitting() {
let mut ed = Editor::new();
type_script(&mut ed, "a S-Left");
assert_eq!(ed.input(Key::Esc, false, false), Effect::None);
assert_eq!(ed.selection(), None);
assert_eq!(ed.input(Key::Char('\\'), false, false), Effect::None);
assert_eq!(ed.input(Key::Esc, false, false), Effect::None);
assert!(ed.minibuffer.is_none());
assert_eq!(ed.input(Key::Esc, false, false), Effect::Quit);
}
#[test]
fn ctrl_e_jumps_to_the_end_outside_grids() {
let mut ed = Editor::new();
type_script(&mut ed, r"a+b C-a");
assert_eq!((ed.path.len(), ed.col), (0, 0));
type_script(&mut ed, "C-e");
assert_eq!((ed.path.len(), ed.col), (0, 3), "formula end");
let mut ed = Editor::new();
type_script(&mut ed, r"\bmatrix22 x C-g");
assert!(ed.grid.is_some());
}
#[test]
fn accent_wraps_selection_as_wide_accent() {
let mut ed = Editor::new();
type_script(&mut ed, r"abc S-Left S-Left S-Left \hat");
assert_eq!(latex(&ed), "\\widehat{abc}");
type_script(&mut ed, r"\underline");
assert_eq!(latex(&ed), "\\widehat{\\underline{abc}}");
let mut ed = Editor::new();
type_script(&mut ed, r"x S-Left \vec");
assert_eq!(latex(&ed), "\\vec{x}");
let mut ed = Editor::new();
type_script(&mut ed, r"\frac 1 Down 2 Tab S-Left \bar");
assert_eq!(latex(&ed), "\\overline{\\frac{1}{2}}");
let mut ed = Editor::new();
type_script(&mut ed, r"abc S-Left S-Left S-Left \hat");
type_script(&mut ed, r"\vec");
assert_eq!(latex(&ed), "\\overrightarrow{\\widehat{abc}}");
let mut ed = Editor::new();
type_script(&mut ed, r"AB S-Left S-Left \utilde");
assert_eq!(latex(&ed), "\\utilde{AB}");
let mut ed = Editor::new();
type_script(&mut ed, r"x S-Left \utilde");
assert_eq!(latex(&ed), "\\utilde{x}");
}
#[test]
fn caret_underscore_commands_insert_scripts() {
let mut ed = Editor::new();
type_script(&mut ed, r"x \^z");
assert_eq!(latex(&ed), "x^{z}");
let mut ed = Editor::new();
type_script(&mut ed, r"x \_10 \^gamma");
assert_eq!(latex(&ed), "x_{10}^{\\gamma }");
for spelling in [r"x \z^", r"x \^z^"] {
let mut ed = Editor::new();
type_script(&mut ed, spelling);
assert_eq!(latex(&ed), "x^{z}", "{}", spelling);
}
let mut ed = Editor::new();
type_script(&mut ed, r"x \i_");
assert_eq!(latex(&ed), "x_{i}");
}
#[test]
fn command_known_tracks_execute() {
let ed = Editor::new();
for ok in [
"frac",
"sqrt",
"alpha",
"sin",
"lim",
"argmax",
"hat",
"^z",
"rmdx",
"pmatrix22",
"lr(]",
] {
assert!(ed.command_known(ok), "\\{} should be known", ok);
}
for bad in [
"",
"fra",
"nosuchthing",
"zzz",
"lr",
"lr(",
"matrix",
"pmatrix",
"matrix3",
] {
assert!(!ed.command_known(bad), "\\{} should be unknown", bad);
}
let mut ed = Editor::new();
type_script(&mut ed, "x");
assert!(ed.command_known("frac"));
assert_eq!(latex(&ed), "x");
}
#[test]
fn aliases_resolve_to_the_same_command() {
let command_aliases = [
("sqrt3", "cbrt"),
("sqrt4", "qdrt"),
("negate", "!"),
("xrightarrow", "xto"),
("xleftarrow", "xfrom"),
("xRightarrow", "xTo"),
("xLeftarrow", "xFrom"),
("operatorname", "op"),
("operatorname*", "op*"),
("limits", "op*"),
("delim", "lr"),
];
let symbol_aliases = formulaa::symbols::NAMES
.entries()
.filter_map(|(&from, &ch)| {
if formulaa::symbols::is_func_name(from) {
return None;
}
let to = formulaa::symbols::latex_name(ch)?;
if to.contains('\\') {
return None;
}
(from != to).then_some((from, to))
});
for (from, to) in command_aliases.into_iter().chain(symbol_aliases) {
let (mut a, mut b) = (Editor::new(), Editor::new());
a.execute(from);
b.execute(to);
assert_eq!(
(normalize(&a.root), a.op_entry.as_ref().map(|e| e.0)),
(normalize(&b.root), b.op_entry.as_ref().map(|e| e.0)),
"\\{} should be \\{}",
from,
to
);
assert!(
a.message.is_empty() || !a.message.starts_with("unknown"),
"\\{}",
from
);
}
}
#[test]
fn typing_inside_a_norm_works() {
let mut ed = Editor::new();
ed.execute("norm");
ed.input(Key::Char('x'), false, false);
assert_eq!(latex(&ed), "\\left\\|x\\right\\|");
}
#[test]
fn plain_motions_shed_the_anchor() {
let mut ed = Editor::new();
type_script(&mut ed, r"a+b S-Left Home");
assert_eq!(ed.selection(), None);
type_script(&mut ed, r"Backspace");
assert_eq!(latex(&ed), "a+b", "nothing left of the cursor to delete");
let mut ed = Editor::new();
type_script(&mut ed, r"a+b Left S-Left End");
assert_eq!(ed.selection(), None);
let mut ed = Editor::new();
type_script(&mut ed, r"x ^ abc Left Left S-Right Tab Backspace");
assert_eq!(ed.selection(), Some((1, 2)), "the sup is selected whole");
type_script(&mut ed, r"Backspace");
assert_eq!(latex(&ed), "x", "the second press deletes the sup");
let mut ed = Editor::new();
type_script(&mut ed, r"( abc Left Left S-Right ) Backspace");
assert_eq!(ed.selection(), Some((0, 1)), "the pair is selected whole");
type_script(&mut ed, r"Backspace");
assert_eq!(latex(&ed), "");
}
#[test]
fn enter_on_empty_formula_does_not_crash() {
let mut ed = Editor::new();
type_script(&mut ed, "Enter Enter Up Down a");
assert_eq!(latex(&ed), "a");
}
#[test]
fn grid_edit_mode() {
let mut ed = Editor::new();
type_script(
&mut ed,
r"\bmatrix22 a C-g Right Enter b C-g Down Left Enter c C-g Right Enter d",
);
assert_eq!(
latex(&ed),
"\\begin{bmatrix} a & b \\\\ c & d \\end{bmatrix}"
);
type_script(&mut ed, "C-g r Backspace");
assert_eq!(latex(&ed), "\\begin{bmatrix} a & b \\end{bmatrix}");
type_script(&mut ed, "c Backspace");
assert_eq!(latex(&ed), "\\left[a\\right]");
let mut ed = Editor::new();
type_script(&mut ed, r"\bmatrix22 x C-g | Left Enter Up Enter y");
assert_eq!(
latex(&ed),
"\\begin{bmatrix} & x & \\\\ y & & \\end{bmatrix}"
);
let mut ed = Editor::new();
type_script(&mut ed, r"\bmatrix22 a Down b C-g r Backspace C-z");
assert!(
latex(&ed).contains("\\\\"),
"undo restored the row: {}",
latex(&ed)
);
let mut ed = Editor::new();
type_script(&mut ed, "x C-g d");
assert_eq!(latex(&ed), "xd");
}
#[test]
fn grid_selection_promotes_and_clears() {
let mut ed = Editor::new();
type_script(&mut ed, r"\bmatrix22 a Down b C-g S-Up Backspace");
assert_eq!(latex(&ed), "\\begin{bmatrix} & \\\\ & \\end{bmatrix}");
let mut ed = Editor::new();
type_script(&mut ed, r"\bmatrix22 a Down b C-g S-Up S-Up");
assert!(
matches!(
ed.grid,
Some(formulaa::editor::GridSel::Lanes {
cols: true,
pos: 1,
..
})
),
"{:?}",
ed.grid
);
type_script(&mut ed, "Backspace");
assert_eq!(latex(&ed), "\\begin{bmatrix} \\\\ \\end{bmatrix}");
}
#[test]
fn esc_exits_grid_mode_from_lane_mode() {
let mut ed = Editor::new();
type_script(&mut ed, r"\bmatrix22 x C-g c Esc");
assert!(ed.grid.is_none(), "{:?}", ed.grid);
}
fn redraw(ed: &Editor) {
let _ = ed.decorated();
let _ = ed.marker_extents();
}
#[test]
fn grid_state_survives_undo_redo_and_clicks() {
let mut ed = Editor::new();
type_script(&mut ed, r"\bmatrix22 a C-g | Right Right Right Enter C-z");
redraw(&ed);
type_script(&mut ed, "Backspace");
redraw(&ed);
type_script(&mut ed, "Down Enter");
redraw(&ed);
let mut ed = Editor::new();
type_script(
&mut ed,
r"\bmatrix22 a C-g S-Down C-c Down Right C-v Down Down Right S-Up C-z",
);
redraw(&ed);
type_script(&mut ed, "C-c C-v Backspace");
redraw(&ed);
let mut ed = Editor::new();
type_script(&mut ed, r"\bmatrix22 a Tab Tab + x");
type_script(&mut ed, "C-a"); type_script(&mut ed, r"Right C-g S-Down");
ed.click(1000, 1000); redraw(&ed);
type_script(&mut ed, "C-c C-v");
redraw(&ed);
}
#[test]
fn lane_selection_copies_its_cells() {
let mut ed = Editor::new();
type_script(&mut ed, r"\bmatrix22 a Down b C-g S-Up S-Up C-c");
type_script(&mut ed, "Esc Tab Tab C-v");
assert!(
latex(&ed).ends_with("\\begin{matrix} a \\\\ b \\end{matrix}"),
"{}",
latex(&ed)
);
}
#[test]
fn cell_clip_pastes_over_cells_even_outside_grid_mode() {
let mut ed = Editor::new();
type_script(&mut ed, r"\bmatrix22 a Down b C-g S-Up C-c Esc Right C-v");
assert_eq!(
latex(&ed),
"\\begin{bmatrix} a & a \\\\ b & b \\end{bmatrix}"
);
}
#[test]
fn vmatrix_wraps_a_grid_in_a_norm() {
let mut ed = Editor::new();
ed.execute("Vmatrix22");
type_script(&mut ed, "a Right b");
assert_eq!(latex(&ed), "\\begin{Vmatrix} a & b \\\\ & \\end{Vmatrix}");
}
#[test]
fn grid_cells_copy_paste() {
let mut ed = Editor::new();
type_script(&mut ed, r"\bmatrix22 a Down b C-g S-Up C-c Down Right C-v");
assert_eq!(
latex(&ed),
"\\begin{bmatrix} a & \\\\ b & a \\\\ & b \\end{bmatrix}"
);
type_script(&mut ed, "Esc Tab Tab C-v");
assert!(
latex(&ed).ends_with("\\begin{matrix} a \\\\ b \\end{matrix}"),
"{}",
latex(&ed)
);
}
#[test]
fn rm_and_text_boxes() {
let mut ed = Editor::new();
type_script(&mut ed, r"\rm i.i.d. Enter x");
assert_eq!(latex(&ed), "\\operatorname{i.i.d.}x");
let mut ed = Editor::new();
type_script(&mut ed, r"\rm sin Enter");
assert_eq!(latex(&ed), "\\operatorname{sin}");
let mut ed = Editor::new();
type_script(&mut ed, r"\text if Space x Enter");
assert_eq!(latex(&ed), "\\text{if x}");
let mut ed = Editor::new();
type_script(&mut ed, r"\rm foo Esc \text bar Esc");
assert!(ed.root.is_empty());
}
#[test]
fn undo_redo() {
let mut ed = Editor::new();
type_script(&mut ed, r"a+b \frac 1 Down 2 Tab");
let full = latex(&ed);
type_script(&mut ed, "C-z");
assert_eq!(latex(&ed), "a+b\\frac{1}{}");
type_script(&mut ed, "C-z C-z C-z");
assert_eq!(latex(&ed), "a+");
type_script(&mut ed, "C-r C-r C-r C-r");
assert_eq!(latex(&ed), full);
type_script(&mut ed, "C-z C-z x");
assert_eq!(latex(&ed), "a+b\\frac{x}{}");
type_script(&mut ed, "C-r");
assert_eq!(latex(&ed), "a+b\\frac{x}{}");
let mut ed = Editor::new();
type_script(&mut ed, "a b Left Left Right C-z");
assert_eq!(latex(&ed), "a");
type_script(&mut ed, "z");
assert_eq!(latex(&ed), "az");
}
#[test]
fn op_box_via_keys() {
let mut ed = Editor::new();
type_script(&mut ed, r"\op* esssup Enter n Tab");
assert_eq!(latex(&ed), "\\operatorname*{esssup}_{n}");
let mut ed = Editor::new();
type_script(&mut ed, r"\op* ess Space");
assert_eq!(
latex(&ed),
"\\operatorname{ess}",
"Space did not commit the \\op* box"
);
let mut ed = Editor::new();
type_script(&mut ed, r"\op vol Right +1");
assert_eq!(latex(&ed), "\\operatorname{vol}+1");
let mut ed = Editor::new();
type_script(&mut ed, r"\op foo Esc");
assert!(ed.root.is_empty());
let mut ed = Editor::new();
type_script(&mut ed, r"\limits vol Enter n Tab");
assert_eq!(latex(&ed), "\\operatorname*{vol}_{n}");
}
#[test]
fn enter_at_top_level_breaks_the_line() {
let mut ed = Editor::new();
type_script(&mut ed, "a+b Enter =c");
assert_eq!(latex(&ed), "a+b \\\\ =c");
let pic = aa(&ed);
assert!(
pic.lines().nth(1).is_some_and(|l| l.trim_end() == "┈"),
"separator row:\n{}",
pic
);
type_script(&mut ed, "Up");
assert!(ed.col < 4, "moved to line 1, col {}", ed.col);
type_script(&mut ed, "Down Home Backspace");
assert_eq!(latex(&ed), "a+b=c");
}
#[test]
fn enter_inside_an_inset_is_inert() {
let mut ed = Editor::new();
type_script(&mut ed, r"\pmatrix22 a Enter");
let pic = aa(&ed);
assert_eq!(pic.matches('┼').count(), 1, "still 2×2:\n{}", pic);
assert!(
!ed.root
.iter()
.any(|n| matches!(n, formulaa::ast::Node::Break))
);
type_script(&mut ed, r"\addrow");
assert_eq!(aa(&ed).matches('┼').count(), 2);
}
#[test]
fn brackets_insert_a_delimiter_pair() {
let mut ed = Editor::new();
type_script(&mut ed, "[ x ]");
assert_eq!(latex(&ed), "\\left[x\\right]");
let mut ed = Editor::new();
for k in ['"', 'i', 'f', ' ', 'x', '"'] {
ed.input(Key::Char(k), false, false);
}
assert_eq!(latex(&ed), "\\text{if x}");
let mut ed = Editor::new();
for k in ['"', 'a', '\\', '"', 'b', '"'] {
ed.input(Key::Char(k), false, false);
}
assert_eq!(latex(&ed), "\\text{a\"b}");
}
#[test]
fn double_slash_makes_a_fraction() {
let mut ed = Editor::new();
type_script(&mut ed, "a // 1 Down 2 Tab");
assert_eq!(latex(&ed), "a\\frac{1}{2}");
let mut ed = Editor::new();
type_script(&mut ed, "a / b");
assert_eq!(latex(&ed), "a/b");
}
#[test]
fn copy_cut_paste_by_keys() {
let mut ed = Editor::new();
type_script(&mut ed, "a+b S-Left S-Left C-c End C-v");
assert_eq!(latex(&ed), "a+b+b");
type_script(&mut ed, "S-Left S-Left C-x Home C-v");
assert_eq!(latex(&ed), "+ba+b");
}
#[test]
fn arrows_collapse_selection_to_its_ends() {
let mut ed = Editor::new();
type_script(&mut ed, "a+b S-Left S-Left Left x");
assert_eq!(latex(&ed), "ax+b");
let mut ed = Editor::new();
type_script(&mut ed, "a+b S-Left S-Left Right y");
assert_eq!(latex(&ed), "a+by");
}
#[test]
fn ctrl_a_jumps_to_document_start() {
let mut ed = Editor::new();
type_script(&mut ed, r"a // 1 Down 2 C-a x");
assert_eq!(latex(&ed), "xa\\frac{1}{2}");
assert!(ed.path.is_empty());
}
#[test]
fn vertical_exits_a_grid_at_its_edge() {
let mut ed = Editor::new();
type_script(&mut ed, r"\pmatrix22 a Down b Down x");
assert!(ed.path.is_empty(), "path: {:?}", ed.path);
assert!(latex(&ed).ends_with('x'), "latex: {}", latex(&ed));
let mut ed = Editor::new();
type_script(&mut ed, r"\pmatrix22 a Up y");
assert!(ed.path.is_empty());
assert!(latex(&ed).starts_with('y'), "latex: {}", latex(&ed));
}
#[test]
fn free_cursor_mode_snaps_on_enter() {
let mut ed = Editor::new();
type_script(&mut ed, r"x + \frac 1 Down 2 Tab C-f Down Enter");
assert!(
matches!(ed.path.last(), Some((_, formulaa::ast::Field::FracDen))),
"path: {:?}",
ed.path
);
let mut ed = Editor::new();
type_script(&mut ed, r"a+b");
let before = (ed.path.clone(), ed.col);
type_script(&mut ed, "C-f Left Left Esc");
assert_eq!((ed.path.clone(), ed.col), before);
assert!(ed.free.is_none());
}
#[test]
fn free_cursor_auto_expands_collapsed_elements() {
let mut ed = Editor::new();
type_script(&mut ed, r"x ^ 2 Tab + \sum Tab C-f");
assert!(ed.ghost.is_empty());
type_script(&mut ed, "Left");
assert!(
!ed.ghost.is_empty(),
"approaching the bare ∑ must materialize its slots"
);
for _ in 0..4 {
type_script(&mut ed, "Left");
}
assert!(
ed.ghost
.iter()
.any(|p| matches!(p.last(), Some((_, formulaa::ast::Field::SupArg)))),
"ghosts: {:?}",
ed.ghost
);
type_script(&mut ed, "Enter");
assert!(ed.free.is_none());
assert!(ed.col <= ed.cur_row().len());
}
#[test]
fn click_moves_the_cursor() {
let mut ed = Editor::new();
type_script(&mut ed, "a+b");
ed.click(1, 0);
assert!(ed.path.is_empty());
assert_eq!(ed.col, 1);
let mut ed = Editor::new();
type_script(&mut ed, r"x + \frac 1 Down 22 Tab");
ed.click(4, 2);
assert!(
matches!(ed.path.last(), Some((_, formulaa::ast::Field::FracDen))),
"path: {:?}",
ed.path
);
}
#[test]
fn shift_up_selects_enclosing_structure() {
let mut ed = Editor::new();
type_script(&mut ed, r"x + \frac 1 Down 2 S-Up Backspace");
assert_eq!(latex(&ed), "x+");
}
#[test]
fn space_is_a_formatting_spacer() {
let mut ed = Editor::new();
type_script(&mut ed, "a Space b");
assert_eq!(aa(&ed), "𝑎 𝑏");
assert_eq!(latex(&ed), "ab"); }
#[test]
fn selection_does_not_survive_leaving_the_row() {
let mut ed = Editor::new();
type_script(&mut ed, r"\frac ab S-Left Tab ^ x");
assert_eq!(latex(&ed), "\\frac{ab}{}^{x}");
}
#[test]
fn a_symbol_replaces_the_selection() {
let mut ed = Editor::new();
type_script(&mut ed, r"a b S-Left S-Left");
assert_eq!(ed.selection(), Some((0, 2)));
type_script(&mut ed, r"\alpha");
assert_eq!(ed.selection(), None, "the selection was consumed");
assert_eq!(latex(&ed), "\\alpha ");
let mut ed = Editor::new();
type_script(&mut ed, r"abc S-Left S-Left x");
assert_eq!(latex(&ed), "ax");
let mut ed = Editor::new();
type_script(&mut ed, r"ab S-Left /");
assert_eq!(latex(&ed), "a/");
let mut ed = Editor::new();
type_script(&mut ed, r"a b S-Left S-Left \hat");
assert_eq!(latex(&ed), "\\widehat{ab}");
}
#[test]
fn content_inserts_replace_the_selection() {
let mut ed = Editor::new();
type_script(&mut ed, r"abc S-Left S-Left Space");
assert_eq!(latex(&ed), "a");
let mut ed = Editor::new();
type_script(&mut ed, r"ab S-Left \sin");
assert_eq!(latex(&ed), "a\\operatorname{sin}");
let mut ed = Editor::new();
type_script(&mut ed, r"ab S-Left \sum n Tab");
assert_eq!(latex(&ed), "a\\sum_{n}");
let mut ed = Editor::new();
type_script(&mut ed, r"ab S-Left \pmatrix22 x");
assert_eq!(latex(&ed), "a\\begin{pmatrix} x & \\\\ & \\end{pmatrix}");
let mut ed = Editor::new();
type_script(&mut ed, r"ab S-Left \rm d Enter");
assert_eq!(latex(&ed), "a\\mathrm{d}");
let mut ed = Editor::new();
type_script(&mut ed, r"ab S-Left Enter c");
assert_eq!(latex(&ed), "a \\\\ c");
}
#[test]
fn block_select_starts_with_the_slot_it_stands_in() {
let mut ed = Editor::new();
type_script(&mut ed, r"// a + b Down 2 Up C-b Enter");
assert_eq!(ed.selection(), Some((0, 3)), "not the whole numerator");
assert_eq!(latex(&ed), "\\frac{a+b}{2}", "the formula moved");
let mut ed = Editor::new();
type_script(&mut ed, r"// a + b Down 2 Up C-b Up Enter");
assert_eq!(ed.path, vec![], "the step out stayed inside");
assert_eq!(ed.selection(), Some((0, 1)), "not the fraction");
let mut ed = Editor::new();
type_script(&mut ed, r"// a Down 2 Up C-b Enter");
assert_eq!(
ed.path,
vec![(0, Field::FracNum)],
"not the numerator's own slot"
);
assert_eq!(ed.selection(), Some((0, 1)));
let mut ed = Editor::new();
type_script(&mut ed, r"// \sqrt x Down 2 Up");
let boxes = ed.block_targets();
assert_eq!(
boxes.len(),
3,
"a coincident slot/node pair was painted twice: {:?}",
boxes
);
}
#[test]
fn block_select_mode_selects_a_structure() {
let mut ed = Editor::new();
type_script(&mut ed, r"1 // 2 Down 3 C-b Up");
let (root, cursor) = ed.decorated();
assert!(cursor.is_some(), "cursor stays threaded during modes");
assert!(
root.iter().any(
|n| matches!(n, formulaa::ast::Node::Sym(c) if (0xE000..0xE0F0).contains(&(*c as u32)))
),
"block mark missing: {:?}",
root
);
type_script(&mut ed, "Enter");
assert_eq!(ed.selection(), Some((1, 2)));
type_script(&mut ed, "S-Left");
assert_eq!(ed.selection(), Some((0, 2)));
type_script(&mut ed, "S-Right");
assert_eq!(ed.selection(), Some((1, 2)), "shrinks back");
type_script(&mut ed, "Backspace");
assert_eq!(latex(&ed), "1");
let mut ed = Editor::new();
type_script(&mut ed, r"1 // 2 Down 3 C-b Up S-Left");
assert!(ed.block.is_none(), "mode exits");
assert_eq!(ed.selection(), Some((0, 2)));
let mut ed = Editor::new();
type_script(&mut ed, r"\pmatrix22 x");
let (path, col) = (ed.path.clone(), ed.col);
type_script(&mut ed, r"C-b");
assert_eq!(
ed.block.as_ref().map(Vec::len),
Some(3),
"cell + Array + Delim"
);
assert_eq!(ed.block_sel, 0, "the cell it stands in first");
type_script(&mut ed, "Up");
assert_eq!(ed.block_sel, 1);
type_script(&mut ed, "Down");
assert_eq!(ed.block_sel, 0);
type_script(&mut ed, "C-b");
assert!(ed.block.is_none());
assert_eq!((ed.path, ed.col), (path, col), "cursor untouched");
let mut ed = Editor::new();
type_script(&mut ed, r"\pmatrix22 x C-b Up Up Enter \sqrt");
assert_eq!(
latex(&ed),
"\\sqrt{\\begin{pmatrix} x & \\\\ & \\end{pmatrix}}"
);
let mut ed = Editor::new();
type_script(&mut ed, r"1 // 2 Down 3 C-b a");
assert!(ed.block.is_none());
}
#[test]
fn whole_selection_flip_then_plain_semantics() {
let mut ed = Editor::new();
type_script(&mut ed, r"1 // 2 Down 3 S-Up S-Left");
assert_eq!(ed.selection(), Some((0, 2)));
type_script(&mut ed, "S-Right");
assert_eq!(ed.selection(), Some((1, 2)), "shrinks back");
let mut ed = Editor::new();
type_script(&mut ed, r"abcde Left Left S-Left");
assert_eq!(ed.selection(), Some((2, 3)));
type_script(&mut ed, "S-Right");
assert_eq!(ed.selection(), None, "shrink collapses");
}
#[test]
fn mid_is_divides_outside_a_delimiter() {
let mut ed = Editor::new();
type_script(&mut ed, r"a \mid b");
assert_eq!(latex(&ed), "a\\mid b");
let mut ed = Editor::new();
type_script(&mut ed, r"a \!mid b");
assert_eq!(latex(&ed), "a\\nmid b");
let mut ed = Editor::new();
type_script(&mut ed, r"( x \mid P");
assert_eq!(latex(&ed), "\\left(x\\middle|P\\right)");
}
#[test]
fn bang_toggles_the_preceding_symbol() {
let mut ed = Editor::new();
type_script(&mut ed, r"a = \! b");
assert_eq!(latex(&ed), "a\\ne b");
let mut ed = Editor::new();
type_script(&mut ed, r"x \in \!");
assert_eq!(latex(&ed), "x\\notin ");
type_script(&mut ed, r"\!");
assert_eq!(latex(&ed), "x\\in ");
type_script(&mut ed, r"\!");
assert_eq!(latex(&ed), "x\\notin ");
let mut ed = Editor::new();
type_script(&mut ed, r"a \ne \! b");
assert_eq!(latex(&ed), "a=b");
let mut ed = Editor::new();
type_script(&mut ed, r"a \!");
assert!(ed.message_error);
let mut ed = Editor::new();
type_script(&mut ed, r"\!");
assert!(ed.message_error);
}
#[test]
fn bang_spellings_negate_relations() {
let mut ed = Editor::new();
type_script(&mut ed, r"a \!= b");
assert_eq!(latex(&ed), "a\\ne b");
let mut ed = Editor::new();
type_script(&mut ed, r"a \=! b");
assert_eq!(latex(&ed), "a\\ne b");
let mut ed = Editor::new();
type_script(&mut ed, r"a \!in B");
assert_eq!(latex(&ed), "a\\notin B");
let mut ed = Editor::new();
type_script(&mut ed, r"a \!le b");
assert_eq!(latex(&ed), "a\\nleq b");
let mut ed = Editor::new();
type_script(&mut ed, r"a \subset! b");
assert_eq!(latex(&ed), "a\\not\\subset b");
}
#[test]
fn rm_of_a_digit_is_just_the_digit() {
let mut ed = Editor::new();
type_script(&mut ed, r"\rm1 Tab x");
assert_roundtrip(&ed, &[]);
assert_eq!(latex(&ed), "1x");
}
#[test]
fn a_line_break_stays_out_of_insets() {
let mut ed = Editor::new();
type_script(&mut ed, r"a Enter b S-Left S-Left S-Left");
assert_eq!(ed.selection(), Some((2, 3)), "selection stops at the break");
type_script(&mut ed, "^");
assert_eq!(latex(&ed), "a \\\\ ^{b}");
assert_roundtrip(&ed, &[]);
let mut ed = Editor::new();
type_script(&mut ed, r"a Enter b S-Up \vec");
assert_roundtrip(&ed, &[]);
assert!(
!latex(&ed).contains("\\vec{a \\\\ b}"),
"no break in the base: {}",
latex(&ed)
);
let mut ed = Editor::new();
type_script(&mut ed, r"\norm \mid");
assert_eq!(latex(&ed), "\\left\\|\\mid \\right\\|");
let mut ed = Editor::new();
type_script(&mut ed, r"a Enter b Left Left S-Right");
assert_eq!(ed.selection(), None, "and stops before it too");
}
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x
}
fn pick<'a, T>(&mut self, xs: &'a [T]) -> &'a T {
&xs[(self.next() % xs.len() as u64) as usize]
}
}
fn assert_roundtrip(ed: &Editor, history: &[String]) {
let (droot, cursor) = ed.decorated();
if let Some((p, c)) = cursor {
let b = render_root(&droot, Some((&p[..], c)), &RenderCtx::canonical());
assert!(
b.caret.is_some(),
"caret lost\n--- keys ---\n{}",
history.join(" ")
);
}
let row = normalize(&ed.root);
if row.is_empty() {
return;
}
let aa = render_root(&row, None, &RenderCtx::canonical()).to_text();
let expected = normalize(&formulaa::render::absorb_spacers(&row));
let parsed = parse(&aa).unwrap_or_else(|e| {
panic!(
"parse failed: {}\n--- AA ---\n{}\n--- keys ---\n{}",
e,
aa,
history.join(" ")
)
});
assert_eq!(
parsed,
expected,
"AST mismatch\n--- AA ---\n{}\n--- keys ---\n{}",
aa,
history.join(" ")
);
}
#[test]
fn property_random_key_sequences_roundtrip() {
let n: usize = std::env::var("FORMULAA_UI_PROP_N")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(200);
let seed: u64 = std::env::var("FORMULAA_UI_PROP_SEED")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(0xDEC0DE);
let mut rng = Rng(seed);
let chars: Vec<char> = "abxyn12+=-*/.,<>|'~αβ∑∫()^_{}[]\\\" ".chars().collect();
let named_pool = [
Key::Left,
Key::Right,
Key::Up,
Key::Down,
Key::Home,
Key::End,
Key::Tab,
Key::Enter,
Key::Backspace,
Key::Delete,
Key::Esc,
];
for _ in 0..n {
let mut ed = Editor::new();
let mut history: Vec<String> = Vec::new();
for _ in 0..60 {
let r = rng.next() % 100;
let (key, shift, ctrl) = if r < 55 {
(Key::Char(*rng.pick(&chars)), false, false)
} else if r < 75 {
(*rng.pick(&named_pool), false, false)
} else if r < 85 {
(*rng.pick(&[Key::Left, Key::Right]), true, false)
} else if r < 95 {
(
Key::Char(
*rng.pick(&['t', 'b', 'e', 'y', 's', 'z', 'r', 'c', 'x', 'v', 'f', 'a']),
),
false,
true,
)
} else if r < 98 {
(Key::Char(*rng.pick(&chars)), true, false)
} else {
let (x, y) = ((rng.next() % 24) as usize, (rng.next() % 8) as usize);
history.push(format!("Click({},{})", x, y));
ed.click(x, y);
assert_roundtrip(&ed, &history);
continue;
};
history.push(format!(
"{}{}{:?}",
if ctrl { "C-" } else { "" },
if shift { "S-" } else { "" },
key
));
let _ = ed.input(key, shift, ctrl);
assert_roundtrip(&ed, &history);
}
}
}
#[test]
fn backspace_unwraps_a_delimiter() {
let open = |ed: &mut Editor| {
type_script(ed, "( foo Home");
};
let mut ed = Editor::new();
open(&mut ed);
assert_eq!(latex(&ed), "\\left(foo\\right)");
type_script(&mut ed, "Backspace");
assert_eq!(latex(&ed), "\\left(foo\\right)", "the first press deletes");
type_script(&mut ed, "Backspace");
assert_eq!(latex(&ed), "foo");
assert_eq!(ed.selection(), Some((0, 3)), "the contents are selected");
type_script(&mut ed, "Backspace");
assert_eq!(latex(&ed), "");
let mut ed = Editor::new();
open(&mut ed);
type_script(&mut ed, "Backspace Backspace Right");
assert_eq!(latex(&ed), "foo");
assert_eq!(ed.selection(), None);
let mut ed = Editor::new();
open(&mut ed);
type_script(&mut ed, "Backspace Right Left Backspace");
assert_eq!(latex(&ed), "\\left(foo\\right)");
}
#[test]
fn ctrl_d_deletes_forward_and_unwraps() {
let mut ed = Editor::new();
type_script(&mut ed, "abc Home C-d");
assert_eq!(latex(&ed), "bc", "^D deletes the character ahead");
let mut ed = Editor::new();
type_script(&mut ed, "( foo");
type_script(&mut ed, "C-d");
assert_eq!(latex(&ed), "\\left(foo\\right)", "the first press deletes");
type_script(&mut ed, "C-d");
assert_eq!(latex(&ed), "foo");
assert_eq!(ed.selection(), Some((0, 3)));
}
#[test]
fn shift_selecting_a_bracket_arms_the_pair() {
let mut ed = Editor::new();
type_script(&mut ed, "( foo ) Home S-Right");
assert_eq!(latex(&ed), "\\left(foo\\right)");
assert_eq!(ed.selection(), None, "arming made a selection");
type_script(&mut ed, "Backspace");
assert_eq!(latex(&ed), "foo");
assert_eq!(ed.selection(), None, "the shift unwrap selected something");
assert_eq!(ed.col, 0, "the cursor left its side");
let mut ed = Editor::new();
type_script(&mut ed, "( foo ) S-Left Delete");
assert_eq!(latex(&ed), "foo");
assert_eq!((ed.selection(), ed.col), (None, 3));
let mut ed = Editor::new();
type_script(&mut ed, "( foo ) Home S-Right");
let (root, _) = ed.decorated();
let has_mark = root.iter().any(|n| {
matches!(n, formulaa::ast::Node::Sym(c)
if formulaa::glyphs::Mark::decode(*c) == Some(formulaa::glyphs::Mark::Delims { open: true }))
});
assert!(has_mark, "no armed marks in {:?}", root);
let mut ed = Editor::new();
type_script(&mut ed, "( foo ) Home S-Right S-Right");
assert_eq!(ed.selection(), Some((0, 1)));
type_script(&mut ed, "Backspace");
assert_eq!(latex(&ed), "");
let mut ed = Editor::new();
type_script(&mut ed, "x ( foo ) Home S-Right S-Right Backspace");
assert_eq!(latex(&ed), "");
let mut ed = Editor::new();
type_script(&mut ed, r"\lr(|) a ) Home S-Right");
assert_eq!(ed.selection(), Some((0, 1)));
let mut ed = Editor::new();
type_script(&mut ed, "( foo Home S-Left Backspace");
assert_eq!(latex(&ed), "foo");
assert_eq!(ed.selection(), Some((0, 3)));
let mut ed = Editor::new();
type_script(&mut ed, "( foo S-Right Backspace");
assert_eq!(latex(&ed), "foo");
}
#[test]
fn a_radical_unwraps_like_a_pair() {
let mut ed = Editor::new();
type_script(&mut ed, r"\sqrt foo Home Backspace");
assert_eq!(latex(&ed), "\\sqrt{foo}", "the first press deletes");
type_script(&mut ed, "Backspace");
assert_eq!(latex(&ed), "foo");
assert_eq!(ed.selection(), Some((0, 3)));
let mut ed = Editor::new();
type_script(&mut ed, r"\sqrt foo Right S-Left Backspace");
assert_eq!(latex(&ed), "foo");
let mut ed = Editor::new();
type_script(&mut ed, r"\sqrt foo C-d C-d");
assert_eq!(latex(&ed), "\\sqrt{foo}");
let mut ed = Editor::new();
type_script(&mut ed, r"\sqrt foo S-Right Backspace");
assert_eq!(latex(&ed), "\\sqrt{fo}");
}
#[test]
fn norm_unwraps_like_a_delimiter() {
let mut ed = Editor::new();
type_script(&mut ed, r"\norm x Home Backspace Backspace");
assert_eq!(latex(&ed), "x");
let mut ed = Editor::new();
type_script(&mut ed, r"\norm x C-d C-d");
assert_eq!(latex(&ed), "x");
let mut ed = Editor::new();
type_script(&mut ed, r"\norm x Right S-Left Backspace");
assert_eq!(latex(&ed), "x");
}
#[test]
fn unwrap_leaves_middles_and_grids_alone() {
let mut ed = Editor::new();
type_script(&mut ed, r"\set x Home");
assert_eq!(ed.col, 0, "cursor is against the opening brace");
assert!(
matches!(ed.path.last(), Some((_, formulaa::ast::Field::Seg(0)))),
"cursor is in the delimiter's segment: {:?}",
ed.path
);
type_script(&mut ed, "Backspace Backspace");
assert!(
latex(&ed).contains("\\middle|"),
"the set-builder bar survived: {}",
latex(&ed)
);
let mut ed = Editor::new();
type_script(&mut ed, r"\pmatrix22 x Tab Home");
assert!(
matches!(ed.path.last(), Some((_, formulaa::ast::Field::Seg(0)))),
"cursor is in the delimiter's segment: {:?}",
ed.path
);
type_script(&mut ed, "Backspace Backspace");
assert!(
latex(&ed).contains("pmatrix"),
"the matrix survived: {}",
latex(&ed)
);
let mut ed = Editor::new();
type_script(&mut ed, "x ( Backspace");
assert_eq!(latex(&ed), "x", "an empty pair needs one Backspace");
}
#[test]
fn tab_completion_picks_a_row() {
let mut ed = Editor::new();
type_script(&mut ed, r"\ a l Down Enter");
assert_eq!(latex(&ed), "\\alpha ");
assert!(ed.completion.is_none(), "the popup outlived the pick");
assert!(ed.minibuffer.is_none());
let mut ed = Editor::new();
type_script(&mut ed, r"\ a l Down");
let second = ed.completion.as_ref().unwrap().items[1]
.commit()
.unwrap()
.to_string();
type_script(&mut ed, "Down Enter");
let mut expected = Editor::new();
expected.execute(&second);
assert_eq!(latex(&ed), latex(&expected), "picked \\{}", second);
let mut ed = Editor::new();
type_script(&mut ed, r"\ a l Down Up");
let list = ed.completion.as_ref().unwrap();
assert_eq!(list.sel, list.items.len() - 1);
}
#[test]
fn tab_completion_follows_the_query() {
let mut ed = Editor::new();
type_script(&mut ed, r"\ a l Down");
assert_eq!(
ed.completion.as_ref().unwrap().items[0].symbol,
"α",
"the α row leads \\al"
);
type_script(&mut ed, "e p h");
let list = ed.completion.as_ref().expect("the popup stayed open");
assert_eq!(list.items[0].commit(), Some("aleph"));
type_script(&mut ed, "Backspace Backspace Backspace");
assert_eq!(ed.completion.as_ref().unwrap().items[0].symbol, "α");
type_script(&mut ed, "Esc");
assert!(ed.completion.is_none());
assert_eq!(ed.minibuffer.as_deref(), Some("al"));
type_script(&mut ed, "Esc");
assert!(ed.minibuffer.is_none());
}
#[test]
fn tab_completion_with_no_matches_stays_quiet() {
let mut ed = Editor::new();
type_script(&mut ed, r"\ q q z z Tab");
assert!(ed.completion.is_none());
assert!(ed.message.is_empty(), "{:?}", ed.message);
assert_eq!(ed.minibuffer.as_deref(), Some("qqzz"));
}
#[test]
fn block_select_paints_only_a_step_in_each_direction() {
use formulaa::glyphs::Mark;
let mut ed = Editor::new();
type_script(&mut ed, r"( 1 // \pmatrix22 x");
type_script(&mut ed, "C-b");
let depth = ed.block.as_ref().map(Vec::len).unwrap_or(0);
assert!(depth >= 4, "expected a deep chain, got {}", depth);
let painted = |ed: &Editor| -> Vec<usize> {
let (root, _) = ed.decorated();
fn walk(row: &formulaa::ast::Row, out: &mut Vec<usize>) {
for n in row {
if let formulaa::ast::Node::Sym(c) = n
&& let Some(Mark::BlockOpen { rank }) = Mark::decode(*c)
{
out.push(rank);
}
for f in n.fields() {
walk(n.field(f), out);
}
}
}
let mut out = Vec::new();
walk(&root, &mut out);
out.sort_unstable();
out
};
assert_eq!(painted(&ed), vec![0, 1]);
type_script(&mut ed, "Up");
assert_eq!(painted(&ed), vec![1, 2]);
type_script(&mut ed, "Up");
assert_eq!(painted(&ed), vec![2, 3]);
for _ in 0..depth {
type_script(&mut ed, "Up");
}
assert_eq!(painted(&ed), vec![depth - 1]);
}
#[test]
fn arming_does_not_survive_undo_or_a_click() {
let mut ed = Editor::new();
type_script(&mut ed, "( a Home x Home Backspace C-z Backspace");
assert!(
latex(&ed).contains("\\left("),
"undo left the pair armed, so one Backspace unwrapped it: {}",
latex(&ed)
);
let lit = |ed: &Editor| {
use formulaa::glyphs::Mark;
fn walk(row: &formulaa::ast::Row, out: &mut bool) {
for n in row {
if let formulaa::ast::Node::Sym(c) = n
&& matches!(Mark::decode(*c), Some(Mark::Delims { .. }))
{
*out = true;
}
for f in n.fields() {
walk(n.field(f), out);
}
}
}
let (root, _) = ed.decorated();
let mut found = false;
walk(&root, &mut found);
found
};
let mut ed = Editor::new();
type_script(&mut ed, "( foo Home Backspace");
let armed_path = ed.path.clone();
assert!(lit(&ed), "the pair should be lit after arming");
ed.click(3, 0); assert_eq!(ed.path, armed_path, "the click stayed in the segment");
assert_ne!(ed.col, 0, "…but moved off the armed column");
assert!(!lit(&ed), "a click left the pair lit up as armed");
}
#[test]
fn a_click_closes_the_completion_popup() {
let mut ed = Editor::new();
type_script(&mut ed, r"\ a l Down");
assert!(ed.completion.is_some());
ed.click(0, 0);
assert!(ed.completion.is_none(), "the popup outlived the click");
type_script(&mut ed, r"\ Enter");
assert_eq!(latex(&ed), "");
}
#[test]
fn the_popup_tracks_the_query_and_both_commit_keys_take_it() {
let mut ed = Editor::new();
type_script(&mut ed, r"\ a l Down Down");
let picked = ed
.completion
.as_ref()
.unwrap()
.selected()
.and_then(|i| i.commit())
.unwrap()
.to_string();
type_script(&mut ed, "Space");
let mut expected = Editor::new();
expected.execute(&picked);
assert_eq!(latex(&ed), latex(&expected), "Space took \\{}", picked);
let mut ed = Editor::new();
type_script(&mut ed, r"\ a l Down");
assert!(!ed.completion.as_ref().unwrap().items.is_empty());
type_script(&mut ed, "q q z");
assert!(
ed.completion.as_ref().is_some_and(|l| l.items.is_empty()),
"the popup closed on a non-matching query"
);
type_script(&mut ed, "Backspace Backspace Backspace");
assert!(
ed.completion.as_ref().is_some_and(|l| !l.items.is_empty()),
"backspacing did not bring the list back"
);
let mut ed = Editor::new();
type_script(&mut ed, r"\ a l Down q q z Enter");
assert!(ed.message.contains("is not a command"), "{:?}", ed.message);
}
#[test]
fn tab_finishes_and_the_arrows_browse() {
let mut ed = Editor::new();
type_script(&mut ed, r"\ a l p h a Tab");
assert_eq!(latex(&ed), "\\alpha ");
assert!(ed.minibuffer.is_none() && ed.completion.is_none());
let mut ed = Editor::new();
type_script(&mut ed, r"\ a l Tab");
assert_eq!(latex(&ed), "\\alpha ");
let mut ed = Editor::new();
type_script(&mut ed, r"\ x y z z Tab");
assert_eq!(latex(&ed), "", "Tab executed a non-command");
assert!(ed.minibuffer.is_some(), "the typing was lost");
let mut ed = Editor::new();
type_script(&mut ed, r"\ a l Down");
let first = ed.completion.as_ref().unwrap();
assert_eq!(first.sel, 0, "the revealing press skipped a row");
type_script(&mut ed, "Down");
let second = ed
.completion
.as_ref()
.unwrap()
.selected()
.and_then(|i| i.commit())
.unwrap()
.to_string();
type_script(&mut ed, "Tab");
let mut expected = Editor::new();
expected.execute(&second);
assert_eq!(latex(&ed), latex(&expected), "Tab took \\{}", second);
}
#[test]
fn a_stalled_step_closes_the_list() {
let mut ed = Editor::new();
type_script(&mut ed, r"\ f r a Down Down Enter");
assert_eq!(
ed.minibuffer.as_deref(),
Some("frak"),
"the step stalled early"
);
assert!(ed.completion.is_some(), "the list closed with a step taken");
type_script(&mut ed, "Enter");
assert_eq!(ed.minibuffer.as_deref(), Some("frak"));
assert!(
ed.completion.is_none(),
"the list stayed up with nothing to add"
);
type_script(&mut ed, "a Enter");
assert_eq!(latex(&ed), "\\mathfrak{a}");
}
#[test]
fn shape_rows_extend_the_spelling() {
let mut ed = Editor::new();
type_script(&mut ed, r"\ l r Down");
let list = ed.completion.as_ref().expect("the spec tokens are listed");
let spec: Vec<_> = list
.items
.iter()
.filter(|i| i.names.starts_with("lr"))
.collect();
assert!(spec.len() >= 8, "{:?}", list.items);
assert!(spec.iter().all(|i| i.is_step()), "{:?}", spec);
assert!(list.selected().is_none(), "a step row was committable");
let token = ed
.completion
.as_ref()
.unwrap()
.highlighted()
.and_then(|i| i.step_to())
.map(str::to_string);
type_script(&mut ed, "Enter");
assert_eq!(ed.minibuffer, token, "the token was not written");
assert!(latex(&ed).is_empty(), "a step row executed something");
assert!(
ed.completion.as_ref().is_some_and(|l| !l.items.is_empty()),
"the list did not ask for the rest"
);
let mut ed = Editor::new();
type_script(&mut ed, r"\ f r a k Down");
let first = ed.completion.as_ref().unwrap().sel;
type_script(&mut ed, "Down");
assert_ne!(
ed.completion.as_ref().unwrap().sel,
first,
"the list did not move"
);
let mut ed = Editor::new();
type_script(&mut ed, r"\ f r Down");
while ed
.completion
.as_ref()
.and_then(|l| l.highlighted())
.is_some_and(|i| !i.names.starts_with("frak{"))
{
type_script(&mut ed, "Down");
}
type_script(&mut ed, "Enter");
assert_eq!(ed.minibuffer.as_deref(), Some("frak"));
type_script(&mut ed, "A Enter");
assert_eq!(aa(&ed), "𝔄");
}
#[test]
fn delimiter_names_are_spec_tokens() {
for cmd in ["lparen", "lbrack", "lceil", "lfloor", "rparen", "rceil"] {
let mut ed = Editor::new();
ed.execute(cmd);
assert!(
ed.message.contains("is not a command"),
"\\{}: {:?}",
cmd,
ed.message
);
assert!(aa(&ed).is_empty(), "\\{} inserted something", cmd);
}
let mut ed = Editor::new();
ed.execute("mid");
assert_eq!(aa(&ed), "∣", "\\mid is the divides atom outside a pair");
let mut ed = Editor::new();
type_script(&mut ed, r"\lr\lceil\rfloor x");
assert_eq!(aa(&ed), "⌈𝑥⌋");
let mut ed = Editor::new();
type_script(&mut ed, r"\lr");
assert!(aa(&ed).is_empty(), "bare \\lr inserted {}", aa(&ed));
}
#[test]
fn tab_does_not_commit_a_half_written_spec() {
for q in [r"\ l r", r"\ d e l i m"] {
let mut ed = Editor::new();
type_script(&mut ed, &format!("{} Tab", q));
assert!(ed.message.is_empty(), "{:?} -> {:?}", q, ed.message);
assert!(ed.minibuffer.is_some(), "{:?} closed the minibuffer", q);
assert!(
ed.completion.as_ref().is_some_and(|l| !l.items.is_empty()),
"{:?} offered nothing",
q
);
}
let mut ed = Editor::new();
type_script(&mut ed, r"\ a l p h a Tab");
assert_eq!(latex(&ed), "\\alpha ");
}
#[test]
fn container_commands_wrap_the_selection() {
let mut ed = Editor::new();
type_script(&mut ed, r"ab S-Left S-Left \norm");
assert_eq!(latex(&ed), "\\left\\|ab\\right\\|");
let mut ed = Editor::new();
type_script(&mut ed, r"ab S-Left S-Left \abs");
assert_eq!(latex(&ed), "\\left|ab\\right|");
let mut ed = Editor::new();
type_script(&mut ed, r"ab S-Left S-Left \lr(]");
assert_eq!(latex(&ed), "\\left(ab\\right]");
let mut ed = Editor::new();
type_script(&mut ed, r"a S-Left \braket b");
assert_eq!(latex(&ed), "\\left\\langle a\\middle|b\\right\\rangle ");
let mut ed = Editor::new();
type_script(&mut ed, r"a S-Left \set b");
assert_eq!(latex(&ed), "\\left\\{a\\middle|b\\right\\}");
}
#[test]
fn mode_commands_run_from_the_minibuffer() {
let mut ed = Editor::new();
type_script(&mut ed, r"x \free");
assert!(ed.free.is_some(), "free mode did not start");
let mut ed = Editor::new();
type_script(&mut ed, r"x \f");
assert!(ed.free.is_some());
let mut ed = Editor::new();
type_script(&mut ed, r"x ^ 2 \b");
assert!(ed.block.is_some(), "block select did not start");
let mut ed = Editor::new();
type_script(&mut ed, r"\pmatrix22 x \g");
assert!(ed.grid.is_some(), "grid mode did not start");
let mut ed = Editor::new();
let fx = type_script(&mut ed, r"ab \clipboard");
assert!(fx.contains(&Effect::CopyAa), "{:?}", fx);
let mut ed = Editor::new();
let fx = type_script(&mut ed, r"ab \c");
assert!(!fx.contains(&Effect::CopyAa), "\\c still copies");
let mut ed = Editor::new();
let fx = type_script(&mut ed, r"ab \write");
assert!(fx.contains(&Effect::Write), "{:?}", fx);
let mut ed = Editor::new();
let fx = type_script(&mut ed, r"ab \wq");
assert!(fx.contains(&Effect::WriteQuit), "{:?}", fx);
let mut ed = Editor::new();
let fx = type_script(&mut ed, r"\quit");
assert!(fx.contains(&Effect::Quit), "{:?}", fx);
let mut ed = Editor::new();
let fx = type_script(&mut ed, r"\q");
assert!(!fx.contains(&Effect::Quit), "\\q still quits");
let mut ed = Editor::new();
type_script(&mut ed, r"x \ f Tab");
assert!(ed.free.is_none(), "Tab ran a mode command");
type_script(&mut ed, r"Tab Tab");
assert!(ed.free.is_none(), "Tab took the mode row");
type_script(&mut ed, r"Enter");
assert!(ed.free.is_some(), "Enter did not run it");
let mut ed = Editor::new();
type_script(&mut ed, r"\tau");
assert_eq!(latex(&ed), "\\tau ");
let mut ed = Editor::new();
type_script(&mut ed, r"\ta");
assert_eq!(latex(&ed), "\\tau ");
}
#[test]
fn grid_mode_exits_on_backslash_and_enter_keeps_the_cell() {
let mut ed = Editor::new();
type_script(&mut ed, r"\pmatrix22 ab C-g");
assert!(ed.grid.is_some());
type_script(&mut ed, r"\");
assert!(ed.grid.is_none(), "backslash did not leave grid mode");
assert!(
ed.minibuffer.is_none(),
"the leaving key opened the minibuffer"
);
let mut ed = Editor::new();
type_script(&mut ed, r"\pmatrix22 ab C-g Enter");
assert!(ed.grid.is_none());
assert_eq!(ed.selection(), Some((0, 2)), "the cell is not selected");
type_script(&mut ed, r"\norm");
assert!(latex(&ed).contains("\\|ab"), "{}", latex(&ed));
let mut ed = Editor::new();
type_script(&mut ed, r"\pmatrix22 ab C-g S-Right Enter");
assert!(ed.grid.is_none());
assert_eq!(ed.selection(), None);
}
#[test]
fn clicking_a_completion_row_accepts_it() {
let mut ed = Editor::new();
type_script(&mut ed, r"\ a l Down");
let second = ed.completion.as_ref().unwrap().items[1]
.commit()
.unwrap()
.to_string();
ed.completion_click(1);
let mut expected = Editor::new();
expected.execute(&second);
assert_eq!(latex(&ed), latex(&expected), "clicked \\{}", second);
assert!(ed.completion.is_none() && ed.minibuffer.is_none());
let mut ed = Editor::new();
type_script(&mut ed, r"\ l r Down");
ed.completion_click(0);
assert!(ed.minibuffer.as_deref().is_some_and(|m| m.len() > 2));
assert!(latex(&ed).is_empty(), "a step row executed something");
let mut ed = Editor::new();
type_script(&mut ed, r"\ a l Down");
ed.completion_click(99);
assert!(ed.completion.is_some() && latex(&ed).is_empty());
}
#[test]
fn block_select_reaches_the_whole_formula() {
let mut ed = Editor::new();
type_script(&mut ed, "x+y C-b");
assert!(ed.block.is_some());
type_script(&mut ed, "Enter");
assert_eq!(ed.selection(), Some((0, 3)));
let mut ed = Editor::new();
type_script(&mut ed, "x+ ( y C-b Up Up Up Enter");
assert_eq!(ed.selection(), Some((0, 3)));
let mut ed = Editor::new();
type_script(&mut ed, "( x C-b");
assert_eq!(ed.block.as_ref().map(Vec::len), Some(2));
let mut ed = Editor::new();
type_script(&mut ed, "C-b");
assert!(ed.block.is_none() && ed.message.is_empty());
}
#[test]
fn backspace_peels_accents_before_the_base() {
let mut ed = Editor::new();
type_script(&mut ed, r"a \hat \vec");
assert_eq!(latex(&ed), "\\vec{\\hat{a}}");
type_script(&mut ed, "Backspace");
assert_eq!(latex(&ed), "\\hat{a}", "the outermost mark peels first");
type_script(&mut ed, "Backspace");
assert_eq!(latex(&ed), "a", "the last mark leaves a bare atom");
type_script(&mut ed, "Backspace");
assert_eq!(latex(&ed), "");
let mut ed = Editor::new();
type_script(&mut ed, r"a \underline \hat Backspace");
assert_eq!(latex(&ed), "\\underline{a}");
}
#[test]
fn wide_accents_edit_and_unwrap() {
let mut ed = Editor::new();
type_script(&mut ed, r"ab S-Left S-Left \hat");
assert_eq!(latex(&ed), "\\widehat{ab}");
type_script(&mut ed, "Left c");
assert_eq!(latex(&ed), "\\widehat{abc}");
type_script(&mut ed, "Home Backspace");
assert_eq!(latex(&ed), "\\widehat{abc}", "the first press deletes");
type_script(&mut ed, "Backspace");
assert_eq!(latex(&ed), "abc");
assert_eq!(ed.selection(), Some((0, 3)));
let mut ed = Editor::new();
type_script(&mut ed, r"ab S-Left S-Left \hat Backspace");
assert_eq!(latex(&ed), "\\widehat{ab}", "the first press deletes");
assert_eq!(ed.selection(), Some((0, 1)), "the accent is selected whole");
type_script(&mut ed, "Backspace");
assert_eq!(latex(&ed), "");
}
#[test]
fn an_armed_mid_deletes_and_merges() {
let mut ed = Editor::new();
type_script(&mut ed, r"\braket a Right b");
assert_eq!(latex(&ed), "\\left\\langle a\\middle|b\\right\\rangle ");
type_script(&mut ed, "Home S-Left Backspace");
assert_eq!(latex(&ed), "\\left\\langle ab\\right\\rangle ");
let mut ed = Editor::new();
type_script(&mut ed, r"\braket a Right b Left Left S-Right Delete");
assert_eq!(latex(&ed), "\\left\\langle ab\\right\\rangle ");
let mut ed = Editor::new();
type_script(&mut ed, r"\braket a Right b Home S-Left End Backspace");
assert_eq!(
latex(&ed),
"\\left\\langle a\\middle|\\right\\rangle ",
"a disarmed Backspace still merged"
);
}
#[test]
fn divides_is_the_atom_everywhere() {
let mut ed = Editor::new();
type_script(&mut ed, r"a \divides b");
assert_eq!(latex(&ed), "a\\mid b");
let mut ed = Editor::new();
type_script(&mut ed, r"( x \divides y");
assert_eq!(latex(&ed), "\\left(x\\mid y\\right)");
}
#[test]
fn whole_formula_ring_encloses_everything() {
use formulaa::ast::Node;
use formulaa::glyphs::Mark;
let decode = |row: &[Node]| -> Vec<String> {
row.iter()
.map(|n| match n {
Node::Sym(c) => match Mark::decode(*c) {
Some(Mark::BlockOpen { rank }) => format!("open{rank}"),
Some(Mark::BlockClose) => "close".into(),
_ => "atom".into(),
},
_ => "node".into(),
})
.collect()
};
let mut ed = Editor::new();
type_script(&mut ed, r"a Space 1 // 2 Tab Space a");
type_script(&mut ed, "Left Left Left C-b");
assert!(ed.block.is_some());
let (root, _) = ed.decorated();
let seq = decode(&root);
assert_eq!(
seq.last().map(String::as_str),
Some("close"),
"the ring does not reach the end: {seq:?}"
);
assert_eq!(seq.first().map(String::as_str), Some("open1"), "{seq:?}");
let mut ed = Editor::new();
type_script(
&mut ed,
r"1 // 2 Tab Space a Space a Left Left Left Left Left C-b",
);
assert!(ed.block.is_some());
let (root, _) = ed.decorated();
let seq = decode(&root);
assert_eq!(seq.first().map(String::as_str), Some("open1"), "{seq:?}");
assert!(seq.iter().any(|s| s == "open0"), "{seq:?}");
assert_eq!(seq.last().map(String::as_str), Some("close"), "{seq:?}");
let closes: Vec<usize> = seq
.iter()
.enumerate()
.filter(|(_, s)| *s == "close")
.map(|(i, _)| i)
.collect();
assert_eq!(closes.len(), 2, "{seq:?}");
assert!(closes[0] < seq.len() - 1, "the boxes collapsed: {seq:?}");
}
#[test]
fn armed_mid_one_shot_covers_every_exit() {
let mut ed = Editor::new();
type_script(&mut ed, r"\braket a Right b Home S-Left C-d");
assert_eq!(latex(&ed), "\\left\\langle ab\\right\\rangle ");
let mut ed = Editor::new();
type_script(&mut ed, r"\braket a Right b Home S-Left");
ed.click(0, 0);
type_script(&mut ed, "Backspace");
assert!(
latex(&ed).contains("middle"),
"a stale armed mid merged after a click: {}",
latex(&ed)
);
let mut ed = Editor::new();
type_script(&mut ed, r"\braket a Right b Home S-Left C-z Backspace");
assert!(
latex(&ed).contains("middle"),
"a stale armed mid merged after undo: {}",
latex(&ed)
);
}
#[test]
fn chords_do_not_type_into_the_minibuffer() {
let mut ed = Editor::new();
type_script(&mut ed, r"\ a C-v C-z C-y");
assert_eq!(ed.minibuffer.as_deref(), Some("a"));
}
#[test]
fn click_commit_gets_its_own_undo_step() {
let mut ed = Editor::new();
type_script(&mut ed, r"abc \text h i");
ed.click(0, 0); assert!(latex(&ed).contains("text"), "{}", latex(&ed));
type_script(&mut ed, "C-z");
assert_eq!(latex(&ed), "abc", "undo fused the commit with older edits");
}
#[test]
fn grid_commit_selects_the_cell_not_the_inner_row() {
let mut ed = Editor::new();
type_script(&mut ed, r"\bmatrix22 1 // 2 C-g Enter x");
let tex = latex(&ed);
assert!(!tex.contains("frac"), "the inner row was selected: {tex}");
assert!(tex.contains('x'), "{tex}");
}
#[test]
fn grid_copy_blips_and_clear_does_not() {
let mut ed = Editor::new();
type_script(&mut ed, r"\bmatrix22 a C-g C-c");
assert!(ed.copy_flash, "a cell copy got no acknowledgement");
let mut ed = Editor::new();
type_script(&mut ed, r"\bmatrix22 a C-g Backspace");
assert!(!ed.copy_flash, "a destructive clear blipped as a copy");
}
#[test]
fn a_stale_text_escape_does_not_leak_into_the_next_box() {
let mut ed = Editor::new();
type_script(&mut ed, r"\text a");
ed.input(Key::Char('\\'), false, false); ed.click(30, 0); type_script(&mut ed, r"\text");
ed.input(Key::Char('"'), false, false); assert!(
ed.op_entry.is_none(),
"the stale escape swallowed the closing quote"
);
}