use crate::command::{Command, CommandResult, Dir, ExitDir};
use crate::doc::{Document, DocFragment};
use crate::host::{Host, RenderOutput};
use crate::menu::{Menu, RowEffect};
use crate::model::{Cursor, NodeId, Selection, Symbol};
use crate::{nav, selection};
#[derive(Debug, Clone)]
pub struct UndoEntry(pub(crate) Document);
pub struct Editor<H: Host> {
tree: crate::model::Tree,
cursor: Cursor,
sel_anchor: Option<Cursor>,
pending_menu: Option<Menu>,
host: H,
}
impl<H: Host> Editor<H> {
pub fn new(host: H, initial: Option<Document>) -> Self {
let tree = match initial {
Some(d) => crate::model::Tree::from_doc(&d),
None => crate::model::Tree::new(),
};
let cursor = Cursor {
seq: tree.root(),
index: 0,
};
Self {
tree,
cursor,
sel_anchor: None,
pending_menu: None,
host,
}
}
pub fn cursor(&self) -> Cursor {
self.cursor
}
pub fn matrix_shape(&self) -> Option<(usize, usize)> {
self.tree.matrix_shape_at(self.cursor)
}
pub fn current_selection(&self) -> Option<Selection> {
self.sel_anchor
.filter(|a| a.seq == self.cursor.seq && a.index != self.cursor.index)
.map(|a| Selection {
seq: self.cursor.seq,
anchor: a.index,
focus: self.cursor.index,
})
}
pub fn exec(&mut self, cmd: Command) -> CommandResult {
if self.pending_menu.is_some() {
if let Some(result) = self.exec_menu(&cmd) {
return result;
}
self.pending_menu = None;
}
match cmd {
Command::Move(Dir::Left) => match nav::move_left(&self.tree, self.cursor) {
Some(c) => self.moved(c),
None => self.boundary(ExitDir::Left),
},
Command::Move(Dir::Right) => match nav::move_right(&self.tree, self.cursor) {
Some(c) => self.moved(c),
None => self.boundary(ExitDir::Right),
},
Command::Move(Dir::Up) => match nav::vertical(&self.tree, self.cursor, true) {
Some(c) => self.moved(c),
None => self.vertical_boundary(true),
},
Command::Move(Dir::Down) => match nav::vertical(&self.tree, self.cursor, false) {
Some(c) => self.moved(c),
None => self.vertical_boundary(false),
},
Command::MoveLineStart => self.moved(Cursor {
seq: self.cursor.seq,
index: 0,
}),
Command::MoveLineEnd => self.moved(Cursor {
seq: self.cursor.seq,
index: self.tree.len(self.cursor.seq),
}),
Command::Tab => match self.next_fill_target(true) {
Some(c) => self.moved(c),
None => self.exit_result(ExitDir::Right),
},
Command::ShiftTab => match self.next_fill_target(false) {
Some(c) => self.moved(c),
None => self.exit_result(ExitDir::Left),
},
Command::MoveTo(p) => {
let (latex, spans) = self.tree.export_latex();
let ir = self.host.typeset(&latex);
let c = crate::matcher::hit_test(&ir, &spans, &self.tree, p);
self.moved(crate::nav::normalize(&self.tree, c))
}
Command::ExtendTo(p) => {
let (latex, spans) = self.tree.export_latex();
let ir = self.host.typeset(&latex);
let target = crate::nav::normalize(
&self.tree,
crate::matcher::hit_test(&ir, &spans, &self.tree, p),
);
self.extend_to(target)
}
Command::Extend(Dir::Left) => self.extend(false),
Command::Extend(Dir::Right) => self.extend(true),
Command::Extend(Dir::Up) | Command::Extend(Dir::Down) => self.no_op(),
Command::SelectAll => {
let s = selection::select_all(&self.tree);
self.cursor = Cursor {
seq: s.seq,
index: s.focus,
};
self.sel_anchor = Some(Cursor { seq: s.seq, index: s.anchor });
self.no_op()
}
Command::Collapse => {
self.sel_anchor = None;
self.no_op()
}
Command::Confirm => {
self.sel_anchor = None;
self.no_op()
}
Command::MenuSelect(_) => self.no_op(),
Command::InsertAtom(sym) => {
let before = self.snapshot();
self.replace_selection();
let c = self.tree.insert_atom(self.cursor, sym);
self.after_edit(before, c)
}
Command::InsertText(text) => {
let before = self.snapshot();
self.replace_selection();
let mut c = self.cursor;
for ch in text.chars() {
c = self.tree.insert_atom(c, Symbol::from_char(ch));
}
self.after_edit(before, c)
}
Command::InsertFraction(style) => {
let before = self.snapshot();
let sel = self.current_selection();
let c = self.tree.insert_fraction(self.cursor, style, sel);
self.after_edit(before, c)
}
Command::InsertScript(slot) => {
if let Some(c) = self.tree.bigop_limit_target(self.cursor, slot) {
self.moved(c)
} else {
let before = self.snapshot();
let c = self.tree.attach_script(self.cursor, slot);
self.after_edit(before, c)
}
}
Command::InsertBigOp(op) => {
let before = self.snapshot();
let c = self.tree.insert_big_op(self.cursor, op);
self.after_edit(before, c)
}
Command::InsertSqrt => {
let before = self.snapshot();
let sel = self.current_selection();
let c = self.tree.insert_sqrt(self.cursor, sel);
self.after_edit(before, c)
}
Command::InsertDelimiters { open, close } => {
let before = self.snapshot();
let sel = self.current_selection();
let c = self.tree.insert_delimiters(self.cursor, open, close, sel);
self.after_edit(before, c)
}
Command::InsertAccent(mark) => {
let before = self.snapshot();
let sel = self.current_selection();
let c = self.tree.insert_accent(self.cursor, mark, sel);
self.after_edit(before, c)
}
Command::InsertUnderOver(spec) => {
let before = self.snapshot();
let sel = self.current_selection();
let c = self.tree.insert_under_over(self.cursor, spec, sel);
self.after_edit(before, c)
}
Command::InsertStyled(variant) | Command::ApplyStyle(variant) => {
let before = self.snapshot();
let sel = self.current_selection();
let c = self.tree.insert_styled(self.cursor, variant, sel);
self.after_edit(before, c)
}
Command::InsertMatrix { env, rows, cols } => {
let before = self.snapshot();
self.replace_selection();
let c = self.tree.insert_matrix(self.cursor, env, rows, cols);
self.after_edit(before, c)
}
Command::InsertFragment(frag) => {
let before = self.snapshot();
self.replace_selection();
let c = self.tree.insert_fragment(self.cursor, &frag);
self.after_edit(before, c)
}
Command::DeleteBackward => {
if let Some(s) = self.current_selection() {
let before = self.snapshot();
let c = self.tree.delete_selection(s);
self.after_edit(before, c)
} else if let Some(node) = nav::adjacent_structure_backward(&self.tree, self.cursor) {
self.trigger_structure_backspace(node)
} else if self.cursor.seq == self.tree.root() && self.tree.is_empty(self.cursor.seq)
{
self.close_result()
} else {
let before = self.snapshot();
let c = self.tree.delete_backward(self.cursor);
self.after_edit(before, c)
}
}
Command::DeleteForward => {
if let Some(s) = self.current_selection() {
let before = self.snapshot();
let c = self.tree.delete_selection(s);
self.after_edit(before, c)
} else if let Some(node) = nav::adjacent_structure_forward(&self.tree, self.cursor) {
self.trigger_structure_backspace(node)
} else if self.cursor.seq == self.tree.root() && self.tree.is_empty(self.cursor.seq)
{
self.close_result()
} else {
let before = self.snapshot();
let c = self.tree.delete_forward(self.cursor);
self.after_edit(before, c)
}
}
Command::MatrixInsertRow(side) => {
let before = self.snapshot();
let c = self.tree.matrix_insert_row(self.cursor, side);
self.after_edit(before, c)
}
Command::MatrixDeleteRow => {
let before = self.snapshot();
let c = self.tree.matrix_delete_row(self.cursor);
self.after_edit(before, c)
}
Command::MatrixInsertCol(side) => {
let before = self.snapshot();
let c = self.tree.matrix_insert_col(self.cursor, side);
self.after_edit(before, c)
}
Command::MatrixDeleteCol => {
let before = self.snapshot();
let c = self.tree.matrix_delete_col(self.cursor);
self.after_edit(before, c)
}
}
}
pub fn render(&self) -> RenderOutput {
let (latex, spans) = self.tree.export_latex();
let ir = self.host.typeset(&latex);
crate::matcher::render(
&self.tree,
self.cursor,
self.current_selection(),
&spans,
ir,
self.pending_menu.as_ref(),
)
}
pub fn selection_latex(&self) -> String {
match self.current_selection() {
Some(s) => self.tree.selection_latex(s),
None => String::new(),
}
}
pub fn selection_fragment(&self) -> DocFragment {
match self.current_selection() {
Some(s) => {
let lo = s.anchor.min(s.focus);
let hi = s.anchor.max(s.focus);
self.tree
.to_doc()
.root()
.get(lo..hi)
.map(<[_]>::to_vec)
.unwrap_or_default()
}
None => Vec::new(),
}
}
pub fn apply_undo(&mut self, entry: UndoEntry) -> UndoEntry {
let current = self.tree.to_doc();
self.tree = crate::model::Tree::from_doc(&entry.0);
self.cursor = Cursor {
seq: self.tree.root(),
index: self.tree.len(self.tree.root()),
};
self.sel_anchor = None;
self.host.on_render();
UndoEntry(current)
}
pub fn enter_from(&mut self, side: ExitDir) {
let root = self.tree.root();
let index = match side {
ExitDir::Left | ExitDir::Up => 0,
ExitDir::Right | ExitDir::Down => self.tree.len(root),
};
self.cursor = Cursor { seq: root, index };
self.sel_anchor = None;
self.pending_menu = None;
self.host.on_render();
}
pub fn document(&self) -> Document {
self.tree.to_doc()
}
pub fn latex(&self) -> String {
self.tree.export_display()
}
fn snapshot(&self) -> Document {
self.tree.to_doc()
}
fn replace_selection(&mut self) {
if let Some(s) = self.current_selection() {
self.cursor = self.tree.delete_selection(s);
self.sel_anchor = None;
}
}
fn select_node(&mut self, node: NodeId) -> CommandResult {
let Some((seq, idx)) = self.tree.index_in_parent(node) else {
return self.no_op();
};
self.cursor = Cursor { seq, index: idx + 1 };
self.sel_anchor = Some(Cursor { seq, index: idx });
self.no_op()
}
fn trigger_structure_backspace(&mut self, node: NodeId) -> CommandResult {
match self.tree.swap_variants(node) {
Some(variants) => {
self.pending_menu = Some(Menu::for_node(node, variants));
self.no_op()
}
None => self.select_node(node),
}
}
fn exec_menu(&mut self, cmd: &Command) -> Option<CommandResult> {
match cmd {
Command::InsertAtom(sym) if sym.latex.chars().count() == 1 => {
let menu = self.pending_menu.as_mut()?;
menu.query.push_str(&sym.latex);
menu.selected = 0;
Some(self.no_op())
}
Command::InsertText(text) => {
let menu = self.pending_menu.as_mut()?;
menu.query.push_str(text);
menu.selected = 0;
Some(self.no_op())
}
Command::DeleteBackward => {
let menu = self.pending_menu.as_mut()?;
if menu.query.is_empty() {
let selected = menu.selected;
Some(self.commit_menu_row(selected))
} else {
menu.query.pop();
menu.selected = 0;
Some(self.no_op())
}
}
Command::Move(Dir::Up) => {
let menu = self.pending_menu.as_mut()?;
menu.selected = menu.selected.saturating_sub(1);
Some(self.no_op())
}
Command::Move(Dir::Down) => {
let menu = self.pending_menu.as_mut()?;
let max = menu.visible().len().saturating_sub(1);
menu.selected = (menu.selected + 1).min(max);
Some(self.no_op())
}
Command::Confirm => {
let selected = self.pending_menu.as_ref()?.selected;
Some(self.commit_menu_row(selected))
}
Command::MenuSelect(i) => Some(self.commit_menu_row(*i)),
Command::Collapse => {
self.pending_menu = None;
Some(self.no_op())
}
_ => None,
}
}
fn commit_menu_row(&mut self, visible_idx: usize) -> CommandResult {
let Some(menu) = self.pending_menu.take() else {
return self.no_op();
};
let visible = menu.visible();
let Some(row) = visible.get(visible_idx) else {
return self.no_op();
};
match row {
RowEffect::Delete => {
let before = self.snapshot();
let Some((seq, idx)) = self.tree.index_in_parent(menu.anchor) else {
return self.no_op();
};
let c = self.tree.delete_selection(Selection { seq, anchor: idx, focus: idx + 1 });
self.after_edit(before, c)
}
RowEffect::Swap(variant) => {
let before = self.snapshot();
self.tree.apply_swap(menu.anchor, variant);
self.after_edit(before, self.cursor)
}
}
}
fn extend(&mut self, right: bool) -> CommandResult {
let sel = self.current_selection().unwrap_or(Selection {
seq: self.cursor.seq,
anchor: self.cursor.index,
focus: self.cursor.index,
});
let ns = selection::extend(&self.tree, sel, right);
self.cursor = Cursor {
seq: ns.seq,
index: ns.focus,
};
self.sel_anchor = Some(Cursor { seq: ns.seq, index: ns.anchor });
self.no_op()
}
fn next_fill_target(&self, forward: bool) -> Option<Cursor> {
if forward {
if let Some(parent) = self.tree.seq_parent(self.cursor.seq) {
if let Some(crate::model::Kind::BigOp { lower, upper, .. }) =
self.tree.kind(parent)
{
if self.cursor.seq == *lower && self.tree.is_empty(*upper) {
return Some(Cursor {
seq: *upper,
index: 0,
});
}
}
}
if self.cursor.index > 0 {
let prev = self.tree.items(self.cursor.seq)[self.cursor.index - 1];
if let Some(crate::model::Kind::BigOp { lower, upper, .. }) =
self.tree.kind(prev)
{
for s in [*lower, *upper] {
if self.tree.is_empty(s) {
return Some(Cursor { seq: s, index: 0 });
}
}
}
}
if let Some(parent) = self.tree.seq_parent(self.cursor.seq) {
if let Some(crate::model::Kind::Sqrt { index, radicand }) = self.tree.kind(parent) {
if self.cursor.seq == *radicand && self.tree.is_empty(*index) {
return Some(Cursor {
seq: *index,
index: 0,
});
}
}
}
}
let mut c = self.cursor;
for _ in 0..100_000 {
c = if forward {
nav::move_right(&self.tree, c)?
} else {
nav::move_left(&self.tree, c)?
};
if self.tree.is_empty(c.seq) {
return Some(Cursor {
seq: c.seq,
index: 0,
});
}
}
None
}
fn after_edit(&mut self, before: Document, cursor: Cursor) -> CommandResult {
self.cursor = cursor;
self.sel_anchor = None;
let doc = self.tree.to_doc();
self.host.on_change(&doc);
self.host.on_render();
CommandResult {
cursor,
undo: Some(UndoEntry(before)),
exit: None,
}
}
fn extend_to(&mut self, target: Cursor) -> CommandResult {
let anchor = self.sel_anchor.unwrap_or(self.cursor);
let sel = selection::extend_to(&self.tree, anchor, target);
self.sel_anchor = Some(Cursor { seq: sel.seq, index: sel.anchor });
self.cursor = Cursor { seq: sel.seq, index: sel.focus };
self.no_op()
}
fn moved(&mut self, cursor: Cursor) -> CommandResult {
self.cursor = cursor;
self.sel_anchor = None;
CommandResult {
cursor,
undo: None,
exit: None,
}
}
fn no_op(&self) -> CommandResult {
CommandResult {
cursor: self.cursor,
undo: None,
exit: None,
}
}
fn boundary(&mut self, dir: ExitDir) -> CommandResult {
if self.current_selection().is_some() {
return self.moved(self.cursor);
}
self.exit_result(dir)
}
fn vertical_boundary(&mut self, up: bool) -> CommandResult {
if self.current_selection().is_some() {
return self.moved(self.cursor);
}
let dir = if up { ExitDir::Up } else { ExitDir::Down };
let mut seq = self.cursor.seq;
loop {
let Some(parent) = self.tree.seq_parent(seq) else {
return self.exit_result(dir);
};
let Some((pseq, _)) = self.tree.index_in_parent(parent) else {
return self.exit_result(dir);
};
if nav::vertical(&self.tree, Cursor { seq: pseq, index: 0 }, up).is_some() {
return self.moved(self.cursor);
}
seq = pseq;
}
}
fn exit_result(&self, dir: ExitDir) -> CommandResult {
self.host.on_exit(dir);
CommandResult {
cursor: self.cursor,
undo: None,
exit: Some(dir),
}
}
fn close_result(&self) -> CommandResult {
self.host.request_close();
CommandResult {
cursor: self.cursor,
undo: None,
exit: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::command::Command;
use crate::host::Host;
use crate::model::{FracStyle, Kind, MathClass, ScriptSlot, Symbol, Variant};
struct NullHost;
impl Host for NullHost {
fn typeset(&self, _latex: &str) -> mathtex_ir::Fragment {
mathtex_ir::Fragment::default()
}
fn on_change(&self, _doc: &Document) {}
fn on_exit(&self, _dir: ExitDir) {}
fn request_close(&self) {}
fn on_render(&self) {}
}
#[derive(Default)]
struct HostLog {
exits: std::cell::RefCell<Vec<ExitDir>>,
closes: std::cell::Cell<usize>,
renders: std::cell::Cell<usize>,
}
struct RecordingHost(std::rc::Rc<HostLog>);
impl Host for RecordingHost {
fn typeset(&self, _latex: &str) -> mathtex_ir::Fragment {
mathtex_ir::Fragment::default()
}
fn on_change(&self, _doc: &Document) {}
fn on_exit(&self, dir: ExitDir) {
self.0.exits.borrow_mut().push(dir);
}
fn request_close(&self) {
self.0.closes.set(self.0.closes.get() + 1);
}
fn on_render(&self) {
self.0.renders.set(self.0.renders.get() + 1);
}
}
fn recording() -> (std::rc::Rc<HostLog>, Editor<RecordingHost>) {
let log = std::rc::Rc::new(HostLog::default());
let ed = Editor::new(RecordingHost(log.clone()), None);
(log, ed)
}
fn sym(c: &str) -> Symbol {
Symbol {
latex: c.into(),
class: MathClass::Ord,
}
}
#[test]
fn insert_fraction_and_navigate() {
let mut ed = Editor::new(NullHost, None);
ed.exec(Command::InsertFraction(FracStyle::Bar));
assert!(ed.tree.is_empty(ed.cursor.seq));
ed.exec(Command::InsertAtom(sym("x")));
assert_eq!(ed.tree.len(ed.cursor.seq), 1);
}
#[test]
fn backspace_next_to_structure_selects_then_deletes() {
let mut ed = Editor::new(NullHost, None);
ed.exec(Command::InsertFraction(FracStyle::Bar));
ed.exec(Command::InsertAtom(sym("a")));
let root = ed.tree.root();
let frac = ed.tree.items(root)[0];
let den = ed.tree.child_seqs(frac)[1];
ed.cursor = Cursor { seq: den, index: 0 };
ed.exec(Command::InsertAtom(sym("b"))); ed.cursor = Cursor { seq: root, index: 1 };
ed.sel_anchor = None;
ed.exec(Command::DeleteBackward);
assert_eq!(ed.tree.len(root), 1);
let sel = ed.current_selection().expect("first backspace should select the structure");
assert_eq!((sel.seq, sel.anchor, sel.focus), (root, 0, 1));
ed.exec(Command::DeleteBackward);
assert_eq!(ed.tree.len(root), 0);
}
#[test]
fn backspace_next_to_delim_opens_swap_menu() {
let mut ed = Editor::new(NullHost, None);
ed.exec(Command::InsertDelimiters { open: '(', close: ')' });
let root = ed.tree.root();
ed.cursor = Cursor { seq: root, index: 1 };
ed.sel_anchor = None;
ed.exec(Command::DeleteBackward);
assert!(ed.current_selection().is_none());
let out = ed.render();
let menu = out.menu.expect("swap menu should be open");
assert_eq!(menu.items[0].label, "Delete"); assert!(menu.items.len() > 1); assert_eq!(ed.tree.len(root), 1); }
#[test]
fn menu_filters_by_typed_query_but_keeps_delete_pinned() {
let mut ed = Editor::new(NullHost, None);
ed.exec(Command::InsertDelimiters { open: '(', close: ')' });
let root = ed.tree.root();
ed.cursor = Cursor { seq: root, index: 1 };
ed.sel_anchor = None;
ed.exec(Command::DeleteBackward);
ed.exec(Command::InsertText("brace".into()));
let out = ed.render();
let menu = out.menu.expect("menu should still be open while typing");
assert!(menu.items.iter().any(|i| i.label == "Delete")); assert!(menu.items.iter().any(|i| i.label.to_lowercase().contains("brace")));
assert!(!menu.items.iter().any(|i| i.label.contains('[')));
}
#[test]
fn menu_backspace_with_empty_query_confirms_delete() {
let mut ed = Editor::new(NullHost, None);
ed.exec(Command::InsertDelimiters { open: '(', close: ')' });
ed.exec(Command::InsertAtom(sym("x")));
let root = ed.tree.root();
ed.cursor = Cursor { seq: root, index: 1 };
ed.sel_anchor = None;
ed.exec(Command::DeleteBackward); ed.exec(Command::DeleteBackward); assert_eq!(ed.tree.len(root), 0);
assert!(ed.render().menu.is_none());
}
#[test]
fn menu_select_swaps_the_bracket_in_place() {
let mut ed = Editor::new(NullHost, None);
ed.exec(Command::InsertDelimiters { open: '(', close: ')' });
ed.exec(Command::InsertAtom(sym("x")));
let root = ed.tree.root();
ed.cursor = Cursor { seq: root, index: 1 };
ed.sel_anchor = None;
ed.exec(Command::DeleteBackward);
let out = ed.render();
let menu = out.menu.unwrap();
let idx = menu
.items
.iter()
.position(|i| i.label.starts_with("[ ]"))
.expect("bracket alternative present");
ed.exec(Command::MenuSelect(idx));
assert!(ed.render().menu.is_none()); let delim = ed.tree.items(root)[0];
match ed.tree.kind(delim) {
Some(Kind::Delim { open, close, body }) => {
assert_eq!((*open, *close), ('[', ']'));
assert_eq!(ed.tree.len(*body), 1); }
_ => panic!("expected Delim"),
}
}
#[test]
fn menu_escape_cancels_without_changes() {
let mut ed = Editor::new(NullHost, None);
ed.exec(Command::InsertDelimiters { open: '(', close: ')' });
let root = ed.tree.root();
ed.cursor = Cursor { seq: root, index: 1 };
ed.sel_anchor = None;
ed.exec(Command::DeleteBackward); ed.exec(Command::Collapse); assert!(ed.render().menu.is_none());
assert_eq!(ed.tree.len(root), 1); match ed.tree.kind(ed.tree.items(root)[0]) {
Some(Kind::Delim { open, close, .. }) => assert_eq!((*open, *close), ('(', ')')),
_ => panic!("expected Delim"),
}
}
#[test]
fn backspace_next_to_text_block_enters_instead_of_selecting() {
let mut ed = Editor::new(NullHost, None);
let c = ed.exec(Command::InsertStyled(Variant::Text));
let content = c.cursor.seq;
ed.cursor = Cursor { seq: content, index: 0 };
ed.exec(Command::InsertAtom(sym("x")));
let root = ed.tree.root();
ed.cursor = Cursor { seq: root, index: 1 };
ed.sel_anchor = None;
ed.exec(Command::DeleteBackward);
assert!(ed.current_selection().is_none());
assert_eq!(ed.cursor.seq, content);
assert_eq!(ed.tree.len(root), 1);
assert_eq!(ed.tree.len(content), 1);
}
#[test]
fn typing_while_structure_selected_replaces_it() {
let mut ed = Editor::new(NullHost, None);
ed.exec(Command::InsertFraction(FracStyle::Bar));
let root = ed.tree.root();
ed.cursor = Cursor { seq: root, index: 1 };
ed.sel_anchor = None;
ed.exec(Command::DeleteBackward); assert!(ed.current_selection().is_some());
ed.exec(Command::InsertAtom(sym("z")));
assert_eq!(ed.tree.len(root), 1);
assert!(matches!(ed.tree.kind(ed.tree.items(root)[0]), Some(Kind::Atom(_))));
}
#[test]
fn delete_forward_next_to_structure_selects_then_deletes() {
let mut ed = Editor::new(NullHost, None);
ed.exec(Command::InsertFraction(FracStyle::Bar));
let root = ed.tree.root();
ed.cursor = Cursor { seq: root, index: 0 };
ed.sel_anchor = None;
ed.exec(Command::DeleteForward);
assert_eq!(ed.tree.len(root), 1);
let sel = ed.current_selection().expect("first delete-forward should select the structure");
assert_eq!(
(sel.seq, sel.anchor.min(sel.focus), sel.anchor.max(sel.focus)),
(root, 0, 1)
);
ed.exec(Command::DeleteForward);
assert_eq!(ed.tree.len(root), 0);
}
#[test]
fn select_all_then_delete_clears() {
let mut ed = Editor::new(NullHost, None);
ed.exec(Command::InsertText("abc".into()));
ed.exec(Command::SelectAll);
assert!(ed.current_selection().is_some());
ed.exec(Command::DeleteBackward);
assert_eq!(ed.document().len(), 0);
}
#[test]
fn pointer_drag_anchors_at_cursor_not_first_move_target() {
let mut ed = Editor::new(NullHost, None);
ed.exec(Command::InsertText("abcd".into()));
let root = ed.tree.root();
ed.cursor = Cursor { seq: root, index: 1 };
ed.sel_anchor = None;
ed.extend_to(Cursor { seq: root, index: 3 });
assert_eq!(ed.sel_anchor, Some(Cursor { seq: root, index: 1 }));
let sel = ed.current_selection().unwrap();
assert_eq!((sel.anchor, sel.focus), (1, 3));
ed.extend_to(Cursor { seq: root, index: 2 });
let sel = ed.current_selection().unwrap();
assert_eq!((sel.anchor, sel.focus), (1, 2));
}
#[test]
fn pointer_extend_with_existing_selection_keeps_its_anchor() {
let mut ed = Editor::new(NullHost, None);
ed.exec(Command::InsertText("abcd".into()));
let root = ed.tree.root();
ed.cursor = Cursor { seq: root, index: 1 };
ed.sel_anchor = Some(Cursor { seq: root, index: 0 });
ed.extend_to(Cursor { seq: root, index: 4 });
assert_eq!(ed.sel_anchor, Some(Cursor { seq: root, index: 0 }));
let sel = ed.current_selection().unwrap();
assert_eq!((sel.anchor, sel.focus), (0, 4));
}
#[test]
fn pointer_drag_through_a_child_seq_promotes_to_common_ancestor() {
let mut ed = Editor::new(NullHost, None);
ed.exec(Command::InsertFraction(FracStyle::Bar));
let root = ed.tree.root();
let frac = ed.tree.items(root)[0];
let num = ed.tree.child_seqs(frac)[0];
ed.cursor = Cursor { seq: root, index: 0 };
ed.sel_anchor = None;
ed.extend_to(Cursor { seq: num, index: 0 });
assert_eq!(ed.sel_anchor, Some(Cursor { seq: root, index: 0 }));
let sel = ed.current_selection().unwrap();
assert_eq!((sel.seq, sel.anchor, sel.focus), (root, 0, 1));
ed.extend_to(Cursor { seq: root, index: 1 });
let sel = ed.current_selection().unwrap();
assert_eq!((sel.seq, sel.anchor, sel.focus), (root, 0, 1));
}
#[test]
fn pointer_drag_from_inside_a_script_base_past_the_end_selects_everything() {
let mut ed = Editor::new(NullHost, None);
ed.exec(Command::InsertAtom(sym("x")));
ed.exec(Command::InsertScript(ScriptSlot::Sup));
ed.exec(Command::InsertAtom(sym("2")));
let root = ed.tree.root();
ed.cursor = Cursor { seq: root, index: ed.tree.len(root) };
ed.sel_anchor = None;
ed.exec(Command::InsertAtom(sym("+")));
ed.exec(Command::InsertAtom(sym("2")));
assert_eq!(ed.latex(), "x^2+2");
let script = ed.tree.items(root)[0];
let base = match ed.tree.kind(script) {
Some(Kind::Script { base, .. }) => *base,
_ => panic!("expected a Script"),
};
ed.cursor = Cursor { seq: base, index: 1 };
ed.sel_anchor = None;
let end = ed.tree.len(root);
ed.extend_to(Cursor { seq: root, index: end });
let sel = ed.current_selection().unwrap();
assert_eq!(sel.seq, root);
assert_eq!((sel.anchor.min(sel.focus), sel.anchor.max(sel.focus)), (0, end));
}
#[test]
fn arrow_left_at_root_start_exits_left() {
let (log, mut ed) = recording();
ed.exec(Command::InsertText("ab".into()));
let root = ed.tree.root();
ed.cursor = Cursor { seq: root, index: 0 };
ed.sel_anchor = None;
let r = ed.exec(Command::Move(Dir::Left));
assert_eq!(r.exit, Some(ExitDir::Left));
assert_eq!(&*log.exits.borrow(), &[ExitDir::Left]);
assert_eq!(ed.cursor, Cursor { seq: root, index: 0 });
}
#[test]
fn arrow_right_at_root_end_exits_right() {
let (log, mut ed) = recording();
ed.exec(Command::InsertText("ab".into()));
let root = ed.tree.root();
let r = ed.exec(Command::Move(Dir::Right));
assert_eq!(r.exit, Some(ExitDir::Right));
assert_eq!(&*log.exits.borrow(), &[ExitDir::Right]);
assert_eq!(ed.cursor, Cursor { seq: root, index: 2 });
}
#[test]
fn arrows_in_flat_sequence_exit_up_and_down() {
let (log, mut ed) = recording();
ed.exec(Command::InsertText("ab".into()));
let root = ed.tree.root();
ed.cursor = Cursor { seq: root, index: 1 };
ed.sel_anchor = None;
let r = ed.exec(Command::Move(Dir::Up));
assert_eq!(r.exit, Some(ExitDir::Up));
let r = ed.exec(Command::Move(Dir::Down));
assert_eq!(r.exit, Some(ExitDir::Down));
assert_eq!(&*log.exits.borrow(), &[ExitDir::Up, ExitDir::Down]);
assert_eq!(ed.cursor, Cursor { seq: root, index: 1 });
}
#[test]
fn arrow_down_in_numerator_enters_denominator_not_exit() {
let (log, mut ed) = recording();
ed.exec(Command::InsertFraction(FracStyle::Bar));
let frac = ed.tree.items(ed.tree.root())[0];
let den = ed.tree.child_seqs(frac)[1];
let r = ed.exec(Command::Move(Dir::Down));
assert_eq!(r.exit, None);
assert_eq!(ed.cursor.seq, den);
assert!(log.exits.borrow().is_empty());
}
#[test]
fn arrow_at_boundary_with_selection_collapses_without_exit() {
let (log, mut ed) = recording();
ed.exec(Command::InsertText("ab".into()));
let root = ed.tree.root();
ed.cursor = Cursor { seq: root, index: 2 };
ed.sel_anchor = Some(Cursor { seq: root, index: 0 });
let r = ed.exec(Command::Move(Dir::Right));
assert_eq!(r.exit, None);
assert!(log.exits.borrow().is_empty());
assert!(ed.current_selection().is_none());
assert_eq!(ed.cursor, Cursor { seq: root, index: 2 });
}
#[test]
fn delete_on_empty_root_requests_close_without_mutation() {
let (log, mut ed) = recording();
let r = ed.exec(Command::DeleteBackward);
assert_eq!(log.closes.get(), 1);
assert!(r.undo.is_none());
assert!(r.exit.is_none());
assert_eq!(ed.document().len(), 0);
assert_eq!(ed.cursor, Cursor { seq: ed.tree.root(), index: 0 });
let r = ed.exec(Command::DeleteForward);
assert_eq!(log.closes.get(), 2);
assert!(r.undo.is_none());
assert_eq!(ed.document().len(), 0);
}
#[test]
fn enter_from_places_caret_at_matching_edge() {
let (log, mut ed) = recording();
ed.exec(Command::InsertText("ab".into()));
let root = ed.tree.root();
ed.cursor = Cursor { seq: root, index: 2 };
ed.sel_anchor = Some(Cursor { seq: root, index: 0 });
let renders = log.renders.get();
ed.enter_from(ExitDir::Left);
assert_eq!(ed.cursor, Cursor { seq: root, index: 0 });
assert!(ed.sel_anchor.is_none());
assert_eq!(log.renders.get(), renders + 1);
ed.enter_from(ExitDir::Right);
assert_eq!(ed.cursor, Cursor { seq: root, index: 2 });
ed.enter_from(ExitDir::Up);
assert_eq!(ed.cursor, Cursor { seq: root, index: 0 });
ed.enter_from(ExitDir::Down);
assert_eq!(ed.cursor, Cursor { seq: root, index: 2 });
}
#[test]
fn undo_restores_previous_document() {
let mut ed = Editor::new(NullHost, None);
let r1 = ed.exec(Command::InsertAtom(sym("a")));
let r2 = ed.exec(Command::InsertAtom(sym("b")));
assert_eq!(ed.document().len(), 2);
ed.apply_undo(r2.undo.unwrap());
assert_eq!(ed.document().len(), 1);
ed.apply_undo(r1.undo.unwrap());
assert_eq!(ed.document().len(), 0);
}
}