Skip to main content

mathtex_editor_keymap/
lib.rs

1//! Input policy for mathtex-editor: key events and typed text to editor commands, with words and autocorrect.
2
3use std::error::Error;
4use std::fmt;
5
6use mathtex_editor_core::{
7    Command, Deco, Dir, FracStyle, InputContext, Mark, MatrixEnv, ScriptSlot, Symbol, UnderOverSpec, Variant,
8};
9
10#[cfg(test)]
11mod fuzz;
12#[cfg(test)]
13mod tests;
14
15/// A normalized key event from the host matching browser `KeyboardEvent` fields.
16#[derive(Debug, Clone)]
17pub struct KeyInput {
18    /// The `KeyboardEvent.key` value such as `"a"`, `"/"`, `"ArrowLeft"`, or `"Backspace"`.
19    pub key: String,
20    /// Whether the shift modifier is active.
21    pub shift: bool,
22    /// Whether the control modifier is active.
23    pub ctrl: bool,
24    /// Whether the alt modifier is active.
25    pub alt: bool,
26    /// Whether the meta modifier is active.
27    pub meta: bool,
28}
29
30/// A word [`Keymap::define_word`] refused because it is empty or holds more than ASCII letters.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct InvalidWord(pub String);
33
34impl fmt::Display for InvalidWord {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        write!(f, "{:?} cannot be typed as a word, words are one or more ASCII letters", self.0)
37    }
38}
39
40impl Error for InvalidWord {}
41
42/// A palette row with a trigger word and display label.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct KeymapEntry<'a> {
45    /// The shortcut trigger word.
46    pub word: &'a str,
47    /// The label shown for the shortcut.
48    pub label: &'a str,
49}
50
51#[derive(Debug, Clone)]
52struct Override {
53    word: String,
54    label: String,
55    commands: Vec<Command>,
56}
57
58/// Input policy for one editor, holding a pending word between keys, see the crate docs for the contract.
59#[derive(Debug, Clone)]
60pub struct Keymap {
61    word: String,
62    last: Option<char>,
63    // The `InputContext::serial` the next call carries when the host ran exactly our commands.
64    expect: u64,
65    overrides: Vec<Override>,
66    suffix_matching: bool,
67    autocorrect: bool,
68}
69
70impl Default for Keymap {
71    fn default() -> Self {
72        Self::new()
73    }
74}
75
76impl Keymap {
77    /// A keymap with the built in catalog, suffix matching, and autocorrect.
78    pub fn new() -> Self {
79        Self {
80            word: String::new(),
81            last: None,
82            expect: 0,
83            overrides: Vec::new(),
84            suffix_matching: true,
85            autocorrect: true,
86        }
87    }
88
89    /// Forget the pending word and last character, call it whenever the caret moves outside the keymap.
90    pub fn reset(&mut self) {
91        self.word.clear();
92        self.last = None;
93    }
94
95    /// Whether space converts the longest known suffix of the pending word, on by default.
96    pub fn set_suffix_matching(&mut self, on: bool) {
97        self.suffix_matching = on;
98    }
99
100    /// Whether two character sequences such as `<=` become symbols, on by default.
101    pub fn set_autocorrect(&mut self, on: bool) {
102        self.autocorrect = on;
103    }
104
105    /// Register or replace a word shortcut, which lists after the ones defined before it.
106    pub fn define_word(&mut self, word: &str, label: &str, commands: Vec<Command>) -> Result<(), InvalidWord> {
107        if word.is_empty() || !word.bytes().all(|b| b.is_ascii_alphabetic()) {
108            return Err(InvalidWord(word.to_string()));
109        }
110        let entry = Override { word: word.to_string(), label: label.to_string(), commands };
111        match self.overrides.iter_mut().find(|o| o.word == word) {
112            Some(o) => *o = entry,
113            None => self.overrides.push(entry),
114        }
115        Ok(())
116    }
117
118    /// Remove a word shortcut defined by the host, built in words stay.
119    pub fn undefine_word(&mut self, word: &str) {
120        self.overrides.retain(|o| o.word != word);
121    }
122
123    /// Every word, host words in definition order and then the catalog in its fixed order.
124    pub fn entries(&self) -> Vec<KeymapEntry<'_>> {
125        let mut out: Vec<KeymapEntry<'_>> =
126            self.overrides.iter().map(|o| KeymapEntry { word: &o.word, label: &o.label }).collect();
127        for s in CATALOG {
128            if !self.is_overridden(s.word) {
129                out.push(KeymapEntry { word: s.word, label: s.label });
130            }
131        }
132        out
133    }
134
135    /// The commands a word inserts, for a host rendered palette.
136    pub fn commands_for_word(&self, word: &str) -> Option<Vec<Command>> {
137        if let Some(o) = self.overrides.iter().find(|o| o.word == word) {
138            return Some(o.commands.clone());
139        }
140        CATALOG.iter().find(|s| s.word == word).map(|s| s.insert.commands())
141    }
142
143    /// Translate a key event, the host runs every returned command once and in order.
144    pub fn map_key(&mut self, input: &KeyInput, ctx: &InputContext) -> Vec<Command> {
145        self.sync(ctx);
146        let out = self.key(input, ctx);
147        self.expect = ctx.serial + out.len() as u64;
148        out
149    }
150
151    /// Translate committed text as if each character were typed, newlines and tabs are not typed in math.
152    pub fn map_text(&mut self, text: &str, ctx: &InputContext) -> Vec<Command> {
153        self.sync(ctx);
154        let mut cx = Cx::new(ctx);
155        let mut out = Vec::new();
156        for ch in text.chars() {
157            let ch = match ch {
158                '\r' => continue,
159                '\n' | '\t' if cx.literal() => ' ',
160                '\n' | '\t' => {
161                    self.reset();
162                    continue;
163                }
164                c => c,
165            };
166            out.extend(self.type_char(ch, &mut cx));
167        }
168        self.expect = ctx.serial + out.len() as u64;
169        out
170    }
171
172    fn is_overridden(&self, word: &str) -> bool {
173        self.overrides.iter().any(|o| o.word == word)
174    }
175
176    /// Drop pending state when anything but our own commands ran since the last call.
177    fn sync(&mut self, ctx: &InputContext) {
178        if ctx.serial != self.expect {
179            self.reset();
180        }
181    }
182
183    fn key(&mut self, input: &KeyInput, ctx: &InputContext) -> Vec<Command> {
184        if let Some(cmd) = named_key(input) {
185            self.reset();
186            return vec![cmd];
187        }
188        let ch = single_char(&input.key).filter(|c| !c.is_control());
189        // AltGr arrives as control plus alt and types a character.
190        let altgr = input.ctrl && input.alt && !input.meta && ch.is_some();
191        if (input.ctrl || input.meta) && !altgr {
192            self.reset();
193            return if input.key.eq_ignore_ascii_case("a") { vec![Command::SelectAll] } else { vec![] };
194        }
195        match ch {
196            Some(c) => self.type_char(c, &mut Cx::new(ctx)),
197            None => {
198                if !MODIFIER_KEYS.contains(&input.key.as_str()) {
199                    self.reset();
200                }
201                vec![]
202            }
203        }
204    }
205
206    fn type_char(&mut self, ch: char, cx: &mut Cx) -> Vec<Command> {
207        if cx.literal() {
208            self.reset();
209            return vec![Command::InsertText(ch.to_string())];
210        }
211        if ch == ' ' {
212            return self.convert(cx);
213        }
214        let prev = self.last.take();
215        if ch.is_ascii_alphabetic() {
216            self.word.push(ch);
217        } else {
218            self.word.clear();
219        }
220        let mut out = char_commands(ch, cx);
221        match prev.filter(|_| self.autocorrect).and_then(|p| autocorrect(p, ch, cx).map(|with| (p, with))) {
222            Some((p, with)) => out.push(Command::ReplaceTyped { typed: [p, ch].iter().collect(), with }),
223            None => self.last = Some(ch),
224        }
225        out
226    }
227
228    /// Replace the longest known word ending the pending word, an unknown word only ends.
229    fn convert(&mut self, cx: &mut Cx) -> Vec<Command> {
230        self.last = None;
231        let word = std::mem::take(&mut self.word);
232        let starts = if self.suffix_matching { 0..word.len() } else { 0..word.len().min(1) };
233        for start in starts {
234            let typed = &word[start..];
235            if let Some(with) = self.commands_for_word(typed) {
236                cx.text |= with.contains(&Command::InsertStyled(Variant::Text));
237                return vec![Command::ReplaceTyped { typed: typed.to_string(), with }];
238            }
239        }
240        vec![]
241    }
242}
243
244/// Caret facts for the next character, updated by hand between the characters of one `map_text` call.
245struct Cx {
246    text: bool,
247    menu: bool,
248    closers: Vec<char>,
249}
250
251impl Cx {
252    fn new(ctx: &InputContext) -> Self {
253        Self { text: ctx.in_text_slot, menu: ctx.menu_open, closers: ctx.closing_delimiter.into_iter().collect() }
254    }
255
256    /// Characters go through untouched, as text or as the open menu's filter.
257    fn literal(&self) -> bool {
258        self.text || self.menu
259    }
260
261    fn open(&mut self, open: char, close: char) -> Vec<Command> {
262        self.closers.push(close);
263        vec![Command::InsertDelimiters { open, close }]
264    }
265
266    fn closes(&self, close: char) -> bool {
267        self.closers.last() == Some(&close)
268    }
269
270    fn close(&mut self, close: char) -> Command {
271        self.closers.pop();
272        Command::CloseDelimiter(close)
273    }
274}
275
276/// Commands for one typed math character.
277fn char_commands(ch: char, cx: &mut Cx) -> Vec<Command> {
278    match ch {
279        '/' => vec![Command::InsertFraction(FracStyle::Bar)],
280        '^' => vec![Command::InsertScript(ScriptSlot::Sup)],
281        '_' => vec![Command::InsertScript(ScriptSlot::Sub)],
282        '(' => cx.open('(', ')'),
283        '[' => cx.open('[', ']'),
284        '{' => cx.open('{', '}'),
285        '|' if cx.closes('|') => vec![cx.close('|')],
286        '|' => cx.open('|', '|'),
287        ')' | ']' | '}' if cx.closes(ch) => vec![cx.close(ch)],
288        '\'' => vec![
289            Command::InsertScript(ScriptSlot::Sup),
290            Command::InsertAtom(Symbol::from_latex("\\prime")),
291            Command::Move(Dir::Right),
292        ],
293        '*' => vec![Command::InsertAtom(Symbol::from_latex("\\cdot"))],
294        c => Symbol::from_char(c).map(Command::InsertAtom).into_iter().collect(),
295    }
296}
297
298/// What replaces a typed pair, `<-`, `==`, `:-`, and `!=` are left alone since they occur in ordinary input.
299fn autocorrect(a: char, b: char, cx: &mut Cx) -> Option<Vec<Command>> {
300    let latex = match (a, b) {
301        ('<', '=') => "\\leq",
302        ('>', '=') => "\\geq",
303        ('~', '~') => "\\approx",
304        ('=', '~') => "\\cong",
305        ('-', '>') => "\\to",
306        ('=', '>') => "\\implies",
307        ('+', '-') => "\\pm",
308        ('-', '+') => "\\mp",
309        ('>', '>') if cx.closes('⟩') => return Some(vec![cx.close('⟩')]),
310        ('<', '<') | ('>', '>') => return Some(cx.open('⟨', '⟩')),
311        _ => return None,
312    };
313    Some(vec![Command::InsertAtom(Symbol::from_latex(latex))])
314}
315
316fn named_key(input: &KeyInput) -> Option<Command> {
317    let dir = |d| if input.shift { Command::Extend(d) } else { Command::Move(d) };
318    Some(match input.key.as_str() {
319        "ArrowLeft" => dir(Dir::Left),
320        "ArrowRight" => dir(Dir::Right),
321        "ArrowUp" => dir(Dir::Up),
322        "ArrowDown" => dir(Dir::Down),
323        "Home" => Command::MoveLineStart,
324        "End" => Command::MoveLineEnd,
325        "Tab" if input.shift => Command::ShiftTab,
326        "Tab" => Command::Tab,
327        "Backspace" => Command::DeleteBackward,
328        "Delete" => Command::DeleteForward,
329        "Enter" => Command::Confirm,
330        "Escape" => Command::Collapse,
331        _ => return None,
332    })
333}
334
335/// Keys pressed on the way to a character, which keep the pending word.
336const MODIFIER_KEYS: &[&str] = &[
337    "Shift", "Control", "Alt", "AltGraph", "Meta", "CapsLock", "NumLock", "ScrollLock", "Fn", "FnLock", "Hyper",
338    "Super", "Symbol", "SymbolLock", "OS", "Dead", "Compose", "Process", "Unidentified",
339];
340
341fn single_char(key: &str) -> Option<char> {
342    let mut it = key.chars();
343    match (it.next(), it.next()) {
344        (Some(c), None) => Some(c),
345        _ => None,
346    }
347}
348
349/// What a catalog word inserts.
350#[derive(Debug, Clone, Copy)]
351enum Insertion {
352    Symbol(&'static str),
353    Fraction(FracStyle),
354    Root,
355    BigOperator(&'static str),
356    Matrix(MatrixEnv),
357    Accent(Mark),
358    Styled(Variant),
359    Brace { over: bool },
360}
361
362impl Insertion {
363    fn commands(self) -> Vec<Command> {
364        let cmd = match self {
365            Insertion::Symbol(latex) => Command::InsertAtom(Symbol::from_latex(latex)),
366            Insertion::Fraction(style) => Command::InsertFraction(style),
367            Insertion::Root => Command::InsertSqrt,
368            Insertion::BigOperator(latex) => Command::InsertBigOp(Symbol::from_latex(latex)),
369            Insertion::Matrix(env) => Command::InsertMatrix { env, rows: 2, cols: 2 },
370            Insertion::Accent(mark) => Command::InsertAccent(mark),
371            Insertion::Styled(variant) => Command::InsertStyled(variant),
372            Insertion::Brace { over } => Command::InsertUnderOver(UnderOverSpec {
373                over,
374                under: !over,
375                over_deco: if over { Deco::Brace } else { Deco::None },
376                under_deco: if over { Deco::None } else { Deco::Brace },
377            }),
378        };
379        vec![cmd]
380    }
381}
382
383#[derive(Debug, Clone, Copy)]
384struct Shortcut {
385    word: &'static str,
386    label: &'static str,
387    insert: Insertion,
388}
389
390const fn sym(word: &'static str, latex: &'static str) -> Shortcut {
391    Shortcut { word, label: word, insert: Insertion::Symbol(latex) }
392}
393
394const fn bigop(word: &'static str, latex: &'static str) -> Shortcut {
395    Shortcut { word, label: word, insert: Insertion::BigOperator(latex) }
396}
397
398const fn shortcut(word: &'static str, label: &'static str, insert: Insertion) -> Shortcut {
399    Shortcut { word, label, insert }
400}
401
402const fn accent(word: &'static str, mark: Mark) -> Shortcut {
403    Shortcut { word, label: word, insert: Insertion::Accent(mark) }
404}
405
406const fn styled(word: &'static str, variant: Variant) -> Shortcut {
407    Shortcut { word, label: word, insert: Insertion::Styled(variant) }
408}
409
410/// The built in words in palette order.
411const CATALOG: &[Shortcut] = &[
412    // structures
413    shortcut("frac", "fraction", Insertion::Fraction(FracStyle::Bar)),
414    shortcut("dfrac", "display fraction", Insertion::Fraction(FracStyle::Display)),
415    shortcut("tfrac", "text fraction", Insertion::Fraction(FracStyle::Text)),
416    shortcut("binom", "binomial", Insertion::Fraction(FracStyle::Binom)),
417    shortcut("sqrt", "square root", Insertion::Root),
418    shortcut("root", "root", Insertion::Root),
419    shortcut("overbrace", "brace above", Insertion::Brace { over: true }),
420    shortcut("underbrace", "brace below", Insertion::Brace { over: false }),
421    // big operators with stacked limits
422    bigop("sum", "\\sum"),
423    bigop("prod", "\\prod"),
424    bigop("coprod", "\\coprod"),
425    bigop("int", "\\int"),
426    bigop("iint", "\\iint"),
427    bigop("iiint", "\\iiint"),
428    bigop("oint", "\\oint"),
429    bigop("bigcup", "\\bigcup"),
430    bigop("bigcap", "\\bigcap"),
431    bigop("bigsqcup", "\\bigsqcup"),
432    bigop("biguplus", "\\biguplus"),
433    bigop("bigoplus", "\\bigoplus"),
434    bigop("bigotimes", "\\bigotimes"),
435    bigop("bigodot", "\\bigodot"),
436    bigop("bigvee", "\\bigvee"),
437    bigop("bigwedge", "\\bigwedge"),
438    // matrices
439    shortcut("pmatrix", "matrix in parentheses", Insertion::Matrix(MatrixEnv::Pmatrix)),
440    shortcut("bmatrix", "matrix in brackets", Insertion::Matrix(MatrixEnv::Bmatrix)),
441    shortcut("vmatrix", "determinant", Insertion::Matrix(MatrixEnv::Vmatrix)),
442    shortcut("matrix", "plain matrix", Insertion::Matrix(MatrixEnv::Matrix)),
443    shortcut("cases", "cases", Insertion::Matrix(MatrixEnv::Cases)),
444    shortcut("aligned", "aligned equations", Insertion::Matrix(MatrixEnv::Aligned)),
445    shortcut("align", "aligned equations, short word", Insertion::Matrix(MatrixEnv::Aligned)),
446    shortcut("array", "array", Insertion::Matrix(MatrixEnv::Array)),
447    // accents
448    accent("hat", Mark::Hat),
449    accent("vec", Mark::Vec),
450    accent("bar", Mark::Bar),
451    accent("tilde", Mark::Tilde),
452    accent("dot", Mark::Dot),
453    accent("ddot", Mark::Ddot),
454    accent("widehat", Mark::Widehat),
455    accent("widetilde", Mark::Widetilde),
456    accent("overline", Mark::Overline),
457    accent("underline", Mark::Underline),
458    accent("check", Mark::Check),
459    accent("breve", Mark::Breve),
460    // styles
461    styled("bold", Variant::Bold),
462    styled("bb", Variant::Blackboard),
463    styled("mathbb", Variant::Blackboard),
464    styled("mathcal", Variant::Calligraphic),
465    styled("mathfrak", Variant::Fraktur),
466    styled("mathrm", Variant::Roman),
467    styled("mathsf", Variant::SansSerif),
468    styled("mathtt", Variant::Typewriter),
469    styled("text", Variant::Text),
470    styled("op", Variant::OperatorName),
471    styled("operatorname", Variant::OperatorName),
472    // lower greek
473    sym("alpha", "\\alpha"),
474    sym("beta", "\\beta"),
475    sym("gamma", "\\gamma"),
476    sym("delta", "\\delta"),
477    sym("epsilon", "\\epsilon"),
478    sym("varepsilon", "\\varepsilon"),
479    sym("zeta", "\\zeta"),
480    sym("eta", "\\eta"),
481    sym("theta", "\\theta"),
482    sym("vartheta", "\\vartheta"),
483    sym("iota", "\\iota"),
484    sym("kappa", "\\kappa"),
485    sym("lambda", "\\lambda"),
486    sym("mu", "\\mu"),
487    sym("nu", "\\nu"),
488    sym("xi", "\\xi"),
489    sym("pi", "\\pi"),
490    sym("varpi", "\\varpi"),
491    sym("rho", "\\rho"),
492    sym("varrho", "\\varrho"),
493    sym("sigma", "\\sigma"),
494    sym("varsigma", "\\varsigma"),
495    sym("tau", "\\tau"),
496    sym("upsilon", "\\upsilon"),
497    sym("phi", "\\phi"),
498    sym("varphi", "\\varphi"),
499    sym("chi", "\\chi"),
500    sym("psi", "\\psi"),
501    sym("omega", "\\omega"),
502    // upper greek
503    sym("Gamma", "\\Gamma"),
504    sym("Delta", "\\Delta"),
505    sym("Theta", "\\Theta"),
506    sym("Lambda", "\\Lambda"),
507    sym("Xi", "\\Xi"),
508    sym("Pi", "\\Pi"),
509    sym("Sigma", "\\Sigma"),
510    sym("Upsilon", "\\Upsilon"),
511    sym("Phi", "\\Phi"),
512    sym("Psi", "\\Psi"),
513    sym("Omega", "\\Omega"),
514    // letterlike and calculus symbols
515    sym("infty", "\\infty"),
516    sym("infinity", "\\infty"),
517    sym("partial", "\\partial"),
518    sym("nabla", "\\nabla"),
519    sym("ell", "\\ell"),
520    sym("hbar", "\\hbar"),
521    shortcut("dd", "differential", Insertion::Symbol("\\mathrm{d}")),
522    // binary operators
523    sym("pm", "\\pm"),
524    sym("mp", "\\mp"),
525    sym("times", "\\times"),
526    sym("div", "\\div"),
527    sym("cdot", "\\cdot"),
528    sym("ast", "\\ast"),
529    sym("star", "\\star"),
530    sym("circ", "\\circ"),
531    sym("oplus", "\\oplus"),
532    sym("otimes", "\\otimes"),
533    sym("setminus", "\\setminus"),
534    sym("cup", "\\cup"),
535    sym("cap", "\\cap"),
536    sym("wedge", "\\wedge"),
537    sym("vee", "\\vee"),
538    sym("land", "\\land"),
539    sym("lor", "\\lor"),
540    // relations
541    sym("leq", "\\leq"),
542    sym("le", "\\leq"),
543    sym("geq", "\\geq"),
544    sym("ge", "\\geq"),
545    sym("neq", "\\neq"),
546    sym("ne", "\\neq"),
547    sym("ll", "\\ll"),
548    sym("gg", "\\gg"),
549    sym("approx", "\\approx"),
550    sym("equiv", "\\equiv"),
551    sym("cong", "\\cong"),
552    sym("sim", "\\sim"),
553    sym("propto", "\\propto"),
554    sym("mid", "\\mid"),
555    sym("perp", "\\perp"),
556    sym("parallel", "\\parallel"),
557    sym("in", "\\in"),
558    sym("notin", "\\notin"),
559    sym("ni", "\\ni"),
560    sym("subset", "\\subset"),
561    sym("subseteq", "\\subseteq"),
562    sym("supset", "\\supset"),
563    sym("supseteq", "\\supseteq"),
564    // arrows
565    sym("to", "\\to"),
566    sym("gets", "\\gets"),
567    sym("mapsto", "\\mapsto"),
568    sym("rightarrow", "\\rightarrow"),
569    sym("leftarrow", "\\leftarrow"),
570    sym("Rightarrow", "\\Rightarrow"),
571    sym("Leftarrow", "\\Leftarrow"),
572    sym("Leftrightarrow", "\\Leftrightarrow"),
573    sym("Longrightarrow", "\\Longrightarrow"),
574    sym("implies", "\\implies"),
575    sym("iff", "\\iff"),
576    // sets and logic
577    sym("emptyset", "\\emptyset"),
578    sym("forall", "\\forall"),
579    sym("exists", "\\exists"),
580    sym("neg", "\\neg"),
581    // dots and misc
582    sym("cdots", "\\cdots"),
583    sym("ldots", "\\ldots"),
584    sym("dots", "\\dots"),
585    sym("vdots", "\\vdots"),
586    sym("ddots", "\\ddots"),
587    sym("angle", "\\angle"),
588    // upright operator names
589    sym("sin", "\\sin"),
590    sym("cos", "\\cos"),
591    sym("tan", "\\tan"),
592    sym("cot", "\\cot"),
593    sym("sec", "\\sec"),
594    sym("csc", "\\csc"),
595    sym("sinh", "\\sinh"),
596    sym("cosh", "\\cosh"),
597    sym("tanh", "\\tanh"),
598    sym("arcsin", "\\arcsin"),
599    sym("arccos", "\\arccos"),
600    sym("arctan", "\\arctan"),
601    sym("log", "\\log"),
602    sym("ln", "\\ln"),
603    sym("exp", "\\exp"),
604    sym("lim", "\\lim"),
605    sym("max", "\\max"),
606    sym("min", "\\min"),
607    sym("sup", "\\sup"),
608    sym("inf", "\\inf"),
609    sym("gcd", "\\gcd"),
610    sym("det", "\\det"),
611    sym("dim", "\\dim"),
612    sym("ker", "\\ker"),
613    sym("arg", "\\arg"),
614    sym("deg", "\\deg"),
615    sym("hom", "\\hom"),
616];