Neovim the Lazy Way

Laziness isn't typically the first work people think of when building a custom Neovim configuration, but now, building a Lazy config is easier than ever!

A picture of the Neovim logo being lazy.
Neovim
Apr 24, 2023

There's a fantastic YouTube video called "Build Your Rust Lightsaber" by No Boilerplate that changed the way I think about tooling as a professional software developer. I highly recommend watching it, even if you have no interest in the Rust programming language. There's a lot to be learned by watching someone work at their best.

I discovered this video at a time when I was considering moving away from a pre-packaged Neovim configuration (AstroNvim) to a completely custom configuration. I had attempted to configure my own Neovim editor before, but things quickly became complex, and I abandoned the idea in favor of writing software instead of configuring it.

After watching a video on the topic, I felt inspired to create a Neovim configuration that was tailored to my preferences as a developer. The idea of having a setup that was uniquely mine was empowering. However, I was concerned about the challenges I had faced in my previous attempts to create a workable configuration. Despite using software like Vim-Plug and Packer, which are both powerful tools in their own right, I found it difficult to create a configuration that was both scalable and easy to use, that didn't also result in bugs.

Luckily for me, I found Lazy.

The Lazy Way

While browsing Github for Neovim plugins, I discovered a new repository I hadn't seen before: Lazy.nvim by folke. Folke, the developer of multiple other Neovim plugins that I use daily, had just released version 1.0 days before. After reading the README, I was impressed by the simplicity and power of this new Neovim plugin manager.

Lazy has several core tenets, but a few features stood out to me. First and foremost, Lazy makes it easy to modularize plugin configurations. Previously, plugin declarations, configurations, and keybindings were spread throughout my configuration, making it tedious to add, configure, and remove plugins. With Lazy's modularity, I have built a Neovim configuration where there is one place for each plugin's declarations, configurations, and keybindings.

Secondly, Lazy encourages fast startups. It automatically caches Lua modules for quicker startups and implements lazy-loading by default when declaring plugins with keybindings. When adding the keys key to the plugin table and putting the relevant keybinds within it, Lazy defaults to lazy-loading the plugin only when one of the declared keybinds has been registered. Even though the plugin loads entirely when the keybind is first registered, Lazy's automatic caching and bytecode compilation of Lua modules ensures there is no lag in starting and running the plugin.

The Setup

Now that I am fully committed to using Lazy to manage my Neovim configuration, it only took a few lines of code to create a simple, modular system that I have complete confidence in. Previously, every time I had to update Neovim or any of my plugins, I held my breath, hoping everything would work after the update. Now, however, I can confidently upgrade, knowing that even if something goes wrong, I can quickly and easily adjust my plugin configurations to solve the issue or disable the plugin altogether while I search for a fix.

Here’s what my Neovim configuration filesystem looks like as of the writing of this post:

.
├── LICENSE.md
├── README.md
├── init.lua
├── lazy-lock.json
└── lua
    ├── keybinds.lua
    ├── options.lua
    └── plugins
        ├── alpha.lua
        ├── catppuccin.lua
        ├── centerpad.lua
        ├── comment.lua
        ├── gitsigns.lua
        ├── harpoon.lua
        ├── indent-blankline.lua
        ├── init.lua
        ├── lsp.lua
        ├── lualine.lua
        ├── null-ls.lua
        ├── nvim-treesitter-context.lua
        ├── sayonara.lua
        ├── smart-splits.lua
        ├── telescope-tmux.lua
        ├── telescope.lua
        ├── treesitter.lua
        ├── undotree.lua
        ├── vim-blade.lua
        ├── vim-fugitive.lua
        ├── vim-maximizer.lua
        ├── vim-sleuth.lua
        ├── vim-surround.lua
        ├── vim-test.lua
        └── vimux.lua

3 directories, 31 files

For those of you who aren’t familiar with Neovim configurations, here are a few notable files and directories:

  1. init.lua - This is the entry point for Neovim. The entire modular system is orchestrated from this file, but as you'll see later, this file is remarkably simple and easy to understand.
  2. lua/ - The lua/ directory is a special directory where all of my Lua files exist. When I tell Lua to import a file from my project, it looks in the lua/ directory by default.
  3. plugins/ - This directory is where each plugin (or group of related plugins, like in the case of lsp.lua), gets its own configuration file. Adding a file to this directory will automatically prompt Lazy to install the plugin on the next boot. Editing any of these files will cause Lazy to reload the configuration.

init.lua

Let’s jump right in to where the magic happens. The init.lua controls the entire system, and before I started using Lazy, it would quickly become a huge mess no matter what I did to attempt to keep things lean and clean. Now, though, it’s simple and dare I say elegant! Take a look:

