junegunn/fzf

Add "reload" action for dynamically updating the input list

junegunn opened this issue · 5 comments

See devel branch for the progress.

Summary

Add bindable reload action that can start an arbitrary program and dynamically replace the input list of fzf with its result without restarting fzf.

Rationale

fzf was designed to be a Unix filter that consumes input only once. However, due to the interactive nature of it, some users want to update the input without restarting fzf altogether (using esoteric --no-clear option).

For example,

  • Input comes from a REST API that generates dynamic content, and you want to see the updated list by pressing a special key such as CTRL-R (R for refresh or reload).
  • You want to press a set of keys to dynamically switch between different sets of inputs; CTRL-F for a list of files, and CTRL-D for a list of directories.
  • You use fzf as the secondary filter to the result of a primary filter program such as ripgrep or silver searcher. And you want to restart the primary filter program with an updated query string you typed on fzf because they are much more efficient than fzf for searching through the file contents.
    • You may even want to restart the primary filter program every time you change the query string on fzf. In this case, you probably want to use fzf only as a selector interface rather than a secondary "fuzzy filter", especially because of the incompatible search syntax. The search is completely done by the primary filter as you use the new --phony option (#1723).

Instead of introducing a separate mode of execution as suggested in #751 and #1736. I'd like to add a special action called reload that can be bound to a key or change event using the good old --bind.

Examples

1. Update the list of processes by pressing CTRL-R

ps -ef | fzf --bind 'ctrl-r:reload(ps -ef)' --header 'Press CTRL-R to reload' \
             --header-lines=1 --layout=reverse

2. Switch between sources by pressing CTRL-D or CTRL-F

find . -type f |
  fzf --bind 'ctrl-d:reload(find . -type d),ctrl-f:reload(find . -type f)'

There are two problems here:

  • We're repeating find . -type f command twice
  • The initial find process may take a long time to finish. Since fzf cannot kill the process behind the standard input, the process will keep running even after we hit CTRL-D.

To work around the issues, we set $FZF_DEFAULT_COMMAND to the initial find command, so fzf can start the process and kill it when it has to.

FZF_DEFAULT_COMMAND='find . -type f' fzf \
  --bind 'ctrl-d:reload(find . -type d),ctrl-f:reload($FZF_DEFAULT_COMMAND)'

3. Ripgrep integration

The following example uses fzf as the selector interface for ripgrep. We bound reload action to change event, so every time you type on fzf, ripgrep process will restart with the updated query string denoted by the placeholder expression {q}. Also, note that we used --phony option so that fzf doesn't perform any secondary filtering.

RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
INITIAL_QUERY=""
FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY'" \
  fzf --bind "change:reload:$RG_PREFIX {q} || true" \
      --ansi --phony --query "$INITIAL_QUERY"

If ripgrep doesn't find any matches, it will exit with a non-zero exit status, and fzf will warn you about it. To suppress the warning message, we added || true to the command, so that it always exits with 0.

4. Ripgrep integration with fzf.vim

function! RipgrepFzf(query, fullscreen)
  let command_fmt = 'rg --column --line-number --no-heading --color=always --smart-case %s || true'
  let initial_command = printf(command_fmt, shellescape(a:query))
  let reload_command = printf(command_fmt, '{q}')
  let spec = {'options': ['--phony', '--query', a:query, '--bind', 'change:reload:'.reload_command]}
  call fzf#vim#grep(initial_command, 1, fzf#vim#with_preview(spec), a:fullscreen)
endfunction

command! -nargs=* -bang Rg call RipgrepFzf(<q-args>, <bang>0)

0.19.0 released.

Hey @junegunn I am trying to bind the Ripgrep Integration to Ctrl-f (in zsh), and I don't want to change FZF_DEFAULT_COMMAND so my other settings are affected, I have come up with the following...

RG_PREFIX='rg --column --line-number --no-heading --color=always --smart-case '
INITIAL_QUERY=''
bindkey -s '^f' "$RG_PREFIX '$INITIAL_QUERY' | fzf --bind \"change:reload:$RG_PREFIX {q} || true\" --ansi --phony --query \"$INITIAL_QUERY\"^M"

...which works, but a couple things...

  1. How can I get the file printed back to my current terminal command (similar to how Ctrl-t works)? It seem I have to bind to enter with execute and something like cut... but I haven't been able to get it to work yet...
  2. I am frequently seeing that exiting the command takes several seconds, regardless of whether an entry was accepted or I exited via Ctrl-c, any insights into why this may be and how to improve it?
  1. I'm not a zsh user, so I can't give you the best advice. You might want to study the code in https://gist.github.com/junegunn/8b572b8d4b5eddd8b85e5f4d40f17236
  2. That's why we're temporarily setting FZF_DEFAULT_COMMAND in the above example. By doing so, fzf launches the process and it can kill it when it exits.

Thanks for the tips @junegunn! I solved it by adding the following to my .zshrc, in case others are interested...

# Ctrl-F (Find in Files)
RG_PREFIX='rg --column --line-number --no-heading --color=always --smart-case '
INITIAL_QUERY=''
FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY' $HOME"

__fif() {
    fzf --bind "change:reload:$RG_PREFIX {q} $HOME || true" --ansi --phony --query "$INITIAL_QUERY" | cut -d ':' -f1
}

find-in-files() {
  LBUFFER="${LBUFFER}$(__fif)"
  local ret=$?
  zle reset-prompt
  return $ret
}

zle -N find-in-files
bindkey '^f' find-in-files

...note that I added $HOME in a couple places as I usually want to search beyond just the current directory (and use ~/.ignore or alternatives to maintain speed).

Is it possible to get this working with -1, so once a selection is narrowed down to 1, it's picked?