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 for macro-style keybinding #1383

Closed
Omnikar opened this issue Dec 26, 2021 · 19 comments · Fixed by #4709
Closed

Support for macro-style keybinding #1383

Omnikar opened this issue Dec 26, 2021 · 19 comments · Fixed by #4709
Labels
A-helix-term Area: Helix term improvements C-enhancement Category: Improvements

Comments

@Omnikar
Copy link
Contributor

Omnikar commented Dec 26, 2021

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?

@Omnikar Omnikar added the C-enhancement Category: Improvements label Dec 26, 2021
@kirawi kirawi added the A-helix-term Area: Helix term improvements label Dec 27, 2021
@NNBnh
Copy link
Contributor

NNBnh commented Dec 27, 2021

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"]

@Omnikar
Copy link
Contributor Author

Omnikar commented Dec 27, 2021

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.

@glyh
Copy link

glyh commented Dec 28, 2021

@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.

@Omnikar
Copy link
Contributor Author

Omnikar commented Dec 28, 2021

I'm sorry, can you elaborate? What do you mean by "reamapping"?

@glyh
Copy link

glyh commented Dec 28, 2021

@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.

@Omnikar
Copy link
Contributor Author

Omnikar commented Dec 28, 2021

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.

@glyh
Copy link

glyh commented Dec 28, 2021

Well, in that case it would be alright.

@nluetts
Copy link

nluetts commented Jan 20, 2022

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.

@Omnikar
Copy link
Contributor Author

Omnikar commented Apr 15, 2022

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.

@BeetYeet
Copy link

BeetYeet commented Sep 8, 2022

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?

@getreu
Copy link
Contributor

getreu commented Nov 3, 2022

How would the suggested macro key binding of the key sequence mi" to a key - for example mq in normal mode - look like?

@Omnikar
Copy link
Contributor Author

Omnikar commented Nov 3, 2022

How would the suggested macro key binding of the key sequence mi" to a key - for example mq in normal mode - look like?

m.q = ["select_textobject_inner", "@\""]

@seanwarman
Copy link

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.

@edrex
Copy link

edrex commented Feb 5, 2023

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 #1200 and #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?

@the-mikedavis
Copy link
Member

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.

@edrex
Copy link

edrex commented Feb 6, 2023

What about adding some trait RuntimeCallable both TypableCommands and whatever ways regular commands take input implement?

#5555

@7ombie
Copy link
Contributor

7ombie commented Mar 6, 2024

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).

@CheeseTurtle
Copy link

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).

@lemontheme
Copy link
Contributor

lemontheme commented Jun 30, 2024

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 (#2806 (comment)) – 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 (#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. :) )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-helix-term Area: Helix term improvements C-enhancement Category: Improvements
Projects
None yet
Development

Successfully merging a pull request may close this issue.