Neovim Local Plugin Development
Local Plugins
I am fairly new to the whole NeoVim ecosystem, and so far its pretty amazing. I have been ricing my setup almost daily for about a year just to experiment with everything. One can really sink a ton of time into this and still not have what they want, but its so cool.
I recently got to a point where loading plugins wasn’t enough as I couldn’t find certain plugins I wanted for every little thing. I probably should be using the plugins I have first, at the cost of me sounding like what I say to my kids. In reality, there is a couple little things I wanted, that I couldn’t find out there. A couple things I did find, but they were also about 10 years old and untouched, or they were for Vim also 10 years old untouched. I know that Vim plugins DO work with NeoVim, however when they are 10 years old (not a complete exageration for all of them), they do end up not working correctly or as I want them to. It still does not solve that some I was just not able to find out there.
I could be contributing to some of these plugins and updating them to my image, which is fine, and I just might do that. At the same time, I wanted to try some “vibe coding”. I plugged-in what I wanted to an AI assistant, and then got the code. I recognize, that this probably would not work, and that is exactly what I got, soemthing close that did not work. However, I was able to learn from it and correct the issues myself.
Lazy NVim Setup
One of the issues I ran into was “how do I load this plugin to my setup”? Yes, I know I could have just put things in github and then pulled it down and made updates from some other directory locally only to then push it back up to github and pull the changes in my setup on the next load. I did that for a while, and that was tedious. I did that initially because I didn’t think I would be making that many changes, but I did need to make those changes. I still don’t recommend that way at all.
This will assume a lazy.nvim setup. The first thing to do is to add the dev table to your setup.
Going to the lazy.nvim website, lazy.folke.io, and goign to the installation section, you will see the following code.
-- Bootstrap lazy.nvim
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not (vim.uv or vim.loop).fs_stat(lazypath) then
local lazyrepo = "https://github.com/folke/lazy.nvim.git"
local out = vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable", lazyrepo, lazypath })
if vim.v.shell_error ~= 0 then
vim.api.nvim_echo({
{ "Failed to clone lazy.nvim:\n", "ErrorMsg" },
{ out, "WarningMsg" },
{ "\nPress any key to exit..." },
}, true, {})
vim.fn.getchar()
os.exit(1)
end
end
vim.opt.rtp:prepend(lazypath)
-- Make sure to setup `mapleader` and `maplocalleader` before
-- loading lazy.nvim so that mappings are correct.
-- This is also a good place to setup other settings (vim.opt)
vim.g.mapleader = " "
vim.g.maplocalleader = "\\"
-- Setup lazy.nvim
require("lazy").setup({
spec = {
-- import your plugins
{ import = "plugins" },
},
-- Configure any other settings here. See the documentation for more details.
-- colorscheme that will be used when installing plugins.
install = { colorscheme = { "habamax" } },
-- automatically check for plugin updates
checker = { enabled = true },
})
This is the initial bootstrap for lazy.nvim.
Typically, inside your .config/nvim/init.lua file you will have a line that references this file or you may have it directly inside that file.
This is great to start. You can see how this would start to connect to the LazyVim config plus loading your own plugins and folders and such. We won’t go over that in this post.
What is interesting to note is the full “Configuration” page at https://lazy.folke.io/configuration/
Here you will see so many options available to the boilerplate above, along with the defaults.
Going through this, there is a section table called dev. This is what we want.
require("lazy").setup({
spec = {...},
dev = {
path = "~/dev/nvim-plugins",
pattersn = {},
fallback = false,
},
ui = {...},
})
We see that require("lazy").setup({...}) section, and within it there are different things we can add to make customize this package manager. We have to remember, this is for the Lazy.Nvim package manager, not the plugins or the config nor the editor itself. Just the package manager. It would make sense that to adjust the package manager, we would need to add a dev section to this table. That table section consists of 3 things.
- path
- patterns
- fallback
By default, this is not set.
Dev Path
The bare minimum we can do is just reference the path = "..." to a specific local plugin we want. It is probably better to have this set to the parent directory that we will develop all of our plugins in. It may not be many plugins, but it will probably be best. It should look similar to below…
require("lazy").setup({
spec = {...},
dev = {
path = "~/dev/nvim-plugins",
},
ui = {...},
})
Now, all the projects and plugins we have in ~/dev/nvim-plugins will be loaded.
We can change this directory to wherever we want, or we can even have a function. Why a function?The function could possibly be where when we start nvim with certain parameters, it will load a specific plugin and only that. Maybe, we don’t want to load our local development environment plugins and we want to prefer to use the published git repos instead. Maybe we are helping a specific repo out and we want to load that instead or reference the crazy place we cloned it. All good reasons for a function here, but we have to remember that the function MUST return a directory string.
This gives me some good ideas and maybe in a future post I will go over that, if I make one myself.
Dev Patterns
The patters I find interesting. This needs to be a table of strings specifically. I am curious if we could use this in conjunction with a function, such as to pass a variable of ‘mini’ and it would load the local ‘mini’ ecosystem instead. That, is a good thought experiment.
However, this we could have local versions and use that instead of what is in the github repo. If we are making our own ecosystem of plugins we could have a folder named that and put that name here and prefer that during development. Maybe we want to leverage others plugins that we cloned, and we could put it here.
All good use cases, but not something we need, unless we want only specific dev plugins loaded.
Dev Fallback
This is set to false, and will fall back to the git repo when there is no local plugin.
I believe that some of this concept is a great usecase that while you are developing it, it will live in that folder and then you publish it and delete it after that. When this is set to true, it will load the local version first, and then the git repo. This is good if you want to test out your changes before publishing them. However, if you are just developing and not publishing, then this is not needed.
Dev Config
All you need is that basic config addition mentioned.
require("lazy").setup({
spec = {...},
dev = {
path = "~/dev/nvim-plugins",
},
...,
})
And we are all set to start putting things in that directory.
One thing I did notice, is that sometimes, and it is best practice, to wrap the path in the vim expand function…just like this below.
require("lazy").setup({`
spec = {...},
dev = {
path = vim.fn.expand("~/dev/nvim-plugins"),
},
...,
})
Plugins loading
Now, assuming you have a structure like this below…
~/.config
└── nvim
│ └── lua
│ └── plugins
│ └── plugin1.lua
│ └── plugin2.lua
│ └── ...
└── init.lua
…where the init.lua has the bootstrap mentioned above, and the plugins directory has all the configs for your plugins, we can add another lua file for the local plugins.
~/.config
└── nvim
│ └── lua
│ └── plugins
│ └── plugin1.lua
│ └── plugin2.lua
│ └── local.lua
│ └── ...
└── init.lua
The local.lua file would look something like this below…
return {
"local-test",
dev = true,
config = function()
require("local-test").setup()
end,
}
…or depending on how your plugin is setup…
return {
"local-test",
dev = true,
opts = {},
}
With this, change the name of “local-test” to whatever the name of the folder is in the plugin you created.
Plugin Creation
Inside that ~/dev/nvim-plugins directory, you can create a new directory for your plugin. I typically name it the same as the name I put in the local.lua file. Inside that directory, you can create your plugin however you want. I typically use the init.lua file for the main entry point, and then any other files I need.
The directory structure should look like the following in this example.
~/dev/nvim-plugins
└── local-test
│ └── init.lua
│ └── ...
This will be covered more in the next section.
Easier Way???
I just outlined what some people might think is a complex way of doing things. I just went through the details of what I do, which is not the only way or the best way, but just what works for me.
Instead of changing the lazy.nvim config, which is normal and good, there is an alternative.
Since we have to add a lua config file or section to load the plugin, we can just do something like this below.
-- try this but in a different directory
return {
"toobar",
dev = true,
dir = vim.fn.expand("~/dev/lua/test-plugins/toobar"),
}
Here, instead of having the dev parameter set to true, it is omitted, and we are pointing this to a directory with the dir parameter and where it should be going. The dev parameter is a flag that should be used to tell lazy.nvim to look in that ‘dev directory’ that we setup in the config. In this case, we don’t need it, as that directory should be different than this one we setup. All we need is in that table the dir to look for the plugin.
This is great if you clone some repos and are trying to manage them as you are contributing to them. Maybe you want to override the github one that is normally pulled from. This will do that. Maybe your lua and plugin development is short or not that much, this is a great way for that. I don’t have to go through the list of reasons.