mathtex-editor-session 0.3.0

Batteries included session for mathtex-editor: keymap, typesetting, undo, clipboard and host box tokens over the explicit core
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
//! A complete math editor in one type: editor, keymap, typesetter, undo, clipboard and host box tokens.

mod boxes;
mod cache;
mod tokens;
mod undo;

#[cfg(test)]
mod tests;

use std::collections::BTreeSet;
use std::fmt;

use mathtex_editor_core::{
    CaretPath, Command, Dir, Document, Editor, ExitDir, HostBoxEntry, HostBoxPolicy, MenuView, Point, Repair,
    Selection, Side,
};
use mathtex_editor_keymap::{KeyInput, Keymap};
use mathtex_engine::font::FontLoader;
use mathtex_engine::{HostBoxes, MathMode, TypesetError, Typesetter};
use mathtex_ir::Length;

use crate::boxes::SessionBoxes;
pub use crate::cache::{RenderCache, View};
pub use crate::tokens::{TokenError, TokenRegistry};
pub use crate::undo::{DEFAULT_UNDO_LIMIT, UndoStack};

/// What one session call did, for the host to act on.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct Update {
    /// The document changed.
    pub changed: bool,
    /// The caret tried to leave the formula in this direction.
    pub exit: Option<ExitDir>,
    /// Deleting in an empty formula asks the host to end math mode.
    pub close: bool,
    /// Motion stopped in front of a host box, see [`Session::step_over_host_box`].
    pub entered_host_box: Option<HostBoxEntry>,
    /// The document, caret, selection, or menu changed, so the view should be drawn again.
    pub needs_redraw: bool,
    /// `(old, new)` for every host box a paste gave a fresh token, in document order.
    pub reminted: Vec<(u32, u32)>,
}

/// A selection as the two clipboard flavors a host writes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClipboardData {
    /// The selection as versioned document JSON, which [`Session::paste_json`] reads back.
    pub json: String,
    /// The selection as clean LaTeX for other applications.
    pub tex: String,
}

/// Why [`Session::paste_json`] pasted nothing.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PasteError {
    /// The text is not a valid document, with serde's message.
    Json(String),
    /// The document holds no nodes.
    Empty,
    /// No fresh tokens were left for its host boxes.
    Tokens(TokenError),
}

impl fmt::Display for PasteError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            PasteError::Json(e) => write!(f, "not a document: {e}"),
            PasteError::Empty => write!(f, "the document is empty"),
            PasteError::Tokens(e) => e.fmt(f),
        }
    }
}

impl std::error::Error for PasteError {}

/// What the host sees, compared before and after a call to fill [`Update::needs_redraw`].
#[derive(PartialEq)]
struct Visible {
    revision: u64,
    cursor: CaretPath,
    selection: Option<Selection>,
    menu: Option<MenuView>,
}

/// An editor with keymap, typesetting, undo, clipboard, and host box tokens, driven by a few input calls.
pub struct Session<L: FontLoader> {
    editor: Editor,
    keymap: Keymap,
    typesetter: Typesetter<L>,
    mode: MathMode,
    /// The editor's host box policy, which the editor does not report back.
    policy: HostBoxPolicy,
    cache: RenderCache,
    history: UndoStack,
    tokens: TokenRegistry,
    clipboard: Option<Document>,
    boxes: SessionBoxes,
}

impl<L: FontLoader> fmt::Debug for Session<L> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Session")
            .field("revision", &self.editor.revision())
            .field("typesetter", &self.typesetter)
            .field("mode", &self.mode)
            .finish_non_exhaustive()
    }
}

impl<L: FontLoader> Session<L> {
    /// An empty editor typeset in display style by `typesetter`.
    pub fn new(typesetter: Typesetter<L>) -> Self {
        Self {
            editor: Editor::new(),
            keymap: Keymap::new(),
            typesetter,
            mode: MathMode::Display,
            policy: HostBoxPolicy::Skip,
            cache: RenderCache::new(),
            history: UndoStack::default(),
            tokens: TokenRegistry::new(),
            clipboard: None,
            boxes: SessionBoxes::default(),
        }
    }

    /// Replace the document, repairing what [`Document::validate`] rejects, and clear the history.
    pub fn load(&mut self, mut doc: Document) -> Vec<Repair> {
        let repairs = doc.repair();
        // A repaired document always validates, so the old editor stays only if that ever broke.
        if let Ok(mut editor) = Editor::from_document(&doc) {
            editor.set_host_box_policy(self.policy);
            self.editor = editor;
        }
        self.tokens.reserve_document(&doc);
        self.keymap.reset();
        self.history.clear();
        self.cache.invalidate();
        repairs
    }

