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