helix
helix copied to clipboard
Support for macro-style keybinding
Allow binding keys to sequences of other keys, like how Vim handles keybindings. I don't propose for this to replace the current system of binding keys to commands, but rather add this as a keybinding option, and even allow it to be mixed with command keybindings.
In the following examples, I use a @
prefix to distinguish keypress sequences from command names, and though it is a valid possibility, note that I am not asserting that this is the syntax we should use.
# Binds D to delete the line
"D" = "@xd"
This would also allow for more versatile bindings. For example, zyrafal said on Matrix:
Is there a way to bind a key to switching to a specific register? For example, if i want to bind Ctrl-D to switch to register d
With macro-style keybinding support, this could be achieved like this:
"C-d" = ["select_register", "@d"]
Some more examples of this versatility:
# Binds M to select inside parentheses
"M" = ["select_textobjext_inner", "@("]
# Binds Alt-b to pipe the file through xxd external program
"A-b" = ["select_all", "shell_pipe", "@xxd<ret>"]
I can implement this once #1253 is merged. What are you all's thoughts?
Although I'm not against this feature, I still strongly suggest you take a look at Emacs keybinding approach which I describe in detail on this issue #1200.
Here are the same examples if we are going on Emacs approach:
"C-d" = ["register d"]
"M" = ["select inner brackets"]
"A-b" = ["select all", "pipe xxd"]
Although I'm not against this feature, I still strongly suggest you take a look at Emacs keybinding approach which I describe in detail on this issue #1200.
Here are the same examples if we are going on Emacs approach:
"C-d" = ["register d"] "M" = ["select inner brackets"] "A-b" = ["select all", "pipe xxd"]
IMO, this seems kind of like it would make keybinding rather unintuitive, as opposed to directly mirroring what you'd do in the editor. Additionally, this idea seems to me like it would be a very sizeable endeavour, especially compared to what it would take to implement this issue's idea, and in the end I again think that this idea would provide a more intuitive interface to the user.
@Omnikar Even though I'm a neovim user, I still think the emacs approach is better. Because you don't need to deal with remapping, which can be error-prone, and actually raise the barrier to entry.
I'm sorry, can you elaborate? What do you mean by "reamapping"?
@Omnikar Sorry, that's a typo. By the issue of remapping I mean: A may be mapped to B, yet B is mapped to C, and this can chains up.
If not treated carefully, this can cause problem. And thus we have introduce options like "noremap", however, if we ban the mapping on keys in the first place, there won't be such need at all.
Here, the difference from Vim is that macro-style keybinding is not the only option, and should not be used as the primary option either. Binding keys directly to commands would still be the primary way to configure keys. As such, "noremap" does not become necessary.
Well, in that case it would be alright.
I find the proposed feature quite powerful and essential. It would be great if helix would support it, one way or the other (though I would prefer the "vim-style").
I am trying to make a key binding for piping a selection to fmt
(reformatting Markdown text, as a workaround for #625), but no luck so far, as it seems that only "typeable" commands initiated with :
are allowed to be followed by some arbitrary string (example in the docs). The macro-style keybinding feature would solve the problem.
So, this issue has gotten some attention recently from other issues. I have tried to implement it but have encountered issue with the fact that bound commands can be executed immediately, but macro-style keybinds must be executed by the compositor in a callback. This means that when trying to combine command and macro bindings in a way such as ["shell_pipe", "@xxd<ret>"]
, the commands get executed right away but the macros get put into a callback and don't get executed until afterwards. Additionally, if there are multiple macro keybinds in a sequence, the callback will just be overridden by the last one.
Could a solution be to flatten the sequence of commands and macros to a single macro then?
This would avoid having commands being executed before the macros, since it's all one big macro to be executed in sequence, and sidestep the issue of macros being overwritten, since it would just be a single macro.
The downside is that this approach may require changes to replay_macro
(if that's what you're using to implement this feature), in order to call handle_event(key, cx)
or command.execute(cx)
depending on whether the object is a key or a command.
Edit:
It might not be necessary to change replay_macro
if, when building the flattened macro, a keymap lookup were performed to convert a command to the corresponding key. However this would require macro style keybinds to be processed after all the other key remappings in the config.toml were applied (and generally seems like a messy solution to me).
Hey, preconfigured macros are definitely the next thing I'm looking forward to in helix. Is there any part of this that could use a helping hand?
How would the suggested macro key binding of the key sequence mi"
to a key - for example mq
in normal mode - look like?
How would the suggested macro key binding of the key sequence
mi"
to a key - for examplemq
in normal mode - look like?
m.q = ["select_textobject_inner", "@\""]
If you keep the mapping inside a single string you get the ability to "record" key mappings into a register with a macro.
You could record a macro to register "q" you could simply go and paste the register into your config rather than working it out manually.
This doesn't work in Vim because Vim stores macros in a different format that doesn't match its mappings. Helix could get another feature for free here.
If the mapping includes a double quote, couldn't you choose to put the string in single quotes, or go in and escape it?
m.q = 'mi"'
Or failing that, the user could go in and escape double quotes with a backslash or something.
I don't understand how #5499 is a duplicate of this one. I get the impression there's some reason just allowing binding a command with args is considered harmful, but I don't know why. But somehow it's ok for a "typable command", whatever that is. Can somebody explain it to me like I'm a noob?
update: read https://github.com/helix-editor/helix/issues/1200 and https://github.com/helix-editor/helix/pull/1169 and now i wonder if it's just that remapping TypeableCommands with arguments has been implemented but not "keymaps actions", which may prompt for input. I guess the difficulty is that keymap actions aren't actually functions with args, just actions that prompt?
Ok, so I think I understand now: we want to be able to map keys to arbitrary sequences of actions, but the "keymap actions" that take input hide their args, so in order to achieve this either:
- those need to be converted to TypeableActions, or
- we need to make macros mappable, so that we can interact with those input prompts.
Is that right?
Yep that's correct. There are some implementation details that make it clearer what the "arguments" are for regular commands:
Typable commands take a list of arguments that are parsed out of what you enter in command mode (:
) or the keybinding. Regular commands don't have arguments and there are a few ways to provide "arguments" to them: shell_pipe_to
creates a Prompt UI element where you can fill the shell command to use. Some commands like increment
(<C-a>
) take a count, for example 3<C-a>
increments three times. Some other commands like find_till_char
(t
) setup on-next-key callbacks that do something on the next keyboard event. All of these cases can be covered if we can create bindings that emit key events in the same way as macros.
Converting regular commands to equivalent typable commands that take arguments could work for many cases. It would lead to a lot of duplicate code though and it would be cleaner to use macro keybindings instead.
~~What about adding some trait RuntimeCallable both TypableCommands and whatever ways regular commands take input implement?~~
https://github.com/helix-editor/helix/issues/5555
Having looked through this issue closely, I'm still lost on how it is meant to work.
For example, take maf
: By default, Helix binds maf
to something like match_around_function
(the command doesn't currently have a name). Does binding x
to @maf
bind x
to match_around_function
(regardless of what maf
is currently bound to), or does it just make x
and alias for maf
(so x
is bound to whatever maf
is currently bound to)?
If x
gets bound to match_around_function
(regardless of what maf
is bound to), then @maf
just becomes the name of match_around_function
, which makes no sense. The name should just be a name, like match_around_funtion
or :select-entire-function
.
If x
gets bound to maf
(as a simple alias), then there's no way to bind x
to match_around_function
unless you also retain the default keybinding (so maf
also maps to match_around_function
still). This limits users to defining aliases for bindings, but they cannot fully remap them (x
can be match_around_function
, but you don't free up maf
).
I understand that there are other use cases, and this feature would be useful in those cases, but I was told this feature would allow binding to commands that have no name, like the commands that are (by default) bound to the ma
and mi
prefixes. But (IIUC), it doesn't, whichever way it is intended to work.
The feature request still makes sense, as it handles a bunch of other use cases, but there's still a need to give every command a canonical name, and let users bind any binding to any command they like.
Edit: I've reopened #9814, as it's not especially relevant that this feature request doesn't address #9814 (it never set out to), so the lack of proper command names can be discussed there. That said, I still think it's important to establish explicitly that this feature request just creates aliases (it doesn't sanctify a new class of (odd) names for commands).
One potential use for this would be switching registers before yanking. I have a command for quickly moving lines up/down or "copying" the remaining part of the line above the current line, etc. Unfortunately, these all overwrite any text I had yanked in the default register previously. I get that I could just always specify a different register for normal yanking, but that's not ideal. Also, there's a lot that you can do with multiple registers that's much more difficult, if not impossible, to accomplish with only one (especially since there's no conditionals).
So, this is thread is pretty old. I can't say I understand all the subtleties of commands and registers, so forgive me if I'm restating the obvious here.
As a user, all I want to be able to do is to replay a sequence of key presses by means of a keybinding. I'd like to be able to record a macro interactively (in order to validate it does what I want), dump it to a string, and then have that string evaluated as though I typed the keys. (I realize what I'm describing is self-evident in the context of this thread.)
Today, the only way to achieve this is by having a terminal or a multiplexer simulate the key presses, or – as exemplified here (https://github.com/helix-editor/helix/issues/2806#issuecomment-1923382626) – by using the multiplexer to re-record the macro.
Most of my use cases are extemely simple. They would be served perfectly fine by a single new command, e.g. :replay_macro_from_string '<key events>'
, that evaluates and executes a sequence of key presses. An accompanying command, e.g. :insert-macro <register>
, would insert the macro string into the buffer, so that it can be copied to a keybinding config. (Personally, I think :dump-macro
makes more sense, but 'insert' is more analogous to :insert-output
.)
To illustrate, let me describe one of my use cases. Like others (https://github.com/helix-editor/helix/issues/2806), I want to send code selected in Helix to a Python REPL. I'm not going to get into the sending part here. To make selecting code easier, I group related lines into blocks, or 'cells', demarcated by # %%
comments. This example should give an idea of what I mean:
# %%
print('hi!')
# %%
def f():
...
# %%
To select cell context, I just need to look for two consecutive # %%
comments and select everything in between. In the interactive case, Helix makes this easy thanks to merge_selections <A-minus>
. But it still takes quite a few key presses to accomplish, so I'd like to save it in a keybinding. With the :replay_macro_from_string
command that I (tentatively) propose, that might look like this:
# When inside jupyter cell, search up for start marker '# %%';
# Move down one line.
# Search for end marker '# %%';
# Merge selections.
# Move up one line so as not to include cell marker.
# Result: only cell content is selected.
[keys.normal."minus"]
x = [":replay_macro_from_string", '?# %%<Ret>jxv/# %%<Ret><A-minus>k']
Next, I'd build on that keybinding such that the code, once selected, can be sent to the REPL:
[keys.normal."minus"]
x = [":replay_macro_from_string", '?# %%<Ret>jxv/# %%<Ret><A-minus>k', ":pipe-to send-to-repl"]
And with that, I no longer need my terminal/multiplexer to play the role of key press simulator.
From an implementation standpoint, would this perhaps be simpler than previously discussed alternatives – or am I simply restating the obvious out of ignorance? (In which case my apologies. :) )