Permalink
Switch branches/tags
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
244 lines (190 sloc) 10.1 KB

Elvish completions for git

Completions for git, including automatically-generated completions for both subcommands and command-line options.

Some original inspiration from occivink’s git completer.

Table of Contents

Usage

Install the elvish-completions package using epm:

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

In your rc.elv, load this module:

use github.com/zzamboni/elvish-completions/git

Now you can type git<space>, press Tab and see the corresponding completions. All git commands are automatically completed with their options (automatically extracted from their help messages). Some commands get more specific completions, including add, push, checkout, diff and a few others. Git aliases are automatically detected as well. Aliases which point to a single git command are automatically completed like the original command.

Several components are colorized, you can configure the styles by setting these variables (default values shown):

modified-style  = yellow
untracked-style = red
tracked-style   = ''
branch-style    = blue
remote-style    = cyan

You can change which command is used instead of git by assigning it to $git:git-command. The command assigned needs to understand at least the same commands and options as git. One example of such command is hub. You can also assign functions (for example, a wrapper function around git).

git-command = git

Implementation

Libraries and global variables

We first load a number of libraries, including comp, the Elvish completion framework.

use ./comp
use re
use github.com/muesli/elvish-libs/git
use github.com/zzamboni/elvish-modules/util

This is where the big completion-definition map will get progressively built.

completions = [&]

We store the output of git:status in a global variable to make it easier to access by the different completion functions.

status = [&]

Here we will store the completer function (for easier access and for testing).

git-arg-completer = { }

Configuration variables

The git-command variable contains the command or function to run instead of git (can be assigned to hub, for example, or any wrapper function you want to use, as long as it accepts the same options and commands as git).

<<git-command>>

The $*-style variables contains the style (as per styled) to use for different completion components the completion menu. Set to =”= (an empty string) to show in the normal style.

<<git-completion-styles>>

Utility functions

The -run-git function executes a git-like command, with the given arguments. $git-command can be a single command, a multi-word command or a function and still be executed correctly. We cannot simply run $gitcmd $@rest because Elvish always interprets the first token (the head) to be the command. One example of a multi-word $gitcmd is =”vcsh <repo>”=, after which any git subcommand is valid.

(please note: $git-command is only used for command executed from within this module, not from the muesli/git module which is used for some pieces of information, so the integration is not yet perfect. But for most commands it should work - for example if you use hub instead of git.

fn -run-git [@rest]{
  gitcmds = [$git-command]
  if (eq (kind-of $git-command) string) {
    gitcmds = [(splits " " $git-command)]
  }
  cmd = $gitcmds[0]
  if (eq (kind-of $cmd) string) {
    cmd = (external $cmd)
  }
  $cmd (explode $gitcmds[1:]) $@rest
}

The -git-opts function receives an optional git command, runs git [command] -h and parses the output to extract the command line options. The parsing is done with comp:extract-opts, but we pre-process the output to join options whose descriptions appear in the next line.

fn -git-opts [@cmd]{
  _ = ?(-run-git $@cmd -h 2>&1) | drop 1 | if (eq $cmd []) {
    comp:extract-opts &fold=$true &regex='--(\w[\w-]*)' &regex-map=[&long=1]
  } else {
    comp:extract-opts &fold=$true
  }
}

We define the functions that return different possible values used in the completions. Some of these functions assume that $status contains already the output from git:status, which gets executed as the pre-hook of the git completer function below.

fn MODIFIED      { explode $status[local-modified] | comp:decorate &style=$modified-style }
fn UNTRACKED     { explode $status[untracked] | comp:decorate &style=$untracked-style }
fn MOD-UNTRACKED { MODIFIED; UNTRACKED }
fn TRACKED       { _ = ?(-run-git ls-files 2>/dev/null) | comp:decorate &style=$tracked-style }
fn BRANCHES      [&all=$false]{
  -allarg = []
  if $all { -allarg = ['--all'] }
  _ = ?(-run-git branch --list (explode $-allarg) --format '%(refname:short)' 2>/dev/null |
  comp:decorate &display-suffix=' (branch)' &style=$branch-style)
}
fn REMOTES       { _ = ?(-run-git remote 2>/dev/null | comp:decorate &display-suffix=' (remote)' &style=$remote-style ) }

Initialization of completion definitions

$git:git-completions contains the specialized completions for some git commands. Each sequence is a list of functions which return the possible completions at that point in the command. The ... as a last element in some of them indicates that the last completion function is repeated for all further argument positions. The completion can also be a string, in which case it means an alias for some other command.

git-completions = [
  &add=      [ [stem]{ MOD-UNTRACKED; comp:dirs $stem } ... ]
  &stage=    add
  &checkout= [ { MODIFIED; BRANCHES } ... ]
  &mv=       [ [stem]{ TRACKED; comp:dirs $stem } ... ]
  &rm=       [ [stem]{ TRACKED; comp:dirs $stem } ... ]
  &diff=     [ { MODIFIED; BRANCHES  } ... ]
  &push=     [ $REMOTES~ $BRANCHES~ ]
  &pull=     [ $REMOTES~ { BRANCHES &all } ]
  &merge=    [ $BRANCHES~ ... ]
  &init=     [ [stem]{ put "."; comp:dirs $stem } ]
  &branch=   [ $BRANCHES~ ... ]
]

In the git:init function we initialize the $completions map with the necessary data structure for comp:subcommands to provide the completions. We extract as much information as possible automatically from git itself.

  fn init {
    completions = [&]
    <<init-git-commands>>
    <<init-git-aliases>>
    <<setup-completer>>
}

Next , we fetch the list of valid git commands from the output of git help -a, and store the corresponding completion sequences in $completions. All of them are configured to produce completions for their options, as extracted by the -git-opts function. Commands that have corresponding definitions in $git-completions get them, otherwise they get the generic filename completer.

-run-git help -a | eawk [line @f]{ if (re:match '^  [a-z]' $line) { put $@f } } | each [c]{
  seq = [ $comp:files~ ... ]
  if (has-key $git-completions $c) {
    seq = $git-completions[$c]
  }
  if (eq (kind-of $seq 'string')) {
    completions[$c] = $seq
  } else {
    completions[$c] = (comp:sequence $seq &opts={ -git-opts $c })
  }
}

Next, we parse the defined aliases from the output of git config --list. We store the aliases in completions as well, but we check if an alias points to another valid command. In this case, we store the name of the target command as its value, which comp:expand interprets as “use the completions from the target command”. If an alias does not expand to another existing command, we set up its completions as empty.

-run-git config --list | each [l]{ re:find '^alias\.([^=]+)=(.*)$' $l } | each [m]{
  alias target = $m[groups][1 2][text]
  if (has-key $completions $target) {
    completions[$alias] = $target
  } else {
    completions[$alias] = (comp:sequence [])
  }
}

We setup the completer by assigning the function to the corresponding element of $edit:completion:arg-completer.

git-arg-completer = (comp:subcommands $completions \
  &pre-hook=[@_]{ status = (git:status) } &opts={ -git-opts }
)
edit:completion:arg-completer[git] = $git-arg-completer

We run init by default on load, although it can be re-run if you change any configuration variables (most notably git:git-command).

init

Test suite

use github.com/zzamboni/elvish-completions/git
use github.com/zzamboni/elvish-modules/test

cmds = ($git:git-arg-completer git '')

(test:set github.com/zzamboni/elvish-completions/git
  (test:set "common top-level commands"
    (test:check { has-value $cmds add })
    (test:check { has-value $cmds checkout })
    (test:check { has-value $cmds commit })
  )
)