1#![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#[derive(Debug, Clone)]
11pub struct KeyInput {
12 pub key: String,
14 pub shift: bool,
16 pub ctrl: bool,
18 pub alt: bool,
20 pub meta: bool,
22}
23
24#[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 pub fn new() -> Self {
35 Self::default()
36 }
37
38 pub fn define_word(&mut self, word: impl Into<String>, commands: Vec<Command>) {
40 self.word_overrides.insert(word.into(), commands);
41 }
42
43 pub fn undefine_word(&mut self, word: &str) {
45 self.word_overrides.remove(word);
46 }
47
48 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 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" => Some(Command::Confirm),
85 "Escape" => Some(Command::Collapse), _ => None,
87 };
88 if let Some(cmd) = named {
89 self.reset();
90 return if matches!(cmd, Command::Collapse) {
91 vec![cmd] } 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 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 if ch == ' ' {
125 return self.flush_word();
126 }
127 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 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 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 pub fn commands_for_word(&self, word: &str) -> Option<Vec<Command>> {
187 self.lookup(word)
188 }
189}
190
191#[derive(Debug, Clone, Copy)]
193enum Insertion {
194 Symbol(&'static str),
196 Fraction(FracStyle),
197 Root,
198 BigOperator(&'static str),
200 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#[derive(Debug, Clone, Copy)]
221struct Shortcut {
222 word: &'static str,
223 label: &'static str,
224 insert: Insertion,
225}
226
227#[derive(Debug, Clone, Copy)]
229pub struct KeymapEntry<'a> {
230 pub word: &'a str,
232 pub label: &'a str,
234}
235
236const fn sym(word: &'static str, latex: &'static str) -> Shortcut {
238 Shortcut { word, label: word, insert: Insertion::Symbol(latex) }
239}
240
241const fn bigop(word: &'static str, latex: &'static str) -> Shortcut {
243 Shortcut { word, label: word, insert: Insertion::BigOperator(latex) }
244}
245
246const CATALOG: &[Shortcut] = &[
248 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 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 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 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 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 sym("infty", "\\infty"),
320 sym("infinity", "\\infty"),
321 sym("partial", "\\partial"),
322 sym("nabla", "\\nabla"),
323 Shortcut { word: "dd", label: "differential", insert: Insertion::Symbol("\\mathrm{d}") },
325 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 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 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) }
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
425fn 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
446const 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 #[test]
537 fn map_text_replays_word_lookup_fraction_and_autocorrect() {
538 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")); assert!(words.contains(&"sum")); assert!(words.contains(&"alpha")); assert!(words.contains(&"sin")); }
736}