all repos — site @ master

source for my site, found at icyphox.sh

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://cdn.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.