aboutsummaryrefslogtreecommitdiff
path: root/helix-term
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term')
-rw-r--r--helix-term/src/commands.rs158
-rw-r--r--helix-term/src/keymap.rs36
-rw-r--r--helix-term/src/ui/editor.rs11
3 files changed, 136 insertions, 69 deletions
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 8149bd89..99d1432c 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -134,47 +134,76 @@ fn align_view(doc: &Document, view: &mut View, align: Align) {
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,
-/// and does a side-effect on the state (usually by creating and applying a transaction).
-#[derive(Copy, Clone)]
-pub struct Command {
- name: &'static str,
- fun: fn(cx: &mut Context),
- doc: &'static str,
-}
-
-macro_rules! commands {
+/// A MappableCommand is either a static command like "jump_view_up" or a Typable command like
+/// :format. It causes a side-effect on the state (usually by creating and applying a transaction).
+/// Both of these types of commands can be mapped with keybindings in the config.toml.
+#[derive(Clone)]
+pub enum MappableCommand {
+ Typable {
+ name: String,
+ args: Vec<String>,
+ doc: String,
+ },
+ Static {
+ name: &'static str,
+ fun: fn(cx: &mut Context),
+ doc: &'static str,
+ },
+}
+
+macro_rules! static_commands {
( $($name:ident, $doc:literal,)* ) => {
$(
#[allow(non_upper_case_globals)]
- pub const $name: Self = Self {
+ pub const $name: Self = Self::Static {
name: stringify!($name),
fun: $name,
doc: $doc
};
)*
- pub const COMMAND_LIST: &'static [Self] = &[
+ pub const STATIC_COMMAND_LIST: &'static [Self] = &[
$( Self::$name, )*
];
}
}
-impl Command {
+impl MappableCommand {
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 {
- self.name
+ pub fn name(&self) -> &str {
+ match &self {
+ MappableCommand::Typable { name, .. } => name,
+ MappableCommand::Static { name, .. } => name,
+ }
}
- pub fn doc(&self) -> &'static str {
- self.doc
+ pub fn doc(&self) -> &str {
+ match &self {
+ MappableCommand::Typable { doc, .. } => doc,
+ MappableCommand::Static { doc, .. } => doc,
+ }
}
#[rustfmt::skip]
- commands!(
+ static_commands!(
no_op, "Do nothing",
move_char_left, "Move left",
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 {
- let Command { name, .. } = self;
- f.debug_tuple("Command").field(name).finish()
+ f.debug_tuple("MappableCommand")
+ .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 {
- let Command { name, .. } = self;
- f.write_str(name)
+ f.write_str(self.name())
}
}
-impl std::str::FromStr for Command {
+impl std::str::FromStr for MappableCommand {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
- Command::COMMAND_LIST
- .iter()
- .copied()
- .find(|cmd| cmd.name == s)
- .ok_or_else(|| anyhow!("No command named '{}'", s))
+ 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()
+ .cloned()
+ .find(|cmd| cmd.name() == 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>
where
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 {
- 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,15 +2908,16 @@ mod cmd {
}
];
- pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
- TYPABLE_COMMAND_LIST
- .iter()
- .flat_map(|cmd| {
- std::iter::once((cmd.name, cmd))
- .chain(cmd.aliases.iter().map(move |&alias| (alias, cmd)))
- })
- .collect()
- });
+ pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
+ Lazy::new(|| {
+ TYPABLE_COMMAND_LIST
+ .iter()
+ .flat_map(|cmd| {
+ std::iter::once((cmd.name, cmd))
+ .chain(cmd.aliases.iter().map(move |&alias| (alias, cmd)))
+ })
+ .collect()
+ });
}
fn command_mode(cx: &mut Context) {
@@ -2877,7 +2943,7 @@ fn command_mode(cx: &mut Context) {
if let Some(cmd::TypableCommand {
completer: Some(completer),
..
- }) = cmd::COMMANDS.get(parts[0])
+ }) = cmd::TYPABLE_COMMAND_MAP.get(parts[0])
{
completer(part)
.into_iter()
@@ -2912,7 +2978,7 @@ fn command_mode(cx: &mut Context) {
}
// 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) {
cx.editor.set_error(format!("{}", e));
}
@@ -2925,7 +2991,7 @@ fn command_mode(cx: &mut Context) {
prompt.doc_fn = Box::new(|input: &str| {
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);
}
diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs
index 73cb15f8..ecb0cc6c 100644
--- a/helix-term/src/keymap.rs
+++ b/helix-term/src/keymap.rs
@@ -1,4 +1,4 @@
-pub use crate::commands::Command;
+pub use crate::commands::MappableCommand;
use crate::config::Config;
use helix_core::hashmap;
use helix_view::{document::Mode, info::Info, input::KeyEvent};
@@ -92,7 +92,7 @@ macro_rules! alt {
#[macro_export]
macro_rules! keymap {
(@trie $cmd:ident) => {
- $crate::keymap::KeyTrie::Leaf($crate::commands::Command::$cmd)
+ $crate::keymap::KeyTrie::Leaf($crate::commands::MappableCommand::$cmd)
};
(@trie
@@ -260,8 +260,8 @@ impl DerefMut for KeyTrieNode {
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(untagged)]
pub enum KeyTrie {
- Leaf(Command),
- Sequence(Vec<Command>),
+ Leaf(MappableCommand),
+ Sequence(Vec<MappableCommand>),
Node(KeyTrieNode),
}
@@ -304,9 +304,9 @@ impl KeyTrie {
pub enum KeymapResultKind {
/// Needs more keys to execute a command. Contains valid keys for next keystroke.
Pending(KeyTrieNode),
- Matched(Command),
+ Matched(MappableCommand),
/// Matched a sequence of commands to execute.
- MatchedSequence(Vec<Command>),
+ MatchedSequence(Vec<MappableCommand>),
/// Key was not found in the root keymap
NotFound,
/// 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]) {
- Some(&KeyTrie::Leaf(cmd)) => {
- return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky())
+ Some(KeyTrie::Leaf(ref cmd)) => {
+ return KeymapResult::new(KeymapResultKind::Matched(cmd.clone()), self.sticky())
}
- Some(&KeyTrie::Sequence(ref cmds)) => {
+ Some(KeyTrie::Sequence(ref cmds)) => {
return KeymapResult::new(
KeymapResultKind::MatchedSequence(cmds.clone()),
self.sticky(),
@@ -408,9 +408,9 @@ impl Keymap {
}
KeymapResult::new(KeymapResultKind::Pending(map.clone()), self.sticky())
}
- Some(&KeyTrie::Leaf(cmd)) => {
+ Some(&KeyTrie::Leaf(ref cmd)) => {
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)) => {
self.state.clear();
@@ -833,36 +833,36 @@ mod tests {
let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
assert_eq!(
keymap.get(key!('i')).kind,
- KeymapResultKind::Matched(Command::normal_mode),
+ KeymapResultKind::Matched(MappableCommand::normal_mode),
"Leaf should replace leaf"
);
assert_eq!(
keymap.get(key!('无')).kind,
- KeymapResultKind::Matched(Command::insert_mode),
+ KeymapResultKind::Matched(MappableCommand::insert_mode),
"New leaf should be present in merged keymap"
);
// Assumes that z is a node in the default keymap
assert_eq!(
keymap.get(key!('z')).kind,
- KeymapResultKind::Matched(Command::jump_backward),
+ KeymapResultKind::Matched(MappableCommand::jump_backward),
"Leaf should replace node"
);
// Assumes that `g` is a node in default keymap
assert_eq!(
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"
);
// Assumes that `gg` is in default keymap
assert_eq!(
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"
);
// Assumes that `ge` is in default keymap
assert_eq!(
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"
);
@@ -896,7 +896,7 @@ mod tests {
.root()
.search(&[key!(' '), key!('s'), key!('v')])
.unwrap(),
- &KeyTrie::Leaf(Command::vsplit),
+ &KeyTrie::Leaf(MappableCommand::vsplit),
"Leaf should be present in merged subnode"
);
// Make sure an order was set during merge
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index a7f63f31..39ee15b4 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -31,7 +31,7 @@ use tui::buffer::Buffer as Surface;
pub struct EditorView {
keymaps: Keymaps,
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>,
spinners: ProgressSpinners,
autoinfo: Option<Info>,
@@ -48,7 +48,7 @@ impl EditorView {
Self {
keymaps,
on_next_key: None,
- last_insert: (commands::Command::normal_mode, Vec::new()),
+ last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
completion: None,
spinners: ProgressSpinners::default(),
autoinfo: None,
@@ -875,7 +875,7 @@ impl EditorView {
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)
}
@@ -893,7 +893,8 @@ impl EditorView {
}
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);
}
@@ -907,7 +908,7 @@ impl EditorView {
let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap();
doc.set_selection(view_id, Selection::point(pos));
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);
}