Poor man's CtrlP

2 Points

Lifepillar Lifepillar

6 months ago

Interactively filter a list of items as you type, and return the selected item. Sort of a poor man's CtrlP. 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. It's all self-contained!

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

Although the code seems complicated, it's mostly bell-and-whistles. The core of the idea is extremely simple: (a) use :g to filter out lines as the user is inserting characters; (b) use undo to restore a previous state when the user presses backspace.

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! FilterInteractively(input, prompt) abort
  let l:prompt = a:prompt . '>'
  let l:filter = ''  " Text used to filter the list
  let l:undoseq = [] " Stack to tell whether to undo when pressing backspace (1 = undo, 0 = do not undo)
  botright 10new +setlocal\ buftype=nofile\ bufhidden=wipe\
        \ nobuflisted\ nonumber\ norelativenumber\ noswapfile\ nowrap\
        \ winfixheight\ foldmethod=manual\ nofoldenable\ modifiable\ noreadonly
  let l:cur_buf = bufnr('%') " Store current buffer number
  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 " to 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 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

" Test

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