I migrated my Neovim configuration from vimscript to lua and in the process I learned a few things about Neovim's lua api. One cool thing about it is we can use "vim functions" inside lua, and this integration is good enough to bring vim-plug (a popular plugin manager) into lua. Here I'm going to show you how to use vim-plug
inside your lua configuration.
How does it work?
There are two ways we can call vim functions in lua, that's using vim.fn
or vim.call
.
vim.fn
This is a meta-table, a special object that defines its own behavior for common operations. In this particular case it provides a convenient syntax to call functions.
vim.fn.has('nvim-0.5')
vim.call
Is a function we can use to call vim functions.
vim.call('has', 'nvim-0.5')
What's the difference?
Just details. Basically the example above have the same exact effect. The difference is that you can store vim.fn.has
in another variable, and vim.call('has')
will execute the function has
with no arguments.
Now that we know what's at the heart of this "trick" let's get down to business.
Look ma, no vimscript
In vim-plug
's documentation we can find something like this.
call plug#begin()
Plug 'tpope/vim-sensible'
call plug#end()
They are calling two vim functions and one command. We know how to call those functions in lua.
vim.call('plug#begin')
-- what about Plug?...
vim.call('plug#end')
What about Plug
? It turns out Plug
is a command that calls a function, check it out. So the thing that does the heavy lifting is plug#
. With this knowledge and some lua sourcery we can complete our example.
local Plug = vim.fn['plug#']
vim.call('plug#begin')
Plug 'tpope/vim-sensible'
vim.call('plug#end')
But the story doesn't end there.
That is one way to call Plug
and it works just fine, but sometimes we need to pass a second argument. In vimscript it looks like this.
Plug 'scrooloose/nerdtree', {'on': 'NERDTreeToggle'}
The lua equivalent is not that different but is enough to prevent a clean copy/paste. In lua we need to do this.
Plug('scrooloose/nerdtree', {on = 'NERDTreeToggle'})
Now the parenthesis are mandatory. The second argument is a lua table, notice instead of :
we use =
.
If you need to pass a list you need to use a table too.
Plug('scrooloose/nerdtree', {on = {'NERDTreeToggle', 'NERDTree'})
Here comes little bit of bad news. Plug
has a couple of options that can cause an error, for
and do
. Those are reserved keywords so we need to use a different syntax for them.
Plug('junegunn/goyo.vim', {['for'] = 'markdown'})
We have to wrap it in quotes and square brackets.
Now do
is an interesting one. It takes a string or a function, and the cool thing about is we can give it a vim function or a lua function.
Plug('junegunn/fzf', {['do'] = vim.fn['fzf#install']})
This also works.
Plug('junegunn/fzf', {
['do'] = function()
vim.call('fzf#install')
end
})
Lua interface
Just in case you're not a fan vim.fn
/vim.call
let me show you a little "lua interface" that I wrote.
local configs = {
lazy = {},
start = {}
}
local Plug = {
begin = vim.fn['plug#begin'],
-- "end" is a keyword, need something else
ends = function()
vim.fn['plug#end']()
for i, config in pairs(configs.start) do
config()
end
end
}
local apply_config = function(plugin_name)
local fn = configs.lazy[plugin_name]
if type(fn) == 'function' then fn() end
end
local plug_name = function(repo)
return repo:match("^[%w-]+/([%w-_.]+)$")
end
-- "Meta-functions"
local meta = {
-- Function call "operation"
__call = function(self, repo, opts)
opts = opts or vim.empty_dict()
-- we declare some aliases for `do` and `for`
opts['do'] = opts.run
opts.run = nil
opts['for'] = opts.ft
opts.ft = nil
vim.call('plug#', repo, opts)
-- Add basic support to colocate plugin config
if type(opts.config) == 'function' then
local plugin = opts.as or plug_name(repo)
if opts['for'] == nil and opts.on == nil then
configs.start[plugin] = opts.config
else
configs.lazy[plugin] = opts.config
vim.api.nvim_create_autocmd('User', {
pattern = plugin,
once = true,
callback = function()
apply_config(plugin)
end,
})
end
end
end
}
-- Meta-tables are awesome
return setmetatable(Plug, meta)
Let's pretend we have that code in ~/.config/nvim/lua/usermod/vimplug.lua
, this is how we use it.
local Plug = require('usermod.vimplug')
Plug.begin()
Plug('moll/vim-bbye')
Plug('junegunn/goyo.vim', {ft = 'markdown'})
Plug('echasnovski/mini.comment', {
config = function()
require('mini.comment').setup()
end
})
Plug.ends()
Isn't that just slightly better? I think so.
UPDATE 2021-10-02:
Notice how the last plugin (echasnovski/mini.comment
) has a config
option. I've added this feature so you can put the config for a plugin all in one place.
I've only made some trivial test with this, which seems to work. I don't use vim-plug
anymore so let me know in the comments if something doesn't work.
UPDATE 2024-01-01:
The "lua interface" now requires neovim v0.7 or greater.
Conclusion
We learned about vim.fn
and vim.call
, how we can use it to our advantage and bring vim-plug
into lua. And as a special bonus we figure out how to create a little wrapper that makes it look better.
Thank you for your time. If you find this article useful and want to support my efforts, consider leaving a tip in ko-fi.com/vonheikemen.
Top comments (4)
I can't see to make on work with config, maybe I'm doing it wrong or the problem is with autocmd?
loading this plug without on works just fine
Plug('kyazdani42/nvim-tree.lua', {on = 'NvimTreeToggle',
config = function()
local cfg = require('nvim-tree')
cfg.setup()
end
})
Problem here seems to be vim-plug. It doesn't create the command to load the plugin.
I tested this config.
vim-plug creates the command
Goyo
. I can inspect that using:command Goyo
, it shows me this.Here I can see vim-plug created the command to load the plugin.
But if I try
:command NvimTreeToggle
it gives me an error. The command NvimTreeToggle doesn't exists. This is a bug invim-plug
.nvim-tree nerds to be loaded with setup() for it to start working,
try this:
This is so succint!