feat: Make it possible to keybind `TypableCommands` (#1169)

* Make TypableCommands mappable

* Fix pr comments

* Update PartialEq implementation
pull/1235/head
Oskar Nehlin 3 years ago committed by GitHub
parent 70c62530ee
commit a06871a689
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -134,47 +134,76 @@ fn align_view(doc: &Document, view: &mut View, align: Align) {
view.offset.row = line.saturating_sub(relative); view.offset.row = line.saturating_sub(relative);
} }
/// A command is composed of a static name, and a function that takes the current state plus a count, /// A MappableCommand is either a static command like "jump_view_up" or a Typable command like
/// and does a side-effect on the state (usually by creating and applying a transaction). /// :format. It causes a side-effect on the state (usually by creating and applying a transaction).
#[derive(Copy, Clone)] /// Both of these types of commands can be mapped with keybindings in the config.toml.
pub struct Command { #[derive(Clone)]
pub enum MappableCommand {
Typable {
name: String,
args: Vec<String>,
doc: String,
},
Static {
name: &'static str, name: &'static str,
fun: fn(cx: &mut Context), fun: fn(cx: &mut Context),
doc: &'static str, doc: &'static str,
},
} }
macro_rules! commands { macro_rules! static_commands {
( $($name:ident, $doc:literal,)* ) => { ( $($name:ident, $doc:literal,)* ) => {
$( $(
#[allow(non_upper_case_globals)] #[allow(non_upper_case_globals)]
pub const $name: Self = Self { pub const $name: Self = Self::Static {
name: stringify!($name), name: stringify!($name),
fun: $name, fun: $name,
doc: $doc doc: $doc
}; };
)* )*
pub const COMMAND_LIST: &'static [Self] = &[ pub const STATIC_COMMAND_LIST: &'static [Self] = &[
$( Self::$name, )* $( Self::$name, )*
]; ];
} }
} }
impl Command { impl MappableCommand {
pub fn execute(&self, cx: &mut Context) { pub fn execute(&self, cx: &mut Context) {
(self.fun)(cx); match &self {
MappableCommand::Typable { name, args, doc: _ } => {
let args: Vec<&str> = args.iter().map(|arg| arg.as_str()).collect();
if let Some(command) = cmd::TYPABLE_COMMAND_MAP.get(name.as_str()) {
let mut cx = compositor::Context {
editor: cx.editor,
jobs: cx.jobs,
scroll: None,
};
if let Err(e) = (command.fun)(&mut cx, &args, PromptEvent::Validate) {
cx.editor.set_error(format!("{}", e));
}
}
}
MappableCommand::Static { fun, .. } => (fun)(cx),
}
} }
pub fn name(&self) -> &'static str { pub fn name(&self) -> &str {
self.name match &self {
MappableCommand::Typable { name, .. } => name,
MappableCommand::Static { name, .. } => name,
}
} }
pub fn doc(&self) -> &'static str { pub fn doc(&self) -> &str {
self.doc match &self {
MappableCommand::Typable { doc, .. } => doc,
MappableCommand::Static { doc, .. } => doc,
}
} }
#[rustfmt::skip] #[rustfmt::skip]
commands!( static_commands!(
no_op, "Do nothing", no_op, "Do nothing",
move_char_left, "Move left", move_char_left, "Move left",
move_char_right, "Move right", move_char_right, "Move right",
@ -367,33 +396,51 @@ impl Command {
); );
} }
impl fmt::Debug for Command { impl fmt::Debug for MappableCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Command { name, .. } = self; f.debug_tuple("MappableCommand")
f.debug_tuple("Command").field(name).finish() .field(&self.name())
.finish()
} }
} }
impl fmt::Display for Command { impl fmt::Display for MappableCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Command { name, .. } = self; f.write_str(self.name())
f.write_str(name)
} }
} }
impl std::str::FromStr for Command { impl std::str::FromStr for MappableCommand {
type Err = anyhow::Error; type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
Command::COMMAND_LIST if let Some(suffix) = s.strip_prefix(':') {
let mut typable_command = suffix.split(' ').into_iter().map(|arg| arg.trim());
let name = typable_command
.next()
.ok_or_else(|| anyhow!("Expected typable command name"))?;
let args = typable_command
.map(|s| s.to_owned())
.collect::<Vec<String>>();
cmd::TYPABLE_COMMAND_MAP
.get(name)
.map(|cmd| MappableCommand::Typable {
name: cmd.name.to_owned(),
doc: format!(":{} {:?}", cmd.name, args),
args,
})
.ok_or_else(|| anyhow!("No TypableCommand named '{}'", s))
} else {
MappableCommand::STATIC_COMMAND_LIST
.iter() .iter()
.copied() .cloned()
.find(|cmd| cmd.name == s) .find(|cmd| cmd.name() == s)
.ok_or_else(|| anyhow!("No command named '{}'", s)) .ok_or_else(|| anyhow!("No command named '{}'", s))
} }
} }
}
impl<'de> Deserialize<'de> for Command { impl<'de> Deserialize<'de> for MappableCommand {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where where
D: Deserializer<'de>, D: Deserializer<'de>,
@ -403,9 +450,27 @@ impl<'de> Deserialize<'de> for Command {
} }
} }
impl PartialEq for Command { impl PartialEq for MappableCommand {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.name() == other.name() match (self, other) {
(
MappableCommand::Typable {
name: first_name, ..
},
MappableCommand::Typable {
name: second_name, ..
},
) => first_name == second_name,
(
MappableCommand::Static {
name: first_name, ..
},
MappableCommand::Static {
name: second_name, ..
},
) => first_name == second_name,
_ => false,
}
} }
} }
@ -2843,7 +2908,8 @@ mod cmd {
} }
]; ];
pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| { pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
Lazy::new(|| {
TYPABLE_COMMAND_LIST TYPABLE_COMMAND_LIST
.iter() .iter()
.flat_map(|cmd| { .flat_map(|cmd| {
@ -2877,7 +2943,7 @@ fn command_mode(cx: &mut Context) {
if let Some(cmd::TypableCommand { if let Some(cmd::TypableCommand {
completer: Some(completer), completer: Some(completer),
.. ..
}) = cmd::COMMANDS.get(parts[0]) }) = cmd::TYPABLE_COMMAND_MAP.get(parts[0])
{ {
completer(part) completer(part)
.into_iter() .into_iter()
@ -2912,7 +2978,7 @@ fn command_mode(cx: &mut Context) {
} }
// Handle typable commands // Handle typable commands
if let Some(cmd) = cmd::COMMANDS.get(parts[0]) { if let Some(cmd) = cmd::TYPABLE_COMMAND_MAP.get(parts[0]) {
if let Err(e) = (cmd.fun)(cx, &parts[1..], event) { if let Err(e) = (cmd.fun)(cx, &parts[1..], event) {
cx.editor.set_error(format!("{}", e)); cx.editor.set_error(format!("{}", e));
} }
@ -2925,7 +2991,7 @@ fn command_mode(cx: &mut Context) {
prompt.doc_fn = Box::new(|input: &str| { prompt.doc_fn = Box::new(|input: &str| {
let part = input.split(' ').next().unwrap_or_default(); let part = input.split(' ').next().unwrap_or_default();
if let Some(cmd::TypableCommand { doc, .. }) = cmd::COMMANDS.get(part) { if let Some(cmd::TypableCommand { doc, .. }) = cmd::TYPABLE_COMMAND_MAP.get(part) {
return Some(doc); return Some(doc);
} }

@ -1,4 +1,4 @@
pub use crate::commands::Command; pub use crate::commands::MappableCommand;
use crate::config::Config; use crate::config::Config;
use helix_core::hashmap; use helix_core::hashmap;
use helix_view::{document::Mode, info::Info, input::KeyEvent}; use helix_view::{document::Mode, info::Info, input::KeyEvent};
@ -92,7 +92,7 @@ macro_rules! alt {
#[macro_export] #[macro_export]
macro_rules! keymap { macro_rules! keymap {
(@trie $cmd:ident) => { (@trie $cmd:ident) => {
$crate::keymap::KeyTrie::Leaf($crate::commands::Command::$cmd) $crate::keymap::KeyTrie::Leaf($crate::commands::MappableCommand::$cmd)
}; };
(@trie (@trie
@ -260,8 +260,8 @@ impl DerefMut for KeyTrieNode {
#[derive(Debug, Clone, PartialEq, Deserialize)] #[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum KeyTrie { pub enum KeyTrie {
Leaf(Command), Leaf(MappableCommand),
Sequence(Vec<Command>), Sequence(Vec<MappableCommand>),
Node(KeyTrieNode), Node(KeyTrieNode),
} }
@ -304,9 +304,9 @@ impl KeyTrie {
pub enum KeymapResultKind { pub enum KeymapResultKind {
/// Needs more keys to execute a command. Contains valid keys for next keystroke. /// Needs more keys to execute a command. Contains valid keys for next keystroke.
Pending(KeyTrieNode), Pending(KeyTrieNode),
Matched(Command), Matched(MappableCommand),
/// Matched a sequence of commands to execute. /// Matched a sequence of commands to execute.
MatchedSequence(Vec<Command>), MatchedSequence(Vec<MappableCommand>),
/// Key was not found in the root keymap /// Key was not found in the root keymap
NotFound, NotFound,
/// Key is invalid in combination with previous keys. Contains keys leading upto /// Key is invalid in combination with previous keys. Contains keys leading upto
@ -386,10 +386,10 @@ impl Keymap {
}; };
let trie = match trie_node.search(&[*first]) { let trie = match trie_node.search(&[*first]) {
Some(&KeyTrie::Leaf(cmd)) => { Some(KeyTrie::Leaf(ref cmd)) => {
return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky()) return KeymapResult::new(KeymapResultKind::Matched(cmd.clone()), self.sticky())
} }
Some(&KeyTrie::Sequence(ref cmds)) => { Some(KeyTrie::Sequence(ref cmds)) => {
return KeymapResult::new( return KeymapResult::new(
KeymapResultKind::MatchedSequence(cmds.clone()), KeymapResultKind::MatchedSequence(cmds.clone()),
self.sticky(), self.sticky(),
@ -408,9 +408,9 @@ impl Keymap {
} }
KeymapResult::new(KeymapResultKind::Pending(map.clone()), self.sticky()) KeymapResult::new(KeymapResultKind::Pending(map.clone()), self.sticky())
} }
Some(&KeyTrie::Leaf(cmd)) => { Some(&KeyTrie::Leaf(ref cmd)) => {
self.state.clear(); self.state.clear();
return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky()); return KeymapResult::new(KeymapResultKind::Matched(cmd.clone()), self.sticky());
} }
Some(&KeyTrie::Sequence(ref cmds)) => { Some(&KeyTrie::Sequence(ref cmds)) => {
self.state.clear(); self.state.clear();
@ -833,36 +833,36 @@ mod tests {
let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap(); let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
assert_eq!( assert_eq!(
keymap.get(key!('i')).kind, keymap.get(key!('i')).kind,
KeymapResultKind::Matched(Command::normal_mode), KeymapResultKind::Matched(MappableCommand::normal_mode),
"Leaf should replace leaf" "Leaf should replace leaf"
); );
assert_eq!( assert_eq!(
keymap.get(key!('无')).kind, keymap.get(key!('无')).kind,
KeymapResultKind::Matched(Command::insert_mode), KeymapResultKind::Matched(MappableCommand::insert_mode),
"New leaf should be present in merged keymap" "New leaf should be present in merged keymap"
); );
// Assumes that z is a node in the default keymap // Assumes that z is a node in the default keymap
assert_eq!( assert_eq!(
keymap.get(key!('z')).kind, keymap.get(key!('z')).kind,
KeymapResultKind::Matched(Command::jump_backward), KeymapResultKind::Matched(MappableCommand::jump_backward),
"Leaf should replace node" "Leaf should replace node"
); );
// Assumes that `g` is a node in default keymap // Assumes that `g` is a node in default keymap
assert_eq!( assert_eq!(
keymap.root().search(&[key!('g'), key!('$')]).unwrap(), keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
&KeyTrie::Leaf(Command::goto_line_end), &KeyTrie::Leaf(MappableCommand::goto_line_end),
"Leaf should be present in merged subnode" "Leaf should be present in merged subnode"
); );
// Assumes that `gg` is in default keymap // Assumes that `gg` is in default keymap
assert_eq!( assert_eq!(
keymap.root().search(&[key!('g'), key!('g')]).unwrap(), keymap.root().search(&[key!('g'), key!('g')]).unwrap(),
&KeyTrie::Leaf(Command::delete_char_forward), &KeyTrie::Leaf(MappableCommand::delete_char_forward),
"Leaf should replace old leaf in merged subnode" "Leaf should replace old leaf in merged subnode"
); );
// Assumes that `ge` is in default keymap // Assumes that `ge` is in default keymap
assert_eq!( assert_eq!(
keymap.root().search(&[key!('g'), key!('e')]).unwrap(), keymap.root().search(&[key!('g'), key!('e')]).unwrap(),
&KeyTrie::Leaf(Command::goto_last_line), &KeyTrie::Leaf(MappableCommand::goto_last_line),
"Old leaves in subnode should be present in merged node" "Old leaves in subnode should be present in merged node"
); );
@ -896,7 +896,7 @@ mod tests {
.root() .root()
.search(&[key!(' '), key!('s'), key!('v')]) .search(&[key!(' '), key!('s'), key!('v')])
.unwrap(), .unwrap(),
&KeyTrie::Leaf(Command::vsplit), &KeyTrie::Leaf(MappableCommand::vsplit),
"Leaf should be present in merged subnode" "Leaf should be present in merged subnode"
); );
// Make sure an order was set during merge // Make sure an order was set during merge

@ -31,7 +31,7 @@ use tui::buffer::Buffer as Surface;
pub struct EditorView { pub struct EditorView {
keymaps: Keymaps, keymaps: Keymaps,
on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>, on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
last_insert: (commands::Command, Vec<KeyEvent>), last_insert: (commands::MappableCommand, Vec<KeyEvent>),
pub(crate) completion: Option<Completion>, pub(crate) completion: Option<Completion>,
spinners: ProgressSpinners, spinners: ProgressSpinners,
autoinfo: Option<Info>, autoinfo: Option<Info>,
@ -48,7 +48,7 @@ impl EditorView {
Self { Self {
keymaps, keymaps,
on_next_key: None, on_next_key: None,
last_insert: (commands::Command::normal_mode, Vec::new()), last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
completion: None, completion: None,
spinners: ProgressSpinners::default(), spinners: ProgressSpinners::default(),
autoinfo: None, autoinfo: None,
@ -875,7 +875,7 @@ impl EditorView {
return EventResult::Ignored; return EventResult::Ignored;
} }
commands::Command::yank_main_selection_to_primary_clipboard.execute(cxt); commands::MappableCommand::yank_main_selection_to_primary_clipboard.execute(cxt);
EventResult::Consumed(None) EventResult::Consumed(None)
} }
@ -893,7 +893,8 @@ impl EditorView {
} }
if modifiers == crossterm::event::KeyModifiers::ALT { if modifiers == crossterm::event::KeyModifiers::ALT {
commands::Command::replace_selections_with_primary_clipboard.execute(cxt); commands::MappableCommand::replace_selections_with_primary_clipboard
.execute(cxt);
return EventResult::Consumed(None); return EventResult::Consumed(None);
} }
@ -907,7 +908,7 @@ impl EditorView {
let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap(); let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap();
doc.set_selection(view_id, Selection::point(pos)); doc.set_selection(view_id, Selection::point(pos));
editor.tree.focus = view_id; editor.tree.focus = view_id;
commands::Command::paste_primary_clipboard_before.execute(cxt); commands::MappableCommand::paste_primary_clipboard_before.execute(cxt);
return EventResult::Consumed(None); return EventResult::Consumed(None);
} }

Loading…
Cancel
Save