feat: use readable keybinding notation
This commit is contained in:
+114
-32
@@ -154,42 +154,60 @@ enum Binding {
|
||||
}
|
||||
|
||||
fn parse_key(key: &str) -> Result<Input, String> {
|
||||
let mut chars = key.chars();
|
||||
if let Some(ch) = chars.next()
|
||||
&& chars.next().is_none()
|
||||
&& !ch.is_control()
|
||||
{
|
||||
let character = |name: &str| {
|
||||
let mut chars = name.chars();
|
||||
chars
|
||||
.next()
|
||||
.filter(|ch| !ch.is_control() && chars.next().is_none())
|
||||
};
|
||||
// Keep literal punctuation, especially "+", usable without escaping.
|
||||
if let Some(ch) = character(key) {
|
||||
return Ok(Input::Char(ch));
|
||||
}
|
||||
Ok(match key {
|
||||
"<Esc>" => Input::Escape,
|
||||
"<Enter>" => Input::Enter,
|
||||
"<Backspace>" => Input::Backspace,
|
||||
"<Delete>" => Input::Delete,
|
||||
"<Left>" => Input::Left,
|
||||
"<Right>" => Input::Right,
|
||||
"<Up>" => Input::Up,
|
||||
"<Down>" => Input::Down,
|
||||
"<Home>" => Input::Home,
|
||||
"<End>" => Input::End,
|
||||
"<Tab>" => Input::Char('\t'),
|
||||
"<Space>" => Input::Char(' '),
|
||||
_ => {
|
||||
if let Some(control) = key
|
||||
.strip_prefix("<C-")
|
||||
.and_then(|key| key.strip_suffix('>'))
|
||||
{
|
||||
let mut chars = control.chars();
|
||||
if let Some(ch) = chars.next()
|
||||
&& ch.is_ascii_alphabetic()
|
||||
&& chars.next().is_none()
|
||||
{
|
||||
return Ok(control_key(ch));
|
||||
}
|
||||
let invalid = || format!("Invalid binding key: {key:?}");
|
||||
let (modifier, name) = key
|
||||
.split_once('+')
|
||||
.map_or((None, key), |(modifier, name)| (Some(modifier), name));
|
||||
let input = if let Some(ch) = character(name) {
|
||||
Input::Char(ch)
|
||||
} else {
|
||||
[
|
||||
("esc", Input::Escape),
|
||||
("enter", Input::Enter),
|
||||
("backspace", Input::Backspace),
|
||||
("delete", Input::Delete),
|
||||
("left", Input::Left),
|
||||
("right", Input::Right),
|
||||
("up", Input::Up),
|
||||
("down", Input::Down),
|
||||
("home", Input::Home),
|
||||
("end", Input::End),
|
||||
("tab", Input::Char('\t')),
|
||||
("space", Input::Char(' ')),
|
||||
("minus", Input::Char('-')),
|
||||
("comma", Input::Char(',')),
|
||||
("ampersand", Input::Char('&')),
|
||||
("plus", Input::Char('+')),
|
||||
("backtick", Input::Char('`')),
|
||||
]
|
||||
.into_iter()
|
||||
.find(|(key, _)| name.eq_ignore_ascii_case(key))
|
||||
.map(|(_, input)| input)
|
||||
.ok_or_else(invalid)?
|
||||
};
|
||||
match (modifier, input) {
|
||||
(None, input) => Ok(input),
|
||||
(Some(modifier), Input::Char(ch)) if ch.is_ascii_alphabetic() => {
|
||||
if modifier.eq_ignore_ascii_case("ctrl") {
|
||||
Ok(control_key(ch))
|
||||
} else if modifier.eq_ignore_ascii_case("shift") {
|
||||
Ok(Input::Char(ch.to_ascii_uppercase()))
|
||||
} else {
|
||||
Err(invalid())
|
||||
}
|
||||
return Err(format!("Invalid binding key: {key:?}"));
|
||||
}
|
||||
})
|
||||
_ => Err(invalid()),
|
||||
}
|
||||
}
|
||||
|
||||
fn control_key(ch: char) -> Input {
|
||||
@@ -1394,6 +1412,70 @@ mod tests {
|
||||
assert!(editor.message().contains("core [core]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readable_key_aliases_share_bindings_and_invalid_chords_leave_them_intact() {
|
||||
let mut editor = editor();
|
||||
editor.bind("normal", "shift+i", None).unwrap();
|
||||
editor.handle(Input::Char('I'));
|
||||
assert_eq!(editor.mode(), Mode::Normal);
|
||||
editor
|
||||
.bind("normal", "I", Some("core.insert".into()))
|
||||
.unwrap();
|
||||
editor.handle(Input::Char('I'));
|
||||
assert_eq!(editor.mode(), Mode::Insert);
|
||||
|
||||
editor
|
||||
.bind("insert", "plus", Some("core.newline".into()))
|
||||
.unwrap();
|
||||
editor.bind("insert", "+", None).unwrap();
|
||||
editor.handle(Input::Char('+'));
|
||||
type_text(&mut editor, "界");
|
||||
assert_eq!(editor.lines(), &["界"]);
|
||||
editor
|
||||
.bind("insert", "plus", Some("core.newline".into()))
|
||||
.unwrap();
|
||||
editor.handle(Input::Char('+'));
|
||||
assert_eq!(editor.lines(), &["界", ""]);
|
||||
|
||||
editor.bind("insert", "CTRL+I", None).unwrap();
|
||||
editor.handle(Input::Char('\t'));
|
||||
assert_eq!(editor.lines(), &["界", ""]);
|
||||
editor
|
||||
.bind("insert", "TAB", Some("core.newline".into()))
|
||||
.unwrap();
|
||||
editor.handle(Input::Ctrl('i'));
|
||||
assert_eq!(editor.lines(), &["界", "", ""]);
|
||||
|
||||
editor
|
||||
.bind("insert", "shift+j", Some("core.normal".into()))
|
||||
.unwrap();
|
||||
for key in [
|
||||
"",
|
||||
"<C-r>",
|
||||
"<Esc>",
|
||||
"ctrl+",
|
||||
"+r",
|
||||
"ctrl++r",
|
||||
"ctrl+shift+j",
|
||||
"shift+tab",
|
||||
"shift+1",
|
||||
"alt+j",
|
||||
"cmd+j",
|
||||
"ctrl+界",
|
||||
] {
|
||||
assert!(
|
||||
editor
|
||||
.bind("insert", key, Some("core.newline".into()))
|
||||
.is_err(),
|
||||
"{key}"
|
||||
);
|
||||
}
|
||||
editor.handle(Input::Char('J'));
|
||||
assert_eq!(editor.mode(), Mode::Normal);
|
||||
editor.handle(Input::Char('u'));
|
||||
assert_eq!(editor.lines(), &[String::new()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_bindings_dispatch_only_normal_mode_and_retire_with_owner() {
|
||||
let mut editor = editor();
|
||||
|
||||
+11
-11
@@ -331,23 +331,23 @@ Vamp.configure do |config|
|
||||
config.plugin "healthy.rb"
|
||||
config.bind "n", "core.insert_first_nonblank"
|
||||
config.bind "i", nil
|
||||
config.bind "<C-r>", "core.redo"
|
||||
config.bind "<C-c>", "core.normal", mode: "insert"
|
||||
config.bind "<Esc>", nil, mode: "insert"
|
||||
config.bind "ctrl+r", "core.redo"
|
||||
config.bind "ctrl+c", "core.normal", mode: "insert"
|
||||
config.bind "esc", nil, mode: "insert"
|
||||
config.bind "!", "core.newline", mode: "insert"
|
||||
config.bind "X", nil, mode: "insert"
|
||||
config.bind "shift+x", nil, mode: "insert"
|
||||
config.bind ":", nil
|
||||
config.bind "<Space>", "core.command"
|
||||
config.bind "<Enter>", nil, mode: "command"
|
||||
config.bind "<C-x>", "core.submit", mode: "command"
|
||||
config.bind "<C-c>", "core.normal", mode: "command"
|
||||
config.bind "<C-h>", "core.backspace", mode: "command"
|
||||
config.bind "space", "core.command"
|
||||
config.bind "enter", nil, mode: "command"
|
||||
config.bind "ctrl+x", "core.submit", mode: "command"
|
||||
config.bind "ctrl+c", "core.normal", mode: "command"
|
||||
config.bind "ctrl+h", "core.backspace", mode: "command"
|
||||
config.bind "g", "core.undo"
|
||||
config.bind "g", "healthy"
|
||||
config.bind "n", "core.missing"
|
||||
config.bind "n", "core.undo", mode: "missing"
|
||||
config.bind "<Unknown>", "core.undo"
|
||||
config.bind "<C-f>", "healthy", mode: "insert"
|
||||
config.bind "unknown", "core.undo"
|
||||
config.bind "ctrl+f", "healthy", mode: "insert"
|
||||
end
|
||||
"#,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user