Find in project - TextMate vs. Vim

I spoke on this topic at the March meetup of the Melbourne Ruby group.

TextMate’s ‘Find in Project’ is an extremely useful feature. It presents search hits in a single window and lets you inspect and replace selected occurrences. It understands plain strings and regular expressions. It makes code exploration, renaming and other refactoring a breeze. Responsiveness is fine in small to medium-sized projects that exclude vendor code and log files, and TextMate 2 enhancements address the search performance issues seen in larger projects. There’s not much more to say about TextMate; it just works.

But how do you do it in Vim? When I switched over I was surprised to find no easy answer. Several Vim users told me they don’t need that feature very often and just do it manually. But renaming is an important part of refactoring. It should be quick and easy. So I kept looking, and eventually, solutions began to emerge.

In this post I’ll be using ack (see betterthangrep.com) as a replacement for grep and find. Most of what I’ll say remains relevant regardless of which you choose. If you go with ack, do consider custom configuration like mine, which includes text files and excludes log files by default.

Go to file

The first tip I got was that with external search results read into a buffer, files can be jumped into one by one to make changes across a project.

Say you want to replace ‘persisted’ with ‘recorded’ in the code of tire (an ElasticSearch client). You can start by running this Vim command:

:new | r ! ack persisted

Where :new creates a new buffer, | runs an additional Vim command, r reads into the buffer, ! executes a shell command and ack persisted actually performs the search. The buffer of ack results looks like this:

lib/tire/model/persistence/storage.rb:66:          def persisted?; !!id;           end
test/integration/active_record_searchable_test.rb:56:        assert         results.first.persisted?, "Record should be persisted"
test/models/active_model_article.rb:28:  def persisted?; true; end
test/models/active_model_article_with_callbacks.rb:30:  def persisted?
test/models/supermodel_article.rb:16:  alias :persisted? :exists?
test/unit/model_persistence_test.rb:262:            assert article.persisted?, "#{article.inspect} should be `persisted?`"

With your cursor over a filename, in normal mode, you can press gf to open the file (then :b# to return to the previous buffer). Even better than that, gF will open the relevant file and jump right to the specified line number.

Perform your first substitution with :s/persisted/replaced/g, then repeat it later with :s. Don’t forget to save the modified buffers as you go with :w (write) or all together at the end with :wa (write all).

The ack.vim plugin

Pulling in the output of a shell command is a great general-purpose trick, but when it comes to ack you can save yourself some typing by installing the ack.vim plugin.

This gives you the :Ack command, which opens ack search results in the the quickfix window. Our search command becomes:

:Ack persisted

From the quickfix window, ack.vim lets you open a match with o or enter, or preview a match (keeping the cursor in the quickfix window) with go. Pressing v will open the match in a vertical split.

The quickfix window is not specific to ack.vim. It’s a general purpose feature of Vim itself. For more information check out :help quickfix. You’ll find that you can (re)open the quickfix window with :copen, but beware that some of the ack.vim shortcuts won’t work anymore.

The args list

Derek Wyatt’s excellent Vim tutorial screencasts do cover editing many files. His example demonstrated loading relevant files into Vim’s args (arguments) list from the shell then recording a macro to process the first file, save it and move on (:n for next, :p for previous). The recorded macro was then repeated to cycle through each file in the list of filename arguments, making the required changes along the way.

This is clearly a very promising technique. Unfortunately, loading the args list by invoking Vim on the command line isn’t always ideal - particularly if you’re not running Vim in client/server mode. You want to be able to set up the args list for a batch edit without leaving Vim.

And it turns out that you can! Run :args (or :ar) to see the current args list. Set the args list by giving filenames to the same command:

:args fileone filetwo filethree

Even better (but less obvious from :help args) is that you can load the args list using wildcards or the output of a shell command:

:args **/*.rb
:args `ack -l persisted`

Check out :help argadd and :help argdelete as well.

Processing all args with :argdo

Not only can you cycle through the args list one by one, you can also run a Vim command on all those files in one go. I discovered the :argdo command via James O’Beirne. You can read more about it in :help argdo.

With argdo, our seach and replace in project (confirming each substitution after examining it in context) becomes a two step process. First, load the relevant files into the args list. Then, in all those files (:argdo), on every line of each file (%), substitute ‘persisted’ with ‘recorded’ (s/persisted/recorded/), everywhere in each line (g), asking for confirmation each time (c), and save each buffer if changes were made (update):

:args `ack -l persisted`
:argdo %s/persisted/recorded/gc | update

Processing the quickfix list

Vim provides ways to iterate over args (:argdo), buffers (:bufdo), windows (:windo) and tabs (:tabdo), but surprisingly it has no equivalent for processing the contents of the quickfix list. Fortunately, this is easy to fix.

You can install a solution in the form of the QFDo plugin, or head over to Stack Overflow to read the code from its original source. With QFDo in place, our search and replace can be performed as follows:

:Ack persisted
:QFDo %s/persisted/recorded/gc | update

Another Stack Overflow contributor has offered an alternative approach: Qargs, which will load the quickfix list into the args list, allowing you to proceed with :argdo:

:Ack persisted
:Qargs | argdo %s/persisted/recorded/gc | update

I’m not yet sure which I prefer, but I’m leaning towards Qargs with :cclose added, to close the quickfix window as it is loaded into args (returning the cursor to the main window, ready for the next command).

Closer to TextMate with greplace.vim

Those who want something closer to the experience of TextMate’s ‘Find in Project’ might like to try the greplace.vim plugin.

With greplace.vim you can search across your project with :Gsearch, edit hits together, in the new buffer it creates, and then write those changes back into their respective files with :Greplace. It’s also possible to restrict your search to files in the args list (:Gargsearch), and to load hits into the editing buffer from the quickfix list (:Gqfopen).

The greplace.vim plugin’s interpretation of filename masks (which :Gsearch requires) remains something of a mystery to me, but if you find you really want to see search hits in one place when editing them, rather than jumping from file to file to confirm changes, greplace.vim is a strong candidate for integration into your Vim search and replace workflow.

Conclusion

TextMate makes find in project easy. It is something you’ll miss for a while if you’re making the switch to Vim. But Vim does have equivalent functionality, and with a bit of practice and streamlining it’s probably more effective overall.

Vim matches TextMate’s power, and raises it. For example, TextMate can do find and replace of strings or regular expressions across selected files, but in Vim it is trivial to perform batch processing of selected files with a complex macro (e.g. :argdo normal @a).

You can use your shell to select files to pass to TextMate, but from Vim you can drive powerful two-way interaction with the shell from your editor, combining and exploiting UNIX’s many tools to a greater degree, without needing to write extensions for each use case.