Releases: ericplane/Luix
Luix v1.5.0 - Per-file framework detection & parity improvements
Active-framework detection + status-bar picker
Every Lua / Luau file now has an active framework, picked per file
in this priority order:
- Explicit override — the new
luix.activeFrameworksetting
(defaultauto). Set toreact/roact/fusion/videto
force everything everywhere. - In-file
require(…)— first match wins, in
roact → vide → fusion → reactorder (Roact tested first because
its name contains "react"). - In-file factory call — alias of the first
e(...)/New "..."/create "..."etc. → its framework. - Workspace fallback — at activation Luix samples up to 25
indexed files and uses the most-represented framework as the
tie-breaker for brand-new files. - None — no UI signal → snippets and completions stay quiet
(same gate the existinglooksLikeUIFilepolicy used).
A new status-bar item on the right ($(symbol-namespace) Luix: Vide) shows the current pick with a tooltip explaining where it
came from ("detected from require(…) import", "set by
luix.activeFramework", etc.). Click it to open a quickpick of
Auto, React, Roact, Fusion, Vide; the choice writes
luix.activeFramework to workspace settings. The picker is also
available via the command palette as
Luix: Set active framework….
Vide parens form — Vide.create("Frame", { … })
Vide accepts both create "Frame" { … } and
create("Frame", { … }) / Vide.create("Frame", { … }). Until
1.5.0 Luix only recognised the curried shape — the parens form
silently fell out of every detector, so prop completions, hover
docs, and call-tree inlay hints all stopped firing inside it.
- New
recognizedCallShapes?: CallShape[]onFrameworkSpec. Vide
registers both"parens"and"curried", so its aliases now land
in both partition buckets. getAliasPartition()iteratesrecognizedCallShapes(defaulting
to[callShape]for backward compatibility), so other frameworks
behave exactly as before.Vide.createadded to Vide's aliases.findAllCreateElementCallsImpl's parens regex now captures the
matched alias. When the alias is also in the curried bucket
(Vide), the props brace doubles as the inline-children container —
Vide.create("Frame", { Child(...) })parses withChild(...)
as a child, not as a stray prop.
Three new tests cover the cases (findEnclosingPropsCall,
findAllCreateElementCalls nested-tree, findEnclosingFactoryStringArg).
Thank you to @Zyntion for the idea!
Snippet / scaffold parity across all four frameworks
With per-file gating in place, every framework gets the same
snippet matrix without polluting the dropdown for the others:
- Roact in the scaffold quickpick — Explorer right-click → New
component now lists Roact alongside React / Fusion / Vide.
Template mirrors React's but usesRoact.createElementand drops
theReact.ReactNodereturn annotation. - Element snippets — Fusion (
n*), Vide (c*), and Roact (r*)
now have the same 11-snippet baseline React (e*) always had:
Frame, ScrollingFrame, TextLabel, TextButton, ImageLabel,
ImageButton, UIListLayout, UIGridLayout, UIPadding, UICorner,
UIStroke. Bodies use each framework's idiomatic call shape and
event syntax ([OnEvent "Activated"]for Fusion, plain
Activated = function() … endfor Vide). - Function-component scaffolds —
rfc(React), newrofc
(Roact), newnfc(Fusion), newvfc(Vide). Each is gated to
its framework — typingrfcin a Vide file no longer surfaces a
React scaffold. - Event-handler shorthand snippets —
reactEvent(React),
newroactEvent(Roact), newonEvent(Fusion), newvideEvent
(Vide). Each emits the right syntax for its framework — Fusion
gets[OnEvent "Activated"], Vide gets bareActivated = function() … end, etc. - State-primitive snippets — new for both Fusion (
value,
computed,spring,tween,observer,forKeys,
forValues,forPairs) and Vide (source,derive,effect,
cleanup,untrack,batch,show,switch,indexes,
values). Each gated to its framework.
Computed-key fast-paths for all event-bearing frameworks
The [React.Event.X|] / [React.Change.X|] autocomplete used to
be hard-coded to the React prefix. Extended to:
- Roact —
[Roact.Event.X|]/[Roact.Change.X|](identical
regex shape, just theReact|Roactalternation). - Fusion —
[OnEvent "X|"]/[OnChange "X|"]/[Out "X|"]
(the quoted-string shape Fusion uses). Property choice lists are
pulled from the resolved class the same way the React fast-path
does.
The dynamic computed-key starter snippets (React.Event /
React.Change choice-list entries shown inside an empty [|])
now emit the right shape for the active framework:
Roact.Event / Roact.Change for Roact files,
OnEvent / OnChange / Out for Fusion files. Vide files don't
get any computed-key starters because events are plain prop keys
there (no […] shape exists).
Setting
luix.activeFramework—"auto"(default) /
"react"/"roact"/"fusion"/"vide". Documented in the
Settings UI with the full precedence ladder.
Files changed
src/frameworks.ts (CallShape, recognizedCallShapes, Vide aliases,
partition logic), src/parser.ts (capturing alias, inline-children
on parens), src/activeFramework.ts (new), src/statusBar.ts
(new), src/extension.ts (status bar + picker + workspace
fallback wiring), src/completion.ts (per-framework fast-paths +
active-framework gating), src/elementSnippets.ts (per-framework
gating, 27 new element snippets, 18 new state snippets, 3 new
scaffolds, 3 new event shorthands), src/scaffolds.ts (Roact
template + quickpick entry), src/test/extension.test.ts (fork's
3 Vide-parens tests + updated VIDE_PARTITION), package.json (new
setting, new command, version 1.4.5 → 1.5.0).
Luix v1.4.5 - UDim2 & Completion Fixes
UDim2.fromScale ↔ UDim2.fromOffset conversion that resolves through the parent chain
The existing UDim2 quick-fix can only flip forms when the value is
lossless — UDim2.new(0.5, 0, 0.3, 0) ↔ UDim2.fromScale(0.5, 0.3),
etc. It can't help when you want to materialise
UDim2.fromScale(1, 0.15) as concrete pixels because that requires
knowing the parent's size.
New refactor action: place the cursor on any
UDim2.fromScale(…) / UDim2.fromOffset(…) literal that's the value
of an element's Size = prop, hit Ctrl+. (or click the lightbulb),
and pick "Convert to UDim2.fromOffset(…)" (or fromScale). Luix
walks up the source-order parent element chain, multiplies through
each ancestor's scale until it hits a concrete pixel Size, and
emits the resolved value.
e("Frame", { Size = UDim2.fromOffset(800, 600) }, {
e("Frame", { Size = UDim2.fromScale(0.5, 0.5) }, {
e("Frame", { Size = UDim2.fromScale(1, 0.15) }), -- cursor here
}),
})→ Convert to UDim2.fromOffset(400, 45). The action's title shows
the computed value so you can confirm before accepting.
Recognises reactive :map(...) Sizes when walking the parent
chain — `Size = popupScale:map(function(s) return UDim2.fromOffset(460
- s, 360 _ s) end)
is treated as a 460×360 anchor, not as "non-literal, give up", because the coefficient comes straight out of the innerUDim2.fromOffsetcall. Accepts the three strict shapes for each axis:,_, and_
(so460 _ sands _ 460both work). Anything more elaborate (460 + bonus,clamp(s, 0, 1) _ 460`) skips that
ancestor rather than guessing — same "no fallback" principle as
below.
No invented numbers. When the parent chain can't be resolved
(no parent in source, every ancestor uses fromScale with no
fromOffset / :map(fromOffset(...)) anchor, a mixed-axis
UDim2.new(0.5, 10, …) somewhere in the chain, or a non-literal
Size = props.size), the action stays hidden rather than emitting
a plausible-looking-but-wrong fallback value. A missing lightbulb is
better than silently shifting your layout.
"Calculate Size from children" — the second UDim2 action
Companion action that fires on a parent element whose Size is
UDim2.fromScale(…) (or fromOffset(…)) and computes the implied
pixel size from its children. Handles:
- Children with literal
UDim2.fromOffset(W, H)Sizes — pooled
according to the layout rule (see below). UIListLayout—FillDirection(Vertical / Horizontal) and
thePadding = UDim.new(0, N)gap. Children are summed along the
fill axis, max-pooled across.UIPadding—Padding{Top,Bottom,Left,Right} = UDim.new(0, N)margins added to the pooled total.- Layout decorators (
UICorner,UIStroke,UIGradient,
UIFlexItem,UIScale, theUI*Constraintfamily) are
recognised and skipped — they don't take up content space.
Without a UIListLayout, falls back to the bounding-box rule
(max(child.size) on both axes), suitable for free-positioned
children. The action stays hidden when any contentful child has a
Size we can't reduce to pixels (mixed scale, a variable, a :map(...)
binding without a clean coefficient) — same honest-or-quiet principle
as the chain-walking action.
Component completions now auto-import the require
Accepting a workspace-component suggestion (DailyQuestCard,
StylizedButton, …) from Luix's completion dropdown used to insert
just the identifier, leaving the file broken until the user manually
added local DailyQuestCard = require(…) at the top. The completion
item now carries an additionalTextEdits entry that inserts the
require line — at the same position the existing
luix.autoImport quick-fix would — whenever:
- The component lives in a different file than the current one
(same-file definitions need no import). - A
local <Name> = require(…)for it isn't already present.
The import path respects luix.autoImport.style (relative /
alias) and the luix.autoImport.aliases mapping, identical to the
existing diagnostic-driven quick-fix. The item's detail line shows
the resolved path so you can see what's about to be inserted before
accepting.
Workspace components only surface in files that look like UI code
Typing a single letter in a server script, a pure-logic module, or
anywhere else with no UI in sight used to fuzzy-match every workspace
function starting with that letter (Pro → PopularGamepasses,
ProductRegistry.GetGamepassProduct, …) as if they were Luix
components. Two compounding bugs:
looksLikeUIFilewas too permissive — the "file has at least
one function" check fired on any file, since the parser indexes
every function definition (not just component-shaped ones). A
server-sideProductRegistrymodule with utility functions counts
as "has functions" → falsely flagged as UI. Now relies on the
precise signals only: an actual factory call (e(...),
New "...",create "...",Roact.createElement(...)) or a
require()of a known UI framework. Files without either —
server scripts, DataStore utilities, plain libraries — won't
surface workspace components.knownComponentNamesdidn't filter bylooksLikeComponent—
the workspace index stored every function definition under its
last-segment name, and the completion provider treated all of
them as components. Now filtered to functions that either return
an element call (detectedBaseset) or carry an explicit
@extends ClassNameannotation, matching the rule the sidebar
already uses.
Suppress workspace components inside Luau type X = … declarations
Typing export type Gamepass| to write a type alias used to dump
every workspace component starting with Gamepass into the dropdown
(GamepassCard, GamepassHero, GamepassInfoOverlay, …) because
the direct-call detector treated a bare identifier with no preceding
keyword as a value-position function call. Both the type-name slot
(export type X|) and the RHS (type X = MyComp|) are now
detected as type-position and the workspace-component suggestions
are suppressed. Covers type, export type, and the (uncommon)
local type prefixes.
Rect editor — X / Y now accept negative values
Roblox treats negative ImageRectOffset as panning the visible rect
to the right / down relative to the source texture origin — a
legitimate use case, especially when scroll-zooming out (below 100%)
to position a rect that extends past the top-left of the source. The
editor used to hard-clamp X and Y to [0, maxRect()], so dragging
or arrow-stepping past the left / top edge silently snapped back to
zero. Both axes now use the symmetric range [-maxRect(), maxRect()]
across all four input paths: numeric field, wheel step, arrow step,
and drag + resize handles. W and H stay non-negative (Roblox doesn't
accept negative ImageRectSize).
Luix v1.4.4 - Rename & Polish Hotfix
Workspace-wide component rename (F2)
Pressing F2 on a component identifier now renames the definition
and every call site across the workspace — including the Vide and
Fusion direct-call shapes (MyButton({ … }), MyButton { … }) that
luau-lsp's rename frequently can't see because the call isn't tied to
the function definition through types.
- Conservative gate.
prepareRenameonly accepts identifiers
Luix's workspace index has already classified as a component
(functions that return an element call or carry a Luix annotation).
PressingF2on a random local variable falls back to luau-lsp. - Three reference shapes covered: the
local function MyButton(…)
/local MyButton = function(…)definition, every
e(MyButton, …)/Roact.createElement(MyButton, …)parens-form
call site, and every directMyButton({ … })/MyButton { … }
curried-form call site. - Skipped by design: member-access references like
obj:MyButton(…)andModule.MyButton, and the
require("…/MyButton")path basename — the path is a filename, not
an identifier, and renaming the file is a separate workflow. - Refuses same-name collisions so two definitions don't end up
indexed under one key.
Performance & correctness sweep
A small audit shipped a stack of perf and bug fixes that mostly
matter on big files and large workspaces:
-
Memoised the workspace component-name views
(WorkspaceIndex.knownComponentNames/knownDirectCallTargets) —
previously rebuilt by walking every indexed file every time a
provider asked for them, and 4+ providers ask per keystroke. Now
cached and invalidated only on real cache mutations / relevant
config changes. -
Memoised
getAliasPartitionandgetEnabledFrameworks— same
problem at a smaller scale (~34 call sites, 3–5× per completion
invocation). Cache busts onluix.frameworks, the per-framework
*.aliaseskeys, andluix.vide.directInstanceCalls. -
Capped the backward brace-walk in
findEnclosingPropsCallat
16 KB. With the cursor outside any table in a 50K-line file, the
unbounded walk used to scan the full document on every keystroke for
every provider that calls it. Also cached the compiledRegExp
objects per alias-key (previously only the alias alternation
string was cached; theRegExpwas allocated fresh each call). -
Faster
extractPropEntriesfor diagnostics / hover previews /
rect / gradient. AddedextractPropEntriesFromDocument(text, start, end)that reuses the document-level mask cache — the
per-substring path always missed the cache, causing ~400+ redundant
mask builds per diagnostic recompute on a busy file. -
Replaced the per-match brace-walk in deprecation diagnostics
with interval-containment against a single precomputed list of
props-table ranges. Was O(matches × document), now O(N + matches ·
log C). -
Deduped repeated work in
ReactLuauPropsCompletionProvider— the[React.Event.…]
fast-path used to re-detect the enclosing call and re-fetch
getAliasPartition+knownDirectCallTargetsafter falling
through to the normal prop path. One setup pass now, reused. -
scanCachelength-pre-check + size bump (4 → 8 entries). On
big files the value-equality string compare was the dominant cache
check cost; length and alias-key are checked first so misses now
short-circuit in O(1). -
CodeLens references — synchronous count + lazy peek.
ComponentReferencesLensProviderused to open every workspace file
with a hit for every component, every refresh. Now it computes a
synchronous count viaWorkspaceIndex.countCallSites, and only
fetches the actual locations when the user clicks the lens
(luix.peekComponentReferencescommand). Also honours the
CancellationTokenVS Code passes in. -
Fixed
sortProps.onSavesilently breaking on nested elements.
The previous implementation emitted one TextEdit per call's props
body, and nested calls (the textbook UI shape — `Frame > TextLabelUIPadding
) produced overlapping ranges that VS Code rejects, aborting the whole save formatter. Now emits one edit per outer-most call with the nested sorts spliced in viasortBodyRecursive`. -
Fixed API-dump cache invalidation. Enabling
luix.useRobloxApiDumpand merging new props for e.g.Frameused
to leaveScrollingFrame/TextLabel/ etc. with their stale
pre-merge flattened prop lists, so descendants never saw the new
props until reload. The merge now writes to the source hierarchy
and rebuilds the derived caches. -
Cleared
WorkspaceIndex._persistTimerindispose(). A
timer armed within 5 s of window close held a closure on the cache
and wrote to disk after the extension shut down. One-line fix. -
Plugged
imageGutterdouble-dispose. Decoration types were
being tracked in both the disposables list and
typesByAsset;clearAllDecorationsdisposed via the map, then
dispose()later double-disposed via the list. Now tracked only in
the map. -
Expanded
DiagnosticsManager's config-change watch list to
includeframeworks, the per-framework*.aliaseskeys, and
vide.directInstanceCalls. Enabling Vide at runtime now refreshes
diagnostics on every open file instead of leaving them stale until
the next keystroke. -
Luixoutput channel — wired into the asset-thumbnail fetch /
CDN failure paths so background failures stop being silent. Open
via Output → Luix. -
Bug fix: workspace-component completions inside a props table.
Typinge("Frame", { Name = "Test", eTextButt|used to surface
eTextButtonas a component suggestion even though the cursor was
at a prop-key slot. Now suppressed when the cursor is at a key
position inside a props table, except for Vide (which allows inline
child expressions in that position). -
Bug fix: workspace-component completions inside string literals.
TypingText = "WEEKLY m|"used to surface workspace components
whose names start withm(Minimap,MoneyDisplay, …) even
though the cursor was inside a string. Now suppressed by checking
the code mask. Also patched the opt-in
FactoryOpenParenCompletionProvideragainst the same edge case
("some(inside a string used to look like a factory call to the
walker). -
Bug fix: all Luix snippets used to fire inside string literals.
Static VS Code snippets fuzzy-match against any character in their
prefix — typing a singlerinsideText = "Resets in 00:12:34 r"
surfacedreactEvent,rfc, anduseRef; typingeFraat a
prop-key slot inside a props table offered to expand into a full
e("Frame", { ... })call. Every Luix snippet —
element constructors (eFrame,nFrame,cFrame,eTextLabel,
…), hooks (useState,useEffect,useRef,useMemo,
useCallback),rfc,reactEvent,cfangles,cfanglesrad—
is now served byElementSnippetCompletionProviderand gated:- Code-mask check suppresses every snippet inside string
literals. - Prop-key-position check suppresses element snippets at key
slots, except when the parent framework allows inline child
expressions (Vide). Hooks / scaffold /reactEvent/
cfangles*skip this gate because they don't collide with
plausible prop-name typing. - Enabled-frameworks filter — Fusion-only projects no longer
seeeFrame/cFrame, Vide-only projects don't seeeFrame/
nFrame, etc. React/Roact-style patterns (hooks,rfc,
reactEvent) only surface when React or Roact is enabled.
Framework-agnostic value expressions (cfangles*) are
unrestricted.
snippets/luix.code-snippetsis now empty (kept as a placeholder
for the manifest'scontributes.snippetsregistration). - Code-mask check suppresses every snippet inside string
-
Bug fix: completions inside
[…]computed-key brackets.
Typing[Reac|(to write[React.Event.Activated]by hand)
surfaced workspace components likeReactCharm/
ReactErrorBoundary/ReactRobloxbecause
isAtPropKeyPositionwalked back through[and]
indiscriminately and the providers thought the cursor was at a
fresh key slot. Worse, typing[React.|then surfaced Frame's
Archivable/Name/AutoLocalize/ etc. as if it were the
start of a prop name in the outer table. Added a new
isInsideComputedKeyhelper and gated three providers on it:ReactLuauPropsCompletionProvider— bails after the
[React.Event.X|]/[React.Change.X|]fast-path (so those
still fire correctly) so the general prop-name path doesn't
pollute the dropdown.FactoryComponentCompletionProvider— no workspace components
inside[…].ElementSnippetCompletionProvider— noeFrame/reactEvent
/ etc. inside[…](thereactEventsnippet expands to a
[React.Event.…] = function() … endentry, so triggering it
inside an existing[…]would have produced nested brackets).
Also added two dynamic computed-key starter snippets that fire
only inside[…]and only when React or Roact is enabled:React.Event— expands to
React.Event.${1|<every event on the resolved class>|}. The
choice list is built fromflattenClassEventson the element
Luix detects you're inside (e.g. aTextButtongetsActivated,
MouseButton1Click,MouseEnter, …; aFramegets the
GuiObjectevent set). Falls back toGuiObjectwhen the class
can't be resolved (custom component without an@extends).React.Change— same shape, body is
React.Change.${1|<every property on the resolved class>|},
so any prop you could listen to is one tab away.
Useful when luau-lsp isn't surfacing the
Reactimport (it can
miss it for various scope / c...
Luix v1.4.3 - Vide Hotfix
Size / Position completion now hands you the constructor picker
Accepting a UDim2-typed prop (Size, Position, CanvasSize,
CellSize, TileSize, PageSize, CellPadding) used to insert the
full Size = UDim2.new(0, 0, 0, 0), template with tab stops in each
channel — fine if you wanted .new, annoying if you wanted
.fromScale or .fromOffset (which are at least as common in modern
Vide / React-Luau code), because you had to delete and retype.
Now UDim2 follows the same pattern as Color3, UDim, and Font:
Luix inserts Size = UDim2. and immediately opens the suggest
dropdown, so you can pick .new / .fromScale / .fromOffset /
.fromAxis (or any spacing token you've defined under
luix.spacing). Picking a constructor still inserts its full
per-channel snippet, so the original Tab-through-each-value workflow
is preserved for the cases where you do want .new.
Direct instance-call detection — Frame({...}) etc. for Vide
Vide lets you construct built-in Roblox UI instances by calling the
class name directly — Frame({ Size = … }),
TextButton({ Activated = … }), ScrollingFrame { CanvasSize = … } —
instead of going through create "Frame" { … }. Luix now recognises
these as instance-creation sites and surfaces the matching class's
prop + event completions inside the table.
luix.vide.directInstanceCalls(defaulttrue) — opt-out
setting in case you have local variables named after Roblox UI
classes that you call with a table for unrelated reasons. Only
applies when Vide is inluix.frameworks— React-only / Roact-only
projects are unaffected.- Curated allowlist. Re-uses Luix's existing class hierarchy
(Frame, ScrollingFrame, TextLabel, TextButton, ImageLabel,
ImageButton, ScreenGui, BillboardGui, everyUI*constraint /
layout / decorator), minus the abstract bases (Instance,
GuiObject,GuiButton, …). Non-UI Roblox class names (Camera,
Sound,Tween,Workspace, …) are deliberately absent — Luix
doesn't model them, and they're common local-variable names. - Events get merged just like
create "Frame" {…}. Direct
instance calls are attributed to Vide downstream, soActivated,
MouseEnter, etc. surface as suggestions on instance classes that
have them — matching the canonical curried form. - Workspace components shadow built-in class names. If you've
defined your ownlocal function Frame(props)somewhere, that
component's declared props win over Roblox'sFrame.
Direct component-call detection for Vide / Fusion
Custom Vide and Fusion components are typically invoked directly with
a props table — StylizedButton({ Theme = "Green", Text = "..." }) or
the curried StylizedButton { Theme = "Green", ... } — rather than
wrapped in a factory call. Luix's parser previously only recognised
the alias-prefixed forms (e(Comp, { … }), create "Frame" { … },
New "Frame" { … }), so prop completions, hover docs, anchor presets,
and prop-validation diagnostics all silently bailed inside the props
table of a direct component call. luau-lsp's word-based suggestions
then filled the gap with unrelated identifiers (TextLabel,
TextService, etc.).
findEnclosingPropsCallnow accepts adirectComponentsset.
When the cursor is in a{ … }table preceded by<Identifier>(
or<Identifier>and the identifier appears in that set, the call
is treated as a direct component call. The result carries a new
isDirectComponentCall: trueflag so callers can branch (e.g.
skip merging built-in-instance events).WorkspaceIndex.knownComponentNames()exposes a synchronous
snapshot of every component the index has seen, which is what the
completion / hover / anchor-preset / diagnostics paths now pass in.
The set is the entire safety gate — without it the curried regex
would match everyf { … }table-call in the language.- Method calls and qualified accesses stay quiet.
obj:Card({ … })andMod.Card({ … })are deliberately excluded
via the leading char-class so non-UI code can't accidentally
trigger prop completions. - Framework-mediated calls take precedence. The existing
parens / curried alias paths run first; the direct-call detection is
a strict fallback. Mixing styles in one file (e("Frame", { … StylizedButton({ … }) … })) keeps each call's framework attribution
intact.
9 unit tests added (Direct component-call detection suite) covering
the happy path, the safety cases (unknown identifier, no-set
back-compat, method calls, qualified accesses), and the
mixed-framework nesting case.
Luix v1.4.2 - Thumbnail/Gutter Hotfix
Asset thumbnail hover / gutter — false "moderated" after edits
Changing an rbxassetid://… value (or hovering one Roblox has just
started thumbnailing) often showed "Thumbnail unavailable (asset may
be moderated, deleted, or the API is unreachable)" for a full minute,
even when the asset was fine — only a window reload would clear it.
- Stop caching transient API states. Roblox's thumbnails API
returnsPending/InReviewwhile it's still generating the
image (typical for freshly-referenced assets), and
Error/TemporarilyUnavailableduring backend hiccups. The
previous implementation only treatedCompletedas success and
cached every other state — transient or not — as a failure for
60 seconds. Now transient states bypass the cache entirely so the
next hover or gutter refresh retries against the (usually
now-Completed) response. - 10 s settled-failure TTL (was 60 s). Even when a fetch
genuinely fails — typo'd ID, network blip — fixing it gets a fresh
fetch on the next interaction instead of stranding the user behind
a minute-long cache. - State-aware hover message. Instead of always saying "asset
may be moderated, deleted, or the API is unreachable", the hover
now reflects the actual API state: "Roblox is still generating
the thumbnail — hover again in a few seconds" for Pending /
InReview, "temporarily unavailable" for Error /
TemporarilyUnavailable, "asset has been moderated or removed"
for Blocked / Moderated.
Luix v1.4.1 - Wally Hotfix
Regenerate Wally types — Windows PowerShell + Defender fix
The Regenerate Wally types button (and the
luix.wally.regenerateTypes command) chained its three steps with
&&, which Windows PowerShell 5.1 — the default shell on Windows —
rejects as an invalid statement separator, so the command failed
before wally install even started.
- Shell-aware chaining. On Windows PowerShell 5.1 the chain
is now emitted ascmd1; if ($?) { cmd2; … }. PowerShell 7+,
cmd.exe, and POSIX shells continue to use&&. - 300 → 1500 ms breather between
wally installand
rojo sourcemap. Windows Defender briefly locks the freshly
writtenPackages/*.lualink files for real-time scanning. Without
a pause, rojo's sourcemap silently misses them and
wally-package-typesthen reportsLinker node 'Packages/X.lua' not found in sourcemapfor every top-level package. The delay is
emitted in the active shell's syntax (Start-Sleep/
timeout /nobreak/sleep).
Luix v1.4.0 - The Visuals Update
This release adds three full-fledged visual editors (gradient,
sprite-rect, plus four hover previews), a new diagnostic, and a
sort-props action. Editors are marked Preview in the UI so
users know they're still being polished.
Gradient editor (luix.gradient.*)
A single Edit UIGradient CodeLens above every
e("UIGradient", { … }) element opens a combined side-panel editor
with Color, Transparency, and Rotation. Standalone
ColorSequence.new(...) and NumberSequence.new(...) literals (not
inside a UIGradient) get a focused per-literal editor instead.
- Colour ramp — drag triangle stops, click the strip to add,
per-stop colour picker, hover-indicator pill showing the offset. - Transparency curve — grid canvas with draggable circle stops
(X = time, Y = value), envelope shading when non-zero. - Rotation slider — −180° to 180°, slider + numeric input.
- Preview square — combines colour + transparency + rotation
and multiplies by the parent element'sBackgroundColor3
(Roblox'sUIGradientsemantics), so what you see matches what
Roblox renders. - Output respects
luix.color3.defaultFormatforfromRGB/
fromHex/new. DefaultColor = ColorSequence.new(white),
Transparency = NumberSequence.new(0), andRotation = 0are
omitted from the written-back props block — no noise. - Hover previews —
ColorSequenceshows the gradient strip,
NumberSequenceshows the value curve. Toggles:
luix.gradient.codeLensEnabled(on),
luix.gradient.previewOnHover(on). - Polish —
Shiftsnaps drag/click to 0.05, hover indicator
on strip and curve, scroll-wheel + arrow stepping on numeric
fields, decimal input always renders.regardless of locale,
blur-revert on invalid input.
Rect editor (luix.rectEditor.*)
An Edit sprite rect CodeLens appears above every
e("ImageLabel" | "ImageButton", { … }) whose Image prop is a
literal rbxassetid://…. Opens a side-panel editor:
- Thumbnail fetched from
thumbnails.roblox.comat the largest
available size, with a fallback ladder (768 → 512 → 420 → 256 → 150). - Draggable rectangle with 8 resize handles; dimmed mask shows
what gets cropped. - X / Y / W / H number fields with
Shift= 10× step. - Aspect-ratio auto-detect — reads a sibling
UIAspectRatioConstraintor a fixed-pixelSize = UDim2.fromOffset(…)
and pre-fills the Frame aspect input. - Crop preview overlay — a dashed yellow box inside the
selection showing exactly whatScaleType.Cropwill actually
render given the frame aspect. Hidden for other ScaleTypes. - Native dimension auto-detect via Open Cloud — when
luix.openCloud.apiKeyis set (key needs thelegacy-asset:manage
permission), the editor hits
apis.roblox.com/asset-delivery-api/v1/assetId/{id}, downloads the
returned CDN location, and reads the PNG/JPEG header to extract the
true pixel dimensions. One call per asset, ever — results are
persisted toglobalState. - Source dimensions fallback — without an API key (or if the
lookup fails) the editor uses the thumbnail's natural size and lets
you typeSource W/Source Hmanually. Manual values are also
cached per asset, so the typing cost is one-time. - Scroll-to-zoom — wheel on the canvas zooms (15% – 300%),
with a discoverable 🔍 zoom bar (− / % / +) in the corner. - Rect can exceed source dimensions — W and H accept up to 4×
source; the rect can be dragged past the image edge. Overflow is
signalled with a dashed amber border. - Stripped on the "full image" default —
ImageRectOffset = (0,0)
andImageRectSize = (0,0)are omitted on Apply.
Unused-prop diagnostic (luix.unusedProps.enabled, on by default)
Props declared in a component's parameter type
(props: { Foo: …, Bar: … }) or its @luix-props annotation that
are never read in the body are flagged with the unused-declaration
grey-out style (Hint severity, Unnecessary tag — same treatment
TypeScript uses for unused locals).
Skipped automatically when the body forwards props wholesale
(e(Base, props), for k, v in props do, computed-key indexing)
since static analysis can't determine downstream usage. Squiggle
lands on the field name inside the type annotation when possible,
otherwise on the function definition line.
Visual hover previews (luix.hoverPreviews.enabled, on by default)
Inline SVG previews rendered straight from your literal values —
no network requests, no caching:
TweenInfo.new(...)— 240×140 graph of the easing curve.
All 12EasingStyle× 4EasingDirectioncombinations are
implemented as math functions. Below the curve: duration,
repeat-count, reverses, and delay summary.e("UIPadding", { … })— box visualisation with the inner
content area indented by the configuredPaddingTop/Right/
Bottom/Left. Each non-zero side gets its pixel value labeled.e("UICorner", { … })— rounded rectangle rendered at the
configuredCornerRadius.e("UIStroke", { … })— sample box with the stroke applied
at the configuredThickness/Color/Transparency.
Sort props by category (luix.sortProps.*)
- Code action — Right-click anywhere inside a props table → 💡
Sort props by category. Reorders props by category (then by
canonical order within each category, stable on ties). - Format-on-save —
luix.sortProps.onSave(defaultfalse).
When enabled, every props table in the document is sorted on
save. Off by default so saving a teammate's file doesn't
reshape their layout. - Configurable order —
luix.sortProps.categoryOrder(string
array) lets you reorder or remove categories. Defaults:
Identification → Layout → Style → Visibility → Image → Text →
Behavior → Events → Refs → Children → Other. - Computed-key aware — captures
[React.Event.Activated],
[OnEvent "…"],[Children], etc. Vide-style plain identifier
events (Activated = function() … end) are recognised too. - Comment-safe — tables containing
--comments are skipped
so comments never get detached. Idempotent: re-sorting a sorted
table is a no-op (no spurious save edits).
Per-class prop type overrides
Some Roblox prop names mean different things on different classes
(e.g. Frame.Style → Enum.FrameStyle, but
GuiButton.Style → Enum.ButtonStyle). 1.3.0's global PROP_TYPES
map could only hold one value per prop name and silently picked
the wrong enum on Frame.
New PROP_TYPE_OVERRIDES map keyed by class, plus a
getPropType(className, propName) helper that walks the class
hierarchy. The hover tooltip, completion value-snippet, and the
wrong-enum diagnostic now all resolve Style (and any future
conflicts) per-class. Adding more is a one-line entry per class.
Wrap-in code action — fixes (regressions from 1.3.0)
- Multi-element selection — selecting
UIPadding + TextLabel
now wraps those two siblings in a new container, instead of
wrapping their parentFrame(the previous behaviour found the
smallest call containing the selection, which is always the
parent for multi-element selections). - Indentation — wrapped lines no longer get double-indented.
The previousindentLines(text, baseIndent + step)prepended
both the base AND the new step to lines that already had their
original indent baked in, producing 4-tab-deepName = …etc.
Expanded class & prop catalogue (src/data.ts)
Built-in classHierarchy and PROP_TYPES significantly expanded
to track newer Roblox additions and previously-missing props:
- GuiBase2d —
RootLocalizationTable,SelectionBehaviorDown/Left/Right/Up,SelectionGroup,SelectionChangedevent. - GuiObject —
InputSink,NextSelection*,SelectionImageObject. - GuiButton —
HoverHapticEffect,PressHapticEffect,Style. - Frame —
Style. - VideoFrame —
MaximumResolution,RollOffMaxDistance/MinDistance/Mode. - TextLabel / TextButton —
OpenTypeFeatures. - TextBox — re-rooted under
GuiObject(wasTextLabel) to
match Roblox's actual hierarchy; full text-prop set mirrored. - BillboardGui —
Adornee,ResetOnSpawn,TabKeyboardNavigation. - SurfaceGui —
Active,TabKeyboardNavigation. - ScreenGui —
TabKeyboardNavigation. - UIStroke —
BorderOffset,BorderStrokePosition,StrokeSizingMode,ZIndex. - Instance —
Archivable. - And 40+ new
PROP_TYPESentries for the above, covering
Rect,Camera,Player,LocalizationTable, plus several
new enum types (BorderStrokePosition,HapticEffect,
InputSink,NormalId,RollOffMode,SelectionBehavior,
StrokeSizingMode,VideoSampleSize).
Luix v1.3.0 - The Tokens & Fonts Update
New diagnostics (gated by luix.propValidation.enabled, on by default)
- Numeric-range warnings —
Transparency = 1.5,Rotation = 720,
BorderSizePixel = 100, etc. Per-prop bounds. - TextScaled gotcha —
TextScaled = truewith a pure-scaleSize
(or noSize) collapses text to zero — now flagged with a fix
recommendation.
Color contrast warnings (off by default)
luix.contrastWarnings.enabled — flags any TextColor3 whose
WCAG-AA contrast ratio against the nearest ancestor's
BackgroundColor3 falls below 4.5:1. Both colors must be literal
Color3 expressions; reactive Fusion/Vide values are skipped.
Design tokens beyond color
Mirror of luix.palette for two more types:
luix.spacing— typeUDim.to surface entries like
spacing.md→UDim.new(0, 16).luix.fonts— typeFont.to surface entries like
fonts.display→Font.fromName("Gotham", Enum.FontWeight.Bold).
Empty by default; opt-in via user config.
Color3 → palette extractor
Cursor on any Color3 literal → 💡 Save Color3 to luix.palette… —
prompts for a token name and a target (User / Workspace settings).
The literal is added to luix.palette so it surfaces in future
Color3. completions.
Frame-stats CodeLens (off by default)
luix.frameStatsLens.enabled — ▸ Frame — 24 descendants, 4 layers deep above every meaty subtree. luix.frameStatsLens.minDescendants
(default 5) controls the threshold.
Workspace-wide validation summary (off by default)
luix.workspaceValidation.enabled — the Luix sidebar shows
"Project diagnostics — N warnings · M errors across X files".
Click jumps to the Problems panel. Aggregates Luix + every other
publisher's diagnostics.
Class picker on ( (off by default)
luix.classNameCompletion.triggerOnOpenParen — typing e( (without
a quote) opens the class picker; accepting inserts the full
"ClassName", { … }) body. Off by default because ( is a broad
trigger; on saves one keystroke per element when enabled.
CFrame.Angles snippets
cfangles → CFrame.Angles(math.rad(…), math.rad(…), math.rad(…))
cfanglesrad → CFrame.Angles(…, …, …) (radians form)
Background — workspace index persistence (on by default)
luix.indexPersistence.enabled — the parsed component index is now
saved to disk between sessions; unchanged files skip re-parsing on
cold start. Speeds up activation on large workspaces with no
behavioral difference. Disable to keep Luix offline.
Background — opt-in Roblox API-dump augmentation
luix.useRobloxApiDump — fetch the community-maintained
Mini-API-Dump JSON once a day and add any new properties Roblox has
shipped to existing classes' completion lists. Additive only — the
hand-curated built-in data still wins on conflicts.
Roblox font family + weight autocomplete
- Inside
Font.fromName("…"), the family dropdown surfaces 36
built-in Roblox families with their supported weights. Popular UI
families (BuilderSans, Gotham, Roboto, SourceSansPro, …) sort first. - After
Enum.FontWeight.inside aFont.fromName(...)call, the
weight dropdown shows ONLY the weights the family actually ships.
Font.fromName("Cartoon", Enum.FontWeight.|)lists justRegular;
Font.fromName("Roboto", Enum.FontWeight.|)lists all nine. luix.customFontslets you register custom font families and
their supported weights — they merge into the same completions,
surface above built-ins, and weight-filter the same way.
Font (deprecated) removed from TextLabel / TextButton / TextBox
These classes no longer suggest the deprecated Font property —
only FontFace shows up in the completion list. The existing
deprecation diagnostic still catches anyone who writes
Font = Enum.Font.X and offers the Replace with FontFace = …
quick-fix.
Smarter value completion for Color3 / UDim / Font props
Accepting BackgroundColor3 / TextColor3 / Padding / FontFace
now inserts a Color3. / UDim. / Font. prefix and auto-opens
the suggest dropdown. The dropdown lists:
- Built-in constructors first —
fromRGB,fromHex,new,
fromHSV(for Color3);new(for UDim);fromName,fromId(for
Font). Picking one inserts the full call with per-channel tab stops
preserved. - Tokens defined in your settings below —
palette.primary,
spacing.md,fonts.display, … swap the prefix for the literal
expression.
Type f to filter to constructors; type p (or s / fonts) to
filter to tokens.
RichText <font> / <stroke> / <mark> — no more presumptuous defaults
Accepting one of these tags now leaves the attribute slot empty and
parks the cursor inside the tag. Type any letter and the attribute
completion fires — pick color, size, weight, etc. and the
value-with-quote pair fills in. Previously the snippet always
pre-filled color="…" regardless of intent.
Other improvements
- Prop completion no longer doubles trailing commas: typing
Bac,
then acceptingBackgroundColor3produces a single trailing comma
with the cursor after it, regardless of the existing,. - Prop / anchor-preset completions are now gated to key position —
typingFontFace = Font.|no longer surfacesBackgroundColor3/
anchor:c/ etc. alongside the font constructors. - All settings now live under the
luix.*prefix. The silent
fallback to legacyreactLuauPropsHelper.*keys has been removed
because VS Code'sinspect()could mistake a user-set{}(equal
to the default) for "not set" and quietly pick up the old keys.
Anyone with stalereactLuauPropsHelper.*entries needs to rename
them toluix.*(the format is identical). - All user-visible strings, diagnostic codes, and the diagnostic
collection have been re-branded fromreact-luau-props-helperto
luixso the Problems panel and hover tooltips show the right
source. .vscodeignoreextended to excludedevforum-post/, design SVG /
PNG drafts,PUBLISHING.md, lockfile, and tooling configs.
Marketplace package size stays under ~135 KB.