Lua development in NeovimEdit
On versions
- Neovim currently embeds Lua 5.1; verify this with 
:lua print(_VERSION). 
- But really, it’s LuaJIT 2.1.0-beta3; see this with 
:lua print(jit.version). 
- In the Neovim repo after building (which is as easy as 
make), you can find a REPL at .deps/usr/bin/luajit.
- The LuaJIT REPL doesn’t use readline, which is annoying.
 
- Use 
brew install rlwrap to add fake readline support; invoke with rlwrap .deps/usr/bin/luajit; I’ve added a crude alias for this to my dotfiles (nlua). 
 
- Lua 5.x updates include breaking changes, so be wary of reading the documentation for the wrong version.
- An example breaking change: 
... is the vararg operator for LuaJIT, but other versions use (used?) arg. 
 
Language quirks
- 
Indices are 1-based.
- This causes confusion in Neovim because some APIs use 0-based indices (eg. 
vim.api.nvim_buf_get_lines) while others use 1-based indices (eg. vim.api.nvim_win_set_cursor; details in :h api-indexing). 
 
- 
Standard library is tiny, meaning you have to implement basic things yourself.
 
- 
~= is the "not equals" operator.
 
- 
No regular expressions (just "patterns").
- Example: instead of 
\b word boundary, you can use %f[%A] (a %f "frontier" pattern, look for where %A — ie. a non-word character — starts to match). 
- Non-greedy matches: instead of greedy 
*, non-greedy {-} (Vimscript), you can use -. 
 
- 
No ternaries:
- Can’t write 
a ? b : c 
- Can’t write 
if a then b else c 
- Can write 
a and b or c (but beware, if b is false, expression will always evaluate to c) 
 
- 
0 is truthy, so functions in the Vim API that return 0 or 1 need an explicit == check; eg:
-- Passes conditonally:
if vim.fn.has('autocmd') == 1 then; end
-- Always passes:
if vim.fn.has('autocmd') then; end
 
- 
Some methods return tuples, which is great, but can make their use awkward in some situations; eg:
-- `gsub` returns the new string plus the index of the match.
-- We want to make a new list with the result of a couple of
-- `gsub` calls, but we can't do this:
list = {
  lines[1]:gsub('a', 'b'),
  lines[2]:gsub('c', 'd'),
}
-- or this...
list = {
  lines[1]:gsub('a', 'b')[1],
  lines[2]:gsub('c', 'd')[1],
}
-- we have to do this...
list = {
  ({lines[1]:gsub('a', 'b')})[1],
  ({lines[2]:gsub('c', 'd')})[1],
}
 
Language strengths
- Small/simple (easy to learn).
 
- Fast, apparently.
 
- Functions are first class values.
 
Syntax
- Line comments: 
-- 
- Block comments: 
--[[ to --]] 
- Functions: 
function optional_name() to end 
- Loops: 
for ... do to end:
- Numbers: 
for i = 1, 5 do/end (loops through 1 to 5, inclusive). 
- Lists: 
for i, val in ipairs({'a', 'b', 'c'}) do/end 
 
- Conditionals:
if ... then to end; also: 
else; and 
elseif ... then (else if will usually be a syntax error, because it needs an additional end). 
 
