Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
274 lines (210 sloc) 9.29 KB

Bang-bang key bindings for Elvish

Implement the !! (last command), !$ (last argument of last command) and !<n> (nth argument of last command) shortcuts in Elvish.

This file is written in literate programming style, to make it easy to explain. See alias.elv for the generated file.

Table of Contents

Usage

Install the elvish-modules package using epm:

use epm
epm:install github.com/zzamboni/elvish-modules

In your rc.elv, load this module.

use github.com/zzamboni/elvish-modules/bang-bang

When you press !, you will see a menu showing you the different keys your can press, for example, if you try to execute a command that requires root privileges:

[~]─> ls /var/spool/mqueue/
ls: cannot open directory '/var/spool/mqueue/': Permission denied
Exception: ls exited with 2

You can type sudo and then press !, which will show you the menu:

[~]─> sudo <!>
bang-lastcmd [A C]
!     ls /var/spool/mqueue/
0     ls
1/$   /var/spool/mqueue/
Alt-! !

If you press ! a second time, the full command will be inserted at the point:

[~]─> sudo ls /var/spool/mqueue/

If you wanted to see the permissions on that directory next, you could use the !$ shortcut instead:

[~]─> ls -ld <!>
bang-lastcmd [A C]
!     sudo ls /var/spool/mqueue/
0     sudo
1     ls
2/$   /var/spool/mqueue/
Alt-! !

Pressing $ (or 2) at this point will insert the last argument:

[~]─> ls -ld /var/spool/mqueue/

By default, bang-bang:init (which gets called automatically when the module loads) also binds the default “lastcmd” key (Alt-1), and when repeated, it will insert the full command. This means it fully emulates the default “last command” behavior. If you want to bind bang-bang to other keys, you can pass them in a list in the &extra-triggers option to bang-bang:init. For example, to bind bang-bang to Alt-` in addition to !:

bang-bang:init &extra-triggers=["Alt-`"]

By default, Alt-! (Alt-Shift-1) can be used to insert an exclamation mark when you really need one. This works both from insert mode or from “bang-mode” after you have typed the first exclamation mark. If you want to bind this to a different key, specify it with the &plain-bang option to bang-bang:init, like this:

bang-bang:init &plain-bang="Alt-3"

Implementation

Libraries

We load some necessary libraries.

use ./util
use re

Configuration

If you want hooks to be run either before or after entering bang-bang mode, you can add them as lambdas to these variables.

before-lastcmd = []
after-lastcmd = []

$-plain-bang-insert contains the key that is used to insert a plain !, also after entering lastcmd. Do not set directly, instead pass the &plain-bang option to init.

-plain-bang-insert = ""

$-extra-trigger-keys is an array containing the additional keys that will trigger bang-bang mode. These keys will also be bound, when pressed twice, to insert the full last command. Do not set directly, instead pass the &-extra-triggers option to init.

-extra-trigger-keys = []

Inserting a plain exclamation mark

This function gets bound to the keys specified in -plain-bang-insert.

fn insert-plain-bang { edit:insert:start; edit:insert-at-dot "!" }

bang-bang mode function

The bang-bang:lastcmd function is the central function of this module.

fn lastcmd {
  <<lastcmd code below>>
}

First, we run the “before” hooks, if any.

for hook $before-lastcmd { $hook }

We get the last command and split it in words for later use.

last = (edit:command-history -1)
### This is a workaround for a break in the edit:command-history values,
### used while https://github.com/elves/elvish/issues/821 gets fixed
extracted-cmd = (re:find 'unknown \{(.*) \d+\}' (to-string $last[cmd]))[groups][1][text]
last[cmd] = $extracted-cmd
### END workaround
parts = [(edit:wordify $last[cmd])]

We also get how wide the first column of the display should be, so that we can draw the selector keys right-aligned.

nitems = (count $parts)
indicator-width = (util:max (count $nitems) (count $-plain-bang-insert))
filler = (repeat $indicator-width ' ' | joins '')

The -display-text function returns the string to display in the menu, with the indicator right-aligned to $indicator-width spaces.

fn -display-text [ind text]{
  indcol = $filler$ind
  put $indcol[(- $indicator-width):]" "$text
}

We create the two “fixed” items of the bang-bang menu: the full command and the plain exclamation mark. Each menu item is a map with three keys: content is the text to insert when the option is selected, display is the text to show in the menu, and filter-text is the text which can be used by the user to filter options - usually it’s the same as content.

cmd = [
  &content=     $last[cmd]
  &display=     (-display-text "!" $last[cmd])
  &filter-text= $last[cmd]
]
bang = [
  &content=     "!"
  &display=     (-display-text $-plain-bang-insert "!")
  &filter-text= "!"
]

We now populate the menu items for each word of the command. For the last one, we also indicate that it can be selected with $.

items = [
  (range $nitems |
    each [i]{
      text = $parts[$i]
      if (eq $i (- $nitems 1)) {
        i = "$"
      } elif (> $i 9) {
        i = ""
      }
      put [
        &content=     $text
        &display=     (-display-text $i $text)
        &filter-text= $text
      ]
    }
  )
]

Finally, we put the whole list together.

candidates = [$cmd $@items $bang]

Now we create a list with the keybindings for the different elements of the menu. One-key bindings are only assigned for the first 9 elements and for the last one.

fn insert-full-cmd { edit:insert:start; edit:insert-at-dot $last[cmd] }
fn insert-part [n]{ edit:insert:start; edit:insert-at-dot $parts[$n] }
bindings = [
  &!=                   $insert-full-cmd~
  &"$"=                 { insert-part -1 }
  &$-plain-bang-insert= $insert-plain-bang~
]
for k $-extra-trigger-keys {
  bindings[$k] = $insert-full-cmd~
}
range (util:min (count $parts) 10) | each [i]{
  bindings[(to-string $i)] = { insert-part $i }
}

Finally, we invoke narrow mode with all the information we have put together, to display the menu and act on the corresponding choice.

edit:-narrow-read {
  put $@candidates
} [arg]{
  edit:insert-at-dot $arg[content]
  for hook $after-lastcmd { $hook }
} &modeline="bang-bang " &auto-commit=$true &ignore-case=$true &bindings=$bindings

Initialization

The init function gets called to set up the keybindings. This function can receive two options:

  • &plain-bang (string) to specify the key to insert a plain exclamation mark when needed. Defaults to =”Alt-!”=.
  • &extra-triggers (array of strings) to specify additional keys (other than !) to trigger bang-bang mode. All of these keys will also be bound, when pressed twice, to insert the full last command (just like !!). Defaults to ["Alt-1"], which emulates the default last-command keybinding in Elvish.
fn init [&plain-bang="Alt-!" &extra-triggers=["Alt-1"]]{
  -plain-bang-insert = $plain-bang
  -extra-trigger-keys = $extra-triggers
  edit:insert:binding[!] = $lastcmd~
  for k $extra-triggers {
    edit:insert:binding[$k] = $lastcmd~
  }
  edit:insert:binding[$-plain-bang-insert] = $insert-plain-bang~
}

We call init automatically on module load, although you can all it manually if you want to change the defaults for plain-bang or extra-triggers.

init
You can’t perform that action at this time.