mirror of https://github.com/helix-editor/helix
Merge 645eb759cb
into 362e97e927
commit
16743e8fbc
|
@ -41,6 +41,13 @@ g = { a = "code_action" } # Maps `ga` to show possible code actions
|
||||||
"ret" = ["open_below", "normal_mode"] # Maps the enter key to open_below then re-enter normal mode
|
"ret" = ["open_below", "normal_mode"] # Maps the enter key to open_below then re-enter normal mode
|
||||||
"A-x" = "@x<A-d>" # Maps Alt-x to a macro selecting the whole line and deleting it without yanking it
|
"A-x" = "@x<A-d>" # Maps Alt-x to a macro selecting the whole line and deleting it without yanking it
|
||||||
|
|
||||||
|
# You can create labeled sub-menus and provide friendly labels for typeable commands
|
||||||
|
[keys.normal.space.f] # Registering multiple mappings under a single entry creates a sub-menu (accessed by 'space', 'f' in this case)
|
||||||
|
label = "File" # The menu is called file and within it:
|
||||||
|
f = "file_picker" # 'f' opens the file picker
|
||||||
|
s = { label = "Save", command = ":write" } # 's' saves the current file
|
||||||
|
c = { label = "Edit Config", command = ":open ~/.config/helix/config.toml" } # 'c' opens the helix config file
|
||||||
|
|
||||||
[keys.insert]
|
[keys.insert]
|
||||||
"A-x" = "normal_mode" # Maps Alt-X to enter normal mode
|
"A-x" = "normal_mode" # Maps Alt-X to enter normal mode
|
||||||
j = { k = "normal_mode" } # Maps `jk` to exit insert mode
|
j = { k = "normal_mode" } # Maps `jk` to exit insert mode
|
||||||
|
|
|
@ -174,6 +174,129 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parsing_menus() {
|
||||||
|
use crate::keymap;
|
||||||
|
use helix_core::hashmap;
|
||||||
|
use helix_view::document::Mode;
|
||||||
|
|
||||||
|
let sample_keymaps = r#"
|
||||||
|
[keys.normal]
|
||||||
|
f = { f = "file_picker", c = "wclose" }
|
||||||
|
b = { label = "buffer", b = "buffer_picker", n = "goto_next_buffer" }
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let mut keys = keymap::default();
|
||||||
|
merge_keys(
|
||||||
|
&mut keys,
|
||||||
|
hashmap! {
|
||||||
|
Mode::Normal => keymap!({ "Normal mode"
|
||||||
|
"f" => { ""
|
||||||
|
"f" => file_picker,
|
||||||
|
"c" => wclose,
|
||||||
|
},
|
||||||
|
"b" => { "buffer"
|
||||||
|
"b" => buffer_picker,
|
||||||
|
"n" => goto_next_buffer,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Config::load_test(sample_keymaps),
|
||||||
|
Config {
|
||||||
|
keys,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parsing_typable_commands() {
|
||||||
|
use crate::keymap;
|
||||||
|
use crate::keymap::MappableCommand;
|
||||||
|
use helix_view::document::Mode;
|
||||||
|
use helix_view::input::KeyEvent;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
let sample_keymaps = r#"
|
||||||
|
[keys.normal]
|
||||||
|
o = { label = "Edit Config", command = ":open ~/.config" }
|
||||||
|
c = ":buffer-close"
|
||||||
|
h = ["vsplit", "normal_mode", "swap_view_left"]
|
||||||
|
j = {command = ["hsplit", "normal_mode", "swap_view_down"], label = "split down"}
|
||||||
|
n = { label = "Delete word", command = "@wd" }
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let config = Config::load_test(sample_keymaps);
|
||||||
|
|
||||||
|
let tree = config.keys.get(&Mode::Normal).unwrap();
|
||||||
|
|
||||||
|
if let keymap::KeyTrie::Node(node) = tree {
|
||||||
|
let open_node = node.get(&KeyEvent::from_str("o").unwrap()).unwrap();
|
||||||
|
|
||||||
|
if let keymap::KeyTrie::MappableCommand(MappableCommand::Typable { doc, .. }) =
|
||||||
|
open_node
|
||||||
|
{
|
||||||
|
assert_eq!(doc, "Edit Config");
|
||||||
|
} else {
|
||||||
|
panic!("Edit Config did not parse to typable command");
|
||||||
|
}
|
||||||
|
|
||||||
|
let close_node = node.get(&KeyEvent::from_str("c").unwrap()).unwrap();
|
||||||
|
if let keymap::KeyTrie::MappableCommand(MappableCommand::Typable { doc, .. }) =
|
||||||
|
close_node
|
||||||
|
{
|
||||||
|
assert_eq!(doc, ":buffer-close []");
|
||||||
|
} else {
|
||||||
|
panic!(":buffer-close command did not parse to typable command");
|
||||||
|
}
|
||||||
|
|
||||||
|
let split_left = node.get(&KeyEvent::from_str("h").unwrap()).unwrap();
|
||||||
|
if let keymap::KeyTrie::Sequence(label, cmds) = split_left {
|
||||||
|
assert_eq!(label, KeyTrie::DEFAULT_SEQUENCE_LABEL);
|
||||||
|
assert_eq!(
|
||||||
|
*cmds,
|
||||||
|
vec![
|
||||||
|
MappableCommand::vsplit,
|
||||||
|
MappableCommand::normal_mode,
|
||||||
|
MappableCommand::swap_view_left
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let split_down = node.get(&KeyEvent::from_str("j").unwrap()).unwrap();
|
||||||
|
if let keymap::KeyTrie::Sequence(label, cmds) = split_down {
|
||||||
|
assert_eq!(label, "split down");
|
||||||
|
assert_eq!(
|
||||||
|
*cmds,
|
||||||
|
vec![
|
||||||
|
MappableCommand::hsplit,
|
||||||
|
MappableCommand::normal_mode,
|
||||||
|
MappableCommand::swap_view_down
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let macro_keys = node.get(&KeyEvent::from_str("n").unwrap()).unwrap();
|
||||||
|
if let keymap::KeyTrie::MappableCommand(MappableCommand::Macro { name, keys }) =
|
||||||
|
macro_keys
|
||||||
|
{
|
||||||
|
assert_eq!(name, "Delete word");
|
||||||
|
assert_eq!(
|
||||||
|
keys,
|
||||||
|
&vec![
|
||||||
|
KeyEvent::from_str("w").unwrap(),
|
||||||
|
KeyEvent::from_str("d").unwrap()
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("Config did not parse to trie");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn keys_resolve_to_correct_defaults() {
|
fn keys_resolve_to_correct_defaults() {
|
||||||
// From serde default
|
// From serde default
|
||||||
|
|
|
@ -12,6 +12,7 @@ use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
collections::{BTreeSet, HashMap},
|
collections::{BTreeSet, HashMap},
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
|
str::FromStr,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -83,7 +84,7 @@ impl KeyTrieNode {
|
||||||
cmd.doc()
|
cmd.doc()
|
||||||
}
|
}
|
||||||
KeyTrie::Node(n) => &n.name,
|
KeyTrie::Node(n) => &n.name,
|
||||||
KeyTrie::Sequence(_) => "[Multiple commands]",
|
KeyTrie::Sequence(l, ..) => l,
|
||||||
};
|
};
|
||||||
match body.iter().position(|(_, d)| d == &desc) {
|
match body.iter().position(|(_, d)| d == &desc) {
|
||||||
Some(pos) => {
|
Some(pos) => {
|
||||||
|
@ -133,10 +134,18 @@ impl DerefMut for KeyTrieNode {
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum KeyTrie {
|
pub enum KeyTrie {
|
||||||
MappableCommand(MappableCommand),
|
MappableCommand(MappableCommand),
|
||||||
Sequence(Vec<MappableCommand>),
|
Sequence(String, Vec<MappableCommand>),
|
||||||
Node(KeyTrieNode),
|
Node(KeyTrieNode),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl KeyTrie {
|
||||||
|
pub const DEFAULT_SEQUENCE_LABEL: &'static str = "[Multiple commands]";
|
||||||
|
|
||||||
|
pub fn sequence(commands: Vec<MappableCommand>) -> Self {
|
||||||
|
Self::Sequence(Self::DEFAULT_SEQUENCE_LABEL.to_string(), commands)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for KeyTrie {
|
impl<'de> Deserialize<'de> for KeyTrie {
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
where
|
where
|
||||||
|
@ -190,20 +199,99 @@ impl<'de> serde::de::Visitor<'de> for KeyTrieVisitor {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(KeyTrie::Sequence(commands))
|
Ok(KeyTrie::Sequence(
|
||||||
|
KeyTrie::DEFAULT_SEQUENCE_LABEL.to_string(),
|
||||||
|
commands,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
|
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
|
||||||
where
|
where
|
||||||
M: serde::de::MapAccess<'de>,
|
M: serde::de::MapAccess<'de>,
|
||||||
{
|
{
|
||||||
|
let mut label = String::from("");
|
||||||
|
let mut command = None;
|
||||||
let mut mapping = HashMap::new();
|
let mut mapping = HashMap::new();
|
||||||
let mut order = Vec::new();
|
let mut order = Vec::new();
|
||||||
while let Some((key, value)) = map.next_entry::<KeyEvent, KeyTrie>()? {
|
|
||||||
mapping.insert(key, value);
|
while let Some(key) = map.next_key::<String>()? {
|
||||||
order.push(key);
|
match &key as &str {
|
||||||
|
"label" => label = map.next_value::<String>()?,
|
||||||
|
"command" => {
|
||||||
|
command = Some(match map.next_value::<toml::Value>()? {
|
||||||
|
toml::Value::String(s) => {
|
||||||
|
vec![MappableCommand::from_str(&s).map_err(serde::de::Error::custom)?]
|
||||||
|
}
|
||||||
|
toml::Value::Array(arr) => {
|
||||||
|
let mut vec = Vec::with_capacity(arr.len());
|
||||||
|
for value in arr {
|
||||||
|
let toml::Value::String(s) = value else {
|
||||||
|
return Err(serde::de::Error::invalid_type(
|
||||||
|
serde::de::Unexpected::Other(value.type_str()),
|
||||||
|
&"string",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
vec.push(
|
||||||
|
MappableCommand::from_str(&s)
|
||||||
|
.map_err(serde::de::Error::custom)?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
vec
|
||||||
|
}
|
||||||
|
value => {
|
||||||
|
return Err(serde::de::Error::invalid_type(
|
||||||
|
serde::de::Unexpected::Other(value.type_str()),
|
||||||
|
&"string or array",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let key_event = key.parse::<KeyEvent>().map_err(serde::de::Error::custom)?;
|
||||||
|
let key_trie = map.next_value::<KeyTrie>()?;
|
||||||
|
mapping.insert(key_event, key_trie);
|
||||||
|
order.push(key_event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match command {
|
||||||
|
None => Ok(KeyTrie::Node(KeyTrieNode::new(label.as_str(), mapping, order))),
|
||||||
|
Some(_command) if !order.is_empty() => {
|
||||||
|
Err(serde::de::Error::custom("ambiguous mapping: 'command' is only valid with 'label', but I found other keys"))
|
||||||
|
}
|
||||||
|
Some(mut commands) if commands.len() == 1 => match commands.pop() {
|
||||||
|
None => Err(serde::de::Error::custom("UNREACHABLE!, vec is empty after checking len == 1")),
|
||||||
|
Some(MappableCommand::Static { .. }) if !label.is_empty() => {
|
||||||
|
Err(serde::de::Error::custom("custom labels are only available for typable commands (the ones starting with ':')"))
|
||||||
|
}
|
||||||
|
Some(MappableCommand::Typable { name, args, .. }) if !label.is_empty() => {
|
||||||
|
Ok(KeyTrie::MappableCommand(MappableCommand::Typable {
|
||||||
|
name,
|
||||||
|
args,
|
||||||
|
doc: label,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// To label/name macro commands from config
|
||||||
|
Some(MappableCommand::Macro { keys, .. }) if !label.is_empty() => {
|
||||||
|
Ok(KeyTrie::MappableCommand(MappableCommand::Macro {
|
||||||
|
keys,
|
||||||
|
name: label
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(command) => Ok(KeyTrie::MappableCommand(command)),
|
||||||
|
}
|
||||||
|
Some(commands) => {
|
||||||
|
let label = if label.is_empty() {
|
||||||
|
KeyTrie::DEFAULT_SEQUENCE_LABEL.to_string()
|
||||||
|
} else {
|
||||||
|
label
|
||||||
|
};
|
||||||
|
Ok(KeyTrie::Sequence(label, commands))
|
||||||
|
},
|
||||||
}
|
}
|
||||||
Ok(KeyTrie::Node(KeyTrieNode::new("", mapping, order)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -226,7 +314,7 @@ impl KeyTrie {
|
||||||
keys.pop();
|
keys.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyTrie::Sequence(_) => {}
|
KeyTrie::Sequence(..) => {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,14 +326,14 @@ impl KeyTrie {
|
||||||
pub fn node(&self) -> Option<&KeyTrieNode> {
|
pub fn node(&self) -> Option<&KeyTrieNode> {
|
||||||
match *self {
|
match *self {
|
||||||
KeyTrie::Node(ref node) => Some(node),
|
KeyTrie::Node(ref node) => Some(node),
|
||||||
KeyTrie::MappableCommand(_) | KeyTrie::Sequence(_) => None,
|
KeyTrie::MappableCommand(_) | KeyTrie::Sequence(..) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn node_mut(&mut self) -> Option<&mut KeyTrieNode> {
|
pub fn node_mut(&mut self) -> Option<&mut KeyTrieNode> {
|
||||||
match *self {
|
match *self {
|
||||||
KeyTrie::Node(ref mut node) => Some(node),
|
KeyTrie::Node(ref mut node) => Some(node),
|
||||||
KeyTrie::MappableCommand(_) | KeyTrie::Sequence(_) => None,
|
KeyTrie::MappableCommand(_) | KeyTrie::Sequence(..) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -262,7 +350,7 @@ impl KeyTrie {
|
||||||
trie = match trie {
|
trie = match trie {
|
||||||
KeyTrie::Node(map) => map.get(key),
|
KeyTrie::Node(map) => map.get(key),
|
||||||
// leaf encountered while keys left to process
|
// leaf encountered while keys left to process
|
||||||
KeyTrie::MappableCommand(_) | KeyTrie::Sequence(_) => None,
|
KeyTrie::MappableCommand(_) | KeyTrie::Sequence(..) => None,
|
||||||
}?
|
}?
|
||||||
}
|
}
|
||||||
Some(trie)
|
Some(trie)
|
||||||
|
@ -352,7 +440,7 @@ impl Keymaps {
|
||||||
Some(KeyTrie::MappableCommand(ref cmd)) => {
|
Some(KeyTrie::MappableCommand(ref cmd)) => {
|
||||||
return KeymapResult::Matched(cmd.clone());
|
return KeymapResult::Matched(cmd.clone());
|
||||||
}
|
}
|
||||||
Some(KeyTrie::Sequence(ref cmds)) => {
|
Some(KeyTrie::Sequence(_, ref cmds)) => {
|
||||||
return KeymapResult::MatchedSequence(cmds.clone());
|
return KeymapResult::MatchedSequence(cmds.clone());
|
||||||
}
|
}
|
||||||
None => return KeymapResult::NotFound,
|
None => return KeymapResult::NotFound,
|
||||||
|
@ -372,7 +460,7 @@ impl Keymaps {
|
||||||
self.state.clear();
|
self.state.clear();
|
||||||
KeymapResult::Matched(cmd.clone())
|
KeymapResult::Matched(cmd.clone())
|
||||||
}
|
}
|
||||||
Some(KeyTrie::Sequence(cmds)) => {
|
Some(KeyTrie::Sequence(_, cmds)) => {
|
||||||
self.state.clear();
|
self.state.clear();
|
||||||
KeymapResult::MatchedSequence(cmds.clone())
|
KeymapResult::MatchedSequence(cmds.clone())
|
||||||
}
|
}
|
||||||
|
@ -597,7 +685,7 @@ mod tests {
|
||||||
let expectation = KeyTrie::Node(KeyTrieNode::new(
|
let expectation = KeyTrie::Node(KeyTrieNode::new(
|
||||||
"",
|
"",
|
||||||
hashmap! {
|
hashmap! {
|
||||||
key => KeyTrie::Sequence(vec!{
|
key => KeyTrie::sequence(vec!{
|
||||||
MappableCommand::select_all,
|
MappableCommand::select_all,
|
||||||
MappableCommand::Typable {
|
MappableCommand::Typable {
|
||||||
name: "pipe".to_string(),
|
name: "pipe".to_string(),
|
||||||
|
|
Loading…
Reference in New Issue