Vim is a customizable text editor geared towards writing code. But open-ended configuration possibilities lure users into wasting time while chasing after perceived minute gains in productivity. This article describes my Vim journey, starting from heavy personal customization and ending with a renewed love for defaults.
As a rule, new users, right after learning a few basic commands, start indulging in Vim configuration porn. Such porn has various different flavours. There is the “exhibitionist” variety which includes dressing Vim up in status lines, colorschemes, and font decorations in order to impress others. It often leads to Vim being displayed on videos and, nomen omen, “vim-porn” screenshots. There is an “integrated” category where people plug and stretch their Vim until it resembles an IDE. Git integration, autocompletion, language servers, and refactoring are the tools of trade in this version. And then there are a few more variants, including “fuzzy”, “packaged”, “conveniently re-mapped”, and I am sure anyone could think up some of their own.
My particular genre was “consistency” - I wanted Vim to be intuitive and predictable.
While working I would constantly keep an eye for digressions in Vim’s behaviour and then spend my time trying to adjust them.
In my fantasy Vim had to feel intuitive and smooth so I tried to correct every little detail.
And, despite “consistency” porn being quite vanilla, I managed to construct a vimrc
file exceeding one thousand lines of code.
No point in sharing the entire collection, but here are a few illustrative examples and the rationale behind them. Feel free to try them out, if that is your thing.
Y
As is widely known Y
is not consistent with other uppercase commands.
It copies the whole line while D
and C
operates from the cursor position to the end of line.
Simple remap fixes the issue.
" make Y consistent with C and D.
nnoremap Y y$
n
and N
Having the same characters always go in the same direction would be a lot easier.
So this map makes n
always go forwards and N
always go backwards(1).
Same for ;
and ,
(2).
" make n always search forward and N backward
nnoremap <expr> n 'Nn'[v:searchforward]
nnoremap <expr> N 'nN'[v:searchforward]
" make ; always "find" forward and , backward
nnoremap <expr> ; getcharsearch().forward ? ';' : ','
nnoremap <expr> , getcharsearch().forward ? ',' : ';'
;
and ,
I disliked the forward and backward f
repetition being under different characters: ;
and ,
.
This set of remaps puts them under the same key and moves away the colon command over to backtick.
The original backtick behaviour is transferred to a single quote.
" make ; and , be on the same key
nnoremap : ,
nnoremap ` :
nnoremap ' `
j
, k
, h
, l
This set of mappings makes the j
, k
, h
, and l
keys perform consistent direction-related action in various circumstances: scrolling, changing and creating windows, moving across paragraphs.
" make J, K, L, and H move the cursor MORE.
nnoremap J }
nnoremap K {
nnoremap L g_
nnoremap H ^
" make <c-j>, <c-k>, <c-l>, and <c-h> scroll the screen.
nnoremap <c-j> <c-e>
nnoremap <c-k> <c-y>
nnoremap <c-l> zl
nnoremap <c-h> zh
" make <a-j>, <a-k>, <a-l>, and <a-h> move to window.
nnoremap <a-j> <c-w>j
nnoremap <a-k> <c-w>k
nnoremap <a-l> <c-w>l
nnoremap <a-h> <c-w>h
" make <a-J>, <a-K>, <a-L>, and <a-H> create windows.
nnoremap <a-J> <c-w>s<c-w>k
nnoremap <a-K> <c-w>s
nnoremap <a-H> <c-w>v
nnoremap <a-L> <c-w>v<c-w>h
J
and K
.
If you think about how Vim moves across sentences and words you will notice it doesn’t place the cursor on the empty space between them, but rather on the first and last characters.
Similarly this bad boy makes J
and K
commands place the cursor on the outermost lines while moving from paragraph to paragraph.
Consistent.
" move to the edge of a paragraph.
function! s:Move(isUp, isInVisual)
if a:isInVisual
normal! gv
end
let curpos = getcurpos()
let firstline='\(^\s*\n\)\zs\s*\S\+'
let lastline ='\s*\S\+\ze\n\s*$'
let flags = 'Wn'. (a:isUp ? 'b' : '')
" Move to first or last line of paragraph, or to the beginning/end of file
let pat = '\('.firstline.'\|'.lastline.'\)\|\%^\|\%$'
" make sure cursor moves and search does not get stuck on current line
call cursor(line('.'), a:isUp ? 1 : col('$'))
let target=search(pat, flags)
if target > 0
let curpos[1]=target
let curpos[2]=curpos[4]
end
call setpos('.',curpos)
endfu
" H K L J for moving to paragraph edge
nnoremap <silent> J :<c-u>call <sid>Move(0, 0)<cr>
nnoremap <silent> K :<c-u>call <sid>Move(1, 0)<cr>
vnoremap <silent> J :<c-u>call <sid>Move(0, 1)<cr>
vnoremap <silent> K :<c-u>call <sid>Move(1, 1)<cr>
onoremap J V:call <sid>Move(0, 0)<cr>
onoremap K V:call <sid>Move(1, 0)<cr>
ge
To understand the inconsistency with ge
consider how Vim behaves across 4 distinct commands:
Cursor position Command Result
------------------------------------------------
word1 word2 de wo word2
^ dw woword2
word1 word2 db word1 rd2
^ dge wordd2
Note that dge
is inconsistent in two ways: first, unlike db
, it deletes the character under the cursor; second, unlike dw
, it deletes the last character of the word it moves towards.
Remap below made it more consistent.
" make commands with ge consistent with b and w
onoremap ge :execute "normal! " . v:count1 . "ge<space>"<cr>
&
Here remap makes the substitution repeat command reuse the flags of the previous invocation.
" make substitution repeat to reuse last flags
nnoremap & :&&<cr>
xnoremap & :&&<cr>
Let that be the end of examples. I had many more but you probably get the point by now. The simple truth is that my thousand line vimrc file didn’t give me any comfort. The feeling I had when I opened it up is best described as a kind of anxiety. I often felt a weird push to improve it, organize it better, make it more consistent. Until one day I had enough and pressed delete.
So, no more vimrc. It did feel sloppy, at first. But I quickly found a new way to get around.
Some of the issues with highly customized configuration files are known to everyone. You have to use a plugin to manage your plugins; have to transfer dotfiles across different servers; lose the ability to work on machines without your configuration files; others cannot use your computer without disabling your setup first. Removing the heavy configuration got rid of those downsides. But I also experienced a few additional less talked about benefits which are listed below.
Reduced confusion
When my vimrc grew, it became harder to remember all the maps and remaps that were in there. So, when adjusting configuration, sometimes I would overwrite my previous remaps and other times I would apply the same “consistency fix” twice, in two slightly different ways, ending up with two different keys being remapped to the same operation.
And proper fixes are often hard, harder than you might think.
Take even the simplest example: making cw
consistent with dw
.
Maybe some haven’t noticed but in Vim cw
behaves like ce
: it changes text to the end of the current word, not to the beginning of the next word.
It looks like an easy thing to change but the best known implementation is close to 20 lines of vimscript(3), and there is no guarantee that it is perfect.
You always have to track various edge cases and add checks for different modes, empty lines, end of file situations.
Without such careful treatment the fix will be inconsistent itself.
In addition some consistency bandages themselves introduced inconsistencies in other places.
As an example take the remap between colon and comma.
Remember - I remapped colon to be consistent with semicolon and transferred the original command-line operation to backtick.
But this left g;
and g,
unadjusted which is, you guessed it, still inconsistent.
Even if we adjust them the ":
register is still there.
So, to redo the last command-line operation I still have to call @:
even if my command-line operation itself is now under backtick.
Those inconsistencies never end.
Increased flexibility
I became accustomed to my simplified mappings and slowly but surely lost the ability to control more complex commands.
A good example is my <leader>R
map:
nnoremap <leader>R :belowright 20split | terminal R --no-save --no-restore<cr>
It opened up a :terminal
buffer below the current window, set it to 20 lines in height, and launched R in it.
Sweet little command, right?
Well yes.
But it only allowed me to open R in a fixed size window.
It somehow took away my knowledge of opening a window anywhere, in any size, and running any command in it.
I got used to this simplicity so much I was no longer able to open python in a 30 line height window without checking the syntax of my R remap in the vimrc first.
Using registers is another good example.
One map I had was <leader>y
which copied the text into the system clipboard.
Simple, convenient, and fast.
When it was no longer there I was forced to issue the full command, which is "+y
.
It may be slightly less convenient but seeing and using this syntax makes it clear what is going on and it naturally encourages the use of different registers.
When I got in the habit of yanking text into the +
register my usage of custom registers became much more frequent as a side effect.
A new side of Vim, previously layered by my custom remaps, opened up and expanded.
Similar thing happened with yanking text and using it to replace another piece of text.
It is a common enough procedure and so I turned to plugins for help.
Using a plugin(4) was convenient.
It was so convenient that I didn’t have to think about it, just yank text, issue an operator, define a motion, and bam - the replacement took place.
After I removed this plugin I had to look for ways Vim can achieve the same task.
And here I had to use registers again.
So a command like cw<c-r>0
would replace the word with contents from the yank register.
This in turn made me accustomed with pasting text from various different registers.
Combine the two register operations described above: yanking into register and pasting from register, and you will see how much more flexible the original approach is. My remaps were too convenient. And as a result they kept me away from becoming familiar with the behaviour of more general default Vim commands.
Understanding defaults
If you read my snippets above you noticed that I payed a lot of attention to j
, k
, h
, and l
keys.
Those were the keys that got me around in my vim beginner days.
So naturally my early configuration attempts started to enhance them.
Surely if j
goes down by one line, why shouldn’t J
go down by paragraph?
I remapped those keys and they became my way of navigating in Vim.
I used j
, k
, h
, l
, and their combinations all the time.
Need to scroll the screen down? I can just jump few paragraphs with my J
key.
Need to place my cursor at a certain position in a paragraph? A few jumps across paragraphs then a few adjustments with lowercase j
, l
, and I am there.
However, when you inspect default Vim motions, those four letters appear to be no more than an afterthought.
They are not as important as a beginner might imagine them to be.
One should be using other means to get around, such as searching, jumping through methods, and navigating by marks.
I placed j
, k
, l
, and h
, at the center of my Vim experience but there was no reason for it, other than my incorrect initial assumptions about how one should do things in Vim.
On top of that some of my perceived inconsistencies were imagined.
At first after I deleted my whole configuration I still tried to keep a few of my “better-thought-out” adjustments in it.
One of them was a direction fix making n
always go forward after search and N
always go backward.
Trying to preserve this map also gave me one of the bigger ‘aha’ moments during this experience.
At this point I no longer had the constellation of my movement remaps and so I was trying to get around using search more.
In a particular situation I wanted to reach the word a few paragraphs above my cursor and so I pressed ?
, started typing the word, and pressed return.
However, in between the cursor and my target there was another occurrence of the same word so my search landed there instead.
While looking at my target and seeing that my cursor got stuck midway, without even thinking about it, I pressed n
.
And, since my n
remap was still in place, my cursor went backwards from where I wanted it to go.
What happened!?
At this point I have been using the adjusted n
and N
for years but the moment I started using Vim without my configuration the original default behaviour was immediately more intuitive.
I simply looked at my target, tried to go there, it wasn’t enough, and the n
key was a natural “move more towards where I need to be” command.
Needless to say it made me drop the remap along with its ;
and ,
equivalent.
Finally some of my configuration was caused by a slight misunderstanding of the real purpose behind certain commands.
As an example I was using *
and #
to search for a word under the cursor.
In such context my remaps made it so that the cursor didn’t initially move after pressing *
and #
but highlighted all the occurrences of the word instead.
Then I would navigate the searches like I normally would using n
and N
.
However, after loosing all those mappings, I started looking at the same keys differently.
The action performed by *
should really be considered as jumping to the next instance of the word.
This makes a particular sense when writing code: you consider adjusting some variable and want to see where is the next place it will have an impact.
Other extensions of the same procedure are gd
, [<c-i>
, and ]<c-i>
.
By changing the behaviour of *
and #
I crippled their original functionality and made myself use an additional n
key to achieve the intended function.
Getting to know Vim
Finally, the biggest reward I got out of this experience was getting to rediscover Vim. It had a drastic effect and changed the way I approach my editor. I started to look at Vim as a line editor first and a text editor second. Indeed, if you look into it, the original intention of Vi was to serve as a visual front for a line editor called ex. And I think it shows. What it means in practice is that Vim encourages you to start your edits with the cursor placed at the start of the line. When you are in this place everything goes smoothly. And when you go against the grain all kinds of bad things start happening.
This might be a bit difficult to convey in words so lets compare two similar examples, one with cursor being at the beginning of line and another where the cursor is at the end of line:
1. fullname = ("name" . "surname")
^
2. ("surname" . "name") = fullname
^
For starters, if you start editing from the beginning of line you will be less troubled by the distinction between inclusive and exclusive motions.
That is - the character the cursor is on will always be included in the motion.
It is not the case when you try to apply a motion backwards.
Just compare the two seemingly equivalent commands for deleting to the end of the word in the two scenarios above: de
in first and db
in second.
Did you notice how db
left the e character unaffected?
This is because the motion is exclusive and it does not include the character that appears further in the buffer, which in this case is e.
You can have that in mind and correct for it using dvb
instead but the point stands - it is more cumbersome.
Second, forward moving keys are often placed more conveniently.
Go to the beginning of the next word - w
, to the end - e
.
These two characters are close to each other.
Compared with them the analogous backward motions are out of place: b
and ge
.
The forward motions are also frequently mapped to lowercase letters which translates into fewer keystrokes.
Go to the first quote when the cursor is on the beginning of the line? f"
, do the same from the end of the line? F"
.
Third, when the cursor is on the start of the line you are given a lot of convenient operations that do not apply otherwise.
Change within the first quotes? - do ci"
without having to move the cursor first.
Can’t do the same in the second example.
Change the value of fullname
variable? f(D
in the first example, F)dv0
or T)d0
in the second.
All the convenient shortcuts (like uppercase C
and D
, i"
, %
, etc.) only really apply when your cursor moves forwards.
Fourth, speaking about convenience, some of the inconsistencies within Vim start to make more sense if we think about the direction.
There is a reason Y
copies whole line by default.
Think about writing code - how often do we really need to copy a piece of code from a middle of the line up to the end?
Not so often, compared to the times we want to change the same part.
Hence, the authors of Vi probably thought that making Y
command consistent with C
would be a waste of a key.
In addition think about '
command which doesn’t jump to the exact place of a mark but only to the beginning of a line that the mark is on.
Why this key is there when a more precise alternative, the backtick, already exists?
The intention, probably, is to provide you with a way to start your work at the start of the line, if you so choose.
Fifth, remember the last time you created a macro. If it was intended to be repeated over multiple lines you probably took care in making sure the cursor was placed at the beginning of the line just after the start of the recording. Again the reason is the same, doing so brought you less trouble and made the macro more general.
Sixth, there is a set of Vim commands that do not extend over the boundaries of a line.
f
and t
are the most obvious examples but they are not the only cases.
You can inspect :help whichwrap
to see more.
The purpose of all these commands is to make your edit contained within a single line.
Vim seems to consider this a feature.
There are a lot of various other details like that, too many to mention.
They include things like “line undo” command: U
, +
and -
movement commands that move across lines by always placing the cursor at the start, the convention that pressing a key twice will execute it on a whole line, etc.
All of these pieces, taken together, leave me with an impression that Vim expects us to perform the operations line by line and encourages us to start our edits at the beginning of a line.
So, after all the trouble, do I have regrets about deleting my thousand line creation? No. The simple truth was that, no matter how much energy I invested in my custom configuration, the people who built and designed Vim knew it better than I did. Implemented defaults and design was there for a reason. Some of my perceived inconsistencies were products of my limited understanding. I made assumptions about what a particular command should do, when it should be used, and how. But those assumptions were not always warranted.
For a brief period I even went into the opposite direction and tried to make my vimrc as minimalistic as possible. This was an exercise in asceticism and it produced a similar negative effect, except in another direction. I still cared too much about my vimrc, only this time, instead of making it more consistent, I was striving to make it as light as possible. A similar paradox happens in some Zen practices where a disciple, being told he shouldn’t attach to things, starts attaching to non-attachment. The correct attitude, at least in the case of Vim, is to let go of the need for control. I learned to trust the choices of people who created Vim and to not interfere with their design. In the long run this attitude saved me from an uphill battle between me and my vim configuration. In the long run it helped me stop wasting my time.
Now, after all the struggle, my vimrc is around 50 lines in length.
It only includes simple and frequently used commands, like this map to toggle search highlighting with ctrl-/
:
nnoremap <silent> <c-_> :set hlsearch!<cr>
Various boilerplate procedures that I previously couldn’t live without remain deleted.
This includes things like setting relative numbers or opening a :terminal
buffer.
I started to look at those commands as a sort of a ritual that I have to perform before editing.
It takes a couple of seconds but it gets me in the mood for work.
Thanks to /u/-romainl-, /u/dutch_gecko, and /u/htranix from /r/vim reddit thread for corrections.