3

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

lakshayg
  • 2,053
  • 2
  • 20
  • 34

1 Answers1

1

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

lakshayg
  • 2,053
  • 2
  • 20
  • 34
  • this script is phenomenally useful; I really love this idea of starting with a shortlist and then pressing space to expand to all git tracked files. I'm currently modifying it to be able to pass along an additional filter on files. That way, I can have separate bindings per filetype, and potentially also one for 'whatever filetype I am currently in' which would be the most common kind of filesearch for me. – TamaMcGlinn Jul 06 '23 at 09:46
  • I made your script into a plugin, with a few improvements: [`Plug 'TamaMcGlinn/vim-fuzzy-recent'`](https://github.com/TamaMcGlinn/vim-fuzzy-recent) So binding to fuzzy_recent#Find() is equivalent to your binding, but you can also go by filetype or do something custom with the result. Enjoy! – TamaMcGlinn Jul 07 '23 at 12:03
  • I'm glad you found it useful. This is a pretty old version of what I have now. Happy to share the latest one if you would like – lakshayg Jul 07 '23 at 17:06
  • sure, it would be interesting to hear what issues you have run into and fixed, since I've just started using this – TamaMcGlinn Jul 10 '23 at 11:00
  • 1
    Here's my most recent version: https://pastebin.com/u9kD76X8. Most notably, this does not require pressing to load all files. It happens automatically if there's no match in recent files (the binding is still present) – lakshayg Jul 10 '23 at 17:04
  • why did you switch from git ls-files to just ls-files? – TamaMcGlinn Jul 13 '23 at 06:30
  • overall it's great improvements and I have put them into the plugin as well; I like the prompts, the 40% height at the bottom, but just 'ls-files' doesn't work. We could add a third level, going from git ls-files to `find . -type f` to list also gitignored files. – TamaMcGlinn Jul 13 '23 at 06:54
  • 1
    Ah, I should have mentioned, ls-files is a custom script which switches between `git ls-files`, `fd` and `find` depending on what is installed and if I am in a git repo – lakshayg Jul 13 '23 at 07:45