Configuring Neovim using Lua
And switching from init.vim to init.lua
If you, like me, never really understood Vimscript and hate the language with a passion, you’re in the right place! You can now get rid of Vimscript wholesale and replace it with a simpler, faster and elegant-er language—Lua! However, this is only possible from Neovim 0.5 onwards1 and as of now, requires you to install Neovim from HEAD. How to do that is left as an exercise to the reader. Also bear in mind that the Lua API is fairly beta right now, and many Vim things don’t have direct interfaces.
So assuming you’re now running Neovim master
, head over to
~/.config/nvim
and create your init.lua
. Why, yes, we’re porting
over your init.vim
to init.lua
right now! Clear your calendar for
the next few hours—bikeshedding your text editor is top priority!
I also recommend going through nanotee/nvim-lua-guide and Learn Lua in Y minutes before starting off.
the directory structure
Lua files are typically under ~/.config/nvim/lua
, and can be loaded as
Lua modules. This is incredibly powerful—you can structure your
configs however you like.
$ tree .config/nvim
.
|-- ftplugin
| `-- ...
|-- init.lua
|-- lua
| |-- maps.lua
| |-- settings.lua
| |-- statusline.lua
| `-- utils.lua
`-- plugin
`-- ...
The common approach is to have different
bits of your config in Lua files under lua/
and require
’d in your
init.lua
, so something like:
-- init.lua
require('settings') -- lua/settings.lua
require('maps') -- lua/maps.lua
require('statusline') -- lua/statusline.lua
the basics: setting options
Vim has 3 kinds of options—global, buffer-local and window-local. In
Vimscript, you’d just set
these. In Lua, however, you will have to
use one of
vim.api.nvim_set_option()
—global optionsvim.api.nvim_buf_set_option()
—buffer-local optionsvim.api.nvim_win_set_option()
—window-local options
These are fairly verbose and very clunky, but fortunately for us, we
have “meta-accesors” for these: vim.{o,wo,bo}
. Here’s an excerpt from
my settings.lua
as an example:
local o = vim.o
local wo = vim.wo
local bo = vim.bo
-- global options
o.swapfile = true
o.dir = '/tmp'
o.smartcase = true
o.laststatus = 2
o.hlsearch = true
o.incsearch = true
o.ignorecase = true
o.scrolloff = 12
-- ... snip ...
-- window-local options
wo.number = false
wo.wrap = false
-- buffer-local options
bo.expandtab = true
If you’re not sure if an option is global, buffer or window-local,
consult the Vim help! For example, :h 'number'
:
'number' 'nu' boolean (default off)
local to window
Also note that you don’t set the negation of an option to true, like
wo.nonumber = true
, you instead set wo.number = false
.
defining autocommands
Unfortunately, autocommands in Vim don’t have a Lua interface—it is
being worked on.2 Until then, you will have to use
vim.api.nvim_command()
, or the shorter vim.cmd()
. I’ve defined a
simple function that takes a Lua table of autocmd
s as an argument, and
creates an augroup
for you.
-- utils.lua
local M = {}
local cmd = vim.cmd
function M.create_augroup(autocmds, name)
cmd('augroup ' .. name)
cmd('autocmd!')
for _, autocmd in ipairs(autocmds) do
cmd('autocmd ' .. table.concat(autocmd, ' '))
end
cmd('augroup END')
end
return M
-- settings.lua
local cmd = vim.cmd
local u = require('utils')
u.create_augroup({
{ 'BufRead,BufNewFile', '/tmp/nail-*', 'setlocal', 'ft=mail' },
{ 'BufRead,BufNewFile', '*s-nail-*', 'setlocal', 'ft=mail' },
}, 'ftmail')
cmd('au BufNewFile,BufRead * if &ft == "" | set ft=text | endif')
defining keymaps
Keymaps can be set via vim.api.nvim_set_keymap()
. It takes 4
arguments: the mode for which the mapping will take effect, the key
sequence, the command to execute and a table of options (:h
:map-arguments
).
-- maps.lua
local map = vim.api.nvim_set_keymap
-- map the leader key
map('n', '<Space>', '', {})
vim.g.mapleader = ' ' -- 'vim.g' sets global variables
options = { noremap = true }
map('n', '<leader><esc>', ':nohlsearch<cr>', options)
map('n', '<leader>n', ':bnext<cr>', options)
map('n', '<leader>p', ':bprev<cr>', options)
For user defined commands, you’re going to have to go the vim.cmd
route:
local cmd = vim.cmd
cmd(':command! WQ wq')
cmd(':command! WQ wq')
cmd(':command! Wq wq')
cmd(':command! Wqa wqa')
cmd(':command! W w')
cmd(':command! Q q')
managing packages
Naturally, you can’t use your favourite Vimscript package manager
anymore, or at least, not without vim.api.nvim_exec
ing a bunch of
Vimscript (ew!). Thankfully, there are a few pure-Lua plugin managers
available to use3—I personally use, and recommend
paq. It’s light and makes use of
the vim.loop
API for async I/O.
paq’s docs are plentiful, so I’ll skip talking about how to set it up.
bonus: writing your own statusline
Imagine using a bloated, third-party statusline, when you can just write your own.4 It’s actually quite simple! Start by defining a table for every mode:
-- statusline.lua
local mode_map = {
['n'] = 'normal ',
['no'] = 'n·operator pending ',
['v'] = 'visual ',
['V'] = 'v·line ',
[''] = 'v·block ',
['s'] = 'select ',
['S'] = 's·line ',
[''] = 's·block ',
['i'] = 'insert ',
['R'] = 'replace ',
['Rv'] = 'v·replace ',
['c'] = 'command ',
['cv'] = 'vim ex ',
['ce'] = 'ex ',
['r'] = 'prompt ',
['rm'] = 'more ',
['r?'] = 'confirm ',
['!'] = 'shell ',
['t'] = 'terminal '
}
The idea is to get the current mode from vim.api.nvim_get_mode()
and
map it to our desired text. Let’s wrap that around in a small mode()
function:
-- statusline.lua
local function mode()
local m = vim.api.nvim_get_mode().mode
if mode_map[m] == nil then return m end
return mode_map[m]
end
Now, set up your highlights. Again, there isn’t any interface for
highlights yet, so whip out that vim.api.nvim_exec()
.
-- statusline.lua
vim.api.nvim_exec(
[[
hi PrimaryBlock ctermfg=06 ctermbg=00
hi SecondaryBlock ctermfg=08 ctermbg=00
hi Blanks ctermfg=07 ctermbg=00
]], false)
Create a new table to represent the entire statusline itself. You can
add any other functions you want (like one that returns the current git
branch, for instance). Read :h 'statusline'
if you don’t understand
what’s going on here.
-- statusline.lua
local stl = {
'%#PrimaryBlock#',
mode(),
'%#SecondaryBlock#',
'%#Blanks#',
'%f',
'%m',
'%=',
'%#SecondaryBlock#',
'%l,%c ',
'%#PrimaryBlock#',
'%{&filetype}',
}
Finally, with the power of table.concat()
, set your statusline. This
is akin to doing a series of string concatenations, but way faster.
-- statusline.lua
vim.o.statusline = table.concat(stl)
this is what being tpope feels like
You can now write that plugin you always wished for! I sat down to write
a plugin for fzy5, which you can
find here
along with my entire Neovim config6. I plan to port a the last of my
plugin/
directory over to Lua, soon™.
And it’s only going to get better when the Lua API is completed. We can all be Vim plugin artists now.
- https://github.com/neovim/neovim/pull/12235 ↩︎
- https://github.com/neovim/neovim/pull/12378 ↩︎
- Also see: packer.nvim ↩︎
- This meme was made by NIH gang. ↩︎
- A less bloated alternative to fzf, written in C. ↩︎
GitHub link— if you’re into that sort of thing.
↩︎
Questions or comments? Send an email.