Although there are many (fuzzy)-finder plugins around—most notably CtrlP—you may find them overkill for your own use cases, too slow, too complicated to extend, or with too many requirements (like Vim compiled with Ruby or Python), etc… Or you would just like to avoid installing yet another plugin. Well, it turns out that implementing your own finder à la CtrlP is pretty easy. If you think “fuzzy == imprecise” then you may like this solution. The core idea is extremely simple:

  1. Populate a new scratch buffer with the items to be filtered, one item per line;
  2. Use  :global!  to filter out items as the user types new characters;
  3. Use Vim undo functionality to restore a previous state when the user presses backspace.

The (fully functional and self-contained!) code below illustrates the idea. It allows you to interactively filter a list of items as you type, and return the selected item. You may use CTRL-K and CTRL-J to move up and down through the list; CTRL-L to clear the filter; ENTER to accept the current line; ESC to cancel. You may anchor the pattern at the start or at the end with ^ and $ respectively, and you can match any single character with . and any string with .*. For instance, ^v.*m will match Vim and vrooom!. The parameters are as follows:

input: either a shell command that sends its output, one item per line, to stdout, or a List of items to be filtered.

prompt: a String to be displayed at the command prompt.

Dealing with a multiple selection is left as an exercise to the reader :-)

   fun! FilterClose(bufnr)
      wincmd p
      execute "bwipe" a:bufnr
      redraw
      echo "\r"
      return []
   endf
    
   fun! Finder(input, prompt) abort
     let l:prompt = a:prompt . '>'
     let l:filter = ""
     let l:undoseq = []
     botright 10new +setlocal\ buftype=nofile\ bufhidden=wipe\
       \ nobuflisted\ nonumber\ norelativenumber\ noswapfile\ nowrap\
       \ foldmethod=manual\ nofoldenable\ modifiable\ noreadonly
     let l:cur_buf = bufnr('%')
     if type(a:input) ==# v:t_string
       let l:input = systemlist(a:input)
       call setline(1, l:input)
     else " Assume List
       call setline(1, a:input)
     endif
     setlocal cursorline
     redraw
     echo l:prompt . " "
     while 1
       let l:error = 0 " Set to 1 when pattern is invalid
       try
         let ch = getchar()
       catch /^Vim:Interrupt$/  " CTRL-C
         return FilterClose(l:cur_buf)
       endtry
       if ch ==# "\<bs>" " Backspace
         let l:filter = l:filter[:-2]
         let l:undo = empty(l:undoseq) ? 0 : remove(l:undoseq, -1)
         if l:undo
           silent norm u
         endif
       elseif ch >=# 0x20 " Printable character
         let l:filter .= nr2char(ch)
         let l:seq_old = get(undotree(), 'seq_cur', 0)
         try " Ignore invalid regexps
           execute 'silent keepp g!:\m' . escape(l:filter, '~\[:') . ':norm "_dd'
         catch /^Vim\%((\a\+)\)\=:E/
           let l:error = 1
         endtry
         let l:seq_new = get(undotree(), 'seq_cur', 0)
         " seq_new != seq_old iff the buffer has changed
         call add(l:undoseq, l:seq_new != l:seq_old)
       elseif ch ==# 0x1B " Escape
         return FilterClose(l:cur_buf)
       elseif ch ==# 0x0D " Enter
         let l:result = empty(getline('.')) ? [] : [getline('.')]
         call FilterClose(l:cur_buf)
         return l:result
       elseif ch ==# 0x0C " CTRL-L (clear)
         call setline(1, type(a:input) ==# v:t_string ? l:input : a:input)
         let l:undoseq = []
         let l:filter = ""
         redraw
       elseif ch ==# 0x0B " CTRL-K
         norm k
       elseif ch ==# 0x0A " CTRL-J
         norm j
       endif
       redraw
       echo (l:error ? "[Invalid pattern] " : "").l:prompt l:filter
     endwhile
   endf

The code can be used as follows:

   let items = Finder(['one','two','three','four','five'], 'Choose')
   echo "You have chosen: " empty(items) ? 'nothing' : items[0]


Finder.gif

Here are a few possible uses:

Buffer switcher:

   let buffers = split(execute('ls'), "\n")
   let choice = Finder(buffers, 'Switch to buffer')
   if !empty(choice)
     execute "buffer" split(choice[0], '\s\+')[0]
   endif

Edit most recently used files (MRU):

   let paths = Finder(v:oldfiles, 'Choose file')
   if !empty(paths)
     execute "args" join(map(paths, 'fnameescape(v:val)'))
   endif

Choose colorscheme:

   let colorschemes = map(globpath(&runtimepath, "colors/*.vim", 0, 1),
     \                'fnamemodify(v:val, ":t:r")')
   let colorschemes += map(globpath(&packpath,
     \                "pack/*/{opt,start}/*/colors/*.vim", 0, 1),
     \                'fnamemodify(v:val, ":t:r")')
   let choice = Finder(colorschemes, 'Choose colorscheme')
   if !empty(choice)
       execute "colorscheme" choice[0]
   endif

Find files in the current directory using find:

     let choice = Finder('find . -type f', "Choose file")
     if !empty(choice)
       execute "edit" choice[0]
     endif

Find files in the current directory using rg:

     let choice = Finder('rg --files .', "Choose file")
     if !empty(choice)
       execute "edit" choice[0]
     endif

Filter quickfix list and jump to item:

    let qfentry = Finder(split(execute('clist'), "\n"), 'Choose qf entry')
    if !empty(qfentry)
      execute "crewind" matchstr(qfentry[0], '^\s*\d\+', )
    endif

Find tag in current buffer (requires Exuberant Ctags):

    let s:tags = Finder(systemlist('ctags -f - --sort=no --excmd=number '
         \ . '--fields= --extra= --file-scope=yes '
         \ . shellescape(expand('%'))), 'Choose tag')
    if !empty(tag)
        let [s:tag, s:bufname, s:line] = split(s:tags[0], '\s\+')
        execute "buffer" "+".s:line s:bufname
    endif
Community content is available under CC-BY-SA unless otherwise noted.