chapters/32.markdown @ 96cf3ead3c63

ftdetect
author Steve Losh <steve@stevelosh.com>
date Sun, 13 Nov 2011 19:12:41 -0500
parents 44918d574627
children e66e6a4e104d
Case Study: Grep Operator, Part One
===================================

In this chapter and the next we're going to walk through creating
a fairly-complicated piece of Vimscript.  We'll talk about several things we
haven't seen before, as well as how some of the things we've studied fit
together in practice.

As you work through this case study make sure to look up anything unfamiliar
with `:help`.  If you coast through without fully understanding everything you
won't learn much.

Grep
----

If you've never used `:grep` you should take a minute to read `:help :grep` and
`:help :make` now.  Read `:help quickfix-window` if you've never used the
quickfix window before.

In a nutshell: `:grep ...` will run an external grep program with any arguments
you give, parse the result, and fill the quickfix list for easy use inside of
Vim.

Our example is going to make it easier to invoke by adding a "grep operator"
that you can use with any of Vim's built-in (or custom!) motions to select the
text you want to search for.

Usage
-----

The first thing you should think about when creating any non-trivial piece of
Vimscript is: "how will this functionality be used?".  Try to come up with
a smooth, easy, intuitive way for you and your code's users to invoke it.

In this case I'll do that step for you:

* We're going to create a "grep operator" and bind it to `<leader>g`. 
* It will act like any other Vim operator and take a motion, like `w` or `i{`. 
* It will perform the search immediately and open the quickfix window to show
  the results.
* It will *not* jump to the first result, because that can be jarring if the
  first result isn't what you're expecting.

Some examples of how you might end up using it:

* `<leader>giw`: Grep for the word under the cursor.
* `<leader>giW`: Grep for the WORD under the cursor.
* `<leader>gi'`: Grep for the contents of the single-quoted string you're
  currently in.
* `viwe<leader>g`: Visually select a word, extend the selection to the end of
  the word after it, then grep for the selected text.

There are many, *many* other ways to use this.  It may seem like it will take
a lot of coding, but actually all we need to do is implement the operator
functionality -- Vim will handle the rest.

A Preliminary Sketch
--------------------

One thing that's sometimes helpful when writing tricky bits of Vimscript is to
simplify your goal and implement that to get an idea of the "shape" your final
solution will take.

Let's simplify our goal to: "create a mapping to search for the word under the
cursor".  This is useful but should be easier, so we can get something running
much faster.  We'll map this to `<leader>g` for now.

We'll start with a skeleton of the mapping and fill it in as we go.  Run this
command:

    :nnoremap <leader> g :grep -R something .<cr>

If you've read `:help grep` this should be pretty easy to understand.  We've
looked at lots of mappings before, and there's nothing new here.

Obviously we're not done yet, so lets refine this mapping until it meets our
simplified goal.

The Search Term
---------------

First we need to search for the word under the cursor, not the string
"something".  Run the following command:

    :nnoremap <leader>g :grep -R <cword> .<cr>

Now try it out.  `<cword>` is a special bit of text you can use in Vim's
command-line mode, and Vim will replace it with "the word under the cursor"
before running the command.

You can use `<cWORD>` to get a WORD instead of a word.  Run this command:

    :nnoremap <leader>g :grep -R <cWORD> .<cr>

Now try the mapping when your cursor is over something like "foo-bar".  Vim will
grep for "foo-bar" instead of just part of the word.

There's still a problem with our search term: if there are any special shell
characters in it Vim will happily pass them along to the external grep command,
which will explode (or, worse, do something terrible).

Go ahead and try this to make sure it breaks.  Type `foo;ls` into a file and run
the mapping while your cursor is over it.  The grep command will fail, and Vim
will actually run an `ls` command as well!  Clearly this could be bad if the
word contained a command more dangerous than `ls`.

To try to fix this we'll quote the argument in the grep call.  Run this command:

    :nnoremap <leader>g :grep -R '<cWORD>' .<cr>

Most shells treat single-quoted text as (almost) literal, so our mapping is much
more robust now.  However there's still one more problem with the search term!
Try the mapping on the word "that's".  It won't work, because the single quote
inside the word interferes with the quotes in the grep command!

To get around this we can use Vim's `shellescape` function.  Read `:help
escape()` and `:help shellescape()` to see how it works (it's pretty simple).

Because `shellescape()` works on Vim strings, we'll need to dynamically build
the command with `execute`.  First run the following command to transform the
`:grep` mapping into `:execute "..."` form:

    :nnoremap <leader>g :execute "grep -R '<cWORD>' ."<cr>

Try it out and make sure it still works.  If not, find any typos and fix them.
Then run the following command, which uses `shellescape` to fix the search term:

    :nnoremap <leader>g :execute "grep -R " . shellescape("<cWORD>") . " ."<cr>

And now our mapping won't break if the word we're searching for happens to
contain strange characters.

The process of starting with a trivial bit of Vimscript and transforming it
little-by-little into something closer to your goal is one you'll find yourself
using often.

Cleanup
-------

There are still a couple of small things to take care of before our mapping is
finished.  First, we said that we don't want to go to the first result
automatically, and we can use `grep!` instead of plain `grep` to do that.  Run
this command:

    :nnoremap <leader>g :execute "grep! -R " . shellescape("<cWORD>") . " ."<cr>

Try it out again and nothing will seem to happen.  Vim has filled the quickfix
window with the results, but we haven't opened it yet.  Run the following
command:

    :nnoremap <leader>g :execute "grep! -R " . shellescape("<cWORD>") . " ."<cr>:copen<cr>

Now try the mapping and you'll see that Vim automatically opens the quickfix
window with the search results.  All we did was tack `:copen<cr>` onto the end
of our mapping.

As the finishing touch we'll remove all the grep output Vim displays while
searching.  Run the following command:

    :nnoremap <leader>g :silent execute "grep! -R " . shellescape("<cWORD>") . " ."<cr>:copen<cr>

We're done, so try it out and admire your hard work!  The `silent` command just
runs the command that follows it while hiding any messages it would normally
display.

Exercises
---------

Add the mapping we just created to your `~/.vimrc` file.

Read `:help :grep`.

Read `:help cnext` and `:help cprevious`.  Try them out after using your new
grep mapping.

Set up mappings for `:cnext` and `:cprevious` to make it easier to quickly run
through matches.

Read `:help copen`.

Add a height to the `:copen` command in the mapping we created to make sure the
quickfix window is opened to whatever height you prefer.

Read `:help silent`.