1use crate::command::{Command, CommandResult, Dir, ExitDir};
4use crate::doc::{Document, DocFragment};
5use crate::host::{Host, RenderOutput};
6use crate::menu::{Menu, RowEffect};
7use crate::model::{Cursor, NodeId, Selection, Symbol};
8use crate::{nav, selection};
9
10#[derive(Debug, Clone)]
12pub struct UndoEntry(pub(crate) Document);
13
14impl UndoEntry {
15 pub fn document(&self) -> &Document {
17 &self.0
18 }
19}
20
21pub struct Editor<H: Host> {
23 tree: crate::model::Tree,
24 cursor: Cursor,
25 sel_anchor: Option<Cursor>,
27 pending_menu: Option<Menu>,
29 host: H,
30}
31
32impl<H: Host> Editor<H> {
33 pub fn new(host: H, initial: Option<Document>) -> Self {
35 let tree = match initial {
36 Some(d) => crate::model::Tree::from_doc(&d),
37 None => crate::model::Tree::new(),
38 };
39 let cursor = Cursor {
40 seq: tree.root(),
41 index: 0,
42 };
43 Self {
44 tree,
45 cursor,
46 sel_anchor: None,
47 pending_menu: None,
48 host,
49 }
50 }
51
52 pub fn cursor(&self) -> Cursor {
54 self.cursor
55 }
56
57 pub fn matrix_shape(&self) -> Option<(usize, usize)> {
59 self.tree.matrix_shape_at(self.cursor)
60 }
61
62 pub fn current_selection(&self) -> Option<Selection> {
64 self.sel_anchor
65 .filter(|a| a.seq == self.cursor.seq && a.index != self.cursor.index)
66 .map(|a| Selection {
67 seq: self.cursor.seq,
68 anchor: a.index,
69 focus: self.cursor.index,
70 })
71 }
72
73 pub fn exec(&mut self, cmd: Command) -> CommandResult {
75 if self.pending_menu.is_some() {
76 if let Some(result) = self.exec_menu(&cmd) {
77 return result;
78 }
79 self.pending_menu = None;
81 }
82 match cmd {
83 Command::Move(Dir::Left) => {
84 if let Some(token) = self.host_box_neighbor(false) {
86 if self.host.on_object_enter(token, crate::command::Side::After) {
87 return self.moved(self.cursor);
88 }
89 }
90 match nav::move_left(&self.tree, self.cursor) {
91 Some(c) => self.moved(c),
92 None => self.boundary(ExitDir::Left),
93 }
94 }
95 Command::Move(Dir::Right) => {
96 if let Some(token) = self.host_box_neighbor(true) {
97 if self.host.on_object_enter(token, crate::command::Side::Before) {
98 return self.moved(self.cursor);
99 }
100 }
101 match nav::move_right(&self.tree, self.cursor) {
102 Some(c) => self.moved(c),
103 None => self.boundary(ExitDir::Right),
104 }
105 }
106 Command::Move(Dir::Up) => match nav::vertical(&self.tree, self.cursor, true) {
107 Some(c) => self.moved(c),
108 None => self.vertical_boundary(true),
109 },
110 Command::Move(Dir::Down) => match nav::vertical(&self.tree, self.cursor, false) {
111 Some(c) => self.moved(c),
112 None => self.vertical_boundary(false),
113 },
114 Command::MoveLineStart => self.moved(Cursor {
115 seq: self.cursor.seq,
116 index: 0,
117 }),
118 Command::MoveLineEnd => self.moved(Cursor {
119 seq: self.cursor.seq,
120 index: self.tree.len(self.cursor.seq),
121 }),
122 Command::Tab => match self.next_fill_target(true) {
123 Some(c) => self.moved(c),
124 None => self.exit_result(ExitDir::Right),
125 },
126 Command::ShiftTab => match self.next_fill_target(false) {
127 Some(c) => self.moved(c),
128 None => self.exit_result(ExitDir::Left),
129 },
130 Command::MoveTo(p) => {
131 let (latex, spans) = self.tree.export_latex();
132 let ir = self.host.typeset(&latex);
133 let c = crate::matcher::hit_test(&ir, &spans, &self.tree, p);
134 self.moved(crate::nav::normalize(&self.tree, c))
135 }
136 Command::ExtendTo(p) => {
137 let (latex, spans) = self.tree.export_latex();
138 let ir = self.host.typeset(&latex);
139 let target = crate::nav::normalize(
140 &self.tree,
141 crate::matcher::hit_test(&ir, &spans, &self.tree, p),
142 );
143 self.extend_to(target)
144 }
145
146 Command::Extend(Dir::Left) => self.extend(false),
147 Command::Extend(Dir::Right) => self.extend(true),
148 Command::Extend(Dir::Up) | Command::Extend(Dir::Down) => self.no_op(),
149 Command::SelectAll => {
150 let s = selection::select_all(&self.tree);
151 self.cursor = Cursor {
152 seq: s.seq,
153 index: s.focus,
154 };
155 self.sel_anchor = Some(Cursor { seq: s.seq, index: s.anchor });
156 self.no_op()
157 }
158 Command::Collapse => {
159 self.sel_anchor = None;
160 self.no_op()
161 }
162 Command::Confirm => {
164 self.sel_anchor = None;
165 self.no_op()
166 }
167 Command::MenuSelect(_) => self.no_op(),
168
169 Command::InsertAtom(sym) => {
170 let before = self.snapshot();
171 self.replace_selection();
172 let c = self.tree.insert_atom(self.cursor, sym);
173 self.after_edit(before, c)
174 }
175 Command::InsertHostBox(token) => {
176 let before = self.snapshot();
177 self.replace_selection();
178 let c = self.tree.insert_host_box(self.cursor, token);
179 self.after_edit(before, c)
180 }
181 Command::InsertText(text) => {
182 let before = self.snapshot();
183 self.replace_selection();
184 let mut c = self.cursor;
185 for ch in text.chars() {
186 c = self.tree.insert_atom(c, Symbol::from_char(ch));
187 }
188 self.after_edit(before, c)
189 }
190 Command::InsertFraction(style) => {
191 let before = self.snapshot();
192 let sel = self.current_selection();
193 let c = self.tree.insert_fraction(self.cursor, style, sel);
194 self.after_edit(before, c)
195 }
196 Command::InsertScript(slot) => {
197 if let Some(c) = self.tree.bigop_limit_target(self.cursor, slot) {
199 self.moved(c)
200 } else {
201 let before = self.snapshot();
202 let c = self.tree.attach_script(self.cursor, slot);
203 self.after_edit(before, c)
204 }
205 }
206 Command::InsertBigOp(op) => {
207 let before = self.snapshot();
208 let c = self.tree.insert_big_op(self.cursor, op);
209 self.after_edit(before, c)
210 }
211 Command::InsertSqrt => {
212 let before = self.snapshot();
213 let sel = self.current_selection();
214 let c = self.tree.insert_sqrt(self.cursor, sel);
215 self.after_edit(before, c)
216 }
217 Command::InsertDelimiters { open, close } => {
218 let before = self.snapshot();
219 let sel = self.current_selection();
220 let c = self.tree.insert_delimiters(self.cursor, open, close, sel);
221 self.after_edit(before, c)
222 }
223 Command::InsertAccent(mark) => {
224 let before = self.snapshot();
225 let sel = self.current_selection();
226 let c = self.tree.insert_accent(self.cursor, mark, sel);
227 self.after_edit(before, c)
228 }
229 Command::InsertUnderOver(spec) => {
230 let before = self.snapshot();
231 let sel = self.current_selection();
232 let c = self.tree.insert_under_over(self.cursor, spec, sel);
233 self.after_edit(before, c)
234 }
235 Command::InsertStyled(variant) | Command::ApplyStyle(variant) => {
236 let before = self.snapshot();
237 let sel = self.current_selection();
238 let c = self.tree.insert_styled(self.cursor, variant, sel);
239 self.after_edit(before, c)
240 }
241 Command::InsertMatrix { env, rows, cols } => {
242 let before = self.snapshot();
243 self.replace_selection();
244 let c = self.tree.insert_matrix(self.cursor, env, rows, cols);
245 self.after_edit(before, c)
246 }
247 Command::InsertFragment(frag) => {
248 let before = self.snapshot();
249 self.replace_selection();
250 let c = self.tree.insert_fragment(self.cursor, &frag);
251 self.after_edit(before, c)
252 }
253
254 Command::DeleteBackward => {
255 if let Some(s) = self.current_selection() {
256 let before = self.snapshot();
257 let c = self.tree.delete_selection(s);
258 self.after_edit(before, c)
259 } else if let Some(node) = nav::adjacent_structure_backward(&self.tree, self.cursor) {
260 self.trigger_structure_backspace(node)
262 } else if self.cursor.seq == self.tree.root() && self.tree.is_empty(self.cursor.seq)
263 {
264 self.close_result()
265 } else {
266 let before = self.snapshot();
267 let c = self.tree.delete_backward(self.cursor);
268 self.after_edit(before, c)
269 }
270 }
271 Command::DeleteForward => {
272 if let Some(s) = self.current_selection() {
273 let before = self.snapshot();
274 let c = self.tree.delete_selection(s);
275 self.after_edit(before, c)
276 } else if let Some(node) = nav::adjacent_structure_forward(&self.tree, self.cursor) {
277 self.trigger_structure_backspace(node)
278 } else if self.cursor.seq == self.tree.root() && self.tree.is_empty(self.cursor.seq)
279 {
280 self.close_result()
281 } else {
282 let before = self.snapshot();
283 let c = self.tree.delete_forward(self.cursor);
284 self.after_edit(before, c)
285 }
286 }
287
288 Command::MatrixInsertRow(side) => {
289 let before = self.snapshot();
290 let c = self.tree.matrix_insert_row(self.cursor, side);
291 self.after_edit(before, c)
292 }
293 Command::MatrixDeleteRow => {
294 let before = self.snapshot();
295 let c = self.tree.matrix_delete_row(self.cursor);
296 self.after_edit(before, c)
297 }
298 Command::MatrixInsertCol(side) => {
299 let before = self.snapshot();
300 let c = self.tree.matrix_insert_col(self.cursor, side);
301 self.after_edit(before, c)
302 }
303 Command::MatrixDeleteCol => {
304 let before = self.snapshot();
305 let c = self.tree.matrix_delete_col(self.cursor);
306 self.after_edit(before, c)
307 }
308 }
309 }
310
311 pub fn render(&self) -> RenderOutput {
313 let (latex, spans) = self.tree.export_latex();
314 let ir = self.host.typeset(&latex);
315 crate::matcher::render(
316 &self.tree,
317 self.cursor,
318 self.current_selection(),
319 &spans,
320 ir,
321 self.pending_menu.as_ref(),
322 )
323 }
324
325 pub fn selection_latex(&self) -> String {
327 match self.current_selection() {
328 Some(s) => self.tree.selection_latex(s),
329 None => String::new(),
330 }
331 }
332 pub fn selection_fragment(&self) -> DocFragment {
334 match self.current_selection() {
335 Some(s) => {
336 let lo = s.anchor.min(s.focus);
337 let hi = s.anchor.max(s.focus);
338 self.tree
339 .to_doc()
340 .root()
341 .get(lo..hi)
342 .map(<[_]>::to_vec)
343 .unwrap_or_default()
344 }
345 None => Vec::new(),
346 }
347 }
348
349 pub fn apply_undo(&mut self, entry: UndoEntry) -> UndoEntry {
351 let current = self.tree.to_doc();
352 self.tree = crate::model::Tree::from_doc(&entry.0);
353 self.cursor = Cursor {
354 seq: self.tree.root(),
355 index: self.tree.len(self.tree.root()),
356 };
357 self.sel_anchor = None;
358 self.host.on_render();
359 UndoEntry(current)
360 }
361
362 pub fn enter_from(&mut self, side: ExitDir) {
364 let root = self.tree.root();
365 let index = match side {
366 ExitDir::Left | ExitDir::Up => 0,
367 ExitDir::Right | ExitDir::Down => self.tree.len(root),
368 };
369 self.cursor = Cursor { seq: root, index };
370 self.sel_anchor = None;
371 self.pending_menu = None;
372 self.host.on_render();
373 }
374
375 pub fn document(&self) -> Document {
377 self.tree.to_doc()
378 }
379
380 pub fn latex(&self) -> String {
382 self.tree.export_display()
383 }
384
385 fn snapshot(&self) -> Document {
386 self.tree.to_doc()
387 }
388
389 fn host_box_neighbor(&self, right: bool) -> Option<u32> {
391 let items = self.tree.items(self.cursor.seq);
392 let node = if right {
393 *items.get(self.cursor.index)?
394 } else {
395 *items.get(self.cursor.index.checked_sub(1)?)?
396 };
397 match self.tree.kind(node) {
398 Some(crate::model::Kind::HostBox { token }) => Some(*token),
399 _ => None,
400 }
401 }
402
403 fn replace_selection(&mut self) {
405 if let Some(s) = self.current_selection() {
406 self.cursor = self.tree.delete_selection(s);
407 self.sel_anchor = None;
408 }
409 }
410
411 fn select_node(&mut self, node: NodeId) -> CommandResult {
413 let Some((seq, idx)) = self.tree.index_in_parent(node) else {
414 return self.no_op();
415 };
416 self.cursor = Cursor { seq, index: idx + 1 };
417 self.sel_anchor = Some(Cursor { seq, index: idx });
418 self.no_op()
419 }
420
421 fn trigger_structure_backspace(&mut self, node: NodeId) -> CommandResult {
423 match self.tree.swap_variants(node) {
424 Some(variants) => {
425 self.pending_menu = Some(Menu::for_node(node, variants));
426 self.no_op()
427 }
428 None => self.select_node(node),
429 }
430 }
431
432 fn exec_menu(&mut self, cmd: &Command) -> Option<CommandResult> {
434 match cmd {
435 Command::InsertAtom(sym) if sym.latex.chars().count() == 1 => {
436 let menu = self.pending_menu.as_mut()?;
437 menu.query.push_str(&sym.latex);
438 menu.selected = 0;
439 Some(self.no_op())
440 }
441 Command::InsertText(text) => {
442 let menu = self.pending_menu.as_mut()?;
443 menu.query.push_str(text);
444 menu.selected = 0;
445 Some(self.no_op())
446 }
447 Command::DeleteBackward => {
448 let menu = self.pending_menu.as_mut()?;
449 if menu.query.is_empty() {
450 let selected = menu.selected;
451 Some(self.commit_menu_row(selected))
452 } else {
453 menu.query.pop();
454 menu.selected = 0;
455 Some(self.no_op())
456 }
457 }
458 Command::Move(Dir::Up) => {
459 let menu = self.pending_menu.as_mut()?;
460 menu.selected = menu.selected.saturating_sub(1);
461 Some(self.no_op())
462 }
463 Command::Move(Dir::Down) => {
464 let menu = self.pending_menu.as_mut()?;
465 let max = menu.visible().len().saturating_sub(1);
466 menu.selected = (menu.selected + 1).min(max);
467 Some(self.no_op())
468 }
469 Command::Confirm => {
470 let selected = self.pending_menu.as_ref()?.selected;
471 Some(self.commit_menu_row(selected))
472 }
473 Command::MenuSelect(i) => Some(self.commit_menu_row(*i)),
474 Command::Collapse => {
475 self.pending_menu = None;
476 Some(self.no_op())
477 }
478 _ => None,
479 }
480 }
481
482 fn commit_menu_row(&mut self, visible_idx: usize) -> CommandResult {
484 let Some(menu) = self.pending_menu.take() else {
485 return self.no_op();
486 };
487 let visible = menu.visible();
488 let Some(row) = visible.get(visible_idx) else {
489 return self.no_op();
490 };
491 match row {
492 RowEffect::Delete => {
493 let before = self.snapshot();
494 let Some((seq, idx)) = self.tree.index_in_parent(menu.anchor) else {
495 return self.no_op();
496 };
497 let c = self.tree.delete_selection(Selection { seq, anchor: idx, focus: idx + 1 });
498 self.after_edit(before, c)
499 }
500 RowEffect::Swap(variant) => {
501 let before = self.snapshot();
502 self.tree.apply_swap(menu.anchor, variant);
503 self.after_edit(before, self.cursor)
504 }
505 }
506 }
507
508 fn extend(&mut self, right: bool) -> CommandResult {
509 let sel = self.current_selection().unwrap_or(Selection {
510 seq: self.cursor.seq,
511 anchor: self.cursor.index,
512 focus: self.cursor.index,
513 });
514 let ns = selection::extend(&self.tree, sel, right);
515 self.cursor = Cursor {
516 seq: ns.seq,
517 index: ns.focus,
518 };
519 self.sel_anchor = Some(Cursor { seq: ns.seq, index: ns.anchor });
520 self.no_op()
521 }
522
523 fn next_fill_target(&self, forward: bool) -> Option<Cursor> {
524 if forward {
526 if let Some(parent) = self.tree.seq_parent(self.cursor.seq) {
528 if let Some(crate::model::Kind::BigOp { lower, upper, .. }) =
529 self.tree.kind(parent)
530 {
531 if self.cursor.seq == *lower && self.tree.is_empty(*upper) {
532 return Some(Cursor {
533 seq: *upper,
534 index: 0,
535 });
536 }
537 }
538 }
539 if self.cursor.index > 0 {
541 let prev = self.tree.items(self.cursor.seq)[self.cursor.index - 1];
542 if let Some(crate::model::Kind::BigOp { lower, upper, .. }) =
543 self.tree.kind(prev)
544 {
545 for s in [*lower, *upper] {
546 if self.tree.is_empty(s) {
547 return Some(Cursor { seq: s, index: 0 });
548 }
549 }
550 }
551 }
552 if let Some(parent) = self.tree.seq_parent(self.cursor.seq) {
554 if let Some(crate::model::Kind::Sqrt { index, radicand }) = self.tree.kind(parent) {
555 if self.cursor.seq == *radicand && self.tree.is_empty(*index) {
556 return Some(Cursor {
557 seq: *index,
558 index: 0,
559 });
560 }
561 }
562 }
563 }
564 let mut c = self.cursor;
565 for _ in 0..100_000 {
566 c = if forward {
567 nav::move_right(&self.tree, c)?
568 } else {
569 nav::move_left(&self.tree, c)?
570 };
571 if self.tree.is_empty(c.seq) {
572 return Some(Cursor {
573 seq: c.seq,
574 index: 0,
575 });
576 }
577 }
578 None
579 }
580
581 fn after_edit(&mut self, before: Document, cursor: Cursor) -> CommandResult {
583 self.cursor = cursor;
584 self.sel_anchor = None;
585 let doc = self.tree.to_doc();
586 self.host.on_change(&doc);
587 self.host.on_render();
588 CommandResult {
589 cursor,
590 undo: Some(UndoEntry(before)),
591 exit: None,
592 }
593 }
594
595 fn extend_to(&mut self, target: Cursor) -> CommandResult {
597 let anchor = self.sel_anchor.unwrap_or(self.cursor);
598 let sel = selection::extend_to(&self.tree, anchor, target);
599 self.sel_anchor = Some(Cursor { seq: sel.seq, index: sel.anchor });
600 self.cursor = Cursor { seq: sel.seq, index: sel.focus };
601 self.no_op()
602 }
603
604 fn moved(&mut self, cursor: Cursor) -> CommandResult {
606 self.cursor = cursor;
607 self.sel_anchor = None;
608 CommandResult {
609 cursor,
610 undo: None,
611 exit: None,
612 }
613 }
614
615 fn no_op(&self) -> CommandResult {
617 CommandResult {
618 cursor: self.cursor,
619 undo: None,
620 exit: None,
621 }
622 }
623
624 fn boundary(&mut self, dir: ExitDir) -> CommandResult {
626 if self.current_selection().is_some() {
627 return self.moved(self.cursor);
628 }
629 self.exit_result(dir)
630 }
631
632 fn vertical_boundary(&mut self, up: bool) -> CommandResult {
634 if self.current_selection().is_some() {
635 return self.moved(self.cursor);
636 }
637 let dir = if up { ExitDir::Up } else { ExitDir::Down };
638 let mut seq = self.cursor.seq;
639 loop {
640 let Some(parent) = self.tree.seq_parent(seq) else {
641 return self.exit_result(dir);
642 };
643 let Some((pseq, _)) = self.tree.index_in_parent(parent) else {
644 return self.exit_result(dir);
645 };
646 if nav::vertical(&self.tree, Cursor { seq: pseq, index: 0 }, up).is_some() {
647 return self.moved(self.cursor);
648 }
649 seq = pseq;
650 }
651 }
652
653 fn exit_result(&self, dir: ExitDir) -> CommandResult {
654 self.host.on_exit(dir);
655 CommandResult {
656 cursor: self.cursor,
657 undo: None,
658 exit: Some(dir),
659 }
660 }
661
662 fn close_result(&self) -> CommandResult {
664 self.host.request_close();
665 CommandResult {
666 cursor: self.cursor,
667 undo: None,
668 exit: None,
669 }
670 }
671}
672
673#[cfg(test)]
674mod tests {
675 use super::*;
676 use crate::command::Command;
677 use crate::host::Host;
678 use crate::model::{FracStyle, Kind, MathClass, ScriptSlot, Symbol, Variant};
679
680 struct NullHost;
681 impl Host for NullHost {
682 fn typeset(&self, _latex: &str) -> mathtex_ir::Fragment {
683 mathtex_ir::Fragment::default()
684 }
685 fn on_change(&self, _doc: &Document) {}
686 fn on_exit(&self, _dir: ExitDir) {}
687 fn request_close(&self) {}
688 fn on_render(&self) {}
689 }
690
691 #[derive(Default)]
692 struct HostLog {
693 exits: std::cell::RefCell<Vec<ExitDir>>,
694 closes: std::cell::Cell<usize>,
695 renders: std::cell::Cell<usize>,
696 }
697
698 struct RecordingHost(std::rc::Rc<HostLog>);
699 impl Host for RecordingHost {
700 fn typeset(&self, _latex: &str) -> mathtex_ir::Fragment {
701 mathtex_ir::Fragment::default()
702 }
703 fn on_change(&self, _doc: &Document) {}
704 fn on_exit(&self, dir: ExitDir) {
705 self.0.exits.borrow_mut().push(dir);
706 }
707 fn request_close(&self) {
708 self.0.closes.set(self.0.closes.get() + 1);
709 }
710 fn on_render(&self) {
711 self.0.renders.set(self.0.renders.get() + 1);
712 }
713 }
714
715 fn recording() -> (std::rc::Rc<HostLog>, Editor<RecordingHost>) {
716 let log = std::rc::Rc::new(HostLog::default());
717 let ed = Editor::new(RecordingHost(log.clone()), None);
718 (log, ed)
719 }
720
721 fn sym(c: &str) -> Symbol {
722 Symbol {
723 latex: c.into(),
724 class: MathClass::Ord,
725 }
726 }
727
728 #[test]
729 fn insert_fraction_and_navigate() {
730 let mut ed = Editor::new(NullHost, None);
731 ed.exec(Command::InsertFraction(FracStyle::Bar));
732 assert!(ed.tree.is_empty(ed.cursor.seq));
734 ed.exec(Command::InsertAtom(sym("x")));
735 assert_eq!(ed.tree.len(ed.cursor.seq), 1);
737 }
738
739 #[test]
740 fn backspace_next_to_structure_selects_then_deletes() {
741 let mut ed = Editor::new(NullHost, None);
742 ed.exec(Command::InsertFraction(FracStyle::Bar));
743 ed.exec(Command::InsertAtom(sym("a")));
744 let root = ed.tree.root();
745 let frac = ed.tree.items(root)[0];
746 let den = ed.tree.child_seqs(frac)[1];
747 ed.cursor = Cursor { seq: den, index: 0 };
748 ed.exec(Command::InsertAtom(sym("b"))); ed.cursor = Cursor { seq: root, index: 1 };
751 ed.sel_anchor = None;
752
753 ed.exec(Command::DeleteBackward);
755 assert_eq!(ed.tree.len(root), 1);
756 let sel = ed.current_selection().expect("first backspace should select the structure");
757 assert_eq!((sel.seq, sel.anchor, sel.focus), (root, 0, 1));
758
759 ed.exec(Command::DeleteBackward);
761 assert_eq!(ed.tree.len(root), 0);
762 }
763
764 #[test]
765 fn backspace_next_to_delim_opens_swap_menu() {
766 let mut ed = Editor::new(NullHost, None);
767 ed.exec(Command::InsertDelimiters { open: '(', close: ')' });
768 let root = ed.tree.root();
769 ed.cursor = Cursor { seq: root, index: 1 };
770 ed.sel_anchor = None;
771
772 ed.exec(Command::DeleteBackward);
773 assert!(ed.current_selection().is_none());
775 let out = ed.render();
776 let menu = out.menu.expect("swap menu should be open");
777 assert_eq!(menu.items[0].label, "Delete"); assert!(menu.items.len() > 1); assert_eq!(ed.tree.len(root), 1); }
781
782 #[test]
783 fn menu_filters_by_typed_query_but_keeps_delete_pinned() {
784 let mut ed = Editor::new(NullHost, None);
785 ed.exec(Command::InsertDelimiters { open: '(', close: ')' });
786 let root = ed.tree.root();
787 ed.cursor = Cursor { seq: root, index: 1 };
788 ed.sel_anchor = None;
789 ed.exec(Command::DeleteBackward); ed.exec(Command::InsertText("brace".into()));
792 let out = ed.render();
793 let menu = out.menu.expect("menu should still be open while typing");
794 assert!(menu.items.iter().any(|i| i.label == "Delete")); assert!(menu.items.iter().any(|i| i.label.to_lowercase().contains("brace")));
796 assert!(!menu.items.iter().any(|i| i.label.contains('[')));
797 }
798
799 #[test]
800 fn menu_backspace_with_empty_query_confirms_delete() {
801 let mut ed = Editor::new(NullHost, None);
802 ed.exec(Command::InsertDelimiters { open: '(', close: ')' });
803 ed.exec(Command::InsertAtom(sym("x")));
804 let root = ed.tree.root();
805 ed.cursor = Cursor { seq: root, index: 1 };
806 ed.sel_anchor = None;
807 ed.exec(Command::DeleteBackward); ed.exec(Command::DeleteBackward); assert_eq!(ed.tree.len(root), 0);
810 assert!(ed.render().menu.is_none());
811 }
812
813 #[test]
814 fn menu_select_swaps_the_bracket_in_place() {
815 let mut ed = Editor::new(NullHost, None);
816 ed.exec(Command::InsertDelimiters { open: '(', close: ')' });
817 ed.exec(Command::InsertAtom(sym("x")));
818 let root = ed.tree.root();
819 ed.cursor = Cursor { seq: root, index: 1 };
820 ed.sel_anchor = None;
821 ed.exec(Command::DeleteBackward); let out = ed.render();
824 let menu = out.menu.unwrap();
825 let idx = menu
826 .items
827 .iter()
828 .position(|i| i.label.starts_with("[ ]"))
829 .expect("bracket alternative present");
830 ed.exec(Command::MenuSelect(idx));
831
832 assert!(ed.render().menu.is_none()); let delim = ed.tree.items(root)[0];
834 match ed.tree.kind(delim) {
835 Some(Kind::Delim { open, close, body }) => {
836 assert_eq!((*open, *close), ('[', ']'));
837 assert_eq!(ed.tree.len(*body), 1); }
839 _ => panic!("expected Delim"),
840 }
841 }
842
843 #[test]
844 fn menu_escape_cancels_without_changes() {
845 let mut ed = Editor::new(NullHost, None);
846 ed.exec(Command::InsertDelimiters { open: '(', close: ')' });
847 let root = ed.tree.root();
848 ed.cursor = Cursor { seq: root, index: 1 };
849 ed.sel_anchor = None;
850 ed.exec(Command::DeleteBackward); ed.exec(Command::Collapse); assert!(ed.render().menu.is_none());
853 assert_eq!(ed.tree.len(root), 1); match ed.tree.kind(ed.tree.items(root)[0]) {
855 Some(Kind::Delim { open, close, .. }) => assert_eq!((*open, *close), ('(', ')')),
856 _ => panic!("expected Delim"),
857 }
858 }
859
860 #[test]
861 fn backspace_next_to_text_block_enters_instead_of_selecting() {
862 let mut ed = Editor::new(NullHost, None);
863 let c = ed.exec(Command::InsertStyled(Variant::Text));
864 let content = c.cursor.seq;
865 ed.cursor = Cursor { seq: content, index: 0 };
866 ed.exec(Command::InsertAtom(sym("x")));
867 let root = ed.tree.root();
868 ed.cursor = Cursor { seq: root, index: 1 };
869 ed.sel_anchor = None;
870
871 ed.exec(Command::DeleteBackward);
872 assert!(ed.current_selection().is_none());
874 assert_eq!(ed.cursor.seq, content);
875 assert_eq!(ed.tree.len(root), 1);
876 assert_eq!(ed.tree.len(content), 1);
877 }
878
879 #[test]
880 fn typing_while_structure_selected_replaces_it() {
881 let mut ed = Editor::new(NullHost, None);
882 ed.exec(Command::InsertFraction(FracStyle::Bar));
883 let root = ed.tree.root();
884 ed.cursor = Cursor { seq: root, index: 1 };
885 ed.sel_anchor = None;
886 ed.exec(Command::DeleteBackward); assert!(ed.current_selection().is_some());
888
889 ed.exec(Command::InsertAtom(sym("z")));
890 assert_eq!(ed.tree.len(root), 1);
891 assert!(matches!(ed.tree.kind(ed.tree.items(root)[0]), Some(Kind::Atom(_))));
892 }
893
894 #[test]
895 fn delete_forward_next_to_structure_selects_then_deletes() {
896 let mut ed = Editor::new(NullHost, None);
897 ed.exec(Command::InsertFraction(FracStyle::Bar));
898 let root = ed.tree.root();
899 ed.cursor = Cursor { seq: root, index: 0 };
900 ed.sel_anchor = None;
901
902 ed.exec(Command::DeleteForward);
903 assert_eq!(ed.tree.len(root), 1);
904 let sel = ed.current_selection().expect("first delete-forward should select the structure");
905 assert_eq!(
906 (sel.seq, sel.anchor.min(sel.focus), sel.anchor.max(sel.focus)),
907 (root, 0, 1)
908 );
909
910 ed.exec(Command::DeleteForward);
911 assert_eq!(ed.tree.len(root), 0);
912 }
913
914 #[test]
915 fn select_all_then_delete_clears() {
916 let mut ed = Editor::new(NullHost, None);
917 ed.exec(Command::InsertText("abc".into()));
918 ed.exec(Command::SelectAll);
919 assert!(ed.current_selection().is_some());
920 ed.exec(Command::DeleteBackward);
921 assert_eq!(ed.document().len(), 0);
922 }
923
924 #[test]
925 fn pointer_drag_anchors_at_cursor_not_first_move_target() {
926 let mut ed = Editor::new(NullHost, None);
927 ed.exec(Command::InsertText("abcd".into()));
928 let root = ed.tree.root();
929 ed.cursor = Cursor { seq: root, index: 1 };
931 ed.sel_anchor = None;
932 ed.extend_to(Cursor { seq: root, index: 3 });
934 assert_eq!(ed.sel_anchor, Some(Cursor { seq: root, index: 1 }));
935 let sel = ed.current_selection().unwrap();
936 assert_eq!((sel.anchor, sel.focus), (1, 3));
937 ed.extend_to(Cursor { seq: root, index: 2 });
939 let sel = ed.current_selection().unwrap();
940 assert_eq!((sel.anchor, sel.focus), (1, 2));
941 }
942
943 #[test]
944 fn pointer_extend_with_existing_selection_keeps_its_anchor() {
945 let mut ed = Editor::new(NullHost, None);
947 ed.exec(Command::InsertText("abcd".into()));
948 let root = ed.tree.root();
949 ed.cursor = Cursor { seq: root, index: 1 };
950 ed.sel_anchor = Some(Cursor { seq: root, index: 0 });
951 ed.extend_to(Cursor { seq: root, index: 4 });
952 assert_eq!(ed.sel_anchor, Some(Cursor { seq: root, index: 0 }));
953 let sel = ed.current_selection().unwrap();
954 assert_eq!((sel.anchor, sel.focus), (0, 4));
955 }
956
957 #[test]
958 fn pointer_drag_through_a_child_seq_promotes_to_common_ancestor() {
959 let mut ed = Editor::new(NullHost, None);
961 ed.exec(Command::InsertFraction(FracStyle::Bar));
962 let root = ed.tree.root();
963 let frac = ed.tree.items(root)[0];
964 let num = ed.tree.child_seqs(frac)[0];
965 ed.cursor = Cursor { seq: root, index: 0 };
967 ed.sel_anchor = None;
968 ed.extend_to(Cursor { seq: num, index: 0 });
970 assert_eq!(ed.sel_anchor, Some(Cursor { seq: root, index: 0 }));
971 let sel = ed.current_selection().unwrap();
972 assert_eq!((sel.seq, sel.anchor, sel.focus), (root, 0, 1));
973 ed.extend_to(Cursor { seq: root, index: 1 });
975 let sel = ed.current_selection().unwrap();
976 assert_eq!((sel.seq, sel.anchor, sel.focus), (root, 0, 1));
977 }
978
979 #[test]
980 fn pointer_drag_from_inside_a_script_base_past_the_end_selects_everything() {
981 let mut ed = Editor::new(NullHost, None);
983 ed.exec(Command::InsertAtom(sym("x")));
984 ed.exec(Command::InsertScript(ScriptSlot::Sup));
985 ed.exec(Command::InsertAtom(sym("2")));
986 let root = ed.tree.root();
987 ed.cursor = Cursor { seq: root, index: ed.tree.len(root) };
989 ed.sel_anchor = None;
990 ed.exec(Command::InsertAtom(sym("+")));
991 ed.exec(Command::InsertAtom(sym("2")));
992 assert_eq!(ed.latex(), "x^2+2");
993
994 let script = ed.tree.items(root)[0];
995 let base = match ed.tree.kind(script) {
996 Some(Kind::Script { base, .. }) => *base,
997 _ => panic!("expected a Script"),
998 };
999 ed.cursor = Cursor { seq: base, index: 1 };
1001 ed.sel_anchor = None;
1002 let end = ed.tree.len(root);
1004 ed.extend_to(Cursor { seq: root, index: end });
1005 let sel = ed.current_selection().unwrap();
1006 assert_eq!(sel.seq, root);
1007 assert_eq!((sel.anchor.min(sel.focus), sel.anchor.max(sel.focus)), (0, end));
1008 }
1009
1010 #[test]
1011 fn arrow_left_at_root_start_exits_left() {
1012 let (log, mut ed) = recording();
1013 ed.exec(Command::InsertText("ab".into()));
1014 let root = ed.tree.root();
1015 ed.cursor = Cursor { seq: root, index: 0 };
1016 ed.sel_anchor = None;
1017 let r = ed.exec(Command::Move(Dir::Left));
1018 assert_eq!(r.exit, Some(ExitDir::Left));
1019 assert_eq!(&*log.exits.borrow(), &[ExitDir::Left]);
1020 assert_eq!(ed.cursor, Cursor { seq: root, index: 0 });
1021 }
1022
1023 #[test]
1024 fn arrow_right_at_root_end_exits_right() {
1025 let (log, mut ed) = recording();
1026 ed.exec(Command::InsertText("ab".into()));
1027 let root = ed.tree.root();
1028 let r = ed.exec(Command::Move(Dir::Right));
1029 assert_eq!(r.exit, Some(ExitDir::Right));
1030 assert_eq!(&*log.exits.borrow(), &[ExitDir::Right]);
1031 assert_eq!(ed.cursor, Cursor { seq: root, index: 2 });
1032 }
1033
1034 #[test]
1035 fn arrows_in_flat_sequence_exit_up_and_down() {
1036 let (log, mut ed) = recording();
1037 ed.exec(Command::InsertText("ab".into()));
1038 let root = ed.tree.root();
1039 ed.cursor = Cursor { seq: root, index: 1 };
1040 ed.sel_anchor = None;
1041 let r = ed.exec(Command::Move(Dir::Up));
1042 assert_eq!(r.exit, Some(ExitDir::Up));
1043 let r = ed.exec(Command::Move(Dir::Down));
1044 assert_eq!(r.exit, Some(ExitDir::Down));
1045 assert_eq!(&*log.exits.borrow(), &[ExitDir::Up, ExitDir::Down]);
1046 assert_eq!(ed.cursor, Cursor { seq: root, index: 1 });
1047 }
1048
1049 #[test]
1050 fn arrow_down_in_numerator_enters_denominator_not_exit() {
1051 let (log, mut ed) = recording();
1052 ed.exec(Command::InsertFraction(FracStyle::Bar));
1053 let frac = ed.tree.items(ed.tree.root())[0];
1054 let den = ed.tree.child_seqs(frac)[1];
1055 let r = ed.exec(Command::Move(Dir::Down));
1056 assert_eq!(r.exit, None);
1057 assert_eq!(ed.cursor.seq, den);
1058 assert!(log.exits.borrow().is_empty());
1059 }
1060
1061 #[test]
1062 fn arrow_at_boundary_with_selection_collapses_without_exit() {
1063 let (log, mut ed) = recording();
1064 ed.exec(Command::InsertText("ab".into()));
1065 let root = ed.tree.root();
1066 ed.cursor = Cursor { seq: root, index: 2 };
1067 ed.sel_anchor = Some(Cursor { seq: root, index: 0 });
1068 let r = ed.exec(Command::Move(Dir::Right));
1069 assert_eq!(r.exit, None);
1070 assert!(log.exits.borrow().is_empty());
1071 assert!(ed.current_selection().is_none());
1072 assert_eq!(ed.cursor, Cursor { seq: root, index: 2 });
1073 }
1074
1075 #[test]
1076 fn delete_on_empty_root_requests_close_without_mutation() {
1077 let (log, mut ed) = recording();
1078 let r = ed.exec(Command::DeleteBackward);
1079 assert_eq!(log.closes.get(), 1);
1080 assert!(r.undo.is_none());
1081 assert!(r.exit.is_none());
1082 assert_eq!(ed.document().len(), 0);
1083 assert_eq!(ed.cursor, Cursor { seq: ed.tree.root(), index: 0 });
1084 let r = ed.exec(Command::DeleteForward);
1085 assert_eq!(log.closes.get(), 2);
1086 assert!(r.undo.is_none());
1087 assert_eq!(ed.document().len(), 0);
1088 }
1089
1090 #[test]
1091 fn enter_from_places_caret_at_matching_edge() {
1092 let (log, mut ed) = recording();
1093 ed.exec(Command::InsertText("ab".into()));
1094 let root = ed.tree.root();
1095 ed.cursor = Cursor { seq: root, index: 2 };
1096 ed.sel_anchor = Some(Cursor { seq: root, index: 0 });
1097 let renders = log.renders.get();
1098 ed.enter_from(ExitDir::Left);
1099 assert_eq!(ed.cursor, Cursor { seq: root, index: 0 });
1100 assert!(ed.sel_anchor.is_none());
1101 assert_eq!(log.renders.get(), renders + 1);
1102 ed.enter_from(ExitDir::Right);
1103 assert_eq!(ed.cursor, Cursor { seq: root, index: 2 });
1104 ed.enter_from(ExitDir::Up);
1105 assert_eq!(ed.cursor, Cursor { seq: root, index: 0 });
1106 ed.enter_from(ExitDir::Down);
1107 assert_eq!(ed.cursor, Cursor { seq: root, index: 2 });
1108 }
1109
1110 #[derive(Default)]
1111 struct ObjectLog {
1112 entries: std::cell::RefCell<Vec<(u32, crate::command::Side)>>,
1113 consume: std::cell::Cell<bool>,
1114 }
1115
1116 struct ObjectHost(std::rc::Rc<ObjectLog>);
1117 impl Host for ObjectHost {
1118 fn typeset(&self, _latex: &str) -> mathtex_ir::Fragment {
1119 mathtex_ir::Fragment::default()
1120 }
1121 fn on_change(&self, _doc: &Document) {}
1122 fn on_exit(&self, _dir: ExitDir) {}
1123 fn request_close(&self) {}
1124 fn on_render(&self) {}
1125 fn on_object_enter(&self, token: u32, side: crate::command::Side) -> bool {
1126 self.0.entries.borrow_mut().push((token, side));
1127 self.0.consume.get()
1128 }
1129 }
1130
1131 #[test]
1132 fn motion_into_host_box_routes_to_host_when_consumed() {
1133 use crate::command::Side;
1134 let log = std::rc::Rc::new(ObjectLog::default());
1135 log.consume.set(true);
1136 let mut ed = Editor::new(ObjectHost(log.clone()), None);
1137 ed.exec(Command::InsertHostBox(17));
1138 let root = ed.tree.root();
1139 assert_eq!(ed.cursor, Cursor { seq: root, index: 1 });
1141 ed.exec(Command::Move(Dir::Left));
1143 assert_eq!(ed.cursor, Cursor { seq: root, index: 1 });
1144 ed.cursor = Cursor { seq: root, index: 0 };
1145 ed.exec(Command::Move(Dir::Right));
1146 assert_eq!(ed.cursor, Cursor { seq: root, index: 0 });
1147 assert_eq!(&*log.entries.borrow(), &[(17, Side::After), (17, Side::Before)]);
1148 }
1149
1150 #[test]
1151 fn motion_over_host_box_skips_when_defaulted() {
1152 let mut ed = Editor::new(NullHost, None);
1153 ed.exec(Command::InsertText("a".into()));
1154 ed.exec(Command::InsertHostBox(9));
1155 ed.exec(Command::InsertText("b".into()));
1156 let root = ed.tree.root();
1157 ed.cursor = Cursor { seq: root, index: 1 };
1158 ed.sel_anchor = None;
1159 let r = ed.exec(Command::Move(Dir::Right));
1161 assert_eq!(r.cursor, Cursor { seq: root, index: 2 });
1162 let r = ed.exec(Command::Move(Dir::Left));
1163 assert_eq!(r.cursor, Cursor { seq: root, index: 1 });
1164 }
1165
1166 #[test]
1167 fn host_box_deletes_whole_and_undo_restores_the_token() {
1168 let mut ed = Editor::new(NullHost, None);
1169 ed.exec(Command::InsertHostBox(17));
1170 assert_eq!(ed.tree.len(ed.tree.root()), 1);
1171 let del = ed.exec(Command::DeleteBackward);
1173 assert_eq!(ed.tree.len(ed.tree.root()), 0);
1174 ed.apply_undo(del.undo.unwrap());
1175 let root = ed.tree.root();
1176 assert_eq!(ed.tree.len(root), 1);
1177 match ed.tree.kind(ed.tree.items(root)[0]) {
1178 Some(Kind::HostBox { token }) => assert_eq!(*token, 17),
1179 _ => panic!("expected HostBox"),
1180 }
1181 }
1182
1183 #[test]
1184 fn selection_spanning_host_box_copies_one_entry_with_the_token() {
1185 let mut ed = Editor::new(NullHost, None);
1186 ed.exec(Command::InsertText("a".into()));
1187 ed.exec(Command::InsertHostBox(17));
1188 ed.exec(Command::InsertText("b".into()));
1189 ed.exec(Command::SelectAll);
1190 let frag = ed.selection_fragment();
1191 assert_eq!(frag.len(), 3);
1192 assert!(matches!(frag[1], crate::doc::NodeDoc::HostBox { token: 17 }));
1193 assert_eq!(ed.selection_latex(), "a\\hostbox{17}b");
1194 }
1195
1196 #[test]
1197 fn undo_restores_previous_document() {
1198 let mut ed = Editor::new(NullHost, None);
1199 let r1 = ed.exec(Command::InsertAtom(sym("a")));
1200 let r2 = ed.exec(Command::InsertAtom(sym("b")));
1201 assert_eq!(ed.document().len(), 2);
1202 ed.apply_undo(r2.undo.unwrap());
1204 assert_eq!(ed.document().len(), 1);
1205 ed.apply_undo(r1.undo.unwrap());
1206 assert_eq!(ed.document().len(), 0);
1207 }
1208}