chapters/33.markdown @ a16e1fecfe07 default tip

Be clear
author Steve Losh <steve@stevelosh.com>
date Mon, 27 Mar 2017 13:10:55 +0000
parents 5868e6263612
children (none)
Case Study: Grep Operator, Part Two
===================================

Now that we've got a preliminary sketch of our solution, it's time to flesh it
out into something powerful.

Remember: our original goal was to create a "grep operator".  There are a whole
bunch of new things we need to cover to do this, but we're going to follow the
same process we did in the last chapter: start with something simple and
transform it until it does what you need.

Before we start, comment out the mapping we creating the previous chapter from
your `~/.vimrc` file -- we're going to use the same keystroke for our new
operator.

Create a File
-------------

Creating an operator will take a number of commands and typing those out by
hand will get tedious very quickly.  You could add it to your `~/.vimrc` file,
but let's create a separate file just for this operator instead.  It's meaty
enough to warrant a file of its own.

First, find your Vim `plugin` directory.  On Linux or OS X this will be at
`~/.vim/plugin`.  If you're on Windows it will be inside the `vimfiles`
directory in your home directory. (Use the command: `:echo $HOME` in Vim if
you're not sure where this is). If this directory doesn't exist, create it.

Inside `plugin/` create a file named `grep-operator.vim`.  This is where you'll
place the code for this new operator.  When you're editing the file you can run
`:source %` to reload the code at any time.  This file will also be loaded each
time you open Vim just like `~/.vimrc`.

Remember that you *must* write the file before you source it for the changes to
be seen!

Skeleton
--------

To create a new Vim operator you'll start with two components: a function and
a mapping.  Start by adding the following code to `grep-operator.vim`:

    :::vim
    nnoremap <leader>g :set operatorfunc=GrepOperator<cr>g@

    function! GrepOperator(type)
        echom "Test"
    endfunction

Write the file and source it with `:source %`.  Try it out by pressing
`<leader>giw` to say "grep inside word".  Vim will echo `Test` *after* accepting
the `iw` motion, which means we've laid out the skeleton.

The function is simple and nothing we haven't seen before, but that mapping is
a bit more complicated.  First we set the `operatorfunc` option to our function,
and then we run `g@` which calls this function as an operator.  This may seem
a bit convoluted, but it's how Vim works.

For now it's okay to consider this mapping to be black magic.  You can delve
into the detailed documentation later.

Visual Mode
-----------

We've added the operator to normal mode, but we'll want to be able to use it
from visual mode as well.  Add another mapping below the first:

    :::vim
    vnoremap <leader>g :<c-u>call GrepOperator(visualmode())<cr>

Write and source the file.  Now visually select something and press `<leader>g`.
Nothing happens, but Vim does echo `Test`, so our function is getting called.

We've seen the `<c-u>` in this mapping before but never explained what it did.
Try visually selecting some text and pressing `:`.  Vim will open a command line
as it usually does when `:` is pressed, but it automatically fills in `'<,'>` at
the beginning of the line!

Vim is trying to be helpful and inserts this text to make the command you're
about to run function on the visually selected range.  In this case, however, we
don't want the help.  We use `<c-u>` to say "delete from the cursor to the
beginning of the line", removing the text.  This leaves us with a bare `:`,
ready for the `call` command.

The `call GrepOperator()` is simply a function call like we've seen before, but
the `visualmode()` we're passing as an argument is new.  This function is
a built-in Vim function that returns a one-character string representing the
last type of visual mode used: `"v"` for characterwise, `"V"` for
linewise, and a `Ctrl-v` character for blockwise.

Motion Types
------------

The function we defined takes a `type` argument.  We know that when we use the
operator from visual mode it will be the result of `visualmode()`, but what
about when we run it as an operator from normal mode?

Edit the function body so the file looks like this:

    :::vim
    nnoremap <leader>g :set operatorfunc=GrepOperator<cr>g@
    vnoremap <leader>g :<c-u>call GrepOperator(visualmode())<cr>

    function! GrepOperator(type)
        echom a:type
    endfunction

Source the file, then go ahead and try it out in a variety of ways.  Some
examples of the output you get are:

* Pressing `viw<leader>g` echoes `v` because we were in characterwise visual
  mode.
* Pressing `Vjj<leader>g` echoes `V` because we were in linewise visual mode.
* Pressing `<leader>giw` echoes `char` because we used a characterwise motion
  with the operator.
* Pressing `<leader>gG` echoes `line` because we used a linewise motion with the
  operator.

Now we know how we can tell the difference between motion types, which will be
important when we select the text to search for.

Copying the Text
----------------

Our function is going to need to somehow get access to the text the user wants
to search for, and the easiest way to do that is to simply copy it.  Edit the
function to look like this:

    :::vim
    nnoremap <leader>g :set operatorfunc=GrepOperator<cr>g@
    vnoremap <leader>g :<c-u>call GrepOperator(visualmode())<cr>

    function! GrepOperator(type)
        if a:type ==# 'v'
            execute "normal! `<v`>y"
        elseif a:type ==# 'char'
            execute "normal! `[v`]y"
        else
            return
        endif

        echom @@
    endfunction

Wow.  That's a lot of new stuff.  Try it out by pressing things like
`<leader>giw`, `<leader>g2e` and `vi(<leader>g`.  Each time Vim will echo the
text that the motion covers, so clearly we're making progress!

Let's break this new code down one step at a time.  First we have an `if`
statement that checks the `a:type` argument.  If the type is `'v'` it was called
from characterwise visual mode, so we do something to copy the visually-selected
text.

Notice that we use the case-sensitive comparison `==#`.  If we used plain `==`
and the user has `ignorecase` set it would match `"V"` as well, which is *not*
what we want.  Code defensively!

The second case of the `if` fires if the operator was called from normal mode
using a characterwise motion.

The final case simply returns.  We explicitly ignore the cases of
linewise/blockwise visual mode and linewise/blockwise motions.  Grep doesn't
search across lines by default, so having a newline in the search pattern
doesn't make any sense!

Each of our two `if` cases runs a `normal!` command that does two
things:

* Visually select the range of text we want by:
    * Moving to mark at the beginning of the range.
    * Entering characterwise visual mode.
    * Moving to the mark at the end of the range.
* Yanking the visually selected text.

Don't worry about the specific marks for now.  You'll learn why they need to be
different when you complete the exercises at the end of this chapter.

The final line of the function echoes the variable `@@`.  Remember that
variables starting with an `@` are registers.  `@@` is the "unnamed" register:
the one that Vim places text into when you yank or delete without specify
a particular register.

In a nutshell: we select the text to search for, yank it, then echo the yanked
text.

Escaping the Search Term
------------------------

Now that we've got the text we need in a Vim string we can escape it like we did
in the previous chapter.  Modify the `echom` command so it looks like this:

    :::vim
    nnoremap <leader>g :set operatorfunc=GrepOperator<cr>g@
    vnoremap <leader>g :<c-u>call GrepOperator(visualmode())<cr>

    function! GrepOperator(type)
        if a:type ==# 'v'
            normal! `<v`>y
        elseif a:type ==# 'char'
            normal! `[v`]y
        else
            return
        endif

        echom shellescape(@@)
    endfunction