Neovim APIs
vim.gsplit(string, separator, plain): Returns an iterator; if plain is passed and true, separator is interpreted literally instead of as a pattern. 
vim.inspect(): eg. :lua print(vim.inspect(something)) 
vim.split(string, separator, plain): Returns a list-like table; if plain is passed and true, separator is interpreted literally instead of as a pattern. 
vim.trim(): trims a string. 
Shortcuts
Reading options
vim.o[option] (eg. vim.o.hidden or vim.o.hid etc) is shorthand for vim.api.nvim_get_option('hidden'). 
vim.bo[option] (eg. vim.bo.filetype or vim.bo.ft etc) is shorthand for vim.api.nvim_buf_get_option(0, 'filetype'). 
vim.wo[option] (eg. vim.wo.list etc) is shorthand for vim.api.nvim_win_get_option(0, 'list'). 
Calling Vimscript functions
vim.fn[name] (eg. vim.fn.exists etc) runs a Vimscript function of the same name. 
Running Vim commands
vim.cmd(string) (eg. vim.cmd('highlight clear')) runs a Vim command. 
String methods
- 
Available methods (via :lua print(vim.inspect(vim.tbl_keys(string)))):
{ "find", "lower", "format", "rep", "gsub", "len", "gmatch", "dump", "match", "reverse", "byte", "char", "upper", "gfind", "sub" }
gmatch(): returns an iterator that finds a pattern; can be used to split a string (eg. to split words on commas: for word in string.gmatch(words, '[^,]+') do/end) but note that vim.gsplit() is probably easier to use. 
sub(startIndex, endIndex): returns a substring; indices are 1-based and inclusive; eg. ("foobar"):sub(2, 5) is "ooba". 
len(): returns length, but not that ("foo"):len() can be written as #"foo". 
 
Table methods
table.concat(tbl, joiner): eg. table.concat(words, ' '). 
table.insert(tbl, value) 
Pro-Tips™
- For testing, can blow away module from cache with: 
:lua package.loaded['some.pack'] = nil. 
:lua print(_VERSION).:lua print(jit.version).make), you can find a REPL at .deps/usr/bin/luajit.
- The LuaJIT REPL doesn’t use readline, which is annoying.
 - Use 
