This is neither an in-depth article about advanced vim features nor is it an ideology-inducing primer on why the oh-so-powerful god of editors is such a great monarch. This is just my experience discovering vim after using several different code editors, including Visual Studio Code, for the last 3 or 4 years.
First impressions
I had my first encounter with vim when borrowing a laptop from a friend of mine. We were working together over the weekend on a school project, and I wanted to commit some changes I made while he was cooking lunch. Soon I found myself unable to exit vim, like so many others have, and asked my friend about this new terminal editor I hadn't seen before and was so unintuitive. I learned just enough that I could do basic editing, and from that point onward, I didn't pay vim the slightest attention. That was 4 years ago.
Over the years (oh yes, so many), I only heard relatively bad things about vim. I happened to be teaching a course on assembly at my school where the course project was implementing a subset of vim in NASM. It wasn't very popular among the students, and it certainly didn't resonate with me.
Fast forward to a couple of months ago when I decided to learn how to touch type, which is a story for another day, and realized that touch typing aligned very well with vim, as I'll explain in a bit. However, the thing that made me want to try it out was curiosity: I was reading through a uses page by a developer I admire, and there it read:
In the summer of 2020, I completely switched to neovim.
It was certainly intriguing. I knew that some people used vim, and I had heard stories of how it was very cool as a programmer because you didn't have to reach for the mouse or the arrow keys anymore. But my experience with it had been the total opposite. Nonetheless, I decided to give it a chance when I got decent at touch typing.
Research
While doing so, I spent some days researching and reading about vim in my free time and was very impressed by what I found. It turns out that it actually is optimized for developers. As programmers, we spend more time editing code than writing it, and this is very easy to do inside vim . This wouldn't have come as a surprise if I knew that it comes from a long series of editors beginning in 1969. vim is an improvement over vi (vim stands for "VI iMproved"), which is an improvement over ex, which is an improvement over ed. That's quite a lot of development cycles.
Furthermore, there's a ton of configuration options. At first, I thought it had to look like a 90's hacker terminal, green over black, which is plain unusable by today's standards. I mean, come on, when you can use a pretty and polished interface like vscode's, why would you be so hard on your eyes? But, it is very customizable. After you look around in the vim community, you'll learn about several projects, like neovim, that take this a step further, resulting in pretty nice interfaces like these:
One of the main concerns I had was being able to do everything I did with vscode, and oh boy, what I would discover. For example, the first question everyone has (at least I did) is whether one can have completion features, code actions , quick fixes, symbol renaming, go to definition, all the good stuff that today's IDEs have: all of these use a standard called Language Server Protocol, you can read this introduction to go a bit more in-depth in the subject.
LSP
Say you want to communicate two applications over a network, and you have a proper transport layer. Say that one of the applications is a client and the other is a server. You write code to bridge the communication between the two, resulting in a language only those two applications know.
But now you have a second client application that wants to communicate with the server. What do you do? Do you write more communication code for this? It turns out that the best solution for this is to write a proxy language that every client and server application knows how to parse. Thus we have protocols like HTTP.
The same thing happens with LSP. You have several client interfaces and several language servers, and you don't want to write code to communicate each client with each server. The solution is having a standard protocol for communication, in this case, JSON-RPC. Now, the vscode client developers implement converting the actions of a user inside the editor to the protocol and sending them to a language server, which knows how to parse it and respond accordingly, like one of these.
The funny thing is that neovim has native support for LSP, so it has the same capabilities vscode has, as long as the client and server implementations exist, which is true for most programming languages.
Trying the thing
First, I started using a vscode extension for vim emulation. That way I started getting familiar with it and also got more time to improve touch typing—at this point, I am still memorizing the keyboard.
Basic vim features
I'll explain some of the basic features the extension offers that I found very convenient and started to make me love vim. These features meant that I had to reach for the touchpad/mouse less and less over time. I'll compare it with vscode where appropriate.
Say you have the following lines and your cursor is the |
character:
|let vimRocks = () => console.log('I want the cursor at the first a');
Say you want the cursor to be at the first appearance of a
in the line. In vscode, I would just jump between words with
Ctrl + Right Arrow
until I got to the want
word and then repeat
without having Ctrl
pressed. In vim we type fa
—find a
.
Some clever people would now say: But I can do that with Ctrl + f
—
and I would agree—yet those are two more keystrokes.
Now say that after having our cursor in the first a
, you want to
get to the next occurrence of an a
. In vscode we press
Enter
to repeat the search—assuming we searched in the first place.
In vim we do ;
.
Finally, say you wanted to go back to the first occurrence, which is a bit
weird in vscode: we press Shift + F4
(Counterpart to F4
, which is
"next match"). In vim, we press ,
. Look at both sequences beside
each other.
<Ctrl>fa<ESC><Ctrl>f<Enter><ESC><Shift><F4>
-------------------------------------------
fa;,
Now, let's take a look at a different example which showcases actual editing. Say we have the following two lines of HTML:
|<div id="fancy-card" class="awesome card classes">
<div id="the-greatest-card" class="wow the greatest class">
And now, say we wanted to make the first line look like the the second one, and for this example, I will show three ways of doing it with vim.
We can delete the first line, copy the second line and paste it,
which is the following sequence of keys: ddyyP
. A second way
to do it would be overwriting the first line with the second,
namely: jyykVp
. Finally, suppose that we didn't have the second
line and we have to do it by hand.
First we type ci"
—mnemonics for change inner/inside ", which
gives us the following:
<div id="|" class="awesome card classes">
Now, we write the new id and after being done we execute f";ci"
,
which yields:
<div id="the-greatest-card" class="|">
First way: Second way: Third way:
And then we write the new classes. Not too shabby. All of this you can accomplish with just the extension, which is very good and works pretty well out-of-the-box. This kind of stuff got me very excited.
Finally moving to a new editor
However, it wasn't too long before I realized that this wasn't enough. Even though you get most of vim's functionality for basic editing and moving around, you miss very powerful features that I had learned about in books and blogs. So after two weeks or so, having found a solution for managing my dotfiles and having read Drew Neil's Practical vim, I decided to move to neovim.
This move wasn't abrupt, it was progressive, and I believe that once you start moving, it never stops. I know that this sounds very bad at first, but you'll get what I mean in a bit.
I said at the beginning that vim has a ton of configuration. You can use it bare-bones or turn it into a fully-fledged IDE-like experience. vim has a language to write configuration, which is called VimL. Of course, you don't have to learn its ins and outs to shape the editor however you want. On top of that, vim has 30 years growing a community around it. Having a big community around something brings all sorts of use-cases. Also, this gets translated into having a plethora of plugins for the editor. There are so many plugins. And there are so many great plugins. And new plugins are being created every day, and it gets easier to make plugins every day since the advent of neovim. Having said all of that, don't get discouraged, vscode has way too big of a pool of extensions and configuration options, and this doesn't matter to convince someone to use it.
I first started browsing configurations I liked in GitHub, and started copying little bits from here and there. I just read and copied everything I thought that was going to make my life easier. And this is a process that I still do: my vim configuration is ever-changing. Every now and then I find a new trick, or I install a new plugin or a new color scheme.
For example, I setup my environment to write markdown for this blog post using three new plugins I didn't know of:
This way, I got linting and a sort of zen mode that you can see in the next image:
This showcases that there are not really many things you can do in vscode but not in vim. However, it is slow to get to the point where you can make the jump. I am a frontend developer, it was vital to me having everything I use daily readily available, and it took me 2-3 days to be satisfied with my new setup. Now I seldom look back. Every time I need something new or find myself feeling like there should be a better way to do something, I do some research, and it almost always has been asked before.
Show me the good stuff
Now that you know that we can get a comparable experience to state-of-the-art
editors with just vim, you might ask what exactly makes me use it. Well, lots of
stuff. First of all, I might argue that vim is a superset of editors like
sublime and vscode. It's a different paradigm for editing called multimodal
editing. Text editors today have a single mode where you can input text and
have some shortcuts to select and manipulate text, but as I showed before, this
is quite limited if we compare it with the expressiveness of vim's motions. vim
has many different modes, and one of them—Insert mode—is the equivalent to that
text input mode that other editors gave, but even this mode has several cool features,
like deleting the character behind the cursor with Ctrl + h
, the word behind the
cursor with Ctrl + w
or using Ctrl + v
to
insert next non-digit literally.
We use the sequences of commands I showed in the "Trying the thing" section in Normal mode, which is where most of the action happens, but there are modes for visual selection, terminal commands, replacing text, terminal emulation, etc.
And most importantly, you can create shortcuts for your preferred sequences,
or the same shortcut doing different things in different modes. Shortcuts
don't even have to be other commands! They can be function calls or entering some
mode to execute an action. I have a shortcut
to run all the tests in my current project. A different one for entering Goyo,
the command that the goyo.vim
extension creates. A different one to
go to the line and column of the next occurrence of a compiler error in any file
of my currently opened project. The amount of optimization you can achieve is
spectacular.
A Case Study
But that's not even beginning to scratch the surface of what you can do. I'll show you an example of what one can do with just one month of having moved to vim. This happened while sharing my screen in a meeting with my bosses right before a release.
Please note that this is a little bit technical in the sense that you have to know what git and regular expressions are.
Say you have a git repository, and you want to compile a list of release notes.
In my job, we have the policy of always including ticket names at the start of
the PR title. We are divided into cross-functional teams, each having a
unique code like PUR
or SHIP
. So a valid PR title would be "SHIP-1234 Some change
that fixes this hard bug". The format we want our release notes to have is:
RED-20 Lorem ipsum dolor sit amet consectetur
RED-45 Lorem ipsum dolor sit amet consectetur
SHIP-1234 Lorem ipsum dolor sit amet consectetur
SHIP-1237 Lorem ipsum dolor sit amet consectetur
SHIP-1334 Lorem ipsum dolor sit amet consectetur
SHIP-2234 Lorem ipsum dolor sit amet consectetur
Knowing all of this, how would you go about generating the file? I remembered
that the PR title is including in the body of the commit message of the merges.
So now the task was extracting those titles from the output of the
git log
command and
then grouping and sorting them.
Extracting PR titles
First, I tried to do this from the command line, but I didn't understand grep
's
regular expressions, so I turned back to what I just learned: vim. So I ran
git log main... > release-notes.txt
Which creates the release-notes.txt
file and dumps the output of git log
to the
file. The main...
part is a way of telling git to only log the new commits in
the current branch that aren't in main
. That way, I got an output like this inside
release-notes.txt
:
...
commit 0a4e4053f47f7c9557862cc76e70d638c0869205
Merge: 1ac9990f8 e8388f131
Author: Author Name <author@email.ex>
Date: Wed Jun 9 13:49:01 2021 -0400
Merge pull request #3148 from BRANCH_NAME
SHIP-322 Lorem ipsum dolor sit amet consectetur
commit 1ac9990f8b9e7c1c285205539c120ecfb2d42be0
Merge: 21f6417ab 2d3bc451c
Author: Author Name <author@email.ex>
Date: Wed Jun 9 13:45:21 2021 -0400
Merge pull request #3143 from BRANCH_NAME
YEL-188 Change default text in multifilter
commit e8388f1313581f0f6f84e7dddd583788a5bf9d1c
Author: Author Name <author@email.ex>
Date: Wed Jun 9 13:41:26 2021 -0400
refactor: Change state name
commit 2d3bc451c4ead8e38f6682768517f8a6f87eab71
Author: Author Name <author@email.ex>
Date: Wed Jun 9 23:48:24 2021 +0700
fix: Capitalize default text
commit ae2b114be39ce9b8340aefe72e103c90aaef9985
Author: Author Name <author@email.ex>
Date: Wed Jun 9 23:48:07 2021 +0700
fix: Update the package version
...
After that, I needed to create a regular expression to match the codes, taking
advantage of them appearing only in the lines we want to extract. So, I start
searching inside the currently opened file (buffer) by typing /
. This opens a
prompt starting with the character /
and lets you write a regular expression
used to match patterns in the buffer. The expression I came up with is
/\v\w+-\d+ /
. The starting and ending /
are delimiters for the expression.
\v
enters "very magic" mode inside the expression: every
character following that special sequence except a-zA-Z0-9
and _
has
a special meaning. For example, we wouldn't have to escape \+
and just use
+
. Then, \w+
matches words of at least one character, then we match -
,
and finally \d+<space>
matches every sequence of one or more digits followed by
a space. This is a very specific pattern that yields few false positives, but
in this case, none.
Having built the regular expression and being satisfied with that, what remains
is deleting the lines that do not contain that pattern. We accomplish this inside
vim with the sequence :g!//d
. Let that sink in. I just deleted every line in
release-notes.txt
that didn't contain a pattern matched by our regular
expression. I'll explain it briefly. The :
enters Command mode, which
lets us access a whole bunch of useful commands, like g[lobal]
, where
everything inside []
is optional. The g
command executes a vim command
at every line-matching pattern. Its somewhat simplified syntax is:
:g[lobal]/re/command
For example, to print each line matching the pattern re
, we could
run :g/re/p
.
The !
after the g
matches every line not containing the pattern.
//
is a shorthand to use the last search pattern, which is the regular expression
we just built. And d
stands for the d[elete]
command. Not too shabby.
We are getting there already:
SHIP-1334 Lorem ipsum dolor sit amet consectetur
SHIP-1234 Lorem ipsum dolor sit amet consectetur
RED-20 Lorem ipsum dolor sit amet consectetur
SHIP-2234 Lorem ipsum dolor sit amet consectetur
SHIP-1237 Lorem ipsum dolor sit amet consectetur
RED-45 Lorem ipsum dolor sit amet consectetur
Group by Team Code
Edit: This section is redundant, I didn't realize until someone commented on it. We could have just sorted, but I will leave this to showcase macros. Also, a shorter way of moving the current line (.
) to the bottom of the file ($
) is using the m[ove]
command which would translate to the sequence: :.m$
.
Now all that is left is grouping by team and sorting. The easiest way I could
think of doing this was by recording a simple macro and applying it for each line
that matched the team code. A macro is a recording of a sequence of key presses,
a very handy feature to have. We store a macro in a vim register:
a place where we can store text. Recording a macro starts
by pressing q
and selecting a register to save it, one of a-zA-Z
, and we end
the recording by pressing q
again.
What I wanted to achieve was just moving the
line under the cursor to the end of the file—try to do that in vscode—, which
is translated to the sequence qaddGpq
. After recording the macro, I wanted to apply
it to every line containing a team code—say, SHIP
—, and we can lean on the g
command for this too. I executed :g/team code/norm @a<Enter>
for each code,
for example to group the SHIP
team I would run :g/SHIP/norm @a
.
The norm @a
is the new command we are executing at every matched line, which
runs the macro saved in register a
, as you may guess.
norm[al]
is a command that takes a sequence of characters as argument and
executes that sequence as if we were in Normal mode. The @
character
executes a macro stored in a register, which means that @
executes the
macro stored at register a
.
Now our file looks like this:
SHIP-1334 Lorem ipsum dolor sit amet consectetur
SHIP-1234 Lorem ipsum dolor sit amet consectetur
SHIP-2234 Lorem ipsum dolor sit amet consectetur
SHIP-1237 Lorem ipsum dolor sit amet consectetur
RED-45 Lorem ipsum dolor sit amet consectetur
RED-20 Lorem ipsum dolor sit amet consectetur
Sort Lines
This means that the only remaining thing to do is sort each group. We have come
so far! How should we go about doing this? Well, I just selected the whole file
visually with the sequence ggVG
and executed the sort
command with :sort
.
In other words, I pressed ggVG:sort<Enter>
. Neat! And thus we got what we want.
RED-20 Lorem ipsum dolor sit amet consectetur
RED-45 Lorem ipsum dolor sit amet consectetur
SHIP-1234 Lorem ipsum dolor sit amet consectetur
SHIP-1237 Lorem ipsum dolor sit amet consectetur
SHIP-1334 Lorem ipsum dolor sit amet consectetur
SHIP-2234 Lorem ipsum dolor sit amet consectetur
Final Thoughts
Bram Moolenaar—the author of vim—in his classic article Seven Habits of Effective Text Editing talks about sharpening the saw, which is a metaphor for improving vim and keeping at it until you know that it is the best it can be to support you. I agree with him, so I spend a lot of time making sure that editing feels as smooth as possible. And I think you should too.
I hope this article gives you a sneak peek at what you can achieve with vim. Let's be real, every single person that has to code—every person that writes—owes it to themselves to get a better experience at editing text. If you could shave off 10% of the time you spend writing, would you do it? I would. It is analogous to touch typing. We spend a lot of time writing emails, code, messages, documentation, dissertations, essays, and we are happy doing it at 40-50 words per minute, looking at the keyboard at each stroke, and making many mistakes. What if you could improve your accuracy by typing 3 characters wrong out of 100? What if you could reach a speed of 100 words per minute in just 2 months of practicing 15 minutes a day? Isn't it worth it?
Having said all of that, I do have a couple of things that I miss from vscode. One of them is multiple cursors. They cover some edge cases that are very useful and not so uncommon. However, I know that there are some plugins for vim that add this capability. This is me just being lazy. Another thing I miss is a great file manipulation experience in a file explorer inside vim, which I also know is a case covered by plugins.
So yeah, sharpen the saw.