pages/blog/nvim-lua.md (view raw)
1---
2template:
3slug: nvim-lua
4title: Configuring Neovim using Lua
5subtitle: And switching from init.vim to init.lua
6date: 2021-02-07
7---
8
9If you, like me, never really understood Vimscript and hate the language
10with a passion, you're in the right place! You can now get rid of
11Vimscript wholesale and replace it with a simpler, faster and elegant-er
12language -- Lua! _However_, this is only possible from Neovim 0.5
13onwards[^1] and as of now, requires you to install Neovim from HEAD. How
14to do that is left as an exercise to the reader. Also bear in mind that
15the Lua API is fairly beta right now, and many Vim things don't have
16direct interfaces.
17
18[^1]: https://github.com/neovim/neovim/pull/12235
19
20So assuming you're now running Neovim `master`, head over to
21`~/.config/nvim` and create your `init.lua`. Why, yes, we're porting
22over your `init.vim` to `init.lua` right now! Clear your calendar for
23the next few hours -- bikeshedding your text editor is top priority!
24
25I also recommend going through
26[nanotee/nvim-lua-guide](https://github.com/nanotee/nvim-lua-guide)
27and [Learn Lua in Y minutes](https://learnxinyminutes.com/docs/lua/)
28before starting off.
29
30## the directory structure
31
32Lua files are typically under `~/.config/nvim/lua`, and can be loaded as
33Lua modules. This is incredibly powerful -- you can structure your
34configs however you like.
35
36```console
37$ tree .config/nvim
38.
39|-- ftplugin
40| `-- ...
41|-- init.lua
42|-- lua
43| |-- maps.lua
44| |-- settings.lua
45| |-- statusline.lua
46| `-- utils.lua
47`-- plugin
48 `-- ...
49```
50
51The common approach is to have different
52bits of your config in Lua files under `lua/` and `require`'d in your
53`init.lua`, so something like:
54
55```lua
56-- init.lua
57
58require('settings') -- lua/settings.lua
59require('maps') -- lua/maps.lua
60require('statusline') -- lua/statusline.lua
61```
62
63## the basics: setting options
64
65Vim has 3 kinds of options -- global, buffer-local and window-local. In
66Vimscript, you'd just `set` these. In Lua, however, you will have to
67use one of
68
69- `vim.api.nvim_set_option()` -- global options
70- `vim.api.nvim_buf_set_option()` -- buffer-local options
71- `vim.api.nvim_win_set_option()` -- window-local options
72
73These are fairly verbose and very clunky, but fortunately for us, we
74have "meta-accesors" for these: `vim.{o,wo,bo}`. Here's an excerpt from
75my `settings.lua` as an example:
76
77```lua
78local o = vim.o
79local wo = vim.wo
80local bo = vim.bo
81
82-- global options
83o.swapfile = true
84o.dir = '/tmp'
85o.smartcase = true
86o.laststatus = 2
87o.hlsearch = true
88o.incsearch = true
89o.ignorecase = true
90o.scrolloff = 12
91-- ... snip ...
92
93-- window-local options
94wo.number = false
95wo.wrap = false
96
97-- buffer-local options
98bo.expandtab = true
99```
100
101If you're not sure if an option is global, buffer or window-local,
102consult the Vim help! For example, `:h 'number'`:
103
104```
105'number' 'nu' boolean (default off)
106 local to window
107```
108
109Also note that you don't set the negation of an option to true, like
110`wo.nonumber = true`, you instead set `wo.number = false`.
111
112## defining autocommands
113
114Unfortunately, autocommands in Vim don't have a Lua interface -- it is
115being worked on.[^2] Until then, you will have to use
116`vim.api.nvim_command()`, or the shorter `vim.cmd()`. I've defined a
117simple function that takes a Lua table of `autocmd`s as an argument, and
118creates an `augroup` for you.
119
120```lua
121-- utils.lua
122
123local M = {}
124local cmd = vim.cmd
125
126function M.create_augroup(autocmds, name)
127 cmd('augroup ' .. name)
128 cmd('autocmd!')
129 for _, autocmd in ipairs(autocmds) do
130 cmd('autocmd ' .. table.concat(autocmd, ' '))
131 end
132 cmd('augroup END')
133end
134
135return M
136
137-- settings.lua
138local cmd = vim.cmd
139local u = require('utils')
140
141u.create_augroup({
142 { 'BufRead,BufNewFile', '/tmp/nail-*', 'setlocal', 'ft=mail' },
143 { 'BufRead,BufNewFile', '*s-nail-*', 'setlocal', 'ft=mail' },
144}, 'ftmail')
145
146cmd('au BufNewFile,BufRead * if &ft == "" | set ft=text | endif')
147```
148
149[^2]: https://github.com/neovim/neovim/pull/12378
150
151## defining keymaps
152
153Keymaps can be set via `vim.api.nvim_set_keymap()`. It takes 4
154arguments: the mode for which the mapping will take effect, the key
155sequence, the command to execute and a table of options (`:h
156:map-arguments`).
157
158```lua
159-- maps.lua
160
161local map = vim.api.nvim_set_keymap
162
163-- map the leader key
164map('n', '<Space>', '', {})
165vim.g.mapleader = ' ' -- 'vim.g' sets global variables
166
167
168options = { noremap = true }
169map('n', '<leader><esc>', ':nohlsearch<cr>', options)
170map('n', '<leader>n', ':bnext<cr>', options)
171map('n', '<leader>p', ':bprev<cr>', options)
172```
173
174For user defined commands, you're going to have to go the `vim.cmd`
175route:
176
177```lua
178local cmd = vim.cmd
179
180cmd(':command! WQ wq')
181cmd(':command! WQ wq')
182cmd(':command! Wq wq')
183cmd(':command! Wqa wqa')
184cmd(':command! W w')
185cmd(':command! Q q')
186```
187
188## managing packages
189
190Naturally, you can't use your favourite Vimscript package manager
191anymore, or at least, not without `vim.api.nvim_exec`ing a bunch of
192Vimscript (ew!). Thankfully, there are a few pure-Lua plugin managers
193available to use[^3] -- I personally use, and recommend
194[paq](https://github.com/savq/paq-nvim/). It's light and makes use of
195the [`vim.loop`](https://docs.libuv.org/en/v1.x/) API for async I/O.
196paq's docs are plentiful, so I'll skip talking about how to set it up.
197
198[^3]: Also see: [packer.nvim](https://github.com/wbthomason/packer.nvim/)
199
200## bonus: writing your own statusline
201
202Imagine using a bloated, third-party statusline, when you can just write
203your own.[^4] It's actually quite simple! Start by defining a table for
204every mode:
205
206[^4]: This meme was made by NIH gang.
207
208```lua
209-- statusline.lua
210
211 local mode_map = {
212 ['n'] = 'normal ',
213 ['no'] = 'n·operator pending ',
214 ['v'] = 'visual ',
215 ['V'] = 'v·line ',
216 [''] = 'v·block ',
217 ['s'] = 'select ',
218 ['S'] = 's·line ',
219 [''] = 's·block ',
220 ['i'] = 'insert ',
221 ['R'] = 'replace ',
222 ['Rv'] = 'v·replace ',
223 ['c'] = 'command ',
224 ['cv'] = 'vim ex ',
225 ['ce'] = 'ex ',
226 ['r'] = 'prompt ',
227 ['rm'] = 'more ',
228 ['r?'] = 'confirm ',
229 ['!'] = 'shell ',
230 ['t'] = 'terminal '
231}
232```
233
234The idea is to get the current mode from `vim.api.nvim_get_mode()` and
235map it to our desired text. Let's wrap that around in a small `mode()`
236function:
237
238```lua
239-- statusline.lua
240
241local function mode()
242 local m = vim.api.nvim_get_mode().mode
243 if mode_map[m] == nil then return m end
244 return mode_map[m]
245end
246```
247
248Now, set up your highlights. Again, there isn't any interface for
249highlights yet, so whip out that `vim.api.nvim_exec()`.
250
251```lua
252-- statusline.lua
253
254vim.api.nvim_exec(
255[[
256 hi PrimaryBlock ctermfg=06 ctermbg=00
257 hi SecondaryBlock ctermfg=08 ctermbg=00
258 hi Blanks ctermfg=07 ctermbg=00
259]], false)
260```
261
262Create a new table to represent the entire statusline itself. You can
263add any other functions you want (like one that returns the current git
264branch, for instance). Read `:h 'statusline'` if you don't understand
265what's going on here.
266
267```lua
268-- statusline.lua
269
270local stl = {
271 '%#PrimaryBlock#',
272 mode(),
273 '%#SecondaryBlock#',
274 '%#Blanks#',
275 '%f',
276 '%m',
277 '%=',
278 '%#SecondaryBlock#',
279 '%l,%c ',
280 '%#PrimaryBlock#',
281 '%{&filetype}',
282}
283```
284
285Finally, with the power of `table.concat()`, set your statusline. This
286is akin to doing a series of string concatenations, but way faster.
287
288```lua
289-- statusline.lua
290
291vim.o.statusline = table.concat(stl)
292```
293
294![statusline](https://x.icyphox.sh/statusline.png)
295
296## this is what being tpope feels like
297
298You can now write that plugin you always wished for! I sat down to write
299a plugin for [fzy](https://github.com/jhawthorn/fzy)[^5], which you can
300find [here](https://git.icyphox.sh/dotfiles/tree/config/nvim/lua/fzy)
301along with my entire Neovim config[^6]. I plan to port a the last of my
302`plugin/` directory over to Lua, soon™.
303
304And it's only going to get better when the Lua API is completed. We can
305all be Vim plugin artists now.
306
307[^5]: A less bloated alternative to fzf, written in C.
308[^6]: [GitHub link](https://github.com/icyphox/dotfiles/tree/master/config/nvim) --
309 if you're into that sort of thing.