mirror of
https://github.com/junegunn/fzf.vim.git
synced 2026-06-16 04:26:28 +08:00
Add ALT-ENTER to paste selected items into the buffer
- New fzf#vim#paste inserts selections instead of opening them - First item after cursor (space-padded), rest on following lines - Leading whitespace stripped only when pasting after text - Commands opt in via `_paste`; key configurable with g:fzf_vim.paste_key - Skip the key when buffer is not &modifiable - Per command, paste: - path: Files, GFiles, GFiles?, Buffers, History, Locate - line text: Lines, BLines, Marks, Changes - matched text: Rg, Ag, Grep - tag name: Tags, BTags - commit hash: Commits, BCommits
This commit is contained in:
@@ -96,6 +96,12 @@ Commands
|
||||
|
||||
- Most commands support `CTRL-T` / `CTRL-X` / `CTRL-V` key
|
||||
bindings to open in a new tab, a new split, or in a new vertical split
|
||||
- Most commands support `ALT-ENTER` to insert the selected items into the
|
||||
current buffer instead of opening them. The inserted text depends on the
|
||||
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`)
|
||||
- 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.
|
||||
@@ -156,6 +162,17 @@ let g:fzf_vim.preview_window = []
|
||||
" let g:fzf_vim.preview_bash = 'C:\Git\bin\bash.exe'
|
||||
```
|
||||
|
||||
#### Paste key
|
||||
|
||||
Most commands support a key to insert the selected items into the current
|
||||
buffer instead of opening them (see the command list above for what each
|
||||
command inserts). Customize it with `g:fzf_vim.paste_key`.
|
||||
|
||||
```vim
|
||||
" Key to insert the selected items into the current buffer (default: 'alt-enter')
|
||||
let g:fzf_vim.paste_key = 'alt-enter'
|
||||
```
|
||||
|
||||
#### Command-level options
|
||||
|
||||
```vim
|
||||
|
||||
+148
-4
@@ -233,17 +233,54 @@ function! s:reverse_list(opts)
|
||||
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*')
|
||||
let wrapped = fzf#wrap(a:name, opts, a:bang)
|
||||
" 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
|
||||
@@ -352,6 +389,54 @@ let s:default_action = {
|
||||
\ '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
|
||||
|
||||
" 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
|
||||
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
|
||||
@@ -460,6 +545,7 @@ function! fzf#vim#files(dir, ...)
|
||||
endif
|
||||
|
||||
let args.options = ['--scheme', 'path', '-m', '--prompt', strwidth(dir) < &columns / 2 - 20 ? dir : '> ']
|
||||
let args._paste = 1
|
||||
call s:merge_opts(args, s:conf('files_options', []))
|
||||
return s:fzf('files', args, a:000)
|
||||
endfunction
|
||||
@@ -478,6 +564,10 @@ function! s:line_handler(lines)
|
||||
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')
|
||||
@@ -534,6 +624,7 @@ function! fzf#vim#lines(...)
|
||||
return s:fzf('lines', {
|
||||
\ 'source': lines,
|
||||
\ 'sink*': s:function('s:line_handler'),
|
||||
\ '_paste': 1,
|
||||
\ 'options': s:reverse_list(['--tiebreak=index', '--prompt', 'Lines> ', '--ansi', '--extended', '--nth='.nth.'..', '--tabstop=1', '--query', query, '--multi'])
|
||||
\}, args)
|
||||
endfunction
|
||||
@@ -552,6 +643,9 @@ function! s:buffer_line_handler(lines)
|
||||
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]
|
||||
@@ -575,6 +669,7 @@ function! fzf#vim#buffer_lines(...)
|
||||
return s:fzf('blines', {
|
||||
\ 'source': s:buffer_lines(query),
|
||||
\ 'sink*': s:function('s:buffer_line_handler'),
|
||||
\ '_paste': 1,
|
||||
\ 'options': s:reverse_list(['+m', '--tiebreak=index', '--multi', '--prompt', 'BLines> ', '--ansi', '--extended', '--nth=2..', '--tabstop=1'])
|
||||
\}, args)
|
||||
endfunction
|
||||
@@ -630,6 +725,7 @@ endfunction
|
||||
function! fzf#vim#locate(query, ...)
|
||||
return s:fzf('locate', {
|
||||
\ 'source': 'locate '.a:query,
|
||||
\ '_paste': 1,
|
||||
\ 'options': '-m --prompt "Locate> "'
|
||||
\}, a:000)
|
||||
endfunction
|
||||
@@ -705,6 +801,7 @@ endfunction
|
||||
function! fzf#vim#history(...)
|
||||
return s:fzf('history-files', {
|
||||
\ 'source': fzf#vim#_recent_files(),
|
||||
\ '_paste': 1,
|
||||
\ 'options': ['-m', '--header-lines', !empty(expand('%')), '--prompt', 'Hist> ']
|
||||
\}, a:000)
|
||||
endfunction
|
||||
@@ -754,6 +851,7 @@ function! fzf#vim#gitfiles(args, ...)
|
||||
return s:fzf('gfiles', {
|
||||
\ 'source': source,
|
||||
\ 'dir': root,
|
||||
\ '_paste': 1,
|
||||
\ 'options': '--scheme path -m --read0 --prompt "GitFiles> "'
|
||||
\}, a:000)
|
||||
endif
|
||||
@@ -769,7 +867,11 @@ function! fzf#vim#gitfiles(args, ...)
|
||||
\ ? 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))
|
||||
let wrapped = fzf#wrap({
|
||||
" 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]
|
||||
@@ -811,6 +913,10 @@ 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(matchstr(v:val, '\\[\\zs[0-9]*\\ze\\]'))), ':~:.')"))
|
||||
endif
|
||||
let b = matchstr(a:lines[1], '\[\zs[0-9]*\ze\]')
|
||||
if empty(a:lines[0]) && s:conf('buffers_jump', 0)
|
||||
let [t, w] = s:find_open_window(b)
|
||||
@@ -883,6 +989,7 @@ function! fzf#vim#buffers(...)
|
||||
\ 'source': map(sorted, 'fzf#vim#_format_buffer(v:val)'),
|
||||
\ 'sink*': s:function('s:bufopen'),
|
||||
\ 'exit': s:function('s:buffers_exit'),
|
||||
\ '_paste': 1,
|
||||
\ 'options': options
|
||||
\}
|
||||
return s:fzf('buffers', spec, args)
|
||||
@@ -924,6 +1031,10 @@ function! s:ag_handler(name, lines)
|
||||
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
|
||||
@@ -999,6 +1110,7 @@ function! fzf#vim#grep(grep_command, ...)
|
||||
call remove(args, 0)
|
||||
endif
|
||||
|
||||
let opts._paste = 1
|
||||
function! opts.sink(lines) closure
|
||||
return s:ag_handler(get(opts, 'name', name), a:lines)
|
||||
endfunction
|
||||
@@ -1044,6 +1156,7 @@ function! fzf#vim#grep2(command_prefix, query, ...)
|
||||
if len(args) && type(args[0]) == s:TYPE.bool
|
||||
call remove(args, 0)
|
||||
endif
|
||||
let opts._paste = 1
|
||||
function! opts.sink(lines) closure
|
||||
return s:ag_handler(name, a:lines)
|
||||
endfunction
|
||||
@@ -1077,6 +1190,9 @@ 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:]
|
||||
@@ -1110,6 +1226,7 @@ function! fzf#vim#buffer_tags(query, ...)
|
||||
return s:fzf('btags', {
|
||||
\ 'source': s:btags_source(tag_cmds),
|
||||
\ 'sink*': s:function('s:btags_sink'),
|
||||
\ '_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)
|
||||
@@ -1124,6 +1241,10 @@ function! s:tags_sink(lines)
|
||||
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()
|
||||
@@ -1214,6 +1335,7 @@ 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'),
|
||||
\ '_paste': 1,
|
||||
\ 'options': extend(opts, ['--nth', '1..2', '-m', '-d', '\t', '--tiebreak=begin', '--prompt', 'Tags> ', '--query', a:query])}, a:000)
|
||||
endfunction
|
||||
|
||||
@@ -1325,6 +1447,11 @@ function! s:changes_sink(lines)
|
||||
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]
|
||||
|
||||
@@ -1365,6 +1492,7 @@ function! fzf#vim#changes(...)
|
||||
return s:fzf('changes', {
|
||||
\ 'source': all_changes,
|
||||
\ 'sink*': s:function('s:changes_sink'),
|
||||
\ '_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 --inline-info --no-separator', cursor)}, a:000)
|
||||
endfunction
|
||||
|
||||
@@ -1379,6 +1507,10 @@ 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
|
||||
@@ -1401,6 +1533,7 @@ 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'),
|
||||
\ '_paste': 1,
|
||||
\ 'options': '+m -x --ansi --tiebreak=index --header-lines 1 --tiebreak=begin --prompt "Marks> " --list-border --header-border inline --inline-info --no-separator'}, extra)
|
||||
endfunction
|
||||
|
||||
@@ -1555,6 +1688,10 @@ function! s:commits_sink(lines)
|
||||
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 : ''
|
||||
@@ -1616,7 +1753,14 @@ function! s:commits(range, buffer_local, args)
|
||||
let command = 'Commits'
|
||||
endif
|
||||
|
||||
let expect_keys = join(keys(get(g:, 'fzf_action', s:default_action)), ',')
|
||||
" 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 options = {
|
||||
\ 'source': source,
|
||||
\ 'sink*': s:function('s:commits_sink'),
|
||||
@@ -1624,7 +1768,7 @@ function! s:commits(range, buffer_local, args)
|
||||
\ '--inline-info', '--prompt', command.'> ', '--bind=ctrl-s:toggle-sort',
|
||||
\ '--header-border=horizontal', '--no-separator',
|
||||
\ '--header', '• Press '.s:magenta('CTRL-S', 'Special').' to toggle sort, '.s:magenta('CTRL-Y', 'Special').' to yank commit hashes',
|
||||
\ '--expect=ctrl-y,'.expect_keys])
|
||||
\ '--expect=ctrl-y,'.join(expect_keys, ',')])
|
||||
\ }
|
||||
|
||||
if a:buffer_local
|
||||
|
||||
+18
-1
@@ -1,4 +1,4 @@
|
||||
fzf-vim.txt fzf-vim Last change: June 8 2025
|
||||
fzf-vim.txt fzf-vim Last change: May 31 2026
|
||||
FZF-VIM - TABLE OF CONTENTS *fzf-vim* *fzf-vim-toc*
|
||||
==============================================================================
|
||||
|
||||
@@ -150,6 +150,11 @@ COMMANDS *fzf-vim-commands*
|
||||
|
||||
- Most commands support CTRL-T / CTRL-X / CTRL-V key bindings to open in a new
|
||||
tab, a new split, or in a new vertical split
|
||||
- Most commands support ALT-ENTER to insert the selected items into the
|
||||
current buffer instead of opening them. The inserted text depends on the
|
||||
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)
|
||||
- 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.
|
||||
@@ -224,6 +229,18 @@ behavior with `g:fzf_vim.preview_window`. Here are some examples:
|
||||
" let g:fzf_vim.preview_bash = 'C:\Git\bin\bash.exe'
|
||||
<
|
||||
|
||||
Paste key~
|
||||
*fzf-vim-paste-key*
|
||||
*g:fzf_vim.paste_key*
|
||||
|
||||
Most commands support a key to insert the selected items into the current
|
||||
buffer instead of opening them. Customize it with `g:fzf_vim.paste_key`.
|
||||
>
|
||||
" Key to insert the selected items into the current buffer
|
||||
" (default: 'alt-enter')
|
||||
let g:fzf_vim.paste_key = 'alt-enter'
|
||||
<
|
||||
|
||||
Command-level options~
|
||||
*fzf-vim-command-level-options*
|
||||
|
||||
|
||||
Reference in New Issue
Block a user