I've been using Vim for about twenty-five years now. I've always had a
fairly restrained configuration—a few mappings, a few plugins, and not
much in the way of exotic features. My .vimrc was just over a hundred
lines long, and changed infrequently enough that I didn't even bother
with revision control.
I switched to Neovim about a year ago (building it from source), but
until this week, I used it exactly as I did Vim. This worked remarkably
well, and I needed to change only
one line of my configuration.
I installed Neovim v0.5
when it was released last month, and used it for a few weeks while
reading about all the new features in it. Last weekend, I felt an
uncharacteristic urge to try them out, and I'm glad I did. Here's a
quick overview of what I learned.
Update (2021-08-28): There's also an update about
all the changes I made in the first
month after I originally wrote this article.
Treesitter
Treesitter is an
incremental parsing library
from Github. It is fast enough to run on every keystroke, and can bring
syntax-awareness to features that were characteristically regex-based
hacks in traditional Vim.
My work often involves
reading unfamiliar code,
usually after something has gone wrong. Using fzf to navigate using live
grep and tags was very convenient, but all the jumping around often made
me wish for a better understanding of where in the code I am at any
time. The
tagbar plugin used an
in-memory ctags database to show a "minimap" of classes and functions
in the current file, but was painfully slow to update in response to
moving around the file.
Treesitter makes it possible to do this quickly and accurately, as shown
by the
nvim-treesitter-context
plugin. (Treesitter also provides statusline text to display the current
context.)
Text objects
defined using Treesitter queries make it easy to reliably select an
entire function or swap the order of parameters, and other such
conveniences big and small.
Another example is
nvim-ts-context-commenstring,
which uses Treesitter queries to set
commentstring
depending on the language you're editing, so that your commenting plugin
(I'm using
kommentary now)
will switch comment styles for CSS or JS inside HTML, or HTML embedded
in JS.
Having immediate access to competent source code context information
makes it possible to implement many things that never seemed worth doing
with ad-hoc parsing in VimScript.
This is the feature that I'm most excited about, because of the breadth
of its applicability. I managed to go years without wanting to write a
Vim plugin, but I'm already looking forward to writing some plugins
based on Treesitter using the Lua API.
Language server integration
We have VS Code to thank for defining the
language server protocol, by which
any editor can request an external language server to analyse the source
code and provide immediate feedback on syntax or style errors, and
features like completion, refactoring, and context-sensitive help.
Editors can focus on editing text, and language servers can accumulate
language-specific expertise in one place. This also makes it possible
for editors to benefit from new code analysis features without changes.
For example,
pyright
provides static type checking in addition to the usual Python
diagnostics (and other language server features).
Features like these are not new to Vim. There have been many
plugins that displayed compiler or linter diagnostics in the editor
(e.g., Syntastic) or offered completion based on sources of varying
quality. Some recent plugins have even included LSP clients (e.g., ALE,
coc.nvim). I've used Syntastic and ALE before (but without an external
language server).
Neovim v0.5 comes with a builtin LSP client. There's still a fair amount
of configuration required (though much less than before), and you still
need some plugins (primarily
nvim-lspconfig
to simplify the configuration), and you need to install the external
language servers. But there's a pleasant consistency to the features
available, how they are configured and used across languages, and how
one interacts with the resulting diagnostics.
Of course, this consistency owes in large part to the concept of a
unified language server protocol in the first place; but a lot of work
has also gone into the Neovim implementation recently, and it shows.
I no longer need to use ctags for anything. No matter what code I'm
editing, I can just type gd to go to the definition of any symbol, or gr
to see who references it, or use [e and e] to move between LSP
diagnostic messages, no matter which language server they come from.
There's some overlap (perhaps even… a
synergy?
:-) between Treesitter and LSP features. Both can act as a completion
source, for example, and some pseudo-language servers seek to provide
language-independent code transformations using treesitter features.
Dare I look forward to using structural editing in my everyday life
someday?
Debugger integration
Like the LSP, VS Code also introduced a
Debug Adapter Protocol.
Neovim does not have builtin support yet, but the
nvim-dap plugin
supports it, in conjunction with a language-specific adapter like
nvim-dap-python.
The
nvim-dap-ui plugin
provides a basic debugger interface within Neovim, and there's also a
plugin to display local variables through
virtual
text.
I've installed all of this stuff and played with it enough to verify
that it's working, but I haven't had much of a chance to use it yet.
Package management
I used pathogen for
years and years, until I recently switched to
vim-plug at around
the time I started using
fzf.vim. Vim-plug is
excellent software, and I fully intended to keep using it with Neovim.
At first, I kept all the Vim-plug invocations in vimrc and
put only the "new stuff" in init.lua. When switching back and forth
became too annoying, I
called Plug from init.lua.
But as my configuration grew larger, it became annoying even to switch between the plugin
loading (wrapped between plug#begin and plug#end) and the configuration
code further down.
I switched to
packer,
which allows plugin loading and configuration code to be colocated, but
which needs more time to become as polished as vim-plug. I've had a few
teething troubles (which the Packer author helped me to sort out), but
it already works well, and is clearly improving steadily.
This is what the configuration looks like.
use 'tpope/vim-repeat'
-- Like context.vim, displays class/function/block context
-- at the top of the screen while scrolling through code.
use {
'romgrk/nvim-treesitter-context',
after = { 'nvim-treesitter' },
config = function()
require('treesitter-context').setup({
enable = true,
throttle = true,
})
end
}
Summary: Packer works and the configuration is easier to read. (Lua is a
nicer language to read anyway.)
Telescope
Had I not used ctrlp.vim and progressed to fzf.vim a while ago,
Telescope
would have blown my mind. As it is, however, it does all the things that
I'm already used to (mostly live grep and selecting files in git), and I
can appreciate how far it goes beyond those beginnings, both in terms of
what it can do, and what it makes possible.
Fzf may be faster and a tiny bit better at searching files, but
Telescope already has a lot of extra builtin features, and plugins to
add many more. It is written in Lua and runs inside Neovim, giving it a
distinct advantage over an external binary like fzf that relies solely
on filtering piped input. Composing together separate pickers, sorters,
previewers, and actions is a model that fzf doesn't (and can't easily)
support.
Telescope supports all of the things many features
(see update below) an fzf user might
expect, like buffers, commands, mappings, Git files and branches, and so
on; but it also has builtin support for LSP diagnostics and Treesitter
queries. I also found plugins to support the debugging functionality
described above, and to use the Github CLI, of all things.
I do have one complaint about using Telescope in general. Many builtin
pickers and plugins provide actions (e.g., git_delete_branch) that have
a default key mapping. For example, you can use C-a to "approve" a PR in
the Github plugin, but I usually don't remember what the mappings are. I
wish Telescope had a consistent mechanism to discover them, like g? in
Fugitive windows. (There's now a
WIP
pull request to implement this.)
Completion
In the past, I've tried out Vim's builtin completion support, and
various completion plugins (e.g., YouCompleteMe), but never used them
much in practice. This time around, I wanted to try out LSP-provided
completions.
There are many completion plugins available for Neovim, but these days
nvim-compe seems to
be the obvious choice replaced by nvim-cmp (see update below).
It can handle LSP completions and complete paths, buffer contents,
spellings, snippets, and some other stuff besides. Everything I tried
worked well, and it's not obtrusive once I turned off autocomplete.
(Strictly speaking, one can use LSP completions without a plugin, with
builtin completion support. But I wanted to try nvim-compe anyway.)
Status line
When I first started using Neovim, I couldn't find a way to turn off the
default statusline, but I got used to it after a while.
Not surprisingly, there is a profusion of statusline plugins that let
you assemble increasingly eye-searing status lines. I saw
lualine mentioned
somewhere, and I tweaked its behaviour and
appearance to suit me, and it
works just fine. It displays the filename, cursor position, and
treesitter status line (if any). I haven't tried any other plugins, so I
don't know how they compare.
To underscore my commitment to modernity, I've just added the name of
the current git branch to the statusline too.
Key mappings
Another new plugin I really like is
which-key.nvim.
If you type ^W and pause, it will popup a summary of the next keys you
can press and what they will do. It can do this for operators,
movements, and other mappings (builtin or user-defined). For me, it also
supersedes
peekaboo,
which showed register contents if you typed " and paused; it also works
with spelling suggestions.
I never had many custom mappings earlier, but I need them with my new
setup (especially for Telescope and the LSP/DAP plugins), and a reminder
is helpful. Setting
timeoutlen
to 700 means the which-key popup doesn't appear often—only when I start
typing a mapping and pause halfway to try to remember what comes next.
If you just keep typing, the popup disappears with no fuss.
What's especially nice is that it behaves sensibly with no
configuration. Even if you don't register your mappings using the
which-key interface, it tries hard to display a useful hint (e.g., the
name of the function that will be called). But you can also do this to
get nice titles:
require('which-key').register({
["<C-f>"] = {
"<cmd>lua require('telescope-files').project_files()<CR>",
"Find files",
},
["<C-b>"] = { "<cmd>Telescope buffers<CR>", "Buffers" },
["<C-g>"] = { "<cmd>Telescope live_grep<CR>", "Live grep" },
["<C-t>"] = {
name = "+Telescope",
["<C-t>"] = { "<cmd>Telescope builtin<CR>", "Builtins" },
h = { "<cmd>Telescope help_tags<CR>", "Help tags" },
},
})
Other plugins
I discovered
unicode.vim, which
offers completion of Unicode characters based on their name, as well as
a UnicodeGA function that will identify characters (like ga). It tells
you if there are any digraphs to type the character, and what to search
for to find it in a document.
'₹' U+20B9 Dec:8377 INDIAN RUPEE SIGN (RU) ₹ /\%u20b9 "\u20b9"
I was never a heavy user of snippets, but I had
UltiSnips installed,
with a few small snippets. I investigated modern alternatives like
LuaSnip, but I didn't
want to translate my snippets to Lua, and the fact that there is a
Telescope plugin for UltiSnips
made me stick with UltiSnips (see update).
I don't use file managers much since installing fzf.vim, but
NvimTree is an
alternative to
NERDTree. It's not
as polished, but it does provide more file management features and is
actively developed (NERDTree is not). I now use
Rnvimr, which pops
up a full Ranger instance
inside Neovim.
Octo.nvim provides
a Telescope-based interface to the
Github CLI. I find it a bit
overwhelming still, but it works, and being able to open, review, and
merge issues and PRs from my editor is an interesting prospect, not to
mention reacting with a rocket emoji on PRs.
Appearance
Everything is too colourful by default for my taste (in Vim or Neovim).
I disabled all colours in Vim decades ago. I found all the colour
schemes too violent on the eyes, and even with muted colours, I didn't
like syntax highlighting because it was all based on regex hacks. I
especially disliked things constantly changing colour when I switched
modes or typed stuff.
Did Neovim cure me? Nope. But I wrote my own colour scheme! Out of all
the things I've done with Neovim recently, this is the one that
astonishes me the most.
Lush.nvim helped me to
start from scratch and apply judicious tweaks to individual highlights.
I now have subtle helpful touches (e.g., LSP signs and misspelled words
are coloured, and my statusline is a constant beige in every mode, with
no distracting colour changes), but everything else continues to look
reassuringly off white-on-black.
I have my terminal
configured to use
Source Code Pro,
and I sometimes see ✗-ed out little boxes instead of icons (e.g., in
NvimTree or Telescope). It's possible to fix this by
patching the font,
but I haven't bothered to try just downloaded a
patched font from nerdfonts.
I never liked gvim, but I wonder if
neovim-qt is any
better. It would be nice to not have to struggle with Unicode support in
terminals in 2022.
Giving up on clipboard=autoselect
This was the only thing in my vimrc that
didn't work at all with Neovim.
Setting clipboard=unnamed and mapping <LeftRelease> to yank mouse
selections into the * register is a perfectly serviceable workaround.
Update
In the weeks since I wrote this article, I've made many small changes to
my Neovim configuration. Here's a quick summary.
I switched from nvim-compe to
nvim-cmp, a pure-Lua
plugin by the same author, redesigned for better LSP completion support.
It's not yet as well-documented or tested as its predecessor, but it's
worked well for me.
I switched from UltiSnips to
LuaSnip and rewrote my
few snippets in Lua. I don't use any of the exotic LuaSnip features like
automatically-updating snippets, but I like the possibilities.
I use null-ls
to integrate standalone diagnostic and formatting tools (e.g.,
shellcheck and
black) into the LSP setup.
There are other good choices here, like
nvim-lint and
efm-langserver,
but I like the way null-ls works.
I added the
lsp_signature
plugin for signature help (displayed as virtual text), and
symbols-outline
to display a tree view of symbols in a buffer (like Tagbar, but
LSP-driven).
I realised that Telescope does not implement some basic features I used
in fzf.vim, like being able to mark and open multiple files, or being
able to specify both search patterns and filenames to grep. There are
also a few annoying bugs, but I'm confident all of this will be sorted
out in the not-too-distant future.
Finally, one change I didn't make: setting
statusline
manually got me to about 90% of the (little) functionality I wanted, but
I didn't want to put in the effort needed to handle inactive and special
(e.g., help and quickfix) buffers properly. Lualine takes care of it
all for me.
Summary
It took some patience and experimentation to settle on the right bits
and pieces that worked well for me. Even though a friend described this
as a "Frankenvim", everything I used to rely on still works, and I have
some nice new features besides. I'm glad I put in the effort.