What gets rewritten and what stays as typed
A formatter earns trust by being predictable about its reach. The table below is the whole contract. Anything in the left column is regenerated from the token stream. Anything in the right column comes out byte for byte as you pasted it.
| Rewritten | Kept as written |
|---|---|
| Indentation | Line breaks inside table constructors and argument lists |
Spaces around =, .., comparisons and arithmetic | Text inside strings, long strings and comments |
| Spaces after commas, none before | Order of statements, names, values |
Space inside { }, on or off | Numbers, including 0x1p4 and 1e-3 |
| Quote character on short strings, if asked | Strings holding the other quote or a backslash |
| Trailing semicolons at line ends | Semicolons used as table field separators |
| Runs of blank lines, capped at your setting | Single blank lines between functions |
| One line blocks, when set to expand | One line blocks, when set to keep |
No statement is moved, merged or reordered. No line is wrapped at a column limit. The output has the same tokens in the same order as the input, which is a property you should hold every formatter to before pasting its result into a commit.
Keywords are the braces
Lua has no curly braces around blocks. A block opens with function, then, do or repeat and closes with end or until, and else and elseif close one arm while opening the next. Indentation has to follow those keywords, which means the formatter has to know a keyword from a word.
The previous version of this page did not. It searched each line for the letters end and outdented whenever they appeared, so local friend = "send" pulled the whole function body one level left. A comment reading -- end of setup did the same. The rewrite lexes the source first, so end counts only when the tokenizer sees a bare keyword outside a string or comment.
Every opener is paired with its closer during the lexing pass. The strip under the editors reports the totals, and when they disagree it names the line where pairing first went wrong. Read that number before reading the output, because once a block is unbalanced every indent below it is a guess.
function Limiter:sweep(maxIdle)local dropped=0
for key,b in pairs(self.buckets) do if now()-b.seen>maxIdle then self.buckets[key]=nil dropped=dropped+1 end end
return dropped
endfunction Limiter:sweep(maxIdle)local dropped = 0
for key, b in pairs(self.buckets) do
if now() - b.seen > maxIdle then
self.buckets[key] = nil
dropped = dropped + 1
end
end
return dropped
endTwo statements sharing a line in the source, self.buckets[key]=nil dropped=dropped+1, come out on separate lines. Lua permits the spacing because the grammar has no statement terminator, and minifiers rely on the fact. The formatter breaks before local, return, if, for and the other statement keywords, and before a bare name following a complete expression, which is what turns a minified module back into readable lines.
Long brackets keep their level
A Lua long string opens with [[ or with equals signs between the brackets, [==[, and closes only on the matching ]==]. The equals count is the whole point: a level two string holds ]] as ordinary text. Block comments use the same rule behind --. The lexer counts the equals signs on the way in and searches for the exact matching closer, so a SQL query stored in [=[ ... ]=] with a ]] inside survives untouched.
Long strings are never reindented, since their leading whitespace is part of the value. Block comments standing alone on their lines are shifted to the surrounding indent, with the relative indent of their inner lines preserved. An unterminated long bracket swallows the rest of the file, the same way the real parser would, and the strip under the editors reports it.
Expanding or keeping one line blocks
Lua code written by hand is full of short guards: if not x then return end, while busy do wait() end. StyLua and most style guides expand these onto three lines, and the default here does the same. Set the control to Keep and any block whose opener and closer share a source line stays as one line, so the guard clauses survive while the multi line blocks around them still get rebuilt.
if not cfg.enabled then
return
end
table.sort(rows, function(a, b)return a.ts < b.ts
end)if not cfg.enabled then return end
table.sort(rows, function(a, b) return a.ts < b.ts end)Keep only respects blocks already on one line. A block spread over several lines in the source is never collapsed, because deciding what fits on a line needs a column limit and this page does not have one.
Pick the indent the codebase already uses
Lua projects disagree about indentation more than most languages, partly because the reference manual takes no position. Match the file you are editing rather than the default here.
- StyLua defaultTabs. The formatter most Lua repositories run in CI, and its untouched config writes a tab per level.
- Neovim runtime2 spaces, set in the repository's own
.stylua.toml. Plugin authors who target Neovim usually follow along. - OpenResty modules4 spaces across the
lua-resty-*libraries and most nginx handlers written against them. - Kong2 spaces, enforced by its
.editorconfigand luacheck setup. - LuaRocks3 spaces. Rare, but the package manager's own source is written this way, so contributions have to match.
- Roblox LuauTabs, per the Roblox Lua style guide. Studio scripts default to tabs as well.
Version note. The lexer knows the Lua 5.3 and 5.4 operators //, &, |, ~, << and >>, plus goto labels written ::name::, and treats them the same on a 5.1 file where they would be syntax errors. Luau compound assignments such as += and ..= are spaced correctly. Luau type annotations are not understood, so local n: number comes out as local n:number.
Where it stops
- No column limit. A 300 character
string.formatcall stays on one line. - Table constructors keep the line breaks you wrote. A table typed on one line stays on one line, and a table split across lines keeps its shape, so a config table is not rewritten into something you did not write.
- Multi line expressions such as a condition continued with
andon the next line receive the block indent, not a hanging indent aligned to the operator. - Comments are preserved verbatim and stay attached to the line they were on. Nothing reflows comment text.
- Luau type syntax, MoonScript, Fennel and Teal are outside scope. Teal in particular formats badly because
recordandenumblocks are not known openers. - Sources over 2 MB are refused, because the formatter runs in the tab and a file of that size would freeze it.
Treat the output as a reading copy. A commit should go through the formatter your repository already runs, because that one has the column limit and the line breaking rules and this one, by design, does not.
Let the repository own the rule
Once a team agrees on a style, the arguments end only if a tool enforces it. StyLua is the usual pick. Drop a config at the repository root mirroring what you settled on above.
# .stylua.toml
column_width = 100
line_endings = "Unix"
indent_type = "Spaces"
indent_width = 4
quote_style = "AutoPreferDouble"
call_parentheses = "Always"
collapse_simple_statement = "Never"Then wire it in so drift fails on the pull request rather than in a rebase weeks later.
stylua --check .
stylua src/ spec/
luacheck src/ --std lua54 --no-unused-argsThe first line changes nothing and exits non zero when any file differs from the configured style, which is the one worth putting in continuous integration. The luacheck line is a separate concern, linting rather than formatting, but the two tend to be adopted together and share the same .luarc or .luacheckrc neighborhood at the repository root.
