The Dot

The Most Powerful Vim Command

Vim Logo
Vim Logo
© Mohammed Saed

Introduction


Our work is repetitive by nature. Whether we’re making the same small change in several places or moving around between similar regions of a document, we repeat many actions. Anything that can streamline a repetitive workflow will save our time multifold.

Vim is optimized for repetition. Its efficiency stems from the way it tracks our most recent actions. We can always replay the last change with a single keystroke. Powerful as this sounds, it’s useless unless we learn to craft our actions so that they perform a useful unit of work when replayed. Mastering this concept is the key to becoming effective in Vim.

The dot command, this seemingly simple command is the most versatile tool in the box, and understanding it is the first step towards Vim mastery.

Meet the Dot Command


Vim’s documentation simply states that the dot command “repeats the last change”. It doesn’t sound like much, but in that simple definition we’ll find the kernel of what makes Vim’s modal editing model so effective. First we have to ask, “What is a change?”

To understand the power of the dot command, we have to realize that the “last change” could by one of many things. A change could act at the level of individual characters, entire lines, or even the whole file. To demonstrate, we’ll use this following snippet of text.

image1

The x command deletes the character under the cursor. When we use the dot command in this context, “repeat last change” tells Vim to delete the character under the cursor.

image2

image3

image4

We can restore the file to its original state by pressing the u key a few times to undo the changes.

The dd command also performs a deletion, but this one acts on the current line as a whole. If we use the dot command after dd, then “repeat last change” instructs Vim do delete the current line.

image5

image6

image7

Finally, the >G command increases the indentation from the current line until the end of the file. If we follow this command with the dot command, then “repeat last change” tells vim to increase the indentation level from the current position to the end of the file.

image8

image9

image10

image11

image12

The x, dd, and > commands are all executed from Normal mode, but we also create a change each time we dip into Insert mode. From the moment we enter Insert mode (by pressing i, for example) until we return to Normal mode (by pressing <Esc>), Vim records every keystroke. After making a change such as this, the dot command will replay our keystrokes.

Don’t Repeat Yourself


For such a common use case as appending a semicolon at the end of a series of lines, Vim provides a dedicated command that combines two steps into one.

Suppose we have a snippet of JavaScript code like this.

image13

We need to append a semicolon at the end of each line. Doing so involves moving our cursor to the end of the line and then switching to Insert mode to make the change. The $ command will handle the motion for us, and then we can run a;<Esc> to make the change.

To finish the job we could run the exact same sequence of keystrokes on the next two lines, but that would be missing a trick. The dot command will repeat the last change, so instead of duplicating our efforts, we could just run j$. twice. One keystroke (.) buys us three (a;<Esc>). It’s a small saving, but these efficiencies accumulate when repeated.

But let’s take a closer look at this pattern: j$.. The j command moves the cursor down one line, and then the $ command moves it to the end of the line. We’ve used two keystrokes just to maneuver our cursor into position so that we can use the dot command. Do you sense that there’s room for improvement here?

While the a command appends after the current cursor position, the A command appends at the end of the current line. It doesn’t matter where our cursor is at the time, pressing A will switch to Insert mode and move the cursor to the end of the line. In other words, it squashes $a into a single keystroke. Two for the price of one. We see that Vim has a handful of compound commands.

We could say that the A command compounds two actions ($a) into a single keystroke. It’s not alone in doing this. Many of Vim’s single-key commands can be seen as a condensed version of two or more other commands. The table below shows some examples.

Compound CommandEquivalent in Longhand
Cc$
scl
S^C
I^i
A$a
oA<CR>
Oko

If you catch yourself running ko (or worse, k$a<CR>), stop! Think about what you’re doing. Then recognize that you could have used the O command instead.

PS: All these compound commands switch from Normal mode to Insert mode. Think about that and how it might affect the dot command.

Here is a refinement of our previous JavaScript example:

image14

image15

image16

image17

image18

By using A instead of $a, we give the dot command a boost. Instead of having to position the cursor at the end of the line we want to change, we just have to make sure it is somewhere (anywhere!) on that line. Now we can repeat the change on consecutive lines just by typing j. as many times as it takes.

Take One Step Back, Then Three Forward


We can pad a single character with two spaces (one in front, the other behind) by using an idiomatic Vim solution. At first it might look slightly odd, but the solution has the benefit of being repeatable, which allows us to complete the task effortlessly.

Suppose we have a line of code that looks like this.

image19

Concatenating strings in JavaScript never looks pretty, but we could make this a little easier on the eye by padding each + sign with spaces to make it look like this.

image20

Vim provides an idiomatic approach to solve this, and it looks like this.

image21

image22

image23

image24

image25

image26

image27

The s command compounds two steps into one: it deletes the character under the cursor and then enters Insert mode. Having deleted the + sign, we then type ␣+␣ and leave Insert mode.

One step back and then three steps forward. It’s a strange little dance that might seem unintuitive, but we get a big win by doing it this way: we can repeat the change with the dot command; all we need to do is position our cursor on the next + symbol, and the dot command will repeat that little dance.

