chapters/16.markdown @ a16e1fecfe07 default tip

Be clear
author Steve Losh <steve@stevelosh.com>
date Mon, 27 Mar 2017 13:10:55 +0000
parents 1b0acafc0591
children (none)
More Operator-Pending Mappings
==============================

The idea of operators and movements is one of the most important concepts in Vim,
and it's one of the biggest reasons Vim is so efficient.  We're going to
practice defining new motions a bit more, because extending this powerful idea
makes Vim even *more* powerful.

Let's say you're writing some text in Markdown.  If you haven't used Markdown
before, don't worry, for our purposes here it's very simple.  Type the following
into a file:

    :::markdown
    Topic One
    =========

    This is some text about topic one.

    It has multiple paragraphs.

    Topic Two
    =========

    This is some text about topic two.  It has only one paragraph.

The lines "underlined" with `=` characters are treated as headings by Markdown.
Let's create some mappings that let us target headings with movements.  Run the
following command:

    :::vim
    :onoremap ih :<c-u>execute "normal! ?^==\\+$\r:nohlsearch\rkvg_"<cr>

This mapping is pretty complicated, so put your cursor in one of the paragraphs
(not the headings) and type `cih`.  Vim will delete the heading of whatever
section you're in and put you in insert mode ("change inside heading").

It uses some things we've never seen before, so let's look at each piece
individually.  The first part of the mapping, `:onoremap ih` is just the mapping
command that we've seen before, so we'll skip over that.  We'll keep ignoring
the `<c-u>` for the moment as well.

Now we're looking at the remainder of the line:

    :::vim
    :execute "normal! ?^==\\+$\r:nohlsearch\rkvg_"<cr>

Normal
------

The `:normal` command takes a set of characters and performs whatever action
they would do if they were typed in normal mode.  We'll go into greater detail
in a later chapter, but we've seen it a few times already so it's time to at
least get a taste.  Run this command:

    :::vim
    :normal gg

Vim will move you to the top of the file.  Now run this command:

    :::vim
    :normal >>

Vim will indent the current line.

For now, don't worry about the `!` after `normal` in our mapping.  We'll talk
about that later.

Execute
-------

The `execute` command takes a Vimscript string (which we'll cover in more detail
later) and performs it as a command.  Run this:

    :::vim
    :execute "write"

Vim will write your file, just as if you had typed `:write<cr>`.  Now run this
command:

    :::vim
    :execute "normal! gg"

Vim will run `:normal! gg`, which as we just saw will move you to the top of the
file.  But why bother with this when we could just run the `normal!` command
itself?

Look at the following command and try to guess what it will do:

    :::vim
    :normal! gg/a<cr>

It seems like it should:

* Move to the top of the file.
* Start a search.
* Fill in "a" as the target to search for.
* Press return to perform the search.

Run it.  Vim will move to the top of the file and nothing else!

The problem is that `normal!` doesn't recognize "special characters" like
`<cr>`.  There are a number of ways around this, but the easiest to use and read
is `execute`.

When `execute` looks at the string you tell it to run, it will substitute any
special characters it finds *before* running it.  In this case, `\r` is an
escape sequence that means "carriage return".  The double backslash is also an
escape sequence that puts a literal backslash in the string.

If we perform this replacement in our mapping and look at the result we can see
that the mapping is going to perform:

    :::text
    :normal! ?^==\+$<cr>:nohlsearch<cr>kvg_
                    ^^^^           ^^^^
                     ||             ||
    These are ACTUAL carriage returns, NOT the four characters
    "left angle bracket", "c", "r", and "right angle bracket".

So now `normal!` will execute these characters as if we had typed them in normal
mode.  Let's split them apart at the returns to find out what they're doing:

    :::vim
    ?^==\+$
    :nohlsearch
    kvg_

The first piece, `?^==\+$` performs a search backwards for any line that
consists of two or more equal signs and nothing else. This will leave our cursor
on the first character of the line of equal signs.

We're searching backwards because when you say "change inside heading" while
your cursor is in a section of text, you probably want to change the heading for
*that* section, not the next one.

The second piece is the `:nohlsearch` command.  This simply clears the search
highlighting from the search we just performed so it's not distracting.

The final piece is a sequence of three normal mode commands:

* `k`: move up a line.  Since we were on the first character of the line of
  equal signs, we're now on the first character of the heading text.
* `v`: enter (characterwise) visual mode.
* `g_`: move to the last non-blank character of the current line.  We use this
  instead of `$` because `$` would highlight the newline character as well, and
  this isn't what we want.

Results
-------

That was a lot of work, but now we've looked at each part of the mapping.  To
recap:

* We created a operator-pending mapping for "inside this section's heading".
* We used `execute` and `normal!` to run the normal commands we needed to select
  the heading, and allowing us to use special characters in those.
* Our mapping searches for the line of equal signs which denotes a heading and
  visually selects the heading text above that.
* Vim handles the rest.

Let's look at one more mapping before we move on.  Run the following command:

    :::vim
    :onoremap ah :<c-u>execute "normal! ?^==\\+$\r:nohlsearch\rg_vk0"<cr>

Try it by putting your cursor in a section's text and typing `cah`.  This time
Vim will delete not only the heading's text but also the line of equal signs
that denotes a heading.  You can think of this movement as "*around* this
section's heading".

What's different about this mapping?  Let's look at them side by side:

    :::vim
    :onoremap ih :<c-u>execute "normal! ?^==\\+$\r:nohlsearch\rkvg_"<cr>
    :onoremap ah :<c-u>execute "normal! ?^==\\+$\r:nohlsearch\rg_vk0"<cr>

The only difference from the previous mapping is the very end, where we select
the text to operate on:

    :::text
    inside heading: kvg_
    around heading: g_vk0

The rest of the mapping is the same, so we still start on the first character of
the line of equal signs.  From there:

* `g_`: move to the last non-blank character in the line.
* `v`: enter (characterwise) visual mode.
* `k`: move up a line.  This puts us on the line containing the heading's text.
* `0`: move to the first character of the line.

The result is that both the text and the equal signs end up visually selected,
and Vim performs the operation on both.

Exercises
---------

Markdown can also have headings delimited with lines of `-` characters.  Adjust
the regex in these mappings to work for either type of heading.  You may want to
check out `:help pattern-overview`.  Remember that the regex is inside of
a string, so backslashes will need to be escaped.

Add two autocommands to your `~/.vimrc` file that will create these mappings.
Make sure to only map them in the appropriate buffers, and make sure to group
them so they don't get duplicated each time you source the file.

Read `:help normal`.

Read `:help execute`.

Read `:help expr-quote` to see the escape sequences you can use in strings.

Create a "inside next email address" operator-pending mapping so you can say
"change inside next email address".  `in@` is a good candidate for the keys to
map. You'll probably want to use `/...some regex...<cr>` for this.