Permalink
Switch branches/tags
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
290 lines (227 sloc) 8.12 KB

Directory history and functions for Elvish

Keep and move through the directory history, including a graphical chooser, similar to Elvish’s Location mode, but showing a chronological directory history instead of a weighted one.

This file is written in literate programming style. See dir.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 the dir module. This automatically sets up the chdir hook to keep track of directory history:

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

You can set up keybindings to navigate the directory history and to trigger the history chooser. The module includes two convenience functions left-word-or-prev-dir and right-word-or-next-dir which “do the right thing” depending on the contents of the current command. For example:

edit:insert:binding[Alt-b] = $dir:left-word-or-prev-dir~
edit:insert:binding[Alt-f] = $dir:right-word-or-next-dir~
edit:insert:binding[Alt-i] = $dir:history-chooser~

If you want the cd command to recognize a hyphen as “the previous directory”, you need to set up an alias from cd to call dir:cd instead. You can use the alias module for this:

use github.com/zzamboni/elvish-modules/alias
alias:new cd "use github.com/zzamboni/elvish-modules/dir; dir:cd"

The dir:cdb function changes to the base directory of its given argument. For example, dir:cdb ~/tmp/foo.txt will cd to ~/tmp. I usually set up an alias for cdb as well:

alias:new cdb "use github.com/zzamboni/elvish-modules/dir; dir:cdb"

Please note that for the aliases to take effect, you need to add the following line at the end of your rc.elv file:

-exports- = (alias:export)

See alias.org for full usage instructions for the alias module.

Implementation

use builtin
use narrow

Configuration

before-chooser and after-chooser can contain lambdas that will be run before and after starting the history-chooser.

before-chooser = []
after-chooser = []

Maximum stack size, 0 for no limit

max-stack-size = 100

Internal variables and functions

The stack and a pointer into it, which points to the current directory. Normally the cursor points to the end of the stack, but it can move with back and forward.

-dirstack = [ $pwd ]
-cursor = (- (count $-dirstack) 1)

Remove everything after $cursor from the stack.

fn -trimstack {
  -dirstack = $-dirstack[0:(+ $-cursor 1)]
}

Stack query functions

Return the current contents of the directory stack.

fn stack { put $@-dirstack }

Return the number of elements in the stack.

fn stacksize { count $-dirstack }

Pretty-print the stack, with the current directory highlighted.

fn history {
  for index [(range 0 (stacksize))] {
    if (== $index $-cursor) {
      echo (styled "* "$-dirstack[$index] green)
    } else {
      echo "  "$-dirstack[$index]
    }
  }
}

Return the current directory in the stack, empty string if stack is empty

fn curdir {
  if (> (stacksize) 0) {
    put $-dirstack[$-cursor]
  } else {
    put ""
  }
}

Stack manipulation functions

Add $pwd into the stack at $-cursor, trimming it to the last $max-stack-size entries, only if it’s different than the current directory (i.e. you can call push multiple times in the same directory, for example as part of a prompt hook, and it will only be added once). Pushing a directory invalidates any directories after it in the stack (i.e. if $cursor was not pointing at the end of the stack). After push, $cursor is always pointing to the last element of the stack.

fn push {
  if (or (== (stacksize) 0) (!=s $pwd (curdir))) {
    -dirstack = [ (explode $-dirstack[0:(+ $-cursor 1)]) $pwd ]
    if (> (stacksize) $max-stack-size) {
      -dirstack = $-dirstack[(- $max-stack-size):]
    }
    -cursor = (- (stacksize) 1)
  }
}

Move back and forward through the stack. These functions do not alter the stack, only the value of $cursor.

fn back {
  if (> $-cursor 0) {
    -cursor = (- $-cursor 1)
    builtin:cd $-dirstack[$-cursor]
  } else {
    echo "Beginning of directory history!" > /dev/tty
  }
}

fn forward {
  if (< $-cursor (- (stacksize) 1)) {
    -cursor = (+ $-cursor 1)
    builtin:cd $-dirstack[$-cursor]
  } else {
    echo "End of directory history!" > /dev/tty
  }
}

Pop the previous directory on the stack, removes the current one. Successive pops walk back the stack until it’s empty, but don’t allow you to move forward again.

fn pop {
  if (> $-cursor 0) {
    back
    -trimstack
  } else {
    echo "No previous directory to pop!" > /dev/tty
  }
}

Directory changing

cd wrapper which supports =”-“= to indicate the previous directory. Can be aliased to the cd command.

fn cd [@dir]{
  if (and (== (count $dir) 1) (eq $dir[0] "-")) {
    builtin:cd $-dirstack[(- $-cursor 1)]
  } else {
    builtin:cd $@dir
  }
}

cd to the base directory of the argument.

fn cdb [p]{ cd (dirname $p) }

Utility functions to move the cursor by a word or move through the directory history, depending on the contents of the command. These only work when bound to keys, due to their use of $edit:current-command.

fn left-word-or-prev-dir {
  if (> (count $edit:current-command) 0) {
    edit:move-dot-left-word
  } else {
    back
  }
}

fn right-word-or-next-dir {
  if (> (count $edit:current-command) 0) {
    edit:move-dot-right-word
  } else {
    forward
  }
}
fn left-small-word-or-prev-dir {
  if (> (count $edit:current-command) 0) {
    edit:move-dot-left-small-word
  } else {
    back
  }
}

fn right-small-word-or-next-dir {
  if (> (count $edit:current-command) 0) {
    edit:move-dot-right-small-word
  } else {
    forward
  }
}

Interactive dir history chooser.

fn history-chooser {
  for hook $before-chooser { $hook }
  index = 0
  candidates = [(each [arg]{
        put [
          &content=$arg
          &display=$index" "$arg
          &filter-text=$index" "$arg
        ]
        index = (+ $index 1)
  } $-dirstack)]
  edit:-narrow-read {
    put $@candidates
  } [arg]{
    builtin:cd $arg[content]
    for hook $after-chooser { $hook }
  } &modeline="Dir history " &ignore-case=$true &keep-bottom=$true
}

Initialization

Set up a hook to push the current directory after every cd, to automatically populate the directory history.

fn init {
  after-chdir = [ $@after-chdir [dir]{ push } ]
}

init