Written July 17, 2012. Tagged Vim.
In my blog post about switching to Vim, I mentioned that I had yet to find a project-wide search-and-replace solution that I like.
Now I have: I use git-grep-vim for project-wide search, then vim-qargs to run a vim search-and-replace command over every result file.
I think it's unlikely that Vim will have a solution any time soon fully as convenient and well-integrated as in more modern editors. The per-buffer find-and-replace in Vim is dusted with enough special Vim magic that it won't match up perfectly with the syntax of project-wide search tools like ack, grep or git-grep. And in a large project, you need fast tools like those.
So while I don't know of a solution that is on par with the convenience of search-and-replace in other editors, I do consider this one more convenient than switching to another editor β for a while I used TextMate alongside Vim for project-wide find-and-replace.
And as usual with Vim, you get a lot of power you may not find in those other editors.
I use my fork of git-grep-vim for project-wide search in Vim.
It's based on ack.vim but uses git-grep instead of ack.
You search like this:
:GitGrep some.*pattern
You can optionally specify one or several directories to limit the search to:
:GitGrep some.*pattern app/models lib spec
And pass in flags (-w
makes it match only at word boundaries):
:GitGrep -w hello
Add the results of a new search to the existing result list:
:GitGrep hello
:GitGrepAdd goodbye
With multiple words or some special characters, quote the search string:
:GitGrep "hello there"
Or cheat to save some typing (the regex .
matches any character, including a space):
:GitGrep hello.there
To save some typing, this line in my ~/.vimrc
brings up the search prompt when I hit ,a
(the "a" was for "ack", once):
" We want the trailing space.
exe "nnoremap <leader>a :GitGrep "
As long as you're in a Git repository, I much prefer git-grep to ack. It's faster, and ack will ignore unknown filetypes (like Haml) unless reconfigured.
That said, git-grep can also mislead. It won't find results in new files before they've been added to Git, or in .gitignore
d files, and as mentioned, it only works in directories managed by Git. You can trade in some speed for removing these gotchas with the flags --untracked
, --no-exclude-standard
and --no-index
.
I use project-wide search all the time. It's not just for search-and-replace, of course.
The search results end up in the quickfix window, a split window in Vim that you can move through like any Vim buffer. Hitting β
will show the result from the current line.
There's a bunch of commands to open items from the quickfix list, without even needing to have it open (close it with :cclose
).
I've mapped the +
and -
keys to :cnext
and :cprevious
, for skipping one item at the time. I've mapped g+
and g-
to :cpfile
and :cnfile
, for skipping one file at the time.
Some of my favorite commands for the quickfix list are :colder
and :cnewer
, to revisit the previous or next item list. This means you can run one project search for "foo", notice some issue with bars, search for "bar", do stuff, then run :colder
and you're back in the "foo" search. It's effectively a stack of project searches to pop and push at leisure.
Vim has a couple of commands like :bufdo
and :windo
, which let you run a command in every open buffer or window. Drew Neil (of Vimcasts and Practical Vim) with other contributors wrote a :Qargdo for when you want to run a command against every file in the quickfix list.
My fork of vim-qargs uses the conveniently short :Qdo
for the same.
For project-wide search-and-replace, then, you would simply do something like this:
:GitGrep foo.*bar
:Qdo %s/foo.*bar/baz
You probably want to configure
" Automatically save changes before switching buffer with some
" commands, like :cnfile.
set autowrite
so each buffer is saved automatically when it moves to the next one. Otherwise, you could do:
:Qdo %s/foo.*bar/baz | update
It is, of course, a bit inconvenient to repeat the search string. But because the syntax of git-grep (or some other tool) will often diverge from the Vim search syntax, I've settled for typing the search string twice rather than having it automatically reused.
While I would prefer to only type the search string once in the common case, there is an upside to having separate steps for the global find and the local search-and-replace, and that's flexibility.
You have your choice of global search tool, with their different features and trade-offs. The raw speed of git-grep in a Git repository is a wonderful thing. With :GitGrepAdd
you can search for "x" in directory a plus "y" in directory b.
And on the other end, you get all the magic (or nomagic) of Vim. Though I don't think I've used it with :Qdo
yet, you could apply some complex macro and not just a substitution.
One thing I have done is chain multiple substitutions (replace "a" with "b" if present and then "c" with "d" if present), which I think would be more difficult with a more convenient but less flexible setup.
I'm curious to hear what others use. Let me know!