    /// The editor, for queries such as `document`, `menu`, and `matrix_shape`.
    pub fn editor(&self) -> &Editor {
        &self.editor
    }

    /// The keymap, for its palette entries.
    pub fn keymap(&self) -> &Keymap {
        &self.keymap
    }

    /// The keymap, for defining words and switching autocorrect.
    pub fn keymap_mut(&mut self) -> &mut Keymap {
        &mut self.keymap
    }

    /// The typesetter, whose font loader draws the faces a view's glyph runs name.
    pub fn typesetter(&self) -> &Typesetter<L> {
        &self.typesetter
    }

    /// The undo history.
    pub fn history(&self) -> &UndoStack {
        &self.history
    }

    /// Keep at most `limit` undo steps.
    pub fn set_undo_limit(&mut self, limit: usize) {
        self.history.set_limit(limit);
    }

    /// Typeset inline or in display style.
    pub fn set_math_mode(&mut self, mode: MathMode) {
        self.mode = mode;
        self.cache.invalidate();
    }

    /// Choose whether horizontal motion steps over host boxes or stops and reports them.
    pub fn set_host_box_policy(&mut self, policy: HostBoxPolicy) {
        self.policy = policy;
        self.editor.set_host_box_policy(policy);
    }

    /// Feed a key event through the keymap, its commands form one undo step.
    pub fn key(&mut self, input: &KeyInput) -> Update {
        let ctx = self.editor.input_context();
        let commands = self.keymap.map_key(input, &ctx);
        self.run(commands)
    }

    /// Feed committed text such as IME output, as if each character were typed.
    pub fn text(&mut self, s: &str) -> Update {
        let ctx = self.editor.input_context();
        let commands = self.keymap.map_text(s, &ctx);
        self.run(commands)
    }

    /// Place the caret at `at`, or extend the selection to it, in the coordinates of [`View::render`].
    pub fn pointer(&mut self, at: Point, extend: bool) -> Update {
        self.keymap.reset();
        let hit = self.cache.hit_test(&self.editor, &mut self.typesetter, self.mode, &self.boxes, at);
        match hit {
            Ok(Some(path)) if extend => self.run(vec![Command::ExtendTo(path)]),
            Ok(Some(path)) => self.run(vec![Command::MoveTo(path)]),
            _ => Update::default(),
        }
    }

    /// Run one host command, such as a toolbar button or a menu row, as its own undo step.
    pub fn command(&mut self, cmd: Command) -> Update {
        self.keymap.reset();
        match &cmd {
            Command::InsertHostBox(token) => {
                let _ = self.tokens.reserve(*token);
            }
            Command::InsertDocument(doc) => self.tokens.reserve_document(doc),
            _ => {}
        }
        self.run(vec![cmd])
    }

    /// Insert what a palette word inserts, as one undo step like typing it.
    pub fn commit_word(&mut self, word: &str) -> Update {
        let Some(commands) = self.keymap.commands_for_word(word) else {
            return Update::default();
        };
        self.keymap.reset();
        self.run(commands)
    }

    /// Cross the host box a motion stopped at, when the host does not take the caret into it.
    pub fn step_over_host_box(&mut self, entry: HostBoxEntry) -> Update {
        let dir = match entry.side {
            Side::Before => Dir::Right,
            Side::After => Dir::Left,
        };
        self.editor.set_host_box_policy(HostBoxPolicy::Skip);
        let update = self.command(Command::Move(dir));
        self.editor.set_host_box_policy(self.policy);
        update
    }

    /// Undo the last step.
    pub fn undo(&mut self) -> Update {
        self.keymap.reset();
        let Some(target) = self.history.undo(self.editor.snapshot()) else {
            return Update::default();
        };
        self.restore(&target)
    }

    /// Redo the last undone step.
    pub fn redo(&mut self) -> Update {
        self.keymap.reset();
        let Some(target) = self.history.redo(self.editor.snapshot()) else {
            return Update::default();
        };
        self.restore(&target)
    }

    fn restore(&mut self, target: &mathtex_editor_core::Snapshot) -> Update {
        // History snapshots come from this editor, so they always restore.
        let changed = self.editor.restore(target).is_ok();
        Update { changed, needs_redraw: changed, ..Update::default() }
    }

    /// Copy the selection to the internal clipboard and return it for the system clipboard.
    pub fn copy(&mut self) -> Option<ClipboardData> {
        let doc = self.editor.selection_document()?;
        let tex = self.editor.selection_tex()?;
        let json = serde_json::to_string(&doc).ok()?;
        self.clipboard = Some(doc);
        Some(ClipboardData { json, tex })
    }

