Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- " findfile.vim -- interactive file search
- " Experimental; caveat emptor.
- " Documentation is at the end.
- " Public Domain.
- let s:cpo = &cpo
- set cpo&vim
- " OPTIONS
- " Regexp searches are highlighted.
- let s:hiregexp = 1
- let s:defmode = 'basic'
- let s:canpreview = 1
- let s:hioneitem = 1
- let s:thisfile = expand('<sfile>:t')
- let s:cacheprefix = expand('~/findcache')
- let s:unavailprv = expand('~/[not\ available]')
- " Pertinent line numbers in the Find File window.
- let s:infolnum = 1
- let s:inputlnum = 2
- let s:item1lnum = 3
- " Search modes.
- let s:modes = ['basic', 'advanced', 'regexp']
- let s:modesym = ' +/'
- " s:textchanged() can be invoked on:
- let s:typing = 0
- let s:return = 1
- let s:modechange = 2
- fun! s:err(msg)
- echoerr '(' . s:thisfile . ') ' . a:msg
- endfun
- fun! s:inputchanged()
- return b:findinput != getline(s:inputlnum)
- endfun
- fun! s:oneitem()
- return b:findnfilt == 1
- endfun
- fun! s:oninfoline()
- return line('.') == s:infolnum
- endfun
- fun! s:oninputline()
- return line('.') == s:inputlnum
- endfun
- fun! s:onitems()
- return line('.') > s:inputlnum
- endfun
- fun! s:emptyinput()
- return getline(s:inputlnum) == ''
- endfun
- fun! s:cyclemode()
- let b:findmodeidx = (b:findmodeidx + 1) % len(s:modes)
- let b:findmode = s:modes[b:findmodeidx]
- call s:textchanged(s:modechange)
- redraw
- endfun
- fun! s:bs()
- if s:oninputline() && s:emptyinput()
- call s:cyclemode()
- return ''
- else
- return "\<BS>"
- endif
- endfun
- fun! s:filterord(input, list)
- let input = substitute(a:input, '^\s\+\|\s\+$', '', 'g')
- if input == ''
- return a:list
- endif
- let terms = '\%#=2\V' .
- \ substitute(escape(input, '\'), '\s\+', '\\.\\*', 'g')
- return input =~# '\u'
- \ ? filter(copy(a:list), {i, item -> item =~# terms})
- \ : filter(copy(a:list), {i, item -> item =~? terms})
- endfun
- fun! s:filterunord(input, list)
- let input = substitute(a:input, '^\s\+\|\s\+$', '', 'g')
- if input == ''
- return a:list
- endif
- let terms = split(input, ' ')
- call filter(terms, {i, term -> term != '!'})
- let b:terms = terms
- if empty(terms)
- return a:list
- endif
- call map(terms, {i, term -> escape(term, '\')})
- let exact = input =~# '\u'
- let list = terms[0][0] == '!'
- \ ? exact
- \ ? filter(copy(a:list), {i, item -> item !~# '\V' . terms[0][1:]})
- \ : filter(copy(a:list), {i, item -> item !~? '\V' . terms[0][1:]})
- \ : exact
- \ ? filter(copy(a:list), {i, item -> item =~# '\V' . terms[0]})
- \ : filter(copy(a:list), {i, item -> item =~? '\V' . terms[0]})
- if len(terms) > 1
- let filt = []
- for item in list
- for term in terms[1:]
- let match = term[0] == '!'
- \ ? exact
- \ ? item !~# '\V' . term[1:]
- \ : item !~? '\V' . term[1:]
- \ : exact
- \ ? item =~# '\V' . term
- \ : item =~? '\V' . term
- if !match
- break
- endif
- endfor
- if match
- call add(filt, item)
- endif
- endfor
- else
- let filt = list
- endif
- return filt
- endfun
- fun! s:open()
- let file = s:oninputline()
- \ ? getline('$')
- \ : getline('.')
- if isdirectory(file) || filereadable(file)
- hide pclose
- if bufexists(file)
- " Don't mess about if the buffer is already
- " open. You could be on a slow network.
- exe 'b ' . fnameescape(file)
- doau bufenter
- else
- exe 'e ' . fnameescape(file)
- doau bufenter,bufread
- endif
- endif
- endfun
- " <CR>'s behaviour.
- fun! s:cr()
- let n = bufnr('%')
- if s:oninfoline()
- return ''
- elseif s:oninputline() && b:findmode == 'regexp' && s:inputchanged()
- call s:textchanged(s:return)
- return ''
- elseif s:oninputline() && !s:oneitem()
- return ''
- elseif mode() == 'i' && s:onitems()
- return "\<CR>"
- endif
- stopinsert
- call s:open()
- return ''
- endfun
- fun! s:updateuiinfo()
- if line('$') == 1
- call append(1, '')
- endif
- sign unplace 2
- exe 'sign place 2 line=' . s:inputlnum . ' name=find buffer=' . bufnr('%')
- call setline(s:infolnum, '[' . s:modesym[b:findmodeidx] . '] ' .
- \ b:findinfo . ': ' . b:findnfilt)
- if s:inputchanged()
- call setline(s:inputlnum, b:findinput)
- endif
- " Clear any messages.
- echo ''
- exe s:inputlnum
- endfun
- fun! s:updateuiitems()
- let matches = b:findfilt[0:99]
- if b:findnfilt > 100
- call add(matches, '...')
- endif
- noau silent! exe s:item1lnum . ',$d'
- noau call append(s:inputlnum, matches)
- endfun
- fun! s:textchanged(when)
- " No change since we started, but the autocmd can fire anyway during
- " initialisation.
- if b:changedtick == b:findchangedtick
- return
- endif
- " Somebody trashed the buffer.
- if line('$') == 1
- call s:initbufvars(b:findcount, b:finddir, b:finditems)
- call s:reset()
- call s:updateuiinfo()
- return
- endif
- " Somebody trashed the info. line.
- if s:oninfoline()
- call s:updateuiitems()
- call s:updateuiinfo()
- return
- endif
- " Somebody trashed the input line.
- " Like, really trashed it.
- if s:oninputline() &&
- \ b:findnfilt < b:findnitems &&
- \ get(b:findfilt, 0, '') != getline(s:item1lnum)
- call s:updateuiitems()
- call s:updateuiinfo()
- return
- endif
- " Somebody edited the list of matches.
- if !s:oninputline()
- return
- endif
- let input = getline(s:inputlnum)
- let p = getpos('.')
- " Well done, Vimmer! You managed to change the input line.
- " Wait. A regexp? So impatient!
- " You'll have to wait until you've hit <CR>.
- if b:findmode == 'regexp' && a:when == s:typing && !s:emptyinput()
- return
- endif
- " Oh, it's empty. How disappointing.
- if s:emptyinput()
- call s:reset()
- silent! exe s:item1lnum . ',$d'
- call s:updateuiinfo()
- call setpos('.', p)
- return
- endif
- " Early returns out of the way; let's do this.
- echo 'Searching ...'
- if b:findmode == 'regexp'
- let b:findfilt = copy(b:finditems)
- if input =~# '\u'
- call filter(b:findfilt, {i, item -> item =~# input})
- else
- call filter(b:findfilt, {i, item -> item =~? input})
- endif
- if s:hiregexp
- call clearmatches()
- if !empty(b:findfilt)
- call matchadd('search', input, -1)
- endif
- endif
- elseif b:findmode == 'advanced'
- " If the search type hasn't changed, and input is a subset of
- " b:findinput, filter the previous filtered list, rather than
- " starting from scratch.
- " XXX: not good enough now that ! terms can result in widening
- " the search. We need to keep track of the b:findfilt list for
- " the previous set of terms and use that as the basis of the
- " filtering operation when appending to a ! term.
- let items = a:when != s:modechange &&
- \ len(s:filterord(b:findinput, [input]))
- \ ? b:findfilt
- \ : b:finditems
- let b:findfilt = s:filterunord(input, items)
- elseif b:findmode == 'basic'
- " Yes, I know I'm repeating the code above.
- " TODO: remove this comment when you've fixed the XXX.
- let items = a:when != s:modechange &&
- \ len(s:filterord(b:findinput, [input]))
- \ ? b:findfilt
- \ : b:finditems
- let b:findfilt = s:filterord(input, items)
- else
- s:err('Invalid search mode.')
- endif
- let b:findnfilt = len(b:findfilt)
- " Display results.
- let b:findinput = input
- call s:updateuiinfo()
- call s:updateuiitems()
- " Clear the 'Searching ...' message, in case edits were done in
- " Normal mode, or 'showmode' isn't set (that will clear the message
- " otherwise).
- echo ''
- call setpos('.', p)
- redraw
- silent call s:preview()
- endfun
- fun! s:preview()
- if !s:canpreview
- return
- endif
- if !has('quickfix')
- return
- endif
- if s:oninputline() && !s:oneitem() || s:oninfoline()
- hide pclose
- return
- endif
- let file = s:oneitem()
- \ ? getline('$')
- \ : getline('.')
- " Don't attempt to preview remote FS; too slow over VPN.
- if file[0:1] == '//'
- return
- endif
- if b:finddidpedit || winheight(0) > (&previewheight+1+3)
- let b:finddidpedit = 1
- let view = winsaveview()
- if filereadable(file)
- " noau to avoid triggering cursormoved, and then another
- " call to s:preview() and a big mess.
- noau silent! exe 'pedit ' . fnameescape(file)
- else
- noau exe 'pedit ' . s:unavailprv
- endif
- call winrestview(view)
- endif
- endfun
- fun! s:reset()
- hide pclose
- let b:finddidpedit = 0
- let b:findfilt = b:finditems
- let b:findnfilt = b:findnitems
- let b:findinput = ''
- endfun
- fun! s:initbufvars(count, dir, items)
- let b:findchangedtick = b:changedtick
- let b:finddir = a:dir
- let b:findinfo = '#' . a:count . ' ' . a:dir
- let b:finditems = a:items
- let b:findnitems = len(a:items)
- let b:findcount = a:count
- let b:findmodeidx = index(s:modes, s:defmode)
- let b:findmode = s:modes[b:findmodeidx]
- call s:reset()
- endfun
- fun! s:initwin(count, dir, items)
- sign define find linehl=specialkey texthl=specialkey text=>
- call s:initbufvars(a:count, a:dir, a:items)
- call clearmatches()
- if s:hioneitem
- call matchadd('search', '\%' . s:item1lnum . 'l.*\%$', -1)
- endif
- let &l:stl = '> ' . b:findinfo . '%= %l,%v %P%<'
- setl noswf bt=nofile nobl cul noudf scl=yes bh=wipe
- set cpo-=v cpo-=$
- au! textchanged <buffer> call s:textchanged(s:typing)
- au! textchangedi <buffer> call s:textchanged(s:typing)
- au! bufleave <buffer> let &cpo = s:cpo | hide pclose |
- \ call clearmatches()
- au! bufenter <buffer> set cpo-=$ cpo-=v
- au! cursormoved <buffer> silent call s:preview()
- ino <buffer> <silent> <CR> <C-R>=<SID>cr()<CR>
- nno <buffer> <silent> <CR> :<C-U>call <SID>cr()<CR>
- ino <buffer> <silent> <BS> <C-R>=<SID>bs()<CR>
- nno <buffer> <silent> <BS> :<C-U>call <SID>cyclemode()<CR>
- call s:updateuiinfo()
- startinsert
- endfun
- fun! s:find(count, bang, mods, dir)
- if &mod && !&hidden && a:mods !~# '\<hide\>'
- call s:err('Buffer modified. :w the buffer, :set hidden, ' .
- \ 'or use :hide Find')
- return
- endif
- exe a:mods . ' enew'
- let max = 100
- let cache = s:cacheprefix . a:count
- if a:dir != ''
- let append = a:bang == ''
- let dir = fnamemodify(a:dir, ':p')
- let out = systemlist('find ' . dir)
- silent! let cached = append ? readfile(cache) : []
- if !empty(cached)
- let dir .= ', ' . cached[0]
- let cached = cached[1:]
- endif
- let lines = [dir] + cached + out
- call writefile(lines, cache)
- endif
- let cached = a:dir == '' ? readfile(cache) : lines
- let dir = cached[0]
- let items = cached[1:]
- call s:initwin(a:count, dir, items)
- endfun
- com! -range=0 -bang -nargs=? Find
- \ call s:find(<count>, <q-bang>, <q-mods>, <q-args>)
- let &cpo = s:cpo
- finish
- REQUIREMENTS
- Vim 8.
- find(1) for generating file and directory lists.
- INVOCATION
- :[N] Find [!] [DIR]
- N Optional cache number. Default: 0.
- :[N] Find Reads the initial list of files from ~/findcacheN,
- and makes the current window a Find File window.
- Insert mode is started in the Search Input Line.
- :[N] Find! Same as :Find.
- :[N] Find DIR Uses find(1) to find files and directories
- starting at DIR. The list is appended to
- ~/findcacheN. Otherwise the same as for :Find.
- :[N] Find! DIR Uses find(1) to find files and directories
- starting at DIR. ~/findcacheN is overwritten
- with this list. Otherwise the same as :Find.
- :Find may be used with :hide
- :Find may not be followed with '|'.
- FIND FILE WINDOW
- The local status line is '> Find file (N)', where N is the cached
- list in use.
- The first line is the Search Input Line. It has a '>' marker in the
- sign column.
- The remaining lines display a maximum of a hundred found files.
- If more files are found, '...' is displayed in the last line, along
- with a total count.
- <CR> in a Find File window act as follows:
- Search Input Line, Insert mode:
- If there is more than one file found, leave Insert mode.
- If there is one file found, open it.
- Search Input Line, Normal mode:
- If there is more than one file found, do nothing.
- If there is one file found, open it.
- Other lines, Insert mode:
- <CR>
- Other lines, Normal mode:
- Open the file.
- Preview
- If there is a single found file, the preview window displays its
- contents. If there is more than one file found, move the cursor
- over a filename to display it in the preview window. If a file
- is unreadable the preview window is empty.
- The preview window will be closed when leaving the Find File
- window.
- Typical usage
- To open a file, with a mapping for :Find:
- \o
- start typing stuff
- hit <CR> when there is one match left
- SEARCHING
- Three search modes are available: regular expressions, ordered
- search terms, and unordered search terms. They all work as if
- 'ignorecase' and 'smartcase' are set.
- 1. Regular expressions
- Requires '/' as the first character of the search.
- Press <CR> to perform the search.
- 2. Ordered search terms
- This is the default. They are separated by spaces. They are
- searched for literally, and must appear in matching file paths
- in the same order as typed. E.g.
- > foo bar baz
- will match '/foofoo/aaa/barbar/bbb/bazbaz', but not
- '/foofoo/aaa/bazbaz/bbb/barbar'
- Search results update as you type.
- 3. Unordered search terms
- Requires '?' as the first character of the search. Like (2) they
- are separated by spaces, but may appear matching file paths in
- any order. E.g.
- > ?baz foo bar
- will match '/foofoo/aaa/barbar/bbb/bazbaz'.
- Search terms can be negated by prefixing with '!'. E.g.
- > ?bar !foo
- will match '/bar/baz',
- but not '/foo/bar/baz'.
- Search results update as you type.
- TASKS
- [*] The filtering optimisation for unordered terms is no longer
- correct since adding negation. See the code. It will still work
- for single character negations.
- [*] Fix crude sign unplace *. It will remove signs everywhere.
- [*] Docmentation.
- [.] A ~73k line file list, albeit larger than usual, will cause
- ~32MB memory usage (Windows)! Not a problem for me, but this
- seems excessive. The lists don't seem to get garbage-collected.
- This becomes a problem if you have multiple Find File windows
- open. This isn't normal, but is still a potential issue.
- [ ] Add required features to REQUIREMENTS.
- [ ] Exclude certain patterns from the search results? E.g. undo
- files and other junk. (Change the find(1) command.)
- [ ] Unordered search allows overlapping terms. Maybe this is
- a feature.
- [ ] Restore the preview window if it was already open. It's ot an
- issue for me personally, but might be nice. It could take some
- effort though.
- [ ] Is there a better way to display an empty preview window than
- this?
- exe 'pedit ' . s:unavailprv
- [ ] is there a way to detect an active VPN connection?
- [ ] Undo on input line should invoke s:textchanged
Add Comment
Please, Sign In to add comment