How to repeat a zle widget an arbitrary number of times when the completion menu is open

zlezsh

I'm using the zsh shell, and I'm trying to install a few key bindings to use keys similar to the ones I would use in a Vim buffer, when a completion menu is open.

So, inside the menuselect keymap I've bound the keys j and k to the zle widgets down-line-or-history and up-line-or-history, by adding the following lines inside ~/.zshrc:

bindkey -M menuselect 'j' down-line-or-history
bindkey -M menuselect 'k' up-line-or-history

down-line-or-history and up-line-or-history are described in man zshzle as follows:

down-line-or-history (^N ESC-[B) (j) (ESC-[B)
        Move down a line in the buffer, or if already at the bottom line, move to the next event in  the  his‐
        tory list.

up-line-or-history (^P ESC-[A) (k) (ESC-[A)
        Move up a line in the buffer, or if already at the top line, move to the previous event in the history
        list.

Now, I would like to bind C-d and C-u to the same widgets, but repeating them an arbitrary number of times, for example 5.

At first I tried this simple code:

some-widget() {
    zle backward-char -n 5
}
zle -N some-widget
bindkey '^D' some-widget

It binds C-d to the zle widget backward-char, but repeats it 5 times.

Then, I tried to rewrite the code, moving the key binding from the default keymap to the menuselect keymap:

some-widget() {
    zle backward-char -n 5
}
zle -N some-widget
bindkey -M menuselect '^D' some-widget

But it doesn't work as I expected, because when I hit C-d while a completion menu is open, zle seems to execute the default widget bound to C-d, which is delete-char-or-list:

delete-char-or-list (^D) (unbound) (unbound)
        Delete the character under the cursor.  If the cursor is at the end of the line, list possible comple‐
        tions for the current word.

It exits the current completion menu, and lists possible completions for the current word, instead of moving the cursor backward 5 times.

If it had worked as I expected, I would probably have ended up using this final code:

fast-down-line-or-history() {
    zle down-line-or-history -n 5
}
zle -N fast-down-line-or-history
bindkey -M menuselect '^D' fast-down-line-or-history

fast-up-line-or-history() {
    zle up-line-or-history -n 5
}
zle -N fast-up-line-or-history
bindkey -M menuselect '^U' fast-up-line-or-history

But since it doesn't, I need to find how to repeat a zle widget when a completion menu is open.

How to modify the previous code, so that down-line-or-history is repeated 5 times when hitting C-d while the completion menu is open?

Best Answer

Your code doesn't work because the widgets "always perform the same task within the menu selection map and cannot be replaced by user defined widgets, nor can the set of functions be extended".

However, the following does work:

bindkey -M menuselect 'j' down-line-or-history
bindkey -M menuselect 'k' up-line-or-history
bindkey -M menuselect -s '^D' 'jjjjj'
bindkey -M menuselect -s '^U' 'kkkkk'

The -s option lets you specify a literal string that will be input to the Zsh Line Editor (ZLE) as if you had typed it yourself. So, now, when you press ctrlD, will press j five times for you, giving you the desired effect.

Note, though, that it's not particularly fast. I can literally see the cursor moving 5 steps up or down, one step at a time. ?

Instead, if you have group-names enabled, you might be better off binding

  • ctrlD to vi-forward-blank-word, which (according to the documentation) "moves the mark to the first line of the next group of matches" and
  • ctrlU to vi-backward-blank-word, which "moves the mark to the last line of the previous group of matches".
Related Question