My friend Jim recently made a tragic mistake. On a rainy December afternoon between tickets I received this text in our group chat:
Might be mostly for Zach but I got a clean slate to start working in vim again
finally. Where to start from scratch?
I felt the electric thrill I imagine trapdoor spiders feel at the first tremor of the ground around them, and immediately began to write this write up.
If you’re reading this and you’re not Jim: Welcome! If you’re looking for a super easy way to setup modal editing, Zed has a great emulator, and kickstart.nvim is a great out of the box configuration that tries to be easy to update.
A side note: I’m sure there are some seriously fantastic “Nvim distributions” out there, I’ve just never happened to use any of them. I started with Vim in 2018 and had a one file configuration with a couple plugins using vim-plug, and since then, every change to my config has been incremental, so I basically missed the wave of ever needing an out of the box configuration, and by the time I had become confident enough to not define my self worth by my dotfiles, I had amassed a certain amount of muscles memory and comfort in the nest I had made for myself so as to make switching to anything else uncomfortable. But if that doesn’t describe you, then those might work great for you.
First things First.
We’re gonna be using NeoVim, not Vim. Installation is available through almost every package management system on every OS, just make sure you have the latest stable version. Neovim doesn’t change versions much anymore, but there was a dark time when stuff changed a frequently and a significantly and it made the community of smaller plugin authors a bit cavalier about backwards compatibility. Most stuff works very seamlessly across versions, but there’s no reason to make more potential work for yourself.
Neovim uses lua or vimscript (or technically anything that conforms to the lua spec for object files, which is so cool, but not relevant right now unless you want to write your vim settings in C, Rust, Zig, or another LLVM language), and lua is amazingly better than vimscript, especially now that pretty much all of the vim interaction api is first class lua. Defining a text object or an operator can be a little tricky (unless you use yop.nvim ;) but we’re not doing anything like that right now.
It would be smart to glance at a Learn X in Y style guide on lua for ~5min, and to have it open while you work for reference. It’s meant to be a super easy language, but has a couple quirks (array indexing, 2 ways to iterate over collections, everything being a table) that might take some time to get the hang of.
Settings
Everything I’m going to be talking about from now on is going to be within
~/.config/nvim/
on Linux and Mac. The location of this dir might be different
on Windows, I don’t know. You also don’t need a .vimrc
in your home directory
anymore, and just to make sure it doesn’t interfere with anything, it’d be smart
to slap a .bk
on that bad boy if you’ve got one.
The first thing you’ll want to put into your config dir is an init.lua
file.
This is your new entry point. You can put literally everything in this one file,
but since I’m not one for pedagogical contrivance, fuck that, we’re splitting
shit up like adults. In the config dir, you can also make a dir called lua
.
Files and directories in here can be require
d from anywhere in your neovim
config, but they aren’t loaded by default. If you think I lie like a dog on a
rug, you can test this out by putting a file in the lua dir called
zach_lies.lua
the lua/
and putting vim.print("I told you so")
as the top
line. Now load up neovim (by typing nvim
in your terminal), and nothing will
print. But, if you enter command mode with :
and type lua require("zach_lies")
, then your command prompt will reply with I told you so
(press enter to dismiss this).
See how much you learned just now? You learnt about calling the print function
vim.print
which will be super useful for debugging. You learnt about the
runtime path of lua files for nvim. You learnt that to require a file, you leave
off the lua
part, and that the lua/
dir is the root of the path, AND that
the files aren’t run automatically except for the init.lua
! I guess I am into
pedagogical contrivance! And lying!
Cool now we can start configuring things.
Configuring Things!
There’s one setting that’s fairly important to configure first, as in, above
everything else in the file, and that’s your leader
key. My leader key is
space
and so at the very top of my init.lua is vim.g.mapleader = " "
. Pick
whatever leader you want, but it has to be first because keymaps that use the
leader, need to know it’s actual value when it’s defined. Another way of saying
this is that mapping expansion isn’t done by reference, it’s evaluated once, and
stored in it’s expanded form.
Cool! Now we get to tweak things. I’m gonna go roughly in this order:
- Things we configure outside of any plugin
- settings that configure neovim itself
- keymaps that don’t reference a plugin
- autocmds that don’t reference any plugins
- plugin stuff
Feel free to jump around, I’m not your dad.
Vanilla settings
You can find my settings file
here
but you can ignore most of it if you want, or just use one you had before.
I’ve put everything in a file called settings.lua
and required it in my
init.lua
. Try that out, or just put these settings in your init
.
Line numbers
The fastest way I’ve found to jump to a specific line you can see on the screen
consistently in vim is to move by line numbers. You can do this with absolute
line numbers (the default setting) by saying <line_number>G
but this can get
tedious in long files. Instead, you can just use relative line numbers which
show you how far a given line is from the line you’re on right now. e.g 34 lines
up. If you just set relativenumber
to be true, though, the current line will
show as 0
which is useless, so first you have to set number
to be true, as
seen in this snippet:
vim.opt.number = true
vim.opt.relativenumber = true
Now, the line you’re on will show you the actual absolute line number, and you’ll still be able to see the relative line numbers. The fact that you have to set both is for historical reasons and is best to be forgotten immediately.
Splitting
Intuitively, I expect splits to put the new window on the right, and/or below the current window. To get that to happen in vim you have to set
vim.opt.splitbelow = true
vim.opt.splitright = true
Clipboard
This is some peoples downfall. Vim, by default, doesn’t paste from and yank to the system clipboard. To make it all use one clipboard, set
vim.opt.clipboard:append("unnamedplus")
Vanilla Keymaps
Here’s some nice keymaps I use for window nav that require no plugins.
local map = vim.keymap.set
map("n", "<Leader>wl", ":rightbelow vsp<CR>", { silent = true, desc = "Split right" })
map("n", "<Leader>wh", ":leftabove vsp<CR>", { silent = true, desc = "Split left" })
map("n", "<Leader>wk", ":leftabove sp<CR>", { silent = true, desc = "Split up" })
map("n", "<Leader>wj", ":rightbelow sp<CR>", { silent = true, desc = "Split down" })
map("n", "<C-j>", "<C-W><C-J>", { silent = true, desc = "select window below" })
map("n", "<C-k>", "<C-W><C-k>", { silent = true, desc = "select window above" })
map("n", "<C-l>", "<C-W><C-l>", { silent = true, desc = "select window to the right" })
map("n", "<C-h>", "<C-W><C-h>", { silent = true, desc = "select window to the left" })
map("n", "<Leader>wL", "<C-W>L", { silent = true, desc = "Move window right" })
map("n", "<Leader>wK", "<C-W>K", { silent = true, desc = "Move window up" })
map("n", "<Leader>wJ", "<C-W>J", { silent = true, desc = "Move window down" })
map("n", "<Leader>wH", "<C-W>H", { silent = true, desc = "Move window left" })
Plugins!
Now the main event. Let’s start installing some plugins. First off, you technically don’t need a plugin manager, but damn does it make life easier. If it’s been a while since you’ve configured neovim from scratch you might remember Packer as the preferred plugin manager, but now the undisputed king is Lazy. Made by a guy named Folke, who is sort of the neovim equivalent of Tim Pope (but less mean usually), Lazy is a great package manager, but be careful, folke also publishes lazyvim which is a neovim “distribution”. I’ve heard it’s good, but not what we’ll be using.
This is Lazy.nvim’s repo while this is
the documentation. Use the documentation
to install lazy, I recommend the structured setup
, and hey! It uses the old
lua/
directory from way back at the top of this post! Neat!
Lazy’s docs are great and are worth a thorough read, but I’ll summarize a
couple points that are worth noting. With the amount of functionality that a
modern editing experience needs, and to keep any sense of speed when loading a
file or using the editor, you basically need some type of lazy loading system.
Previously, this was tough because if a plugin got used by another plugin
before it was loaded, all hell would break loose, but Lazy does this clever
thing where it shims itself into the require
builtin, and when a module
is required checks to see if it exists in lazy’s registry of unloaded plugins.
If it does, then lazy loads that plugin into the runtime path, runs whatever
configuration callbacks you specify, and then let’s the original require
proceed. Pretty cool!
Essentially, lazy has two callbacks that you can use to configure stuff:
init
is the callback that always runs on nvim startup. A common pattern is to create User Commands, Autocmds, or keymaps here thatrequire
the plugins modules in their function body. This means they get loaded when used, but not beforeconfig
is run at plugin loading time, and is where you most often callrequire('plugin').setup()
if the plugin requires it. This is a super common pattern.
Plugin Specs
Lazy’s plugin specification is all data driven. You construct lua tables in files in a certain directory, and Lazy reads, coalesces, and inspects the data structure made out of all of your files/tables in there. Let’s look at a real plugin spec of mine to get a better idea of how it works.
Spectre is a neovim plugin that does structural search and replace, and it’s spec in my configuration looks like this:
The first thing you should look at is the string value that’s the first entry in
the table. windwp/nvim-spectre
is the github user and repo that the plugin can
be found at. You can also specify a url
key if your plugin isn’t on github,
e.g url = "https://gitlab.com/yorickpeterse/nvim-pqf.git"
.
Next is the cmd
key. This specifies what user commands should also load the
plugin, and can be a table of values.
dependencies
is just what it says on the tin. These are other plugins that
the plugin your trying to specify needs in order to run. These can just be
strings that describe the repo location, or they can be full plugin spec tables
with their own configuration and dependencies, etc. etc. recursion.
init
is the callback that runs when neovim starts up. This is where I’m
setting keymaps, so that they’re always available to me, and since the plugin
gets required
when I use it, it doesn’t get loaded until I need it.
config
is run once when the plugin is first required, and then not again.
Here, I’m just using the super common pattern of calling the plugins setup
function, which can also sometimes have options passed to it as a table by
convention.
Ok! That’s the basics of Lazy, but there’s definitely much fancier stuff to learn. From here on out, the direction you go is kind of yours to pick, but I’m going to try to give you a list of plugins that are ubiquitous/important, and then maybe a few that I think are just good or neat. Check out my dotfiles for how I configure them
Plugins I use
Oil
File explorer that treats files as lines in a normal nvim buffer, where you can delete, rename, and move the files by just editing the text. Also good at ssh.
lspconfig
This is how you setup your language servers. Configuring this could easily be it’s own post. Right now, I’ll leave you to the docs but I’ll start working on another write up shortly.
Lualine
A status line configuration plugin. Pretty straightforward, can make some pretty stuff. There’s about as many statusline plugins as there are people that use neovim, so if you don’t like this one, there’s another somewhere you might like more.
Cmp
This is the de-facto auto-complete plugin. Vim/Neovim has built in completion, with selection and snippets and everything, but this provides some async handling of special lsp stuff, as well as an easy way to load new sources for completion (like dictionaries, or file paths).
This is a bad plugin, and I could/might write a whole post on it’s quirks.
luasnip
Luasnip is a snippet creation/management plugin. You can create some wild snippets with this, or you can just use collections of snippets from VSCode or snipmate or whatever. Great stuff.
Checkout my snippets if you want, but also note that I use basic JSON specified snippets as well, so fear not.
Neogit
Neogit is a git management tool for staging, committing, handling diffs, and basically everything git related in neovim. It’s a spiritual successor to Magit from emacs-land and replaces fugitive from the old vim days.
GitSigns
Git staging status in your sign gutter. Very useful.
Nvim autopairs
If autopairing doesn’t drive you insane, you’ll like this plugin. If it does drive you insane… don’t use it?
CamelSnek
Easy converting between snake_case, camelCase, PascalCase etc. with operators! I use it all the time.
gruvbox
The colorscheme I use. Love it. Classic.
Comments
Fun-fact! Commenting and un-commenting are builtin to neovim now! Run :h commenting
to learn more!
Easy align
An Old vim plugin that I’ve used for years to align text based on separators. Very sophisticated plugin, but I might also look around to see if there’s a newer lua one if I’m bored some day
Notify
A nice looking notification pop-up that provides a commonly used way of async alerting for things. It’s also much nicer than the default neovim one that floods your command palette with text.
Surround
The modern neovim replacement for the old tpope plugin. Let’s you surround text
with more text, like "
or {
but also let’s you specify fairly advanced
surrounds, like wrapping a block in an arrow function, or elixir style maps with
%{
.
Telescope
One of the original neovim specific plugins. Telescope handles fuzzy finding/menu nav. You can use it for grepping, finding files, navigating diagnostics, or even create pretty powerful mini UIs with it. Probably my most used plugin.
Treesitter
Treesitter is a language for expressing language grammars, a parser for those grammars, and a way to interact with those grammars after the fact. Neovim integrates it first class to do things like highlighting, editing, commenting, etc. This plugin lets you easy install and configure those grammars for different languages.
which-key
This is a keymap menu that popus up as you use keymaps if you have a delay over
a certain time. For example: say you want to delete a contiguous block of text
and you can remember that the command for it probably starts with hitting d in
normal mode, but you can’t remember the rest of the text object. You can hit d
and then wait a bit, and then which key will show you a nice popup of all
available subsequent keymappings.
You can also use it to create menus for plugins you maybe use less frequently.
Zen mode
If you’re like me, you have 1 billion windows open at any given time while looking for where you want to make a change, but you like to edit with just one window open. Zen mode gives you a beautiful window that shows just the buffer you want to edit in an uncluttered way, and when you’re done editing, just leave that buffer and zen will put back all your original windows, just the way you had it. Pretty nice!
Conclusion
Ok This is a lot of stuff, and I’ve left a bunch up to the reader. This was intentional and inconsiderate. All of the plugins I’ve mentioned have good docs that are going to be totally sufficient for setting them up, but I can also write up more specific steps on any plugin that’s confusing if it would be helpful. My dots are also fairly stable and follow good patterns, so check in with those if something is going wrong.