Lua Formatter & Beautifier

A World of Warcraft addon someone zipped up in 2014, a Neovim config with every plugin spec on one line, a Redis EVAL script pasted out of a log. Drop the Lua on the left and read a rebuilt copy on the right, indented by the keywords the parser sees rather than by whatever whitespace arrived with the paste.

CtrlEnter reformats
Sourceyour paste, untouched
Rebuiltread only, copy or download it

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.

RewrittenKept as written
IndentationLine breaks inside table constructors and argument lists
Spaces around =, .., comparisons and arithmeticText inside strings, long strings and comments
Spaces after commas, none beforeOrder of statements, names, values
Space inside { }, on or offNumbers, including 0x1p4 and 1e-3
Quote character on short strings, if askedStrings holding the other quote or a backslash
Trailing semicolons at line endsSemicolons used as table field separators
Runs of blank lines, capped at your settingSingle blank lines between functions
One line blocks, when set to expandOne 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.

Pasted
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
end
Rebuilt
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
end

Two 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.

Expand
if not cfg.enabled then
return
end
table.sort(rows, function(a, b)return a.ts < b.ts
end)
Keep
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 .editorconfig and 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.format call 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 and on 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 record and enum blocks 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-args

The 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.

Lua formatting questions

Is my Lua sent anywhere?

No. The lexer and the printer are JavaScript inside this page. A game addon, a proprietary OpenResty handler or a Redis script with keys in it stays in the tab, which is not true of formatters backed by an API.

Why is the output different from StyLua?

StyLua wraps at a column width, collapses or expands table constructors, and sorts requires when told to. This page rebuilds indentation, spacing and block boundaries while leaving your line breaks alone. For a reading copy the result is close. For a commit, run StyLua.

The indentation drifts halfway down the file. What happened?

A block is unbalanced somewhere above the drift. Look at the strip under the editors first: it counts openers against end and until keywords and names the line where pairing first failed. An unterminated long string or block comment produces the same symptom and is reported the same way.

Does it handle method calls and string call syntax?

Yes. obj:method(x) keeps no space around the colon, require "json" and f { 1, 2 } keep one space between the name and the argument, and a bare name after a complete expression starts a new line so minified modules split back into statements.

Which quote style should I pick?

Whatever the surrounding file uses. Double is the StyLua default and what most newer Lua code uses. Strings containing the target quote or a backslash are left alone rather than rewritten with escapes, so the change is always safe to apply.

How do I get the original back?

Your paste stays in the left pane. Formatting writes only to the right pane, so both versions sit side by side until you press Clear or reload.