Editor
The larvae language server, the analyzer inside it, every [lsp] key, and the VS Code extension
updated Aug 30, 20266 min read
larvae-lsp is a full language server. It carries Luau's own analysis frontend, vendored and compiled in, so hover, completion, type diagnostics, and inlay hints come from the same type inference Luau runs. The lints, the formatter, and the code actions larvae always served ride in the same process. One server, both halves, measured against luau-lsp on a real project until the answers matched.
The server installs beside the CLI: larvae self install copies larvae-lsp and its analyzer library to ~/.larvae/bin. The Larvae extension starts it in VS Code; any other editor starts larvae-lsp over stdio. Setup is on editor setup.
What the server answers
| capability | what comes back |
|---|---|
| diagnostics | Luau's type errors with their error numbers, larvae's lints, a worm's findings, and the cross-realm requires larvae check reports, in one publish |
| hover | the inferred type, doc comments, and the card speaks the author's words: a const binding says const, an export local says so, a member says hp: number |
| completion | members, keywords ranked above auto-imports, service auto-imports, module auto-imports, require path completion, and deprecated entries struck through |
| signature help | parameters with the active one marked |
| inlay hints | variable types, parameter types and names, function return types; a double click accepts a type hint into the file |
| go to definition | locals, members, modules, and requires, aliases included |
| document symbols, semantic tokens, rename, document links, colors | as an editor expects |
| formatting | the same layout larvae fmt writes |
| code actions | larvae's lint fixes plus what the worms of the project offer |
Inlay hints hold still while you type
[lsp.inlay_hints] update_delay holds the hints during a burst of keystrokes and recomputes once after the pause, so a hint never lands mid-word. Held hints follow their lines as edits move them. type_hint_max_length truncates a long label for display; the accept edit still writes the whole type, and a hint whose text is display notation rather than syntax, @metatable for one, never inserts.
Hovering a hint answers with the same card as hovering the name it annotates.
The dialect is served
const bindings and export local declarations parse, type, and hover as written. Value exports ride Luau's LuauExportValueSyntax flag, which larvae turns on by default; [lsp.fflags] with LuauExportValueSyntax = "false" restores stock parsing, and export then reports as the error it is there.
Requires resolve every way the build resolves them
String forms, .luaurc and [aliases] names, @game/... paths, and the instance forms: require(script.Parent.Widget), require(game.ReplicatedStorage.App), and the GetService, FindFirstChild, and WaitForChild call spellings all carry types. A worm-claimed target lowers the same way a string require does, so a .luaux component or a data file types through any spelling.
A module that returns { } and carries export type lines hints and hovers as { type PlayerData, type Slot } instead of the empty table, because the type exports are what the module is for.
The instance tree
The rojo sourcemap becomes types: each node is a declared type, and every script learns its own, so script.Parent.Config and game.ReplicatedStorage.Packages resolve with real children. A folder that holds only worm-claimed files joins the tree although rojo omits it.
[lsp] sourcemap names the file. With sourcemap_autogenerate on, the server keeps it fresh itself: sourcemap_command names the generator, run through the shell in the project root, and an empty command infers rojo from rojo_project_file and runs rojo sourcemap --watch. The generator and everything it started die with the server. A generator that does not start is said once and costs the autogeneration alone.
character_type decides what Player.Character types to: r15 and r6 carry the body parts of their rig, every part optional because a part can be destroyed or not yet streamed in. not_set types the union of the two.
[lsp.studio] links a running Studio through the companion plugin, so the live DataModel joins the tree.
Config as code
A .config.luau file is Luau's config written as Luau, and the server types it as such. The session wraps the file's return value in a check against the declared config shape, forces strict mode on the buffer, and the keys then typecheck, complete, and hover: languagemode, the lint table with every rule name, linterrors, typeerrors, globals, and aliases. A wrong key or a wrong value squiggles before anything reads the file. luau-lsp wires the same file the same way, so the two servers agree about it. Inlay hints stay out of a config file, because a type hint on data is noise.
Deprecated members
A deprecated use draws a strikethrough in the file and in the completion list. [lsp.completion] hide_deprecated drops deprecated entries from the list whole, and stays off by default. Where the platform marks a member, larvae's own deprecated finding stands down, so one use never reports twice.
Worms in the editor
A worm serves the editor in three tiers, declared in its manifest so gating never runs worm code:
- Resolve. The worm answers the requires it claims and hands the analyzer lowered Luau, so
require("./data.json")and a.luauxcomponent carry their real types. - Declarations.
.d.luautext the worm injects at session start. - Respond. The worm transforms hover, completions, or diagnostics before the editor sees them.
A worm that lints plain Luau reports beside the builtin lints, in the editor and in larvae lint alike. The worm side of all of this is on using worms and worm.toml.
[lsp.<worm>] holds the editor settings a worm declares:
[lsp.oop]
hide_private_completions = falseEach key checks against the worm's own [options], so a typo, a wrong type, or a name that is no worm warns instead of doing nothing.
The [lsp] table
| key | default | meaning |
|---|---|---|
enabled |
true |
off answers every request with nothing, so another server owns the files |
analyzer |
true |
off serves the lints, format, and actions alone; hover, completion, and type diagnostics go quiet |
claim_only |
false |
on serves only the files worms claim, so stock luau-lsp can own the plain Luau |
character_type |
"r15" |
r15, r6, or not_set |
sourcemap |
"sourcemap.json" |
the sourcemap to read, relative to the project root |
sourcemap_autogenerate |
true |
the server runs the sourcemap command itself |
sourcemap_command |
"" |
the generator, through the shell; empty infers rojo |
definitions |
[] |
extra type definition files, loaded after the platform globals; larvae analyze --definitions adds to the list |
rojo_project_file |
"default.project.json" |
what the inferred rojo command reads |
The subtables, each with its own keys: [lsp.completion], [lsp.inlay_hints], [lsp.signature_help], [lsp.hover], [lsp.index], [lsp.studio], [lsp.fflags], and [lsp.bytecode]. larvae self code completes every key with its description, and the extension mirrors them as editor settings, so the schema is the reference most people read. Two worth naming:
[lsp.fflags]controls Luau's own flags:enable_new_solverturns the new type solver on,enable_by_defaultturns on everything stable, andoversets one flag by name.[lsp.bytecode]tunes thelarvae/bytecodeandlarvae/compilerRemarksrequests the extension's compiled views use.
The VS Code extension
The Larvae extension starts the server, watches larvae.toml and restarts on change, and mirrors the [lsp] keys as larvae-lsp.* settings. Where the project file and the editor settings speak about one key, the project wins, because the project travels with the repository. The extension also asks larvae which extensions the project's worms claim before the client starts, so a .luaux buffer is served from the first keystroke.
Coexisting with luau-lsp
claim_only = true narrows larvae to the files worms claim and leaves plain Luau to another server. A worm whose hooks answer inside plain files widens that narrowing for its own answers and says so once. With the analyzer in the server there is no longer a reason to run two servers; the switch stays for the projects that want one anyway.
Related
- editor setup, the one-time steps
- configuration, the rest of
larvae.toml - using worms, what a worm adds to the editor