Skip to content

Instantly share code, notes, and snippets.

@duganchen
Last active September 13, 2022 13:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save duganchen/404fd3ebc9d42be980f2957aa1764dca to your computer and use it in GitHub Desktop.
Save duganchen/404fd3ebc9d42be980f2957aa1764dca to your computer and use it in GitHub Desktop.

Fuzzy Finding Setup and MRU-based Directory Changer

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.

Components

Scaffolding

Find Files

Directory MRU Update

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.

FISH

CD MRU Hook And Previews

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

"j" Command"

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.

"jc" 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.

"ranger-cd" 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

Ranger

Directory MRU Update

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

Commands

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

Keybindings

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:

Vim Integration

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).

Resources

There were a lot of FZF-setup blog entries when I started this, but these were particularly important:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment