Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support macro-style keybindings #4709

Merged
merged 3 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions book/src/remapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,20 @@ Ctrl, Shift and Alt modifiers are encoded respectively with the prefixes

Keys can be disabled by binding them to the `no_op` command.

A list of commands is available in the [Keymap](https://docs.helix-editor.com/keymap.html) documentation
and in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs) at the invocation of `static_commands!` macro and the `TypableCommandList`.
## Commands

There are three kinds of commands that can be used in keymaps:

* Static commands: commands like `move_char_right` which are usually bound to
keys and used for movement and editing. A list of static commands is
available in the [Keymap](./keymap.html) documentation and in the source code
in [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs)
at the invocation of `static_commands!` macro and the `TypableCommandList`.
* Typable commands: commands that can be executed from command mode (`:`), for
example `:write!`. See the [Commands](./commands.html) documentation for a
list of available typeable commands.
* Macros: sequences of keys that are executed in order. These keybindings
start with `@` and then list any number of keys to be executed. For example
`@miw` can be used to select the surrounding word. For now, macro keybindings
are not allowed in keybinding sequences due to limitations in the way that
command sequences are executed.
49 changes: 46 additions & 3 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,16 @@ where

use helix_view::{align_view, Align};

/// 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.
/// MappableCommands are commands that can be bound to keys, executable in
/// normal, insert or select mode.
///
/// There are three kinds:
///
/// * Static: commands usually bound to keys and used for editing, movement,
/// etc., for example `move_char_left`.
/// * Typable: commands executable from command mode, prefixed with a `:`,
/// for example `:write!`.
/// * Macro: a sequence of keys to execute, for example `@miw`.
#[derive(Clone)]
pub enum MappableCommand {
Typable {
Expand All @@ -191,6 +198,10 @@ pub enum MappableCommand {
fun: fn(cx: &mut Context),
doc: &'static str,
},
Macro {
name: String,
keys: Vec<KeyEvent>,
},
}

macro_rules! static_commands {
Expand Down Expand Up @@ -227,20 +238,39 @@ impl MappableCommand {
}
}
Self::Static { fun, .. } => (fun)(cx),
Self::Macro { keys, .. } => {
// Protect against recursive macros.
if cx.editor.macro_replaying.contains(&'@') {
cx.editor.set_error(
"Cannot execute macro because the [@] register is already playing a macro",
);
return;
}
cx.editor.macro_replaying.push('@');
let keys = keys.clone();
cx.callback.push(Box::new(move |compositor, cx| {
for key in keys.into_iter() {
compositor.handle_event(&compositor::Event::Key(key), cx);
}
cx.editor.macro_replaying.pop();
}));
}
}
}

pub fn name(&self) -> &str {
match &self {
Self::Typable { name, .. } => name,
Self::Static { name, .. } => name,
Self::Macro { name, .. } => name,
}
}

pub fn doc(&self) -> &str {
match &self {
Self::Typable { doc, .. } => doc,
Self::Static { doc, .. } => doc,
Self::Macro { name, .. } => name,
}
}

Expand Down Expand Up @@ -543,6 +573,11 @@ impl fmt::Debug for MappableCommand {
.field(name)
.field(args)
.finish(),
MappableCommand::Macro { name, keys, .. } => f
.debug_tuple("MappableCommand")
.field(name)
.field(keys)
.finish(),
}
}
}
Expand Down Expand Up @@ -573,6 +608,11 @@ impl std::str::FromStr for MappableCommand {
args,
})
.ok_or_else(|| anyhow!("No TypableCommand named '{}'", s))
} else if let Some(suffix) = s.strip_prefix('@') {
helix_view::input::parse_macro(suffix).map(|keys| Self::Macro {
name: s.to_string(),
keys,
})
} else {
MappableCommand::STATIC_COMMAND_LIST
.iter()
Expand Down Expand Up @@ -3135,6 +3175,9 @@ pub fn command_palette(cx: &mut Context) {
ui::PickerColumn::new("name", |item, _| match item {
MappableCommand::Typable { name, .. } => format!(":{name}").into(),
MappableCommand::Static { name, .. } => (*name).into(),
MappableCommand::Macro { .. } => {
unreachable!("macros aren't included in the command palette")
}
}),
ui::PickerColumn::new(
"bindings",
Expand Down
14 changes: 14 additions & 0 deletions helix-term/src/keymap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,19 @@ impl<'de> serde::de::Visitor<'de> for KeyTrieVisitor {
.map_err(serde::de::Error::custom)?,
)
}

// Prevent macro keybindings from being used in command sequences.
// This is meant to be a temporary restriction pending a larger
// refactor of how command sequences are executed.
if commands
.iter()
.any(|cmd| matches!(cmd, MappableCommand::Macro { .. }))
{
return Err(serde::de::Error::custom(
"macro keybindings may not be used in command sequences",
));
}

Ok(KeyTrie::Sequence(commands))
}

Expand All @@ -199,6 +212,7 @@ impl KeyTrie {
// recursively visit all nodes in keymap
fn map_node(cmd_map: &mut ReverseKeymap, node: &KeyTrie, keys: &mut Vec<KeyEvent>) {
match node {
KeyTrie::MappableCommand(MappableCommand::Macro { .. }) => {}
KeyTrie::MappableCommand(cmd) => {
let name = cmd.name();
if name != "no_op" {
Expand Down
Loading