last updated: November 08, 2022
6 minute read
Global Find and Replace in Neovim
Of all the VSCode features I've re-implemented in Neovim, none have given me as much trouble as the global find and replace. In VSCode, the global-search ui is built into the sidebar with options to include/exclude files, and search by whole word, case sensitive, case insensitive, and any combination of the three. Once you've narrowed down your search, you can easily replace all instances in the same ui. For renaming across multiple files, it's a lifesaver. Luckily, with a bit of a work, and a little flexibility, we can get an analogous solution working in Neovim.
tl;dr
Find your first-pass results with fzf-lua/telescope, populate them into the quickfix list, narrow it down with bqf, and replace all instances by executing cdo s/find/replace
with a custom user command to accept arguments for find
and replace
.
Search with fzf
fzf is an awesome command line utility for fuzzy finding just about anything (seriously, anything). In our case, we'll be using the fzf-lua
package for Neovim, and focusing on just one specific command: grep
grep
allows you to input a search term and narrow down the results "fuzzily" by filename. I use the following key remaps, but feel free to use your own:
nnoremap <leader>zf <cmd>lua require('fzf-lua').grep()<cr>
If you prefer to use telescope, you can use the following remap:
nnoremap <leader>zf <cmd>lua require('telescope.builtin').grep_string({ search = vim.fn.input("Grep For > ")})<cr>
Once you trigger the command, you'll be greeted with a prompt to input your initial search term:
Enter the search term (in our case, let's look for the string fzf
) and a window will appear with the filenames of every file containing your string.
You can then narrow down your results by inputting a part of the filename, or negating a filename if you begin with a shebang i.e. !.md
. See the fzf docs for more options.
In our case, we negated any files with packer
in the filepath. We can then export a subset of the files to the quickfix list by marking a result with tab ...
... and pressing enter.
For telescope
, you need to do tweak the settings a bit:
local telescope = require("telescope")local actions = require("telescope.actions")local action_state = require("telescope.actions.state")local custom_actions = {}function custom_actions.fzf_multi_select(prompt_bufnr)local function get_table_size(t)local count = 0for _ in pairs(t) docount = count + 1endreturn countendlocal picker = action_state.get_current_picker(prompt_bufnr)local num_selections = get_table_size(picker:get_multi_selection())if num_selections > 1 thenactions.send_selected_to_qflist(prompt_bufnr)actions.open_qflist()elseactions.file_edit(prompt_bufnr)endendtelescope.setup({defaults = {mappings = {n = {["<cr>"] = custom_actions.fzf_multi_select,},},},})
This will allow you to mark a selection with tab
/shift-tab
and export the selections to the quickfix list with enter
.
Back to our example, let's mark all the instances where we remap an fzf command. This will populate the quickfix list, which should look something like the following:
Narrow Down with bqf
The second plugin we'll use is bqf, an awesome utility for the quickfix list. I won't dive into all the details, but some of the highlights are:
- create another list by marking an entry with tab and pressing
zn
. - navigate between lists with
>
and<
zf
to fuzzy search within the current list- navigate by file (not result) with
ctrl-p
andctrl-n
- scroll in the preview window with
ctrl-b
andctrl-f
Note that to use zf
, you'll need to install fzf.vim.
I also use some bqf-agnostic quickfix list remaps that may be helpful:
nnoremap gn :cnext<cr>nnoremap gp :cprevious<cr>nnoremap ge :copen<cr>nnoremap gq :cclose<cr>
Between creating lists, fuzzy searching within a list, and repeating, you should have enough tools to narrow down your search to a final, single list. This brings us to the final section: replacing.
Replace with cdo
The key to this last step is the native Neovim cdo
command. If we enter the command :help cdo
in Neovim, we learn the following info:
In practice, this means we need to narrow our search to a single quickfix list (i.e. with zn
and zf
) so we can replace its every instance.
For the cmd
, we'll be using the substitute
command. Although there's a whole boatload of options we can use, the defaults are generally good enough: s/foo/bar/
will replace a single instance of foo
with bar
.
Some common substitute
options include the /g
flag (i.e. s/foo/bar/g
) which replaces all the instances in the current line, the %s
flag (i.e. %s/foo/bar
) to replace all the instances in all lines, the /i
flag (i.e. s/foo/bar/i
) for case-insensitive replacements, and the /I
flag for case-sensitive. Check out this great guide for more examples.
Luckily, since cdo
applies its command argument to every instance in the quickfix list, we need neither the /g
flag nor the %s
flag – I'll leave it up to you to decide on the /i
and /I
flags.
Custom User Command
Technically, you could stop here and you'd be good to go – just use the :cdo s/foo/bar/
command as above. If you're like me, however, this can feel a bit unweildy. What about creating a key remap to interpolate the foo
and bar
? While it's not possible for a Neovim remap to accept arguments, a user command can!
Our custom user command will accept two arguments, a foo
and a bar
, and execute a Neovim command that interpolates the values into a string of the form :cdo s/foo/bar/
and saves each file. Let's look at the following code:
vim.api.nvim_create_user_command("FindAndReplace", function(opts)vim.api.nvim_command(string.format("cdo s/%s/%s", opts.fargs[1], opts.fargs[2]))vim.api.nvim_command("cfdo update")end, { nargs = "*" })vim.api.nvim_set_keymap("n","<leader>ir",":FindAndReplace ",{ noremap = true }))
First, the nargs = "*"
indicates that our user command can take any number of arguments. The arguments are passed through opts.fargs
as entries in a dictionary. We can interpolate the values with string.format
(remember that Lua is 1-indexed!), and execute the command with nvim_command
. We run the cfdo update
command to save each file in the quickfix list, and finally set a key-mapping.
If you'd like to pass an argument with a space, just escape the white space like so:
:FindAndReplace foo hello\ there
and the second argument will be passed as hello there
.
One last note: if you regret your find and replace, don't panic! You can easily undo your changes with :cdo undo
.
you might also like:
Five Interesting Things From my Neovim Config
October 16, 2022
A few interesting tidbits I've picked up from tinkering with my config