├── LICENSE ├── README └── plugin └── neovim-fuzzy.vim /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Alexis Sellier 2016. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Neil Mitchell nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | 2 | 3 | neovim-fuzzy 4 | 5 | 6 | Fuzzy file finding for neovim, via fzy[1]. 7 | 8 | [1]: https://github.com/jhawthorn/fzy 9 | 10 | . Rationale 11 | 12 | To my knowledge, fzy delivers the best results out of all fuzzy finders, 13 | including fzf, ctrl-p, command-t and unite. This is due to the advanced scoring 14 | algorithm[2]. 15 | 16 | [2]: https://github.com/jhawthorn/fzy/blob/master/ALGORITHM.md 17 | 18 | . Requirements 19 | 20 | * neovim >= 0.1.5 21 | * fzy 22 | * rg[1] or ag[2] >= 0.33.0 23 | 24 | [1]: https://github.com/BurntSushi/ripgrep 25 | [2]: http://geoff.greer.fm/ag/ 26 | 27 | . Installation 28 | 29 | Install `fzy` via your package manager, or check https://github.com/jhawthorn/fzy 30 | for instructions. 31 | 32 | If you're using vim-plug, add this to your vimrc: 33 | 34 | Plug 'cloudhead/neovim-fuzzy' 35 | 36 | You can also copy the contents of this directory into your .vim folder. 37 | 38 | . Usage 39 | 40 | Add something like this to your vimrc: 41 | 42 | nnoremap :FuzzyOpen 43 | 44 | Then hit to open the finder. 45 | 46 | Once in the fzy finder: 47 | 48 | close fzy pane 49 | open selected file with default open command 50 | open selected file in new horizontal split 51 | open selected file in new vertical split 52 | open selected file in new tab 53 | next entry 54 | previous entry 55 | 56 | See the fzy documentation for the full list of key bindings. 57 | 58 | neovim-fuzzy-specific keybindings can be disabled with: 59 | 60 | let g:fuzzy_bindkeys = 0 61 | 62 | Set your own keybindings for opening files in splits with: 63 | 64 | autocmd FileType fuzzy tnoremap :FuzzyOpenFileInTab 65 | autocmd FileType fuzzy tnoremap :FuzzyOpenFileInSplit 66 | autocmd FileType fuzzy tnoremap :FuzzyOpenFileInVSplit 67 | 68 | When no input is given, fuzzy shows the alternate buffer (also known as '#'), 69 | followed by other open buffers, followed by all other files. 70 | 71 | Fuzzy also lets you search within files, via the :FuzzyGrep command. You can 72 | use it on its own, or pass it an expression to search. 73 | -------------------------------------------------------------------------------- /plugin/neovim-fuzzy.vim: -------------------------------------------------------------------------------- 1 | " 2 | " neovim-fuzzy 3 | " 4 | " Author: Alexis Sellier 5 | " Version: 0.2 6 | " 7 | 8 | if !exists("g:fuzzy_bindkeys") 9 | let g:fuzzy_bindkeys = 1 10 | endif 11 | 12 | if exists("g:loaded_fuzzy") || &cp || !has('nvim') 13 | finish 14 | endif 15 | let g:loaded_fuzzy = 1 16 | 17 | if !exists("g:fuzzy_bufferpos") 18 | let g:fuzzy_bufferpos = 'below' 19 | endif 20 | 21 | if !exists("g:fuzzy_opencmd") 22 | let g:fuzzy_opencmd = 'edit' 23 | endif 24 | 25 | if !exists("g:fuzzy_executable") 26 | let g:fuzzy_executable = 'fzy' 27 | endif 28 | 29 | if !exists("g:fuzzy_winheight") 30 | let g:fuzzy_winheight = 12 31 | endif 32 | 33 | if !exists("g:fuzzy_rootcmds") 34 | let g:fuzzy_rootcmds = [ 35 | \ ["git", "rev-parse", "--show-toplevel"], 36 | \ ["hg", "root"] 37 | \ ] 38 | endif 39 | 40 | if !exists("g:fuzzy_hidden") 41 | let g:fuzzy_hidden = 0 42 | endif 43 | 44 | let g:fuzzy_splitcmd_map = { 45 | \ 'current' : 'edit', 46 | \ 'vsplit' : 'vsplit', 47 | \ 'split' : 'split', 48 | \ 'tab' : 'tabe' 49 | \ } 50 | 51 | if g:fuzzy_bindkeys 52 | autocmd FileType fuzzy tnoremap :FuzzyKill 53 | autocmd FileType fuzzy tnoremap :FuzzyOpenFileInTab 54 | autocmd FileType fuzzy tnoremap :FuzzyOpenFileInSplit 55 | autocmd FileType fuzzy tnoremap :FuzzyOpenFileInVSplit 56 | endif 57 | 58 | let s:fuzzy_job_id = 0 59 | let s:fuzzy_prev_window = -1 60 | let s:fuzzy_prev_window_height = -1 61 | let s:fuzzy_bufnr = -1 62 | let s:fuzzy_source = {} 63 | let s:fuzzy_selected_opencmd = '' 64 | 65 | function! s:strip(str) 66 | return substitute(a:str, '\n*$', '', 'g') 67 | endfunction 68 | 69 | function! s:fuzzy_getroot() 70 | for cmd in g:fuzzy_rootcmds 71 | if executable(cmd[0]) 72 | let result = system(cmd) 73 | if v:shell_error == 0 74 | return s:strip(result) 75 | endif 76 | endif 77 | endfor 78 | return "." 79 | endfunction 80 | 81 | function! s:fuzzy_err_noexec() 82 | throw "Fuzzy: no search executable was found. " . 83 | \ "Please make sure either '" . s:ag.path . 84 | \ "' or '" . s:rg.path . "' are in your path" 85 | endfunction 86 | 87 | " Methods to be replaced by an actual implementation. 88 | function! s:fuzzy_source.find(...) dict 89 | call s:fuzzy_err_noexec() 90 | endfunction 91 | 92 | function! s:fuzzy_source.find_contents(...) dict 93 | call s:fuzzy_err_noexec() 94 | endfunction 95 | 96 | " 97 | " ag (the silver searcher) 98 | " 99 | let s:ag = { 'path': 'ag' } 100 | 101 | function! s:ag.find(root) dict 102 | return systemlist([ 103 | \ s:ag.path, "--silent", "--nocolor", "-g", "", "-Q" 104 | \ ] + (g:fuzzy_hidden ? ["--hidden"] : []) + (empty(a:root) ? [] : [a:root])) 105 | endfunction 106 | 107 | function! s:ag.find_contents(query) dict 108 | let query = empty(a:query) ? '^(?=.)' : a:query 109 | return systemlist(s:ag.path . (g:fuzzy_hidden ? " --hidden " : " ") . "--noheading --nogroup --nocolor -S " . shellescape(query) . " .") 110 | endfunction 111 | 112 | " 113 | " rg (ripgrep) 114 | " 115 | let s:rg = { 'path': 'rg' } 116 | 117 | function! s:rg.find(root) dict 118 | return systemlist([ 119 | \ s:rg.path, "--color", "never", "--files", "--fixed-strings" 120 | \ ] + (g:fuzzy_hidden ? ["--hidden"] : []) + (empty(a:root) ? [] : [a:root])) 121 | endfunction 122 | 123 | function! s:rg.find_contents(query) dict 124 | let query = empty(a:query) ? '.' : shellescape(a:query) 125 | return systemlist([ 126 | \ s:rg.path, "-n", "--no-heading", "--color", "never", "-S", query 127 | \ ] + (g:fuzzy_hidden ? ["--hidden"] : [])) 128 | endfunction 129 | 130 | function! s:rg.find_todo() dict 131 | return systemlist([ 132 | \ s:rg.path, "-n", "--no-heading", "--color", "never", "TODO|FIXME" 133 | \ ]) 134 | endfunction 135 | 136 | " Set the finder based on available binaries. 137 | if executable(s:rg.path) 138 | let s:fuzzy_source = s:rg 139 | elseif executable(s:ag.path) 140 | let s:fuzzy_source = s:ag 141 | endif 142 | 143 | command! -nargs=? FuzzyGrep call s:fuzzy_grep() 144 | command! -nargs=? FuzzyOpen call s:fuzzy_open() 145 | command! FuzzyOpenFileInTab call s:fuzzy_split('tab') 146 | command! FuzzyOpenFileInSplit call s:fuzzy_split('split') 147 | command! FuzzyOpenFileInVSplit call s:fuzzy_split('vsplit') 148 | command! FuzzyTodo call s:fuzzy_todo() 149 | command! FuzzyKill call s:fuzzy_kill() 150 | 151 | function! s:fuzzy_kill() 152 | echo 153 | call jobstop(s:fuzzy_job_id) 154 | endfunction 155 | 156 | function! s:fuzzy_todo() abort 157 | function! Cleanup(key, val) 158 | return substitute(substitute(a:val, "\\s\\+", " ", "g"), '\(:[0-9]\+:\).*\ze\(TODO\|FIXME\)', '\1 ', "") 159 | endfunction 160 | 161 | try 162 | let contents = map(s:fuzzy_source.find_todo(), function('Cleanup')) 163 | catch 164 | echoerr v:exception 165 | return 166 | endtry 167 | 168 | let opts = { 'lines': g:fuzzy_winheight, 'statusfmt': 'FuzzyTodo %s (%d results)', 'root': '.' } 169 | 170 | function! opts.handler(result) abort 171 | let parts = split(join(a:result), ':') 172 | let name = parts[0] 173 | let lnum = parts[1] 174 | let text = parts[2] " Not used. 175 | 176 | return { 'name': name, 'lnum': lnum } 177 | endfunction 178 | 179 | return s:fuzzy(contents, opts) 180 | endfunction 181 | 182 | function! s:fuzzy_grep(str) abort 183 | try 184 | let contents = s:fuzzy_source.find_contents(a:str) 185 | catch 186 | echoerr v:exception 187 | return 188 | endtry 189 | 190 | let opts = { 'lines': g:fuzzy_winheight, 'statusfmt': 'FuzzyGrep %s (%d results)', 'root': '.' } 191 | 192 | function! opts.handler(result) abort 193 | let parts = split(join(a:result), ':') 194 | let name = parts[0] 195 | let lnum = parts[1] 196 | let text = parts[2] " Not used. 197 | 198 | return { 'name': name, 'lnum': lnum } 199 | endfunction 200 | 201 | return s:fuzzy(contents, opts) 202 | endfunction 203 | 204 | function! s:fuzzy_open(root) abort 205 | let root = empty(a:root) ? s:fuzzy_getroot() : a:root 206 | exe 'lcd' root 207 | 208 | " Get open buffers. 209 | let bufs = filter(range(1, bufnr('$')), 210 | \ 'buflisted(v:val) && !empty(bufname(v:val))') 211 | 212 | " Filter open buffers out. 213 | let bufs = filter(range(1, bufnr('$')), 214 | \ 'bufnr("%") != v:val && bufnr("#") != v:val') 215 | 216 | " Get the full buffer name if possible. 217 | let bufs = map(bufs, 'expand(bufname(v:val))') 218 | 219 | " Add the '#' buffer at the head of the list. 220 | if bufnr('#') > 0 && bufnr('%') != bufnr('#') 221 | let altbufname = expand(bufname('#')) 222 | if !empty(altbufname) && buflisted(altbufname) 223 | call insert(bufs, altbufname) 224 | end 225 | endif 226 | 227 | " Save a list of files the find command should ignore. 228 | let ignorelist = !empty(bufname('%')) ? bufs + [expand(bufname('%'))] : bufs 229 | 230 | " Get all files, minus the open buffers. 231 | try 232 | let results = s:fuzzy_source.find([]) 233 | let files = filter(results, 'index(ignorelist, v:val) == -1') 234 | catch 235 | echoerr v:exception 236 | return 237 | finally 238 | lcd - 239 | endtry 240 | 241 | " Put it all together. 242 | let result = bufs + files 243 | 244 | let opts = { 'lines': g:fuzzy_winheight, 'statusfmt': 'FuzzyOpen %s (%d files)', 'root': root } 245 | function! opts.handler(result) 246 | return { 'name': join(a:result) } 247 | endfunction 248 | 249 | return s:fuzzy(result, opts) 250 | endfunction 251 | 252 | function! s:fuzzy(choices, opts) abort 253 | let inputs = tempname() 254 | let outputs = tempname() 255 | 256 | if !executable(g:fuzzy_executable) 257 | echoerr "Fuzzy: the executable '" . g:fuzzy_executable . "' was not found in your path" 258 | return 259 | endif 260 | 261 | " Clear the command line. 262 | echo 263 | 264 | call writefile(a:choices, inputs) 265 | 266 | let command = g:fuzzy_executable . " -l " . a:opts.lines . " > " . outputs . " < " . inputs 267 | let opts = { 'outputs': outputs, 'handler': a:opts.handler, 'root': a:opts.root } 268 | 269 | function! opts.on_exit(id, code, _event) abort 270 | " NOTE: The order of these operations is important: Doing the delete first 271 | " would leave an empty buffer in netrw. Doing the resize first would break 272 | " the height of other splits below it. 273 | call win_gotoid(s:fuzzy_prev_window) 274 | exe 'silent' 'bdelete!' s:fuzzy_bufnr 275 | exe 'resize' s:fuzzy_prev_window_height 276 | 277 | if a:code != 0 || !filereadable(self.outputs) 278 | return 279 | endif 280 | 281 | let results = readfile(self.outputs) 282 | if !empty(results) 283 | for result in results 284 | let file = self.handler([result]) 285 | exe 'lcd' self.root 286 | 287 | if s:fuzzy_selected_opencmd == '' 288 | let s:fuzzy_selected_opencmd = g:fuzzy_opencmd 289 | endif 290 | 291 | silent execute s:fuzzy_selected_opencmd . ' ' . fnameescape(expand(file.name)) 292 | 293 | lcd - 294 | if has_key(file, 'lnum') 295 | silent execute file.lnum 296 | normal! zz 297 | endif 298 | endfor 299 | endif 300 | endfunction 301 | 302 | let s:fuzzy_prev_window = win_getid() 303 | let s:fuzzy_prev_window_height = winheight('%') 304 | 305 | if bufnr(s:fuzzy_bufnr) > 0 306 | exe 'keepalt' g:fuzzy_bufferpos a:opts.lines . 'sp' bufname(s:fuzzy_bufnr) 307 | else 308 | exe 'keepalt' g:fuzzy_bufferpos a:opts.lines . 'new' 309 | let s:fuzzy_selected_opencmd = "" 310 | let s:fuzzy_job_id = termopen(command, opts) 311 | let b:fuzzy_status = printf( 312 | \ a:opts.statusfmt, 313 | \ fnamemodify(opts.root, ':~:.'), 314 | \ len(a:choices)) 315 | setlocal statusline=%{b:fuzzy_status} 316 | set norelativenumber 317 | set nonumber 318 | set nospell 319 | endif 320 | let s:fuzzy_bufnr = bufnr('%') 321 | set filetype=fuzzy 322 | startinsert 323 | endfunction 324 | 325 | function! s:fuzzy_split(split) 326 | let cmd = get(g:fuzzy_splitcmd_map, a:split, '') 327 | if cmd != '' 328 | let s:fuzzy_selected_opencmd = cmd 329 | if exists('*chansend') 330 | call chansend(s:fuzzy_job_id, "\r\n") 331 | else 332 | call jobsend(s:fuzzy_job_id, "\r\n") 333 | endif 334 | endif 335 | endfunction 336 | --------------------------------------------------------------------------------