Files
fzf.vim/autoload/fzf/vim.vim
T
Junegunn Choi b83a8e5ddd Show a buffer-local entry in the buffer the run started on
BLines, BTags and a lowercase mark carry a position but no buffer, so they
landed wherever the window had got to. The window can leave that buffer
while fzf is open, including through another run's show key, and BLines then
applied its line number to the wrong buffer, clamped to its length.

The callback carries the buffer alongside the window and returns to it
first, or does nothing if it is gone.
2026-08-26 21:25:33 +09:00

2401 lines
78 KiB
VimL
Executable File

" Copyright (c) 2023 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:conf(name, default)
let conf = get(g:, 'fzf_vim', {})
return get(conf, a:name, get(g:, 'fzf_' . a:name, a:default))
endfunction
let s:winpath = {}
function! s:winpath(path)
if has_key(s:winpath, a:path)
return s:winpath[a:path]
endif
let winpath = split(system('for %A in ("'.a:path.'") do @echo %~sA'), "\n")[0]
let s:winpath[a:path] = winpath
return winpath
endfunction
let s:warned = 0
function! s:bash()
if exists('s:bash')
return s:bash
endif
let custom_bash = s:conf('preview_bash', '')
let git_bash = 'C:\Program Files\Git\bin\bash.exe'
let scoop_git_bash = exists('$GIT_INSTALL_ROOT') ? $GIT_INSTALL_ROOT . '\bin\bash.exe' : ''
let candidates = filter(s:is_win ? [custom_bash, git_bash, scoop_git_bash, 'bash'] : [custom_bash, 'bash'], 'len(v:val)')
let found = filter(map(copy(candidates), 'exepath(v:val)'), 'len(v:val)')
if empty(found)
if !s:warned
call s:warn(printf('Preview window not supported (%s not found)', join(candidates, ', ')))
let s:warned = 1
endif
let s:bash = ''
return s:bash
endif
let s:bash = found[0]
" Make 8.3 filename via cmd.exe
if s:is_win
let s:bash = s:winpath(s:bash)
endif
return s:bash
endfunction
function! s:escape_for_bash(path)
if !s:is_win
return fzf#shellescape(a:path)
endif
if !exists('s:is_linux_like_bash')
call system(s:bash . ' -c "ls /mnt/[A-Za-z]"')
let s:is_linux_like_bash = v:shell_error == 0
endif
let path = substitute(a:path, '\', '/', 'g')
if s:is_linux_like_bash
let path = substitute(path, '^\([A-Z]\):', '/mnt/\L\1', '')
endif
return escape(path, ' ')
endfunction
let s:min_version = '0.63.0'
let s:is_win = has('win32') || has('win64')
let s:is_wsl_bash = s:is_win && (exepath('bash') =~? 'Windows[/\\]system32[/\\]bash.exe$')
let s:layout_keys = ['window', 'up', 'down', 'left', 'right']
let s:bin_dir = expand('<sfile>:p:h:h:h').'/bin/'
let s:bin = {
\ 'preview': s:bin_dir.'preview.sh',
\ 'tags': s:bin_dir.'tags.pl' }
let s:TYPE = {'bool': type(0), 'dict': type({}), 'funcref': type(function('call')), 'string': type(''), 'list': type([])}
let s:wide = 120
let s:checked = 0
function! s:check_requirements()
if s:checked
return
endif
if !exists('*fzf#run')
throw "fzf#run function not found. You also need Vim plugin from the main fzf repository (i.e. junegunn/fzf *and* junegunn/fzf.vim)"
endif
if !exists('*fzf#exec')
throw "fzf#exec function not found. You need to upgrade Vim plugin from the main fzf repository ('junegunn/fzf')"
endif
let s:checked = !empty(fzf#exec(s:min_version))
endfunction
function! s:extend_opts(dict, eopts, prepend)
if empty(a:eopts)
return
endif
if has_key(a:dict, 'options')
if type(a:dict.options) == s:TYPE.list && type(a:eopts) == s:TYPE.list
if a:prepend
let a:dict.options = extend(copy(a:eopts), a:dict.options)
else
call extend(a:dict.options, a:eopts)
endif
else
let all_opts = a:prepend ? [a:eopts, a:dict.options] : [a:dict.options, a:eopts]
let a:dict.options = join(map(all_opts, 'type(v:val) == s:TYPE.list ? join(map(copy(v:val), "fzf#shellescape(v:val)")) : v:val'))
endif
else
let a:dict.options = a:eopts
endif
endfunction
function! s:merge_opts(dict, eopts)
return s:extend_opts(a:dict, a:eopts, 0)
endfunction
function! s:prepend_opts(dict, eopts)
return s:extend_opts(a:dict, a:eopts, 1)
endfunction
" [spec to wrap], [preview window expression], [toggle-preview keys...]
function! fzf#vim#with_preview(...)
" Default spec
let spec = {}
let window = ''
let args = copy(a:000)
" Spec to wrap
if len(args) && type(args[0]) == s:TYPE.dict
let spec = copy(args[0])
call remove(args, 0)
endif
if !executable(s:bash())
return spec
endif
" Placeholder expression (TODO/TBD: undocumented)
let placeholder = get(spec, 'placeholder', '{}')
" g:fzf_preview_window
if empty(args)
let preview_args = s:conf('preview_window', ['', 'ctrl-/'])
if empty(preview_args)
let args = ['hidden']
else
" For backward-compatiblity
let args = type(preview_args) == type('') ? [preview_args] : copy(preview_args)
endif
endif
if len(args) && type(args[0]) == s:TYPE.string
if len(args[0]) && args[0] !~# '^\(up\|down\|left\|right\|hidden\)'
throw 'invalid preview window: '.args[0]
endif
let window = args[0]
call remove(args, 0)
endif
let preview = []
if len(window)
let preview += ['--preview-window', window]
endif
if s:is_win
if empty($MSWINHOME)
let $MSWINHOME = $HOME
endif
if s:is_wsl_bash && $WSLENV !~# '[:]\?MSWINHOME\(\/[^:]*\)\?\(:\|$\)'
let $WSLENV = 'MSWINHOME/u:'.$WSLENV
endif
endif
let preview_cmd = s:bash() . ' ' . s:escape_for_bash(s:bin.preview)
if len(placeholder)
let preview += ['--preview', preview_cmd.' '.placeholder]
end
if &ambiwidth ==# 'double'
let preview += ['--no-unicode']
end
if len(args)
call extend(preview, ['--bind', join(map(args, 'v:val.":toggle-preview"'), ',')])
endif
call s:merge_opts(spec, preview)
return spec
endfunction
function! s:remove_layout(opts)
for key in s:layout_keys
if has_key(a:opts, key)
call remove(a:opts, key)
endif
endfor
return a:opts
endfunction
function! s:reverse_list(opts)
let tokens = map(split($FZF_DEFAULT_OPTS, '[^a-z-]'), 'substitute(v:val, "^--", "", "")')
if index(tokens, 'reverse') < 0
return extend(['--layout=reverse-list'], a:opts)
endif
return a:opts
endfunction
" Call fzf#wrap with g:fzf_action temporarily replaced by a:actions, restoring
" it afterwards. fzf#wrap derives --expect from g:fzf_action, so this controls
" which keys are bound for a given command.
function! s:wrap_with_action(actions, ...)
let had_action = exists('g:fzf_action')
let saved_action = had_action ? g:fzf_action : 0
let g:fzf_action = a:actions
try
return call('fzf#wrap', a:000)
finally
if had_action
let g:fzf_action = saved_action
else
unlet g:fzf_action
endif
endtry
endfunction
function! s:wrap(name, opts, bang)
" fzf#wrap does not append --expect if sink or sink* is found
let opts = copy(a:opts)
" `_paste` opts in to the paste key (s:paste_key()). Sink-less commands have it
" dispatched by the base s:common_sink; custom-sink commands keep the key in
" --expect and project the selection themselves. Skip it when the current
" buffer cannot be modified, as there is nothing to paste into.
let paste = get(opts, '_paste', 0) && &modifiable
silent! call remove(opts, '_paste')
let options = ''
if has_key(opts, 'options')
let options = type(opts.options) == s:TYPE.list ? join(opts.options) : opts.options
endif
let action = get(g:, 'fzf_action', s:default_action)
if options !~ '--expect' && has_key(opts, 'sink*')
let Sink = remove(opts, 'sink*')
" A custom sink routes the pressed key through s:action_for, which only
" honors string actions. Expose only the string actions so that funcref
" actions are not bound here. When the command opts in to paste, keep the
" paste key so the sink can handle it.
let actions = filter(copy(action), 'type(v:val) == s:TYPE.string')
if paste
let actions[s:paste_key()] = ''
endif
let wrapped = s:wrap_with_action(actions, a:name, opts, a:bang)
let wrapped['sink*'] = Sink
elseif paste
" Sink-less command: let the base s:common_sink dispatch the paste funcref.
let actions = extend(copy(action), {s:paste_key(): function('fzf#vim#paste')})
let wrapped = s:wrap_with_action(actions, a:name, opts, a:bang)
else
let wrapped = fzf#wrap(a:name, opts, a:bang)
endif
return wrapped
endfunction
function! s:strip(str)
return substitute(a:str, '^\s*\|\s*$', '', 'g')
endfunction
function! s:rstrip(str)
return substitute(a:str, '\s*$', '', 'g')
endfunction
function! s:chomp(str)
return substitute(a:str, '\n*$', '', 'g')
endfunction
function! s:escape(path)
let path = fnameescape(a:path)
return s:is_win ? escape(path, '$') : path
endfunction
if v:version >= 704
function! s:function(name)
return function(a:name)
endfunction
else
function! s:function(name)
" By Ingo Karkat
return function(substitute(a:name, '^s:', matchstr(expand('<sfile>'), '<SNR>\d\+_\zefunction$'), ''))
endfunction
endif
function! s:get_color(attr, ...)
let gui = has('termguicolors') && &termguicolors
let fam = gui ? 'gui' : 'cterm'
let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$'
for group in a:000
let code = synIDattr(synIDtrans(hlID(group)), a:attr, fam)
if code =~? pat
return code
endif
endfor
return ''
endfunction
let s:ansi = {'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34, 'magenta': 35, 'cyan': 36}
function! s:csi(color, fg)
let prefix = a:fg ? '38;' : '48;'
if a:color[0] == '#'
return prefix.'2;'.join(map([a:color[1:2], a:color[3:4], a:color[5:6]], 'str2nr(v:val, 16)'), ';')
endif
return prefix.'5;'.a:color
endfunction
function! s:ansi(str, group, default, ...)
let fg = s:get_color('fg', a:group)
let bg = s:get_color('bg', a:group)
let color = (empty(fg) ? s:ansi[a:default] : s:csi(fg, 1)) .
\ (empty(bg) ? '' : ';'.s:csi(bg, 0))
return printf("\x1b[%s%sm%s\x1b[m", color, a:0 ? ';1' : '', a:str)
endfunction
for s:color_name in keys(s:ansi)
execute "function! s:".s:color_name."(str, ...)\n"
\ " return s:ansi(a:str, get(a:, 1, ''), '".s:color_name."')\n"
\ "endfunction"
endfor
" Keys in a:000[0] are already bound by this run, so they are left out of the
" defaults. Enter is fzf's accept rather than a g:fzf_action entry, so taking
" it over would otherwise list it twice, as Show and as Open
function! s:build_hint(entries, defaults, ...)
let entries = copy(a:entries)
let taken = get(a:, 1, [])
let keys = []
let actions = get(g:, 'fzf_action', s:default_action)
if a:defaults
if index(taken, 'enter') < 0 && index(taken, 'return') < 0
call add(entries, ['Enter', 'Open'])
endif
for [key, name, label] in [['ctrl-x', 'C-X', 'HSplit'], ['ctrl-v', 'C-V', 'VSplit'], ['ctrl-t', 'C-T', 'Tab']]
let Cmd = get(actions, key, '')
if index(taken, key) < 0 && type(Cmd) == s:TYPE.string && Cmd ==# s:default_action[key]
call add(entries, [name, label])
endif
endfor
endif
for entry in entries
call add(keys, s:magenta(entry[0], 'Special').' '.entry[1])
endfor
return join(keys, ' ')
endfunction
function! s:buflisted()
return filter(range(1, bufnr('$')), 'buflisted(v:val) && getbufvar(v:val, "&filetype") != "qf"')
endfunction
function! s:fzf(name, opts, extra)
call s:check_requirements()
let [extra, bang] = [{}, 0]
if len(a:extra) <= 1
let first = get(a:extra, 0, 0)
if type(first) == s:TYPE.dict
let extra = first
else
let bang = first
endif
elseif len(a:extra) == 2
let [extra, bang] = a:extra
else
throw 'invalid number of arguments'
endif
let extra = copy(extra)
let eopts = has_key(extra, 'options') ? remove(extra, 'options') : ''
let merged = extend(copy(a:opts), extra)
call s:merge_opts(merged, eopts)
" Command-level fzf options
call s:merge_opts(merged, s:conf(a:name.'_options', []))
let fifo = s:add_hints(merged, bang)
let ok = 0
try
let result = fzf#run(s:wrap(a:name, merged, bang))
let ok = 1
return result
finally
" Nothing calls 'exit' if the run never started
if !ok && !empty(fifo)
call fzf#vim#ipc#stop(fifo)
endif
endtry
endfunction
let s:default_action = {
\ 'ctrl-t': 'tab split',
\ 'ctrl-x': 'split',
\ 'ctrl-v': 'vsplit' }
" Key that pastes the selected items into the current buffer instead of opening
" them. Commands opt in by setting `_paste` on the spec (see s:wrap), and their
" sink projects each selected entry to the text to paste before calling
" fzf#vim#paste(). Configurable via g:fzf_vim.paste_key.
function! s:paste_key()
return s:conf('paste_key', 'alt-enter')
endfunction
" Key that opens the current entry in the window fzf was started from and
" leaves fzf open, labelled 'Show' in the footer to set it apart from Enter,
" which opens it and closes fzf. Commands opt in by setting `_show` on the
" spec (see s:add_hints). Configurable via g:fzf_vim.show_key, and set to an
" empty string to turn the key off.
function! s:show_key()
return s:conf('show_key', 'ctrl-o')
endfunction
" A key can be a comma separated list, which fzf binds to every key in it.
" fzf escapes 'alt-,' before splitting on the comma, so do the same. It also
" lowercases a key name before matching it, except for 'alt-X' and a bare
" character, where it takes the rune as typed, so fold the same ones here and
" the comparisons below agree with fzf
function! s:split_keys(keys)
let sep = nr2char(1)
let keys = map(split(substitute(a:keys, '\c\(alt-\),', '\1'.sep, 'g'), ','),
\ 'substitute(v:val, sep, ",", "g")')
" 'alt-X' keeps X as typed but not its prefix, so fold only the prefix there
return map(keys, 'strchars(v:val) == 1 ? v:val
\ : v:val =~? "^alt-.$" ? "alt-".v:val[4:] : tolower(v:val)')
endfunction
" 'ctrl-alt-x' -> 'C-A-X' and 'double-click' -> 'Dbl-Click', to match the
" other footer labels. Only the first key of a list is labelled, so that an
" alternative like 'double-click' does not crowd the footer with a name
" several times the width of the others
function! s:key_label(keys)
let label = substitute(get(s:split_keys(a:keys), 0, ''), '\c\<ctrl-', 'C-', 'g')
let label = substitute(label, '\c\<alt-', 'A-', 'g')
let label = substitute(label, '\c\<shift-', 'S-', 'g')
let label = substitute(label, '\c\<double-', 'Dbl-', 'g')
" A single character reads better uppercased and a name capitalized, as in
" 'C-X' and 'Enter'
return substitute(label, '\a\+',
\ '\=len(submatch(0)) == 1 ? toupper(submatch(0))
\ : toupper(submatch(0)[0]).tolower(submatch(0)[1:])', 'g')
endfunction
" Insert the given items into the current buffer. The first item is inserted
" after the cursor (so that it works even at the end of the line); a single
" space is added before it when the preceding text does not already end with a
" whitespace. Remaining items are appended on the following lines, preserving
" their leading whitespace so that copied blocks keep their indentation. When
" pasting after some text, the first item's leading whitespace is stripped.
function! fzf#vim#paste(items) abort
if empty(a:items)
return
endif
if !&modifiable
return s:warn('Cannot paste into a nomodifiable buffer')
endif
let line = getline('.')
let idx = empty(line) ? 0 : col('.')
let head = strpart(line, 0, idx)
let tail = strpart(line, idx)
let pad = (!empty(head) && head !~ '\s$') ? ' ' : ''
let first = empty(head) ? a:items[0] : substitute(a:items[0], '^\s*', '', '')
let first = pad . first
call setline('.', head . first . tail)
call cursor(line('.'), idx + strlen(first))
if len(a:items) > 1
call append(line('.'), a:items[1:])
endif
endfunction
" True when the selected lines (as passed to a sink*) request a paste.
function! s:is_paste(lines)
return get(a:lines, 0, '') ==# s:paste_key()
endfunction
" Drop the first `count` whitespace-separated columns from `line` and return
" the rest, stripped. Used to extract the line text from formatted entries
" (e.g. Marks: `mark line col text`, Changes: `buf offset line col text`).
function! s:rest_after_columns(line, count)
let rest = a:line
for _ in range(a:count)
let rest = substitute(rest, '^\s*\S\+', '', '')
endfor
return s:strip(rest)
endfunction
function! s:execute_silent(cmd)
silent keepjumps keepalt execute a:cmd
endfunction
" [key, [filename, [stay_on_edit: 0]]]
function! s:action_for(key, ...)
let Cmd = get(get(g:, 'fzf_action', s:default_action), a:key, '')
let cmd = type(Cmd) == s:TYPE.string ? Cmd : ''
" See If the command is the default action that opens the selected file in
" the current window. i.e. :edit
let edit = stridx('edit', cmd) == 0 " empty, e, ed, ..
" If no extra argument is given, we just execute the command and ignore
" errors. e.g. E471: Argument required: tab drop
if !a:0
if !edit
call setpos("''", getpos('.'))
silent! call s:execute_silent(cmd)
endif
else
" For the default edit action, we don't execute the action if the
" selected file is already opened in the current window, or we are
" instructed to stay on the current buffer.
let stay = edit && (a:0 > 1 && a:2 || fnamemodify(a:1, ':p') ==# expand('%:p'))
if !stay
call setpos("''", getpos('.'))
call s:execute_silent((len(cmd) ? cmd : 'edit').' '.s:escape(a:1))
endif
endif
endfunction
function! s:open(target)
if fnamemodify(a:target, ':p') ==# expand('%:p')
return
endif
execute 'edit' s:escape(a:target)
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
function! s:warn(message)
echohl WarningMsg
echom a:message
echohl None
return 0
endfunction
function! s:fill_quickfix(name, list)
if len(a:list) > 1
let Handler = s:conf('listproc_'.a:name, s:conf('listproc', function('fzf#vim#listproc#quickfix')))
call call(Handler, [a:list], {})
return 1
endif
return 0
endfunction
function! fzf#vim#_uniq(list)
let visited = {}
let ret = []
for l in a:list
if !empty(l) && !has_key(visited, l)
call add(ret, l)
let visited[l] = 1
endif
endfor
return ret
endfunction
" ------------------------------------------------------------------
" Files
" ------------------------------------------------------------------
function! s:shortpath()
let short = fnamemodify(getcwd(), ':~:.')
if !has('win32unix')
let short = pathshorten(short)
endif
let slash = (s:is_win && !&shellslash) ? '\' : '/'
return empty(short) ? '~'.slash : short . (short =~ escape(slash, '\').'$' ? '' : slash)
endfunction
" Nothing to do when the window already shows the file, and re-editing it
" would reload it and lose the cursor. 'hide' so that an unmodifiable or
" modified buffer does not turn the key into a no-op with E37 behind fzf.
function! s:edit_cmds(winid, path)
let bufnr = winbufnr(a:winid)
if bufnr > 0 && fnamemodify(bufname(bufnr), ':p') ==# fnamemodify(a:path, ':p')
return []
endif
return ['keepalt keepjumps hide edit '.s:escape(a:path)]
endfunction
" :edit raises the swap dialog before it throws, and a dialog from this
" callback is invisible behind fzf and swallows the next keys. Refuse the
" file instead and leave the real prompt to the sink.
function! s:swap_abort()
let s:swap_clash = expand('<afile>')
let v:swapchoice = 'q'
endfunction
" The buffer number starts the third tab separated field. Matching the whole
" line would find brackets in the path first, as in 'log[12].txt'
function! s:buffer_number(line)
return matchstr(get(split(a:line, "\t", 1), 2, ''), '\[\zs[0-9]*\ze\]')
endfunction
" fzf#run restores the working directory while fzf runs and only re-applies
" 'dir' for the sink, so a relative entry has to be resolved here
function! s:in_dir(dir, path) abort
" filereadable does not expand a leading tilde, which :History reports
if a:path =~# '^\~'
return fnamemodify(a:path, ':p')
endif
if empty(a:dir) || a:path =~# '^/' || a:path =~# '^\a:[\\/]'
return a:path
endif
return fnamemodify(expand(escape(a:dir, '[]*?{}')), ':p').a:path
endfunction
" Shows the entry without closing fzf. The callback can fire while the fzf
" window is current, so the entry goes to the window fzf was started from.
" Scopes the options s:goto_entry needs, which every early return has to
" restore, hence the split.
function! s:show_entry(winid, bufnr, dir, kind, entry) abort
if empty(a:entry)
return
endif
" 'acd' would move the cwd on the first open, and s:ag_to_qf resolves a
" relative match against it. Tag addresses are searched with the settings
" s:tags_sink uses.
let [magic, wrapscan, acd] = [&magic, &wrapscan, &acd]
let &acd = 0
if a:kind ==# 'tags' || a:kind ==# 'btags'
let [&magic, &wrapscan] = [0, 1]
endif
try
call s:goto_entry(a:winid, a:bufnr, a:dir, a:kind, a:entry)
finally
let [&magic, &wrapscan, &acd] = [magic, wrapscan, acd]
endtry
endfunction
" Parses the entry the way that command's sink does and moves the window to
" it. Runs with the options s:show_entry set.
function! s:goto_entry(winid, bufnr, dir, kind, entry) abort
let entry = a:entry
if a:kind ==# 'gitstatus'
" ' M path', '?? path', or a rename reported as 'old -> new'
let entry = substitute(entry[3:], '.* -> ', '', '')
endif
if a:kind ==# 'buffer'
let bufnr = s:buffer_number(entry)
if empty(bufnr)
return
endif
let cmds = ['keepalt keepjumps hide buffer '.bufnr]
elseif a:kind ==# 'lines'
" bufnr, name, line number, text
let chunks = split(entry, "\t", 1)
if len(chunks) < 3 || !bufexists(str2nr(chunks[0]))
return
endif
let cmds = ['keepalt keepjumps hide buffer '.str2nr(chunks[0]),
\ 'keepjumps '.str2nr(chunks[2]), 'normal! ^zvzz']
elseif a:kind ==# 'blines'
" line number, text, in the buffer the run started from, which the window
" can have left while fzf was open
let lnum = str2nr(split(entry, "\t", 1)[0])
if lnum <= 0 || !bufexists(a:bufnr)
return
endif
let cmds = ['keepalt keepjumps hide buffer '.a:bufnr,
\ 'keepjumps '.lnum, 'normal! ^zvzz']
elseif a:kind ==# 'tags'
let parts = split(entry, '\t\zs')
if len(parts) < 3
return
endif
let excmd = matchstr(join(parts[2:-2], '')[:-2], '^.\{-}\ze;\?"\t')
let relpath = parts[1][:-2]
let path = relpath =~ (s:is_win ? '^[A-Z]:\' : '^/')
\ ? relpath : join([fnamemodify(parts[-1], ':h'), relpath], '/')
let path = expand(path, 1)
if empty(excmd) || !filereadable(path)
return
endif
let cmds = ['keepalt keepjumps hide edit '.s:escape(path),
\ 'keepjumps '.excmd, 'normal! ^zvzz']
elseif a:kind ==# 'btags'
let parts = split(entry, "\t")
if len(parts) < 3 || !bufexists(a:bufnr)
return
endif
let cmds = ['keepalt keepjumps hide buffer '.a:bufnr,
\ 'keepjumps '.parts[2], 'normal! zvzz']
elseif a:kind ==# 'marks'
" A lowercase mark is local to the buffer the run started on
let mark = matchstr(entry, '^\s*\zs\S')
if empty(mark) || !bufexists(a:bufnr)
return
endif
let cmds = ['keepalt keepjumps hide buffer '.a:bufnr,
\ 'keepalt keepjumps hide normal! `'.mark.'zvzz']
elseif a:kind ==# 'changes'
" buffer, offset, line, column, text
let parts = split(entry)
if len(parts) < 4 || !bufexists(str2nr(parts[0]))
return
endif
" The offset forms are relative to the current position, so use the
" recorded position instead and stay idempotent across presses
let cmds = ['keepalt keepjumps hide buffer '.str2nr(parts[0]),
\ printf('keepjumps call cursor(%d, %d)', str2nr(parts[2]), str2nr(parts[3])),
\ 'normal! zvzz']
elseif a:kind ==# 'grep'
" Vim sends one line per callback, so multi-line entries arrive in pieces
try
let entry = s:ag_to_qf(entry)
catch
return
endtry
" A continuation line can still parse, e.g. 'timeout: 30' yields a line
" number and a filename that does not exist
if empty(get(entry, 'lnum', '')) || !filereadable(s:in_dir(a:dir, get(entry, 'filename', '')))
return
endif
let cmds = s:edit_cmds(a:winid, s:in_dir(a:dir, entry.filename))
\ + ['keepjumps '.entry.lnum]
if has_key(entry, 'col')
call add(cmds, printf('call cursor(0, %d)', entry.col))
endif
call add(cmds, 'normal! zvzz')
else
let path = s:in_dir(a:dir, entry)
if !filereadable(path)
return
endif
let cmds = s:edit_cmds(a:winid, path)
endif
" fzf#run opens its own tabpage for a layout it cannot split, and s:can_show
" cannot tell in advance without copying that decision out of the plugin.
" Say so rather than doing nothing. win_id2tabwin() reports 0 for a closed
" window as well as for one on another tabpage
let [tab, win] = win_id2tabwin(a:winid)
if !win
call s:warn('The window fzf started from is gone')
return
endif
if tab != tabpagenr()
call s:warn('Cannot show the entry from another tab page')
return
endif
let s:swap_clash = ''
augroup fzf_vim_peek
autocmd!
autocmd SwapExists * call s:swap_abort()
augroup END
try
call win_execute(a:winid, cmds)
redraw
catch
" s:callback ignores E325 for the same reason, and redraw would wipe this
if stridx(v:exception, ':E325:') < 0
call s:warn(v:exception)
endif
finally
autocmd! fzf_vim_peek
endtry
if !empty(s:swap_clash)
call s:warn('Swap file exists: '.fnamemodify(s:swap_clash, ':t'))
endif
endfunction
" Whether the show key can be offered for this run. Decided from the merged
" spec, since the layout can arrive with the per-call spec, and a bang puts
" fzf on its own tabpage where the window we would open in is not visible.
function! s:can_show(spec, bang, key, claimed) abort
" Older fzf blocks Vim in popup and fullscreen modes, so the callback would
" only run after fzf exits, overriding whatever was selected. fzf#run is
" only asynchronous when it has a terminal to put fzf in, and win32unix
" without winpty falls back to a blocking run. Windows has no printf.
if get(g:, 'loaded_fzf', 0) < 20260821 || !exists('*win_execute') || a:bang
\ || !(has('nvim-0.2.1') || (has('terminal') && has('patch-8.0.995')))
\ || has('win32unix') || s:is_win
return 0
endif
" Checked here so that ipc#start cannot warn on every invocation
if !executable('mkfifo') || (!exists('*job_start') && !exists('*jobstart'))
return 0
endif
" fzf matches --expect before the key bindings, so an action or the paste
" key would win over ours, and a key the command binds itself would win by
" coming later in the options. Any key of the list losing disables all of
" them, so that the footer cannot advertise a key that does nothing
let actions = get(g:, 'fzf_action', s:default_action)
let paste = s:split_keys(s:paste_key())
for key in s:split_keys(a:key)
if has_key(actions, key) || index(paste, key) >= 0 || index(a:claimed, key) >= 0
return 0
endif
endfor
" 'enew' leaves no other window, and a tab layout puts fzf on a tabpage of
" its own, where the window we would open in is not visible
" s:layout_keys here is the subset this script strips, so spell out the full
" set; a spec with 'tmux' must not fall back to g:fzf_layout
let keys = ['window', 'popup', 'tmux', 'up', 'down', 'left', 'right']
let layout = empty(filter(keys, 'has_key(a:spec, v:val)'))
\ ? get(g:, 'fzf_layout', {}) : a:spec
if (has_key(layout, 'tmux') || has_key(layout, 'popup'))
\ && !exists('$TMUX') && !exists('$ZELLIJ')
return 0
endif
let window = get(layout, 'window', '')
return type(window) != s:TYPE.string
\ || window !~# '\<enew\>\|tabnew\|tabedit\|\<tab\>'
endfunction
" Installs the show key binding and the key hint footer. '_show' names the entry
" kind, '_hint' opts in to the footer and carries the command's own keys as
" [label, description] or [label, description, keys]. The fzf key names in
" 'keys' are what the show key is checked against, since the label cannot be.
" Returns the fifo of the ipc channel it started, so the caller can stop it if
" the run never reaches 'exit'
function! s:add_hints(spec, bang) abort
let started = ''
let taken = []
let kind = s:pluck(a:spec, '_show', '')
let entries = s:pluck(a:spec, '_hint', 0)
let claimed = []
if type(entries) == s:TYPE.list
for entry in entries
for name in get(entry, 2, [])
call extend(claimed, s:split_keys(name))
endfor
endfor
endif
let key = s:show_key()
if !empty(kind) && !empty(key) && s:can_show(a:spec, a:bang, key, claimed)
let dir = get(a:spec, 'dir', '')
let fifo = fzf#vim#ipc#start(function('s:show_entry',
\ [win_getid(), bufnr(''), empty(dir) ? getcwd() : dir, kind]))
if !empty(fifo)
" Wrap an 'exit' the spec already carries. call() drops the dict of a
" dict function, so the wrapped ones are partials
let Exit = get(a:spec, 'exit', 0)
let a:spec.exit = { code -> [fzf#vim#ipc#stop(fifo),
\ type(Exit) == s:TYPE.funcref ? call(Exit, [code]) : 0] }
call s:prepend_opts(a:spec, ['--bind', key.':execute-silent:printf ''%s\n'' {} > '.fzf#shellescape(fifo)])
let entries = [[s:key_label(key), 'Show']] + (type(entries) == s:TYPE.list ? entries : [])
let started = fifo
let taken = s:split_keys(key)
endif
endif
if type(entries) == s:TYPE.list
call s:prepend_opts(a:spec, ['--footer', s:build_hint(entries, 1, taken)])
endif
return started
endfunction
function! fzf#vim#files(dir, ...)
let args = {}
if !empty(a:dir)
if !isdirectory(expand(a:dir))
return s:warn('Invalid directory')
endif
let slash = (s:is_win && !&shellslash) ? '\\' : '/'
let dir = substitute(a:dir, '[/\\]*$', slash, '')
let args.dir = dir
else
let dir = s:shortpath()
endif
let args.options = ['--scheme', 'path', '-m', '--prompt', strwidth(dir) < &columns / 2 - 20 ? dir : '> ']
let args._paste = 1
let args._show = 'file'
let args._hint = []
call s:merge_opts(args, s:conf('files_options', []))
return s:fzf('files', args, a:000)
endfunction
" ------------------------------------------------------------------
" Lines
" ------------------------------------------------------------------
function! s:line_handler(lines)
if len(a:lines) < 2
return
endif
let qfl = []
for line in a:lines[1:]
let chunks = split(line, "\t", 1)
call add(qfl, {'bufnr': str2nr(chunks[0]), 'lnum': str2nr(chunks[2]), 'text': join(chunks[3:], "\t")})
endfor
if s:is_paste(a:lines)
return fzf#vim#paste(map(copy(qfl), 's:rstrip(v:val.text)'))
endif
call s:action_for(a:lines[0])
if !s:fill_quickfix('lines', qfl)
let chunks = split(a:lines[1], '\t')
execute 'buffer' chunks[0]
execute chunks[2]
endif
normal! ^zvzz
endfunction
function! fzf#vim#_lines(all)
let cur = []
let rest = []
let buf = bufnr('')
let longest_name = 0
let display_bufnames = &columns > s:wide
if display_bufnames
let bufnames = {}
for b in s:buflisted()
let bufnames[b] = pathshorten(fnamemodify(bufname(b), ":~:."))
let longest_name = max([longest_name, len(bufnames[b])])
endfor
endif
let len_bufnames = min([15, longest_name])
for b in s:buflisted()
let lines = getbufline(b, 1, "$")
if empty(lines)
let path = fnamemodify(bufname(b), ':p')
let lines = filereadable(path) ? readfile(path) : []
endif
if display_bufnames
let bufname = bufnames[b]
if len(bufname) > len_bufnames + 1
let bufname = '…' . bufname[-len_bufnames+1:]
endif
let bufname = printf(s:green("%".len_bufnames."s", "Directory"), bufname)
else
let bufname = ''
endif
let linefmt = s:blue("%2d\t", "TabLine")."%s".s:yellow("\t%4d ", "LineNr")."\t%s"
call extend(b == buf ? cur : rest,
\ filter(
\ map(lines,
\ '(!a:all && empty(v:val)) ? "" : printf(linefmt, b, bufname, v:key + 1, v:val)'),
\ 'a:all || !empty(v:val)'))
endfor
return [display_bufnames, extend(cur, rest)]
endfunction
function! fzf#vim#lines(...)
let [display_bufnames, lines] = fzf#vim#_lines(1)
let nth = display_bufnames ? 3 : 2
let [query, args] = (a:0 && type(a:1) == type('')) ?
\ [a:1, a:000[1:]] : ['', a:000]
return s:fzf('lines', {
\ 'source': lines,
\ 'sink*': s:function('s:line_handler'),
\ '_show': 'lines',
\ '_hint': [],
\ '_paste': 1,
\ 'options': s:reverse_list(['--tiebreak=index', '--prompt', 'Lines> ', '--ansi', '--extended', '--nth='.nth.'..', '--tabstop=1', '--query', query, '--multi'])
\}, args)
endfunction
" ------------------------------------------------------------------
" BLines
" ------------------------------------------------------------------
function! s:buffer_line_handler(lines)
if len(a:lines) < 2
return
endif
let qfl = []
for line in a:lines[1:]
let chunks = split(line, "\t", 1)
let ln = chunks[0]
let ltxt = join(chunks[1:], "\t")
call add(qfl, {'filename': expand('%'), 'lnum': str2nr(ln), 'text': ltxt})
endfor
if s:is_paste(a:lines)
return fzf#vim#paste(map(copy(qfl), 's:rstrip(v:val.text)'))
endif
call s:action_for(a:lines[0])
if !s:fill_quickfix('blines', qfl)
execute split(a:lines[1], '\t')[0]
endif
normal! ^zvzz
endfunction
function! s:buffer_lines(query)
let linefmt = s:yellow(" %4d ", "LineNr")."\t%s"
let fmtexpr = 'printf(linefmt, v:key + 1, v:val)'
let lines = getline(1, '$')
if empty(a:query)
return map(lines, fmtexpr)
end
return filter(map(lines, 'v:val =~ a:query ? '.fmtexpr.' : ""'), 'len(v:val)')
endfunction
function! fzf#vim#buffer_lines(...)
let [query, args] = (a:0 && type(a:1) == type('')) ?
\ [a:1, a:000[1:]] : ['', a:000]
return s:fzf('blines', {
\ 'source': s:buffer_lines(query),
\ 'sink*': s:function('s:buffer_line_handler'),
\ '_show': 'blines',
\ '_hint': [],
\ '_paste': 1,
\ 'options': s:reverse_list(['+m', '--tiebreak=index', '--multi', '--prompt', 'BLines> ', '--ansi', '--extended', '--nth=2..', '--tabstop=1'])
\}, args)
endfunction
" ------------------------------------------------------------------
" Colors
" ------------------------------------------------------------------
function! s:colors_exit(fifo, original, code)
if !empty(a:original) && a:code > 0 && a:original !=# get(g:, 'colors_name', '')
execute 'colo' a:original
endif
call fzf#vim#ipc#stop(a:fifo)
endfunction
function! fzf#vim#colors(...)
let colors = getcompletion('', 'color')
" Put the current colorscheme at the top
let original = get(g:, 'colors_name', '')
if !empty(original)
let colors = [original] + filter(colors, 'original != v:val')
endif
let spec = {
\ 'source': colors,
\ 'sink': 'colo',
\ 'options': ['+m', '--prompt', 'Colors> ']
\}
let fifo = ''
if !a:1 " We can't set up IPC in fullscreen mode in Vim
let fifo = fzf#vim#ipc#start({ msg -> execute('colo '.msg) })
if len(fifo)
call extend(spec.options, ['--no-tmux', '--no-padding', '--no-margin', '--bind', 'focus:execute-silent:printf ''%s\n'' {} > '.fzf#shellescape(fifo)])
let spec.exit = function(s:function('s:colors_exit'), [fifo, original])
let maxwidth = max(map(copy(colors), 'strwidth(v:val)'))
let spec.window = { 'width': maxwidth + 8, 'height': len(colors) + 5 }
endif
endif
let ok = 0
try
call s:fzf('colors', spec, a:000)
let ok = 1
finally
" Nothing calls 'exit' if the run never started
if !ok && !empty(fifo)
call fzf#vim#ipc#stop(fifo)
endif
endtry
endfunction
" ------------------------------------------------------------------
" Locate
" ------------------------------------------------------------------
function! fzf#vim#locate(query, ...)
return s:fzf('locate', {
\ 'source': 'locate '.a:query,
\ '_paste': 1,
\ '_show': 'file',
\ '_hint': [],
\ 'options': '-m --prompt "Locate> "'
\}, a:000)
endfunction
" ------------------------------------------------------------------
" History[:/]
" ------------------------------------------------------------------
function! fzf#vim#_recent_files()
return fzf#vim#_uniq(map(
\ filter([expand('%')], 'len(v:val)')
\ + filter(map(fzf#vim#_buflisted_sorted(), 'bufname(v:val)'), 'len(v:val)')
\ + filter(copy(v:oldfiles), "filereadable(fnamemodify(v:val, ':p'))"),
\ 'fnamemodify(v:val, ":~:.")'))
endfunction
function! s:history_source(type)
let max = histnr(a:type)
if max <= 0
return ['No entries']
endif
let fmt = s:yellow(' %'.len(string(max)).'d ', 'Number')
let list = filter(map(range(1, max), 'histget(a:type, - v:val)'), '!empty(v:val)')
return map(list, 'printf(fmt, len(list) - v:key)." ".v:val')
endfunction
nnoremap <plug>(-fzf-vim-do) :execute g:__fzf_command<cr>
nnoremap <plug>(-fzf-/) /
nnoremap <plug>(-fzf-:) :
function! s:history_sink(type, lines)
if len(a:lines) < 2
return
endif
let prefix = "\<plug>(-fzf-".a:type.')'
let key = a:lines[0]
let item = matchstr(a:lines[1], ' *[0-9]\+ *\zs.*')
if key == 'ctrl-e'
redraw
call feedkeys(a:type.item, 'nt')
else
if a:type == ':'
call histadd(a:type, item)
endif
let g:__fzf_command = "normal ".prefix.item."\<cr>"
call feedkeys("\<plug>(-fzf-vim-do)")
endif
endfunction
function! s:cmd_history_sink(lines)
call s:history_sink(':', a:lines)
endfunction
function! s:search_history_sink(lines)
call s:history_sink('/', a:lines)
endfunction
function! s:history_common(name, source, sinklist, prompt, args)
let footer = s:magenta('C-E', 'Special').' Edit'
return s:fzf(a:name, {
\ 'source': a:source,
\ 'sink*': a:sinklist,
\ 'options': ['+m', '--ansi', '--prompt', a:prompt, '--footer', footer, '--expect=ctrl-e', '--scheme=history']}, a:args)
endfunction
function! fzf#vim#command_history(...)
call s:history_common('history-command', s:history_source(':'), s:function('s:cmd_history_sink'), 'Hist:> ', a:000)
endfunction
function! fzf#vim#search_history(...)
call s:history_common('history-search', s:history_source('/'), s:function('s:search_history_sink'), 'Hist/> ', a:000)
endfunction
function! fzf#vim#history(...)
return s:fzf('history-files', {
\ 'source': fzf#vim#_recent_files(),
\ '_paste': 1,
\ '_show': 'file',
\ '_hint': [],
\ 'options': ['-m', '--header-lines', !empty(expand('%')), '--prompt', 'Hist> ']
\}, a:000)
endfunction
" ------------------------------------------------------------------
" GFiles[?]
" ------------------------------------------------------------------
function! s:get_git_root(dir)
if empty(a:dir) && exists('*FugitiveWorkTree')
return FugitiveWorkTree()
endif
let dir = len(a:dir) ? a:dir : substitute(split(expand('%:p:h'), '[/\\]\.git\([/\\]\|$\)')[0], '^fugitive://', '', '')
silent let root = systemlist('git -C ' . shellescape(dir) . ' rev-parse --show-toplevel')[0]
return v:shell_error ? '' : (len(a:dir) ? fnamemodify(a:dir, ':p') : root)
endfunction
function! s:version_requirement(val, min)
for idx in range(0, len(a:min) - 1)
let v = get(a:val, idx, 0)
if v < a:min[idx] | return 0
elseif v > a:min[idx] | return 1
endif
endfor
return 1
endfunction
function! s:git_version_requirement(...)
if !exists('s:git_version')
let s:git_version = map(split(split(system('git --version'))[2], '\.'), 'str2nr(v:val)')
endif
return s:version_requirement(s:git_version, a:000)
endfunction
function! fzf#vim#gitfiles(args, ...)
let dir = get(get(a:, 1, {}), 'dir', '')
let root = s:get_git_root(dir)
if empty(root)
return s:warn('Not in git repo')
endif
let prefix = 'git -C ' . fzf#shellescape(root) . ' '
if a:args != '?'
let source = prefix . 'ls-files -z ' . a:args
if s:git_version_requirement(2, 31)
let source .= ' --deduplicate'
endif
return s:fzf('gfiles', {
\ 'source': source,
\ 'dir': root,
\ '_paste': 1,
\ '_show': 'file',
\ '_hint': [],
\ 'options': '--scheme path -m --read0 --prompt "GitFiles> "'
\}, a:000)
endif
" Here be dragons!
" We're trying to access the common sink function that fzf#wrap injects to
" the options dictionary.
let bar = s:is_win ? '^|' : '|'
let diff_prefix = 'git -C ' . s:escape_for_bash(root) . ' '
let preview = printf(
\ s:bash() . ' -c "if [[ {1} =~ M ]]; then %s; else %s {-1}; fi"',
\ executable('delta')
\ ? diff_prefix . 'diff -- {-1} ' . bar . ' delta --width $FZF_PREVIEW_COLUMNS --file-style=omit ' . bar . ' sed 1d'
\ : diff_prefix . 'diff --color=always -- {-1} ' . bar . ' sed 1,4d',
\ s:escape_for_bash(s:bin.preview))
" Expose the paste funcref so that fzf#wrap binds the paste key in --expect
" and the common sink dispatches it (with the stripped paths via newsink).
let action = get(g:, 'fzf_action', s:default_action)
let actions = &modifiable ? extend(copy(action), {s:paste_key(): function('fzf#vim#paste')}) : action
let wrapped = s:wrap_with_action(actions, {
\ 'source': prefix . '-c color.status=always status --short --untracked-files=all',
\ 'dir': root,
\ 'options': ['--scheme', 'path', '--ansi', '--multi', '--nth', '2..,..', '--tiebreak=index', '--prompt', 'GitFiles?> ', '--preview', preview]
\})
call s:remove_layout(wrapped)
let wrapped.common_sink = remove(wrapped, 'sink*')
function! wrapped.newsink(lines)
let lines = extend(a:lines[0:0], map(a:lines[1:], 'substitute(v:val[3:], ".* -> ", "", "")'))
return self.common_sink(lines)
endfunction
let wrapped['sink*'] = remove(wrapped, 'newsink')
let wrapped._show = 'gitstatus'
let wrapped._hint = []
return s:fzf('gfiles-diff', wrapped, a:000)
endfunction
" ------------------------------------------------------------------
" Buffers
" ------------------------------------------------------------------
function! s:find_open_window(b)
let [tcur, tcnt] = [tabpagenr() - 1, tabpagenr('$')]
for toff in range(0, tabpagenr('$') - 1)
let t = (tcur + toff) % tcnt + 1
let buffers = tabpagebuflist(t)
for w in range(1, len(buffers))
let b = buffers[w - 1]
if b == a:b
return [t, w]
endif
endfor
endfor
return [0, 0]
endfunction
function! s:jump(t, w)
execute a:t.'tabnext'
execute a:w.'wincmd w'
endfunction
function! s:bufopen(lines)
if len(a:lines) < 2
return
endif
if s:is_paste(a:lines)
return fzf#vim#paste(map(a:lines[1:],
\ "fnamemodify(bufname(str2nr(s:buffer_number(v:val))), ':~:.')"))
endif
let b = s:buffer_number(a:lines[1])
if empty(a:lines[0]) && s:conf('buffers_jump', 0)
let [t, w] = s:find_open_window(b)
if t
call s:jump(t, w)
return
endif
endif
call s:action_for(a:lines[0])
execute 'buffer' b
endfunction
function! s:buffers_exit(path, code)
let lines = filereadable(a:path) ? readfile(a:path) : []
call delete(a:path)
for line in lines
let b = s:buffer_number(line)
if !empty(b)
silent! execute 'bdelete' b
endif
endfor
endfunction
function! fzf#vim#_format_buffer(b)
let name = bufname(a:b)
let line = exists('*getbufinfo') ? getbufinfo(a:b)[0]['lnum'] : 0
let fullname = empty(name) ? '' : fnamemodify(name, ":p:~:.")
let dispname = empty(name) ? '[No Name]' : name
let flag = a:b == bufnr('') ? s:blue('%', 'Conditional') :
\ (a:b == bufnr('#') ? s:magenta('#', 'Special') : ' ')
let modified = getbufvar(a:b, '&modified') ? s:red(' [+]', 'Exception') : ''
let readonly = getbufvar(a:b, '&modifiable') ? '' : s:green(' [RO]', 'Constant')
let extra = join(filter([modified, readonly], '!empty(v:val)'), '')
let target = empty(name) ? '' : (line == 0 ? fullname : fullname.':'.line)
return s:rstrip(printf("%s\t%d\t[%s] %s\t%s\t%s", target, line, s:yellow(a:b, 'Number'), flag, dispname, extra))
endfunction
function! s:sort_buffers(...)
let [b1, b2] = map(copy(a:000), 'get(g:fzf#vim#buffers, v:val, v:val)')
" Using minus between a float and a number in a sort function causes an error
return b1 < b2 ? 1 : -1
endfunction
function! fzf#vim#_buflisted_sorted()
return sort(s:buflisted(), 's:sort_buffers')
endfunction
" [query (string)], [bufnrs (list)], [spec (dict)], [fullscreen (bool)]
function! fzf#vim#buffers(...)
let [query, args] = (a:0 && type(a:1) == type('')) ?
\ [a:1, a:000[1:]] : ['', a:000]
if len(args) && type(args[0]) == s:TYPE.list
let [buffers; args] = args
else
let buffers = s:buflisted()
endif
let sorted = sort(buffers, 's:sort_buffers')
let tabstop = len(max(sorted)) >= 4 ? 9 : 8
let delete_file = tempname()
" map() below rewrites sorted in place, so read the first buffer first
let first = get(sorted, 0, 0)
let spec = {
\ 'source': map(sorted, 'fzf#vim#_format_buffer(v:val)'),
\ 'sink*': s:function('s:bufopen'),
\ 'exit': function(s:function('s:buffers_exit'), [delete_file]),
\ '_paste': 1
\}
let spec._show = 'buffer'
let spec._hint = [['C-A-X', 'Unload', ['ctrl-alt-x']]]
let options = ['+m', '-x', '--tiebreak=index', '--ansi', '-d', '\t', '--with-nth', '3..', '-n', '2,1..2', '--prompt', 'Buf> ', '--query', query, '--preview-window', '+{2}/2', '--tabstop', tabstop, '--bind', 'ctrl-alt-x:execute-silent(echo {} >> '.fzf#shellescape(delete_file).')+exclude']
if bufnr('') == first
call extend(options, ['--sync', '--bind', 'start:pos:2'])
endif
call s:merge_opts(spec, options)
return s:fzf('buffers', spec, args)
endfunction
" ------------------------------------------------------------------
" Ag / Rg
" ------------------------------------------------------------------
function! s:ag_to_qf(line)
let parts = matchlist(a:line, '\(.\{-}\)\s*:\s*\(\d\+\)\%(\s*:\s*\(\d\+\)\)\?\%(\s*:\(.*\)\)\?')
let file = &acd ? fnamemodify(parts[1], ':p') : parts[1]
if has('win32unix') && file !~ '/'
let file = substitute(file, '\', '/', 'g')
endif
let dict = {'filename': file, 'lnum': parts[2], 'text': parts[4]}
if len(parts[3])
let dict.col = parts[3]
endif
return dict
endfunction
function! s:ag_handler(name, lines)
if len(a:lines) < 2
return
endif
let multi_line = min([s:conf('grep_multi_line', 0), 1])
let lines = []
if multi_line && executable('perl')
for idx in range(1, len(a:lines), multi_line + 1)
call add(lines, join(a:lines[idx:idx + multi_line], ''))
endfor
else
let lines = a:lines[1:]
endif
let list = map(filter(lines, 'len(v:val)'), 's:ag_to_qf(v:val)')
if empty(list)
return
endif
if s:is_paste(a:lines)
return fzf#vim#paste(map(copy(list), 's:rstrip(v:val.text)'))
endif
call s:action_for(a:lines[0], list[0].filename, len(list) > 1)
if s:fill_quickfix(a:name, list)
return
endif
" Single item selected
let first = list[0]
try
execute first.lnum
if has_key(first, 'col')
call cursor(0, first.col)
endif
normal! zvzz
catch
endtry
endfunction
" query, [ag options], [spec (dict)], [fullscreen (bool)]
function! fzf#vim#ag(query, ...)
if type(a:query) != s:TYPE.string
return s:warn('Invalid query argument')
endif
let query = empty(a:query) ? '^(?=.)' : a:query
let args = copy(a:000)
let ag_opts = len(args) > 1 && type(args[0]) == s:TYPE.string ? remove(args, 0) : ''
let command = ag_opts . ' -- ' . fzf#shellescape(query)
return call('fzf#vim#ag_raw', insert(args, command, 0))
endfunction
" ag command suffix, [spec (dict)], [fullscreen (bool)]
function! fzf#vim#ag_raw(command_suffix, ...)
if !executable('ag')
return s:warn('ag is not found')
endif
return call('fzf#vim#grep', extend(['ag --nogroup --column --color '.a:command_suffix, 1], a:000))
endfunction
function! s:grep_multi_line(opts)
" TODO: Non-global option
let multi_line = s:conf('grep_multi_line', 0)
if multi_line && executable('perl')
let opts = copy(a:opts)
let extra = ['--read0', '--highlight-line']
if multi_line > 1
call extend(extra, ['--gap', multi_line - 1])
endif
let opts.options = extend(copy(opts.options), extra)
return [opts, printf(" | perl -pe 's/\\n/%s/; s/^([^:]+:){2,3}/$&\\n /'", '\0')]
endif
return [a:opts, '']
endfunction
" command (string), [spec (dict)], [fullscreen (bool)]
function! fzf#vim#grep(grep_command, ...)
let args = copy(a:000)
let words = []
for word in split(a:grep_command)
if word !~# '^[a-z]'
break
endif
call add(words, word)
endfor
let words = empty(words) ? ['grep'] : words
let name = join(words, '-')
let capname = join(map(words, 'toupper(v:val[0]).v:val[1:]'), '')
let opts = {
\ 'options': ['--ansi', '--prompt', capname.'> ',
\ '--multi', '--bind', 'alt-a:select-all,alt-d:deselect-all',
\ '--delimiter', ':', '--preview-window', '+{2}/2']
\}
if len(args) && type(args[0]) == s:TYPE.bool
call remove(args, 0)
endif
let opts._paste = 1
let opts._show = 'grep'
let opts._hint = [['A-a/d', 'Select/deselect all', ['alt-a', 'alt-d']]]
function! opts.sink(lines) closure
return s:ag_handler(get(opts, 'name', name), a:lines)
endfunction
let opts['sink*'] = remove(opts, 'sink')
let [opts, suffix] = s:grep_multi_line(opts)
let command = a:grep_command . suffix
try
let prev_default_command = $FZF_DEFAULT_COMMAND
let $FZF_DEFAULT_COMMAND = command
return s:fzf(name, opts, args)
finally
let $FZF_DEFAULT_COMMAND = prev_default_command
endtry
endfunction
" command_prefix (string), initial_query (string), [spec (dict)], [fullscreen (bool)]
function! fzf#vim#grep2(command_prefix, query, ...)
let args = copy(a:000)
let words = []
for word in split(a:command_prefix)
if word !~# '^[a-z]'
break
endif
call add(words, word)
endfor
let words = empty(words) ? ['grep'] : words
let name = join(words, '-')
let fallback = s:is_win ? '' : ' || :'
let opts = {
\ 'source': s:is_win ? 'cd .' : ':',
\ 'options': ['--ansi', '--prompt', toupper(name).'> ', '--query', a:query,
\ '--disabled',
\ '--multi', '--bind', 'alt-a:select-all,alt-d:deselect-all',
\ '--delimiter', ':', '--preview-window', '+{2}/2']
\}
let [opts, suffix] = s:grep_multi_line(opts)
let suffix = escape(suffix, '{')
call extend(opts.options, ['--bind', 'start:reload:'.a:command_prefix.' '.fzf#shellescape(a:query).suffix])
call extend(opts.options, ['--bind', 'change:reload:'.a:command_prefix.' {q}'.suffix.fallback])
if len(args) && type(args[0]) == s:TYPE.bool
call remove(args, 0)
endif
let opts._paste = 1
let opts._show = 'grep'
let opts._hint = [['A-a/d', 'Select/deselect all', ['alt-a', 'alt-d']]]
function! opts.sink(lines) closure
return s:ag_handler(name, a:lines)
endfunction
let opts['sink*'] = remove(opts, 'sink')
return s:fzf(name, opts, args)
endfunction
" ------------------------------------------------------------------
" BTags
" ------------------------------------------------------------------
function! s:btags_source(tag_cmds)
if !filereadable(expand('%'))
throw 'Save the file first'
endif
for cmd in a:tag_cmds
let lines = split(system(cmd), "\n")
if !v:shell_error && len(lines)
break
endif
endfor
if v:shell_error
throw get(lines, 0, 'Failed to extract tags')
elseif empty(lines)
throw 'No tags found'
endif
return map(s:align_lists(map(lines, 'split(v:val, "\t")')), 'join(v:val, "\t")')
endfunction
function! s:btags_sink(lines)
if len(a:lines) < 2
return
endif
if s:is_paste(a:lines)
return fzf#vim#paste(map(a:lines[1:], 's:strip(split(v:val, "\t")[0])'))
endif
call s:action_for(a:lines[0])
let qfl = []
for line in a:lines[1:]
call s:execute_silent(split(line, "\t")[2])
call add(qfl, {'filename': expand('%'), 'lnum': line('.'), 'text': getline('.')})
endfor
if len(qfl) > 1
" Go back to the original position
normal! g`'
" Because 'listproc' will use 'cfirst' to go to the first item in the list
call s:fill_quickfix('btags', qfl)
else
normal! zvzz
endif
endfunction
" query, [tag commands], [spec (dict)], [fullscreen (bool)]
function! fzf#vim#buffer_tags(query, ...)
let args = copy(a:000)
let escaped = fzf#shellescape(expand('%'))
let null = s:is_win ? 'nul' : '/dev/null'
let sort = has('unix') && !has('win32unix') && executable('sort') ? '| sort -s -k 5' : ''
let tag_cmds = (len(args) > 1 && type(args[0]) != type({})) ? remove(args, 0) : [
\ printf('ctags -f - --sort=yes --excmd=number --language-force=%s %s 2> %s %s', get({ 'cpp': 'c++' }, &filetype, &filetype), escaped, null, sort),
\ printf('ctags -f - --sort=yes --excmd=number %s 2> %s %s', escaped, null, sort)]
if type(tag_cmds) != type([])
let tag_cmds = [tag_cmds]
endif
try
return s:fzf('btags', {
\ 'source': s:btags_source(tag_cmds),
\ 'sink*': s:function('s:btags_sink'),
\ '_show': 'btags',
\ '_hint': [],
\ '_paste': 1,
\ 'options': s:reverse_list(['-m', '-d', '\t', '--with-nth', '1,4..', '-n', '1', '--prompt', 'BTags> ', '--query', a:query, '--preview-window', '+{3}/2'])}, args)
catch
return s:warn(v:exception)
endtry
endfunction
" ------------------------------------------------------------------
" Tags
" ------------------------------------------------------------------
function! s:tags_sink(lines)
if len(a:lines) < 2
return
endif
if s:is_paste(a:lines)
return fzf#vim#paste(map(a:lines[1:], 's:strip(split(v:val, "\t")[0])'))
endif
" Remember the current position
let buf = bufnr('')
let view = winsaveview()
let qfl = []
let [key; list] = a:lines
try
let [magic, &magic, wrapscan, &wrapscan, acd, &acd] = [&magic, 0, &wrapscan, 1, &acd, 0]
for line in list
try
let parts = split(line, '\t\zs')
let excmd = matchstr(join(parts[2:-2], '')[:-2], '^.\{-}\ze;\?"\t')
let base = fnamemodify(parts[-1], ':h')
let relpath = parts[1][:-2]
let abspath = relpath =~ (s:is_win ? '^[A-Z]:\' : '^/') ? relpath : join([base, relpath], '/')
if len(list) == 1
call s:action_for(key, expand(abspath, 1))
else
call s:open(expand(abspath, 1))
endif
call s:execute_silent(excmd)
call add(qfl, {'filename': expand('%'), 'lnum': line('.'), 'text': getline('.')})
catch /^Vim:Interrupt$/
break
catch
call s:warn(v:exception)
endtry
endfor
finally
let [&magic, &wrapscan, &acd] = [magic, wrapscan, acd]
endtry
if len(qfl) > 1
" Go back to the original position. Because 'listproc' will use 'cfirst'
" to go to the first item in the list.
call s:execute_silent('b '.buf)
call winrestview(view)
" However, if a non-default action is triggered, we need to open the first
" entry using the action, to be as backward compatible as possible.
call s:action_for(key, qfl[0].filename, 1)
call s:fill_quickfix('tags', qfl)
else
normal! ^zvzz
endif
endfunction
function! fzf#vim#tags(query, ...)
if !executable('perl')
return s:warn('Tags command requires perl')
endif
if len(a:query) && !executable('readtags')
return s:warn('readtags from universal-ctags is required to pre-filter tags with a prefix')
endif
if empty(tagfiles())
call inputsave()
echohl WarningMsg
let gen = input('tags not found. Generate? (y/N) ')
echohl None
call inputrestore()
redraw
if gen =~? '^y'
call s:warn('Preparing tags')
call system(s:conf('tags_command', 'ctags -R'.(s:is_win ? ' --output-format=e-ctags' : '')))
if empty(tagfiles())
return s:warn('Failed to create tags')
endif
else
return s:warn('No tags found')
endif
endif
let tagfiles = tagfiles()
let v2_limit = 1024 * 1024 * 200
for tagfile in tagfiles
let v2_limit -= getfsize(tagfile)
if v2_limit < 0
break
endif
endfor
let opts = v2_limit < 0 ? ['--algo=v1'] : []
let args = insert(map(tagfiles, 'fzf#shellescape(fnamemodify(v:val, ":p"))'), fzf#shellescape(a:query), 0)
return s:fzf('tags', {
\ 'source': join(['perl', fzf#shellescape(s:bin.tags), join(args)]),
\ 'sink*': s:function('s:tags_sink'),
\ '_show': 'tags',
\ '_hint': [],
\ '_paste': 1,
\ 'options': extend(opts, ['--nth', '1..2', '-m', '-d', '\t', '--tiebreak=begin', '--prompt', 'Tags> ', '--query', a:query])}, a:000)
endfunction
" ------------------------------------------------------------------
" Snippets (UltiSnips)
" ------------------------------------------------------------------
function! s:inject_snippet(line)
let snip = split(a:line, "\t")[0]
execute 'normal! a'.s:strip(snip)."\<c-r>=UltiSnips#ExpandSnippet()\<cr>"
endfunction
function! fzf#vim#snippets(...)
if !exists(':UltiSnipsEdit')
return s:warn('UltiSnips not found')
endif
let list = UltiSnips#SnippetsInCurrentScope()
if empty(list)
return s:warn('No snippets available here')
endif
let aligned = sort(s:align_lists(items(list)))
let colored = map(aligned, 's:yellow(v:val[0])."\t".v:val[1]')
return s:fzf('snippets', {
\ 'source': colored,
\ 'options': '--ansi --tiebreak=index +m -n 1,.. -d "\t"',
\ 'sink': s:function('s:inject_snippet')}, a:000)
endfunction
" ------------------------------------------------------------------
" Commands
" ------------------------------------------------------------------
let s:tab = "\t"
function! s:format_cmd(line)
return substitute(a:line, '\C \([A-Z]\S*\) ',
\ '\=s:tab.s:yellow(submatch(1), "Function").s:tab', '')
endfunction
function! s:command_sink(lines)
if len(a:lines) < 2
return
endif
let cmd = matchstr(a:lines[1], s:tab.'\zs\S*\ze'.s:tab)
if empty(a:lines[0])
call feedkeys(':'.cmd.(a:lines[1][0] == '!' ? '' : ' '), 'nt')
else
call feedkeys(':'.cmd."\<cr>", 'nt')
endif
endfunction
let s:fmt_excmd = ' '.s:blue('%-38s', 'Statement').'%s'
function! s:format_excmd(ex)
let match = matchlist(a:ex, '^|:\(\S\+\)|\s*\S*\(.*\)')
return printf(s:fmt_excmd, s:tab.match[1].s:tab, s:strip(match[2]))
endfunction
function! s:excmds()
let help = globpath($VIMRUNTIME, 'doc/index.txt')
if empty(help)
return []
endif
let commands = []
let command = ''
for line in readfile(help)
if line =~ '^|:[^|]'
if !empty(command)
call add(commands, s:format_excmd(command))
endif
let command = line
elseif line =~ '^\s\+\S' && !empty(command)
let command .= substitute(line, '^\s*', ' ', '')
elseif !empty(commands) && line =~ '^\s*$'
break
endif
endfor
if !empty(command)
call add(commands, s:format_excmd(command))
endif
return commands
endfunction
function! fzf#vim#commands(...)
redir => cout
silent command
redir END
let list = split(cout, "\n")
return s:fzf('commands', {
\ 'source': extend(extend(list[0:0], map(list[1:], 's:format_cmd(v:val)')), s:excmds()),
\ 'sink*': s:function('s:command_sink'),
\ 'options': '--ansi --expect '.s:conf('commands_expect', 'ctrl-x').
\ ' --tiebreak=index --header-lines 1 -x --prompt "Commands> " -n2,3,2..3 --tabstop=1 -d "\t" --list-border --header-border inline --info inline-right --no-separator'}, a:000)
endfunction
" ------------------------------------------------------------------
" Changes
" ------------------------------------------------------------------
function! s:format_change(bufnr, offset, item)
let buflines = getbufline(a:bufnr, a:item.lnum)
if empty(buflines)
return ''
endif
return printf("%3d %s %4d %3d %s", a:bufnr, s:yellow(printf('%6s', a:offset)), a:item.lnum, a:item.col, buflines[0])
endfunction
function! s:changes_sink(lines)
if len(a:lines) < 2
return
endif
if s:is_paste(a:lines)
" buf offset line col text -> the text column
return fzf#vim#paste(map(a:lines[1:], 's:rest_after_columns(v:val, 4)'))
endif
call s:action_for(a:lines[0])
let [b, o, l, c] = split(a:lines[1])[0:3]
if o == '-'
execute 'buffer' b
call cursor(l, c)
elseif o[0] == '+'
execute 'normal!' o[1:].'g,'
else
execute 'normal!' o.'g;'
endif
endfunction
function! s:format_change_offset(current, index, cursor)
if !a:current
return '-'
endif
let offset = a:index - a:cursor + 1
if offset < 0
return '+'.-offset
endif
return offset
endfunction
function! fzf#vim#changes(...)
let all_changes = ["buf offset line col text"]
let cursor = 0
for bufnr in fzf#vim#_buflisted_sorted()
let [changes, position_or_length] = getchangelist(bufnr)
let current = bufnr('') == bufnr
if current
let cursor = len(changes) - position_or_length
endif
let all_changes += filter(map(reverse(changes), { idx, val -> s:format_change(bufnr, s:format_change_offset(current, idx, cursor), val) }), '!empty(v:val)')
endfor
return s:fzf('changes', {
\ 'source': all_changes,
\ 'sink*': s:function('s:changes_sink'),
\ '_show': 'changes',
\ '_hint': [],
\ '_paste': 1,
\ 'options': printf('+m -x --ansi --tiebreak=index --header-lines=1 --cycle --scroll-off 999 --sync --bind start:pos:%d --prompt "Changes> " --list-border --header-border inline --info inline-right --no-separator', cursor)}, a:000)
endfunction
" ------------------------------------------------------------------
" Marks
" ------------------------------------------------------------------
function! s:format_mark(line)
return substitute(a:line, '\S', '\=s:yellow(submatch(0), "Number")', '')
endfunction
function! s:mark_sink(lines)
if len(a:lines) < 2
return
endif
if s:is_paste(a:lines)
" mark line col file/text -> the file/text column
return fzf#vim#paste(map(a:lines[1:], 's:rest_after_columns(v:val, 3)'))
endif
call s:action_for(a:lines[0])
execute 'normal! `'.matchstr(a:lines[1], '\S').'zz'
endfunction
function! fzf#vim#marks(...) abort
let [initial_marks, extra] = (a:0 && type(a:1) == type('')) ?
\ [a:1, a:000[1:]] : ['', a:000]
redir => cout
execute 'silent! marks' initial_marks
redir END
let list = split(cout, "\n")
" If first line is not the expected header, no marks found
if empty(list) || list[0] =~# '^E'
return s:warn('No marks found')
endif
return s:fzf('marks', {
\ 'source': extend(list[0:0], map(list[1:], 's:format_mark(v:val)')),
\ 'sink*': s:function('s:mark_sink'),
\ '_show': 'marks',
\ '_hint': [],
\ '_paste': 1,
\ 'options': '+m -x --ansi --tiebreak=index --header-lines 1 --tiebreak=begin --prompt "Marks> " --list-border --header-border inline --info inline-right --no-separator'}, extra)
endfunction
" ------------------------------------------------------------------
" Jumps
" ------------------------------------------------------------------
function! s:jump_format(line)
let line = substitute(a:line, '[0-9]\+', '\=s:yellow(submatch(0), "Number")', '')
let line = substitute(line, '\s.\{-}\ze:[0-9]\+:', '\=s:green(submatch(0), "Directory")', '')
let line = substitute(line, '\%(:[0-9]\+\)\+:', '\=s:black(submatch(0), "NonText")', '')
return line
endfunction
function! s:jump_sink(pos, lines)
if len(a:lines) < 2
return
endif
keepjumps call s:action_for(a:lines[0])
let idx = str2nr(a:lines[1])
let delta = idx - a:pos - 1
if delta < 0
execute 'normal! ' . -delta . "\<C-O>"
else
execute 'normal! ' . delta . "\<C-I>"
endif
normal! zvzz
endfunction
function! fzf#vim#jumps(...)
let [jumps, pos] = getjumplist()
if empty(jumps)
return s:warn('No jumps')
endif
let jumplist = []
for idx in range(len(jumps))
let jump = jumps[idx]
let loc = expand('#'.jump.bufnr.':p:~:.')
if empty(loc)
let loc = '[No Name]'
endif
let loc .= ':'.jump.lnum
if jump.col
let loc .= ':'.jump.col
endif
let line = printf('%-2d %s: %s', idx+1, loc, getbufoneline(jump.bufnr, jump.lnum))
call add(jumplist, line)
endfor
let current = -pos-1
return s:fzf('jumps', {
\ 'source': map(jumplist, 's:jump_format(v:val)'),
\ 'sink*': function(s:function('s:jump_sink'), [pos]),
\ 'options': ['+m', '-x', '--ansi', '--tiebreak=index', '--cycle', '--scroll-off=999', '--sync', '--bind', 'start:pos('.current.')+offset-middle', '--tac', '--tiebreak=begin', '--prompt', 'Jumps> ', '--preview-window', '+{3}/2', '--tabstop=2', '--delimiter', '[:\s]+'],
\ }, a:000)
endfunction
" ------------------------------------------------------------------
" Help tags
" ------------------------------------------------------------------
function! s:helptag_sink(line)
let [tag, file, path] = split(a:line, "\t")[0:2]
let rtp = fnamemodify(path, ':p:h:h')
if stridx(&rtp, rtp) < 0
execute 'set rtp+='.s:escape(rtp)
endif
execute 'help' tag
endfunction
function! fzf#vim#helptags(...)
if !executable('perl')
return s:warn('Helptags command requires perl')
endif
let sorted = sort(split(globpath(&runtimepath, 'doc/tags', 1), '\n'))
let tags = exists('*uniq') ? uniq(sorted) : fzf#vim#_uniq(sorted)
let script = tempname()
call writefile(['for my $filename (@ARGV) { open(my $file,q(<),$filename) or die; while (<$file>) { /(.*?)\t(.*?)\t(.*)/; push @lines, sprintf(qq('.s:green('%-40s', 'Label').'\t%s\t%s\t%s\n), $1, $2, $filename, $3); } close($file) or die; } print for sort @lines;'], script)
let spec = {
\ 'source': 'perl '.fzf#shellescape(script).' '.join(map(tags, 'fzf#shellescape(v:val)')),
\ 'sink': s:function('s:helptag_sink'),
\ 'exit': { _ -> delete(script) },
\ 'options': ['--ansi', '+m', '--tiebreak=begin', '--with-nth', '..3']}
let ok = 0
try
let result = s:fzf('helptags', spec, a:000)
let ok = 1
return result
finally
" Nothing calls 'exit' if the run never started
if !ok
call delete(script)
endif
endtry
endfunction
" ------------------------------------------------------------------
" File types
" ------------------------------------------------------------------
function! fzf#vim#filetypes(...)
return s:fzf('filetypes', {
\ 'source': fzf#vim#_uniq(sort(map(split(globpath(&rtp, 'syntax/*.vim'), '\n'),
\ 'fnamemodify(v:val, ":t:r")'))),
\ 'sink': 'setf',
\ 'options': '+m --prompt="File types> "'
\}, a:000)
endfunction
" ------------------------------------------------------------------
" Windows
" ------------------------------------------------------------------
function! s:format_win(tab, win, buf)
let modified = getbufvar(a:buf, '&modified')
let name = bufname(a:buf)
let name = empty(name) ? s:tab.s:tab.'[No Name]' : ' '.s:tab.name
let active = tabpagewinnr(a:tab) == a:win
return (active? s:blue('>', 'Operator') : ' ') . name . s:tab . (modified? s:red(' [+]', 'Exception') : '')
endfunction
function! s:windows_sink(line)
let list = matchlist(a:line, '^ *\([0-9]\+\) *\([0-9]\+\)')
call s:jump(list[1], list[2])
endfunction
function! fzf#vim#windows(...)
let lines = []
for t in range(1, tabpagenr('$'))
let buffers = tabpagebuflist(t)
for w in range(1, len(buffers))
call add(lines,
\ printf('%s %s %s',
\ s:yellow(printf('%3d', t), 'Number'),
\ s:cyan(printf('%3d', w), 'String'),
\ s:format_win(t, w, buffers[w-1])))
endfor
endfor
return s:fzf('windows', {
\ 'source': extend(['Tab Win Name'], lines),
\ 'sink': s:function('s:windows_sink'),
\ 'options': '+m --ansi --tiebreak=begin --header-lines=1 --tabstop=1 -d "\t" --list-border --header-border inline --info inline-right --no-separator'}, a:000)
endfunction
" ------------------------------------------------------------------
" Commits / BCommits
" ------------------------------------------------------------------
function! s:yank_to_register(data)
let @" = a:data
silent! let @* = a:data
silent! let @+ = a:data
endfunction
function! s:commits_sink(lines)
if len(a:lines) < 2
return
endif
let pat = '[0-9a-f]\{7,40}'
if a:lines[0] == 'ctrl-y'
let hashes = join(filter(map(a:lines[1:], 'matchstr(v:val, pat)'), 'len(v:val)'))
return s:yank_to_register(hashes)
end
if s:is_paste(a:lines)
return fzf#vim#paste(filter(map(a:lines[1:], 'matchstr(v:val, pat)'), 'len(v:val)'))
endif
let diff = a:lines[0] == 'ctrl-d'
let Cmd = get(get(g:, 'fzf_action', s:default_action), a:lines[0], '')
let cmd = type(Cmd) == s:TYPE.string ? Cmd : ''
let buf = bufnr('')
for idx in range(1, len(a:lines) - 1)
let sha = matchstr(a:lines[idx], pat)
if !empty(sha)
if diff
if idx > 1
execute 'tab sb' buf
endif
execute 'Gdiff' sha
else
" Since fugitive buffers are unlisted, we can't keep using 'e'
let c = empty(cmd) ? (idx == 1 ? 'edit' : 'tab split') : cmd
execute c FugitiveFind(sha)
endif
endif
endfor
endfunction
function! s:commits(range, buffer_local, args)
let s:git_root = s:get_git_root('')
if empty(s:git_root)
return s:warn('Not in git repository')
endif
let prefix = 'git -C ' . fzf#shellescape(s:git_root) . ' '
let source = prefix . 'log '.s:conf('commits_log_options', '--color=always '.fzf#shellescape('--format=%C(auto)%h%d %s %C(green)%cr'))
let current = expand('%:p')
let managed = 0
if !empty(current)
call system(prefix . 'show '.fzf#shellescape(current).' 2> '.(s:is_win ? 'nul' : '/dev/null'))
let managed = !v:shell_error
endif
let args = copy(a:args)
let log_opts = len(args) && type(args[0]) == type('') ? remove(args, 0) : ''
let with_preview = !s:is_win && &columns > s:wide
if len(a:range) || a:buffer_local
if !managed
return s:warn('The current buffer is not in the working tree')
endif
if len(a:range)
let source .= join([printf(' -L %d,%d:%s --no-patch', a:range[0], a:range[1], fzf#shellescape(current)), log_opts])
if with_preview
let previewparams = join([printf('log -L %d,%d:%s', a:range[0], a:range[1], fzf#shellescape(current)), log_opts])
let previewfilter = " | awk '/commit {1}/ {flag=1;print;next} /^[^ ]*commit/{flag=0} flag' "
let previewcmd = prefix . previewparams .' --color=always '. previewfilter
endif
else
let source .= join([' --follow', log_opts, fzf#shellescape(current)])
endif
let command = 'BCommits'
else
let source .= join([' --graph', log_opts])
let command = 'Commits'
endif
" Only string actions are meaningful for the commits sink; the paste key is
" handled explicitly in s:commits_sink (pastes the commit hashes), and only
" when the current buffer can be modified.
let action = get(g:, 'fzf_action', s:default_action)
let expect_keys = filter(keys(action), 'type(action[v:val]) == s:TYPE.string')
if &modifiable
call add(expect_keys, s:paste_key())
endif
let hint = s:build_hint([['C-S', 'Toggle sort'], ['C-Y', 'Yank hashes']], 1)
let options = {
\ 'source': source,
\ 'sink*': s:function('s:commits_sink'),
\ 'options': s:reverse_list(['--ansi', '--multi', '--scheme=history',
\ '--prompt', command.'> ', '--bind=ctrl-s:toggle-sort',
\ '--footer', hint,
\ '--expect=ctrl-y,'.join(expect_keys, ',')])
\ }
if a:buffer_local
let options.options[-2] .= ', '.s:magenta('CTRL-D', 'Special').' to diff'
let options.options[-1] .= ',ctrl-d'
endif
if with_preview
if !len(a:range)
let orderfile = tempname()
call writefile([current[len(s:git_root)+1:]], orderfile)
let previewcmd = 'echo {} | grep -o "[a-f0-9]\{7,\}" | head -1 | xargs ' . prefix . 'show -O'.fzf#shellescape(orderfile).' --format=format: --color=always '
endif
let suffix = executable('delta') ? '| delta --width $FZF_PREVIEW_COLUMNS' : ''
call extend(options.options, ['--preview', previewcmd . suffix])
endif
return s:fzf(a:buffer_local ? 'bcommits' : 'commits', options, args)
endfunction
" Heuristically determine if the user specified a range
function! s:given_range(line1, line2)
" 1. From visual mode
" :'<,'>Commits
" 2. From command-line
" :10,20Commits
if a:line1 == line("'<") && a:line2 == line("'>") ||
\ (a:line1 != 1 || a:line2 != line('$'))
return [a:line1, a:line2]
endif
return []
endfunction
" [git-log-args], [spec (dict)], [fullscreen (bool)]
function! fzf#vim#commits(...) range
if exists('b:fzf_winview')
call winrestview(b:fzf_winview)
unlet b:fzf_winview
endif
return s:commits(s:given_range(a:firstline, a:lastline), 0, a:000)
endfunction
" [git-log-args], [spec (dict)], [fullscreen (bool)]
function! fzf#vim#buffer_commits(...) range
if exists('b:fzf_winview')
call winrestview(b:fzf_winview)
unlet b:fzf_winview
endif
return s:commits(s:given_range(a:firstline, a:lastline), 1, a:000)
endfunction
" ------------------------------------------------------------------
" fzf#vim#maps(mode, opts[with count and op])
" ------------------------------------------------------------------
function! s:align_pairs(list)
let maxlen = 0
let pairs = []
for elem in a:list
let match = matchlist(elem, '^\(\S*\)\s*\(.*\)$')
let [_, k, v] = match[0:2]
let maxlen = max([maxlen, len(k)])
call add(pairs, [k, substitute(v, '^\*\?[@ ]\?', '', '')])
endfor
let maxlen = min([maxlen, 35])
return map(pairs, "printf('%-'.maxlen.'s', v:val[0]).' '.v:val[1]")
endfunction
function! s:highlight_keys(str)
return substitute(
\ substitute(a:str, '<[^ >]\+>', s:yellow('\0', 'Special'), 'g'),
\ '<Plug>', s:blue('<Plug>', 'SpecialKey'), 'g')
endfunction
function! s:key_sink(gv, cnt, reg, op, line)
let key = matchstr(a:line, '^\S*')
redraw
call feedkeys(a:gv.a:cnt.a:reg, 'n')
call feedkeys(a:op.
\ substitute(key, '<[^ >]\+>', '\=eval("\"\\".submatch(0)."\"")', 'g'))
endfunction
function! fzf#vim#maps(mode, ...)
let gv = a:mode == 'x' ? 'gv' : ''
let cnt = v:count == 0 ? '' : v:count
let reg = empty(v:register) ? '' : ('"'.v:register)
let op = a:mode == 'o' ? v:operator : ''
redir => cout
silent execute 'verbose' a:mode.'map'
redir END
let list = []
let curr = ''
for line in split(cout, "\n")
if line =~ "^\t"
let src = "\t".substitute(matchstr(line, '/\zs[^/\\]*\ze$'), ' [^ ]* ', ':', '')
call add(list, printf('%s %s', curr, s:green(src, 'Comment')))
let curr = ''
else
if !empty(curr)
call add(list, curr)
endif
let curr = line[3:]
endif
endfor
if !empty(curr)
call add(list, curr)
endif
let aligned = s:align_pairs(list)
let sorted = sort(aligned)
let colored = map(sorted, 's:highlight_keys(v:val)')
let pcolor = a:mode == 'x' ? 9 : a:mode == 'o' ? 10 : 12
return s:fzf('maps', {
\ 'source': colored,
\ 'sink': function(s:function('s:key_sink'), [gv, cnt, reg, op]),
\ 'options': '--prompt "Maps ('.a:mode.')> " --ansi --no-hscroll --nth 1,.. --color prompt:'.pcolor}, a:000)
endfunction
" ----------------------------------------------------------------------------
" fzf#vim#complete - completion helper
" ----------------------------------------------------------------------------
inoremap <silent> <Plug>(-fzf-complete-trigger) <c-o>:call <sid>complete_trigger()<cr>
function! s:pluck(dict, key, default)
return has_key(a:dict, a:key) ? remove(a:dict, a:key) : a:default
endfunction
function! s:complete_trigger()
let opts = copy(s:opts)
call s:prepend_opts(opts, ['+m', '-q', s:query])
let Reducer = s:pluck(opts, 'reducer', s:function('s:first_line'))
let opts['sink*'] = function(s:function('s:complete_insert'), [s:query, s:eol, Reducer])
call fzf#run(opts)
endfunction
" The default reducer
function! s:first_line(lines)
return a:lines[0]
endfunction
function! s:complete_insert(query, eol, Reducer, lines)
if empty(a:lines)
return
endif
let chars = strchars(a:query)
if chars == 0 | let del = ''
elseif chars == 1 | let del = '"_x'
else | let del = (chars - 1).'"_dvh'
endif
let data = call(a:Reducer, [a:lines])
let ve = &ve
set ve=
execute 'normal!' ((a:eol || empty(chars)) ? '' : 'h').del.(a:eol ? 'a': 'i').data
let &ve = ve
if mode() =~ 't'
call feedkeys('a', 'n')
elseif has('nvim')
execute "normal! \<esc>la"
else
call feedkeys("\<Plug>(-fzf-complete-finish)")
endif
endfunction
nnoremap <silent> <Plug>(-fzf-complete-finish) a
inoremap <silent> <Plug>(-fzf-complete-finish) <c-o>l
function! s:eval(dict, key, arg)
if has_key(a:dict, a:key) && type(a:dict[a:key]) == s:TYPE.funcref
let ret = copy(a:dict)
let ret[a:key] = call(a:dict[a:key], [a:arg])
return ret
endif
return a:dict
endfunction
function! fzf#vim#complete(...)
if a:0 == 0
let s:opts = fzf#wrap()
elseif type(a:1) == s:TYPE.dict
let s:opts = copy(a:1)
elseif type(a:1) == s:TYPE.string
let s:opts = extend({'source': a:1}, get(a:000, 1, fzf#wrap()))
else
echoerr 'Invalid argument: '.string(a:000)
return ''
endif
for s in ['sink', 'sink*']
if has_key(s:opts, s)
call remove(s:opts, s)
endif
endfor
let eol = col('$')
let ve = &ve
set ve=all
let s:eol = col('.') == eol
let &ve = ve
let Prefix = s:pluck(s:opts, 'prefix', '\k*$')
if col('.') == 1
let s:query = ''
else
let full_prefix = getline('.')[0 : col('.')-2]
if type(Prefix) == s:TYPE.funcref
let s:query = call(Prefix, [full_prefix])
else
let s:query = matchstr(full_prefix, Prefix)
endif
endif
let s:opts = s:eval(s:opts, 'source', s:query)
let s:opts = s:eval(s:opts, 'options', s:query)
let s:opts = s:eval(s:opts, 'extra_options', s:query)
if has_key(s:opts, 'extra_options')
call s:merge_opts(s:opts, remove(s:opts, 'extra_options'))
endif
if has_key(s:opts, 'options')
if type(s:opts.options) == s:TYPE.list
call add(s:opts.options, '--no-expect')
else
let s:opts.options .= ' --no-expect'
endif
endif
call feedkeys("\<Plug>(-fzf-complete-trigger)")
return ''
endfunction
" ------------------------------------------------------------------
let &cpo = s:cpo_save
unlet s:cpo_save