Learn Vimscript the Hard Way

Operator-Pending Mappings

In this chapter we're going to explore one more rabbit hole in Vim's mapping system: "operator-pending mappings". Let's step back for a second and make sure we're clear on vocabulary.

An operator is a command that waits for you to enter a movement command, and then does something on the text between where you currently are and where the movement would take you.

Some examples of operators are d, y, and c. For example:

Keys   Operator   Movement
----   --------   -------------
dw     Delete     to next word
ci(    Change     inside parens
yt,    Yank       until comma

Movement Mappings

Vim lets you create new movements that work with all existing commands. Run the following command:

:onoremap p i(

Now type the following text into a buffer:

return person.get_pets(type="cat", fluffy_only=True)

Put your cursor on the word "cat" and type dp. What happened? Vim deleted all the text inside the parentheses. You can think of this new movement as "parameters".

The onoremap command tells Vim that when it's waiting for a movement to give to an operator and it sees p, it should treat it like i(. When we ran dp it was like saying "delete parameters", which Vim translates to "delete inside parentheses".

We can use this new mapping immediately with all operators. Type the same text as before into the buffer (or simply undo the change):

return person.get_pets(type="cat", fluffy_only=True)

Put your cursor on the word "cat" and type cp. What happened? Vim deleted all the text inside the parentheses, but this time it left you in insert mode because you used "change" instead of "delete".

Let's try another example. Run the following command:

:onoremap b /return<cr>

Now type the following text into a buffer:

def count(i):
    i += 1
    print i

    return foo

Put your cursor on the i in the second line and press db. What happened? Vim deleted the entire body of the function, all the way up until the return, which our mapping used Vim's normal search to find.

When you're trying to think about how to define a new operator-pending movement, you can think of it like this:

  1. Start at the cursor position.
  2. Enter visual mode (charwise).
  3. ... mapping keys go here ...
  4. All the text you want to include in the movement should now be selected.

It's your job to fill in step three with the appropriate keys.

Changing the Start

You may have already seen a problem in what we've learned so far. If our movements always have to start at the current cursor position it limits what we can do.

Vim isn't in the habit of limiting what you can do, so of course there's a way around this problem. Run the following command:

:onoremap in( :<c-u>normal! f(vi(<cr>

This might look frightening, but let's try it out. Enter the following text into the buffer:

print foo(bar)

Put your cursor somewhere in the word print and type cin(. Vim will delete the contents of the parentheses and place you in insert mode between them.

You can think of this mapping as meaning "inside next parentheses", and it will perform the operator on the text inside the next set of parentheses on the current line.

Let's make a companion "inside last parentheses" ("previous" would be a better word, but it would shadow the "paragraph" movement). Run the following command:

:onoremap il( :<c-u>normal! F)vi(<cr>

Try it out on some text of your own to make sure it works.

So how do these mappings work? First, the <c-u> is something special that you can ignore for now -- just trust me that it needs to be there to make the mappings work in all cases. If we remove that we're left with:

:normal! F)vi(<cr>

:normal! is something we'll talk about in a later chapter, but for now it's enough to know that it is a command used to simulate pressing keys in normal mode. For example, running :normal! dddd will delete two lines, just like pressing dddd. The <cr> at the end of the mapping is what executes the :normal! command.

So now we know that the mapping is essentially just running the last block of keys:

F)vi(

This is fairly simple:

  • F): Move backwards to the nearest ) character.
  • vi(: Visually select inside the parentheses.

We end up with the text we want to operate on visually selected, and Vim performs the operation on it as normal.

General Rules

A good way to keep the multiple ways of creating operator-pending mappings straight is the remember the following two rules:

  • If your operator-pending mapping ends with some text visually selected, Vim will operate on that text.
  • Otherwise, Vim will operate on the text between the original cursor position and the new position.

Exercises

Create operator-pending mappings for "around next parentheses" and "around last parentheses".

Create similar mappings for in/around next/last for curly brackets.

Read :help omap-info and see if you can puzzle out what the <c-u> in the examples is for.