Newer
Older
" Copyright (c) 2017 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.
if exists('g:loaded_fzf')
finish
endif
let g:loaded_fzf = 1
let s:is_win = has('win32') || has('win64')
if s:is_win && &shellslash
set noshellslash
let s:base_dir = expand('<sfile>:h:h')
set shellslash
else
let s:base_dir = expand('<sfile>:h:h')
endif
if s:is_win
function! s:fzf_call(fn, ...)
let shellslash = &shellslash
try
set noshellslash
return call(a:fn, a:000)
finally
let &shellslash = shellslash
endtry
endfunction
" Use utf-8 for fzf.vim commands
" Return array of shell commands for cmd.exe
function! s:wrap_cmds(cmds)
return ['@echo off', 'for /f "tokens=4" %%a in (''chcp'') do set origchcp=%%a', 'chcp 65001 > nul'] +
\ (type(a:cmds) == type([]) ? a:cmds : [a:cmds]) +
\ ['chcp %origchcp% > nul']
endfunction
else
function! s:fzf_call(fn, ...)
return call(a:fn, a:000)
endfunction
function! s:wrap_cmds(cmds)
return a:cmds
endfunction
Jan Edmund Lazo
committed
function! s:shellesc_cmd(arg)
let escaped = substitute(a:arg, '[&|<>()@^]', '^&', 'g')
let escaped = substitute(escaped, '%', '%%', 'g')
let escaped = substitute(escaped, '"', '\\^&', 'g')
let escaped = substitute(escaped, '\\\+\(\\^\)', '\\\\\1', 'g')
return '^"'.substitute(escaped, '[^\\]\zs\\$', '\\\\', '').'^"'
endfunction
function! fzf#shellescape(arg, ...)
let shell = get(a:000, 0, &shell)
if shell =~# 'cmd.exe$'
return s:shellesc_cmd(a:arg)
endif
return s:fzf_call('shellescape', a:arg)
endfunction
function! s:fzf_getcwd()
return s:fzf_call('getcwd')
endfunction
function! s:fzf_fnamemodify(fname, mods)
return s:fzf_call('fnamemodify', a:fname, a:mods)
endfunction
function! s:fzf_expand(fmt)
return s:fzf_call('expand', a:fmt, 1)
endfunction
function! s:fzf_tempname()
return s:fzf_call('tempname')
endfunction
let s:default_layout = { 'down': '~40%' }
let s:layout_keys = ['window', 'up', 'down', 'left', 'right']
let s:fzf_go = s:base_dir.'/bin/fzf'
let s:fzf_tmux = s:base_dir.'/bin/fzf-tmux'
let s:install = s:base_dir.'/install'
let s:installed = 0
function! s:fzf_exec()
if !exists('s:exec')
elseif executable('fzf')
let s:exec = 'fzf'
elseif s:is_win && !has('win32unix')
call s:warn('fzf executable not found.')
call s:warn('Download fzf binary for Windows from https://github.com/junegunn/fzf-bin/releases/')
call s:warn('and place it as '.s:base_dir.'\bin\fzf.exe')
throw 'fzf executable not found'
elseif !s:installed && executable(s:install) &&
\ input('fzf executable not found. Download binary? (y/n) ') =~? '^y'
redraw
echo
call s:warn('Downloading fzf binary. Please wait ...')
let s:installed = 1
call system(s:install.' --bin')
return s:fzf_exec()
redraw
throw 'fzf executable not found'
return fzf#shellescape(s:exec)
function! s:tmux_enabled()
if has('gui_running')
return 0
endif
if exists('s:tmux')
endif
let s:tmux = 0
if exists('$TMUX') && executable(s:fzf_tmux)
let output = system('tmux -V')
let s:tmux = !v:shell_error && output >= 'tmux 1.7'
endif
endfunction
let escaped_chars = '$%#''"'
if has('unix')
let escaped_chars .= ' \'
endif
return escape(a:path, escaped_chars)
" Upgrade legacy options
function! s:upgrade(dict)
let copy = copy(a:dict)
if has_key(copy, 'tmux')
let copy.down = remove(copy, 'tmux')
endif
if has_key(copy, 'tmux_height')
let copy.down = remove(copy, 'tmux_height')
endif
if has_key(copy, 'tmux_width')
let copy.right = remove(copy, 'tmux_width')
endif
return copy
endfunction
function! s:error(msg)
echohl ErrorMsg
echom a:msg
echohl None
endfunction
function! s:warn(msg)
echohl WarningMsg
echom a:msg
echohl None
endfunction
function! s:has_any(dict, keys)
for key in a:keys
if has_key(a:dict, key)
return 1
endif
endfor
return 0
endfunction
function! s:open(cmd, target)
if stridx('edit', a:cmd) == 0 && s:fzf_fnamemodify(a:target, ':p') ==# s:fzf_expand('%:p')
return
endif
execute a:cmd s:escape(a:target)
endfunction
function! s:common_sink(action, lines) abort
if len(a:lines) < 2
return
endif
let key = remove(a:lines, 0)
let cmd = get(a:action, key, 'e')
if len(a:lines) > 1
augroup fzf_swap
autocmd SwapExists * let v:swapchoice='o'
\| call s:warn('fzf: E325: swap file exists: '.s:fzf_expand('<afile>'))
let empty = empty(s:fzf_expand('%')) && line('$') == 1 && empty(getline(1)) && !&modified
let autochdir = &autochdir
set noautochdir
for item in a:lines
if empty
execute 'e' s:escape(item)
let empty = 0
else
call s:open(cmd, item)
endif
if !has('patch-8.0.0177') && !has('nvim-0.2') && exists('#BufEnter')
\ && isdirectory(item)
doautocmd BufEnter
endif
endfor
finally
let &autochdir = autochdir
silent! autocmd! fzf_swap
endtry
endfunction
let gui = has('termguicolors') && &termguicolors
let fam = gui ? 'gui' : 'cterm'
let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$'
let code = synIDattr(synIDtrans(hlID(group)), a:attr, fam)
if code =~? pat
return code
endif
endfor
return ''
endfunction
function! s:defaults()
let rules = copy(get(g:, 'fzf_colors', {}))
let colors = join(map(items(filter(map(rules, 'call("s:get_color", v:val)'), '!empty(v:val)')), 'join(v:val, ":")'), ',')
return empty(colors) ? '' : ('--color='.colors)
endfunction
function! s:validate_layout(layout)
for key in keys(a:layout)
if index(s:layout_keys, key) < 0
throw printf('Invalid entry in g:fzf_layout: %s (allowed: %s)%s',
\ key, join(s:layout_keys, ', '), key == 'options' ? '. Use $FZF_DEFAULT_OPTS.' : '')
endif
endfor
return a:layout
endfunction
function! s:evaluate_opts(options)
return type(a:options) == type([]) ?
Jan Edmund Lazo
committed
\ join(map(copy(a:options), 'fzf#shellescape(v:val)')) : a:options
" [name string,] [opts dict,] [fullscreen boolean]
function! fzf#wrap(...)
let args = ['', {}, 0]
let expects = map(copy(args), 'type(v:val)')
let tidx = 0
for arg in copy(a:000)
let tidx = index(expects, type(arg), tidx)
if tidx < 0
throw 'Invalid arguments (expected: [name string] [opts dict] [fullscreen boolean])'
endif
let args[tidx] = arg
let tidx += 1
endfor
let [name, opts, bang] = args
if len(name)
let opts.name = name
end
" Layout: g:fzf_layout (and deprecated g:fzf_height)
if bang
for key in s:layout_keys
if has_key(opts, key)
call remove(opts, key)
endif
endfor
elseif !s:has_any(opts, s:layout_keys)
if !exists('g:fzf_layout') && exists('g:fzf_height')
let opts.down = g:fzf_height
else
let opts = extend(opts, s:validate_layout(get(g:, 'fzf_layout', s:default_layout)))
let opts.options = s:defaults() .' '. s:evaluate_opts(get(opts, 'options', ''))
if len(name) && len(get(g:, 'fzf_history_dir', ''))
let dir = s:fzf_expand(g:fzf_history_dir)
if !isdirectory(dir)
call mkdir(dir, 'p')
endif
let history = fzf#shellescape(dir.'/'.name)
let opts.options = join(['--history', history, opts.options])
endif
" Action: g:fzf_action
if !s:has_any(opts, ['sink', 'sink*'])
let opts._action = get(g:, 'fzf_action', s:default_action)
let opts.options .= ' --expect='.join(keys(opts._action), ',')
function! opts.sink(lines) abort
return s:common_sink(self._action, a:lines)
endfunction
let opts['sink*'] = remove(opts, 'sink')
endif
return opts
endfunction
let useshellslash = &shellslash
set noshellslash
else
set shell=sh
endif
if has('nvim')
let running = filter(range(1, bufnr('$')), "bufname(v:val) =~# ';#FZF'")
if len(running)
call s:warn('FZF is already running (in buffer '.join(running, ', ').')!')
return []
endif
let dict = exists('a:1') ? s:upgrade(a:1) : {}
let temps = { 'result': s:fzf_tempname() }
let optstr = s:evaluate_opts(get(dict, 'options', ''))
try
let fzf_exec = s:fzf_exec()
catch
throw v:exception
endtry
if has('nvim') && !has_key(dict, 'dir')
let dict.dir = s:fzf_getcwd()
if has('win32unix') && has_key(dict, 'dir')
let dict.dir = fnamemodify(dict.dir, ':p')
endif
if !has_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND) && !s:is_win
let temps.source = s:fzf_tempname()
call writefile(s:wrap_cmds(split($FZF_DEFAULT_COMMAND, "\n")), temps.source)
let dict.source = (empty($SHELL) ? &shell : $SHELL).' '.fzf#shellescape(temps.source)
if has_key(dict, 'source')
let source = dict.source
let type = type(source)
if type == 1
let prefix = source.'|'
elseif type == 3
let temps.input = s:fzf_tempname()
Jan Edmund Lazo
committed
let prefix = (s:is_win ? 'type ' : 'cat ').fzf#shellescape(temps.input).'|'
let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0)
\ !(has('nvim') || s:is_win || has('win32unix') || s:present(dict, 'up', 'left', 'right')) &&
\ executable('tput') && filereadable('/dev/tty')
let use_term = has('nvim') && !s:is_win
let use_tmux = (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:tmux_enabled() && s:splittable(dict)
if prefer_tmux && use_tmux
let use_height = 0
let use_term = 0
endif
let height = s:calc_size(&lines, dict.down, dict)
let optstr .= ' --height='.height
let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
return s:execute_term(dict, command, temps)
endif
let lines = use_tmux ? s:execute_tmux(dict, command, temps)
call s:callback(dict, lines)
return lines
function! s:present(dict, ...)
for key in a:000
if !empty(get(a:dict, key, ''))
return 1
endif
endfor
return 0
endfunction
function! s:fzf_tmux(dict)
let size = ''
for o in ['up', 'down', 'left', 'right']
if s:present(a:dict, o)
let spec = a:dict[o]
if (o == 'up' || o == 'down') && spec[0] == '~'
let size = '-'.o[0].s:calc_size(&lines, spec, a:dict)
else
" Legacy boolean option
let size = '-'.o[0].(spec == 1 ? '' : substitute(spec, '^\~', '', ''))
endif
endfor
return printf('LINES=%d COLUMNS=%d %s %s %s --',
Jan Edmund Lazo
committed
\ &lines, &columns, fzf#shellescape(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-'))
return s:present(a:dict, 'up', 'down') && &lines > 15 ||
\ s:present(a:dict, 'left', 'right') && &columns > 40
if s:present(a:dict, 'dir')
let cwd = s:fzf_getcwd()
if get(a:dict, 'prev_dir', '') ==# cwd
return 1
endif
let a:dict.prev_dir = cwd
let a:dict.dir = s:fzf_getcwd()
augroup fzf_popd
autocmd!
autocmd WinEnter * call s:dopopd()
augroup END
function! s:dopopd()
if !exists('w:fzf_prev_dir') || exists('*haslocaldir') && !haslocaldir()
return
execute 'lcd' s:escape(w:fzf_prev_dir)
unlet w:fzf_prev_dir
function! s:xterm_launcher()
let fmt = 'xterm -T "[fzf]" -bg "\%s" -fg "\%s" -geometry %dx%d+%d+%d -e bash -ic %%s'
if has('gui_macvim')
let fmt .= '&& osascript -e "tell application \"MacVim\" to activate"'
\ synIDattr(hlID("Normal"), "bg"), synIDattr(hlID("Normal"), "fg"),
\ &columns, &lines/2, getwinposx(), getwinposy())
endfunction
let s:launcher = '%s'
else
let s:launcher = function('s:xterm_launcher')
endif
function! s:exit_handler(code, command, ...)
if a:code == 130
return 0
elseif a:code > 1
call s:error('Error running ' . a:command)
if !empty(a:000)
sleep
endif
return 0
endif
return 1
endfunction
function! s:execute(dict, command, use_height, temps) abort
silent! !clear 2> /dev/null
endif
Jan Edmund Lazo
committed
let escaped = (a:use_height || s:is_win) ? a:command : escape(substitute(a:command, '\n', '\\n', 'g'), '%#!')
let Launcher = get(a:dict, 'launcher', get(g:, 'Fzf_launcher', get(g:, 'fzf_launcher', s:launcher)))
let fmt = type(Launcher) == 2 ? call(Launcher, []) : Launcher
if has('unix')
let escaped = "'".substitute(escaped, "'", "'\"'\"'", 'g')."'"
endif
let command = printf(fmt, escaped)
Jan Edmund Lazo
committed
let command = escaped
if s:is_win
let batchfile = s:fzf_tempname().'.bat'
call writefile(s:wrap_cmds(command), batchfile)
let command = batchfile
let a:temps.batchfile = batchfile
if has('nvim')
let s:dict = a:dict
let s:temps = a:temps
let fzf = {}
function! fzf.on_exit(job_id, exit_status, event) dict
let lines = s:collect(s:temps)
call s:callback(s:dict, lines)
endfunction
let cmd = 'start /wait cmd /c '.command
call jobstart(cmd, fzf)
return []
endif
elseif has('win32unix') && $TERM !=# 'cygwin'
let shellscript = s:fzf_tempname()
call writefile([command], shellscript)
let command = 'cmd.exe /C '.fzf#shellescape('set "TERM=" & start /WAIT sh -c '.shellscript)
let a:temps.shellscript = shellscript
let stdin = has_key(a:dict, 'source') ? '' : '< /dev/tty'
call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s %s 2> /dev/tty', &lines, command, stdin))
let exit_status = v:shell_error
return s:exit_handler(exit_status, command) ? s:collect(a:temps) : []
function! s:execute_tmux(dict, command, temps) abort
let command = a:command
if s:pushd(a:dict)
" -c '#{pane_current_path}' is only available on tmux 1.9 or above
let command = join(['cd', fzf#shellescape(a:dict.dir), '&&', command])
let exit_status = v:shell_error
return s:exit_handler(exit_status, command) ? s:collect(a:temps) : []
function! s:calc_size(max, val, dict)
let val = substitute(a:val, '^\~', '', '')
if val =~ '%$'
let size = a:max * str2nr(val[:-2]) / 100
let size = min([a:max, str2nr(val)])
endif
let srcsz = -1
if type(get(a:dict, 'source', 0)) == type([])
let srcsz = len(a:dict.source)
let opts = get(a:dict, 'options', '').$FZF_DEFAULT_OPTS
let margin = stridx(opts, '--inline-info') > stridx(opts, '--no-inline-info') ? 1 : 2
let margin += stridx(opts, '--header') > stridx(opts, '--no-header')
return srcsz >= 0 ? min([srcsz + margin, size]) : size
return {'tab': tabpagenr(), 'win': winnr(), 'cnt': winnr('$'), 'tcnt': tabpagenr('$')}
function! s:split(dict)
let directions = {
\ 'up': ['topleft', 'resize', &lines],
\ 'down': ['botright', 'resize', &lines],
\ 'left': ['vertical topleft', 'vertical resize', &columns],
\ 'right': ['vertical botright', 'vertical resize', &columns] }
let ppos = s:getpos()
try
if s:present(a:dict, 'window')
elseif !s:splittable(a:dict)
else
for [dir, triple] in items(directions)
let val = get(a:dict, dir, '')
if !empty(val)
let [cmd, resz, max] = triple
if (dir == 'up' || dir == 'down') && val[0] == '~'
let sz = s:calc_size(max, val, a:dict)
else
let sz = s:calc_size(max, val, {})
endif
execute cmd sz.'new'
execute resz sz
return [ppos, {}]
endif
endfor
return [ppos, { '&l:wfw': &l:wfw, '&l:wfh': &l:wfh }]
setlocal winfixwidth winfixheight
function! s:execute_term(dict, command, temps) abort
let [ppos, winopts] = s:split(a:dict)
let b:fzf = a:dict
let fzf = { 'buf': bufnr(''), 'pbuf': pbuf, 'ppos': ppos, 'dict': a:dict, 'temps': a:temps,
\ 'winopts': winopts, 'winrest': winrest, 'lines': &lines,
\ 'columns': &columns, 'command': a:command }
function! fzf.switch_back(inplace)
if a:inplace && bufnr('') == self.buf
if bufexists(self.pbuf)
execute 'keepalt b' self.pbuf
endif
" No other listed buffer
if bufnr('') == self.buf
enew
endif
endif
endfunction
function! fzf.on_exit(id, code, _event)
if s:getpos() == self.ppos " {'window': 'enew'}
for [opt, val] in items(self.winopts)
execute 'let' opt '=' val
endfor
call self.switch_back(1)
if bufnr('') == self.buf
" We use close instead of bd! since Vim does not close the split when
" there's no other listed buffer (nvim +'set nobuflisted')
close
endif
execute 'tabnext' self.ppos.tab
execute self.ppos.win.'wincmd w'
if bufexists(self.buf)
execute 'bd!' self.buf
endif
if &lines == self.lines && &columns == self.columns && s:getpos() == self.ppos
if !s:exit_handler(a:code, self.command, 1)
return
endif
let lines = s:collect(self.temps)
call s:callback(self.dict, lines)
call self.switch_back(s:getpos() == self.ppos)
try
if s:present(a:dict, 'dir')
execute 'lcd' s:escape(a:dict.dir)
endif
call termopen(a:command . ';#FZF', fzf)
finally
if s:present(a:dict, 'dir')
lcd -
endif
endtry
setlocal nospell bufhidden=wipe nobuflisted
startinsert
return []
endfunction
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
function! s:collect(temps) abort
try
return filereadable(a:temps.result) ? readfile(a:temps.result) : []
finally
for tf in values(a:temps)
silent! call delete(tf)
endfor
endtry
endfunction
function! s:callback(dict, lines) abort
" Since anything can be done in the sink function, there is no telling that
" the change of the working directory was made by &autochdir setting.
"
" We use the following heuristic to determine whether to restore CWD:
" - Always restore the current directory when &autochdir is disabled.
" FIXME This makes it impossible to change directory from inside the sink
" function when &autochdir is not used.
" - In case of an error or an interrupt, a:lines will be empty.
" And it will be an array of a single empty string when fzf was finished
" without a match. In these cases, we presume that the change of the
" directory is not expected and should be undone.
let popd = has_key(a:dict, 'prev_dir') &&
\ (!&autochdir || (empty(a:lines) || len(a:lines) == 1 && empty(a:lines[0])))
if popd
let w:fzf_prev_dir = a:dict.prev_dir
endif
try
for line in a:lines
if type(a:dict.sink) == 2
call a:dict.sink(line)
execute a:dict.sink s:escape(line)
call a:dict['sink*'](a:lines)
catch
if stridx(v:exception, ':E325:') < 0
echoerr v:exception
endif
endtry
" We may have opened a new window or tab
if popd
let w:fzf_prev_dir = a:dict.prev_dir
call s:dopopd()
let s:default_action = {
\ 'ctrl-x': 'split',
\ 'ctrl-v': 'vsplit' }
function! s:shortpath()
let short = pathshorten(fnamemodify(getcwd(), ':~:.'))
let slash = (s:is_win && !&shellslash) ? '\' : '/'
return empty(short) ? '~'.slash : short . (short =~ slash.'$' ? '' : slash)
let opts = { 'options': ['--multi'] }
if len(args) && isdirectory(expand(args[-1]))
let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '')
if s:is_win && !&shellslash
let opts.dir = substitute(opts.dir, '/', '\\', 'g')
endif
let prompt = opts.dir
let prompt = s:shortpath()
call extend(opts.options, ['--prompt', prompt])
call extend(opts.options, args)
call fzf#run(fzf#wrap('FZF', opts, a:bang))
command! -nargs=* -complete=dir -bang FZF call s:cmd(<bang>0, <f-args>)
let &cpo = s:cpo_save
unlet s:cpo_save