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:
- From the vim command line, you can run
:lua <yourCodeHere>
. This is useful for keybindings, commands, and other one-off execution cases. - Inside of a VimL file, you can demarcate lua code with the following code fencing:
lua << EOF
-- your lua code here
EOF
- Inside of a VimL file you can use the
lua
keyword to execute commands similar to the first example. (i.e.lua <yourCodeHere>
).
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
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)