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