#![allow(dead_code)]
use std::collections::HashMap;
use mathtex_editor_core::command::{Command, Dir};
use mathtex_editor_core::model::{FracStyle, MathClass, MatrixEnv, ScriptSlot, Symbol};
#[derive(Debug, Clone)]
pub struct KeyInput {
pub key: String,
pub shift: bool,
pub ctrl: bool,
pub alt: bool,
pub meta: bool,
}
#[derive(Debug, Default)]
pub struct Keymap {
last_char: Option<char>,
word: String,
word_overrides: HashMap<String, Vec<Command>>,
}
impl Keymap {
pub fn new() -> Self {
Self::default()
}
pub fn define_word(&mut self, word: impl Into<String>, commands: Vec<Command>) {
self.word_overrides.insert(word.into(), commands);
}
pub fn undefine_word(&mut self, word: &str) {
self.word_overrides.remove(word);
}
pub fn entries(&self) -> Vec<KeymapEntry<'_>> {
let mut out: Vec<KeymapEntry<'_>> = Vec::new();
for w in self.word_overrides.keys() {
out.push(KeymapEntry { word: w.as_str(), label: w.as_str() });
}
for s in CATALOG {
if !self.word_overrides.contains_key(s.word) {
out.push(KeymapEntry { word: s.word, label: s.label });
}
}
for &op in OPERATORS {
if !self.word_overrides.contains_key(op) {
out.push(KeymapEntry { word: op, label: op });
}
}
out
}
pub fn map_key(&mut self, input: &KeyInput) -> Vec<Command> {
let named = match input.key.as_str() {
"ArrowLeft" => Some(dir_cmd(Dir::Left, input.shift)),
"ArrowRight" => Some(dir_cmd(Dir::Right, input.shift)),
"ArrowUp" => Some(dir_cmd(Dir::Up, input.shift)),
"ArrowDown" => Some(dir_cmd(Dir::Down, input.shift)),
"Home" => Some(Command::MoveLineStart),
"End" => Some(Command::MoveLineEnd),
"Tab" => Some(if input.shift {
Command::ShiftTab
} else {
Command::Tab
}),
"Backspace" => Some(Command::DeleteBackward),
"Delete" => Some(Command::DeleteForward),
"Enter" => Some(Command::Confirm),
"Escape" => Some(Command::Collapse), _ => None,
};
if let Some(cmd) = named {
self.reset();
return if matches!(cmd, Command::Collapse) {
vec![cmd] } else {
vec![cmd]
};
}
if input.ctrl || input.meta {
self.reset();
return match input.key.as_str() {
"a" | "A" => vec![Command::SelectAll],
_ => vec![],
};
}
match single_char(&input.key) {
Some(ch) => self.map_char(ch),
None => vec![],
}
}
pub fn map_text(&mut self, text: &str) -> Vec<Command> {
self.reset();
text.chars().flat_map(|ch| self.map_char(ch)).collect()
}
fn reset(&mut self) {
self.last_char = None;
self.word.clear();
}
fn map_char(&mut self, ch: char) -> Vec<Command> {
if ch == ' ' {
return self.flush_word();
}
if let Some(prev) = self.last_char {
if let Some(repl) = two_char(prev, ch) {
self.reset();
return vec![Command::DeleteBackward, insert(repl)];
}
}
self.last_char = Some(ch);
if ch.is_ascii_alphabetic() {
self.word.push(ch);
} else {
self.word.clear();
}
match ch {
'/' => vec![Command::InsertFraction(FracStyle::Bar)],
'^' => vec![Command::InsertScript(ScriptSlot::Sup)],
'_' => vec![Command::InsertScript(ScriptSlot::Sub)],
'(' => vec![delims('(', ')')],
'[' => vec![delims('[', ']')],
'{' => vec![delims('{', '}')],
'|' => vec![delims('|', '|')],
'\'' => vec![Command::InsertScript(ScriptSlot::Sup), insert("\\prime")],
'*' => vec![insert("\\cdot")],
c => vec![Command::InsertAtom(atom(c))],
}
}
fn flush_word(&mut self) -> Vec<Command> {
self.last_char = None;
let word = std::mem::take(&mut self.word);
if word.is_empty() {
return vec![];
}
match self.lookup(&word) {
Some(mut cmds) => {
let mut out = vec![Command::DeleteBackward; word.chars().count()];
out.append(&mut cmds);
out
}
None => vec![],
}
}
fn lookup(&self, word: &str) -> Option<Vec<Command>> {
if let Some(cmds) = self.word_overrides.get(word) {
return Some(cmds.clone());
}
if let Some(s) = CATALOG.iter().find(|s| s.word == word) {
return Some(s.insert.commands());
}
if OPERATORS.contains(&word) {
return Some(vec![named_atom(&format!("\\{word}"))]);
}
None
}
pub fn commands_for_word(&self, word: &str) -> Option<Vec<Command>> {
self.lookup(word)
}
}
#[derive(Debug, Clone, Copy)]
enum Insertion {
Symbol(&'static str),
Fraction(FracStyle),
Root,
BigOperator(&'static str),
Matrix(MatrixEnv, usize, usize),
}
impl Insertion {
fn commands(self) -> Vec<Command> {
match self {
Insertion::Symbol(latex) => vec![named_atom(latex)],
Insertion::Fraction(style) => vec![Command::InsertFraction(style)],
Insertion::Root => vec![Command::InsertSqrt],
Insertion::BigOperator(latex) => vec![Command::InsertBigOp(Symbol {
latex: latex.to_string(),
class: MathClass::Op,
})],
Insertion::Matrix(env, rows, cols) => vec![Command::InsertMatrix { env, rows, cols }],
}
}
}
#[derive(Debug, Clone, Copy)]
struct Shortcut {
word: &'static str,
label: &'static str,
insert: Insertion,
}
#[derive(Debug, Clone, Copy)]
pub struct KeymapEntry<'a> {
pub word: &'a str,
pub label: &'a str,
}
const fn sym(word: &'static str, latex: &'static str) -> Shortcut {
Shortcut { word, label: word, insert: Insertion::Symbol(latex) }
}
const fn bigop(word: &'static str, latex: &'static str) -> Shortcut {
Shortcut { word, label: word, insert: Insertion::BigOperator(latex) }
}
const CATALOG: &[Shortcut] = &[
Shortcut { word: "frac", label: "fraction", insert: Insertion::Fraction(FracStyle::Bar) },
Shortcut { word: "dfrac", label: "display fraction", insert: Insertion::Fraction(FracStyle::Display) },
Shortcut { word: "binom", label: "binomial", insert: Insertion::Fraction(FracStyle::Binom) },
Shortcut { word: "sqrt", label: "square root", insert: Insertion::Root },
Shortcut { word: "root", label: "root", insert: Insertion::Root },
bigop("sum", "\\sum"),
bigop("prod", "\\prod"),
bigop("coprod", "\\coprod"),
bigop("int", "\\int"),
bigop("iint", "\\iint"),
bigop("iiint", "\\iiint"),
bigop("oint", "\\oint"),
bigop("bigcup", "\\bigcup"),
bigop("bigcap", "\\bigcap"),
bigop("bigsqcup", "\\bigsqcup"),
bigop("biguplus", "\\biguplus"),
bigop("bigoplus", "\\bigoplus"),
bigop("bigotimes", "\\bigotimes"),
bigop("bigodot", "\\bigodot"),
bigop("bigvee", "\\bigvee"),
bigop("bigwedge", "\\bigwedge"),
Shortcut { word: "pmatrix", label: "matrix (parentheses)", insert: Insertion::Matrix(MatrixEnv::Pmatrix, 2, 2) },
Shortcut { word: "bmatrix", label: "matrix (brackets)", insert: Insertion::Matrix(MatrixEnv::Bmatrix, 2, 2) },
Shortcut { word: "vmatrix", label: "matrix (bars / determinant)", insert: Insertion::Matrix(MatrixEnv::Vmatrix, 2, 2) },
Shortcut { word: "matrix", label: "matrix (plain)", insert: Insertion::Matrix(MatrixEnv::Matrix, 2, 2) },
Shortcut { word: "cases", label: "cases", insert: Insertion::Matrix(MatrixEnv::Cases, 2, 2) },
Shortcut { word: "aligned", label: "aligned equations", insert: Insertion::Matrix(MatrixEnv::Aligned, 2, 2) },
Shortcut { word: "align", label: "aligned equations", insert: Insertion::Matrix(MatrixEnv::Aligned, 2, 2) },
Shortcut { word: "array", label: "array", insert: Insertion::Matrix(MatrixEnv::Array, 2, 2) },
sym("alpha", "\\alpha"),
sym("beta", "\\beta"),
sym("gamma", "\\gamma"),
sym("delta", "\\delta"),
sym("epsilon", "\\epsilon"),
sym("varepsilon", "\\varepsilon"),
sym("zeta", "\\zeta"),
sym("eta", "\\eta"),
sym("theta", "\\theta"),
sym("vartheta", "\\vartheta"),
sym("iota", "\\iota"),
sym("kappa", "\\kappa"),
sym("lambda", "\\lambda"),
sym("mu", "\\mu"),
sym("nu", "\\nu"),
sym("xi", "\\xi"),
sym("pi", "\\pi"),
sym("rho", "\\rho"),
sym("sigma", "\\sigma"),
sym("tau", "\\tau"),
sym("upsilon", "\\upsilon"),
sym("phi", "\\phi"),
sym("varphi", "\\varphi"),
sym("chi", "\\chi"),
sym("psi", "\\psi"),
sym("omega", "\\omega"),
sym("Gamma", "\\Gamma"),
sym("Delta", "\\Delta"),
sym("Theta", "\\Theta"),
sym("Lambda", "\\Lambda"),
sym("Xi", "\\Xi"),
sym("Pi", "\\Pi"),
sym("Sigma", "\\Sigma"),
sym("Phi", "\\Phi"),
sym("Psi", "\\Psi"),
sym("Omega", "\\Omega"),
sym("infty", "\\infty"),
sym("infinity", "\\infty"),
sym("partial", "\\partial"),
sym("nabla", "\\nabla"),
Shortcut { word: "dd", label: "differential", insert: Insertion::Symbol("\\mathrm{d}") },
sym("pm", "\\pm"),
sym("mp", "\\mp"),
sym("times", "\\times"),
sym("div", "\\div"),
sym("cdot", "\\cdot"),
sym("ast", "\\ast"),
sym("star", "\\star"),
sym("leq", "\\leq"),
sym("le", "\\leq"),
sym("geq", "\\geq"),
sym("ge", "\\geq"),
sym("neq", "\\neq"),
sym("ne", "\\neq"),
sym("approx", "\\approx"),
sym("equiv", "\\equiv"),
sym("cong", "\\cong"),
sym("sim", "\\sim"),
sym("propto", "\\propto"),
sym("to", "\\to"),
sym("gets", "\\gets"),
sym("implies", "\\implies"),
sym("iff", "\\iff"),
sym("in", "\\in"),
sym("notin", "\\notin"),
sym("subset", "\\subset"),
sym("subseteq", "\\subseteq"),
sym("supset", "\\supset"),
sym("cup", "\\cup"),
sym("cap", "\\cap"),
sym("emptyset", "\\emptyset"),
sym("forall", "\\forall"),
sym("exists", "\\exists"),
sym("neg", "\\neg"),
sym("land", "\\land"),
sym("lor", "\\lor"),
sym("cdots", "\\cdots"),
sym("ldots", "\\ldots"),
sym("dots", "\\dots"),
sym("rightarrow", "\\rightarrow"),
sym("leftarrow", "\\leftarrow"),
sym("angle", "\\angle"),
sym("perp", "\\perp"),
sym("parallel", "\\parallel"),
];
fn dir_cmd(dir: Dir, shift: bool) -> Command {
if shift {
Command::Extend(dir)
} else {
Command::Move(dir)
}
}
fn delims(open: char, close: char) -> Command {
Command::InsertDelimiters { open, close }
}
fn single_char(key: &str) -> Option<char> {
let mut it = key.chars();
match (it.next(), it.next()) {
(Some(c), None) => Some(c),
_ => None,
}
}
fn atom(c: char) -> Symbol {
Symbol::from_char(c) }
fn insert(latex: &str) -> Command {
Command::InsertAtom(Symbol {
latex: latex.to_string(),
class: class_of_latex(latex),
})
}
fn named_atom(latex: &str) -> Command {
insert(latex)
}
fn class_of_latex(latex: &str) -> MathClass {
if let Some(name) = latex.strip_prefix('\\') {
if OPERATORS.contains(&name) {
return MathClass::Op;
}
}
match latex {
"\\leq" | "\\geq" | "\\neq" | "\\equiv" | "\\approx" | "\\cong" | "\\to" | "\\sim"
| "\\implies" | "\\iff" | "\\gets" | "\\propto" | "\\in" | "\\notin" | "\\subset"
| "\\subseteq" | "\\supset" | "\\rightarrow" | "\\leftarrow" | "\\Rightarrow"
| "\\Leftarrow" | "\\perp" | "\\parallel" => MathClass::Rel,
"\\pm" | "\\mp" | "\\times" | "\\div" | "\\cdot" | "\\ast" | "\\star" | "\\cup"
| "\\cap" => MathClass::Bin,
_ => MathClass::Ord,
}
}
fn two_char(a: char, b: char) -> Option<&'static str> {
Some(match (a, b) {
('<', '=') => "\\leq",
('>', '=') => "\\geq",
('!', '=') => "\\neq",
('=', '=') => "\\equiv",
('=', '~') => "\\cong",
('~', '~') => "\\approx",
('-', '>') => "\\to",
('<', '-') => "\\gets",
('=', '>') => "\\implies",
('+', '-') => "\\pm",
('-', '+') => "\\mp",
(':', '-') => "\\div",
('<', '<') => "\\langle",
('>', '>') => "\\rangle",
_ => return None,
})
}
const OPERATORS: &[&str] = &[
"sin", "cos", "tan", "cot", "sec", "csc", "sinh", "cosh", "tanh", "arcsin", "arccos", "arctan",
"log", "ln", "exp", "lim", "max", "min", "sup", "inf", "gcd", "det", "dim", "ker", "arg", "deg",
"hom",
];
#[cfg(test)]
mod tests {
use super::*;
use mathtex_editor_core::model::MatrixEnv;
fn k(key: &str) -> KeyInput {
KeyInput {
key: key.into(),
shift: false,
ctrl: false,
alt: false,
meta: false,
}
}
fn sym(latex: &str, class: MathClass) -> Command {
Command::InsertAtom(Symbol {
latex: latex.into(),
class,
})
}
#[test]
fn arrows_and_shift() {
let mut km = Keymap::new();
assert_eq!(km.map_key(&k("ArrowLeft")), vec![Command::Move(Dir::Left)]);
let mut shifted = k("ArrowRight");
shifted.shift = true;
assert_eq!(km.map_key(&shifted), vec![Command::Extend(Dir::Right)]);
}
#[test]
fn structure_keys() {
let mut km = Keymap::new();
assert_eq!(
km.map_key(&k("/")),
vec![Command::InsertFraction(FracStyle::Bar)]
);
assert_eq!(
km.map_key(&k("(")),
vec![Command::InsertDelimiters {
open: '(',
close: ')'
}]
);
}
#[test]
fn autocorrect_two_char() {
let mut km = Keymap::new();
assert_eq!(km.map_key(&k("<")), vec![sym("<", MathClass::Rel)]);
assert_eq!(
km.map_key(&k("=")),
vec![Command::DeleteBackward, sym("\\leq", MathClass::Rel)]
);
}
#[test]
fn implies_arrow() {
let mut km = Keymap::new();
km.map_key(&k("="));
assert_eq!(
km.map_key(&k(">")),
vec![Command::DeleteBackward, sym("\\implies", MathClass::Rel)]
);
}
#[test]
fn word_on_space_converts() {
let mut km = Keymap::new();
km.map_key(&k("p"));
km.map_key(&k("i"));
assert_eq!(
km.map_key(&k(" ")),
vec![
Command::DeleteBackward,
Command::DeleteBackward,
sym("\\pi", MathClass::Ord)
]
);
}
#[test]
fn map_text_replays_word_lookup_fraction_and_autocorrect() {
let mut km = Keymap::new();
assert_eq!(
km.map_text("pi "),
vec![
sym("p", MathClass::Ord),
sym("i", MathClass::Ord),
Command::DeleteBackward,
Command::DeleteBackward,
sym("\\pi", MathClass::Ord)
]
);
let mut km = Keymap::new();
assert_eq!(
km.map_text("a/b"),
vec![
sym("a", MathClass::Ord),
Command::InsertFraction(FracStyle::Bar),
sym("b", MathClass::Ord),
]
);
let mut km = Keymap::new();
assert_eq!(
km.map_text("a<=b"),
vec![
sym("a", MathClass::Ord),
sym("<", MathClass::Rel),
Command::DeleteBackward,
sym("\\leq", MathClass::Rel),
sym("b", MathClass::Ord),
]
);
}
#[test]
fn operator_word_converts() {
let mut km = Keymap::new();
for c in ["s", "i", "n"] {
km.map_key(&k(c));
}
assert_eq!(
km.map_key(&k(" ")),
vec![
Command::DeleteBackward,
Command::DeleteBackward,
Command::DeleteBackward,
sym("\\sin", MathClass::Op)
]
);
}
#[test]
fn bare_space_inserts_nothing() {
let mut km = Keymap::new();
assert_eq!(km.map_key(&k(" ")), Vec::<Command>::new());
}
#[test]
fn unknown_word_then_space_is_dropped() {
let mut km = Keymap::new();
km.map_key(&k("x"));
km.map_key(&k("y"));
assert_eq!(km.map_key(&k(" ")), Vec::<Command>::new());
}
#[test]
fn plain_letter_inserts_atom() {
let mut km = Keymap::new();
assert_eq!(km.map_key(&k("a")), vec![sym("a", MathClass::Ord)]);
}
#[test]
fn structure_word_builds_bigop() {
let mut km = Keymap::new();
for c in ["s", "u", "m"] {
km.map_key(&k(c));
}
assert_eq!(
km.map_key(&k(" ")),
vec![
Command::DeleteBackward,
Command::DeleteBackward,
Command::DeleteBackward,
Command::InsertBigOp(Symbol {
latex: "\\sum".into(),
class: MathClass::Op
}),
]
);
}
#[test]
fn structure_word_builds_fraction() {
let mut km = Keymap::new();
for c in ["f", "r", "a", "c"] {
km.map_key(&k(c));
}
assert_eq!(
km.map_key(&k(" ")),
vec![
Command::DeleteBackward,
Command::DeleteBackward,
Command::DeleteBackward,
Command::DeleteBackward,
Command::InsertFraction(FracStyle::Bar),
]
);
}
#[test]
fn structure_word_builds_pmatrix() {
let mut km = Keymap::new();
for c in ["p", "m", "a", "t", "r", "i", "x"] {
km.map_key(&k(c));
}
assert_eq!(
km.map_key(&k(" ")),
vec![
Command::DeleteBackward,
Command::DeleteBackward,
Command::DeleteBackward,
Command::DeleteBackward,
Command::DeleteBackward,
Command::DeleteBackward,
Command::DeleteBackward,
Command::InsertMatrix {
env: MatrixEnv::Pmatrix,
rows: 2,
cols: 2
},
]
);
}
#[test]
fn structure_word_builds_cases() {
let mut km = Keymap::new();
for c in ["c", "a", "s", "e", "s"] {
km.map_key(&k(c));
}
assert_eq!(
km.map_key(&k(" ")),
vec![
Command::DeleteBackward,
Command::DeleteBackward,
Command::DeleteBackward,
Command::DeleteBackward,
Command::DeleteBackward,
Command::InsertMatrix {
env: MatrixEnv::Cases,
rows: 2,
cols: 2
},
]
);
}
#[test]
fn host_defined_word_overrides_and_inserts() {
let mut km = Keymap::new();
km.define_word("RR", vec![sym("\\mathbb{R}", MathClass::Ord)]);
for c in ["R", "R"] {
km.map_key(&k(c));
}
assert_eq!(
km.map_key(&k(" ")),
vec![
Command::DeleteBackward,
Command::DeleteBackward,
sym("\\mathbb{R}", MathClass::Ord)
]
);
}
#[test]
fn override_shadows_builtin() {
let mut km = Keymap::new();
km.define_word("pi", vec![sym("\\varpi", MathClass::Ord)]);
for c in ["p", "i"] {
km.map_key(&k(c));
}
assert_eq!(
km.map_key(&k(" ")).last().cloned(),
Some(sym("\\varpi", MathClass::Ord))
);
}
#[test]
fn entries_are_enumerable_for_popover() {
let km = Keymap::new();
let words: Vec<&str> = km.entries().iter().map(|e| e.word).collect();
assert!(words.contains(&"frac")); assert!(words.contains(&"sum")); assert!(words.contains(&"alpha")); assert!(words.contains(&"sin")); }
}