Guest User

Untitled

a guest
Apr 21st, 2018
82
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 15.99 KB | None | 0 0
  1. " findfile.vim -- interactive file search
  2. " Experimental; caveat emptor.
  3. " Documentation is at the end.
  4. " Public Domain.
  5.  
  6. let s:cpo = &cpo
  7. set cpo&vim
  8.  
  9. " OPTIONS
  10. " Regexp searches are highlighted.
  11. let s:hiregexp = 1
  12. let s:defmode = 'basic'
  13. let s:canpreview = 1
  14. let s:hioneitem = 1
  15.  
  16. let s:thisfile = expand('<sfile>:t')
  17. let s:cacheprefix = expand('~/findcache')
  18. let s:unavailprv = expand('~/[not\ available]')
  19.  
  20. " Pertinent line numbers in the Find File window.
  21. let s:infolnum = 1
  22. let s:inputlnum = 2
  23. let s:item1lnum = 3
  24.  
  25. " Search modes.
  26. let s:modes = ['basic', 'advanced', 'regexp']
  27. let s:modesym = ' +/'
  28.  
  29. " s:textchanged() can be invoked on:
  30. let s:typing = 0
  31. let s:return = 1
  32. let s:modechange = 2
  33.  
  34. fun! s:err(msg)
  35. echoerr '(' . s:thisfile . ') ' . a:msg
  36. endfun
  37.  
  38. fun! s:inputchanged()
  39. return b:findinput != getline(s:inputlnum)
  40. endfun
  41.  
  42. fun! s:oneitem()
  43. return b:findnfilt == 1
  44. endfun
  45.  
  46. fun! s:oninfoline()
  47. return line('.') == s:infolnum
  48. endfun
  49.  
  50. fun! s:oninputline()
  51. return line('.') == s:inputlnum
  52. endfun
  53.  
  54. fun! s:onitems()
  55. return line('.') > s:inputlnum
  56. endfun
  57.  
  58. fun! s:emptyinput()
  59. return getline(s:inputlnum) == ''
  60. endfun
  61.  
  62. fun! s:cyclemode()
  63. let b:findmodeidx = (b:findmodeidx + 1) % len(s:modes)
  64. let b:findmode = s:modes[b:findmodeidx]
  65. call s:textchanged(s:modechange)
  66. redraw
  67. endfun
  68.  
  69. fun! s:bs()
  70. if s:oninputline() && s:emptyinput()
  71. call s:cyclemode()
  72. return ''
  73. else
  74. return "\<BS>"
  75. endif
  76. endfun
  77.  
  78. fun! s:filterord(input, list)
  79. let input = substitute(a:input, '^\s\+\|\s\+$', '', 'g')
  80. if input == ''
  81. return a:list
  82. endif
  83. let terms = '\%#=2\V' .
  84. \ substitute(escape(input, '\'), '\s\+', '\\.\\*', 'g')
  85. return input =~# '\u'
  86. \ ? filter(copy(a:list), {i, item -> item =~# terms})
  87. \ : filter(copy(a:list), {i, item -> item =~? terms})
  88. endfun
  89.  
  90. fun! s:filterunord(input, list)
  91. let input = substitute(a:input, '^\s\+\|\s\+$', '', 'g')
  92. if input == ''
  93. return a:list
  94. endif
  95. let terms = split(input, ' ')
  96. call filter(terms, {i, term -> term != '!'})
  97. let b:terms = terms
  98. if empty(terms)
  99. return a:list
  100. endif
  101. call map(terms, {i, term -> escape(term, '\')})
  102. let exact = input =~# '\u'
  103. let list = terms[0][0] == '!'
  104. \ ? exact
  105. \ ? filter(copy(a:list), {i, item -> item !~# '\V' . terms[0][1:]})
  106. \ : filter(copy(a:list), {i, item -> item !~? '\V' . terms[0][1:]})
  107. \ : exact
  108. \ ? filter(copy(a:list), {i, item -> item =~# '\V' . terms[0]})
  109. \ : filter(copy(a:list), {i, item -> item =~? '\V' . terms[0]})
  110. if len(terms) > 1
  111. let filt = []
  112. for item in list
  113. for term in terms[1:]
  114. let match = term[0] == '!'
  115. \ ? exact
  116. \ ? item !~# '\V' . term[1:]
  117. \ : item !~? '\V' . term[1:]
  118. \ : exact
  119. \ ? item =~# '\V' . term
  120. \ : item =~? '\V' . term
  121. if !match
  122. break
  123. endif
  124. endfor
  125. if match
  126. call add(filt, item)
  127. endif
  128. endfor
  129. else
  130. let filt = list
  131. endif
  132. return filt
  133. endfun
  134.  
  135. fun! s:open()
  136. let file = s:oninputline()
  137. \ ? getline('$')
  138. \ : getline('.')
  139. if isdirectory(file) || filereadable(file)
  140. hide pclose
  141. if bufexists(file)
  142. " Don't mess about if the buffer is already
  143. " open. You could be on a slow network.
  144. exe 'b ' . fnameescape(file)
  145. doau bufenter
  146. else
  147. exe 'e ' . fnameescape(file)
  148. doau bufenter,bufread
  149. endif
  150. endif
  151. endfun
  152.  
  153. " <CR>'s behaviour.
  154. fun! s:cr()
  155. let n = bufnr('%')
  156. if s:oninfoline()
  157. return ''
  158. elseif s:oninputline() && b:findmode == 'regexp' && s:inputchanged()
  159. call s:textchanged(s:return)
  160. return ''
  161. elseif s:oninputline() && !s:oneitem()
  162. return ''
  163. elseif mode() == 'i' && s:onitems()
  164. return "\<CR>"
  165. endif
  166. stopinsert
  167. call s:open()
  168. return ''
  169. endfun
  170.  
  171. fun! s:updateuiinfo()
  172. if line('$') == 1
  173. call append(1, '')
  174. endif
  175. sign unplace 2
  176. exe 'sign place 2 line=' . s:inputlnum . ' name=find buffer=' . bufnr('%')
  177. call setline(s:infolnum, '[' . s:modesym[b:findmodeidx] . '] ' .
  178. \ b:findinfo . ': ' . b:findnfilt)
  179. if s:inputchanged()
  180. call setline(s:inputlnum, b:findinput)
  181. endif
  182. " Clear any messages.
  183. echo ''
  184. exe s:inputlnum
  185. endfun
  186.  
  187. fun! s:updateuiitems()
  188. let matches = b:findfilt[0:99]
  189. if b:findnfilt > 100
  190. call add(matches, '...')
  191. endif
  192. noau silent! exe s:item1lnum . ',$d'
  193. noau call append(s:inputlnum, matches)
  194. endfun
  195.  
  196. fun! s:textchanged(when)
  197.  
  198. " No change since we started, but the autocmd can fire anyway during
  199. " initialisation.
  200. if b:changedtick == b:findchangedtick
  201. return
  202. endif
  203.  
  204. " Somebody trashed the buffer.
  205. if line('$') == 1
  206. call s:initbufvars(b:findcount, b:finddir, b:finditems)
  207. call s:reset()
  208. call s:updateuiinfo()
  209. return
  210. endif
  211.  
  212. " Somebody trashed the info. line.
  213. if s:oninfoline()
  214. call s:updateuiitems()
  215. call s:updateuiinfo()
  216. return
  217. endif
  218.  
  219. " Somebody trashed the input line.
  220. " Like, really trashed it.
  221. if s:oninputline() &&
  222. \ b:findnfilt < b:findnitems &&
  223. \ get(b:findfilt, 0, '') != getline(s:item1lnum)
  224. call s:updateuiitems()
  225. call s:updateuiinfo()
  226. return
  227. endif
  228.  
  229. " Somebody edited the list of matches.
  230. if !s:oninputline()
  231. return
  232. endif
  233.  
  234. let input = getline(s:inputlnum)
  235. let p = getpos('.')
  236.  
  237. " Well done, Vimmer! You managed to change the input line.
  238.  
  239. " Wait. A regexp? So impatient!
  240. " You'll have to wait until you've hit <CR>.
  241. if b:findmode == 'regexp' && a:when == s:typing && !s:emptyinput()
  242. return
  243. endif
  244.  
  245. " Oh, it's empty. How disappointing.
  246. if s:emptyinput()
  247. call s:reset()
  248. silent! exe s:item1lnum . ',$d'
  249. call s:updateuiinfo()
  250. call setpos('.', p)
  251. return
  252. endif
  253.  
  254. " Early returns out of the way; let's do this.
  255. echo 'Searching ...'
  256.  
  257. if b:findmode == 'regexp'
  258. let b:findfilt = copy(b:finditems)
  259. if input =~# '\u'
  260. call filter(b:findfilt, {i, item -> item =~# input})
  261. else
  262. call filter(b:findfilt, {i, item -> item =~? input})
  263. endif
  264. if s:hiregexp
  265. call clearmatches()
  266. if !empty(b:findfilt)
  267. call matchadd('search', input, -1)
  268. endif
  269. endif
  270.  
  271. elseif b:findmode == 'advanced'
  272.  
  273. " If the search type hasn't changed, and input is a subset of
  274. " b:findinput, filter the previous filtered list, rather than
  275. " starting from scratch.
  276. " XXX: not good enough now that ! terms can result in widening
  277. " the search. We need to keep track of the b:findfilt list for
  278. " the previous set of terms and use that as the basis of the
  279. " filtering operation when appending to a ! term.
  280. let items = a:when != s:modechange &&
  281. \ len(s:filterord(b:findinput, [input]))
  282. \ ? b:findfilt
  283. \ : b:finditems
  284. let b:findfilt = s:filterunord(input, items)
  285.  
  286. elseif b:findmode == 'basic'
  287. " Yes, I know I'm repeating the code above.
  288. " TODO: remove this comment when you've fixed the XXX.
  289. let items = a:when != s:modechange &&
  290. \ len(s:filterord(b:findinput, [input]))
  291. \ ? b:findfilt
  292. \ : b:finditems
  293. let b:findfilt = s:filterord(input, items)
  294. else
  295. s:err('Invalid search mode.')
  296. endif
  297.  
  298. let b:findnfilt = len(b:findfilt)
  299. " Display results.
  300. let b:findinput = input
  301. call s:updateuiinfo()
  302. call s:updateuiitems()
  303.  
  304. " Clear the 'Searching ...' message, in case edits were done in
  305. " Normal mode, or 'showmode' isn't set (that will clear the message
  306. " otherwise).
  307. echo ''
  308. call setpos('.', p)
  309. redraw
  310. silent call s:preview()
  311. endfun
  312.  
  313. fun! s:preview()
  314. if !s:canpreview
  315. return
  316. endif
  317. if !has('quickfix')
  318. return
  319. endif
  320. if s:oninputline() && !s:oneitem() || s:oninfoline()
  321. hide pclose
  322. return
  323. endif
  324. let file = s:oneitem()
  325. \ ? getline('$')
  326. \ : getline('.')
  327. " Don't attempt to preview remote FS; too slow over VPN.
  328. if file[0:1] == '//'
  329. return
  330. endif
  331. if b:finddidpedit || winheight(0) > (&previewheight+1+3)
  332. let b:finddidpedit = 1
  333. let view = winsaveview()
  334. if filereadable(file)
  335. " noau to avoid triggering cursormoved, and then another
  336. " call to s:preview() and a big mess.
  337. noau silent! exe 'pedit ' . fnameescape(file)
  338. else
  339. noau exe 'pedit ' . s:unavailprv
  340. endif
  341. call winrestview(view)
  342. endif
  343. endfun
  344.  
  345. fun! s:reset()
  346. hide pclose
  347. let b:finddidpedit = 0
  348. let b:findfilt = b:finditems
  349. let b:findnfilt = b:findnitems
  350. let b:findinput = ''
  351. endfun
  352.  
  353. fun! s:initbufvars(count, dir, items)
  354. let b:findchangedtick = b:changedtick
  355. let b:finddir = a:dir
  356. let b:findinfo = '#' . a:count . ' ' . a:dir
  357. let b:finditems = a:items
  358. let b:findnitems = len(a:items)
  359. let b:findcount = a:count
  360. let b:findmodeidx = index(s:modes, s:defmode)
  361. let b:findmode = s:modes[b:findmodeidx]
  362. call s:reset()
  363. endfun
  364.  
  365. fun! s:initwin(count, dir, items)
  366. sign define find linehl=specialkey texthl=specialkey text=>
  367. call s:initbufvars(a:count, a:dir, a:items)
  368.  
  369. call clearmatches()
  370. if s:hioneitem
  371. call matchadd('search', '\%' . s:item1lnum . 'l.*\%$', -1)
  372. endif
  373.  
  374. let &l:stl = '> ' . b:findinfo . '%= %l,%v %P%<'
  375.  
  376. setl noswf bt=nofile nobl cul noudf scl=yes bh=wipe
  377. set cpo-=v cpo-=$
  378.  
  379. au! textchanged <buffer> call s:textchanged(s:typing)
  380. au! textchangedi <buffer> call s:textchanged(s:typing)
  381. au! bufleave <buffer> let &cpo = s:cpo | hide pclose |
  382. \ call clearmatches()
  383. au! bufenter <buffer> set cpo-=$ cpo-=v
  384. au! cursormoved <buffer> silent call s:preview()
  385.  
  386. ino <buffer> <silent> <CR> <C-R>=<SID>cr()<CR>
  387. nno <buffer> <silent> <CR> :<C-U>call <SID>cr()<CR>
  388. ino <buffer> <silent> <BS> <C-R>=<SID>bs()<CR>
  389. nno <buffer> <silent> <BS> :<C-U>call <SID>cyclemode()<CR>
  390.  
  391. call s:updateuiinfo()
  392. startinsert
  393. endfun
  394.  
  395. fun! s:find(count, bang, mods, dir)
  396. if &mod && !&hidden && a:mods !~# '\<hide\>'
  397. call s:err('Buffer modified. :w the buffer, :set hidden, ' .
  398. \ 'or use :hide Find')
  399. return
  400. endif
  401. exe a:mods . ' enew'
  402. let max = 100
  403. let cache = s:cacheprefix . a:count
  404. if a:dir != ''
  405. let append = a:bang == ''
  406. let dir = fnamemodify(a:dir, ':p')
  407. let out = systemlist('find ' . dir)
  408. silent! let cached = append ? readfile(cache) : []
  409. if !empty(cached)
  410. let dir .= ', ' . cached[0]
  411. let cached = cached[1:]
  412. endif
  413. let lines = [dir] + cached + out
  414. call writefile(lines, cache)
  415. endif
  416. let cached = a:dir == '' ? readfile(cache) : lines
  417. let dir = cached[0]
  418. let items = cached[1:]
  419. call s:initwin(a:count, dir, items)
  420. endfun
  421.  
  422. com! -range=0 -bang -nargs=? Find
  423. \ call s:find(<count>, <q-bang>, <q-mods>, <q-args>)
  424.  
  425. let &cpo = s:cpo
  426.  
  427. finish
  428.  
  429. REQUIREMENTS
  430.  
  431. Vim 8.
  432. find(1) for generating file and directory lists.
  433.  
  434. INVOCATION
  435.  
  436. :[N] Find [!] [DIR]
  437.  
  438. N Optional cache number. Default: 0.
  439.  
  440. :[N] Find Reads the initial list of files from ~/findcacheN,
  441. and makes the current window a Find File window.
  442. Insert mode is started in the Search Input Line.
  443.  
  444. :[N] Find! Same as :Find.
  445.  
  446. :[N] Find DIR Uses find(1) to find files and directories
  447. starting at DIR. The list is appended to
  448. ~/findcacheN. Otherwise the same as for :Find.
  449.  
  450.  
  451. :[N] Find! DIR Uses find(1) to find files and directories
  452. starting at DIR. ~/findcacheN is overwritten
  453. with this list. Otherwise the same as :Find.
  454.  
  455. :Find may be used with :hide
  456. :Find may not be followed with '|'.
  457.  
  458. FIND FILE WINDOW
  459.  
  460. The local status line is '> Find file (N)', where N is the cached
  461. list in use.
  462.  
  463. The first line is the Search Input Line. It has a '>' marker in the
  464. sign column.
  465.  
  466. The remaining lines display a maximum of a hundred found files.
  467. If more files are found, '...' is displayed in the last line, along
  468. with a total count.
  469.  
  470.  
  471. <CR> in a Find File window act as follows:
  472.  
  473. Search Input Line, Insert mode:
  474. If there is more than one file found, leave Insert mode.
  475. If there is one file found, open it.
  476.  
  477. Search Input Line, Normal mode:
  478. If there is more than one file found, do nothing.
  479. If there is one file found, open it.
  480.  
  481. Other lines, Insert mode:
  482. <CR>
  483.  
  484. Other lines, Normal mode:
  485. Open the file.
  486.  
  487. Preview
  488.  
  489. If there is a single found file, the preview window displays its
  490. contents. If there is more than one file found, move the cursor
  491. over a filename to display it in the preview window. If a file
  492. is unreadable the preview window is empty.
  493.  
  494. The preview window will be closed when leaving the Find File
  495. window.
  496.  
  497. Typical usage
  498.  
  499. To open a file, with a mapping for :Find:
  500.  
  501. \o
  502. start typing stuff
  503. hit <CR> when there is one match left
  504.  
  505. SEARCHING
  506.  
  507. Three search modes are available: regular expressions, ordered
  508. search terms, and unordered search terms. They all work as if
  509. 'ignorecase' and 'smartcase' are set.
  510.  
  511. 1. Regular expressions
  512.  
  513. Requires '/' as the first character of the search.
  514. Press <CR> to perform the search.
  515.  
  516. 2. Ordered search terms
  517.  
  518. This is the default. They are separated by spaces. They are
  519. searched for literally, and must appear in matching file paths
  520. in the same order as typed. E.g.
  521.  
  522. > foo bar baz
  523.  
  524. will match '/foofoo/aaa/barbar/bbb/bazbaz', but not
  525. '/foofoo/aaa/bazbaz/bbb/barbar'
  526.  
  527. Search results update as you type.
  528.  
  529. 3. Unordered search terms
  530.  
  531. Requires '?' as the first character of the search. Like (2) they
  532. are separated by spaces, but may appear matching file paths in
  533. any order. E.g.
  534.  
  535. > ?baz foo bar
  536.  
  537. will match '/foofoo/aaa/barbar/bbb/bazbaz'.
  538.  
  539. Search terms can be negated by prefixing with '!'. E.g.
  540.  
  541. > ?bar !foo
  542.  
  543. will match '/bar/baz',
  544. but not '/foo/bar/baz'.
  545.  
  546. Search results update as you type.
  547.  
  548. TASKS
  549.  
  550. [*] The filtering optimisation for unordered terms is no longer
  551. correct since adding negation. See the code. It will still work
  552. for single character negations.
  553. [*] Fix crude sign unplace *. It will remove signs everywhere.
  554. [*] Docmentation.
  555. [.] A ~73k line file list, albeit larger than usual, will cause
  556. ~32MB memory usage (Windows)! Not a problem for me, but this
  557. seems excessive. The lists don't seem to get garbage-collected.
  558. This becomes a problem if you have multiple Find File windows
  559. open. This isn't normal, but is still a potential issue.
  560. [ ] Add required features to REQUIREMENTS.
  561. [ ] Exclude certain patterns from the search results? E.g. undo
  562. files and other junk. (Change the find(1) command.)
  563. [ ] Unordered search allows overlapping terms. Maybe this is
  564. a feature.
  565. [ ] Restore the preview window if it was already open. It's ot an
  566. issue for me personally, but might be nice. It could take some
  567. effort though.
  568. [ ] Is there a better way to display an empty preview window than
  569. this?
  570. exe 'pedit ' . s:unavailprv
  571. [ ] is there a way to detect an active VPN connection?
  572. [ ] Undo on input line should invoke s:textchanged
Add Comment
Please, Sign In to add comment