use mathtex_editor_core::{CaretPath, Editor, Edge, MathClass, Outcome, Side};
use crate::*;
fn input(key: &str) -> KeyInput {
KeyInput { key: key.into(), shift: false, ctrl: false, alt: false, meta: false }
}
fn atom(latex: &str) -> Command {
Command::InsertAtom(Symbol::from_latex(latex))
}
struct Host {
ed: Editor,
km: Keymap,
outcomes: Vec<Outcome>,
}
impl Host {
fn new() -> Self {
Self { ed: Editor::new(), km: Keymap::new(), outcomes: Vec::new() }
}
fn apply(&mut self, cmds: Vec<Command>) {
for cmd in cmds {
let o = self.ed.exec(cmd);
self.outcomes.push(o);
}
}
fn press(&mut self, key: KeyInput) {
let ctx = self.ed.input_context();
let cmds = self.km.map_key(&key, &ctx);
self.apply(cmds);
}
fn keys(&mut self, keys: &str) -> &mut Self {
for c in keys.chars() {
self.press(input(&c.to_string()));
}
self
}
fn key(&mut self, key: &str) -> &mut Self {
self.press(input(key));
self
}
fn text(&mut self, text: &str) -> &mut Self {
let ctx = self.ed.input_context();
let cmds = self.km.map_text(text, &ctx);
self.apply(cmds);
self
}
fn host(&mut self, cmd: Command) -> &mut Self {
let _ = self.ed.exec(cmd);
self
}
fn tex(&self) -> String {
self.ed.document().to_tex()
}
fn closed(&self) -> bool {
self.outcomes.iter().any(|o| o.close)
}
}
fn typed(keys: &str) -> String {
Host::new().keys(keys).tex()
}
fn commands(km: &mut Keymap, keys: &[&str]) -> Vec<Vec<Command>> {
let mut ctx = InputContext::default();
let mut out = Vec::new();
for k in keys {
let cmds = km.map_key(&input(k), &ctx);
ctx.serial += cmds.len() as u64;
out.push(cmds);
}
out
}
#[test]
fn named_keys_map_to_motion_and_editing() {
let mut km = Keymap::new();
let mut shifted = input("ArrowRight");
shifted.shift = true;
let ctx = InputContext::default();
assert_eq!(km.map_key(&input("ArrowLeft"), &ctx), vec![Command::Move(Dir::Left)]);
assert_eq!(km.map_key(&shifted, &ctx), vec![Command::Extend(Dir::Right)]);
assert_eq!(km.map_key(&input("Backspace"), &ctx), vec![Command::DeleteBackward]);
assert_eq!(km.map_key(&input("Enter"), &ctx), vec![Command::Confirm]);
assert_eq!(km.map_key(&input("F5"), &ctx), vec![]);
}
#[test]
fn words_become_replace_typed_so_the_core_checks_the_letters() {
let cmds = commands(&mut Keymap::new(), &["p", "i", " "]);
assert_eq!(cmds[2], vec![Command::ReplaceTyped { typed: "pi".into(), with: vec![atom("\\pi")] }]);
let cmds = commands(&mut Keymap::new(), &["<", "="]);
assert_eq!(cmds[1], vec![atom("="), Command::ReplaceTyped { typed: "<=".into(), with: vec![atom("\\leq")] }]);
}
#[test]
fn typing_through_an_editor() {
assert_eq!(Host::new().keys("/1").key("Tab").keys("2").tex(), "\\frac{1}{2}");
assert_eq!(typed("x^2"), "x^2");
assert_eq!(typed("pi r^2"), "\\pi r^2");
assert_eq!(typed("a*b"), "a\\cdot b");
assert_eq!(typed("x+sin y"), "x+\\sin y");
assert_eq!(typed("sum "), "\\sum");
}
#[test]
fn unknown_words_end_at_space_and_the_space_is_swallowed() {
let mut h = Host::new();
h.keys("xy z");
assert_eq!(h.tex(), "xyz");
assert!(h.outcomes.iter().all(|o| o.changed));
}
#[test]
fn autocorrect_pairs_and_the_ones_left_alone() {
assert_eq!(typed("a<=b"), "a\\leq b");
assert_eq!(typed("x->0"), "x\\to0");
assert_eq!(typed("a+-b"), "a\\pm b");
assert_eq!(typed("x~~y"), "x\\approx y");
assert_eq!(typed("x<-1"), "x<-1");
assert_eq!(typed("a==b"), "a==b");
assert_eq!(typed("3!=6"), "3!=6");
assert_eq!(typed("f:-1"), "f:-1");
assert_eq!(typed("a<==b"), "a\\leq=b");
let mut h = Host::new();
h.km.set_autocorrect(false);
assert_eq!(h.keys("a<=b").tex(), "a<=b");
}
#[test]
fn double_angle_keys_open_and_close_an_angle_pair() {
assert_eq!(typed("<<x>>y"), "\\left\\langle x\\right\\rangle y");
assert_eq!(typed("a>>b"), "a\\left\\langle b\\right\\rangle");
}
#[test]
fn closing_characters_leave_matching_delimiters() {
assert_eq!(typed("(x)y"), "\\left(x\\right)y");
assert_eq!(typed("(x^2)y"), "\\left(x^2\\right)y");
assert_eq!(typed("x)"), "x)");
assert_eq!(typed("[x)"), "\\left[x)\\right]");
assert_eq!(typed("|x|y"), "\\left|x\\right|y");
assert_eq!(typed("|(x|"), "\\left|\\left(x\\left|\\right|\\right)\\right|");
}
#[test]
fn prime_leaves_the_caret_after_the_script() {
assert_eq!(typed("f'x"), "f^{\\prime}x");
assert_eq!(typed("f''"), "f^{\\prime\\prime}");
}
#[test]
fn text_slots_take_characters_literally() {
let mut h = Host::new();
h.keys("text ");
assert!(h.ed.input_context().in_text_slot);
h.keys("a b/c pi ");
assert_eq!(h.tex(), "\\text{a\\ b/c\\ pi\\ }");
}
#[test]
fn an_open_menu_takes_characters_as_its_filter() {
let mut h = Host::new();
h.keys("(x").key("ArrowRight").key("Backspace");
assert!(h.ed.input_context().menu_open);
let before = h.ed.document();
h.keys("pi ");
assert_eq!(h.ed.document(), before);
assert_eq!(h.ed.menu().unwrap().query, "pi ");
}
#[test]
fn suffix_matching_picks_the_longest_known_word() {
let table = [
("sin ", "\\sin", "\\sin"),
("xin ", "x\\in", "xin"),
("beta ", "\\beta", "\\beta"),
("zeta ", "\\zeta", "\\zeta"),
("theta ", "\\theta", "\\theta"),
("cdot ", "\\cdot", "\\cdot"),
("xdot ", "x\\dot{}", "xdot"),
("dfrac ", "\\dfrac{}{}", "\\dfrac{}{}"),
("mathbb R", "\\mathbb{R}", "\\mathbb{R}"),
("abb R", "a\\mathbb{R}", "abbR"),
("propto ", "\\propto", "\\propto"),
("xto ", "x\\to", "xto"),
("top ", "t\\operatorname{}", "top"),
];
for (keys, suffix, whole) in table {
assert_eq!(typed(keys), suffix, "{keys:?} with suffix matching");
let mut h = Host::new();
h.km.set_suffix_matching(false);
assert_eq!(h.keys(keys).tex(), whole, "{keys:?} without suffix matching");
}
}
#[test]
fn ctrl_and_meta_chords_are_not_characters_but_altgr_is() {
let ctx = InputContext::default();
let mut km = Keymap::new();
let mut chord = input("a");
chord.ctrl = true;
assert_eq!(km.map_key(&chord, &ctx), vec![Command::SelectAll]);
chord.meta = true;
chord.ctrl = false;
chord.key = "z".into();
assert_eq!(km.map_key(&chord, &ctx), vec![]);
let mut altgr = input("{");
altgr.ctrl = true;
altgr.alt = true;
assert_eq!(km.map_key(&altgr, &ctx), vec![Command::InsertDelimiters { open: '{', close: '}' }]);
}
#[test]
fn chords_and_named_keys_end_the_word_but_modifiers_do_not() {
let mut h = Host::new();
h.keys("p");
let mut chord = input("c");
chord.ctrl = true;
h.press(chord);
h.keys("i ");
assert_eq!(h.tex(), "pi");
let mut h = Host::new();
h.km.define_word("RR", "reals", vec![atom("\\mathbb{R}")]).unwrap();
h.key("Shift").key("R").key("Shift").key("R").key(" ");
assert_eq!(h.tex(), "\\mathbb{R}");
let mut h = Host::new();
h.keys("p").key("PageDown").keys("i ");
assert_eq!(h.tex(), "pi");
}
#[test]
fn the_word_persists_across_map_key_and_map_text() {
let mut h = Host::new();
h.keys("p").text("i ");
assert_eq!(h.tex(), "\\pi");
let mut h = Host::new();
h.text("p").keys("i ");
assert_eq!(h.tex(), "\\pi");
}
#[test]
fn map_text_tracks_the_caret_between_its_characters() {
assert_eq!(Host::new().text("(x)y").tex(), "\\left(x\\right)y");
assert_eq!(Host::new().text("|x|y").tex(), "\\left|x\\right|y");
assert_eq!(Host::new().text("<<x>>").tex(), "\\left\\langle x\\right\\rangle");
assert_eq!(Host::new().text("text a b").tex(), "\\text{a\\ b}");
assert_eq!(Host::new().text("a<=b pi ").tex(), "a\\leq b\\pi");
}
#[test]
fn raw_newlines_and_tabs_are_dropped_in_math_and_spaces_in_text() {
assert_eq!(Host::new().text("a\nb\tc\r\n").tex(), "abc");
assert_eq!(Host::new().text("p\ni ").tex(), "pi");
assert_eq!(Host::new().text("text a\nb\tc").tex(), "\\text{a\\ b\\ c}");
}
#[test]
fn define_word_rejects_untypable_words_and_keeps_definition_order() {
let mut km = Keymap::new();
for bad in ["", "R2", "é", "a b"] {
assert_eq!(km.define_word(bad, "x", vec![]), Err(InvalidWord(bad.into())));
}
km.define_word("zz", "zeds", vec![atom("z")]).unwrap();
km.define_word("RR", "reals", vec![atom("\\mathbb{R}")]).unwrap();
km.define_word("pi", "varpi", vec![atom("\\varpi")]).unwrap();
km.define_word("zz", "zeds again", vec![atom("z")]).unwrap();
let entries = km.entries();
let words: Vec<&str> = entries.iter().map(|e| e.word).collect();
assert_eq!(words[..4], ["zz", "RR", "pi", "frac"]);
assert_eq!(entries[0].label, "zeds again");
assert_eq!(words.iter().filter(|w| **w == "pi").count(), 1);
assert_eq!(km.entries(), entries);
assert_eq!(km.commands_for_word("pi"), Some(vec![atom("\\varpi")]));
km.undefine_word("pi");
assert_eq!(km.commands_for_word("pi"), Some(vec![atom("\\pi")]));
}
#[test]
fn every_catalog_word_is_unique_labelled_and_inserts_something() {
let km = Keymap::new();
let entries = km.entries();
for (i, e) in entries.iter().enumerate() {
assert!(entries[..i].iter().all(|o| o.word != e.word && o.label != e.label), "duplicate {}", e.word);
assert!(e.word.bytes().all(|b| b.is_ascii_alphabetic()));
let mut ed = Editor::new();
let changed = km.commands_for_word(e.word).unwrap().into_iter().any(|c| ed.exec(c).changed);
assert!(changed, "{} inserts nothing", e.word);
}
}
#[test]
fn catalog_classes_agree_with_typed_characters() {
let km = Keymap::new();
let class = |word: &str| match km.commands_for_word(word).as_deref() {
Some([Command::InsertAtom(s)]) => s.class,
other => panic!("{word}: {other:?}"),
};
assert_eq!(Command::InsertAtom(Symbol::from_char('~').unwrap()), km.commands_for_word("sim").unwrap()[0]);
assert_eq!(class("land"), MathClass::Bin);
assert_eq!(class("ldots"), MathClass::Inner);
assert_eq!(class("sin"), MathClass::Op);
assert_eq!(class("mapsto"), MathClass::Rel);
let cmds = commands(&mut Keymap::new(), &["<", "<"]);
assert_eq!(cmds[1][1], Command::ReplaceTyped {
typed: "<<".into(),
with: vec![Command::InsertDelimiters { open: '⟨', close: '⟩' }],
});
}
#[test]
fn accents_styles_and_braces_from_words() {
assert_eq!(typed("vec v"), "\\vec{v}");
assert_eq!(typed("mathcal L"), "\\mathcal{L}");
assert_eq!(typed("overbrace x"), "\\overbrace{x}");
assert_eq!(typed("tfrac "), "\\tfrac{}{}");
assert_eq!(typed("op f"), "\\operatorname{f}");
}
#[test]
fn host_click_between_keys_does_not_convert_across_the_click() {
let mut h = Host::new();
h.host(Command::InsertText("pq".into())).keys("p");
h.ed.set_cursor(&CaretPath::root(1)).unwrap();
h.keys("i ");
assert_eq!(h.tex(), "piqp");
}
#[test]
fn undo_between_keys_deletes_nothing_and_requests_no_close() {
let mut h = Host::new();
let empty = h.ed.snapshot();
h.keys("pi");
h.ed.restore(&empty).unwrap();
h.keys(" ");
assert_eq!(h.tex(), "");
assert!(!h.closed());
let mut h = Host::new();
h.host(Command::InsertText("ab".into()));
let ab = h.ed.snapshot();
h.keys("pi");
h.ed.restore(&ab).unwrap();
h.keys(" ");
assert_eq!(h.tex(), "ab");
}
#[test]
fn palette_pick_between_keys_does_not_join_the_word() {
let mut h = Host::new();
h.keys("p").host(atom("\\alpha")).keys("i ");
assert_eq!(h.tex(), "p\\alpha i");
let mut h = Host::new();
h.keys("s").host(atom("i")).keys("n ");
assert_eq!(h.tex(), "sin");
}
#[test]
fn matrix_row_insert_between_keys_does_not_join_the_word() {
let mut h = Host::new();
h.host(Command::InsertMatrix { env: MatrixEnv::Pmatrix, rows: 1, cols: 1 }).keys("p");
h.host(Command::MatrixInsertRow(Side::After)).keys("i ");
assert_eq!(h.tex(), "\\begin{pmatrix}p \\\\ i\\end{pmatrix}");
}
#[test]
fn script_base_eviction_keeps_every_atom() {
let mut h = Host::new();
h.keys("x^2").key("ArrowLeft").key("ArrowLeft").keys("pi ");
let tex = h.tex();
for part in ["x", "p", "i", "2"] {
assert!(tex.contains(part), "{tex} lost {part}");
}
assert!(!h.closed());
}
#[test]
fn the_host_reset_drops_the_pending_word() {
let mut h = Host::new();
h.keys("p");
h.km.reset();
h.keys("i ");
assert_eq!(h.tex(), "pi");
h.ed.place_at(Edge::End);
h.keys("pi ");
assert_eq!(h.tex(), "pi\\pi");
}
#[test]
fn typing_a_fraction_with_a_word_builds_it() {
let mut host = Host::new();
for key in ["/", "p", "i", " ", "Tab", "2"] {
host.press(input(key));
}
assert_eq!(host.ed.document().to_tex(), "\\frac{\\pi}{2}");
}