fzf.vim: fuzzy matching with a fall-back source

807 views Asked by At

I would like to implement a vim command to select buffers with the following behaviour:

  1. When called, it present the user with a list of loaded buffers and other recently used buffers from the current directory (This is different from the :History command provided by fzf.vim in that we list only the recently used buffers from the current directory, fzf.vim lists all the recent buffers). The user can search for the file name they would like to load
  2. If none of the options matches user's search term, expand the scope of the search by listing all the files in the current directory and letting the user fuzzy search through them.

This is what I have so far (This assumes that junegunn/fzf.vim is already installed):

nnoremap <silent> <space><space> :call <SID>recent_files()<CR>

function! s:recent_files_sink(items)
  if len(a:items) == 2
    execute "edit" a:items[1]
    return
  endif
  call fzf#vim#files("", {'options': ['--query', a:items[0]]})
endfunction

" deduped is a list of items without duplicates, this
" function inserts elements from items into deduped
function! s:add_unique(deduped, items)
  let dict = {}
  for item in a:deduped
    let dict[item] = ''
  endfor

  for f in a:items
    if has_key(dict, f) | continue | endif
    let dict[f] = ''
    call add(a:deduped, f)
  endfor
  return a:deduped
endfunction

function! s:recent_files()
  let regex = '^' . fnamemodify(getcwd(), ":p")
  let buffers = filter(map(
        \ getbufinfo({'buflisted':1}), {_, b -> fnamemodify(b.name, ":p")}),
        \ {_, f -> filereadable(f)}
        \ )
  let recent = filter(
        \ map(copy(v:oldfiles), {_, f -> fnamemodify(f, ":p")}),
        \ {_, f -> filereadable(f) && f =~# regex})
  let combined = s:add_unique(buffers, recent)
  call fzf#run(fzf#wrap({
        \ 'source': map(combined, {_, f -> fnamemodify(f, ":~:.")}),
        \ 'sink*': function('s:recent_files_sink'),
        \ 'options': '--print-query --exit-0 --prompt "Recent> "'
        \ }))
endfunction

SpaceSpace invokes s:recent_files() which lists loaded buffers and recently used files from viminfo. The interesting bit here is the sink* option in the call to fzf#run (4th line from the bottom). The sink is another function. If a filename was selected, the sink function loads it for editing, otherwise, it calls fzf#vim#files, which lists the contents of the directory.

This is pretty close to what I want but there are a couple of problems:

  1. When no matches are found in recent files, the user must press Return to trigger the fall-back. (One can easily argue that this is the correct behaviour)
  2. When the fall-back fzf window is loaded, it starts in the normal mode and not the insert mode
  3. The user must enter the search query again in the new fzf window (solved)

I'm looking for suggestions on how to solve these problems, particularly 2 and 3. I'm also open to solutions that don't meet the specifications exactly but provide a good user experience.

EDIT: I came up with another approach to achieve this using this as the source for fzf

cat recent_files.txt fifo.txt

where recent_files.txt is a list of recent files and fifo.txt is an empty fifo created using mkfifo. A mapping can be added to buffer which triggers a command to write to the fifo. This way, the user can use that mapping to include a list of all files in case they don't find a match in recent files. This approach becomes problematic in cases where user finds the file in recents and presses enter. Since fzf is till waiting to read from fifo, it does not exit https://github.com/junegunn/fzf/issues/2288

1

There are 1 answers

8
lakshayg On BEST ANSWER

I was finally able to come to a solution that is pretty close using fzf's reload functionality. (Thanks to junegunn)

This is how it goes:

nnoremap <silent> <space><space> :call <SID>recent_files()<CR>

" Initialize fzf with a list of loaded buffers and recent files from
" the current directory. If <space> is pressed, we load a list of all
" the files in the current directory
function! s:recent_files()
  let regex = '^' . fnamemodify(getcwd(), ":p")
  let buffers = filter(map(
        \ getbufinfo({'buflisted':1}), {_, b -> fnamemodify(b.name, ":p")}),
        \ {_, f -> filereadable(f)}
        \ )
  let recent = filter(
        \ map(copy(v:oldfiles), {_, f -> fnamemodify(f, ":p")}),
        \ {_, f -> filereadable(f) && f =~# regex})
  let combined = <SID>add_unique(buffers, recent)

  "-------------------------------------------
  " This is the key piece that makes it work
  "-------------------------------------------
  let options = [
        \ '--bind', 'space:reload:git ls-files',
        \ ]

  call fzf#run(fzf#wrap({
        \ 'source': map(combined, {_, f -> fnamemodify(f, ":~:.")}),
        \ 'options': options
        \ }))
endfunction

" deduped is a list of items without duplicates, this
" function inserts elements from items into deduped
function! s:add_unique(deduped, items)
  let dict = {}
  for item in a:deduped
    let dict[item] = ''
  endfor

  for f in a:items
    if has_key(dict, f) | continue | endif
    let dict[f] = ''
    call add(a:deduped, f)
  endfor
  return a:deduped
endfunction

I start FZF by using <space><space>. This FZF window contains only the most recent files from the current directory. If I then press <space>, the FZF window is updated with a new list of files obtained from git ls-files.


Update: Apr 2023

FZF now supports a zero event. It is triggered when no match is found. This can be used to trigger a reload like the example above does with space