Moving to modern Neovim

By Abhijit Menon-Sen <>

; updated

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.

Screenshot of nvim-treesitter-context with coloured function/block context at the top of the screen

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.

Screenshot of pyright LSP diagnostics popup in Neovim

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.