Lua Primer for Neovim

Image of Author
June 17, 2021 (last updated September 15, 2022)

Or: What I wish I knew about Lua before I started writing my Neovim init.lua config file, from scratch, and with no previous Lua experience.

Introduction

This primer will focus on Lua in the context of Neovim. Namely, writing init.lua configuration files and other Lua code for Neovim. This primer will assume the reader is already familiar with programming and programming concepts, but has had basically no exposure to Lua. As such, this primer will not cover "programming basics". Let's get started!

Lua on Your Machine

The lua executable has a builtin REPL. Assuming it's in your path, simply run lua. I recommend asdf as a general purpose version manager. Installing via asdf will also handle installing luarocks, which is the Lua package manager. However, the Lua REPL is not a "sandbox" experience. For example, you can't store local variables. For that, you'll need to write a lua file and pass it to the executable, i.e., lua example.lua. Lastly, Replit has an online Lua REPL.

Variables

Variables are global by default. To make them non-global, you add the local keyword. I recommend making every variable local unless you have a reason not to. (One thing to watch out for here is local function recursion. But, if you are writing a recursive function, note that Lua is optimized for tail-call "recursion".)

a = 3
local b = 4

function wow() return 5 end
local function wow() return 5 end
-- Equivalent to
local wow; wow = function () return 5 end
-- Similar to (but has recursion/self-reference problems)
local wow = function () return 5 end

Everything is a table

There is only one data type in Lua, the table. The table is an associative array, which you might know from other programming languages as a map or dictionary. The two most common constructors for tables are the "array" constructor and the "record" constructor.

local array = { 'one', 'two', 'three' }
local record = { a = 'one', b = 'two', c = 'three' }

-- the array constructor is syntactic sugar for the following,
-- equivalent "record" construction
local array = { [1] = 'one', [2] = 'two', [3] = 'three' }

-- The "record" constructor is itself syntactic sugar for
-- an equivalent multiline construction sequence
local record = {}
record.a = 'one'
record.b = 'two'
record.c = 'three'

print(array[1]) -- => one
print(record.a) -- => one

Tables, as the only fundamental datatype, have a lot of versatility. Keys can be anything (it gets weird). If the key is a string it can be invoked with method-like syntax.

local b = { a = 'one', [2] = 'two', ['b'] = 'three' }
print(b.a) -- => one
print(b['a']) -- => one
print(b[2]) -- => two
print(b['b']) -- => three
print(b.b) -- => three

One final thing to note is you can "unpack" table values directly into a function via the unpack function.

print(unpack({1, 2, 3})) -- => 1, 2, 3

local f = function () return { 'one', 'two' } end
local print_two_things = function (x, y)
  print(x)
  print(y)
end

print_two_things(unpack(f()))
-- => one
-- => two

Strings

Single-quoted and double-quoted strings are interchangeable and can both receive the \ escape sequence. print('lua is "cool"'), print("lua 'rocks'"), print("lua is \"cool\"").

Multiline strings are denoted by the [[ ... ]] syntax. It is common to see this in init.lua files to execute multiple lines of vimscript.

vim.cmd([[
  autocmd BufRead,BufNewFile *.ex,*.exs set filetype=elixir
  autocmd BufRead,BufNewFile *.eex,*.leex,*.sface set filetype=eelixir
  autocmd BufRead,BufNewFile mix.lock set filetype=elixir
]])

Functions

Function syntax in Lua can get pretty quirky, and, at least for me, caused a lot of confusion when I first started reading Lua code. Hands down the most confusing thing in my entire ramp-up on Lua is the fact that you can omit parentheses from function calls. Let's take a look.

Omitting parentheses

You can omit parentheses from function calls in two very common scenarios: When a function takes a single argument and that single argument is either (1) a string, or (2) a table. You can also add a space if you'd like. Everything you are about to see is a valid function call.

print({})
print{}
print {}

print('wow')
print'wow'
print 'wow'
print[[wow]]
print [[wow]]

With this in mind, it's a bit easier to understand Neovim plugins that have configuration examples that look like the following.

require'plugin_name'.setup{
  alpha = true,
  beta = false,
  ...
}

There's nothing special, or tricky, happening, they are just calling the require function (which we are about to cover) with a single string argument. Then, once the plugin's table is loaded, they are calling the setup function with a single table argument.

-- Example plugin code
plugin_name = {
  setup = function (config)
    if config.alpha or config.beta then
      ...
    end

    ...
  end,
  ...
}

Other Fun Facts about Functions

Lua functions can receive a variable number of arguments. Lua functions can receive named arguments. And, last but not least, Lua functions can return multiple values.

-- Example returning multiple values
local f = function () return 1, 2, 3 end
local x, y, z = f()
print(x, y, z) -- => 1, 2, 3

The require keyword

A common way to see configuration examples for plugins for init.lua is as follows.

require'plugin_name'.run_some_setup()

require'plugin_name'.setup{
  ...
}

We now understand the "syntactic sugar-coated" function calls. But, why call require twice? Well, to be honest, I don't know. You can create a local variable if you want to. All I can say is that there is no harm in repeat calls to require with identical strings. If a file has already been required it won't perform path lookup again.

There might be conventions I don't know about around require calls and whether or not to put spaces between "syntactic sugar-coated" function calls. I generally say stick with convention, but until I come across actual claims to that end, I'm sticking with what I find most aesthetically pleasing (local variables and spaces, respectively.)

Miscellany

Semicolons are entirely optional. Within a table, semicolons and commas are interchangeable delimiters. ~= is the syntax for "not equal". Indexes start at 1, not 0.

Conclusion

Hopefully this primer will help you move faster in getting a working init.lua up and running. After all, vim configuration is enough of a time-sink as it is, am I right? XD

Happy coding! :D