10 Commits
Author SHA1 Message Date
Junegunn Choi 5a9f08fb36 Show a directory entry
:Locate returns directories and its sink opens them, but the readable-file
guard made the key silently do nothing for one.
2026-08-27 00:02:54 +09:00
Junegunn Choi a92f1cce23 Reuse s:edit_cmds for a tag in the file already shown
The tag branch edited the file unconditionally, unlike every other kind, so
showing a tag in the window's own buffer reloaded it and reset the undo
history. s:tags_sink does not re-edit either.
2026-08-27 00:02:54 +09:00
Junegunn Choi 05b2324b75 Canonicalize the two fzf key aliases before comparing
fzf reads 'return' as 'enter' and 'alt-return' as 'alt-enter', so
show_key='alt-return' passed the check against the default paste key and
--expect closed fzf while the footer advertised Show. ctrl-m is not an alias,
it goes through the generic ctrl- branch to its own event.
2026-08-27 00:01:26 +09:00
Junegunn Choi c63f25a2ba Document that a command's own key withholds the show key
Missed when the check learned about them. Also hyphenate 'comma-separated'.
2026-08-26 23:40:32 +09:00
Junegunn Choi e541ce6037 Keep a newline in an entry from splitting the fifo payload
The fifo is read a line at a time, but ':GFiles' reads NUL separated names
from 'git ls-files -z', so an entry can hold a newline. It arrived as two
callbacks, and a fragment that named another tracked file opened that one
instead.

The binding swaps the newline for \1 and s:show_entry puts it back, so a
multi-line entry stays one payload.
2026-08-26 23:28:07 +09:00
Junegunn Choi cb6ea5e06f Prefer a relative entry over tilde expansion in s:in_dir
Only :History reports a path as '~/...', but every command went through the
same branch, so an entry under a directory literally named '~' resolved to
the home directory and Show opened the wrong file or nothing. Resolve
against the run directory first and fall back to the home reading.
2026-08-26 22:47:46 +09:00
Junegunn Choi 4ae5a659f8 Fold g:fzf_action keys before the show key check
fzf#wrap turns them into --expect, which fzf matches with the same case
folding as a binding, so g:fzf_action = {'CTRL-O': 'split'} slipped past the
check. The footer advertised Show while --expect closed fzf instead.
2026-08-26 22:46:09 +09:00
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
Junegunn Choi 091f9c92f9 Withhold the show key from a key the command binds
Setting show_key to a key a command binds for its own hint, ctrl-alt-x on
Buffers or alt-a on Rg, advertised both in the footer while the command's
binding won by coming later in the options. '_hint' entries now carry the
fzf key names behind the label, which is what the check compares, since a
label like 'A-a/d' cannot be.
2026-08-26 20:52:47 +09:00
Junegunn Choi a246483a09 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.
2026-08-26 18:21:17 +09:00
4 changed files with 592 additions and 100 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`,
`g:fzf_action`, or the command itself, as `CTRL-ALT-X` is in `:Buffers`
- 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
+465 -69
View File
@@ -352,21 +352,27 @@ 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
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
endif
for [key, action] in entries
call add(keys, s:magenta(key, 'Special').' '.action)
for entry in entries
call add(keys, s:magenta(entry[0], 'Special').' '.entry[1])
endfor
return join(keys, ' ')
endfunction
@@ -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,49 @@ 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
" fzf spells these two events more than one way
let s:key_aliases = {'return': 'enter', 'alt-return': 'alt-enter'}
" 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
call map(keys, 'strchars(v:val) == 1 ? v:val
\ : v:val =~? "^alt-.$" ? "alt-".v:val[4:] : tolower(v:val)')
return map(keys, 'get(s:key_aliases, v:val, 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 +486,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 +613,300 @@ 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
if empty(a:dir) || a:path =~# '^/' || a:path =~# '^\a:[\\/]'
return a:path
endif
let path = fnamemodify(expand(escape(a:dir, '[]*?{}')), ':p').a:path
" :History reports a path under the home directory as '~/...', which
" filereadable does not expand. Other commands can emit a relative path
" under a directory named '~', so prefer that reading when it exists
if a:path =~# '^\~' && !filereadable(path)
return fnamemodify(a:path, ':p')
endif
return 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
" Undo the newline escaping the binding applies
let entry = substitute(a:entry, nr2char(1), "\n", 'g')
if empty(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, 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 = s:edit_cmds(a:winid, 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'
" A multi-line match arrives whole, and s:ag_to_qf reads the first line
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)
" :Locate and a custom source can return a directory, which the sink opens
if !filereadable(path) && !isdirectory(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
" g:fzf_action keys go through fzf as --expect, so they need the same
" folding as the show key before they can be compared
let actions = map(keys(get(g:, 'fzf_action', s:default_action)),
\ 'get(s:split_keys(v:val), 0, v:val)')
let paste = s:split_keys(s:paste_key())
for key in s:split_keys(a:key)
if index(actions, key) >= 0 || 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] }
" The fifo is read a line at a time, but an entry can contain a newline
" where the source is NUL separated, as ':GFiles' is. Swap it for a
" byte no path is going to hold and put it back in s:show_entry
call s:prepend_opts(a:spec, ['--bind', key.':execute-silent:{ printf ''%s'' {} | tr ''\n'' ''\1''; echo; } > '.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 +922,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 +1002,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 +1049,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 +1059,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 +1081,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 +1111,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 +1190,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 +1242,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 +1275,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 +1309,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 +1323,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 +1369,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', ['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
@@ -1128,6 +1504,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', ['alt-a', 'alt-d']]]
function! opts.sink(lines) closure
return s:ag_handler(get(opts, 'name', name), a:lines)
endfunction
@@ -1174,6 +1552,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', ['alt-a', 'alt-d']]]
function! opts.sink(lines) closure
return s:ag_handler(name, a:lines)
endfunction
@@ -1243,6 +1623,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 +1734,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 +1893,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 +1936,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 +1952,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 +1972,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 +1984,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 +2013,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 +2256,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 +2296,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 +2312,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 +2322,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
+28
View File
@@ -155,6 +155,14 @@ 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, g:fzf_action, or the command itself, as
CTRL-ALT-X is in :Buffers
- 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 +249,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*