local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
    vim.fn.system({
        "git",
        "clone",
        "--filter=blob:none",
        "https://github.com/folke/lazy.nvim.git",
        "--branch=stable", -- latest stable release
        lazypath,
    })
end
vim.opt.rtp:prepend(lazypath)

vim.g.mapleader = " "

require("options")
require("keybinds")

require("lazy").setup("plugins")

The first 12 lines of code are copied and pasted from the Lazy installation documentation, so my init.lua file is only four lines long. Let's go through them here:

  1. vim.g.mapleader = " " - This sets my vim leader key to <SPACE>
  2. require("options") and require("keybinds") - This imports two files: one that contains all of my Vim options, such as line wrapping, number of spaces in a tab, and smart indenting, and another that contains all of my generic Vim keybindings that don’t interact with any of my installed plugins.
  3. require("lazy").setup("plugins") - This line is the secret sauce behind the entire configuration. Here, we bring Lazy into the picture and instruct it to set itself up to consider each file in the plugins/ directory as a configuration for a plugin.

The Plugins Directory

Before we dive into the installation and configuration of plugins, there is one final step required for Lazy to watch each file in the plugins/ directory. Create an init.lua file in the plugins/ directory with the following contents:

return {}

That’s it! That’s the rest of the setup. Now that we’ve added this plugins/init.lua file, we can start adding our plugins! Let’s take the smart-splits.nvim plugin as our example.

Installing & Configuring Plugins

As of the writing of this post, this is the contents of my plugins/smart-splits.lua configuration file:

return {
    {
        "mrjones2014/smart-splits.nvim",
        keys = {
            -- window movement
            { "<C-h>",      function() require("smart-splits").move_cursor_left() end },
            { "<C-j>",      function() require("smart-splits").move_cursor_down() end },
            { "<C-k>",      function() require("smart-splits").move_cursor_up() end },
            { "<C-l>",      function() require("smart-splits").move_cursor_right() end },

            -- resizing
            { "<leader>wr", function() require("smart-splits").start_resize_mode() end },

        },
        config = function()
            require('smart-splits').setup({
                -- Configuration removed for brevity. See
								-- my nvim config here for the full file:
								-- https://github.com/alexandersix/nvim-config
            })
        end
    }
}

Some content has been removed for brevity. However, the configuration file can be broken down into three main parts: the plugin name, the keys section, and the config section. Let's go through each of these sections.

Plugin Name

The plugin name section is simple: it tells Lazy which plugin to install. Since the smart-splits.nvim plugin is hosted on Github, all you have to do is wrap the repository name (including the creator's username) in a string, and Lazy will do the rest.

This section is technically the only required section for a plugin file. Some plugins don't require any keybindings or configuration, so this line may be the only one in the entire configuration file. You can refer to vimux.lua in my alexandersix/nvim-config repository as an example.

Keys

The keys section is where we can define any keybindings that we want to use that involve this plugin. This section causes a change in how Lazy handles this particular plugin, though. By default, when Lazy reads configuration files and encounters a keys section, it will not load the associated plugin when launching Neovim. Instead, the plugin will only be loaded once one of the keybinds is registered by Neovim.

For example, the smart-splits plugin will only be loaded when attempting to move across or resize splits. Thanks to Lazy's high performance, I have never noticed any delay when using this configuration, even when moving splits for the first time. I've been using this setup daily for months, and it's been impressively seamless.

If a plugin requires keybindings and you do not want to lazy-load it, you can add a lazy = false section to the configuration file. This will override Lazy's default behavior and ensure that the plugin is loaded immediately upon launching Neovim.

Config

The config section is where we can add any necessary configuration for when the plugin is loaded for the first time. This configuration typically takes the form of a large Lua table (somewhat similar to a JavaScript object), but can be incredibly complex and detailed if desired, since the config section itself is just a Lua function.

For the sake of keeping code snippets short, I have omitted the configuration options in this document. However, feel free to check out my alexandersix/nvim-config repository if you are interested in how I configure this particular plugin.

Wrapping it Up

And that's it! Before using Lazy, I relied heavily on pre-built Neovim configurations to use my favorite editor with some level of ease. However, those pre-built configurations tended to break frequently if I tried to customize them too much, or just not allow me to customize as much as I needed. Now that I've gone all-in on Lazy, configuring my Neovim setup has never been easier. With my setup configured exactly the way I like it, I'm much more productive and happier when writing code.

If you're a Neovim user who currently relies on a pre-built configuration or someone who wants to try Neovim but doesn't want to spend time learning how to configure it, give Lazy a try. If you want a starting point, feel free to fork and clone my Neovim configuration! Just make sure to remove any configurations you don't need. The whole point of this is to quickly and easily craft the editor that best fits the way you like to develop software.

Let me know if you're using Lazy and what you think of it! You can reach out to me on Twitter to start a conversation or drop into my Twitch stream to chat about it live!