I was using Vim all my professional life but I’ve never made an effort to use conscientiously. I’ve just copy-pasted someone’s config, installed some random plugins and tried to live with it, grumbling in the background when things went not the way I wanted.
It came to a point when I switched to the Visual Studio Code because I wanted a more integrated experience. And I quite liked it! Mainly it’s because its Vim emulation is the best across all the editors including Atom, Sublime and JetBrains products. This is very important to me because I strongly believe that Vim editing language is superior to anything else.
So I’ve used the VS code with Vim mode (of course) for a while but from time to time I missed some Vim features like flexible splits.
And so I decided to revamp my Vim setup. But this time I made it differently.
I introspected my workflow and tuned Vim to the way I work. Not the other way around where you change your habits to work around editor setup. And I encourage you to do this yourself regardless of your editor.
Disclaimer: My setup may seem wrong to you but that’s because it’s tailored to my needs. Don’t blindly copy-paste my config – read the help, think and make it yours.
Here is the quick outline of what I did:
Let’s do this one quick – I use Neovim. I think it’s the best thing happened to the Vim community in the last decade. I like the project philosophy and that it rattled up Vim and now Vim 8.0 has adopted ideas from Neovim like async job control and terminal.
To install Neovim I recommend using AppImage. You just download the single file and run it. No libs, no containers, nothing. It also allows me to run the latest version hassle free. I’ve never used appimage before and thought that it would distribute as some kind of container image but it’s actually a good old binary:
$ file nvim.appimage
nvim.appimage: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.18, stripped
After installing Neovim you should really run :checkhealth
and fix top issues
– install the clipboard and python provider.
Next, read the help for Neovim setup – :h nvim-from-vim
. I’m doing it simple,
just put this
set runtimepath^=~/.vim runtimepath+=~/.vim/after
let &packpath = &runtimepath
source ~/.vimrc
to the .config/nvim/init.vim
and use the ~/.vimrc
for the configuration.
After that, let’s start digging into it.
What this gives you is the latest version of Neovim that’s not conflicting with anything and compatible with Vim.
IMO, Vim help is the most underestimated feature of Vim. I haven’t used it until this revamp and, boy, what I’ve missed! So many useless searching, reading silly blogs and StackOverflow could be avoided if I’ve used the help system.
Vim help consists of 3.7 megabytes of text, half a million of words
$ wc neovim-0.3.4/runtime/doc/* | tail -n1
90804 543942 3592651 total
Also, almost every plugin you install has its own help so these numbers are not final.
Vim help topics are comprehensive, detailed and cross-referenced. You may be overwhelmed at first because there is a lot of information here. But don’t be discouraged – it’s much much more efficient and useful to read and grasp comprehensive help topic than mindlessly searching for blog posts or StackOverflow. If you could only learn one thing from this post – please, learn to love the Vim help system.
Some tips that helped me.
:h patt
then TAB to find help on the subject starting with patt:h patt
then Ctrl-D to find help on the subject containing pattOr even better – read the :help help
which is help on help!
Let’s look at the example, if you type :h word-m
Vim will open help on word
motions:
==============================================================================
4. Word motions *word-motions*
<S-Right> or *<S-Right>* *w*
w [count] words forward. |exclusive| motion.
<C-Right> or *<C-Right>* *W*
W [count] WORDS forward. |exclusive| motion.
...
Here you can see the header Word motions
, its tag word-motions
that is used
as a subject for :h
command.
Next, you see the help itself describing word motions.
Note that there are some words that have some funky symbols around them or shown
in different colors. Anything that doesn’t look like the plain text is a help
topic by itself – you can jump into it by Ctrl-]
. So in this example, we could
find what is [count]
or what is |exclusive|
motion. And that’s enough for
efficient using of Vim help.
Here are the things that I’ve found in Vim help:
:h statusline
.
All the blog posts were just a waste of time.:h ins-completion
describes comprehensive builtin
completion system. Now, I’m using Ctrl-X Ctrl-F to complete filenames in the
current directory (useful to insert links in Markdown files). Also, whole line
completion with Ctrl-X Ctrl-L is useful for editing data files.:h window-moving
taught me that you can move splits
around, e.g. Ctrl-w H will move current window to the left (it will also
convert vertical split to horizontal). Also, the whole :h windows.txt
is amazing.Finally, I recommend to everyone familiar with Vim to review :h quickref
from time to time.
After I’ve learned to use Vim help I started to discover things that I’ve missed but that was always there.
Remember to check the help for each thing in this list – I’ve conveniently supplied Vim help command and a link to online help.
Auto commands allow you to tune Vim behavior based on filename or filetype. Basically, it executes Vim commands on events.
I use it to set correct filetype for some exotic files like this
autocmd BufRead,BufNewFile *.pp setfiletype ruby
autocmd BufRead,BufNewFile alert.rules setfiletype yaml
Or to tune settings for particular filetype like this
autocmd FileType yaml set tabstop=2 shiftwidth=2
Other editors required me to install full-blown extensions like Puppet extension or YAML extension but with Vim I keep things simple and lightweight.
This feature is so awesome yet none of the other editors have it.
It sounds simple – when you exit Vim your edit history is saved so you can open the file again 2 days later and undo the changes.
Edit history is an important part of your context so I think once you get used to it you couldn’t use any other editor without this feature.
To enable persistent undo I’ve done this:
set undodir=~/.vim/undodir
set undofile
Bliss!
This one is actually more of a hard fix than a feature.
Clipboard in Linux is a complicated story. All these buffers and selections don’t make things understandable. And Vim makes it even more complicated with its registers.
For years I had these mappings
" C-c and C-v - Copy/Paste to global clipboard
vmap <C-c> "+yi
imap <C-v> <esc>"+gpi
that makes Ctrl-c and Ctrl-v work.
But why use two-key combos when you can use a simple y
and p
for copying and
pasting?
Turns out, you can make it work very nice by using this single setting:
set clipboard+=unnamed
It makes y
and p
copy and paste to the “global” buffer that is used by other
apps like the browser.
What I like the most about Vim is that its normal mode allows you to use all keys for a command while others require to use some key combo based on modifier (Ctrl-o, Ctrl-s).
When you can use any key for a command it’s natural to use a single key
shortcuts, e.g. p
to paste the text.
And what is even more awesome is that you can map a key or a sequence of keys at your own will.
Here are my most used mappings:
nnoremap ; :Buffers<CR>
nnoremap f :Files<CR>
nnoremap T :Tags<CR>
nnoremap t :BTags<CR>
nnoremap s :Ag<CR>
NOTE: these mappings override default Vim motions and actions because I don’t use them. It may be better for you to map it via leader key. Anyway, read the help on what these letters do by default and decide whether you want to override them.
These mappings invoke fzf
command (more on this later) using a single
key.
If I need to go to some function I just press t
and got the list of tags of
the current file. Not Ctrl-t
, not Shift-t
, just t
. Combined with fzf
fuzzy find it’s very powerful.
For years I’ve been using Vim in a terminal without knowing that I’ve been using 8-bit colorscheme. And it was actually ok because 256 colors is kinda enough.
It’s worth noting that I’m using my own colorscheme called tile. While tuning some of the colors I didn’t understand why I don’t see the difference and then I’ve read the help on syntax highlighting and realized that I want true colors in Vim.
Also, most of the colorschemes that you see in the wild, e.g. on https://vimcolors.com/ are presented in the 24-bit colors. So you’ll be disappointed when you don’t see the same colors when you install the colorscheme in your Vim.
Also also, your terminal is almost certainly capable of displaying in True Color so why limit yourself to the 256?
It’s all boils down to the simple set termguicolors
in your vimrc. This
options simply enable true color for Vim. Here is the difference with my
colorscheme:
The last one is quick but so great that I even tweeted about it:
All of the things above already boosted my productivity but Vim can do even better when you know what you want.
In my case, here was the list:
fzf
ag
(the_silver_searcher
)So let’s dive in.
For me working with projects is about saving context – Open files, layout, cursor positions, settings, etc.
Vim has sessions (:help session
) that does all that.
To save a session you have to :mksession!
(or short :mks!
) and then to load
session start it with vim -S Session.vim
. It may be enough for you but I found
it kinda cumbersome to use as is.
First thing I’ve tried was to automate saving session. I’ve tried nice and
simple obsession plugin that does just
that. For the loading part, I’ve created bash alias alias vims='vim -S Session.vim'
.
This was OK but a few things were annoying. The way I work is like this: I have
multiple projects that are kept in separate directories as separate git repos.
If I want to do something I cd
into that dir, open the file, edit it or just
view, and then do something else.
When I was opening a file with Vim inside a directory session wasn’t applied, so
I had to manually :source
it. After doing this for a week it was obvious that
it’s not the way I wanted.
And then I’ve found an amazing vim-workspace plugin that does
exactly what I need. It creates a session when you :ToggleWorkspace
and keeps
it updated. Then when you open any file in the workspace it automatically loads
the session.
It also has very nice command :CloseHiddenBuffers
that, well, closes hidden
buffers. It’s very useful because during session lifetime you open files and Vim
keeps them open. With this single command you can leave only the current buffer.
So I settled on the vim-workspace and found peace.
Since the last time I’ve done Vim configuration, which was around 2008, a lot of things changes. But the most exploded sphere in Vim, from my point of view, was autocompletion support in Vim.
Vim gained sophisticated completion engine (:h ins-completion
)
with the omni-competion that gave birth to the whole load of plugins.
YouCompleteMe, OmniCppComplete,
neocomplcache/neocomplete/deoplete, AutoComplPop, clang_complete, …
It is complicated and I was exhausted while researching on this topic, so here is the shortest possible guide on completion plugins:
My choice is deoplete because it’s fast, versatile, and not heavy. If you want to keep things native, then I’d recommend using VimCompletesMe. I’ve tried to use YouCompleteMe, had some troubles with installation, gave it 250 MB and it just showed me the function names without signatures and argument names. So I was disappointed and switched to deoplete that provides more info.
For the Deoplete I’ve added a few completion sources:
There is also tmux-complete that can complete from other tmux panes. Like view logs in one pane and Vim in the other pane can complete the values from it! It works but I don’t use tmux much.
There is also webcomplete completion source that completes from the currently open web page in Chrome. Alas, it works only on macos. There is an open discussion about adding support for Chrome on Linux.
The ability to quickly open file is crucial to my productivity. And I need to
open a file by partial name. As an example, suppose I’m working in some ansible
repo. I know that I have a template file for setting environment vars. I don’t
remember exactly the full path but I know that it has env
in it.
So I use fzf
to sift through the list of file in the project that is generated
by ag -l
. Here is how it works live:
There are other plugins that do that like
CtrlP but I use fzf
for other things
– list of buffers (open files), search, git commits, list of tags, history of
search and history of command. Anything that should be sifted through is piped
to the fzf
because it does this job really well.
File find is launched with a single letter command f
in the normal mode.
Before this revamp I’ve used builtin /
Vim command to search in the current
buffer and :Ag
to search in the files. I really like ag
– it’s fast and
very handy.
After I’ve embarked on the fzf
I hooked Ag output to it and now it works even
better:
File search is launched with a single letter command s
in the normal mode.
This was my long wished dream – when I stumble on some function I want to see its callers. Sounds simple but it’s a difficult task. The only thing that can do it and that is not tied to an IDE is cscope.
But cscope is a, how to say nice, weird thing. It requires you to build its own database by supplying a list of files and then provides tui interface to interact with. Its documentation doesn’t help much and it feels that nobody uses it.
This idiosyncratic cscope workflow was the main reason why I occasionally opted for other editors and IDEs. Just to see if they have “find usages” implemented well.
But this time I said to myself – you have to make it work. And here is what I did.
First, I started with automatically generating cscope database. I use vim-gutentags for this – it generates ctags index and cscope database on file save.
Then to integrate cscope I’ve tried different things:
" cscope
function! Cscope(option, query)
let color = '{ x = $1; $1 = ""; z = $3; $3 = ""; printf "\033[34m%s\033[0m:\033[31m%s\033[0m\011\033[37m%s\033[0m\n", x,z,$0; }'
let opts = {
\ 'source': "cscope -dL" . a:option . " " . a:query . " | awk '" . color . "'",
\ 'options': ['--ansi', '--prompt', '> ',
\ '--multi', '--bind', 'alt-a:select-all,alt-d:deselect-all',
\ '--color', 'fg:188,fg+:222,bg+:#3a3a3a,hl+:104'],
\ 'down': '40%'
\ }
function! opts.sink(lines)
let data = split(a:lines)
let file = split(data[0], ":")
execute 'e ' . '+' . file[1] . ' ' . file[0]
endfunction
call fzf#run(opts)
endfunction
" Invoke command. 'g' is for call graph, kinda.
nnoremap <silent> <Leader>g :call Cscope('3', expand('<cword>'))<CR>
What it does is call cscope and feed its output to fzf. '3'
is the field
number in cscope TUI interface (yeah, you read it correct, :facepalm:)
corresponding to Find functions calling this function
.
This thing works – I pasted it to my vimrc and invoke it via <Leader>g
but it
needs to be packaged as a plugin. Maybe I’ll do this sometime.
Overall cscope feels like fucking dirt but we don’t have anything better.
I’ve got used to console interface of git because it’s stable, independent of any editor and it provides all features of git because it’s the main interface. And I’m very comfortable with this way of working with git.
So my requirements for Git was pretty little – actually, I wanted to explore how this integration could help my workflow.
First, I’ve tried fugitive but quickly found that it was not for me. It was not suitable for my workflow. The main problem is that it messes my windows layout by opening its own buffers with git output:
:Gstatus
I want to see the changes, so I invoke :Gdiff
. It
opens the diff in the closest window replacing buffer I was editing. That’s
OK but when I’m done with the diff I want to close diff and return to the
previous buffer. And this is where it gets complicated – diff is a 2 window,
so I have to return with Ctrl-o to the previous buffer in one window and then
kill the other buffer with :bd. This is really not convenient.:Glog
just spits git log output in messages.:Gblame
shows the standard git blame output and that’s OK. When I try to
view commit from blame it opens it in the current window, again messing with
my layout, and scrolls the commit to the diff of the chosen lines. This is not
what I want, I want to view the commit message and other related changes. The
scrolled part is what I already saw when I was doing blame.So I’ve ditched it and settled on vim-gitgutter because it’s nice and doesn’t interfere with my workflow. This plugin shows line status in the gutter. And it provides a motion for next/previous hunk.
Then I’ve tried to use vimagit and it’s great! This is what I really want for Git integration – a convenient staging of changes and writing commit message. Vimagit gives me a buffer with unstaged and staged diffs and a commit message section and simple to use mappings. Really great!
Finally, I’ve found git-messenger that shows blame info (with history) in the floating window.
Similar to Git this wasn’t a hard requirement because I’m doing building and linting from the shell or automatically in CI. But, again, I wanted to explore what could be done here.
I setup Neomake as a linting engine. It has a pre-configured list
of linters depending on filetype. I’ve configured it to run on only on buffer
write (it can be launched at an interval, at reading, etc.) to avoid useless
work. The count of warnings and errors of neomake run is shown in the in
statusline (see screenshot below ). And the results of linting can be viewed in
location list – :lopen,
:lnext,
:lprev.
Also, Neomake can invoke make program (:help makeprg
)
without blocking the UI so I’ve added this mapping and that’s it:
nnoremap <leader>m :Neomake!<cr>
The results of build are in the QuickFix list (:help quickfix
).
This plugin is a godsend for me. I use splits a lot and
sometimes I want to temporary zoom the current window. With this plugin, I just
do <Ctrl-w>z
to toggle the zoom. This is similar to the tmux
zoom feature.
vim-sensible provides sensible defaults like enabling filetype, autoread, statusline. But most important for me was this line
set formatoptions+=j " Delete comment character when joining commented lines
Commentary plugin adds actions to quickly comment line, selection or pretty much any motion.
Surround plugin allows me to easily add, change or delete “surroundings”. For
example, I often use it to add quotes to the word with ysw"
(I have a
mapping for that) and change single quotes to double quotes
with cs'"
.
So here I am, happily living with Vim for about 3 months now. I’ve intentionally waited from posting this to prove myself that my new setup is worth it. And, gosh, it is!
The main boost was getting comfortable with reading Vim help. Yes, I’m trying again to convince you about reading it because it makes you reason about what you do correctly.
And the key point is to tune Vim into your workflow, not the other way around.
Also, I’m tweaking things as I keep finding new ways to make my life in the
editor more pleasant. The recent one was set hidden
(:h hidden
) to
prevent nagging 'No write since last change'
message when switching buffers.
There is no magic here in Vim when you put some conscientious effort and try to do things your way.
That’s it for now, till the next time!