use crate::{Key, KeyInput, Keymap, Modifiers};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LegacyForm {
Representable,
CollapsesTo(KeyInput),
Unrepresentable,
}
impl KeyInput {
#[must_use]
pub fn legacy_form(&self) -> LegacyForm {
if self.modifiers().contains(Modifiers::SUPER) {
return LegacyForm::Unrepresentable;
}
let collapsed = collapse(self.key(), self.modifiers());
if collapsed == *self {
LegacyForm::Representable
} else {
LegacyForm::CollapsesTo(collapsed)
}
}
}
fn collapse(key: Key, mods: Modifiers) -> KeyInput {
if mods.contains(Modifiers::CTRL) {
if let Key::Char(c) = key {
let aliased = match c.to_ascii_lowercase() {
'i' => Some(Key::Tab),
'm' => Some(Key::Enter),
'[' => Some(Key::Esc),
_ => None,
};
if let Some(named) = aliased {
let rest = mods
.difference(Modifiers::CTRL)
.difference(Modifiers::SHIFT);
return KeyInput::new(named, rest);
}
if mods.contains(Modifiers::SHIFT) {
return KeyInput::new(key, mods.difference(Modifiers::SHIFT));
}
}
}
KeyInput::new(key, mods)
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum LegacyLint {
Unrepresentable {
chord: String,
},
CollapsesTo {
chord: String,
collapses_to: String,
},
}
#[must_use]
pub fn legacy_lints<A>(keymap: &Keymap<A>) -> Vec<LegacyLint> {
let mut lints: Vec<LegacyLint> = keymap
.iter()
.filter_map(|(input, _)| match input.legacy_form() {
LegacyForm::Representable => None,
LegacyForm::Unrepresentable => Some(LegacyLint::Unrepresentable {
chord: input.to_string(),
}),
LegacyForm::CollapsesTo(target) => Some(LegacyLint::CollapsesTo {
chord: input.to_string(),
collapses_to: target.to_string(),
}),
})
.collect();
lints.sort_by(|a, b| lint_chord(a).cmp(lint_chord(b)));
lints
}
fn lint_chord(lint: &LegacyLint) -> &str {
match lint {
LegacyLint::Unrepresentable { chord } | LegacyLint::CollapsesTo { chord, .. } => chord,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Key, KeyInput, Modifiers};
fn ctrl(c: char) -> KeyInput {
KeyInput::new(Key::Char(c), Modifiers::CTRL)
}
fn ctrl_shift(c: char) -> KeyInput {
KeyInput::new(Key::Char(c), Modifiers::CTRL | Modifiers::SHIFT)
}
#[test]
fn super_chords_are_unrepresentable() {
let cmd_s = KeyInput::new(Key::Char('s'), Modifiers::SUPER);
assert_eq!(cmd_s.legacy_form(), LegacyForm::Unrepresentable);
let cmd_shift_s = KeyInput::new(Key::Char('s'), Modifiers::SUPER | Modifiers::SHIFT);
assert_eq!(cmd_shift_s.legacy_form(), LegacyForm::Unrepresentable);
}
#[test]
fn ctrl_shift_letter_collapses_to_ctrl_letter() {
assert_eq!(
ctrl_shift('s').legacy_form(),
LegacyForm::CollapsesTo(ctrl('s'))
);
}
#[test]
fn ctrl_i_m_bracket_alias_named_keys() {
assert_eq!(
ctrl('i').legacy_form(),
LegacyForm::CollapsesTo(KeyInput::new(Key::Tab, Modifiers::NONE))
);
assert_eq!(
ctrl('m').legacy_form(),
LegacyForm::CollapsesTo(KeyInput::new(Key::Enter, Modifiers::NONE))
);
assert_eq!(
ctrl('[').legacy_form(),
LegacyForm::CollapsesTo(KeyInput::new(Key::Esc, Modifiers::NONE))
);
assert_eq!(
ctrl_shift('i').legacy_form(),
LegacyForm::CollapsesTo(KeyInput::new(Key::Tab, Modifiers::NONE))
);
}
#[test]
fn plain_and_ctrl_letter_are_representable() {
assert_eq!(
KeyInput::new(Key::Char('a'), Modifiers::NONE).legacy_form(),
LegacyForm::Representable
);
assert_eq!(ctrl('s').legacy_form(), LegacyForm::Representable);
}
#[test]
fn named_function_and_arrow_keys_are_representable() {
for key in [
Key::Tab,
Key::Enter,
Key::Esc,
Key::Up,
Key::F(1),
Key::Home,
] {
assert_eq!(
KeyInput::new(key, Modifiers::NONE).legacy_form(),
LegacyForm::Representable,
"{key:?} should be representable"
);
}
assert_eq!(
KeyInput::new(Key::Tab, Modifiers::SHIFT).legacy_form(),
LegacyForm::Representable
);
}
fn lint_map(chords: &[KeyInput]) -> Keymap<u8> {
let mut map = Keymap::new();
for (i, &c) in chords.iter().enumerate() {
map.bind(c, u8::try_from(i).unwrap());
}
map
}
#[test]
fn legacy_lints_empty_map_is_empty() {
assert_eq!(legacy_lints(&Keymap::<u8>::new()), vec![]);
}
#[test]
fn legacy_lints_classify_each_arm_with_canonical_strings() {
let map = lint_map(&[
KeyInput::new(Key::Char('s'), Modifiers::CTRL),
KeyInput::new(Key::Char('s'), Modifiers::SUPER),
KeyInput::new(Key::Char('s'), Modifiers::CTRL | Modifiers::SHIFT),
ctrl('i'),
]);
assert_eq!(
legacy_lints(&map),
vec![
LegacyLint::CollapsesTo {
chord: "ctrl+i".to_string(),
collapses_to: "tab".to_string(),
},
LegacyLint::CollapsesTo {
chord: "ctrl+shift+s".to_string(),
collapses_to: "ctrl+s".to_string(),
},
LegacyLint::Unrepresentable {
chord: "super+s".to_string(),
},
]
);
}
#[test]
fn legacy_lints_super_dominates_over_shift_collapse() {
let map = lint_map(&[KeyInput::new(
Key::Char('s'),
Modifiers::SUPER | Modifiers::SHIFT,
)]);
assert_eq!(
legacy_lints(&map),
vec![LegacyLint::Unrepresentable {
chord: "shift+super+s".to_string(),
}]
);
}
#[test]
fn legacy_lints_omit_representable_and_count_matches() {
let map = lint_map(&[
KeyInput::new(Key::Char('a'), Modifiers::CTRL), KeyInput::new(Key::Up, Modifiers::NONE), KeyInput::new(Key::F(1), Modifiers::NONE), KeyInput::new(Key::Char('1'), Modifiers::SUPER), ctrl('m'), ]);
assert_eq!(legacy_lints(&map).len(), 2);
}
#[test]
fn legacy_lints_collapses_to_target_is_canonical_and_representable() {
let map = lint_map(&[ctrl('i'), ctrl('m'), ctrl('['), ctrl_shift('s')]);
for lint in legacy_lints(&map) {
if let LegacyLint::CollapsesTo { collapses_to, .. } = lint {
let parsed: KeyInput = collapses_to.parse().expect("target parses");
assert_eq!(parsed.legacy_form(), LegacyForm::Representable);
}
}
}
#[test]
fn collapse_target_is_itself_a_stable_legacy_form() {
for chord in [
ctrl_shift('s'),
ctrl('i'),
ctrl('m'),
ctrl('['),
ctrl_shift('i'),
] {
if let LegacyForm::CollapsesTo(target) = chord.legacy_form() {
assert_eq!(
target.legacy_form(),
LegacyForm::Representable,
"{chord:?} collapsed to a non-stable form {target:?}"
);
} else {
panic!("{chord:?} expected to collapse");
}
}
}
}