Skip to main content

mathtex_editor_keymap/
lib.rs

1//! The default Hypatia keymap maps host input to primitive `Command`s.
2#![allow(dead_code)]
3
4use std::collections::HashMap;
5
6use mathtex_editor_core::command::{Command, Dir};
7use mathtex_editor_core::model::{FracStyle, MathClass, MatrixEnv, ScriptSlot, Symbol};
8
9/// A normalized key event from the host matching browser `KeyboardEvent` fields.
10#[derive(Debug, Clone)]
11pub struct KeyInput {
12    /// The `KeyboardEvent.key` value such as `"a"`, `"/"`, `"ArrowLeft"`, or `"Backspace"`.
13    pub key: String,
14    /// Whether the shift modifier is active.
15    pub shift: bool,
16    /// Whether the control modifier is active.
17    pub ctrl: bool,
18    /// Whether the alt modifier is active.
19    pub alt: bool,
20    /// Whether the meta modifier is active.
21    pub meta: bool,
22}
23
24/// Input state for character autocorrect, word autoconvert, and host word shortcuts.
25#[derive(Debug, Default)]
26pub struct Keymap {
27    last_char: Option<char>,
28    word: String,
29    word_overrides: HashMap<String, Vec<Command>>,
30}
31
32impl Keymap {
33    /// Create an empty keymap with the built in shortcuts.
34    pub fn new() -> Self {
35        Self::default()
36    }
37
38    /// Register a user word shortcut so `word` plus space expands to `commands`.
39    pub fn define_word(&mut self, word: impl Into<String>, commands: Vec<Command>) {
40        self.word_overrides.insert(word.into(), commands);
41    }
42
43    /// Remove a previously registered word shortcut while leaving built ins unaffected.
44    pub fn undefine_word(&mut self, word: &str) {
45        self.word_overrides.remove(word);
46    }
47
48    /// Return words the keymap can expand for a `\` command popover.
49    pub fn entries(&self) -> Vec<KeymapEntry<'_>> {
50        let mut out: Vec<KeymapEntry<'_>> = Vec::new();
51        for w in self.word_overrides.keys() {
52            out.push(KeymapEntry { word: w.as_str(), label: w.as_str() });
53        }
54        for s in CATALOG {
55            if !self.word_overrides.contains_key(s.word) {
56                out.push(KeymapEntry { word: s.word, label: s.label });
57            }
58        }
59        for &op in OPERATORS {
60            if !self.word_overrides.contains_key(op) {
61                out.push(KeymapEntry { word: op, label: op });
62            }
63        }
64        out
65    }
66
67    /// Translate a key event into zero or more editor commands.
68    pub fn map_key(&mut self, input: &KeyInput) -> Vec<Command> {
69        let named = match input.key.as_str() {
70            "ArrowLeft" => Some(dir_cmd(Dir::Left, input.shift)),
71            "ArrowRight" => Some(dir_cmd(Dir::Right, input.shift)),
72            "ArrowUp" => Some(dir_cmd(Dir::Up, input.shift)),
73            "ArrowDown" => Some(dir_cmd(Dir::Down, input.shift)),
74            "Home" => Some(Command::MoveLineStart),
75            "End" => Some(Command::MoveLineEnd),
76            "Tab" => Some(if input.shift {
77                Command::ShiftTab
78            } else {
79                Command::Tab
80            }),
81            "Backspace" => Some(Command::DeleteBackward),
82            "Delete" => Some(Command::DeleteForward),
83            // Enter confirms a pending Backspace swap menu or behaves like Collapse.
84            "Enter" => Some(Command::Confirm),
85            "Escape" => Some(Command::Collapse), // host decides; reset state
86            _ => None,
87        };
88        if let Some(cmd) = named {
89            self.reset();
90            return if matches!(cmd, Command::Collapse) {
91                vec![cmd] // benign reset, host may also act
92            } else {
93                vec![cmd]
94            };
95        }
96
97        if input.ctrl || input.meta {
98            self.reset();
99            return match input.key.as_str() {
100                "a" | "A" => vec![Command::SelectAll],
101                _ => vec![],
102            };
103        }
104
105        match single_char(&input.key) {
106            Some(ch) => self.map_char(ch),
107            None => vec![],
108        }
109    }
110
111    /// Translate committed text by replaying it through `map_char` one character at a time.
112    pub fn map_text(&mut self, text: &str) -> Vec<Command> {
113        self.reset();
114        text.chars().flat_map(|ch| self.map_char(ch)).collect()
115    }
116
117    fn reset(&mut self) {
118        self.last_char = None;
119        self.word.clear();
120    }
121
122    fn map_char(&mut self, ch: char) -> Vec<Command> {
123        // Space converts the preceding word or is swallowed.
124        if ch == ' ' {
125            return self.flush_word();
126        }
127        // Two character symbol autocorrect.
128        if let Some(prev) = self.last_char {
129            if let Some(repl) = two_char(prev, ch) {
130                self.reset();
131                return vec![Command::DeleteBackward, insert(repl)];
132            }
133        }
134        self.last_char = Some(ch);
135        if ch.is_ascii_alphabetic() {
136            self.word.push(ch);
137        } else {
138            self.word.clear();
139        }
140        match ch {
141            '/' => vec![Command::InsertFraction(FracStyle::Bar)],
142            '^' => vec![Command::InsertScript(ScriptSlot::Sup)],
143            '_' => vec![Command::InsertScript(ScriptSlot::Sub)],
144            '(' => vec![delims('(', ')')],
145            '[' => vec![delims('[', ']')],
146            '{' => vec![delims('{', '}')],
147            '|' => vec![delims('|', '|')],
148            '\'' => vec![Command::InsertScript(ScriptSlot::Sup), insert("\\prime")],
149            '*' => vec![insert("\\cdot")],
150            c => vec![Command::InsertAtom(atom(c))],
151        }
152    }
153
154    /// Convert the buffered word on space or swallow the space if the word is unknown.
155    fn flush_word(&mut self) -> Vec<Command> {
156        self.last_char = None;
157        let word = std::mem::take(&mut self.word);
158        if word.is_empty() {
159            return vec![];
160        }
161        match self.lookup(&word) {
162            Some(mut cmds) => {
163                let mut out = vec![Command::DeleteBackward; word.chars().count()];
164                out.append(&mut cmds);
165                out
166            }
167            None => vec![],
168        }
169    }
170
171    /// Resolve a word using host overrides, catalog entries, then upright operators.
172    fn lookup(&self, word: &str) -> Option<Vec<Command>> {
173        if let Some(cmds) = self.word_overrides.get(word) {
174            return Some(cmds.clone());
175        }
176        if let Some(s) = CATALOG.iter().find(|s| s.word == word) {
177            return Some(s.insert.commands());
178        }
179        if OPERATORS.contains(&word) {
180            return Some(vec![named_atom(&format!("\\{word}"))]);
181        }
182        None
183    }
184
185    /// Resolve a word directly for a host rendered `\` command palette.
186    pub fn commands_for_word(&self, word: &str) -> Option<Vec<Command>> {
187        self.lookup(word)
188    }
189}
190
191/// The insertion shape shared by catalog entries.
192#[derive(Debug, Clone, Copy)]
193enum Insertion {
194    /// A named atom whose class comes from [`class_of_latex`].
195    Symbol(&'static str),
196    Fraction(FracStyle),
197    Root,
198    /// A big operator with stacked limit slots.
199    BigOperator(&'static str),
200    /// A matrix like environment with a fixed `rows` x `cols` grid.
201    Matrix(MatrixEnv, usize, usize),
202}
203
204impl Insertion {
205    fn commands(self) -> Vec<Command> {
206        match self {
207            Insertion::Symbol(latex) => vec![named_atom(latex)],
208            Insertion::Fraction(style) => vec![Command::InsertFraction(style)],
209            Insertion::Root => vec![Command::InsertSqrt],
210            Insertion::BigOperator(latex) => vec![Command::InsertBigOp(Symbol {
211                latex: latex.to_string(),
212                class: MathClass::Op,
213            })],
214            Insertion::Matrix(env, rows, cols) => vec![Command::InsertMatrix { env, rows, cols }],
215        }
216    }
217}
218
219/// One catalog row with a trigger word, display label, and insertion.
220#[derive(Debug, Clone, Copy)]
221struct Shortcut {
222    word: &'static str,
223    label: &'static str,
224    insert: Insertion,
225}
226
227/// A popover row with a trigger word and display label.
228#[derive(Debug, Clone, Copy)]
229pub struct KeymapEntry<'a> {
230    /// The shortcut trigger word.
231    pub word: &'a str,
232    /// The label shown for the shortcut.
233    pub label: &'a str,
234}
235
236/// Terse constructor for a plain named symbol entry whose label defaults to the word.
237const fn sym(word: &'static str, latex: &'static str) -> Shortcut {
238    Shortcut { word, label: word, insert: Insertion::Symbol(latex) }
239}
240
241/// Terse constructor for a big operator entry whose label defaults to the word.
242const fn bigop(word: &'static str, latex: &'static str) -> Shortcut {
243    Shortcut { word, label: word, insert: Insertion::BigOperator(latex) }
244}
245
246/// The built in word to insertion catalog for structures, big operators, and named symbols.
247const CATALOG: &[Shortcut] = &[
248    // structures
249    Shortcut { word: "frac", label: "fraction", insert: Insertion::Fraction(FracStyle::Bar) },
250    Shortcut { word: "dfrac", label: "display fraction", insert: Insertion::Fraction(FracStyle::Display) },
251    Shortcut { word: "binom", label: "binomial", insert: Insertion::Fraction(FracStyle::Binom) },
252    Shortcut { word: "sqrt", label: "square root", insert: Insertion::Root },
253    Shortcut { word: "root", label: "root", insert: Insertion::Root },
254    // big operators with stacked limits
255    bigop("sum", "\\sum"),
256    bigop("prod", "\\prod"),
257    bigop("coprod", "\\coprod"),
258    bigop("int", "\\int"),
259    bigop("iint", "\\iint"),
260    bigop("iiint", "\\iiint"),
261    bigop("oint", "\\oint"),
262    bigop("bigcup", "\\bigcup"),
263    bigop("bigcap", "\\bigcap"),
264    bigop("bigsqcup", "\\bigsqcup"),
265    bigop("biguplus", "\\biguplus"),
266    bigop("bigoplus", "\\bigoplus"),
267    bigop("bigotimes", "\\bigotimes"),
268    bigop("bigodot", "\\bigodot"),
269    bigop("bigvee", "\\bigvee"),
270    bigop("bigwedge", "\\bigwedge"),
271    // matrices
272    Shortcut { word: "pmatrix", label: "matrix (parentheses)", insert: Insertion::Matrix(MatrixEnv::Pmatrix, 2, 2) },
273    Shortcut { word: "bmatrix", label: "matrix (brackets)", insert: Insertion::Matrix(MatrixEnv::Bmatrix, 2, 2) },
274    Shortcut { word: "vmatrix", label: "matrix (bars / determinant)", insert: Insertion::Matrix(MatrixEnv::Vmatrix, 2, 2) },
275    Shortcut { word: "matrix", label: "matrix (plain)", insert: Insertion::Matrix(MatrixEnv::Matrix, 2, 2) },
276    Shortcut { word: "cases", label: "cases", insert: Insertion::Matrix(MatrixEnv::Cases, 2, 2) },
277    Shortcut { word: "aligned", label: "aligned equations", insert: Insertion::Matrix(MatrixEnv::Aligned, 2, 2) },
278    Shortcut { word: "align", label: "aligned equations", insert: Insertion::Matrix(MatrixEnv::Aligned, 2, 2) },
279    Shortcut { word: "array", label: "array", insert: Insertion::Matrix(MatrixEnv::Array, 2, 2) },
280    // lower greek
281    sym("alpha", "\\alpha"),
282    sym("beta", "\\beta"),
283    sym("gamma", "\\gamma"),
284    sym("delta", "\\delta"),
285    sym("epsilon", "\\epsilon"),
286    sym("varepsilon", "\\varepsilon"),
287    sym("zeta", "\\zeta"),
288    sym("eta", "\\eta"),
289    sym("theta", "\\theta"),
290    sym("vartheta", "\\vartheta"),
291    sym("iota", "\\iota"),
292    sym("kappa", "\\kappa"),
293    sym("lambda", "\\lambda"),
294    sym("mu", "\\mu"),
295    sym("nu", "\\nu"),
296    sym("xi", "\\xi"),
297    sym("pi", "\\pi"),
298    sym("rho", "\\rho"),
299    sym("sigma", "\\sigma"),
300    sym("tau", "\\tau"),
301    sym("upsilon", "\\upsilon"),
302    sym("phi", "\\phi"),
303    sym("varphi", "\\varphi"),
304    sym("chi", "\\chi"),
305    sym("psi", "\\psi"),
306    sym("omega", "\\omega"),
307    // upper greek
308    sym("Gamma", "\\Gamma"),
309    sym("Delta", "\\Delta"),
310    sym("Theta", "\\Theta"),
311    sym("Lambda", "\\Lambda"),
312    sym("Xi", "\\Xi"),
313    sym("Pi", "\\Pi"),
314    sym("Sigma", "\\Sigma"),
315    sym("Phi", "\\Phi"),
316    sym("Psi", "\\Psi"),
317    sym("Omega", "\\Omega"),
318    // calculus and misc symbols
319    sym("infty", "\\infty"),
320    sym("infinity", "\\infty"),
321    sym("partial", "\\partial"),
322    sym("nabla", "\\nabla"),
323    // The differential is upright `\mathrm{d}` for integrals and derivatives.
324    Shortcut { word: "dd", label: "differential", insert: Insertion::Symbol("\\mathrm{d}") },
325    // binary operators and relations
326    sym("pm", "\\pm"),
327    sym("mp", "\\mp"),
328    sym("times", "\\times"),
329    sym("div", "\\div"),
330    sym("cdot", "\\cdot"),
331    sym("ast", "\\ast"),
332    sym("star", "\\star"),
333    sym("leq", "\\leq"),
334    sym("le", "\\leq"),
335    sym("geq", "\\geq"),
336    sym("ge", "\\geq"),
337    sym("neq", "\\neq"),
338    sym("ne", "\\neq"),
339    sym("approx", "\\approx"),
340    sym("equiv", "\\equiv"),
341    sym("cong", "\\cong"),
342    sym("sim", "\\sim"),
343    sym("propto", "\\propto"),
344    sym("to", "\\to"),
345    sym("gets", "\\gets"),
346    sym("implies", "\\implies"),
347    sym("iff", "\\iff"),
348    // sets and logic
349    sym("in", "\\in"),
350    sym("notin", "\\notin"),
351    sym("subset", "\\subset"),
352    sym("subseteq", "\\subseteq"),
353    sym("supset", "\\supset"),
354    sym("cup", "\\cup"),
355    sym("cap", "\\cap"),
356    sym("emptyset", "\\emptyset"),
357    sym("forall", "\\forall"),
358    sym("exists", "\\exists"),
359    sym("neg", "\\neg"),
360    sym("land", "\\land"),
361    sym("lor", "\\lor"),
362    // dots, arrows, and misc symbols
363    sym("cdots", "\\cdots"),
364    sym("ldots", "\\ldots"),
365    sym("dots", "\\dots"),
366    sym("rightarrow", "\\rightarrow"),
367    sym("leftarrow", "\\leftarrow"),
368    sym("angle", "\\angle"),
369    sym("perp", "\\perp"),
370    sym("parallel", "\\parallel"),
371];
372
373fn dir_cmd(dir: Dir, shift: bool) -> Command {
374    if shift {
375        Command::Extend(dir)
376    } else {
377        Command::Move(dir)
378    }
379}
380
381fn delims(open: char, close: char) -> Command {
382    Command::InsertDelimiters { open, close }
383}
384
385fn single_char(key: &str) -> Option<char> {
386    let mut it = key.chars();
387    match (it.next(), it.next()) {
388        (Some(c), None) => Some(c),
389        _ => None,
390    }
391}
392
393fn atom(c: char) -> Symbol {
394    Symbol::from_char(c) // escapes TeX specials like %, $, ~, and \
395}
396
397fn insert(latex: &str) -> Command {
398    Command::InsertAtom(Symbol {
399        latex: latex.to_string(),
400        class: class_of_latex(latex),
401    })
402}
403
404fn named_atom(latex: &str) -> Command {
405    insert(latex)
406}
407
408fn class_of_latex(latex: &str) -> MathClass {
409    if let Some(name) = latex.strip_prefix('\\') {
410        if OPERATORS.contains(&name) {
411            return MathClass::Op;
412        }
413    }
414    match latex {
415        "\\leq" | "\\geq" | "\\neq" | "\\equiv" | "\\approx" | "\\cong" | "\\to" | "\\sim"
416        | "\\implies" | "\\iff" | "\\gets" | "\\propto" | "\\in" | "\\notin" | "\\subset"
417        | "\\subseteq" | "\\supset" | "\\rightarrow" | "\\leftarrow" | "\\Rightarrow"
418        | "\\Leftarrow" | "\\perp" | "\\parallel" => MathClass::Rel,
419        "\\pm" | "\\mp" | "\\times" | "\\div" | "\\cdot" | "\\ast" | "\\star" | "\\cup"
420        | "\\cap" => MathClass::Bin,
421        _ => MathClass::Ord,
422    }
423}
424
425/// The Hypatia two character autoconvert table.
426fn two_char(a: char, b: char) -> Option<&'static str> {
427    Some(match (a, b) {
428        ('<', '=') => "\\leq",
429        ('>', '=') => "\\geq",
430        ('!', '=') => "\\neq",
431        ('=', '=') => "\\equiv",
432        ('=', '~') => "\\cong",
433        ('~', '~') => "\\approx",
434        ('-', '>') => "\\to",
435        ('<', '-') => "\\gets",
436        ('=', '>') => "\\implies",
437        ('+', '-') => "\\pm",
438        ('-', '+') => "\\mp",
439        (':', '-') => "\\div",
440        ('<', '<') => "\\langle",
441        ('>', '>') => "\\rangle",
442        _ => return None,
443    })
444}
445
446/// Named operators that render upright and feed word lookup.
447const OPERATORS: &[&str] = &[
448    "sin", "cos", "tan", "cot", "sec", "csc", "sinh", "cosh", "tanh", "arcsin", "arccos", "arctan",
449    "log", "ln", "exp", "lim", "max", "min", "sup", "inf", "gcd", "det", "dim", "ker", "arg", "deg",
450    "hom",
451];
452
453#[cfg(test)]
454mod tests {
455    use super::*;
456    use mathtex_editor_core::model::MatrixEnv;
457
458    fn k(key: &str) -> KeyInput {
459        KeyInput {
460            key: key.into(),
461            shift: false,
462            ctrl: false,
463            alt: false,
464            meta: false,
465        }
466    }
467
468    fn sym(latex: &str, class: MathClass) -> Command {
469        Command::InsertAtom(Symbol {
470            latex: latex.into(),
471            class,
472        })
473    }
474
475    #[test]
476    fn arrows_and_shift() {
477        let mut km = Keymap::new();
478        assert_eq!(km.map_key(&k("ArrowLeft")), vec![Command::Move(Dir::Left)]);
479        let mut shifted = k("ArrowRight");
480        shifted.shift = true;
481        assert_eq!(km.map_key(&shifted), vec![Command::Extend(Dir::Right)]);
482    }
483
484    #[test]
485    fn structure_keys() {
486        let mut km = Keymap::new();
487        assert_eq!(
488            km.map_key(&k("/")),
489            vec![Command::InsertFraction(FracStyle::Bar)]
490        );
491        assert_eq!(
492            km.map_key(&k("(")),
493            vec![Command::InsertDelimiters {
494                open: '(',
495                close: ')'
496            }]
497        );
498    }
499
500    #[test]
501    fn autocorrect_two_char() {
502        let mut km = Keymap::new();
503        assert_eq!(km.map_key(&k("<")), vec![sym("<", MathClass::Rel)]);
504        assert_eq!(
505            km.map_key(&k("=")),
506            vec![Command::DeleteBackward, sym("\\leq", MathClass::Rel)]
507        );
508    }
509
510    #[test]
511    fn implies_arrow() {
512        let mut km = Keymap::new();
513        km.map_key(&k("="));
514        assert_eq!(
515            km.map_key(&k(">")),
516            vec![Command::DeleteBackward, sym("\\implies", MathClass::Rel)]
517        );
518    }
519
520    #[test]
521    fn word_on_space_converts() {
522        let mut km = Keymap::new();
523        km.map_key(&k("p"));
524        km.map_key(&k("i"));
525        assert_eq!(
526            km.map_key(&k(" ")),
527            vec![
528                Command::DeleteBackward,
529                Command::DeleteBackward,
530                sym("\\pi", MathClass::Ord)
531            ]
532        );
533    }
534
535    /// `map_text` must replay every character through `map_char` so shortcuts still fire.
536    #[test]
537    fn map_text_replays_word_lookup_fraction_and_autocorrect() {
538        // `map_text` queues inserts before the space replacement, matching typed input order.
539        let mut km = Keymap::new();
540        assert_eq!(
541            km.map_text("pi "),
542            vec![
543                sym("p", MathClass::Ord),
544                sym("i", MathClass::Ord),
545                Command::DeleteBackward,
546                Command::DeleteBackward,
547                sym("\\pi", MathClass::Ord)
548            ]
549        );
550
551        let mut km = Keymap::new();
552        assert_eq!(
553            km.map_text("a/b"),
554            vec![
555                sym("a", MathClass::Ord),
556                Command::InsertFraction(FracStyle::Bar),
557                sym("b", MathClass::Ord),
558            ]
559        );
560
561        let mut km = Keymap::new();
562        assert_eq!(
563            km.map_text("a<=b"),
564            vec![
565                sym("a", MathClass::Ord),
566                sym("<", MathClass::Rel),
567                Command::DeleteBackward,
568                sym("\\leq", MathClass::Rel),
569                sym("b", MathClass::Ord),
570            ]
571        );
572    }
573
574    #[test]
575    fn operator_word_converts() {
576        let mut km = Keymap::new();
577        for c in ["s", "i", "n"] {
578            km.map_key(&k(c));
579        }
580        assert_eq!(
581            km.map_key(&k(" ")),
582            vec![
583                Command::DeleteBackward,
584                Command::DeleteBackward,
585                Command::DeleteBackward,
586                sym("\\sin", MathClass::Op)
587            ]
588        );
589    }
590
591    #[test]
592    fn bare_space_inserts_nothing() {
593        let mut km = Keymap::new();
594        assert_eq!(km.map_key(&k(" ")), Vec::<Command>::new());
595    }
596
597    #[test]
598    fn unknown_word_then_space_is_dropped() {
599        let mut km = Keymap::new();
600        km.map_key(&k("x"));
601        km.map_key(&k("y"));
602        assert_eq!(km.map_key(&k(" ")), Vec::<Command>::new());
603    }
604
605    #[test]
606    fn plain_letter_inserts_atom() {
607        let mut km = Keymap::new();
608        assert_eq!(km.map_key(&k("a")), vec![sym("a", MathClass::Ord)]);
609    }
610
611    #[test]
612    fn structure_word_builds_bigop() {
613        let mut km = Keymap::new();
614        for c in ["s", "u", "m"] {
615            km.map_key(&k(c));
616        }
617        assert_eq!(
618            km.map_key(&k(" ")),
619            vec![
620                Command::DeleteBackward,
621                Command::DeleteBackward,
622                Command::DeleteBackward,
623                Command::InsertBigOp(Symbol {
624                    latex: "\\sum".into(),
625                    class: MathClass::Op
626                }),
627            ]
628        );
629    }
630
631    #[test]
632    fn structure_word_builds_fraction() {
633        let mut km = Keymap::new();
634        for c in ["f", "r", "a", "c"] {
635            km.map_key(&k(c));
636        }
637        assert_eq!(
638            km.map_key(&k(" ")),
639            vec![
640                Command::DeleteBackward,
641                Command::DeleteBackward,
642                Command::DeleteBackward,
643                Command::DeleteBackward,
644                Command::InsertFraction(FracStyle::Bar),
645            ]
646        );
647    }
648
649    #[test]
650    fn structure_word_builds_pmatrix() {
651        let mut km = Keymap::new();
652        for c in ["p", "m", "a", "t", "r", "i", "x"] {
653            km.map_key(&k(c));
654        }
655        assert_eq!(
656            km.map_key(&k(" ")),
657            vec![
658                Command::DeleteBackward,
659                Command::DeleteBackward,
660                Command::DeleteBackward,
661                Command::DeleteBackward,
662                Command::DeleteBackward,
663                Command::DeleteBackward,
664                Command::DeleteBackward,
665                Command::InsertMatrix {
666                    env: MatrixEnv::Pmatrix,
667                    rows: 2,
668                    cols: 2
669                },
670            ]
671        );
672    }
673
674    #[test]
675    fn structure_word_builds_cases() {
676        let mut km = Keymap::new();
677        for c in ["c", "a", "s", "e", "s"] {
678            km.map_key(&k(c));
679        }
680        assert_eq!(
681            km.map_key(&k(" ")),
682            vec![
683                Command::DeleteBackward,
684                Command::DeleteBackward,
685                Command::DeleteBackward,
686                Command::DeleteBackward,
687                Command::DeleteBackward,
688                Command::InsertMatrix {
689                    env: MatrixEnv::Cases,
690                    rows: 2,
691                    cols: 2
692                },
693            ]
694        );
695    }
696
697    #[test]
698    fn host_defined_word_overrides_and_inserts() {
699        let mut km = Keymap::new();
700        km.define_word("RR", vec![sym("\\mathbb{R}", MathClass::Ord)]);
701        for c in ["R", "R"] {
702            km.map_key(&k(c));
703        }
704        assert_eq!(
705            km.map_key(&k(" ")),
706            vec![
707                Command::DeleteBackward,
708                Command::DeleteBackward,
709                sym("\\mathbb{R}", MathClass::Ord)
710            ]
711        );
712    }
713
714    #[test]
715    fn override_shadows_builtin() {
716        let mut km = Keymap::new();
717        km.define_word("pi", vec![sym("\\varpi", MathClass::Ord)]);
718        for c in ["p", "i"] {
719            km.map_key(&k(c));
720        }
721        assert_eq!(
722            km.map_key(&k(" ")).last().cloned(),
723            Some(sym("\\varpi", MathClass::Ord))
724        );
725    }
726
727    #[test]
728    fn entries_are_enumerable_for_popover() {
729        let km = Keymap::new();
730        let words: Vec<&str> = km.entries().iter().map(|e| e.word).collect();
731        assert!(words.contains(&"frac")); // structure
732        assert!(words.contains(&"sum")); // big operator
733        assert!(words.contains(&"alpha")); // symbol
734        assert!(words.contains(&"sin")); // operator
735    }
736}