    /// Copy the selection, then delete it as one undo step.
    pub fn cut(&mut self) -> Option<(ClipboardData, Update)> {
        let data = self.copy()?;
        let update = self.command(Command::DeleteBackward);
        Some((data, update))
    }

    /// The internal clipboard.
    pub fn clipboard(&self) -> Option<&Document> {
        self.clipboard.as_ref()
    }

    /// Paste document JSON, giving each host box a fresh token and the size its old token had.
    pub fn paste_json(&mut self, json: &str) -> Result<Update, PasteError> {
        let doc: Document = serde_json::from_str(json).map_err(|e| PasteError::Json(e.to_string()))?;
        if doc.is_empty() {
            return Err(PasteError::Empty);
        }
        self.paste_document(doc).map_err(PasteError::Tokens)
    }

    /// Paste the internal clipboard, giving each host box a fresh token.
    pub fn paste_internal(&mut self) -> Result<Update, TokenError> {
        match self.clipboard.clone() {
            Some(doc) => self.paste_document(doc),
            None => Ok(Update::default()),
        }
    }

    /// Paste plain text through the keymap, as one undo step.
    pub fn paste_text(&mut self, s: &str) -> Update {
        self.text(s)
    }

    fn paste_document(&mut self, mut doc: Document) -> Result<Update, TokenError> {
        let reminted = self.tokens.remint(&mut doc)?;
        for &(old, new) in &reminted {
            self.boxes.copy_size(old, new);
        }
        self.keymap.reset();
        let mut update = self.run(vec![Command::InsertDocument(doc)]);
        if update.changed {
            update.reminted = reminted;
        }
        Ok(update)
    }

    /// Mint a token and insert a host box carrying it.
    pub fn insert_host_box(&mut self) -> Result<(u32, Update), TokenError> {
        let token = self.tokens.mint()?;
        Ok((token, self.command(Command::InsertHostBox(token))))
    }

    /// Mint a token for a host box the host inserts itself.
    pub fn mint_host_token(&mut self) -> Result<u32, TokenError> {
        self.tokens.mint()
    }

    /// Lay out `token` as an empty box of this size, for hosts that draw the box content themselves.
    pub fn set_host_box_size(&mut self, token: u32, width: Length, height: Length, depth: Length) -> Result<(), TokenError> {
        self.tokens.reserve(token)?;
        self.boxes.set_size(token, width, height, depth);
        self.cache.invalidate();
        Ok(())
    }

    /// Answer `\hostbox` for tokens without a size through `provider`.
    pub fn set_host_boxes(&mut self, provider: Box<dyn HostBoxes>) {
        self.boxes.set_provider(Some(provider));
        self.cache.invalidate();
    }

    /// Typeset again on the next view, after the provider's boxes changed.
    pub fn host_boxes_changed(&mut self) {
        self.cache.invalidate();
    }

    /// Tokens the document, the history, or the clipboard reference, and drop the sizes of every other token.
    pub fn live_host_tokens(&mut self) -> BTreeSet<u32> {
        let live = TokenRegistry::live(&self.editor, &self.history, self.clipboard.as_ref());
        self.boxes.retain(&live);
        live
    }

    /// The typeset document with its caret and selection geometry, typeset again only after a change.
    pub fn view(&mut self) -> Result<&View, TypesetError> {
        self.cache.view(&self.editor, &mut self.typesetter, self.mode, &self.boxes)
    }

    /// A standalone view of what a palette word inserts, `None` for an unknown word.
    pub fn preview_word(&mut self, word: &str) -> Option<Result<View, TypesetError>> {
        let commands = self.keymap.commands_for_word(word)?;
        let mut scratch = Editor::new();
        for cmd in commands {
            let _ = scratch.exec(cmd);
        }
        let mut cache = RenderCache::new();
        Some(cache.view(&scratch, &mut self.typesetter, self.mode, &self.boxes).cloned())
    }

    /// Run commands in order as one undo step.
    fn run(&mut self, commands: Vec<Command>) -> Update {
        if commands.is_empty() {
            return Update::default();
        }
        let before = self.editor.snapshot();
        let seen = self.visible();
        let mut update = Update::default();
        for cmd in commands {
            let out = self.editor.exec(cmd);
            update.changed |= out.changed;
            update.close |= out.close;
            update.exit = update.exit.or(out.exit);
            update.entered_host_box = update.entered_host_box.or(out.entered_host_box);
        }
        if update.changed {
            self.history.record(before);
        }
        update.needs_redraw = update.changed || self.visible() != seen;
        update
    }

    fn visible(&self) -> Visible {
        Visible {
            revision: self.editor.revision(),
            cursor: self.editor.cursor(),
            selection: self.editor.selection(),
            menu: self.editor.menu(),
        }
    }
}