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]