The f{char} command tells Vim to look ahead for the next occurrence of the specified character and then move the cursor directly to it if a match is found. So when we type f+, our cursor goes straight to the next + symbol.

Having made our first change, we could jump to the next occurrence by repeating the f+ command, but there’s a better way. The ; command will repeat the last search that f command performed. So instead of typing f+ four times, we can use that command once and then follow up with using the ; command three times.

The ; command takes us to our next target, and the . command repeats the last change, so we can complete the changes by typing ;. three times. Instead of fighting Vim’s modal input model, we’re working with it, and look how much easier it makes this particular task.

Act, Repeat, Reverse


When facing a repetitive task, we can achieve an optimal editing strategy by making both the motion and the change repeatable. Vim has a knack for this. It remembers our actions and keeps the most common ones within close reach so that we can easily replay them.

Repeatable Actions and How to Reverse Them

IntentActRepeatReverse
Make a change{edit}.u
Scan line for next characterf{char}/t{char};,
Scan line for previous characterF{char}/T{char};,
Scan document for next match/pattern<CR>nN
Scan document for previous match?pattern<CR>nN
Perform substitution:s/target/replacement&u
Execute a sequence of changes (macro)qx{changes}q@xu

We’ve seen that the dot command repeats the last change. Since lots of operations count as a change, the dot command proves to be versatile. But some commands can be repeated by other means. For example, @ can be used to repeat any Ex command. Or we can repeat the last :substitute command (which itself happens to be an Ex command as well) by pressing &.

If we know how to repeat our actions without having to spell them out every single time, then we can be more efficient. First we act; then we repeat.

But when so much can be achieved with so few keystrokes, we have to watch our step. It’s easy to get trigger-happy. Rapping out j.j.j. again and again feels a bit like doing a drum roll. What happens if we accidentally hit the j key twitch in a row? Or worse, the . key?

Whenever Vim makes it easy to repeat an action or a motion, it always provides some way of backing out in case we accidentally go too far. In the case of the dot command, we can always hit the u key to undo the last change. If we hit the ; key too many times after using the f{char} command, we’ll miss our mark. But we can back up again by pressing the , key, which repeats the last f{char} search in the reverse direction.

It always helps to know where the reverse gear is in case you accidentally go a step too far. The above table summarizes Vim’s repeatable commands along with their corresponding reverse action. In most cases, the undo command is the one we reach for. No wonder the u key on my keyboard is so worn out!

Find and Replace by Hand


Vim has a :substitute command for find-and-replace tasks, but with this alternative technique, we’ll change the first occurrence by hand and then find and replace every other match one by one. The dot command will save us from labor, but we’ll meet another nifty one-key command that makes jumping between matches a snap.

In the following excerpt, the word “content” appears on every line.

image28

Suppose that we want to use the word “copy” (as in “copywriting”) instead of “content”. Easy enough, you might think; we can just use the substitute command, like this:

image29

But wait a minute! If we run this command, then we’re going to create the phrase “If you are ‘copy’ with this,” which is nonsense!

We’ve run into this problem because the word “content” has two meanings. One is synonymous with “copy” (and pronounced content), the other with “happy” (pronounced content). Technically, we’re dealing with heteronyms (words that are spelled the same but differ in both meaning and pronunciation), but that doesn’t really matter. The point is, we have to watch our step.

We can’t just blindly replace every occurrence of “content” with “copy”. We have to eyeball each one and answer “yay” or “nay” to the question, should this occurrence by changed? The substitute command is up to the task; however, we’ll use a lazy search approach without typing.

You might have guessed by now that the dot command is my favorite single-key Vim trigger. In second place is the * command. This executes a search for the word under the cursor. We could search for the word “content” by pulling up the search prompt and spelling out the word in full, or we could simply place our cursor on the word and hit the * key.

image30

image31

image32

image33

image34

image35

image36

We begin by running f{c} which will jump forward to the first occurrence of c character. Once our cursor is positioned on the word “content”, we can use the * command to search for it. Try it for yourself. Two things should happen: the cursor will jump forward to the next match, and all occurrences will be highlighted. If you don’t see any highlighting, try running :set hls

Having executed a search for the word “content”, we can now advance to the next occurrence just by hitting the n key. With our cursor positioned at the start of the word “content”, we are poised to change it. This involves two steps: deleting the word “content” and then typing its replacement.

The cw command deletes to the end of the word and then drops us into Insert mode, where we can spell out the word “copy”. Vim records our keystrokes until we leave Insert mode, so the full sequence cwcopy<Esc> is considered to be a single change. Pressing the . command deletes to the end of the current word and changes it to “copy”.

We are all set! Each time we press the n key, our cursor advances to the next occurrence of the word “content”. And when we press the . key, it changes the word under the cursor to “copy”. So after pressing n, we can examine the current match and decide if it should be changed to “copy”. If so, we trigger the . command. If not, we don’t. Whatever our decision, we can then move on to the next occurrence by pressing n again. Rinse and repeat until done.