Learn Vimscript the Hard Way

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:

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:

: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:

: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:

:normal gg

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

: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:

:execute "write"

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

: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:

: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:

: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:

?^==\+$
: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:

: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:

: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:

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.