Releases: djdcks12/UNITY-ROSLYN-REPL
Unity Roslyn REPL v0.8.1
[0.8.1] - 2026-05-19
Changed (Object Browser → Patch Method handoff)
- Selecting a method in the
MethodPickerPopup(the popup the Object Browser's Patch Method… action opens) now automatically flips the lower pane into Patches mode before filling the form. Previously the form was populated under whichever tab the user was on — usually Output, because the same row's Inspect action put them there a click ago — and the user had to find their way back to Patches by hand to see what just happened. The flip closes the loop so the click on Patch Method, the row pick in the popup, and the populated patch form read as one continuous gesture. Existing Pull-Original-on-fill behaviour (added with the row context menu in #60) is unchanged: by the time the form is visible, the body editor either holds the pulled source or the status label carries a non-blocking Pull-failure hint. Closes #64.
Added (Compile / Validate Only)
- New toolbar ✓ Validate button (and Ctrl+Shift+Enter / Cmd+Shift+Enter shortcut) compiles the current snippet without running it. Diagnostics flow into Output and the gutter the same way a failed Run does, but the assembly is never loaded into the AppDomain, the wrapper method is never invoked, runtime log capture never starts, and
ReplEngine.LastResult(the_carry-over) is unchanged. Useful when the snippet has side effects, drives a long experiment, or contains an as-yet-untested patch body the user wants to syntax-check before risking it against live Editor / Play Mode state. Closes #66. - A clean compile prints
✓ Validate OK (N ms)into Output and sets the summary chip toValidate OK; a failure prints the same diagnostic lines the failed-Run branch emits and paints gutter markers, so the user can scan the same surfaces they're already used to. The OK line is appended afterRenderResult's find-overlay rebuild signal, so the success branch raisesRaiseRebuilta second time (plus aScrollOutputToBottom) — an open Ctrl+F search picks the new row up immediately instead of holding a stale hit list until the user nudges the query. The CompileError branch doesn't need that extra signal because diagnostics are appended insideRenderResult, before its single rebuild. Run behaviour is unchanged — Validate sits next to Execute, not in front of it. - New public API
ReplEngine.Validate(string userCode, ReplOptions options = null)returns aReplResult(SuccesswithValue == nullon a clean compile,CompileErrorwith diagnostics otherwise). The pipeline mirrorsExecute's wrap + parse + compile path so diagnostics line up against the same wrapped source — but stops atcompilation.Emit(memoryStream): the IL bytes are produced (so emit-only errors like unsafe-IL violations still surface) but the stream is discarded,Assembly.Loadis never called, the compile cache is bypassed (caching off the dry path would blur the "this is a dry check" contract), and the dynamic-assembly counter on the toolbar asm-badge stays put. Assembly name uses aReplValidate_*prefix vs. Execute'sReplDynamic_*so a developer eyeballing a trace can tell which path produced a given diagnostic. - The cooperative-cancel first-run dialog (which Run shows once per workstation before the very first execution) is intentionally skipped on the Validate path — Validate can't loop forever or hang the Editor since user code never runs, so the warning would just train users to dismiss it.
Added (Insert into Code helpers)
- Insert into Code is a new context-menu action across three surfaces: Output tree rows, Object Browser rows, and Watch rows. Each one drops a small C# snippet straight into the Code editor — no clipboard round-trip — so the user can pick a value from the inspection UI, fold it into a snippet, and Run / Validate it without retyping the path. Closes #65.
- Output tree's action emits
return <ExpressionPath>;against the node's safe path (return _.inventory.items[0].count;). It shares the path-staleness gate with Copy Path / Add Watch: whenReplEngine.LastResultno longer points at the tree's root the menu entry picks up the" (stale — \_` rebound)"suffix and disables, so an old tree can't insert a path that would silently resolve against a different object. Object Browser's action reuses the existingBuildBrowserInspectSnippetbuilder (MonoBehaviour →InstanceIDToObject(...) as T, ScriptableObject →LoadAssetAtPath(...), Singleton → comment template), gated onCSharpTypeName.IsRenderable(type). Watch row's action routes through a newWatchEvaluator.WrapAsReturnStatementhelper that mirrorsEvaluateOne's historic statement-detection rule: a watch typed asreturn Foo();(with space *or* tab afterreturn, with or without trailing semicolon) is treated as a complete statement and emitted as-is with one trailing;; otherwise the stored expression is wrapped toreturn ;. Without that dispatch a row whose stored text already starts withreturnwould land in the Code editor as the syntactically invalidreturn return Foo();;`, and the two paths (evaluator / insert helper) would have drifted apart. The row's right-click menu only contains this one entry today, since the rest of the row's actions (toggle / remove) already have explicit controls. - Insertion policy: when the editor is empty or holds the default starter (the user has nothing to lose), the snippet replaces the buffer; otherwise it appends on a fresh line. No confirmation dialog and no auto-run — Run / Validate stay user-driven so the inserted snippet gets at least one glance before it executes. A one-line
📝 Inserted into Code: <preview>toast lands in Output so the user can spot the change without scrolling the Code editor by hand. - New
CodeEditorView.AppendSnippet(string)andCodeEditorView.ReplaceCode(string)are the public insertion API.AppendSnippettrims trailing newlines off the existing buffer before joining so the result is exactly"<existing>\n<snippet>"regardless of whether the user left their draft with a trailing newline;ReplaceCodewipes outright. The host'sInsertSnippetIntoCodeis the single chokepoint every UI action routes through — Output tree, Object Browser, and Watch row all hit it — so the replace-vs-append policy and the toast confirmation stay consistent across surfaces.WatchPanelViewexposes anOnInsertSnippetRequestedAction<string>hook the host wires to that chokepoint; the row menu greys out the action when the hook is null so a panel instantiated in isolation (tests, future surfaces without a Code editor sibling) shows a disabled entry instead of failing silently.
Added (Named pins for live objects)
- Pin as… is a new context-menu action on Object Browser rows and Output tree nodes. The user supplies a C# identifier (e.g.
player,manager,popup) and the live value is bound to that name for the rest of the session. Snippets and Watch expressions can then reference the name unqualified —return player.transform.position;,manager.Count, etc. — exactly like the existing_carry-over, just under a user-chosen handle. Closes #63. - Pins live in process memory only: a domain reload, an Editor restart, or Reset Project Data clears every pin. The new Tools / Roslyn REPL / Pins… window lists the live pins (name + type + value preview) with per-pin ✕ and a global Clear all, and the panel header restates the session-only contract so the user doesn't go looking for a "persist pins" toggle that intentionally doesn't exist.
- Names go through
PinStore.ValidateNamebefore storage: valid C# identifiers only (letter /_first, letter / digit /_continue), C# keywords rejected, and the wrapper's reserved tokens (_,ct, plusReplCodeWrapper.ClassName/ReplCodeWrapper.MethodName—__ReplScriptand__Runtoday) rejected so a pin can't silently maskReplEngine.LastResult, the cancellation token, or the wrapper method itself. A pin named__Runwould otherwise produce two same-named members inside the wrapper class (the injected static property + the method the engine compiles into) and every Run / Watch compile would fail until the user found and removed it; the validator now blocks that name up front, and the reserved set pulls from the wrapper constants directly so a future rename flows through automatically. The "Pin as…" prompt reuses the new sharedStringPromptDialog(extracted fromSnippetLibraryWindow's private nested class so two callers don't have to duplicate IMGUI plumbing); failure messages fromValidateNamesurface as inline warnings rather than aborting the flow. - Wrapper integration:
ReplCodeWrapper.Wrapemits onepublic static dynamic <pinName> => PinStore.Get("<pinName>");line per pin alongside the existing_/ctstatics. Names are spliced into source without quoting sinceValidateNamealready rejects anything that wouldn't be a legal identifier.PinStore.Changedis hooked once-per-domain byReplEngine.EnsureAssemblyLoadHookand routed straight toInvalidateCompileCache— pin set is part of the wrapper source, so the cached MethodInfo would otherwise hand back a stale pre-edit wrapper after every pin mutation.WatchPanelViewsubscribes toPinStore.Changedtoo and re-runs the evaluator on every mutation, so a watch row likeplayer.transform.positiondoesn't stay stuck on a compile error afterplayeris pinned (or keep showing the prior pin's value afterplayeris dropped) until the next Run forces a refresh. Reset Project Data's confirmation dialog now listsN named pin(s)and clears them alongside_; the post-clear "Cleared N items" tally includes the pin count, and the early-return "Nothing to clear" branch checksPinStore.Count == 0so a single dangling pin still routes through the clean path. The success and partial-failure result dialogs also name pins in their summary so a Reset that w...
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 st...
Unity Roslyn REPL v0.7.4
[0.7.4] - 2026-05-18
Added (Ctrl+F find overlay across Output / Watch / Patches)
- Press Ctrl+F (Cmd+F on macOS) anywhere in the REPL window to open a search bar above the toolbar. The bar scans:
- Output panel — log lines + result trees (names + types + previews).
- Watch panel — row expression / preview / type / error / source description, plus the inline expandable tree when the row is open. Collapsed Watch trees are skipped (expand a row first to make its nodes searchable).
- Patches view — the active patches list (target type + method + parameter list + last error) and the live edit form: Type / Method / Params fields plus the multi-line Body editor (one hit per occurrence with a
(line N)label so Next / Prev walks each match in turn). The body editor's parentScrollViewis driven directly viascrollOffsetto bring the match line into the viewport, and a translucent yellow rectangle parented to the ScrollView's content container is positioned over the match line. The column offset measures prefix + match widths through a sentinel-wrappedTextElement.MeasureTextSizeprobe (MeasureTextSize("X" + span + "X").x - MeasureTextSize("XX").x) — wrapping the span in visible anchors moves the leading / trailing whitespace into the interior of the measured string, where Unity's edge-whitespace trim doesn't apply. Without that wrapper an indented line measured as if the leading spaces were zero pixels wide and the marker only covered half the match before running out of width. Form-field hits (Type / Method / Params) paint a thin warm accent border on the matching field via a CSS class. None of the Patches hits callFocus()/SelectRange()on aTextField— the previous focus-then-refocus dance kept stealing the keyboard from the Find input on the way back, which broke "type to refine the query." The marker is independent of focus and stays visible while the user keeps typing.
Unity Roslyn REPL v0.7.3
[0.7.3] - 2026-05-12
Changed (Korean README rewritten in natural prose)
README_kr.mdrewritten to read as native Korean rather than as a literal translation ofREADME.md. Removed translationese tics (over-use of "…할 수 있습니다", chained "…됩니다" passive endings, "권장합니다 / 구성되어 있습니다" stiff phrasings, comma-heavy English-style enumerations), broke up multi-clause English sentences into shorter Korean ones, and softened the section headings ("왜 쓰나요?" → "어떤 일을 도와주나요"). Code blocks, menu paths, the helper table, and the meaning of every section are unchanged so the EN / KR pair still describes the same package; only the prose register moved.
Removed (UsingsStore legacy key migration)
UsingsStore.LegacyKey(RoslynRepl.CustomUsings, the project-agnostic key the pre-Phase-4 builds wrote to) and theMigrateLegacyKeyIfNeededstep that ran on the firstLoadCustom/Saveafter each domain reload are gone. The package shipped publicly at 0.7.2; before that the migration window covered users still upgrading from local 0.7.0 / 0.7.1 builds, but those paths no longer have a real consumer. Carrying the migration forward only added a stateful first-call branch on every load and a tiny_legacyMigrationCheckedflag to remember whether we'd already swept the legacy key this session — both dead weight on every fresh install.- A 0.7.1 install upgraded to 0.7.3 will keep its custom usings (the per-project bucket lookup is unchanged); any value that still sat under the global
RoslynRepl.CustomUsingskey on first launch of an even older build is no longer rescued. Wipe by hand from EditorPrefs if you need to.Reset Project Datacontinues to clear the current per-project key. - Drive-by sweep for similarly-dead compatibility paths:
MethodPatchView.OnPullOriginalClickedno longer special-cases the// Supported scope: void instance methods.historic starter text (only the currentDefaultBodyand its prefix shape are recognised), and a stale "legacy" word inRoslynReplWindow.OnBrowserInstanceChosen's comment was relabelled to "default" so the description matches the live code. The;-vs-,ParameterTypes fallback inMethodPatchSpec.SplitParamTypesand the folder-mismatch warning inReplPackagePathsstay — they describe live code paths that are still load-bearing for fresh installs. Closes #53.
Added (Per-row Delete in the Patches list)
- The Patches list now has a per-row Delete button next to Load / Revert. Reset Project Data was previously the only way to drop a draft from the registry — too broad for "I'm done with this one row", since it also wipes snippets, history, watches, and every other Patch the user is mid-debugging. The list section header is renamed from "Active patches" to plain "Patches" (Revert leaves Inactive drafts behind, so the section is no longer "active only"); the empty-state placeholder reads
(no patches)accordingly. - Delete shows a confirm dialog ("Delete this patch draft?") that calls out the destructive payload (the persisted body for that spec disappears) and adds an explicit auto-revert note when the row is currently Active. The handler reverts the Harmony detour first if installed, then calls
PatchRegistry.Remove(spec)which persists the removal — so a domain reload won't resurrect a deleted draft. Reset Project Data remains the broad cleanup path. Closes #52. PatchPersistence.Savenow deletes the on-disk file when the supplied spec list ends up empty after filtering (instead of writing{"version":1,"items":[]}to disk). Without this, deleting the last patch row through the new button left a zero-content file on disk: the Patches UI rendered empty, butHasAny()kept returningtrueand Reset Project Data fell through the "Nothing to clear" early-return into the stale-file cleanup branch. Centralising the empty case at the persistence layer covers every caller — Remove, Clear-via-registry, AddOrUpdate of a filtered-out spec — without each call site having to remember.Savereturnsbooland propagates theUserSettingsStorage.Deletesuccess flag throughPatchRegistry.Persistand a newPatchRegistry.Remove(spec, out bool persistedOk)overload. The per-row Delete handler now distinguishes a clean success ("Deleted: …") from the soft-failure case where in-memory removal worked butpatches.jsoncouldn't be deleted (locked / read-only) — a domain reload would otherwise resurrect the supposedly-deleted draft. Partial-failure status surfaces inline with a hint to check the Console for the file path.PatchRegistry.Removenow persists before mutating_byKey(build the post-removal snapshot, callSave, only then drop the key from the live dictionary). On a multi-row delete the previous shape mutated first and then wrote — if the write threw on a hard I/O failure (permissions, disk full, atomic-replace contention) the registry was missing a key the file still had, and a domain reload resurrected the supposedly-deleted spec. The Delete UI handler wraps the call intry/catchso the exception surfaces as a "delete failed, list unchanged" status with the underlying error logged to the Console.- The Delete failure status now branches on whether the row was Active before the click. For an inactive draft "patch list unchanged" is accurate; for an active row the earlier
PatchEngine.Revert(spec)already removed the detour and persisted Status = Inactive before the failingRemoveran, so the message instead says "the patch was already reverted (detour removed, row is now Inactive); re-Apply if you want it active again". Without the branch the UI would falsely reassure the user that Play Mode behaviour was unchanged when in fact it was already reverted. PR-review followups on #52.
Unity Roslyn REPL v0.7.2
[0.7.2] - 2026-05-12
Changed (Legacy package-id fallback)
- The literal
com.roslyn-replfolder probe inReplPackagePaths.ResolvePackageRootis gone. Path resolution now relies entirely onPackageInfo.FindForAssembly— Unity resolves embedded packages by thenamefield inpackage.json, not the containing folder name, so forcing the folder to match the published id would have broken every legitimate embedded-dev case (variants / forks / checkouts under different folder names sharing the same id). IsBundledRoslynAssemblyPathnow matches the bundled-DLL location through bothPackageInfo.assetPath(AssetDatabase form, e.g.Packages/com.youngchan.roslyn-repl) andPackageInfo.resolvedPath(filesystem form, e.g.…/Library/PackageCache/com.youngchan.roslyn-repl@0.7.2). The first path covers embedded / direct-folder installs; the second covers OpenUPM and any other source where Unity stages the package underLibrary/PackageCache/. Without the resolvedPath leg, OpenUPM-installed copies — whoseAssembly.Locationnever goes through the AssetDatabase form — would silently fall out of the BundledByUs classification and the setup verifier's "duplicate copy?" branch would lie about what's loaded.- The match also tightened from two independent
Containschecks (/Editor/Plugins/Roslyn/AND/<root>) to a single anchored segment<root>/Editor/Plugins/Roslyn/(with leading/boundary or path-start anchor), so a folder namedcom.roslyn-repl-old/no longer substring-matches a root ofcom.roslyn-repl/. PR-review followup. - Soft nudge for embedded checkouts: when
PackageInfo.assetPathends in a folder name other thancom.youngchan.roslyn-repl, the resolver emits a singleDebug.LogWarningper editor session asking the developer to rename the folder before consuming via OpenUPM. Local development keeps working through PackageInfo in the meantime. Closes #45.
Changed (Output result tree defaults to fields-only)
- The Output result tree no longer invokes user-defined property getters by default. A property getter can run lazy init, IO, log emission, or state mutation — inspecting a value in Output shouldn't trip any of that for the user. Watch already opted out for the same reason (refreshing every Run multiplies the side effects); Output now matches.
- New
Tools / Roslyn REPL / Output: Include Property Getterstoggle flips the behaviour per project. Off (default) walks fields only — instance, private, and inherited. On walks readable instance properties on top of that. The toggle lives inRoslynRepl.Editor.Core.OutputSettings.IncludePropertiesso other surfaces can read or flip it programmatically. - Existing
SimpleObjectSerializer.Options.IncludePropertiesdefault stays attrueso direct callers don't quietly change behaviour; the Output panel routes through a newBuildOutputTreeOptions()helper that consults the setting. README updated. Closes #28.
Changed (Apply-to-file backups moved out of Assets/Packages)
Apply to fileno longer dropsFoo.cs.baknext to the editedFoo.cs. Unity used to import the backup as a regular asset (.metagenerated, Project window pollution, easy to accidentally commit insideAssets/orPackages/). Backups now land under<project>/Library/RoslynRepl/Backups/<yyyyMMdd_HHmmss_fff>_<name>.cs.bak—Library/is the Unity-reserved derived-data folder that the asset importer skips and the stock.gitignoretemplate excludes.- The atomic-write pipeline is unchanged in spirit (same
File.Replaceswap with a same-directory dot-prefixed temp), only the destination of the pre-write backup moved. The Library/ path is passed toFile.Replaceas itsdestinationBackupFileName— Replace requires a non-null backup parameter, and the step-1File.Copyalready wrote identical bytes to that path, so Replace's overwrite is content-equivalent and the user's recovery copy stays valid before / during / after the swap. Source / destination / backup are all resolved to absolute filesystem paths first (Path.GetFullPathon the source) soFile.Replacedoesn't trip on the Unity-style asset paths it would otherwise see mixed with the absolute Library/ path;AssetDatabase.ImportAssetfurther down still receives the project-relative path it expects. Apply confirm dialog and the status bar message updated to point at the new path; README + README_kr Patches sections updated to call out the trade-off (Unity may deleteLibrary/on a reimport — copy a backup out if you need long-term recovery). Closes #44.
Changed (User data moved from EditorPrefs to project-local files)
- Patch bodies, snippets, run history, and watch expressions now live in
<project>/UserSettings/RoslynRepl/*.jsoninstead ofEditorPrefs. On Windows EditorPrefs is registry-backed, so the historical store leaked beyond project deletion and bloated as patches and snippets grew. The new files tie the data to the project folder — deleting the project reclaims everything, andUserSettings/is the same place Unity ships its own per-user / per-project layout files. - Breaking for any pre-release 0.7.1 install: there is no migration from the old EditorPrefs keys. The package has not had a public consumer release yet, so preserving the historical blob would only carry forward dead code on every Load. Any 0.7.1 EditorPrefs entry stays orphaned in the registry; remove it by hand if you need to (the keys started with
RoslynRepl.). - The first time the package writes into the folder it also drops a
UserSettings/RoslynRepl/.gitignorecontaining a single*rule, so files in here never show up ingit status— even on consumer projects whose top-level.gitignoredoesn't already coverUserSettings/. This is defense-in-depth: patch bodies and history entries can carry server URLs / auth tokens / account values from a mid-debug paste, and a stock template gitignore is not a guarantee. Caught in PR review. - New
RoslynRepl.Editor.Core.UserSettingsStorageis the single I/O helper every store routes through. Writes go through the same atomicFile.Replacepipeline the source export uses (write to a dot-prefixed temp in the destination directory, then swap) so a crash mid-save can't leave a half-written blob the next session reads back as garbage. UserSettingsStorage.Deletereturnsboolindicating whether the post-call state is "file does not exist", and routes any failure (locked file, missing permission) throughDebug.LogWarningwith the path. The four file-backedClear()methods (SnippetStore,RunHistoryStore,WatchStore,PatchPersistence— andPatchRegistry.Clearwhich delegates to it) now propagate that flag, andTools / Roslyn REPL / Reset Project Dataaggregates the four results: any survivor surfaces a "partial failure" dialog listing the stuck files, the folder path, and the remediation (close any external editor, retry, or delete by hand) instead of the previous unconditional "Cleared N items" success. Without this the nextLoad()would silently resurrect the supposedly-cleared sensitive data and the user had no idea. PR-review followup.SnippetStore.HasAny/RunHistoryStore.HasAny/WatchStore.HasAnymirror the existingPatchPersistence.HasAnyso Reset Project Data's scope decision counts file existence separately from successful Load. Without those, a corrupt or unreadable JSON would collapse to an empty list and trip the early-return "Nothing to clear" — leaving the actual sensitive file untouched on disk. The confirm dialog now phrases those rows as "the on-disk <store> file (currently unreadable / cannot be decoded — wiped to recover)" so the user sees what the wipe is actually targeting. PR-review followup.- The Run History popup's Clear button now consumes the bool from
RunHistoryStore.Clear. On failure the visible list refreshes from disk (so it matches the actual surviving file state instead of looking empty) and an inline error dialog points at the cause + the Console for the exact path. Previously a lockedrunHistory.jsonwould leave the UI blank while the file — and its sensitive contents — sat untouched, ready to resurface the next time the popup opened. PR-review followup. - File schema is JSON with a
versionfield so future format bumps can be detected; payload shapes are{version, items}for the three list stores and aSpecDtomirror ofMethodPatchSpec(with an explicitHasOriginalBodyflag sonullvs""source snapshots stay distinct across reload) for patches. README persistence + safety sections (EN + KR) updated to point at the new location and to call out the per-folder.gitignore. Closes #27.
Fixed (Generic parameter types in patch specs)
-
MethodPatchSpec.ParameterTypesnow uses;as the per-parameter separator instead of,. Closed-generic CLRType.FullNameoutput embeds commas inside the assembly-qualified inner type list:List`1[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=…]]so the historic comma split in
PatchEngine.ResolveParamTypesshredded any generic parameter into garbage and the spec failed to resolve at Apply / reload / source export — even though the form picker would happily list the method as patchable. Semicolon is illegal in CLR full type names, so it can't collide with anything inside a single parameter's name. -
New
MethodPatchSpec.JoinParamTypes/SplitParamTypeshelpers centralize the encoding.Browse → Fill formand the manual form field both go through the join helper; the engine resolver, the diff, and the active-list display all go through the split helper. Legacy comma-joined data still loads (split falls back to,when the value contains no;and no[), so plain non-generic specs persisted on 0.7.1 keep working. Legacy data that had embedded brackets (i.e. it was already broken) surfaces a clean "type not found" error from the type ...
v0.7.1
[0.7.1] - 2026-05-11
Changed (OpenUPM package id)
- Package id renamed from
com.roslyn-repltocom.youngchan.roslyn-replso the package satisfies OpenUPM's reverse-domain package-name convention before the first public registry release. - Installation snippets, OpenUPM metadata, the Harmony patch id, and package-root lookups now use the new id. Runtime/editor asset lookups resolve through Unity Package Manager metadata with a legacy
Packages/com.roslyn-replfallback so existing local checkout folders keep working during development. - The OpenUPM descriptor was renamed to
Documentation~/openupm/com.youngchan.roslyn-repl.yml; copy that file toopenupm/openupm/data/packages/com.youngchan.roslyn-repl.ymlwhen submitting the registry PR.
v0.7.0
Unity Roslyn REPL 0.7.0
This release turns Unity Roslyn REPL into a more complete Editor debugging toolkit.
Highlights
- Interactive C# REPL inside the Unity Editor
- Rich Output inspector for objects, collections, dictionaries, and Unity objects
- Watch panel with cached evaluation for repeated expressions
- Object Browser for scene objects, assets, resources, and singleton-style instances
- Snippet library, run history, and custom using management
- Runtime method patching with Pull Original, diff preview, revert, and source export
- Korean README added
Safety and reliability
- Editor-only package footprint
- Project-scoped saved data
- Reset Project Data cleanup flow
- Setup verification for Roslyn/Harmony dependencies
- Clear cooperative-timeout warning for long-running snippets
Package
Install via Unity Package Manager using the Git URL, or download the attached .tgz.
For the full detailed changelog, see CHANGELOG.md.