use std::collections::HashMap;
use mathtex_editor_core::{CaretPath, Editor, MathClass, NodeDoc, Side, Snapshot};
use crate::*;
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 ^= self.0 >> 12;
self.0 ^= self.0 << 25;
self.0 ^= self.0 >> 27;
self.0.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
fn below(&mut self, n: usize) -> usize {
(self.next() % n as u64) as usize
}
fn pick<T: Copy>(&mut self, items: &[T]) -> T {
items[self.below(items.len())]
}
fn chance(&mut self, percent: usize) -> bool {
self.below(100) < percent
}
}
const WORDS: &[&str] = &["pi", "sin", "in", "eta", "beta", "to", "text", "sum", "frac", "xpi", "pin"];
const CHARS: &[char] = &['p', 'i', 's', 'n', 'x', 't', 'o', 'e', '<', '=', '>', '-', '~', '|', '(', ')', '\'', '^', '/'];
const HOST_ATOMS: &[&str] = &["p", "i", "n", "\\alpha"];
const OLD: MathClass = MathClass::Inner;
fn old_atoms(ed: &Editor) -> HashMap<String, usize> {
let mut counts = HashMap::new();
ed.document().visit(|n| {
if let NodeDoc::Atom(s) = n {
if s.class == OLD {
*counts.entry(s.latex.clone()).or_insert(0) += 1;
}
}
});
counts
}
struct Run {
ed: Editor,
km: Keymap,
history: Vec<Snapshot>,
conversions: usize,
}
impl Run {
fn apply(&mut self, cmds: Vec<Command>, seed: u64) {
for cmd in cmds {
let selected = self.ed.selection().is_some();
let before = old_atoms(&self.ed);
let out = self.ed.exec(cmd.clone());
assert!(!out.close, "seed {seed}: {cmd:?} asked to close");
let after = old_atoms(&self.ed);
if !selected {
for (latex, &n) in &before {
let left = after.get(latex).copied().unwrap_or(0);
assert!(left >= n, "seed {seed}: {cmd:?} deleted an earlier {latex}");
}
}
if matches!(cmd, Command::ReplaceTyped { .. }) && out.changed {
self.conversions += 1;
}
}
}
fn user(&mut self, rng: &mut Rng, seed: u64) {
let text = if rng.chance(35) {
format!("{} ", rng.pick(WORDS))
} else if rng.chance(15) {
" ".to_string()
} else {
rng.pick(CHARS).to_string()
};
if rng.chance(30) {
let ctx = self.ed.input_context();
let cmds = self.km.map_text(&text, &ctx);
self.apply(cmds, seed);
return;
}
for c in text.chars() {
let ctx = self.ed.input_context();
let key = KeyInput { key: c.to_string(), shift: false, ctrl: false, alt: false, meta: false };
let cmds = self.km.map_key(&key, &ctx);
self.apply(cmds, seed);
}
}
fn host(&mut self, rng: &mut Rng, resets: bool) {
self.history.push(self.ed.snapshot());
match rng.below(9) {
0 => {
let dir = rng.pick(&[Dir::Left, Dir::Right, Dir::Up, Dir::Down]);
let _ = self.ed.exec(Command::Move(dir));
}
1 => {
let len = self.ed.document().len();
self.ed.set_cursor(&CaretPath::root(rng.below(len + 1))).unwrap();
}
2 => {
let back = self.history[rng.below(self.history.len())].clone();
self.ed.restore(&back).unwrap();
}
3 => {
let latex = rng.pick(HOST_ATOMS);
let _ = self.ed.exec(Command::InsertAtom(Symbol { latex: latex.into(), class: OLD }));
}
4 => {
let _ = self.ed.exec(Command::InsertMatrix { env: MatrixEnv::Pmatrix, rows: 1, cols: 2 });
}
5 => {
let _ = self.ed.exec(Command::MatrixInsertRow(rng.pick(&[Side::Before, Side::After])));
}
6 => {
let _ = self.ed.exec(Command::DeleteBackward);
}
7 => {
let _ = self.ed.exec(Command::Extend(Dir::Left));
}
_ => {
let _ = self.ed.exec(Command::Collapse);
}
}
self.age();
if resets {
self.km.reset();
}
}
fn age(&mut self) {
if self.ed.menu().is_some() {
return;
}
let mut snap = self.ed.snapshot();
snap.document.visit_mut(|n| {
if let NodeDoc::Atom(s) = n {
s.class = OLD;
}
});
self.ed.restore(&snap).unwrap();
}
}
#[test]
fn keymap_commands_never_delete_atoms_typed_outside_the_current_run() {
let mut conversions = 0;
for seed in 1..=300u64 {
let mut rng = Rng(seed.wrapping_mul(0x9E37_79B9_7F4A_7C15));
let mut run = Run { ed: Editor::new(), km: Keymap::new(), history: Vec::new(), conversions: 0 };
let resets = seed % 2 == 0;
for _ in 0..80 {
if rng.chance(35) {
run.host(&mut rng, resets);
} else {
run.user(&mut rng, seed);
}
}
conversions += run.conversions;
}
assert!(conversions > 500, "only {conversions} conversions ran");
}