From init.vim to init.lua

Why Lua?

Neovim has an embedded lua 5.1 runtime which is used to create faster and more powerful extensions of your favorite editor. In the Neovim charter, it lists one of its goals as developing a first-class lua scripting alternative to VimL. One of the reasons for doing this is that VimL is a slow interpreted language with almost no optimizations. Much of the time spent in vim startup and in actions from plugins that can block the main loop in the editor is in parsing and executing vimscript. A great explanation of this can be found in Neovim lead maintainer, Justin M. Keyes’ talk, We can have nice things.

With the recent introduction of the built-in LSP client in the master branch written in lua, I became more interested in the possibilities lua has to offer and began trying to use lua in Neovim. I have never written lua before and have not seen very many guides on how to utilize the lua runtime in Neovim, so I want to illustrate the process of learning how to take advantage of the powerful scripting capabilities that are available in the Neovim runtime. Given that my experience is still very basic, these examples will also be quite small, but I hope that it can be a good jumping off point for those interested in using lua more in extending Neovim.

Getting Started

One of the first things I was confused about was how to use lua code inside of vim and vimscript. Luckily, the documentation in :h lua gives a few examples of how lua can be used in the editor. I recommend reading it for an in-depth explanation of how Neovim treats lua and the sourcing of lua files. Here’s a high-level overview of different approaches to executing lua code in your editor:

lua << EOF
-- your lua code here
EOF

One important note here is that Neovim will look for lua code in the runtimepath you’ve set in your settings. Additionally, it will append your runtimepath with /lua/?.lua and /lua/?/init.lua so it is common practice to see a /lua sub-directory inside .nvim. For more detailed information about where Neovim looks for lua code, check out :h lua-require.

Your First Function

Disclaimer


Some of the following code uses the
vim.bo
API call that is currently only available in the master branch of neovim

Porting your init.vim to lua can be a big undertaking, so it’s best to start small. For the first example, we’ll create a function which creates a scratch buffer.

This function will live in a file we’ll call tools, so create it in the lua directory in your nvim config: ~/.config/nvim/lua/tools.lua. Once we’ve created the file, we’ll fill it out with some boilerplate:

-- in tools.lua

local M = {}

function M.makeScratch()
end

return M

Using the table M here allows us to keep things out of the global scope and to use only what we need when calling the function from nvim. We’ll be using the neovim API to make a scratch buffer, so let’s create a shorthand for it in our tools.lua file:

-- in tools.lua
local api = vim.api

local M = {}

function M.makeScratch()
end

return M

We can create a new buffer with the enew command, and the neovim API gives us a way to call nvim commands from lua:

-- in tools.lua
local api = vim.api

local M = {}

function M.makeScratch()
  api.nvim_command('enew') -- equivalent to :enew
end

return M

Next, we want to set some buffer options so that our scratch buffer isn’t listed in the buffer list and doesn’t have a swapfile created for it:

-- in tools.lua
local api = vim.api

local M = {}

function M.makeScratch()
  api.nvim_command('enew') -- equivalent to :enew
  vim.bo[0].buftype=nofile -- set the current buffer's (buffer 0) buftype to nofile
  vim.bo[0].bufhidden=hide
  vim.bo[0].swapfile=false
end

return M

That is all we need to create the scratch buffer! Now let’s use it in our init.vim:

" in init.vim

command! Scratch lua require'tools'.makeScratch()

Now a scratch buffer is created by running the command :Scratch.

You can port your init.vim to lua one function at a time, and if you get stuck, you can always use vim.api.nvim_command! When looking for help, make sure to check out :h api, and :h lua.

Using v:lua

The variable v:lua can be used to call lua functions from within vimscript. A great use case for this is accessing the LSP client’s omnifunc. If you wanted to use the LSP completion for Rust, you may have something like this in your configuration:

" in init.vim
lua << EOF
  local nvim_lsp = require 'nvim_lsp'
  nvim_lsp.rust_analyzer.setup({})
EOF
" in ftplugin/rust.vim

set omnifunc=v:lua.vim.lsp.omnifunc

Interop With vim.fn

It is useful to have access to vimscript functions from inside of lua, especially when interacting with autoloaded functions or functions provided by plugins. In this example, we will have an autocmd that will execute the vimscript function, tools#loadCscope when the VimEnter event happens.

" in autoload/tools.vim

function! tools#loadCscope() abort
  try
    silent cscope add cscope.out
  catch /^Vim\%((\a\+)\)\=:E/
  endtry
endfunction
-- in file that you source, such as init.lua

function sourceCScope()
  vim.fn['tools#loadCscope']() -- no arguments needed
end

function nvim_create_augroups(definitions)
  for group_name, definition in pairs(definitions) do
    vim.api.nvim_command('augroup '..group_name)
    vim.api.nvim_command('autocmd!')
    for _, def in ipairs(definition) do
      local command = table.concat(vim.tbl_flatten{'autocmd', def}, ' ')
      vim.api.nvim_command(command)
    end
    vim.api.nvim_command('augroup END')
  end
end

local autocmds = {
  startup = {
    {"VimEnter",        "*",      [[lua sourceCScope()]]};
  }
}

nvim_create_augroups(autocmds)