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:
Junegunn Choi
2026-05-31 23:24:35 +09:00
parent e5b53de3d3
commit c2b97c62b9
3 changed files with 183 additions and 5 deletions
+17
View File
@@ -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
View File
@@ -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
View File
@@ -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*