Most of the time I spend using FZF’s Vim plugin, I’m grepping for a line in a codebase so I can paste it into the file I’m working on.
I do this so much, that the process of opening the file containing a grep match, yanking the line, then closing the buffer again; started to grate.
I wanted to a macro for this. One that would let me yank directly from the results window with Ctrl-y
.
To do this, let’s learn about FZF’s --expect
option, and “sink functions”.
“Completing” FZF
When you’re looking at FZF’s results window in Vim, hitting Enter tells it “I’ve found what I’m after, take this filename and open it in a new buffer (or whatever the command does)”. In FZF terminology, pressing Enter “completes” the command.
But we can also hit Ctrl-v
to open a file in a split, and Ctrl-t
to open in a new tab.
All of these different keys that “complete” FZF are configured by the Vim plugin, and there’s nothing stopping us adding our own!
--expect
The first step is telling FZF which key presses we want it to complete with. We do this with FZF’s --expect
option.
Starting with custom Rg command example from the FZF’s Vim plugin’s docs, let’s add an --expect
argument.
command! -bang -nargs=* Rg
\ call fzf#vim#grep(
\ 'rg --column --line-number --no-heading --color=always --smart-case -- '.shellescape(<q-args>), 1,
\ fzf#vim#with_preview({ 'options': '--expect=ctrl-y' }), <bang>0)
By default, these --expect
keys will perform the default complete action. In this case, taking you to the file containing your search term.
Running this Rg
command, typing in a search term, then hitting Ctrl-y
, you’ll see FZF do just that!
The next step is getting FZF to run a custom completion function for our --expect
parameter.
Sink Functions
Sink functions are the things that process the results of FZF commands.
The sink function for the bulit-in Files
command handles opening the filename that we pick in the FZF window. Buffers
' sink function simply opens the buffer with the chosen name.
Now that we’ve added a new way to complete our Rg
command, we need a new sink function to handle it.
function! RgSink(sink_lines)
echo a:sink_lines
" ...
endfunction
command! -bang -nargs=* Rg
We use this sink*
property to tell FZF to use our custom sink function. At the moment, all it does is echo the the argument that FZF gives it.
💡 This config dictionary can include
sink
orsink*
— the first is for FZF commands that only have one completion key (Enter), the second is for multiple (Enter andCtrl-y
, in our case)
Let’s have a look at what’s echo
-ed. Running our command then hitting Ctrl-y
on any old result, I see this —
['ctrl-y', '.gitconfig:1:1:[color]']
Running it again and hitting Enter —
['', '.gitconfig:1:1:[color]']
The first item in this tuple is the completion key, and the second is the result itself.
So we just need a bit of Vimscript to parse this tuple and either add the line to the unnamed register (the default register for yanking, cutting, and pasting); or open the file.
function! RgSink(sink_lines)
let split_lines = split(a:sink_lines[1], ':')
let filename = split_lines[0]
let line_number = split_lines[1]
let line_content = split_lines[3]
if a:sink_lines[0] == 'ctrl-y'
" If completed with ctrl-y, yank the line content
let @" = line_content . "\n"
elseif len(a:sink_lines) > 2
" If multiple lines were selected using tab, open them into a quickfix window
cex a:sink_lines[1:]
copen
elseif a:sink_lines[0] == ''
" Else if completed with enter, open file
silent execute 'edit ' filename
silent execute ': ' line_number
endif
endfunction
That’s all we need!
That first elseif
is there to handle results with multiple lines, selected with tab. As far as I know, we can’t call the sink for the built-in Rg command, so we have to re-implement this stuff ourselves.
This sink doesn’t handle Ctrl-v
, Ctrl-x
, and Ctrl-t
like the built in commands do, but it wouldn’t be too much work to extend our solution to!