From f13853cc8dbf019652051eda1f3be2a537dac794 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 15 Aug 2015 01:26:01 +0900 Subject: [PATCH] Initial commit --- README.md | 67 +++++++++++ plugin/fzf.vim | 315 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 382 insertions(+) create mode 100644 README.md create mode 100644 plugin/fzf.vim diff --git a/README.md b/README.md new file mode 100644 index 0000000..22a8a91 --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +fzf.vim +======= + +A set of [fzf][fzf]-based Vim commands. + +Rationale +--------- + +[fzf][fzf] in itself is not a Vim plugin, and the official repository only +provides the [basic wrapper function][run] for Vim and it's up to the users to +write their own Vim commands with it. However, I've learned that many users of +fzf are not familiar with Vimscript and are looking for the "default" +implementation of the features they can find in the alternative Vim plugins. + +This repository is a bundle of fzf-based commands extracted from my +[.vimrc][vimrc] to address such needs. The commands are opinionated and not +designed to be extremely flexible or configurable, and they are not guaranteed +to be backward-compatible. + +Installation +------------ + +Using [vim-plug](https://github.com/junegunn/vim-plug): + +```vim +Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': 'yes \| ./install' } +Plug 'junegunn/fzf.vim' +``` + +List of commands +---------------- + +| Command | List | +| --- | --- | +| `Buffers` | Open buffers | +| `Colors` | Color schemes | +| `Ag [PATTERN]` | [ag][ag] search result (`CTRL-A` to select all, `CTRL-D` to deselect all) | +| `Lines` | Lines in loaded buffers | +| `Tags` | Tags in the project (`ctags -R`) | +| `BTags` | Tags in the current buffer | +| `Locate PATH` | `locate` command output | +| `History` | `v:oldfiles` and open buffers | + +- All commands except `Colors` support `CTRL-T` / `CTRL-X` / `CTRL-V` key + bindings to open in a new tab, a new split, or in a new vertical split. +- Bang-versions of the commands (e.g. `Ag!`) will open fzf in fullscreen + +Customization +------------- + +```vim +" This is the default extra key bindings +let g:fzf_action = { + \ 'ctrl-t': 'tabedit', + \ 'ctrl-x': 'split', + \ 'ctrl-v': 'vsplit' } +``` + +License +------- + +MIT + +[fzf]: https://github.com/junegunn/fzf +[run]: https://github.com/junegunn/fzf#usage-as-vim-plugin +[vimrc]: https://github.com/junegunn/dotfiles/blob/master/vimrc +[ag]: https://github.com/ggreer/the_silver_searcher diff --git a/plugin/fzf.vim b/plugin/fzf.vim new file mode 100644 index 0000000..382fad0 --- /dev/null +++ b/plugin/fzf.vim @@ -0,0 +1,315 @@ +" Copyright (c) 2015 Junegunn Choi +" +" MIT License +" +" Permission is hereby granted, free of charge, to any person obtaining +" a copy of this software and associated documentation files (the +" "Software"), to deal in the Software without restriction, including +" without limitation the rights to use, copy, modify, merge, publish, +" distribute, sublicense, and/or sell copies of the Software, and to +" permit persons to whom the Software is furnished to do so, subject to +" the following conditions: +" +" The above copyright notice and this permission notice shall be +" included in all copies or substantial portions of the Software. +" +" THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +" EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +" MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +" NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +" LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +" OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +" WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +let s:cpo_save = &cpo +set cpo&vim + +" ------------------------------------------------------------------ +" Common +" ------------------------------------------------------------------ +function! s:strip(str) + return substitute(a:str, '^\s*\|\s*$', '', 'g') +endfunction + +function! s:escape(path) + return escape(a:path, ' %#\') +endfunction + +function! s:ansi(str, col, bold) + return printf("\x1b[%s%sm%s\x1b[m", a:col, a:bold ? ';1' : '', a:str) +endfunction + +for [s:c, s:a] in items({'red': 31, 'green': 32, 'yellow': 33, 'blue': 34, 'magenta': 35}) + execute "function! s:".s:c."(str, ...)\n" + \ " return s:ansi(a:str, ".s:a.", get(a:, 1, 0))\n" + \ "endfunction" +endfor + +function! s:buflisted() + return filter(range(1, bufnr('$')), 'buflisted(v:val)') +endfunction + +function! s:fzf(opts, bang) + return fzf#run(extend(a:opts, a:bang ? {} : get(g:, 'fzf_window', {'down': '40%'}))) +endfunction + +let s:default_action = { + \ 'ctrl-t': 'tabedit', + \ 'ctrl-x': 'split', + \ 'ctrl-v': 'vsplit' } + +function! s:expect() + return ' --expect='.join(keys(get(g:, 'fzf_action', s:default_action)), ',') +endfunction + +function! s:common_sink(lines) abort + if len(a:lines) < 2 + return + endif + let key = remove(a:lines, 0) + let cmd = get(get(g:, 'fzf_action', s:default_action), key, 'e') + try + let autochdir = &autochdir + set noautochdir + for item in a:lines + execute cmd s:escape(item) + endfor + finally + let &autochdir = autochdir + endtry +endfunction + +function! s:align_lists(lists) + let maxes = {} + for list in a:lists + let i = 0 + while i < len(list) + let maxes[i] = max([get(maxes, i, 0), len(list[i])]) + let i += 1 + endwhile + endfor + for list in a:lists + call map(list, "printf('%-'.maxes[v:key].'s', v:val)") + endfor + return a:lists +endfunction + +" ------------------------------------------------------------------ +" Lines +" ------------------------------------------------------------------ +function! s:line_handler(lines) + if len(a:lines) < 2 + return + endif + let cmd = get(get(g:, 'fzf_action', s:default_action), a:lines[0], '') + if !empty(cmd) + execute 'silent' cmd + endif + + let keys = split(a:lines[1], '\t') + execute 'buffer' keys[0][1:-2] + execute keys[1][0:-2] + normal! ^zz +endfunction + +function! s:buffer_lines() + let res = [] + for b in s:buflisted() + call extend(res, + \ map(getbufline(b, 0, "$"), + \ 'printf("[%s]\t%s:\t%s", s:blue(b, 1), s:yellow(v:key + 1, 1), v:val)')) + endfor + return res +endfunction + +command! -bang Lines call s:fzf({ +\ 'source': buffer_lines(), +\ 'sink*': function('line_handler'), +\ 'options': '+m --prompt "Lines> " --ansi --extended --nth=3..'.s:expect() +\}, 0) + +" ------------------------------------------------------------------ +" Colors +" ------------------------------------------------------------------ +command! -bang Colors call s:fzf({ +\ 'source': map(split(globpath(&rtp, "colors/*.vim"), "\n"), +\ "substitute(fnamemodify(v:val, ':t'), '\\..\\{-}$', '', '')"), +\ 'sink': 'colo', +\ 'options': '+m --prompt="Colors> "' +\}, 0) + +" ------------------------------------------------------------------ +" Locate +" ------------------------------------------------------------------ +command! -bang -nargs=1 Locate call s:fzf({ +\ 'source': 'locate ', +\ 'sink*': function('common_sink'), +\ 'options': '-m --prompt "Locate> "' . s:expect() +\}, 0) + +" ------------------------------------------------------------------ +" History +" ------------------------------------------------------------------ +function! s:all_files() + return extend( + \ filter(copy(v:oldfiles), + \ "v:val !~ 'fugitive:\\|NERD_tree\\|^/tmp/\\|.git/'"), + \ map(s:buflisted(), 'bufname(v:val)')) +endfunction + +command! -bang History call s:fzf({ +\ 'source': reverse(s:all_files()), +\ 'sink*': function('common_sink'), +\ 'options': '--prompt "Hist> " -m' . s:expect(), +\}, 0) + +" ------------------------------------------------------------------ +" Buffers +" ------------------------------------------------------------------ +function! s:bufopen(lines) + if len(a:lines) < 2 + return + endif + let cmd = get(get(g:, 'fzf_action', s:default_action), a:lines[0], '') + if !empty(cmd) + execute 'silent' cmd + endif + execute 'buffer' matchstr(a:lines[1], '\[\zs[0-9]*\ze\]') +endfunction + +function! s:format_buffer(b) + let name = bufname(a:b) + let flag = a:b == bufnr('') ? s:blue('%') : + \ (a:b == bufnr('#') ? s:magenta('#') : ' ') + let modified = getbufvar(a:b, '&modified') ? s:red(' [+]') : '' + let readonly = getbufvar(a:b, '&modifiable') ? '' : s:green(' [RO]') + let extra = join(filter([modified, readonly], '!empty(v:val)'), '') + return s:strip(printf("[%s] %s\t%s\t%s", s:yellow(a:b, 1), flag, name, extra)) +endfunction + +function! s:bufselect(bang) + let bufs = map(s:buflisted(), 's:format_buffer(v:val)') + let height = min([len(bufs), &lines * 4 / 10]) + + call fzf#run(extend({ + \ 'source': reverse(bufs), + \ 'sink*': function('s:bufopen'), + \ 'options': '+m --ansi -d "\t" -n 2,1..2 --prompt="Buf> "'.s:expect(), + \}, a:bang ? {} : {'down': height + 2})) +endfunction + +command! -bang Buffers call s:bufselect(0) + +" ------------------------------------------------------------------ +" Ag +" ------------------------------------------------------------------ +function! s:ag_to_qf(line) + let parts = split(a:line, ':') + return {'filename': parts[0], 'lnum': parts[1], 'col': parts[2], + \ 'text': join(parts[3:], ':')} +endfunction + +function! s:ag_handler(lines) + if len(a:lines) < 2 + return + endif + + let cmd = get(get(g:, 'fzf_action', s:default_action), a:lines[0], 'e') + let list = map(a:lines[1:], 's:ag_to_qf(v:val)') + + let first = list[0] + execute cmd s:escape(first.filename) + execute first.lnum + execute 'normal!' first.col.'|zz' + + if len(list) > 1 + call setqflist(list) + copen + wincmd p + endif +endfunction + +command! -bang -nargs=* Ag call s:fzf({ +\ 'source': printf('ag --nogroup --column --color "%s"', +\ escape(empty() ? '^(?=.)' : , '"\')), +\ 'sink*': function('ag_handler'), +\ 'options': '--ansi --delimiter : --nth 4.. --prompt "Ag> " '. +\ '--multi --bind ctrl-a:select-all,ctrl-d:deselect-all '. +\ '--color hl:68,hl+:110'.s:expect()}, 0) + +" ------------------------------------------------------------------ +" BTags +" ------------------------------------------------------------------ +function! s:btags_source() + let lines = map(split(system(printf( + \ 'ctags -f - --sort=no --excmd=number --language-force=%s %s', + \ &filetype, expand('%:S'))), "\n"), 'split(v:val, "\t")') + if v:shell_error + throw 'failed to extract tags' + endif + return map(s:align_lists(lines), 'join(v:val, "\t")') +endfunction + +function! s:btags_sink(lines) + if len(a:lines) < 2 + return + endif + let cmd = get(get(g:, 'fzf_action', s:default_action), a:lines[0], '') + if !empty(cmd) + execute 'silent' cmd '%' + endif + execute split(a:lines[1], "\t")[2] +endfunction + +function! s:btags(bang) + try + call s:fzf({ + \ 'source': s:btags_source(), + \ 'options': '+m -d "\t" --with-nth 1,4.. -n 1 --prompt "BTags> "'.s:expect(), + \ 'sink*': function('s:btags_sink')}, a:bang) + catch + echohl WarningMsg + echom v:exception + echohl None + endtry +endfunction + +command! -bang BTags call s:btags(0) + +" ------------------------------------------------------------------ +" Tags +" ------------------------------------------------------------------ +function! s:tags_sink(lines) + if len(a:lines) < 2 + return + endif + let cmd = get(get(g:, 'fzf_action', s:default_action), a:lines[0], 'e') + let parts = split(a:lines[1], '\t\zs') + let excmd = matchstr(parts[2:], '^.*\ze;"\t') + execute 'silent' cmd s:escape(parts[1][:-2]) + let [magic, &magic] = [&magic, 0] + execute excmd + let &magic = magic +endfunction + +function! s:tags(bang) + if empty(tagfiles()) + echohl WarningMsg + echom 'Preparing tags' + echohl None + call system('ctags -R') + endif + + call s:fzf({ + \ 'source': 'cat '.join(map(tagfiles(), 'fnamemodify(v:val, ":S")')). + \ '| grep -v "^!"', + \ 'options': '+m -d "\t" --with-nth 1,4.. -n 1 --prompt "Tags> "'.s:expect(), + \ 'sink*': function('s:tags_sink')}, a:bang) +endfunction + +command! -bang Tags call s:tags(0) + +" ------------------------------------------------------------------ +let &cpo = s:cpo_save +unlet s:cpo_save +