Repository navigation
Unity Roslyn REPL v0.8.0
[0.8.0] - 2026-05-19
Added (Toolbar _ carry-over badge)
- New toolbar pill
_ : Player (GameObject)surfaces the current_carry-over target so users can see at a glance whether the engine has a live value bound to_(the static the wrapper exposes inside snippets and Watch expressions). The badge appears wheneverReplEngine.LastResultis non-null and hides when it's null — Run that returns a non-null value, Object Browser inspect, and explicitReplEngine.SetLastResultall light it up; an explicit Clear orReset Project Dataextinguishes it. Closes #59. - Clicking the badge re-inspects the live
_value: clears Output and renders the sameSimpleObjectSerializer.ToTreeshape Browse-inspect emits, so the user can fold the tree open and read the object's fields without typingreturn _;themselves. The summary chip readsInspect _while in this mode so it's distinguishable from a Run or Browse landing. The badge's hover tooltip prints a concrete usage example (return _;/_.someField) so a newer user who's never seen the wrapper static doesn't have to guess what "available" means. - A small ✕ sits inside the same pill and drops the carry-over via
ReplEngine.ResetLastResult(). The clear routes through the engine (not directly at the badge) so every subscriber — this UI, future panels, the engine's own follow-up paths — sees the transition through one event. - Object Browser inspect now emits an inline **
→ available as \_` in Code and Watch. Try: return ;** info line right above the rendered tree. Without it users had to notice the toolbar badge separately to realise their next snippet / watch could reach the value through`; the inline mention plus a concrete invocation removes both the "available?" and the "available where?" ambiguity from the same glance as the inspection itself.
Changed (ReplEngine carry-over plumbing)
ReplEngine.LastResultmutations now route through a single privateAssignLastResultchokepoint (SetLastResult,ResetLastResult, and the in-Execute success path all funnel through it). The new publicLastResultChangedevent fires from there with aforceflag so explicit assignment (Set / successful Execute) notifies subscribers even when the new reference equals the current one — same-reference reassigns happen naturally when a snippet mutates and returns the same object (_.name = "Hero"; return _;) and the badge would otherwise freeze on the prior name. Reset keeps theReferenceEqualsshort-circuit so a no-opResetLastResult()on an already-null engine doesn't churn the UI.- Subscribers are invoked through
GetInvocationList()with a per-handlertry / catch. A misbehaving UI surface that throws fromLastResultChanged(a torn-down panel, a stale VisualElement) can no longer propagate up throughExecute's outer catch and convert a successful snippet into aReplResult.RuntimeError. The carry-over field still updates and remaining subscribers still fire; the failure surfaces as a singleDebug.LogWarningso the diagnostic isn't silent.
Added (Object Browser row context menu)
-
Object Browser rows now expose an explicit right-click menu — Inspect, Set as
_, Patch Method…, Copy Type Name, Copy Inspect Snippet — so the panel reads as a set of action entry points instead of a list whose only contract is "double-click does something depending on mode." Closes #60. -
Inspect mirrors the Output-mode double-click flow: clears Output, renders
SimpleObjectSerializer.ToTree, binds_to the value, refreshes Watch. Patch Method… mirrors the Patches-mode double-click flow (method picker for the row's runtime type, falls back toDeclaredTypewhen the live value is null, e.g. singleton accessors). Both routes share the same helpers double-click drives, so the menu can't drift away from the existing behaviour. -
Set as
_binds the value without rewriting Output — sameReplEngine.SetLastResult(...)the inspect path uses, just without the tree render. The toolbar_badge picks the new value up throughLastResultChanged, and the action also fires_watch.Refresh()at the same commit point Inspect uses so a Watch panel holding_.fooexpressions immediately re-evaluates against the new target instead of showing stale data from the previous_. The pane summary chip readsBound \_`rather thanBrowsed`. -
Copy Type Name renders the runtime type through
CSharpTypeName.Renderso the clipboard receives valid C# source (Outer.Inner,Dictionary<int, string>) rather than the reflection form (Outer+Inner, generic backticks, assembly-qualified arguments). Falls back to the displayTypeNamewhen the type isn't expressible as source (open generic parameters); paste-and-Run scenarios get something useful when the type permits and never get silently-broken syntax. Copy Inspect Snippet writes a small C# template into the clipboard, shaped by the row's category:- MonoBehaviour → resolves by InstanceID so the snippet returns the exact row the user right-clicked. Live rows emit
return UnityEditor.EditorUtility.InstanceIDToObject({id}) as T;; rows with no live value (surfaced throughDeclaredTypealone) fall back toFindFirstObjectByType<T>(). The previous first-match form picked whichever component the scene-wide query found first, missed inactive GameObjects entirely, and never reached prefab-asset components — InstanceID covers all three. The ID is Editor-session-scoped (a domain reload / Play Mode transition rotates them); right-click again for a fresh snippet after either event. - ScriptableObject → resolves the specific asset by path, not by type-wide search. Main-asset rows get
LoadAssetAtPath<T>("Assets/.../Foo.asset"); sub-asset rows getLoadAllAssetsAtPath+.OfType<T>().FirstOrDefault(a => a.name == "...")so they don't collide with the path's main asset. The previousFindAssets("t:T").FirstOrDefault()shape would land on whichever asset came first in the project — wrong row whenever multiple SO instances of the same type existed. In-memory ScriptableObjects (created viaCreateInstance, no asset path) fall back to the type-wide template with a comment flagging that the template may not pick the same row. - Singleton → comment-only template with a TODO: the locator doesn't carry the accessor name (
.Instancevs.Ivs a property is project-specific) so we don't guess.
Both copy actions echo a one-line confirmation into Output so the click doesn't feel like a no-op.
- MonoBehaviour → resolves by InstanceID so the snippet returns the exact row the user right-clicked. Live rows emit
-
Menu entries are gated by liveness / renderability so the surface never promises an action it can't deliver. Inspect / Set as
_goDisabledwhen the row's value resolved to null or a destroyedUnityEngine.Object. Patch Method stays enabled when the row has aDeclaredTypeeven if the live value is gone (matching the singleton fallback). Copy Inspect Snippet goesDisabledwhen the row's type can't be rendered as C# source (open generic / generic parameter / no type at all) — sameCSharpTypeName.IsRenderablecheck the snippet builder uses, so a successful click always produces source the user can paste and Run. -
Double-click behaviour is unchanged.
ObjectBrowserViewexposes a parallelOnRowActionevent in addition to the legacyOnInstanceChosen; host wires both, and the action handlers all reuse the sameRenderBrowserInspect/OpenBrowserPatchMethodhelpers the double-click path calls. A pooledContextualMenuManipulatorattached once inMakeRowwalks up from the click target'suserDatato find the boundInstanceEntry, so the manipulator allocates the action lambdas at creation rather than per scroll-bind.
Added (Output tree node context menu)
- Output tree rows now expose a right-click menu — Add Watch, Copy Path, Copy Value, Inspect This, Set as
_— so the rendered tree reads as a probe surface instead of a static dump. Right-click any field, list element, or dictionary entry and the menu acts on that exact node. Closes #61. - Add Watch pipes the node's expression path into
WatchStore.Add(which dedupes + firesChanged, picked up by the Watch panel automatically). Copy Path writes the same expression to the system clipboard so users can paste it into the Code editor, a Watch row, or external notes. Copy Value copies the row's 1-linePreviewtext (works even on truncation / error placeholders so it's never a dead-end). Inspect This clears Output and renders a freshSimpleObjectSerializer.ToTreeagainst the node's value — same shape Browse-inspect and the_badge re-inspect emit, including the→ available as \` in Code and Watch. Try: return ;hint and thecarry-over rebind. **Set as** is Inspect-without-the-tree: rebindsto the node's value and refreshes Watch at the same commit point so.foo` rows track the new target, leaving the existing Output intact. - Menu entries are gated so the surface never promises an action it can't deliver. Add Watch / Copy Path require a safe expression path and a fresh root anchor — path-based actions disable themselves and pick up a
" (stale — \` rebound)"label suffix wheneverReplEngine.LastResultno longer points at the tree's root, because the same.foo.barpath would otherwise silently resolve against a different object after the user reboundvia a later **Set as** / Object Browser action / toolbar ✕. Two un-stick paths: **Set as** on the (result) row re-roots the *existing* tree (the engine'smatchesRootValueagain, so the menu drops the stale labels and Add Watch / Copy Path light back up), or **Inspect This** on any inner row rebuilds a fresh tree rooted at that node. **Set as** on an inner row deliberately *doesn't* clear the stale state — it rebindsbut leaves the previous tree visible with its oldRootValue, so those rows stay stale-disabled; the user wanting paths anchored on the newbuilds a new tree via Inspect This. **Inspect This** / **Set as** stay enabled across rebinds because they use the capturednode.Valuedirectly. They're also gated on liveness (disabled for null entries and destroyedUnityEngine.Objectrows); **Copy Value** stays enabled whereverPreview` is populated. - New
ReplValueNode.Value,ReplValueNode.ExpressionPath, andReplValueNode.RootValuefields carry the per-node payload fromSimpleObjectSerializer.ToTreeto the menu.RootValueis stamped on every node in the tree (including placeholder / error nodes) so the stale-check has a stable identity reference even on rows that don't have their own usable Value.ToTreegained an optionalrootPathparameter (defaults to"_", matching the carry-over the UI flows always rebind on the same beat);WatchEvaluator.BuildTreeoverrides it with(<userExpression>)so a Watch sub-tree's paths grow from the row the user actually typed rather than a stale_. Field / property paths accumulate asparent.nameonly when the name is a path-safe C# identifier under the same grammar Watch's fallback parser reads (letter / underscore start, letter / digit / underscore continue); compiler-generated fields on anonymous, state-machine, and display types (<A>i__Field,<>1__state, etc.) are reflection-visible becauseIncludeNonPublicdefaults to true, but their names start with<so they get a null child path — the row still renders so the user can read the value, Add Watch / Copy Path just disable. Enumerable indices accumulate asparent[idx]. Dictionary keys are narrowed to exactly what the Watch fallback parser (WatchEvaluator.TryReadIndex) can re-parse — non-negativeintdigits andstringkeys with no control chars, embedded", or\. Bool / char / single-value enum / negative-int / long / unsigned / float / decimal / escaped string keys all reject (ExpressionPath = null); the normal compile path supports more, but the encoder only emits forms both code paths can resolve so Add Watch never lights up on a row that would silently drift the moment Watch falls back. - A pooled
ContextualMenuManipulatorattached once per cell inMakeColumn.makeCellreads the boundReplValueNodeoffuserData(set inbindCellon every recycle) and walks up from the click target'sevt.targetto find it. Same allocation-light shape the Object Browser menu uses; right-clicking any of the three columns (Name / Type / Value) produces the same menu against the same row, so the user doesn't have to remember which cell is the "row handle."
Added (Per-row enable / disable for Watch)
- Each Watch row now leads with a small checkbox so the user can silence individual rows without deleting them. The header gains ✓ All / ✗ All bulk buttons for the obvious "turn everything off during this Run" / "turn everything back on" affordances. Closes #62.
- Disabled rows stay visible in the panel but render muted (italic, 50% opacity across the expression / value / type cells and any "Resolved from" line) with a fixed
(disabled)preview so the row's existence — and its persisted expression — is still obvious at a glance. The expand chevron disables on those rows since there's no tree to walk until the next refresh re-evaluates the expression. WatchEvaluator.RefreshAllwalksWatchStore.LoadEntries()instead ofLoad(), emitting aWatchResult { Disabled = true, Preview = "(disabled)" }placeholder for off rows. No call intoEvaluateOnefor those, so the rebind cost of a heavy expression goes to zero the moment the user toggles it off — including any side effects the fallback path'sResources.FindObjectsOfTypeAllsweep would normally pay for. The previous-preview snapshot map still tracks disabled expressions so re-enabling a row doesn't fire a false "changed" highlight against a stale value.WatchStoreon-disk shape promoted from envelopev1(List<string>) tov2(List<WatchEntry>—{ expression, enabled }). The reader peeks at theversionfield first and chooses an envelope class accordingly;v1files load through a string-to-entry migrator (enabled = truefor everything) and the nextPersistrewrites the file inv2shape, so the migration is one-time per project with no user action. AReset Project Datastill clears the file outright.- New public surface:
WatchStore.LoadEntries()(full entry list, including disabled),WatchStore.SetEnabled(expression, bool)(single-row toggle, no-op when the flag is already at the target value),WatchStore.SetAllEnabled(bool)(bulk; only persists when at least one row actually changes so a repeat click against an already-uniform state doesn't churn the file). LegacyWatchStore.Load()is retained but narrowed to "enabled expressions only" — the evaluator now goes throughLoadEntries()directly so it can render the off rows, and external callers that just want the active expression list (none ship today) see the sameList<string>they always have. WatchResult.Disabledis the panel-side flag the toggle reads; binding the row'sToggle.valueto!r.Disabledand routingRegisterValueChangedCallbackstraight intoWatchStore.SetEnabled(which itself firesChanged) means the user click → store mutation → panel rebuild loop closes without an extra Refresh from the view.ResetProjectDatacounts watches throughWatchStore.LoadEntries().Countrather than the now-active-onlyLoad().Count. A project with everything muted would otherwise see Reset's stale-file detector misreadwatches.json(which is fine, just full ofenabled:falserows) as unreadable, and the post-clear summary would reportCleared 0 itemswhile three persisted watch expressions actually got wiped. Snippet / history / usings counts are unaffected — only the watch store grew an "active subset" projection in this version.