There have been a lot of blog entries, lately, about setting yourself up with the FZF fuzzy finder. Here's mine.
It includes an implementation of autojump.
Create the following executable Python script. Call it _cdmru_add.
#!/usr/bin/env python3
import argparse
import os
import pathlib
import sys
def main():
parser = argparse.ArgumentParser()
parser.add_argument("visit", help="The directory log the visit", type=pathlib.Path)
parser.add_argument("mru", help="The file in which to log the visit", type=pathlib.Path)
args = parser.parse_args()
if not args.visit.is_dir():
sys.stderr.write(f"{args.visit} is not a directory")
return
visit = os.path.expandvars(args.visit.resolve().expanduser().as_posix()).strip()
mru = []
if args.mru.is_file():
with args.mru.open(mode="r") as f:
mru = [pathlib.Path(directory.strip()) for directory in f.readlines()]
# Just keep the last 25 visits
mru_size = 25
mru = [directory.as_posix() for directory in mru if directory.is_dir()][:mru_size]
try:
visitIndex = mru.index(visit)
except ValueError:
visitIndex = None
if visitIndex is None:
mru.insert(0, visit)
else:
mru.insert(0, mru.pop(visitIndex))
with args.mru.open(mode="w") as f:
for directory in mru:
f.write(str(directory))
f.write("\n")
if __name__ == '__main__':
main()
What does it do? It moves each visited directory to a the front of a list. It takes two parameters: a directory, and a file to log it to. It keeps the last 25 entries.
I'm aware that most shells keep a list of visited directories. This is shell-agnostic and can be integrated with programs such as Ranger.
If we execute this each time we change directories, then we have the basis for an implementation of autojump. We add two more scripts to make the directory changer work.
It builds the list of visited directories to fuzzy-search. Note that it omits the most recent one, because that's already the current directory.
On FISH (which I use), I add a hook to add visited directories to the MRU list. I also set up file previews with Ctrl+T. This will also set up previews in fzf.vim. All of the following go in ~/.config/fish/config/fish:
function cdmru_add --on-variable PWD
status --is-command-substitution
and return
_cdmru_add (pwd) ~/.cdmru.txt
end
_cdmru_add . ~/.cdmru.txt
set -x FZF_DEFAULT_COMMAND 'sh -c "git ls-files 2> /dev/null || fd --type f"'
set -x FZF_DEFAULT_OPTS "-0 -1"
set -x FZF_CTRL_T_COMMAND $FZF_DEFAULT_COMMAND
set -x FZF_CTRL_T_OPTS $FZF_DEFAULT_OPTS --preview=\'bat -p --color=always --italic-text=always {}\'
set -x FZF_CTRL_R_OPTS $FZF_DEFAULT_OPTS
set -x FZF_ALT_C_COMMAND 'fd --type d'
set -x FZF_ALT_C_OPTS FZF_DEFAULT_OPTS
Add the following as ~/.config/fish/functions/j.fish,
function j
if not test -f ~/.cdmru.txt -a -r ~/.cdmru.txt
return false
end
if set -q argv[1]
set d (cat ~/.cdmru.txt | fzf-tmux --query=$argv[1])
and cd $d
else
set d (cat ~/.cdmru.txt | fzf-tmux)
and cd $d
end
end
This is the autojump "j" command.
And the following as ~/.config/fish/functions/jc.fish:
function jc
if set -q argv[1]
set d (fd --type d | fzf-tmux --query=$argv[1])
and cd $d
else
set d (fd --type d | fzf-tmux)
and cd $d
end
end
This is the autojump "jc" command.
While you're add it, add ~/.config/fish/functions/ranger-cd.fish to get **ranger-cd working:
function ranger-cd
set temp (mktemp)
ranger --choosedir=$temp $argv
cd (cat $temp)
rm $temp
end
Now we add a hook to Ranger to add visited directories to the MRU. Add the following as ~/.config/ranger/plugins/cdmru.py:
from __future__ import (absolute_import, division, print_function)
import ranger.api
import os
import subprocess
HOOK_INIT_OLD = ranger.api.hook_init
def hook_init(fm):
def cdmru_add(signal):
subprocess.call(['_cdmru_add', signal.new.path, os.path.expanduser('~/.cdmru.txt')])
fm.signal_bind('cd', cdmru_add)
return HOOK_INIT_OLD(fm)
ranger.api.hook_init = hook_init
Add the following as ~/.config/ranger/commands.py:
class j(Command):
def execute(self):
mru = os.path.expanduser('~/.cdmru.txt')
if not os.path.isfile(mru):
return
args = ['fzf']
if self.arg(1):
args.append("--query={}".format(self.rest(1)))
with open(mru) as f:
proc = self.fm.execute_command(args, stdin=f, stdout=subprocess.PIPE)
if proc.returncode == 0:
stdout, _ = proc.communicate()
self.fm.cd(stdout.strip().decode('utf-8'))
class jc(Command):
def execute(self):
args = ['fzf']
if self.arg(1):
args.append("--query={}".format(self.rest(1)))
fd_proc = subprocess.Popen(['fd', '--type', 'd'], stdout=subprocess.PIPE)
proc = self.fm.execute_command(args, stdin=fd_proc.stdout, stdout=subprocess.PIPE)
fd_proc.stdout.close()
if proc.returncode == 0:
stdout, _ = proc.communicate()
self.fm.cd(os.path.abspath(stdout.strip().decode('utf-8')))
class fzf(Command):
def execute(self):
cmd = "fzf --preview='bat -p --color=always --italic-text=always {}'"
if self.arg(1):
cmd = f'{cmd} --query={self.rest(1)}'
self.fm.notify(cmd)
proc = self.fm.execute_command(cmd, stdout=subprocess.PIPE)
if proc.returncode == 0:
stdout, _ = proc.communicate()
self.fm.select_file(os.path.abspath(stdout.strip().decode('utf-8')))
It will add the following commands:
- j
- jc
- fzf
Add the following keybindings to ~/.config/ranger/rc.conf:
map <alt>c jc
map <C-t> fzf
map <C-g> j
Alt+C and Ctrl+T now do in Ranger what they do in the shell, while Ctrl+g gives you the menu of visited directories:
In ~/.config/ranger/rifle.conf, look for lines that set:
label editor
to: ${VISUAL:-$EDITOR} -- "$@
and change them to set "label editor" to:
nvr --remote-silent -s "$@"
Now, here's what that enables. In tmux, open two splits. In one split, open neovim with:
env NVIM_LISTEN_ADDRESS=/tmp/nvimsocket nvim
Note: with FISH's history completion, typing that command is not as bad as it looks.
In the other split, open Ranger. Highlight a file and press "E" to edit it. It opens it in the neovim instance in the other split!
That means you can forget about NERDTree or any other embedded file browser. Ranger, running in another tmux split, is now your [http://vimcasts.org/blog/2013/01/oil-and-vinegar-split-windows-and-project-drawer/](project drawer).
There were a lot of FZF-setup blog entries when I started this, but these were particularly important: