Skip to main content

mathtex_editor_core/
editor.rs

1//! The editor: document, caret, selection, and menu state, changed only by explicit calls.
2
3use std::fmt;
4use std::sync::atomic::{AtomicU64, Ordering};
5
6use mathtex_ir::Fragment;
7use serde::{Deserialize, Serialize};
8
9use crate::command::{Command, Dir, Edge, ExitDir, HostBoxEntry, HostBoxPolicy, Outcome, Side};
10use crate::doc::{Document, DocumentError};
11use crate::export::{self, Source};
12use crate::geometry::{EmptySlotCaret, Point, RenderOutput, StaleSource};
13use crate::menu::{Menu, MenuView, RowEffect};
14use crate::model::{Cursor, Kind, NodeId, SeqId, SeqRange, Symbol, Tree};
15use crate::path::{CaretPath, PathError, Selection};
16use crate::{matcher, nav, selection};
17
18/// Distinguishes editors so a [`Source`] only renders against the editor that exported it.
19static NEXT_EDITOR: AtomicU64 = AtomicU64::new(1);
20
21/// Everything needed to put an editor back into a previous state, the host keeps its own undo stack.
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct Snapshot {
24    /// The document.
25    pub document: Document,
26    /// The caret.
27    pub cursor: CaretPath,
28    /// The selection, whose focus wins over `cursor` when present.
29    pub selection: Option<Selection>,
30}
31
32/// Why [`Editor::restore`] refused a snapshot.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum RestoreError {
35    /// The snapshot's document breaks a rule of [`Document::validate`].
36    Document(DocumentError),
37    /// A caret of the snapshot does not resolve in its document.
38    Path(PathError),
39}
40
41impl From<PathError> for RestoreError {
42    fn from(e: PathError) -> Self {
43        RestoreError::Path(e)
44    }
45}
46
47impl fmt::Display for RestoreError {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        match self {
50            RestoreError::Document(e) => write!(f, "invalid snapshot document: {e}"),
51            RestoreError::Path(e) => write!(f, "invalid snapshot caret: {e}"),
52        }
53    }
54}
55
56impl std::error::Error for RestoreError {}
57
58/// Facts a keymap needs to interpret the next key.
59#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
60pub struct InputContext {
61    /// The caret is inside `\text{}`, where keys insert literal text.
62    pub in_text_slot: bool,
63    /// The swap or delete menu is open and takes typed characters as its filter.
64    pub menu_open: bool,
65    /// The close character of the innermost delimiters whose end the caret sits at.
66    pub closing_delimiter: Option<char>,
67    /// Counts `exec`, `restore`, and caret setter calls, so a keymap sees whether anything ran between keys.
68    pub serial: u64,
69}
70
71/// A headless structural math editor that never typesets, keeps no history, and has no callbacks.
72pub struct Editor {
73    tree: Tree,
74    cursor: Cursor,
75    /// Selection anchor gap in `cursor.seq`, the caret is the focus.
76    anchor: Option<usize>,
77    menu: Option<Menu>,
78    revision: u64,
79    serial: u64,
80    id: u64,
81    policy: HostBoxPolicy,
82    empty_caret: EmptySlotCaret,
83}
84
85impl Default for Editor {
86    fn default() -> Self {
87        Self::new()
88    }
89}
90
91impl Editor {
92    /// An empty editor.
93    pub fn new() -> Self {
94        Self::with_tree(Tree::new())
95    }
96
97    /// An editor holding `doc`, which must pass [`Document::validate`], see [`Document::repair`].
98    pub fn from_document(doc: &Document) -> Result<Self, DocumentError> {
99        doc.validate()?;
100        Ok(Self::with_tree(Tree::from_doc(doc)))
101    }
102
103    fn with_tree(tree: Tree) -> Self {
104        let cursor = Cursor { seq: tree.root(), index: 0 };
105        Self {
106            tree,
107            cursor,
108            anchor: None,
109            menu: None,
110            revision: 0,
111            serial: 0,
112            id: NEXT_EDITOR.fetch_add(1, Ordering::Relaxed),
113            policy: HostBoxPolicy::Skip,
114            empty_caret: EmptySlotCaret::Start,
115        }
116    }
117
118    /// The whole document.
119    pub fn document(&self) -> Document {
120        self.tree.to_doc()
121    }
122
123    /// Advances on every content change and on [`Editor::restore`], never on motion.
124    pub fn revision(&self) -> u64 {
125        self.revision
126    }
127
128    /// The caret, which is the selection focus while a selection is active.
129    pub fn cursor(&self) -> CaretPath {
130        self.tree.path_of(self.cursor)
131    }
132
133    /// Place the caret, collapsing the selection and closing the menu.
134    pub fn set_cursor(&mut self, at: &CaretPath) -> Result<(), PathError> {
135        let c = self.tree.resolve(at)?;
136        self.cursor = c;
137        self.anchor = None;
138        self.menu = None;
139        self.serial += 1;
140        self.normalize();
141        Ok(())
142    }
143
144    /// The active selection, `None` when the caret is collapsed.
145    pub fn selection(&self) -> Option<Selection> {
146        let s = self.sel()?;
147        let steps = self.tree.seq_steps(s.seq);
148        Some(Selection {
149            anchor: CaretPath { steps: steps.clone(), index: s.anchor },
150            focus: CaretPath { steps, index: s.focus },
151        })
152    }
153
154    /// Select between two carets in the same sequence and close the menu.
155    pub fn set_selection(&mut self, anchor: &CaretPath, focus: &CaretPath) -> Result<(), PathError> {
156        let a = self.tree.resolve(anchor)?;
157        let f = self.tree.resolve(focus)?;
158        if a.seq != f.seq {
159            return Err(PathError::SplitSelection);
160        }
161        self.cursor = f;
162        self.anchor = Some(a.index);
163        self.menu = None;
164        self.serial += 1;
165        self.normalize();
166        Ok(())
167    }
168
169    /// Whether the caret is before everything in the formula.
170    pub fn at_start(&self) -> bool {
171        self.cursor == Cursor { seq: self.tree.root(), index: 0 }
172    }
173
174    /// Whether the caret is after everything in the formula.
175    pub fn at_end(&self) -> bool {
176        let root = self.tree.root();
177        self.cursor == Cursor { seq: root, index: self.tree.len(root) }
178    }
179
180    /// Whether the caret is at the start of its slot.
181    pub fn at_slot_start(&self) -> bool {
182        self.cursor.index == 0
183    }
184
185    /// Whether the caret is at the end of its slot.
186    pub fn at_slot_end(&self) -> bool {
187        self.cursor.index == self.tree.len(self.cursor.seq)
188    }
189
190    /// The ASCII letter atoms right before the caret back to any other item, empty with a selection or menu.
191    pub fn letters_before_caret(&self) -> String {
192        if self.sel().is_some() || self.menu.is_some() {
193            return String::new();
194        }
195        let items = &self.tree.items(self.cursor.seq)[..self.cursor.index];
196        let letter = |&node: &NodeId| match self.tree.kind(node) {
197            Some(Kind::Atom(s)) => match s.latex.as_bytes() {
198                [b] if b.is_ascii_alphabetic() => Some(char::from(*b)),
199                _ => None,
200            },
201            _ => None,
202        };
203        let mut letters: Vec<char> = items.iter().rev().map_while(letter).collect();
204        letters.reverse();
205        letters.into_iter().collect()
206    }
207
208    /// Whether the caret is inside `\text{}`.
209    pub fn in_text_slot(&self) -> bool {
210        self.tree.is_text_slot(self.cursor.seq)
211    }
212
213    /// The open swap or delete menu.
214    pub fn menu(&self) -> Option<MenuView> {
215        self.menu.as_ref().map(Menu::view)
216    }
217
218    /// The structure each open menu row makes with its slots emptied, for typeset previews, `None` for Delete.
219    pub fn menu_previews(&self) -> Vec<Option<Document>> {
220        self.menu.as_ref().map(|m| m.previews(&self.tree)).unwrap_or_default()
221    }
222
223    /// Rows and columns of the matrix whose cell holds the caret.
224    pub fn matrix_shape(&self) -> Option<(usize, usize)> {
225        self.tree.matrix_shape_at(self.cursor)
226    }
227
228    /// Everything a keymap needs to know about the caret in one call.
229    pub fn input_context(&self) -> InputContext {
230        InputContext {
231            in_text_slot: self.in_text_slot(),
232            menu_open: self.menu.is_some(),
233            closing_delimiter: self.innermost_delim().and_then(|(_, close, body)| self.at_end_of(body).then_some(close)),
234            serial: self.serial,
235        }
236    }
237
238    /// Choose whether horizontal motion steps over host boxes or stops and reports them.
239    pub fn set_host_box_policy(&mut self, policy: HostBoxPolicy) {
240        self.policy = policy;
241    }
242
243    /// Choose where [`Editor::render`] puts the caret inside an empty slot, its left edge unless set.
244    pub fn set_empty_slot_caret(&mut self, place: EmptySlotCaret) {
245        self.empty_caret = place;
246    }
247
248    /// Put the caret at an edge of the formula, clearing selection and menu.
249    pub fn place_at(&mut self, edge: Edge) {
250        let root = self.tree.root();
251        let index = match edge {
252            Edge::Start => 0,
253            Edge::End => self.tree.len(root),
254        };
255        self.cursor = Cursor { seq: root, index };
256        self.anchor = None;
257        self.menu = None;
258        self.serial += 1;
259    }
260
261    /// LaTeX for typesetting, with `\phantom{x}` boxes in empty slots so they can be drawn.
262    pub fn source(&self) -> Source {
263        export::source(&self.tree, self.id, self.revision, true)
264    }
265
266    /// Clean LaTeX with spans, so hosts can map a caret to a byte offset in what users see.
267    pub fn display_source(&self) -> Source {
268        export::source(&self.tree, self.id, self.revision, false)
269    }
270
271    fn check(&self, source: &Source) -> Result<(), StaleSource> {
272        if source.revision == self.revision && source.spans.owner == self.id {
273            Ok(())
274        } else {
275            Err(StaleSource { source: source.revision, editor: self.revision })
276        }
277    }
278
279    /// Geometry for the caret, selection, placeholders, menu, and host boxes over `fragment`.
280    pub fn render(&self, source: &Source, fragment: &Fragment) -> Result<RenderOutput, StaleSource> {
281        self.check(source)?;
282        let menu = self.menu.as_ref().map(|m| m.anchor);
283        Ok(matcher::render(&self.tree, self.cursor, self.sel(), &source.spans, fragment, menu, self.empty_caret))
284    }
285
286    /// The caret nearest `at`, `None` when the fragment has no geometry to hit.
287    pub fn hit_test(&self, source: &Source, fragment: &Fragment, at: Point) -> Result<Option<CaretPath>, StaleSource> {
288        self.check(source)?;
289        let hit = matcher::hit_test(&self.tree, &source.spans, fragment, at);
290        Ok(hit.map(|c| self.tree.path_of(nav::normalize(&self.tree, c))))
291    }
292
293    /// The current state for a host undo stack.
294    pub fn snapshot(&self) -> Snapshot {
295        Snapshot { document: self.document(), cursor: self.cursor(), selection: self.selection() }
296    }
297
298    /// Return to a snapshot, closing the menu and advancing the revision, unchanged on error.
299    pub fn restore(&mut self, s: &Snapshot) -> Result<(), RestoreError> {
300        s.document.validate().map_err(RestoreError::Document)?;
301        let tree = Tree::from_doc(&s.document);
302        let (cursor, anchor) = match &s.selection {
303            Some(sel) => {
304                let a = tree.resolve(&sel.anchor)?;
305                let f = tree.resolve(&sel.focus)?;
306                if a.seq != f.seq {
307                    return Err(PathError::SplitSelection.into());
308                }
309                (f, Some(a.index))
310            }
311            None => (tree.resolve(&s.cursor)?, None),
312        };
313        self.tree = tree;
314        self.cursor = cursor;
315        self.anchor = anchor;
316        self.menu = None;
317        self.revision += 1;
318        self.serial += 1;
319        self.normalize();
320        Ok(())
321    }
322
323    /// The selected nodes as a standalone document for the clipboard.
324    pub fn selection_document(&self) -> Option<Document> {
325        let s = self.sel()?;
326        let items = self.tree.items(s.seq);
327        let hi = s.hi().min(items.len());
328        Some(Document::new(items[s.lo()..hi].iter().filter_map(|&n| self.tree.node_to_doc(n)).collect()))
329    }
330
331    /// The selection as clean LaTeX for external clipboards.
332    pub fn selection_tex(&self) -> Option<String> {
333        self.sel().map(|s| export::range_tex(&self.tree, s))
334    }
335
336    /// Run one command and report what it did.
337    pub fn exec(&mut self, cmd: Command) -> Outcome {
338        let edits = self.tree.edits;
339        let before = self.position();
340        let mut out = Outcome::default();
341        self.serial += 1;
342        self.run(cmd, &mut out);
343        out.changed = self.tree.edits != edits;
344        if out.changed {
345            self.revision += 1;
346        }
347        out.moved = self.position() != before;
348        out.revision = self.revision;
349        out
350    }
351
352    fn position(&self) -> (CaretPath, Option<usize>) {
353        (self.cursor(), self.sel().map(|s| s.anchor))
354    }
355
356    fn sel(&self) -> Option<SeqRange> {
357        let a = self.anchor.filter(|&a| a != self.cursor.index)?;
358        Some(SeqRange { seq: self.cursor.seq, anchor: a, focus: self.cursor.index })
359    }
360
361    fn set(&mut self, c: Cursor) {
362        self.cursor = c;
363        self.anchor = None;
364    }
365
366    /// Apply an edit result, a refused edit leaves caret and selection alone.
367    fn edit(&mut self, result: Option<Cursor>) {
368        if let Some(c) = result {
369            self.set(c);
370        }
371    }
372
373    fn run(&mut self, cmd: Command, out: &mut Outcome) {
374        if self.menu.is_some() {
375            if self.run_menu(&cmd) {
376                self.normalize();
377                return;
378            }
379            // Any other command closes the menu and then runs normally.
380            self.menu = None;
381        }
382        self.dispatch(cmd, out);
383        self.normalize();
384    }
385
386    fn dispatch(&mut self, cmd: Command, out: &mut Outcome) {
387        let at = self.cursor;
388        let sel = self.sel();
389        match cmd {
390            Command::Move(dir @ (Dir::Left | Dir::Right)) => {
391                let right = dir == Dir::Right;
392                if self.policy == HostBoxPolicy::Enter {
393                    if let Some(token) = self.host_box_neighbor(right) {
394                        let side = if right { Side::Before } else { Side::After };
395                        out.entered_host_box = Some(HostBoxEntry { token, side });
396                        return;
397                    }
398                }
399                let next = if right { nav::move_right(&self.tree, at) } else { nav::move_left(&self.tree, at) };
400                match next {
401                    Some(c) => self.set(c),
402                    None => self.boundary(if right { ExitDir::Right } else { ExitDir::Left }, out),
403                }
404            }
405            Command::Move(dir) => {
406                let up = dir == Dir::Up;
407                match nav::vertical(&self.tree, at, up) {
408                    Some(c) => self.set(c),
409                    None => self.boundary(if up { ExitDir::Up } else { ExitDir::Down }, out),
410                }
411            }
412            Command::MoveLineStart => self.set(Cursor { seq: at.seq, index: 0 }),
413            Command::MoveLineEnd => self.set(Cursor { seq: at.seq, index: self.tree.len(at.seq) }),
414            Command::Tab => match self.next_fill_target(true) {
415                Some(c) => self.set(c),
416                None => out.exit = Some(ExitDir::Right),
417            },
418            Command::ShiftTab => match self.next_fill_target(false) {
419                Some(c) => self.set(c),
420                None => out.exit = Some(ExitDir::Left),
421            },
422            Command::MoveTo(path) => {
423                if let Ok(c) = self.tree.resolve(&path) {
424                    self.set(c);
425                }
426            }
427            Command::ExtendTo(path) => {
428                if let Ok(target) = self.tree.resolve(&path) {
429                    let anchor = Cursor { seq: at.seq, index: self.anchor.unwrap_or(at.index) };
430                    self.select(selection::extend_to(&self.tree, anchor, target));
431                }
432            }
433            Command::Extend(dir @ (Dir::Left | Dir::Right)) => {
434                let s = sel.unwrap_or(SeqRange { seq: at.seq, anchor: at.index, focus: at.index });
435                self.select(selection::extend(&self.tree, s, dir == Dir::Right));
436            }
437            Command::Extend(_) => {}
438            Command::SelectAll => self.select(selection::select_all(&self.tree)),
439            Command::Collapse | Command::Confirm => self.anchor = None,
440            Command::MenuSelect(_) => {}
441            Command::InsertAtom(sym) => {
442                let r = self.tree.insert_atom(at, sel, sym);
443                self.edit(r);
444            }
445            Command::InsertHostBox(token) => {
446                let r = self.tree.insert_host_box(at, sel, token);
447                self.edit(r);
448            }
449            Command::InsertText(text) => self.insert_text(&text),
450            Command::InsertFraction(style) => {
451                let r = self.tree.insert_fraction(at, style, sel);
452                self.edit(r);
453            }
454            Command::InsertScript(slot) => {
455                // `_` and `^` right after a big operator, evaluation bar, or labeled arrow move into its limits.
456                if let (None, Some(c)) = (sel, self.tree.limit_target(at, slot)) {
457                    self.set(c);
458                } else {
459                    let r = self.tree.attach_script(at, slot, sel);
460                    self.edit(r);
461                }
462            }
463            Command::InsertBigOp(op) => {
464                let r = self.tree.insert_big_op(at, sel, op);
465                self.edit(r);
466            }
467            Command::InsertSqrt => {
468                let r = self.tree.insert_sqrt(at, sel);
469                self.edit(r);
470            }
471            Command::InsertDelimiters { open, close } => {
472                let r = self.tree.insert_delimiters(at, open, close, sel);
473                self.edit(r);
474            }
475            Command::InsertAccent(mark) => {
476                let r = self.tree.insert_accent(at, mark, sel);
477                self.edit(r);
478            }
479            Command::InsertUnderOver(spec) => {
480                let r = self.tree.insert_under_over(at, spec, sel);
481                self.edit(r);
482            }
483            Command::InsertStyled(variant) => {
484                let r = self.tree.insert_styled(at, variant, sel);
485                self.edit(r);
486            }
487            Command::InsertMatrix { env, rows, cols } => {
488                let r = self.tree.insert_matrix(at, sel, env, rows, cols);
489                self.edit(r);
490            }
491            Command::InsertRatio => {
492                let r = self.tree.insert_ratio(at, sel);
493                self.edit(r);
494            }
495            Command::InsertEvalBar => {
496                let r = self.tree.insert_eval_bar(at, sel);
497                self.edit(r);
498            }
499            Command::InsertLabeledArrow(arrow) => {
500                let r = self.tree.insert_labeled_arrow(at, arrow, sel);
501                self.edit(r);
502            }
503            Command::InsertDocument(doc) => {
504                if doc.validate().is_err() {
505                    return;
506                }
507                let r = self.tree.insert_doc(at, sel, &doc);
508                self.edit(r);
509            }
510            Command::DeleteBackward | Command::DeleteForward => {
511                let back = cmd == Command::DeleteBackward;
512                let neighbor = if back {
513                    nav::adjacent_structure_backward(&self.tree, at)
514                } else {
515                    nav::adjacent_structure_forward(&self.tree, at)
516                };
517                if let Some(s) = sel {
518                    let c = self.tree.delete_range(s);
519                    self.set(c);
520                } else if let Some(node) = neighbor {
521                    self.select_or_open_menu(node);
522                } else if at.seq == self.tree.root() && self.tree.is_empty(at.seq) {
523                    out.close = true;
524                } else {
525                    let c = if back { self.tree.delete_backward(at) } else { self.tree.delete_forward(at) };
526                    self.set(c);
527                }
528            }
529            Command::MatrixInsertRow(side) => {
530                let r = self.tree.matrix_insert_row(at, side);
531                self.edit(r);
532            }
533            Command::MatrixDeleteRow => {
534                let r = self.tree.matrix_delete_row(at);
535                self.edit(r);
536            }
537            Command::MatrixInsertCol(side) => {
538                let r = self.tree.matrix_insert_col(at, side);
539                self.edit(r);
540            }
541            Command::MatrixDeleteCol => {
542                let r = self.tree.matrix_delete_col(at);
543                self.edit(r);
544            }
545            Command::ReplaceTyped { typed, with } => {
546                if sel.is_none() && self.typed_matches(&typed) {
547                    let n = typed.chars().count();
548                    let c = self.tree.delete_range(SeqRange { seq: at.seq, anchor: at.index - n, focus: at.index });
549                    self.set(c);
550                    self.normalize();
551                    for cmd in with {
552                        self.run(cmd, out);
553                    }
554                }
555            }
556            Command::CloseDelimiter(close) => {
557                if sel.is_none() {
558                    if let Some(after) = self.closing_target(close) {
559                        self.set(after);
560                        return;
561                    }
562                }
563                if let Some(sym) = Symbol::from_char(close) {
564                    let r = self.tree.insert_atom(at, sel, sym);
565                    self.edit(r);
566                }
567            }
568        }
569    }
570
571    /// Consume commands the open menu understands, `false` lets the command close it and run.
572    fn run_menu(&mut self, cmd: &Command) -> bool {
573        let Some(menu) = self.menu.as_mut() else {
574            return false;
575        };
576        match cmd {
577            Command::InsertAtom(sym) if sym.latex.chars().count() == 1 => {
578                menu.query.push_str(&sym.latex);
579                menu.selected = 0;
580            }
581            Command::InsertText(text) => {
582                menu.query.push_str(text);
583                menu.selected = 0;
584            }
585            // With an empty filter both deletes dissolve the structure, otherwise they edit the filter.
586            Command::DeleteBackward | Command::DeleteForward => {
587                if menu.query.pop().is_none() {
588                    self.commit_menu_row(0);
589                } else {
590                    menu.selected = 0;
591                }
592            }
593            Command::Move(Dir::Up) => menu.selected = menu.selected.saturating_sub(1),
594            Command::Move(Dir::Down) => {
595                let max = menu.visible().len().saturating_sub(1);
596                menu.selected = (menu.selected + 1).min(max);
597            }
598            Command::Confirm => {
599                let selected = menu.selected;
600                self.commit_menu_row(selected);
601            }
602            Command::MenuSelect(i) => self.commit_menu_row(*i),
603            Command::Collapse => self.menu = None,
604            _ => return false,
605        }
606        true
607    }
608
609    /// Commit a visible menu row by dissolving or swapping the anchored structure.
610    fn commit_menu_row(&mut self, visible_idx: usize) {
611        let Some(menu) = self.menu.take() else {
612            return;
613        };
614        match menu.visible().get(visible_idx) {
615            Some(RowEffect::Delete) => {
616                let Some((seq, idx)) = self.tree.index_in_parent(menu.anchor) else {
617                    return;
618                };
619                // Backspace opens the menu with the caret after the structure, so it stays after the content.
620                let after = self.cursor == Cursor { seq, index: idx + 1 };
621                let c = match self.tree.dissolve(menu.anchor) {
622                    Some(d) => {
623                        let end = d.runs.last().map_or(idx, |(_, run)| run.end);
624                        Cursor { seq: d.seq, index: if after { end } else { idx } }
625                    }
626                    None => self.tree.delete_range(SeqRange { seq, anchor: idx, focus: idx + 1 }),
627                };
628                self.set(c);
629            }
630            Some(RowEffect::Swap(kind)) => self.tree.apply_swap(menu.anchor, kind),
631            None => {}
632        }
633    }
634
635    /// Open the swap or delete menu for swappable structures, else select the node.
636    fn select_or_open_menu(&mut self, node: NodeId) {
637        match self.tree.swap_variants(node) {
638            Some(variants) => self.menu = Some(Menu::for_node(node, variants)),
639            None => {
640                if let Some((seq, idx)) = self.tree.index_in_parent(node) {
641                    self.select(SeqRange { seq, anchor: idx, focus: idx + 1 });
642                }
643            }
644        }
645    }
646
647    fn select(&mut self, s: SeqRange) {
648        self.cursor = Cursor { seq: s.seq, index: s.focus };
649        self.anchor = Some(s.anchor);
650    }
651
652    fn insert_text(&mut self, text: &str) {
653        let text_slot = self.tree.is_text_slot(self.cursor.seq);
654        let mut sel = self.sel();
655        let mut at = self.cursor;
656        let mut any = false;
657        for ch in text.chars() {
658            if ch == ' ' && !text_slot {
659                continue;
660            }
661            let Some(sym) = Symbol::from_char(ch) else { continue };
662            if let Some(c) = self.tree.insert_atom(at, sel.take(), sym) {
663                at = c;
664                any = true;
665            }
666        }
667        if any {
668            self.set(at);
669        }
670    }
671
672    /// Collapse an active selection at a boundary, or report the exit for a plain caret.
673    fn boundary(&mut self, dir: ExitDir, out: &mut Outcome) {
674        if self.sel().is_some() {
675            self.anchor = None;
676        } else {
677            out.exit = Some(dir);
678        }
679    }
680
681    /// The token of the host box the caret would cross next.
682    fn host_box_neighbor(&self, right: bool) -> Option<u32> {
683        let items = self.tree.items(self.cursor.seq);
684        let node = if right { items.get(self.cursor.index) } else { items.get(self.cursor.index.checked_sub(1)?) };
685        match self.tree.kind(*node?) {
686            Some(Kind::HostBox { token }) => Some(*token),
687            _ => None,
688        }
689    }
690
691    /// Whether the atoms directly left of the caret spell `typed`.
692    fn typed_matches(&self, typed: &str) -> bool {
693        let items = self.tree.items(self.cursor.seq);
694        let n = typed.chars().count();
695        let Some(start) = self.cursor.index.checked_sub(n) else {
696            return false;
697        };
698        typed.chars().zip(&items[start..self.cursor.index]).all(|(ch, &node)| {
699            matches!((self.tree.kind(node), Symbol::from_char(ch)), (Some(Kind::Atom(s)), Some(e)) if s.latex == e.latex)
700        })
701    }
702
703    /// The innermost delimiters around the caret as `(node, close, body)`.
704    fn innermost_delim(&self) -> Option<(NodeId, char, SeqId)> {
705        let mut seq = self.cursor.seq;
706        loop {
707            let node = self.tree.seq_parent(seq)?;
708            if let Some(Kind::Delim { close, body, .. }) = self.tree.kind(node) {
709                return Some((node, *close, *body));
710            }
711            seq = self.tree.before_parent(seq)?.seq;
712        }
713    }
714
715    /// Whether moving right from the caret would only climb out until the end of `body`.
716    fn at_end_of(&self, body: SeqId) -> bool {
717        let mut cur = self.cursor;
718        loop {
719            if cur.index != self.tree.len(cur.seq) {
720                return false;
721            }
722            if cur.seq == body {
723                return true;
724            }
725            let Some(node) = self.tree.seq_parent(cur.seq) else {
726                return false;
727            };
728            if self.tree.child_seqs(node).last() != Some(&cur.seq) {
729                return false;
730            }
731            let Some((seq, idx)) = self.tree.index_in_parent(node) else {
732                return false;
733            };
734            cur = Cursor { seq, index: idx + 1 };
735        }
736    }
737
738    /// The gap after the innermost enclosing delimiters closed by `close` whose end the caret is at.
739    fn closing_target(&self, close: char) -> Option<Cursor> {
740        let mut seq = self.cursor.seq;
741        loop {
742            let node = self.tree.seq_parent(seq)?;
743            if let Some(Kind::Delim { close: c, body, .. }) = self.tree.kind(node) {
744                if *c == close && self.at_end_of(*body) {
745                    let (pseq, idx) = self.tree.index_in_parent(node)?;
746                    return Some(Cursor { seq: pseq, index: idx + 1 });
747                }
748            }
749            seq = self.tree.before_parent(seq)?.seq;
750        }
751    }
752
753    fn next_fill_target(&self, forward: bool) -> Option<Cursor> {
754        let at = self.cursor;
755        let tree = &self.tree;
756        let empty_start = |s: SeqId| tree.is_empty(s).then_some(Cursor { seq: s, index: 0 });
757        if forward {
758            // Big operator limits fill in reading order, lower then upper.
759            if let Some(Kind::BigOp { lower, upper, .. }) = tree.seq_parent(at.seq).and_then(|p| tree.kind(p)) {
760                if at.seq == *lower {
761                    if let Some(c) = empty_start(*upper) {
762                        return Some(c);
763                    }
764                }
765            }
766            let prev = at.index.checked_sub(1).and_then(|i| tree.items(at.seq).get(i));
767            if let Some(Kind::BigOp { lower, upper, .. }) = prev.and_then(|&p| tree.kind(p)) {
768                if let Some(c) = empty_start(*lower).or_else(|| empty_start(*upper)) {
769                    return Some(c);
770                }
771            }
772            // From a radicand, visit an empty degree before continuing.
773            if let Some(Kind::Sqrt { index, radicand }) = tree.seq_parent(at.seq).and_then(|p| tree.kind(p)) {
774                if at.seq == *radicand {
775                    if let Some(c) = empty_start(*index) {
776                        return Some(c);
777                    }
778                }
779            }
780        }
781        nav::next_empty_slot(tree, at, forward).map(|seq| Cursor { seq, index: 0 })
782    }
783
784    /// Restore every caret invariant, run after each command and state setter.
785    fn normalize(&mut self) {
786        let root = self.tree.root();
787        if !self.tree.seqs.contains_key(self.cursor.seq) {
788            self.cursor = Cursor { seq: root, index: self.tree.len(root) };
789            self.anchor = None;
790        }
791        let len = self.tree.len(self.cursor.seq);
792        self.cursor.index = self.cursor.index.min(len);
793        self.anchor = self.anchor.map(|a| a.min(len));
794        loop {
795            match self.anchor {
796                Some(a) if a != self.cursor.index => {
797                    // A focus on an illegal gap grows the selection to cover the whole Script.
798                    if !nav::is_illegal(&self.tree, self.cursor) {
799                        break;
800                    }
801                    let Some(p) = self.tree.before_parent(self.cursor.seq) else { break };
802                    self.anchor = Some(p.index + 1);
803                    self.cursor = p;
804                }
805                _ => {
806                    self.anchor = None;
807                    self.cursor = nav::normalize(&self.tree, self.cursor);
808                    break;
809                }
810            }
811        }
812        if self.menu.as_ref().is_some_and(|m| !self.tree.nodes.contains_key(m.anchor)) {
813            self.menu = None;
814        }
815    }
816
817    #[cfg(test)]
818    pub(crate) fn tree(&self) -> &Tree {
819        &self.tree
820    }
821
822    #[cfg(test)]
823    pub(crate) fn raw_cursor(&self) -> Cursor {
824        self.cursor
825    }
826
827    #[cfg(test)]
828    pub(crate) fn raw_anchor(&self) -> Option<usize> {
829        self.anchor
830    }
831
832    #[cfg(test)]
833    pub(crate) fn menu_anchor(&self) -> Option<NodeId> {
834        self.menu.as_ref().map(|m| m.anchor)
835    }
836}