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
: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
<c-u> for the moment as well.
Now we're looking at the remainder of the line:
:execute "normal! ?^==\\+$\r:nohlsearch\rkvg_"<cr>
: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 will move you to the top of the file. Now run this command:
Vim will indent the current line.
For now, don't worry about the
normal in our mapping. We'll talk
about that later.
execute command takes a Vimscript string (which we'll cover in more detail
later) and performs it as a command. Run this:
Vim will write your file, just as if you had typed
:write<cr>. Now run this
: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
Look at the following command and try to guess what it will do:
It seems like it should:
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
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".
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
$would highlight the newline character as well, and this isn't what we want.
That was a lot of work, but now we've looked at each part of the mapping. To recap:
normal!to run the normal commands we needed to select the heading, and allowing us to use special characters in those.
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
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.
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
: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.
: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.