Neovim: using vim-plug in lua
Tags: neovim, shell
Published: 2021-08-04 15:43:58

---
title: Neovim: using vim-plug in lua
published: true
description: making vim-plug play nice with lua
tags: #neovim #shell
//cover_image: https://direct_url_to_image.jpg
---

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](https://github.com/junegunn/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.

```lua
vim.fn.has('nvim-0.5')
```

* `vim.call`

Is a function we can use to call vim functions.

```lua
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.

```vim
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.

```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](https://github.com/junegunn/vim-plug/blob/ca0ae0a8b1bd6380caba2d8be43a2a19baf7dbe2/plug.vim#L266). So the thing that does the heavy lifting is `plug#`. With this knowledge and some lua sourcery we can complete our example.

```lua
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.

```vim
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.

```lua
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.

```lua
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.

```lua
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.

```lua
Plug('junegunn/fzf', {['do'] = vim.fn['fzf#install']})
```

This also works.

```lua
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.

```lua
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.

```lua
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 [buy me a coffee ☕](https://www.buymeacoffee.com/vonheikemen).

[![buy me a coffee](https://res.cloudinary.com/vonheikemen/image/upload/v1618466522/buy-me-coffee_ah0uzh.png)](https://www.buymeacoffee.com/vonheikemen)