brew install rlwrapto add fake readline support; invoke withrlwrap .deps/usr/bin/luajit; I’ve added a crude alias for this to my dotfiles (nlua). 
- An example breaking change: 
...is the vararg operator for LuaJIT, but other versions use (used?)arg. 
- 
Indices are 1-based.
- This causes confusion in Neovim because some APIs use 0-based indices (eg. 
vim.api.nvim_buf_get_lines) while others use 1-based indices (eg.vim.api.nvim_win_set_cursor; details in:h api-indexing). 
 - This causes confusion in Neovim because some APIs use 0-based indices (eg. 
 - 
Standard library is tiny, meaning you have to implement basic things yourself.
 - 
~=is the "not equals" operator. - 
No regular expressions (just "patterns").
- Example: instead of 
\bword boundary, you can use%f[%A](a%f"frontier" pattern, look for where%A— ie. a non-word character — starts to match). - Non-greedy matches: instead of greedy 
*, non-greedy{-}(Vimscript), you can use-. 
 - Example: instead of 
 - 
No ternaries:
- Can’t write 
a ? b : c - Can’t write 
if a then b else c - Can write 
a and b or c(but beware, ifbis false, expression will always evaluate toc) 
 - Can’t write 
 - 
0is truthy, so functions in the Vim API that return 0 or 1 need an explicit==check; eg:-- Passes conditonally: if vim.fn.has('autocmd') == 1 then; end -- Always passes: if vim.fn.has('autocmd') then; end - 
Some methods return tuples, which is great, but can make their use awkward in some situations; eg:
-- `gsub` returns the new string plus the index of the match. -- We want to make a new list with the result of a couple of -- `gsub` calls, but we can't do this: list = { lines[1]:gsub('a', 'b'), lines[2]:gsub('c', 'd'), } -- or this... list = { lines[1]:gsub('a', 'b')[1], lines[2]:gsub('c', 'd')[1], } -- we have to do this... list = { ({lines[1]:gsub('a', 'b')})[1], ({lines[2]:gsub('c', 'd')})[1], } 
Language strengths
- Small/simple (easy to learn).
 
- Fast, apparently.
 
- Functions are first class values.
 
Syntax
- Line comments: 
-- 
- Block comments: 
--[[ to --]] 
- Functions: 
function optional_name() to end 
- Loops: 
for ... do to end:
- Numbers: 
for i = 1, 5 do/end (loops through 1 to 5, inclusive). 
- Lists: 
for i, val in ipairs({'a', 'b', 'c'}) do/end 
 
- Conditionals:
if ... then to end; also: 
else; and 
elseif ... then (else if will usually be a syntax error, because it needs an additional end). 
 
Neovim APIs
vim.gsplit(string, separator, plain): Returns an iterator; if plain is passed and true, separator is interpreted literally instead of as a pattern. 
vim.inspect(): eg. :lua print(vim.inspect(something)) 
vim.split(string, separator, plain): Returns a list-like table; if plain is passed and true, separator is interpreted literally instead of as a pattern. 
vim.trim(): trims a string. 
Shortcuts
Reading options
vim.o[option] (eg. vim.o.hidden or vim.o.hid etc) is shorthand for vim.api.nvim_get_option('hidden'). 
vim.bo[option] (eg. vim.bo.filetype or vim.bo.ft etc) is shorthand for vim.api.nvim_buf_get_option(0, 'filetype'). 
vim.wo[option] (eg. vim.wo.list etc) is shorthand for vim.api.nvim_win_get_option(0, 'list'). 
Calling Vimscript functions
vim.fn[name] (eg. vim.fn.exists etc) runs a Vimscript function of the same name. 
Running Vim commands
vim.cmd(string) (eg. vim.cmd('highlight clear')) runs a Vim command. 
String methods
- 
Available methods (via :lua print(vim.inspect(vim.tbl_keys(string)))):
{ "find", "lower", "format", "rep", "gsub", "len", "gmatch", "dump", "match", "reverse", "byte", "char", "upper", "gfind", "sub" }
gmatch(): returns an iterator that finds a pattern; can be used to split a string (eg. to split words on commas: for word in string.gmatch(words, '[^,]+') do/end) but note that vim.gsplit() is probably easier to use. 
sub(startIndex, endIndex): returns a substring; indices are 1-based and inclusive; eg. ("foobar"):sub(2, 5) is "ooba". 
len(): returns length, but not that ("foo"):len() can be written as #"foo". 
 
Table methods
table.concat(tbl, joiner): eg. table.concat(words, ' '). 
table.insert(tbl, value) 
Pro-Tips™
- For testing, can blow away module from cache with: 
:lua package.loaded['some.pack'] = nil. 
- Line comments: 
-- - Block comments: 
--[[to--]] - Functions: 
function optional_name()toend - Loops: 
for ... dotoend:- Numbers: 
for i = 1, 5 do/end(loops through 1 to 5, inclusive). - Lists: 
for i, val in ipairs({'a', 'b', 'c'}) do/end 
 - Numbers: 
 - Conditionals:
if ... thentoend; also:else; andelseif ... then(else ifwill usually be a syntax error, because it needs an additionalend).
 
Neovim APIs
vim.gsplit(string, separator, plain): Returns an iterator; if plain is passed and true, separator is interpreted literally instead of as a pattern. 
vim.inspect(): eg. :lua print(vim.inspect(something)) 
vim.split(string, separator, plain): Returns a list-like table; if plain is passed and true, separator is interpreted literally instead of as a pattern. 
vim.trim(): trims a string. 
Shortcuts
vim.gsplit(string, separator, plain): Returns an iterator; if plain is passed and true, separator is interpreted literally instead of as a pattern.vim.inspect(): eg. :lua print(vim.inspect(something))vim.split(string, separator, plain): Returns a list-like table; if plain is passed and true, separator is interpreted literally instead of as a pattern.vim.trim(): trims a string.Reading options
vim.o[option] (eg. vim.o.hidden or vim.o.hid etc) is shorthand for vim.api.nvim_get_option('hidden'). 
vim.bo[option] (eg. vim.bo.filetype or vim.bo.ft etc) is shorthand for vim.api.nvim_buf_get_option(0, 'filetype'). 
vim.wo[option] (eg. vim.wo.list etc) is shorthand for vim.api.nvim_win_get_option(0, 'list'). 
Calling Vimscript functions
vim.fn[name] (eg. vim.fn.exists etc) runs a Vimscript function of the same name. 
Running Vim commands
vim.cmd(string) (eg. vim.cmd('highlight clear')) runs a Vim command. 
String methods
- 
Available methods (via :lua print(vim.inspect(vim.tbl_keys(string)))):
{ "find", "lower", "format", "rep", "gsub", "len", "gmatch", "dump", "match", "reverse", "byte", "char", "upper", "gfind", "sub" }
gmatch(): returns an iterator that finds a pattern; can be used to split a string (eg. to split words on commas: for word in string.gmatch(words, '[^,]+') do/end) but note that vim.gsplit() is probably easier to use. 
sub(startIndex, endIndex): returns a substring; indices are 1-based and inclusive; eg. ("foobar"):sub(2, 5) is "ooba". 
len(): returns length, but not that ("foo"):len() can be written as #"foo". 
 
Table methods
table.concat(tbl, joiner): eg. table.concat(words, ' '). 
table.insert(tbl, value) 
Pro-Tips™
- For testing, can blow away module from cache with: 
:lua package.loaded['some.pack'] = nil. 
vim.o[option] (eg. vim.o.hidden or vim.o.hid etc) is shorthand for vim.api.nvim_get_option('hidden').vim.bo[option] (eg. vim.bo.filetype or vim.bo.ft etc) is shorthand for vim.api.nvim_buf_get_option(0, 'filetype').vim.wo[option] (eg. vim.wo.list etc) is shorthand for vim.api.nvim_win_get_option(0, 'list').vim.fn[name](eg.vim.fn.existsetc) runs a Vimscript function of the same name.
Running Vim commands
vim.cmd(string) (eg. vim.cmd('highlight clear')) runs a Vim command. 
String methods
- 
Available methods (via :lua print(vim.inspect(vim.tbl_keys(string)))):
{ "find", "lower", "format", "rep", "gsub", "len", "gmatch", "dump", "match", "reverse", "byte", "char", "upper", "gfind", "sub" }
gmatch(): returns an iterator that finds a pattern; can be used to split a string (eg. to split words on commas: for word in string.gmatch(words, '[^,]+') do/end) but note that vim.gsplit() is probably easier to use. 
sub(startIndex, endIndex): returns a substring; indices are 1-based and inclusive; eg. ("foobar"):sub(2, 5) is "ooba". 
len(): returns length, but not that ("foo"):len() can be written as #"foo". 
 
Table methods
table.concat(tbl, joiner): eg. table.concat(words, ' '). 
table.insert(tbl, value) 
Pro-Tips™
- For testing, can blow away module from cache with: 
:lua package.loaded['some.pack'] = nil. 
vim.cmd(string) (eg. vim.cmd('highlight clear')) runs a Vim command.- 
Available methods (via
:lua print(vim.inspect(vim.tbl_keys(string)))):{ "find", "lower", "format", "rep", "gsub", "len", "gmatch", "dump", "match", "reverse", "byte", "char", "upper", "gfind", "sub" }gmatch(): returns an iterator that finds a pattern; can be used to split a string (eg. to splitwordson commas:for word in string.gmatch(words, '[^,]+') do/end) but note thatvim.gsplit()is probably easier to use.sub(startIndex, endIndex): returns a substring; indices are 1-based and inclusive; eg.("foobar"):sub(2, 5)is"ooba".len(): returns length, but not that("foo"):len()can be written as#"foo".
 
Table methods
table.concat(tbl, joiner): eg. table.concat(words, ' '). 
table.insert(tbl, value) 
Pro-Tips™
- For testing, can blow away module from cache with: 
:lua package.loaded['some.pack'] = nil. 
table.concat(tbl, joiner): eg. table.concat(words, ' ').table.insert(tbl, value)- For testing, can blow away module from cache with: 
:lua package.loaded['some.pack'] = nil.