Add ctrl-o to show the entry without closing fzf

Files, GFiles, GFiles?, Rg/Ag, Buffers, History, Locate, Lines, BLines,
Tags, BTags, Marks and Changes. Writes the entry to an fzf#vim#ipc fifo, so
it needs the asynchronous fzf#run from junegunn/fzf@5cb7bab7
(junegunn/fzf#4897, 0.74.4), which the g:loaded_fzf check detects.

Commands declare '_show' for the entry kind and '_hint' for footer keys, so
the footer and the binding cannot disagree. s:can_show withholds both when
the run would block or the window to open in is not visible.

g:fzf_vim.show_key configures the key, empty to turn it off, comma separated
to bind several ('ctrl-o,double-click'). Named for its 'Show' footer label,
since Enter already occupies 'Open' there. Only the first key is labelled,
and a clash on any of them drops all of them, so the footer cannot list a
key that does nothing. Binding Enter drops its 'Open' hint too, since fzf
accepts on Enter rather than through g:fzf_action.

s:show_entry parses each entry the way that command's sink does and runs
win_execute() in the window fzf started from. That keeps the focus in fzf
and works from a popup, which win_gotoid() cannot leave. It opens with
'keepalt keepjumps hide', so stepping through entries leaves the alternate
file and the jumplist unchanged.

fzf#run is asynchronous, so runs can overlap, and a second run would stop
the first's ipc channel and overwrite the script locals its sink reads back
afterwards. Channels are now keyed by fifo path and per-run state is bound
into the callback instead, which is why Buffers, Colors, Jumps, Maps,
Helptags and complete change here.
This commit is contained in:
Junegunn Choi
2026-08-26 18:21:17 +09:00
parent d2a59a992a
commit a246483a09
4 changed files with 557 additions and 98 deletions
+27
View File
@@ -102,6 +102,14 @@ Commands
line content (`:Lines`, `:BLines`, `:Marks`, `:Changes`), matched text
(`:Rg`, `:Ag`, `:Grep`), tag name (`:Tags`, `:BTags`), or commit hash
(`:Commits`, `:BCommits`)
- Most commands support `CTRL-O` to open the current entry in the window fzf
was started from, leaving fzf open, so you can look through several entries
in a single run (`:Files`, `:GFiles`, `:Buffers`, `:History`, `:Locate`,
`:Rg`, `:Ag`, `:Grep`, `:Lines`, `:BLines`, `:Tags`, `:BTags`, `:Marks`,
`:Changes`). Requires fzf 0.74.4 or later, and is not offered in fullscreen
mode, with a layout that leaves no other window (`enew`, `tabnew`), on
Windows, or when the key is already used by `g:fzf_vim.paste_key` or
`g:fzf_action`
- Bang-versions of the commands (e.g. `Ag!`) will open fzf in fullscreen
- You can set `g:fzf_vim.command_prefix` to give the same prefix to the commands
- e.g. `let g:fzf_vim.command_prefix = 'Fzf'` and you have `FzfFiles`, etc.
@@ -173,6 +181,25 @@ command inserts). Customize it with `g:fzf_vim.paste_key`.
let g:fzf_vim.paste_key = 'alt-enter'
```
#### Show key
Most commands support a key that opens the current entry in the window fzf
was started from and leaves fzf open, so you can look through several entries
in one run (see the command list above). It is labelled `Show` in the footer
to set it apart from `Enter`, which opens the entry and closes fzf. Customize
it with `g:fzf_vim.show_key`, or set it to an empty string to turn the key
off. A comma separated list binds every key in it. It takes the key over from
`$FZF_DEFAULT_OPTS`, so pick another key or turn it off to keep a binding of
your own.
```vim
" Key to open the current entry, leaving fzf open (default: 'ctrl-o')
let g:fzf_vim.show_key = 'ctrl-o'
" Use CTRL-O and double-click
let g:fzf_vim.show_key = 'ctrl-o,double-click'
```
#### Command-level options
```vim
+431 -67
View File
@@ -352,15 +352,21 @@ for s:color_name in keys(s:ansi)
\ "endfunction"
endfor
function! s:build_hint(entries, defaults)
" 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
call add(entries, ['Enter', 'Open'])
for [key, name, label] in [['ctrl-x', 'C-X', 'HSplit'], ['ctrl-v', 'C-V', 'VSplit'], ['ctrl-t', 'C-T', 'New tab']]
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 type(Cmd) == s:TYPE.string && Cmd ==# s:default_action[key]
if index(taken, key) < 0 && type(Cmd) == s:TYPE.string && Cmd ==# s:default_action[key]
call add(entries, [name, label])
endif
endfor
@@ -400,7 +406,18 @@ function! s:fzf(name, opts, extra)
" Command-level fzf options
call s:merge_opts(merged, s:conf(a:name.'_options', []))
return fzf#run(s:wrap(a:name, merged, bang))
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 = {
@@ -416,6 +433,45 @@ 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
@@ -426,6 +482,9 @@ 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)
@@ -550,6 +609,272 @@ function! s:shortpath()
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, 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: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, 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
let lnum = str2nr(split(entry, "\t", 1)[0])
if lnum <= 0
return
endif
let cmds = ['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
return
endif
let cmds = ['keepjumps '.parts[2], 'normal! zvzz']
elseif a:kind ==# 'marks'
let mark = matchstr(entry, '^\s*\zs\S')
if empty(mark)
return
endif
let cmds = ['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) 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. 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
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.
" 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 key = s:show_key()
if !empty(kind) && !empty(key) && s:can_show(a:spec, a:bang, key)
let dir = get(a:spec, 'dir', '')
let fifo = fzf#vim#ipc#start(function('s:show_entry',
\ [win_getid(), 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)
@@ -565,6 +890,8 @@ function! fzf#vim#files(dir, ...)
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
@@ -643,6 +970,8 @@ function! fzf#vim#lines(...)
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)
@@ -688,6 +1017,8 @@ function! fzf#vim#buffer_lines(...)
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)
@@ -696,23 +1027,20 @@ endfunction
" ------------------------------------------------------------------
" Colors
" ------------------------------------------------------------------
function! s:colors_exit(code)
if exists('s:colors_name')
if a:code > 0 && s:colors_name != g:colors_name
execute 'colo' s:colors_name
endif
unlet s:colors_name
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()
call fzf#vim#ipc#stop(a:fifo)
endfunction
function! fzf#vim#colors(...)
let colors = getcompletion('', 'color')
" Put the current colorscheme at the top
if exists('g:colors_name')
let s:colors_name = g:colors_name
let colors = [g:colors_name] + filter(colors, 'g:colors_name != v:val')
let original = get(g:, 'colors_name', '')
if !empty(original)
let colors = [original] + filter(colors, 'original != v:val')
endif
let spec = {
@@ -721,17 +1049,27 @@ function! fzf#vim#colors(...)
\ '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:echo {} > '.fifo])
let spec.exit = s:function('s:colors_exit')
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
call s:fzf('colors', spec, a:000)
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
" ------------------------------------------------------------------
@@ -741,6 +1079,8 @@ 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
@@ -818,6 +1158,8 @@ 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
@@ -868,6 +1210,8 @@ function! fzf#vim#gitfiles(args, ...)
\ 'source': source,
\ 'dir': root,
\ '_paste': 1,
\ '_show': 'file',
\ '_hint': [],
\ 'options': '--scheme path -m --read0 --prompt "GitFiles> "'
\}, a:000)
endif
@@ -899,6 +1243,8 @@ function! fzf#vim#gitfiles(args, ...)
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
@@ -931,9 +1277,9 @@ function! s:bufopen(lines)
endif
if s:is_paste(a:lines)
return fzf#vim#paste(map(a:lines[1:],
\ "fnamemodify(bufname(str2nr(matchstr(v:val, '\\[\\zs[0-9]*\\ze\\]'))), ':~:.')"))
\ "fnamemodify(bufname(str2nr(s:buffer_number(v:val))), ':~:.')"))
endif
let b = matchstr(a:lines[1], '\[\zs[0-9]*\ze\]')
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
@@ -945,16 +1291,11 @@ function! s:bufopen(lines)
execute 'buffer' b
endfunction
function! s:buffers_exit(code)
if !exists('s:buffers_delete_file')
return
endif
let path = s:buffers_delete_file
unlet s:buffers_delete_file
let lines = filereadable(path) ? readfile(path) : []
call delete(path)
function! s:buffers_exit(path, code)
let lines = filereadable(a:path) ? readfile(a:path) : []
call delete(a:path)
for line in lines
let b = matchstr(line, '\[\zs[0-9]*\ze\]')
let b = s:buffer_number(line)
if !empty(b)
silent! execute 'bdelete' b
endif
@@ -996,19 +1337,22 @@ function! fzf#vim#buffers(...)
endif
let sorted = sort(buffers, 's:sort_buffers')
let tabstop = len(max(sorted)) >= 4 ? 9 : 8
let s:buffers_delete_file = tempname()
let hint = s:build_hint([['C-A-X', 'Unload']], 1)
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 {} >> '.s:buffers_delete_file.')+exclude', '--footer', hint]
if bufnr('') == get(sorted, 0, 0)
call extend(options, ['--sync', '--bind', 'start:pos:2'])
endif
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': s:function('s:buffers_exit'),
\ '_paste': 1,
\ 'options': options
\ 'exit': function(s:function('s:buffers_exit'), [delete_file]),
\ '_paste': 1
\}
let spec._show = 'buffer'
let spec._hint = [['C-A-X', 'Unload']]
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
@@ -1128,6 +1472,8 @@ function! fzf#vim#grep(grep_command, ...)
endif
let opts._paste = 1
let opts._show = 'grep'
let opts._hint = [['A-a/d', 'Select/deselect all']]
function! opts.sink(lines) closure
return s:ag_handler(get(opts, 'name', name), a:lines)
endfunction
@@ -1174,6 +1520,8 @@ function! fzf#vim#grep2(command_prefix, query, ...)
call remove(args, 0)
endif
let opts._paste = 1
let opts._show = 'grep'
let opts._hint = [['A-a/d', 'Select/deselect all']]
function! opts.sink(lines) closure
return s:ag_handler(name, a:lines)
endfunction
@@ -1243,6 +1591,8 @@ function! fzf#vim#buffer_tags(query, ...)
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
@@ -1352,6 +1702,8 @@ function! fzf#vim#tags(query, ...)
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
@@ -1509,6 +1861,8 @@ function! fzf#vim#changes(...)
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
@@ -1550,6 +1904,8 @@ function! fzf#vim#marks(...) abort
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
@@ -1564,13 +1920,13 @@ function! s:jump_format(line)
return line
endfunction
function! s:jump_sink(lines)
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 - s:jump_current - 1
let delta = idx - a:pos - 1
if delta < 0
execute 'normal! ' . -delta . "\<C-O>"
else
@@ -1584,7 +1940,7 @@ function! fzf#vim#jumps(...)
if empty(jumps)
return s:warn('No jumps')
endif
let s:jumplist = []
let jumplist = []
for idx in range(len(jumps))
let jump = jumps[idx]
let loc = expand('#'.jump.bufnr.':p:~:.')
@@ -1596,13 +1952,12 @@ function! fzf#vim#jumps(...)
let loc .= ':'.jump.col
endif
let line = printf('%-2d %s: %s', idx+1, loc, getbufoneline(jump.bufnr, jump.lnum))
call add(s:jumplist, line)
call add(jumplist, line)
endfor
let s:jump_current = pos
let current = -pos-1
return s:fzf('jumps', {
\ 'source': map(s:jumplist, 's:jump_format(v:val)'),
\ 'sink*': s:function('s:jump_sink'),
\ '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
@@ -1626,16 +1981,25 @@ function! fzf#vim#helptags(...)
let sorted = sort(split(globpath(&runtimepath, 'doc/tags', 1), '\n'))
let tags = exists('*uniq') ? uniq(sorted) : fzf#vim#_uniq(sorted)
if exists('s:helptags_script')
silent! call delete(s:helptags_script)
endif
let s:helptags_script = tempname()
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;'], s:helptags_script)
return s:fzf('helptags', {
\ 'source': 'perl '.fzf#shellescape(s:helptags_script).' '.join(map(tags, 'fzf#shellescape(v:val)')),
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'),
\ 'options': ['--ansi', '+m', '--tiebreak=begin', '--with-nth', '..3']}, a:000)
\ '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
" ------------------------------------------------------------------
@@ -1860,19 +2224,19 @@ function! s:highlight_keys(str)
\ '<Plug>', s:blue('<Plug>', 'SpecialKey'), 'g')
endfunction
function! s:key_sink(line)
function! s:key_sink(gv, cnt, reg, op, line)
let key = matchstr(a:line, '^\S*')
redraw
call feedkeys(s:map_gv.s:map_cnt.s:map_reg, 'n')
call feedkeys(s:map_op.
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 s:map_gv = a:mode == 'x' ? 'gv' : ''
let s:map_cnt = v:count == 0 ? '' : v:count
let s:map_reg = empty(v:register) ? '' : ('"'.v:register)
let s:map_op = a:mode == 'o' ? v:operator : ''
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'
@@ -1900,7 +2264,7 @@ function! fzf#vim#maps(mode, ...)
let pcolor = a:mode == 'x' ? 9 : a:mode == 'o' ? 10 : 12
return s:fzf('maps', {
\ 'source': colored,
\ 'sink': s:function('s:key_sink'),
\ '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
@@ -1916,8 +2280,8 @@ endfunction
function! s:complete_trigger()
let opts = copy(s:opts)
call s:prepend_opts(opts, ['+m', '-q', s:query])
let opts['sink*'] = s:function('s:complete_insert')
let s:reducer = s:pluck(opts, 'reducer', s:function('s:first_line'))
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
@@ -1926,21 +2290,21 @@ function! s:first_line(lines)
return a:lines[0]
endfunction
function! s:complete_insert(lines)
function! s:complete_insert(query, eol, Reducer, lines)
if empty(a:lines)
return
endif
let chars = strchars(s:query)
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(s:reducer, [a:lines])
let data = call(a:Reducer, [a:lines])
let ve = &ve
set ve=
execute 'normal!' ((s:eol || empty(chars)) ? '' : 'h').del.(s:eol ? 'a': 'i').data
execute 'normal!' ((a:eol || empty(chars)) ? '' : 'h').del.(a:eol ? 'a': 'i').data
let &ve = ve
if mode() =~ 't'
call feedkeys('a', 'n')
+72 -31
View File
@@ -28,6 +28,9 @@ function! s:warn(message)
return 0
endfunction
" Channels in flight, keyed by fifo path, so concurrent fzf runs do not clash
let s:channels = {}
function! fzf#vim#ipc#start(Callback)
if !exists('*job_start') && !exists('*jobstart')
call s:warn('job_start/jobstart function not supported')
@@ -39,57 +42,95 @@ function! fzf#vim#ipc#start(Callback)
return ''
endif
call fzf#vim#ipc#stop()
let g:fzf_ipc = { 'fifo': tempname(), 'callback': a:Callback }
if !filereadable(g:fzf_ipc.fifo)
call system('mkfifo '..shellescape(g:fzf_ipc.fifo))
if v:shell_error
let fifo = tempname()
call system('mkfifo '..shellescape(fifo))
if v:shell_error
if !exists('s:mkfifo_failed')
let s:mkfifo_failed = 1
call s:warn('Failed to create fifo')
endif
return ''
endif
call fzf#vim#ipc#restart()
let s:channels[fifo] = { 'fifo': fifo, 'callback': a:Callback }
call fzf#vim#ipc#restart(fifo)
if !s:running(s:channels[fifo].job)
call fzf#vim#ipc#stop(fifo)
return ''
endif
return g:fzf_ipc.fifo
return fifo
endfunction
function! fzf#vim#ipc#restart()
if !exists('g:fzf_ipc')
" Nvim hands over everything one read produced, which can be several messages
" as well as the trailing empty element. Vim's out_cb passes one line at a
" time, so deliver line by line and both behave the same.
function! s:deliver(Callback, lines)
for line in a:lines
if !empty(line)
call call(a:Callback, [line])
endif
endfor
endfunction
" The reader is gone, so leaving the channel registered would block the next
" write on a fifo nothing reads
function! s:drop(fifo)
if has_key(s:channels, a:fifo)
call delete(remove(s:channels, a:fifo).fifo)
endif
endfunction
function! fzf#vim#ipc#restart(fifo)
if !has_key(s:channels, a:fifo)
throw 'fzf#vim#ipc not started'
endif
let Callback = g:fzf_ipc.callback
let chan = s:channels[a:fifo]
let Callback = chan.callback
let fifo = a:fifo
if exists('*job_start')
let g:fzf_ipc.job = job_start(
\ ['cat', g:fzf_ipc.fifo],
\ {'out_cb': { _, msg -> call(Callback, [msg]) },
\ 'exit_cb': { _, status -> status == 0 ? fzf#vim#ipc#restart() : '' }}
let chan.job = job_start(
\ ['cat', fifo],
\ {'out_cb': { _, msg -> has_key(s:channels, fifo) ? call(Callback, [msg]) : '' },
\ 'exit_cb': { _, status -> status == 0 && has_key(s:channels, fifo) ? fzf#vim#ipc#restart(fifo) : s:drop(fifo) }}
\ )
else
let eof = ['']
let g:fzf_ipc.job = jobstart(
\ ['cat', g:fzf_ipc.fifo],
let chan.job = jobstart(
\ ['cat', fifo],
\ {'stdout_buffered': 1,
\ 'on_stdout': { j, msg, e -> msg != eof ? call(Callback, msg) : '' },
\ 'on_exit': { j, status, e -> status == 0 ? fzf#vim#ipc#restart() : '' }}
\ 'on_stdout': { j, msg, e -> has_key(s:channels, fifo) ? s:deliver(Callback, msg) : '' },
\ 'on_exit': { j, status, e -> status == 0 && has_key(s:channels, fifo) ? fzf#vim#ipc#restart(fifo) : s:drop(fifo) }}
\ )
endif
endfunction
function! fzf#vim#ipc#stop()
if !exists('g:fzf_ipc')
" Whether the job handle refers to a live job. Nvim returns -1 or 0 instead of
" a handle when it cannot start one
function! s:running(job)
if exists('*job_status')
return job_status(a:job) ==# 'run'
endif
return a:job > 0
endfunction
function! fzf#vim#ipc#stop(fifo)
if !has_key(s:channels, a:fifo)
return
endif
let job = g:fzf_ipc.job
if exists('*job_stop')
call job_stop(job)
else
call jobstop(job)
call jobwait([job])
endif
" Drop it first, so the exit handler does not restart the job
let chan = remove(s:channels, a:fifo)
" Never throw from here, this runs as part of fzf's exit handling
try
if exists('*job_stop')
call job_stop(chan.job)
else
call jobstop(chan.job)
call jobwait([chan.job])
endif
catch
endtry
call delete(g:fzf_ipc.fifo)
unlet g:fzf_ipc
call delete(chan.fifo)
endfunction
+27
View File
@@ -155,6 +155,13 @@ COMMANDS *fzf-vim-commands*
command: file path (Files, GFiles, Buffers, History, Locate), line content
(Lines, BLines, Marks, Changes), matched text (Rg, Ag, Grep), tag name
(Tags, BTags), or commit hash (Commits, BCommits)
- Most commands support CTRL-O to open the current entry in the window fzf
was started from, leaving fzf open, so you can look through several
entries in a single run (Files, GFiles, Buffers, History, Locate, Rg, Ag,
Grep, Lines, BLines, Tags, BTags, Marks, Changes). Requires fzf 0.74.4 or
later, and is not offered in fullscreen mode, with a layout that leaves no
other window (enew, tabnew), on Windows, or when the key is already used
by g:fzf_vim.paste_key or g:fzf_action
- Bang-versions of the commands (e.g. `Ag!`) will open fzf in fullscreen
- You can set `g:fzf_vim.command_prefix` to give the same prefix to the commands
- e.g. `let g:fzf_vim.command_prefix = 'Fzf'` and you have `FzfFiles`, etc.
@@ -241,6 +248,26 @@ buffer instead of opening them. Customize it with `g:fzf_vim.paste_key`.
let g:fzf_vim.paste_key = 'alt-enter'
<
Show key~
*fzf-vim-show-key*
*g:fzf_vim.show_key*
Most commands support a key that opens the current entry in the window fzf was
started from and leaves fzf open, so you can look through several entries in
one run. It is labelled Show in the footer to set it apart from Enter, which
opens the entry and closes fzf. Customize it with `g:fzf_vim.show_key`, or set
it to an empty string to turn the key off. A comma separated list binds every
key in it. It takes the key over from `$FZF_DEFAULT_OPTS`, so pick another key
or turn it off to keep a binding of your own.
>
" Key to open the current entry, leaving fzf open
" (default: 'ctrl-o')
let g:fzf_vim.show_key = 'ctrl-o'
" Use CTRL-O and double-click
let g:fzf_vim.show_key = 'ctrl-o,double-click'
<
Command-level options~
*fzf-vim-command-level-options*