Velaguides

Editor setup

The VS Code extension, what the language server provides, and wiring the LSP into any editor.

Vela ships a language server, vela-rbxts-lsp, and a VS Code extension that drives it. The server reuses the compiler crate directly, so what the editor tells you about a class name is what the compiler would do with it. The same parser, the same theme resolution, the same diagnostic codes.

VS Code

Install Vela LSP from the marketplace, id astra-void.vela-rbxts-lsp. It requires VS Code ^1.91.0.

It activates on TypeScript and TypeScript React documents with a file: scheme. There is nothing to enable per project — open a .tsx file and the server starts.

Settings

The extension contributes three settings and no commands — changing any of them restarts the client for you.

PropTypeDescription
velaRbxts.lsp.serverPathstringExplicit path to a server binary. A relative path is resolved against the workspace root. Defaults to "", meaning auto-resolve.
velaRbxts.lsp.trace.server"off" | "messages" | "verbose"LSP protocol trace level written to the extension's output channel. Defaults to "off".
velaRbxts.lsp.enabledbooleanWhether to start the language client at all. Defaults to true.
velaRbxts.inlayHints.enabledbooleanShow what each class lowers to as an inlay hint after the class. Added in 0.13.0. Defaults to false.

How the server binary is resolved

velaRbxts.lsp.serverPath, if set, is used directly (resolved against the workspace root when relative). Otherwise the extension launches the server through process.execPath with require.resolve("@vela-rbxts/lsp"), gated on the matching platform package being present.

There is no cargo run fallback. Developing the server itself, build it and set serverPath explicitly.

What the language server provides

The declared capability set is small and deliberate.

Completions. Offered only when the compiler reports the cursor inside a className context, on the trigger characters -, :, ", ' and space. There is no resolve step, so every item arrives complete, and a completion rewrites the segment under the cursor and nothing else. placeholder-transparent is not offered on a textbox, which the compiler turns down. Every other family taking the transparent keyword still offers it.

Hover. Shows what a class token resolves to. A viewport-scaled offset reads as its rem value first — Sets `UIPadding` to `1rem` (16px at the base viewport). — in hovers and completion docs alike. A config that pins rem gets plain pixel wording, matching the emit it produces.

Diagnostics. Push-based via publish_diagnostics, debounced 200 ms and gated on the document version. There is no pull-diagnostics provider.

Document colors and color presentations. Color utilities get a swatch in the gutter and can be edited through the editor’s color picker.

Code actions. Quickfixes driven by diagnostics whose source is vela-rbxts, each offering up to three ranked replacements plus a “remove token” action. There is also the source action source.sortVelaClasses below.

Document highlight. Highlights other occurrences of the token under the cursor.

Inlay hints, since 0.13.0. The editor can show what each class lowers to, after the class:

<frame className="p-4 rounded-l-lg" />
// ↑ UIPadding.PaddingTop/Right/Bottom/Left
// ↑ UICorner.TopLeftRadius, BottomLeftRadius

They are off by default, behind velaRbxts.inlayHints.enabled. The labels come from the compiler’s own lowering, read back through an editor API.

That is the complete list. The server does not provide go-to-definition, references, rename, formatting, semantic tokens or signature help. Your editor falls back to its TypeScript language service for them.

Sorting class names

The compiler exposes a canonical class order, offered as the source action source.sortVelaClasses. It rewrites every className in the document at once — layout, sizing, spacing, colours, radius, typography, with variant-prefixed tokens last:

before
<frame className="text-lg bg-blue-600 flex p-4 hover:bg-blue-700 rounded-lg w-full" />
after
<frame className="flex w-full p-4 bg-blue-600 rounded-lg text-lg hover:bg-blue-700" />

The sort never changes what your classes do. Utilities that can write the same Roblox property are treated as one group and keep their relative order within it. A px-4 written after p-2 stays after it. A template keeps the whitespace around each interpolation.

It does not change how they are laid out either. Whitespace between tokens is carried over as written. A class list broken across several lines stays that way. An arbitrary payload containing a space, such as w-[calc(100% - 4px)], moves as the single class it is. A value whose bracket never closes is left alone entirely.

Run it from the editor’s source-action menu, or on save:

.vscode/settings.json
{
"editor.codeActionsOnSave": {
"source.sortVelaClasses": "explicit"
}
}

Any LSP client supporting source actions can invoke it by that kind. The underlying API is sortClassNames on @vela-rbxts/compiler. It takes { source, fileName } and returns { edits: [{ range: { start, end }, text }] }. Those are the same offsets the server turns into workspace edits.

Text synchronization is incremental and the position encoding is UTF-16.

The editor also suppresses unknown-theme-key while the payload typed so far is still a prefix of a real key. bg-sla never flashes a warning on the way to bg-slate-800, while bg-nope warns immediately.

Other editors

The server speaks LSP over stdio and has no VS Code dependency. Spawn it with:

Terminal window
npx --package @vela-rbxts/lsp vela-rbxts-lsp

The binary is named vela-rbxts-lsp but owned by the @vela-rbxts/lsp package. vela-rbxts-lsp on its own is the VS Code extension id, published as a VSIX, so a bare npx vela-rbxts-lsp resolves nothing.

The server does not read vela.config.ts from disk. It has no file loader and no config discovery. The client evaluates each config and hands over the result. Without that, the server falls back to the built-in default theme and every custom key reports unknown-theme-key.

Pass the configs in initializationOptions:

initializationOptions
{
"workspaceRoot": "/abs/path/to/project",
"configs": [
{ "dir": "/abs/path/to/project", "json": "{\"theme\":{\"colors\":{ }}}" }
]
}

workspaceRoot and configs are top-level siblings. Get the shape wrong and there is no error. The server deserializes with from_value(...).ok().unwrap_or_default(), so anything unparseable becomes zero configs and you are back on the default theme.

Each entry is a directory plus the resolved config serialized as JSON. The server matches a file to the nearest containing dir, which is how per-package configs in a monorepo work.

To update configs after startup, send the custom method vela-rbxts/setConfigs. Its payload carries only configs. That is what the VS Code extension does. It watches **/vela.config.{ts,json}, evaluates each match through @vela-rbxts/rbxtsc-host/project-config, and pushes { dir, json } pairs at startup and on every change.

A client that cannot evaluate TypeScript can build the same JSON any way it likes — the server only cares that the shape matches the resolved TailwindConfig.

Platform packages

The vela-rbxts-lsp launcher resolves one of six platform-specific packages:

  • @vela-rbxts/lsp-darwin-arm64
  • @vela-rbxts/lsp-darwin-x64
  • @vela-rbxts/lsp-linux-arm64-gnu
  • @vela-rbxts/lsp-linux-x64-gnu
  • @vela-rbxts/lsp-linux-x64-musl
  • @vela-rbxts/lsp-win32-x64-msvc

Those six are what the release workflow publishes and what the VSIX bundles. The launcher’s lookup table also names @vela-rbxts/lsp-linux-arm64-musl and @vela-rbxts/lsp-win32-arm64-msvc, but nothing builds them — on those two, build from source.

See also

  • Configuration — the config shape the server expects.
  • Diagnostics — every code the editor can surface, and which ones also appear in a build.
  • Dynamic class names — the case the editor cannot help you with, because nothing is reported at all.