use std::cell::Cell;
use std::rc::Rc;
use std::sync::OnceLock;
use mathtex_editor_core::{
CaretPath, Command, Dir, DocumentError, FracStyle, HostBoxPolicy, MAX_HOST_TOKEN, MatrixEnv, Side, Symbol,
};
use mathtex_editor_keymap::KeyInput;
use mathtex_engine::font::InMemoryFontLoader;
use mathtex_engine::{
Format, FormatBuilder, HostBox, HostBoxRequest, HostBoxes, InMemoryResourceProvider, Options, TypesetError,
Typesetter,
};
use mathtex_ir::Length;
use crate::*;
const PREAMBLE: &str = concat!(
r"\catcode`\{=1 \catcode`\}=2 \catcode`\$=3 \catcode`\#=6 \catcode`\^=7 \catcode`\_=8 ",
r"\fontdimen22\nullfont=0pt ",
r"\textfont2=\nullfont \scriptfont2=\nullfont \scriptscriptfont2=\nullfont ",
r"\textfont3=\nullfont \scriptfont3=\nullfont \scriptscriptfont3=\nullfont ",
r"\def\phantom#1{\hbox{\vrule width 2pt height 2pt}}\def\frac#1#2{{#1\over#2}}",
);
fn session() -> Session<InMemoryFontLoader> {
static FORMAT: OnceLock<Vec<u8>> = OnceLock::new();
let bytes = FORMAT.get_or_init(|| {
FormatBuilder::new(PREAMBLE)
.build(&InMemoryResourceProvider::new(), &InMemoryFontLoader::new())
.expect("the test preamble bakes")
});
let format = Format::from_owned(bytes.clone()).expect("the baked format loads");
let options = Options::default().with_cache_capacity(0);
Session::new(Typesetter::new(format, InMemoryFontLoader::new(), options).expect("no fonts to load"))
}
fn pt(v: i32) -> Length {
Length(v * Length::SP_PER_PT)
}
fn key(s: &mut Session<InMemoryFontLoader>, k: &str) -> Update {
s.key(&KeyInput { key: k.into(), shift: false, ctrl: false, alt: false, meta: false })
}
fn tex(s: &Session<InMemoryFontLoader>) -> String {
s.editor().document().to_tex()
}
fn tokens(s: &Session<InMemoryFontLoader>) -> Vec<u32> {
let mut out = Vec::new();
s.editor().document().visit(|n| {
if let mathtex_editor_core::NodeDoc::HostBox { token } = n {
out.push(*token);
}
});
out
}
struct Counting(Rc<Cell<usize>>);
impl HostBoxes for Counting {
fn host_box(&self, _request: &HostBoxRequest) -> Option<HostBox> {
self.0.set(self.0.get() + 1);
Some(HostBox::new(pt(10), pt(5), pt(1)))
}
}
#[test]
fn typeset_failures_surface_as_errors() {
let mut s = session();
let _ = s.command(Command::InsertAtom(Symbol::from_latex("\\nosuchmacro")));
match s.view() {
Err(TypesetError::Tex { message, .. }) => assert!(message.contains("Undefined control sequence"), "{message}"),
other => panic!("expected a TeX error, got {other:?}"),
}
assert!(s.undo().changed);
assert!(s.view().is_ok());
}
#[test]
fn view_typesets_only_after_an_edit() {
let mut s = session();
let count = Rc::new(Cell::new(0));
s.set_host_boxes(Box::new(Counting(count.clone())));
let (token, _) = s.insert_host_box().unwrap();
let placed = s.view().unwrap().render.host_objects.clone();
assert_eq!(count.get(), 1);
assert_eq!(placed.len(), 1);
assert_eq!((placed[0].token, placed[0].rect.width, placed[0].rect.height), (token, 10.0, 6.0));
let caret = s.view().unwrap().render.caret;
assert!(s.command(Command::Move(Dir::Left)).needs_redraw);
let moved = s.view().unwrap().render.caret;
assert_eq!(count.get(), 1, "motion must not typeset");
assert_eq!((caret.x, moved.x), (10.0, 0.0));
let _ = s.insert_host_box().unwrap();
let _ = s.view().unwrap();
assert_eq!(count.get(), 3);
s.host_boxes_changed();
let _ = s.view().unwrap();
assert_eq!(count.get(), 5);
}
#[test]
fn host_box_sizes_lay_out_and_relayout_when_changed() {
let mut s = session();
let (token, _) = s.insert_host_box().unwrap();
s.set_host_box_size(token, pt(10), pt(4), pt(1)).unwrap();
assert_eq!(s.view().unwrap().render.host_objects[0].rect.width, 10.0);
s.set_host_box_size(token, pt(20), pt(4), pt(1)).unwrap();
assert_eq!(s.view().unwrap().render.host_objects[0].rect.width, 20.0);
assert!(s.view().unwrap().warnings.is_empty());
}
#[test]
fn pointer_places_and_extends_in_view_coordinates() {
let mut s = session();
for _ in 0..2 {
let (token, _) = s.insert_host_box().unwrap();
s.set_host_box_size(token, pt(10), pt(5), pt(0)).unwrap();
}
let placed = s.pointer(Point { x: 3.0, y: 2.0 }, false);
assert!(placed.needs_redraw && !placed.changed);
assert_eq!(s.editor().cursor(), CaretPath::root(0));
assert!(!s.pointer(Point { x: 17.0, y: 2.0 }, true).changed);
let sel = s.editor().selection().unwrap();
assert_eq!((sel.anchor.index, sel.focus.index), (0, 2));
}
#[test]
fn toolbar_palette_and_menu_picks_each_undo_in_one_step() {
let mut s = session();
let _ = s.command(Command::InsertMatrix { env: MatrixEnv::Pmatrix, rows: 1, cols: 1 });
let one = tex(&s);
assert!(s.command(Command::MatrixInsertRow(Side::After)).changed);
assert_eq!(s.editor().matrix_shape(), Some((2, 1)));
assert!(s.undo().changed);
assert_eq!(tex(&s), one);
let mut s = session();
assert!(s.commit_word("sqrt").changed);
assert!(s.commit_word("alpha").changed);
assert_eq!(tex(&s), "\\sqrt{\\alpha}");
let _ = s.undo();
assert_eq!(tex(&s), "\\sqrt{}");
let _ = s.undo();
assert_eq!(tex(&s), "");
assert!(!s.commit_word("nosuchword").changed);
let mut s = session();
let _ = key(&mut s, "(");
let _ = key(&mut s, "x");
let _ = s.command(Command::Move(Dir::Right));
let _ = s.command(Command::DeleteBackward);
assert!(s.editor().menu().is_some());
let before = tex(&s);
assert!(s.command(Command::MenuSelect(1)).changed);
assert_ne!(tex(&s), before);
let _ = s.undo();
assert_eq!(tex(&s), before);
}
#[test]
fn a_keymap_batch_is_one_undo_step() {
let mut s = session();
for k in ["s", "i", "n", " "] {
let _ = key(&mut s, k);
}
assert_eq!(tex(&s), "\\sin");
let _ = s.undo();
assert_eq!(tex(&s), "sin");
let _ = s.undo();
assert_eq!(tex(&s), "si");
}
#[test]
fn any_edit_clears_redo_and_motion_does_not() {
type Edit = fn(&mut Session<InMemoryFontLoader>) -> Update;
let edits: [(&str, Edit); 8] = [
("key", |s| key(s, "b")),
("text", |s| s.text("c")),
("command", |s| s.command(Command::InsertFraction(FracStyle::Bar))),
("word", |s| s.commit_word("pi")),
("paste json", |s| {
let json = s.copy().unwrap().json;
s.paste_json(&json).unwrap()
}),
("paste internal", |s| {
let _ = s.copy().unwrap();
s.paste_internal().unwrap()
}),
("paste text", |s| s.paste_text("d")),
("cut", |s| s.cut().unwrap().1),
];
for (name, edit) in edits {
let mut s = session();
let _ = s.text("a");
let _ = s.text("z");
let _ = s.undo();
let _ = s.command(Command::Move(Dir::Left));
let _ = s.pointer(Point { x: 0.0, y: 0.0 }, false);
let _ = s.command(Command::SelectAll);
assert!(s.history().can_redo(), "{name}: motion kept redo");
assert!(edit(&mut s).changed, "{name}");
assert!(!s.history().can_redo(), "{name} left redo in place");
}
}
#[test]
fn tokens_beyond_the_tex_integer_range_are_refused() {
let mut s = session();
let big = MAX_HOST_TOKEN + 1;
assert_eq!(big, 1 << 31);
assert_eq!(s.set_host_box_size(big, pt(1), pt(1), pt(0)), Err(TokenError::TooLarge(big)));
assert!(!s.command(Command::InsertHostBox(big)).changed);
let json = format!(r#"{{"version":1,"root":[{{"type":"host_box","data":{{"token":{big}}}}}]}}"#);
assert!(matches!(s.paste_json(&json), Err(PasteError::Json(_))));
assert!(s.command(Command::InsertHostBox(MAX_HOST_TOKEN)).changed);
assert!(s.view().unwrap().tex.contains(&format!("\\hostbox{{{MAX_HOST_TOKEN}}}")));
let mut registry = TokenRegistry::new();
registry.reserve(1).unwrap();
registry.reserve(3).unwrap();
assert_eq!((registry.mint(), registry.mint(), registry.mint()), (Ok(2), Ok(4), Ok(5)));
assert_eq!(registry.reserve(big), Err(TokenError::TooLarge(big)));
let mut invalid = mathtex_editor_core::Document::new(vec![mathtex_editor_core::NodeDoc::HostBox { token: big }]);
assert_eq!(invalid.validate(), Err(DocumentError::HostTokenTooLarge(big)));
assert_eq!(s.load(invalid.clone()), [Repair::DroppedHostBox { token: big }]);
assert_eq!(invalid.repair().len(), 1);
}
#[test]
fn pasting_remints_so_no_token_repeats() {
let mut s = session();
let (first, _) = s.insert_host_box().unwrap();
s.set_host_box_size(first, pt(7), pt(3), pt(1)).unwrap();
let _ = s.command(Command::InsertHostBox(first));
let _ = s.command(Command::SelectAll);
let json = s.copy().unwrap().json;
let _ = s.command(Command::Move(Dir::Right));
let pasted = s.paste_internal().unwrap();
assert_eq!(pasted.reminted.len(), 2);
assert!(pasted.reminted.iter().all(|&(old, new)| old == first && new != first));
let again = s.paste_json(&json).unwrap();
assert_eq!(again.reminted.len(), 2);
let all = tokens(&s);
assert_eq!(all.len(), 6);
let mut fresh = all[2..].to_vec();
fresh.sort_unstable();
fresh.dedup();
assert_eq!(fresh.len(), 4, "{all:?}");
assert!(!fresh.contains(&first));
let widths: Vec<f64> = s.view().unwrap().render.host_objects.iter().map(|h| h.rect.width).collect();
assert_eq!(widths, [7.0; 6]);
}
#[test]
fn live_tokens_follow_the_document_history_and_clipboard() {
let mut s = session();
let (token, _) = s.insert_host_box().unwrap();
s.set_host_box_size(token, pt(5), pt(3), pt(1)).unwrap();
let live = |s: &mut Session<InMemoryFontLoader>| s.live_host_tokens().into_iter().collect::<Vec<_>>();
assert_eq!(live(&mut s), [token]);
let _ = s.command(Command::DeleteBackward);
let _ = s.command(Command::DeleteBackward);
assert_eq!(tex(&s), "");
assert_eq!(live(&mut s), [token], "undo can still restore it");
let _ = s.undo();
let _ = s.undo();
assert_eq!(live(&mut s), [token], "redo can still restore it");
let _ = s.text("a");
assert_eq!(live(&mut s), Vec::<u32>::new());
let (other, _) = s.insert_host_box().unwrap();
let _ = s.command(Command::SelectAll);
let _ = s.cut().unwrap();
s.set_undo_limit(0);
assert_eq!(live(&mut s), [other], "the clipboard holds it");
}
#[test]
fn a_stopped_motion_can_step_over_the_host_box() {
let mut s = session();
s.set_host_box_policy(HostBoxPolicy::Enter);
let (token, _) = s.insert_host_box().unwrap();
let stopped = s.command(Command::Move(Dir::Left));
let entry = stopped.entered_host_box.unwrap();
assert_eq!((entry.token, entry.side), (token, Side::After));
assert_eq!(s.editor().cursor(), CaretPath::root(1));
assert!(s.step_over_host_box(entry).needs_redraw);
assert_eq!(s.editor().cursor(), CaretPath::root(0));
assert!(s.command(Command::Move(Dir::Right)).entered_host_box.is_some(), "the policy is back");
}
#[test]
fn previews_typeset_one_word_on_their_own() {
let mut s = session();
let view = s.preview_word("frac").unwrap().unwrap();
assert!(view.tex.starts_with("\\frac"), "{}", view.tex);
assert!(!view.render.placeholders.is_empty());
assert!(s.preview_word("nosuchword").is_none());
assert_eq!(tex(&s), "");
}