The Dot
The Most Powerful Vim Command
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.
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.
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.
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.
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.
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 Command | Equivalent in Longhand |
---|---|
C | c$ |
s | cl |
S | ^C |
I | ^i |
A | $a |
o | A<CR> |
O | ko |
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:
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.
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.
Vim provides an idiomatic approach to solve this, and it looks like this.
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
Intent | Act | Repeat | Reverse |
---|---|---|---|
Make a change | {edit} | . | u |
Scan line for next character | f{char} /t{char} | ; | , |
Scan line for previous character | F{char} /T{char} | ; | , |
Scan document for next match | /pattern<CR> | n | N |
Scan document for previous match | ?pattern<CR> | n | N |
Perform substitution | :s/target/replacement | & | u |
Execute a sequence of changes (macro) | qx{changes}q | @x | u |
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.
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:
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.
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.