Project-specific Vim FZF Behavior
The fzf.vim project (by the same author of fzf itself) includes a set of useful starting points for how to interact with fzf in Vim. Using the bootstrap of fzf.vim and learning the power of ripgrep (rg) allowed me to reduce the productivity decrease I experienced coming from VSCode. However, I found that like most things in Vim, any out-of-the-box configuration is about 90% complete for my usecase.
The main issue I experienced was the fact that I needed project-specific configuration of how I interact with the underlying search tools, specifically rg
. Here’s an example issue I faced:
rg <search> .
is the basic behavior of the:Rg
command provided by fzf.vim. For example,:Rg test
amounts to runningrg test .
. (with some default flags omitted here for clarity)- The default behavior of
rg
is that it does not follow symlinks. It also does not look at files which are ignored for a few reasons (.gitignore
,.ignore
, etc). The files I need to search are through symlinks to directories out of this project, and they are also in.gitignore
. - I can’t change the
.gitignore
for the project, since it’s important that the files I’m searching are not checked in. - All of the above constraints are specific to the project I’m working on, and I don’t want to include extra paths to search in other projects where they won’t exist.
The end result was that, for a specific project, I need to be able to execute:
rg --follow <search> . <path-to-gitignored-dir>
With additional arguments in various parts of the command, it was obvious I would need to override the default behavior of :Rg
.
Customizing :Rg
You can override the command that is executed by :Rg
to provide any customiziation of behavior you may need. With help from the documentation, I was able to come to this solution for passing additional arguments to rg
:
function! RgWrapper(query, ...)
let l:escaped_query = shellescape(a:query)
let l:command = 'rg --column --line-number --no-heading --color=always --smart-case ' . $VIM_RG_OPTIONS .
\ ' ' . l:escaped_query .
\ ' . ' . $VIM_RG_PATHS
let l:preview_cmd = "rg --pretty --context 10 --color=always {1} || bat -f {1}"
let l:options = {
\ 'options': '--delimiter : --nth 2.. --preview '. shellescape(l:preview_cmd),
\ 'down': '40%'
\ }
if a:0 > 0 && a:1 ==# '1' " Check for bang
call fzf#vim#grep(l:command, 1, l:options, 0)
else
call fzf#vim#grep(l:command, 0, l:options, 0)
endif
endfunction
command! -nargs=* -bang Rg call RgWrapper(<q-args>, <bang>0)
Brekaing this down for the important lines:
- Line 3: The custom command uses two env variables,
$VIM_RG_OPTIONS
and$VIM_RG_PATHS
. If these are unset, they will be an empty string and not break the behavior of the command. Otherwise, you can pass in any options or additional paths to search by setting these variables. - Line 6: This sets the preview command so that you can see the highlighted section of the file where the result was found. Note that I am piping to
bat
(ref) which can syntax highlight the file and pass back forced colors with the-f
flag. - The remainder of the lines are copied verbatim from the examples on the fzf.vim library for overriding the command.
Project-specific env variables
Now that we can drive the customization, we want a way to set these env variables for a specific project that is easy to set and forget. My approach for this is direnv. Here are the few steps I took to set it up for my usecase:
- I have direnv installed and loaded (check the documentation on setup).
- I have put
.envrc
in my.gitignore
for my home directory. Here’s a quick one-liner for this, if you have the file already created:echo '.envrc' >> ~/.gitignore
. The reason I did this is that, I don’t want to force every project I work on to include this in its own.gitignore
. - I create a
.envrc
file in my project that looks like:
export VIM_RG_OPTIONS='--follow'
export VIM_RG_PATHS='<path>'
Replace the values with your desired options and paths for the project.
Conclusion
What started as a head-scratcher lead me to better understand some powerful unix tools. I hope this helps anyone interested in a similar workflow, and please reach out if you find that there’s a better approach to this.