DEV Community

Heiker
Heiker

Posted on • Updated on

Neovim: using vim-plug in lua

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')
Enter fullscreen mode Exit fullscreen mode
  • vim.call

Is a function we can use to call vim functions.

vim.call('has', 'nvim-0.5')
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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'}
Enter fullscreen mode Exit fullscreen mode

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'})
Enter fullscreen mode Exit fullscreen mode

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'})
Enter fullscreen mode Exit fullscreen mode

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'})
Enter fullscreen mode Exit fullscreen mode

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']})
Enter fullscreen mode Exit fullscreen mode

This also works.

Plug('junegunn/fzf', {
  ['do'] = function()
    vim.call('fzf#install')
  end
})
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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 buy me a coffee ☕.

buy me a coffee

Latest comments (4)

Collapse
 
diogohss profile image
Diogo Henriques Stauffer Sorio

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
})

Collapse
 
vonheikemen profile image
Heiker • Edited

Problem here seems to be vim-plug. It doesn't create the command to load the plugin.

I tested this config.

call plug#begin()
  Plug 'junegunn/goyo.vim', {'on': 'Goyo'}
  Plug 'kyazdani42/nvim-tree.lua', {'on': 'NvimTreeToggle'}
call plug#end()
Enter fullscreen mode Exit fullscreen mode

vim-plug creates the command Goyo. I can inspect that using :command Goyo, it shows me this.

    Name              Args Address Complete    Definition
!   Goyo              *    .       file        call s:lod_cmd('Goyo', "<bang>", <line1>, <line2>, <q-args>, ['goyo.vim'])
Enter fullscreen mode Exit fullscreen mode

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 in vim-plug.

Collapse
 
diogohss profile image
Diogo Henriques Stauffer Sorio • Edited

nvim-tree nerds to be loaded with setup() for it to start working,

try this:

Plug('kyazdani/nvim-tree.lua', {
    config = function()
        cfg = require('nvim-tree')
        cfg.setup()
    end
})
Enter fullscreen mode Exit fullscreen mode
Collapse
 
musale profile image
Musale Martin

This is so succint!