Erik's Blog - A Case for Using Neovim Without Plugins - 2023-05-06
modified on 2024-09-23

But Why Though?

With the increasing popularity of Neovim there has been a rise of “quick-start” base configurations. This has gone so far that some people seem to think that you can’t or shouldn’t set up your Neovim without a quick-start configuration.

The currently most popular ones seem to be NvChad, LazyVim, and Kickstart.nvim, that come loaded up with a variety of plugins.

These are, of course, great projects; in my opinion Kickstart.nvim serves its purpose of being a base configuration to build on better, because it is comparatively minimal. Having something like this might make a switch from something like VSCode easier for new-comers, but I do worry that the plugins included by default may make it in fact more difficult for new users to know which of the large number of moving parts does what.

On that note: lightweight is a relative term; 1000 lines of code may be small for an entire text editor - shout-out to antirez’s kilo - but for a base configuration it doesn’t feel quite right (looking at you NvChad).

If these 1000 lines of configuration consist of setting up a variety of plugins, it becomes more difficult to understand what functionality the editor provides out of the box and what is actually an add-on.

It also makes setting up your own configuration seem much more difficult than it actually is.

But I Couldn’t Possibly…

Take a look at the talk “How to Do 90% of What Plugins Do (With Just Vim)” by Max Cantor. He goes over couple of built-ins that every Vim/Neovim user should know about like file navigation, go to definition (without need for an LSP), snippets, and code completion (also without LSP), as well as build integration. We will not cover these here specifically, because Max does an outstanding job in his talk.

But How Do I…?

What Neovim has over vim is scripting with a Lua API which provides an arguably better experience than classic Vimscript or Vim9Script; see Exhibit B below for an example. This is often cited as a big boon for plugin developers, but I would argue that it’s just as much of a boon for the user. If you don’t know Lua yet, give it a try, it’s a pretty small language; you can probably get the basics in a few hours. It also has a robust C-API, which is why it’s embedded in all sorts of places, so it may come in handy in other contexts, as well.

Instead of figuring out which plugin does exactly what you need at the moment and how to set it up, why not give it a shot yourself and see how far you get? Self-built tools for personal use have a big advantage over public plugins: specificity. Public plugins by definition have to be more general than something you hack together in an afternoon; they’re made for use by a lot of people. More often than not you may end up twisting the problem to fit the available solution, instead of finding a solution to your problem.

Does building your own tools sometimes go awry and you end up sinking an afternoon into figuring out why your tool isn’t working anymore? Of course. Do you come out the other side a better developer for it? Usually.

Let’s look at two common use cases for plugins.

Exhibit A: Statusline

The statusline is one of those things that has a number of quite popular plugins like airline or lualine. These look very nice and they have a lot of pre-built integration with other plugins that make them quite convenient.

That said, they provide a very specific abstraction over the built-in statusline which may make it harder to actually customize it to your liking. The more you want to deviate from the provided defaults the more likely you may be better off just building your own from scratch.

Here’s a simple example, that shows the current file name, and whether it’s been modified on the left-hand side of the statusline, and the filetype, hexadecimal value of the character currently under the cursor, current cursor position (line and column), and how far along we are in the file in percent on the right-hand side.

set statusline=%f%m%=%y\ 0x%B\ %l:%c\ %p%%

That’s it, that’s all we need. What each of those symbols means can be found in the documentation (:help statusline). There you will also find out how to determine the statusline content with a function call which opens up all manner of possibilities.

Exhibit B: Diagnostics

The diagnostics subsystem (:help diagnostic-api) of Neovim allows to parse the output of custom tools (such as external CLI’s) and display them in the same way a language server would. You might be using something like null-ls for this, but it may be interesting to see how much just a few lines of Lua get us.

Let’s take ESLint as an example. You could implement something like the following snippet in your file type plugin file (:help filetype) for JavaScript files.

-- Run ESlint on the current file
local function RunESLint()
    local filename = vim.fn.expand("%")
    -- show the diagnostics as virtual text
    vim.diagnostic.config({
        virtual_text = {
            source = true,
            prefix = "ESLint:"
        }
    })
    -- run ESlint asynchronously using the unix format because it's easy to
    -- parse
    vim.fn.jobstart({ "npx", "eslint", filename, "-f", "unix" }, {
        stdout_buffered = true,
        on_stdout = function(_, data)
            -- set up a namespace for our diagnostics
            local ns = vim.api.nvim_create_namespace("ESLint")
            vim.diagnostic.reset(ns)
            -- if there was any output, parse it and display the diagnostics
            if data then
                -- unix-style diagnostics are usually formatted as
                -- filename:line:column: message text
                local pattern = ":(%d+):(%d+): (.+)"
                local groups = { "lnum", "col", "message" }
                local ds = {}
                for _, line in ipairs(data) do
                    local d = vim.diagnostic.match(line, pattern, groups, {})
                    if d then
                        table.insert(ds, d)
                    end
                end
                vim.diagnostic.set(ns, 0, ds, {})
                vim.diagnostic.show()
            end
        end,
    })
end

-- set up the autocommand to run on opening a file and after saving
local group = vim.api.nvim_create_augroup("JavascriptLsp", { clear = true })
vim.api.nvim_create_autocmd({ "BufEnter", "BufWritePost" }, {
    pattern = { "*.js", "*.jsx", "*.ts" },
    callback = function()
        RunESLint()
    end,
    group = group,
})

Conclusion

Now, does that mean that all plugins are bad and you should never use them? Of course not. There is some amazing stuff out there. But make sure to also know what your editor provides out of the box. It will surely enhance your experience in the long run.

As some final food for thought: If an editor is unusable without plugins, it is probably not a very good editor. Luckily Neovim is an excellent editor.