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 required 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 that require the plugins modules in their function body. This means they get loaded when used, but not before
  • config is run at plugin loading time, and is where you most often call require('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:

-- Quick linguistic side node, in lua, tables can mix key-value and just value
-- entries. The value entries become indexed key-value entries.
{
    "windwp/nvim-spectre",
    cmd = "Spectre",
    dependencies = {
        "nvim-lua/plenary.nvim",
    },
    init = function()
        vim.keymap.set("n", "<leader>sp", function()
            require("spectre").open()
        end, {})
        vim.keymap.set("x", "<leader>sp", function()
            require("spectre").open_visual({ select_word = true })
        end, {})
        vim.keymap.set("n", "<leader>sfp", function()
            require("spectre").open_file_search()
        end, {})
    end,
    config = function()
        require("spectre").setup()
    end,
}

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.