Write and source the file and try it out by visually selecting some text with
a special character in it and pressing `<leader>g`.  Vim will echo a version of
the selected text suitable for passing to a shell command.

Running Grep
------------

We're finally ready to add the `grep!` command that will perform the actual
search.  Replace the `echom` line so the code looks like this:

    :::vim
    nnoremap <leader>g :set operatorfunc=GrepOperator<cr>g@
    vnoremap <leader>g :<c-u>call GrepOperator(visualmode())<cr>

    function! GrepOperator(type)
        if a:type ==# 'v'
            normal! `<v`>y
        elseif a:type ==# 'char'
            normal! `[v`]y
        else
            return
        endif

        silent execute "grep! -R " . shellescape(@@) . " ."
        copen
    endfunction

This should look familiar.  We simply execute the `silent execute "grep! ..."`
command we came up with in the last chapter.  It's even more readable here
because we're not trying to stuff the entire thing into a `nnoremap` command!

Write and source the file, then try it out and enjoy the fruits of your labor!

Because we've defined a brand new Vim operator we can use it in a lot of
different ways, such as:

* `viw<leader>g`: Visually select a word, then grep for it.
* `<leader>g4w`: Grep for the next four words.
* `<leader>gt;`: Grep until semicolon.
* `<leader>gi[`: Grep inside square brackets.

This highlights one of the best things about Vim: its editing commands are like
a language.  When you add a new verb it automatically works with (most of) the
existing nouns and adjectives.

Exercises
---------

Read `:help visualmode()`.

Read `:help c_ctrl-u`.

Read `:help operatorfunc`.

Read `:help map-operator`.