Lua code and StyLua
Lua turns up wherever a program needs a small embedded scripting language: Neovim and WezTerm configuration, LÖVE and Defold games, Roblox, World of Warcraft addons, Redis EVAL scripts, Nginx through OpenResty, and HAProxy. Its syntax is loose enough that hand-written files drift quickly, with statements packed onto one line, mixed quote styles and inconsistent indentation.
StyLua is an opinionated Lua formatter written in Rust and modelled on Prettier: it parses the file and prints it again from the syntax tree, so the result is the same however messy the input was. It is especially common among Neovim plugin authors, and many plugin repositories check formatting with it in CI. Here it runs as WebAssembly inside the page; your scripts are never sent to a server.
What StyLua changes
- One statement per line.
local a=1 local b=2becomes two lines, andif not ok then return endis expanded into a three-line block. - Spacing. Spaces go around operators and after commas, and table constructors get padding:
{a=1}becomes{ a = 1 }. - Quotes. Strings prefer double quotes, so
'hello'becomes"hello". A string that would need escapes keeps the quote that avoids them, so'say "hi"'is left alone. Long bracket strings like[[ ... ]]are never touched. - Call parentheses. Lua lets you call a function with a single string or table argument and no parentheses. StyLua adds them, so
require 'socket'becomesrequire("socket")andsetup{...}becomessetup({...}). - Line wrapping. Long calls and tables are broken over several lines at 80 columns. Anonymous functions passed as arguments, such as the callback in
vim.keymap.set, keep thefunction()on the call line and close withend, {...}).
Comments are kept in place and require calls stay in the order you wrote them. For a Redis EVAL script, paste the Lua body itself rather than the whole redis-cli command, whose shell quoting is not Lua. To protect a hand-aligned table, put -- stylua: ignore on the line before a statement, or wrap a region in -- stylua: ignore start and -- stylua: ignore end.
Indentation and language versions
There are no Lua-specific options. The toolbar’s Indent decides between 2 spaces, 4 spaces and tabs. StyLua’s own default is tabs, while most Neovim configs use 2 spaces, so match whatever the rest of your repository uses. Lines are wrapped at 80 columns rather than StyLua’s usual 120, and StyLua treats that width as a target rather than a hard cap, so an unbreakable long string can still run past it. StyLua runs on Ctrl/Cmd+Enter, and Ctrl/Cmd+Shift+C copies.
The parser accepts Lua 5.1 through 5.4, which covers LuaJIT as well: goto and ::labels::, integer division //, bitwise operators and <const> and <close> attributes all work. Luau, the Roblox dialect, is only partly supported. A .luau file formats if it sticks to standard Lua, but type annotations such as a: number, type declarations, continue and compound assignments like x += 1 fail with a parse error.
Examples
Neovim keymaps and options
Each option assignment gets its own line, single quotes become double, and the callbacks stay attached to their calls.
vim.keymap.set('n','<leader>ff',function() require('telescope.builtin').find_files({hidden=true}) end,{desc='Find files'})
vim.opt.number=true vim.opt.relativenumber=true vim.opt.tabstop=4
vim.api.nvim_create_autocmd('BufWritePre',{pattern='*.lua',callback=function() vim.lsp.buf.format() end})vim.keymap.set("n", "<leader>ff", function()
require("telescope.builtin").find_files({ hidden = true })
end, { desc = "Find files" })
vim.opt.number = true
vim.opt.relativenumber = true
vim.opt.tabstop = 4
vim.api.nvim_create_autocmd("BufWritePre", {
pattern = "*.lua",
callback = function()
vim.lsp.buf.format()
end,
})
Inventory class with metatables
Nested for and if blocks are indented with tabs, StyLua’s own default, and the table of items is spread over several lines.
local Inventory={} Inventory.__index=Inventory
function Inventory.new(items) local self=setmetatable({},Inventory) self.items=items or {} return self end
function Inventory:total() local t=0 for _,item in ipairs(self.items) do if item.qty>0 then t=t+item.qty*item.price end end return t end
local inv=Inventory.new({{sku="KB-104",qty=1,price=129.9},{sku="MS-220",qty=2,price=39.5}})
print(string.format("total: %.2f",inv:total()))local Inventory = {}
Inventory.__index = Inventory
function Inventory.new(items)
local self = setmetatable({}, Inventory)
self.items = items or {}
return self
end
function Inventory:total()
local t = 0
for _, item in ipairs(self.items) do
if item.qty > 0 then
t = t + item.qty * item.price
end
end
return t
end
local inv = Inventory.new({
{ sku = "KB-104", qty = 1, price = 129.9 },
{ sku = "MS-220", qty = 2, price = 39.5 },
})
print(string.format("total: %.2f", inv:total()))
LÖVE game loop with call shorthand
The parenthesis-free setTitle call gains parentheses, and the … concatenation gets spaces on both sides.
local player={x=100,y=200,speed=180}
function love.load() love.window.setTitle 'Orbit' font=love.graphics.newFont(14) end
function love.update(dt) if love.keyboard.isDown('right') then player.x=player.x+player.speed*dt end end
function love.draw() love.graphics.print('Score: '..score,10,10) love.graphics.circle('fill',player.x,player.y,12) endlocal player = { x = 100, y = 200, speed = 180 }
function love.load()
love.window.setTitle("Orbit")
font = love.graphics.newFont(14)
end
function love.update(dt)
if love.keyboard.isDown("right") then
player.x = player.x + player.speed * dt
end
end
function love.draw()
love.graphics.print("Score: " .. score, 10, 10)
love.graphics.circle("fill", player.x, player.y, 12)
end
Common errors and how to fix them
| Error | Cause | Fix |
|---|---|---|
error parsing: unexpected token `` (1:14 to 1:22), expected `end` to close function body block | A function has no closing end. The empty token in the message is the end of the input. | Add end after the last statement of the function. Counting function, if, for and while openers against end keywords helps find which one is missing. |
error parsing: unexpected token `` (1:19 to 1:19), expected `end` to conclude `if` | An if … then block was never closed with end. | Add end after the last statement of the if block. |
error parsing: unclosed string (1:5 to 1:9) | A quoted string is missing its closing quote. Regular Lua strings cannot span lines. | Close the quote, or use a long bracket string [[ … ]] for text with line breaks. |
error parsing: unexpected token `+` (1:13 to 1:14), expected expression after binary operator | An expression ends with an operator and nothing after it, often from a line that was cut off. | Finish the expression or remove the trailing operator. |
error parsing: | Some mistakes produce the bare prefix with only a position, for example a doubled comma in a table, = =, or Luau syntax such as x += 1 and type annotations. | Look at the highlighted column. If the code is Luau, the error is expected: only standard Lua syntax is supported. |
Frequently asked questions
Is the output identical to running stylua locally?
It matches StyLua with default settings except for two values taken from the page: the toolbar indentation and an 80-column width instead of StyLua’s 120. Set your stylua.toml to the same values for identical results.
Can I format Roblox Luau code?
Only the parts that are plain Lua. Type annotations, type aliases, continue and compound assignment operators are Luau extensions and fail to parse here.
Why were parentheses added to my require calls?
StyLua always writes call parentheses by default, turning require ‘x’ into require(“x”). Both forms do the same thing in Lua.
How do I keep a table aligned by hand?
Put – stylua: ignore on the line above the statement, or surround a section with – stylua: ignore start and – stylua: ignore end.
Does it work with LuaJIT and OpenResty code?
Yes. LuaJIT uses Lua 5.1 syntax with a few 5.2 additions such as goto, all of which the parser accepts.