Skip to content

Releases: djdcks12/UNITY-ROSLYN-REPL

Unity Roslyn REPL v0.8.1

19 May 02:01

Choose a tag to compare

[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 to Validate 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 after RenderResult's find-overlay rebuild signal, so the success branch raises RaiseRebuilt a second time (plus a ScrollOutputToBottom) — 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 inside RenderResult, 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 a ReplResult (Success with Value == null on a clean compile, CompileError with diagnostics otherwise). The pipeline mirrors Execute's wrap + parse + compile path so diagnostics line up against the same wrapped source — but stops at compilation.Emit(memoryStream): the IL bytes are produced (so emit-only errors like unsafe-IL violations still surface) but the stream is discarded, Assembly.Load is 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 a ReplValidate_* prefix vs. Execute's ReplDynamic_* 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: when ReplEngine.LastResult no 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 on CSharpTypeName.IsRenderable(type). Watch row's action routes through a new WatchEvaluator.WrapAsReturnStatementhelper that mirrorsEvaluateOne's historic statement-detection rule: a watch typed as return 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 to return ;. Without that dispatch a row whose stored text already starts with returnwould 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) and CodeEditorView.ReplaceCode(string) are the public insertion API. AppendSnippet trims 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; ReplaceCode wipes outright. The host's InsertSnippetIntoCode is 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. WatchPanelView exposes an OnInsertSnippetRequested Action<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.ValidateName before storage: valid C# identifiers only (letter / _ first, letter / digit / _ continue), C# keywords rejected, and the wrapper's reserved tokens (_, ct, plus ReplCodeWrapper.ClassName / ReplCodeWrapper.MethodName__ReplScript and __Run today) rejected so a pin can't silently mask ReplEngine.LastResult, the cancellation token, or the wrapper method itself. A pin named __Run would 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 shared StringPromptDialog (extracted from SnippetLibraryWindow's private nested class so two callers don't have to duplicate IMGUI plumbing); failure messages from ValidateName surface as inline warnings rather than aborting the flow.
  • Wrapper integration: ReplCodeWrapper.Wrap emits one public static dynamic <pinName> => PinStore.Get("<pinName>"); line per pin alongside the existing _ / ct statics. Names are spliced into source without quoting since ValidateName already rejects anything that wouldn't be a legal identifier. PinStore.Changed is hooked once-per-domain by ReplEngine.EnsureAssemblyLoadHook and routed straight to InvalidateCompileCache — 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. WatchPanelView subscribes to PinStore.Changed too and re-runs the evaluator on every mutation, so a watch row like player.transform.position doesn't stay stuck on a compile error after player is pinned (or keep showing the prior pin's value after player is dropped) until the next Run forces a refresh.
  • Reset Project Data's confirmation dialog now lists N 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 checks PinStore.Count == 0 so 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...
Read more

Unity Roslyn REPL v0.8.0

19 May 00:14

Choose a tag to compare

[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 whenever ReplEngine.LastResult is non-null and hides when it's null — Run that returns a non-null value, Object Browser inspect, and explicit ReplEngine.SetLastResult all light it up; an explicit Clear or Reset Project Data extinguishes it. Closes #59.
  • Clicking the badge re-inspects the live _ value: clears Output and renders the same SimpleObjectSerializer.ToTree shape Browse-inspect emits, so the user can fold the tree open and read the object's fields without typing return _; themselves. The summary chip reads Inspect _ 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.LastResult mutations now route through a single private AssignLastResult chokepoint (SetLastResult, ResetLastResult, and the in-Execute success path all funnel through it). The new public LastResultChanged event fires from there with a force flag 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 the ReferenceEquals short-circuit so a no-op ResetLastResult() on an already-null engine doesn't churn the UI.
  • Subscribers are invoked through GetInvocationList() with a per-handler try / catch. A misbehaving UI surface that throws from LastResultChanged (a torn-down panel, a stale VisualElement) can no longer propagate up through Execute's outer catch and convert a successful snippet into a ReplResult.RuntimeError. The carry-over field still updates and remaining subscribers still fire; the failure surfaces as a single Debug.LogWarning so 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 to DeclaredType when 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 — same ReplEngine.SetLastResult(...) the inspect path uses, just without the tree render. The toolbar _ badge picks the new value up through LastResultChanged, and the action also fires _watch.Refresh() at the same commit point Inspect uses so a Watch panel holding _.foo expressions immediately re-evaluates against the new target instead of showing stale data from the previous _. The pane summary chip reads Bound \_`rather thanBrowsed`.

  • Copy Type Name renders the runtime type through CSharpTypeName.Render so 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 display TypeName when 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 through DeclaredType alone) fall back to FindFirstObjectByType<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 get LoadAllAssetsAtPath + .OfType<T>().FirstOrDefault(a => a.name == "...") so they don't collide with the path's main asset. The previous FindAssets("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 via CreateInstance, 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 (.Instance vs .I vs 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.

  • Menu entries are gated by liveness / renderability so the surface never promises an action it can't deliver. Inspect / Set as _ go Disabled when the row's value resolved to null or a destroyed UnityEngine.Object. Patch Method stays enabled when the row has a DeclaredType even if the live value is gone (matching the singleton fallback). Copy Inspect Snippet goes Disabled when the row's type can't be rendered as C# source (open generic / generic parameter / no type at all) — same CSharpTypeName.IsRenderable check the snippet builder uses, so a successful click always produces source the user can paste and Run.

  • Double-click behaviour is unchanged. ObjectBrowserView exposes a parallel OnRowAction event in addition to the legacy OnInstanceChosen; host wires both, and the action handlers all reuse the same RenderBrowserInspect / OpenBrowserPatchMethod helpers the double-click path calls. A pooled ContextualMenuManipulator attached once in MakeRow walks up from the click target's userData to find the bound InstanceEntry, 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 + fires Changed, 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-line Preview text (works even on truncation / error placeholders so it's never a dead-end). Inspect This clears Output and renders a fresh SimpleObjectSerializer.ToTree against 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: rebinds to 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's matchesRootValueagain, 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 rebinds but leaves the previous tree visible with its oldRootValue`, so those rows stay st...
Read more

Unity Roslyn REPL v0.7.4

18 May 00:57

Choose a tag to compare

[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 parent ScrollView is driven directly via scrollOffset to 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-wrapped TextElement.MeasureTextSize probe (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 call Focus() / SelectRange() on a TextField — 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

12 May 05:33
9ec91cc

Choose a tag to compare

[0.7.3] - 2026-05-12

Changed (Korean README rewritten in natural prose)

  • README_kr.md rewritten to read as native Korean rather than as a literal translation of README.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 the MigrateLegacyKeyIfNeeded step that ran on the first LoadCustom / Save after 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 _legacyMigrationChecked flag 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.CustomUsings key on first launch of an even older build is no longer rescued. Wipe by hand from EditorPrefs if you need to. Reset Project Data continues to clear the current per-project key.
  • Drive-by sweep for similarly-dead compatibility paths: MethodPatchView.OnPullOriginalClicked no longer special-cases the // Supported scope: void instance methods. historic starter text (only the current DefaultBody and its prefix shape are recognised), and a stale "legacy" word in RoslynReplWindow.OnBrowserInstanceChosen's comment was relabelled to "default" so the description matches the live code. The ;-vs-, ParameterTypes fallback in MethodPatchSpec.SplitParamTypes and the folder-mismatch warning in ReplPackagePaths stay — 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.Save now 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, but HasAny() kept returning true and 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.
  • Save returns bool and propagates the UserSettingsStorage.Delete success flag through PatchRegistry.Persist and a new PatchRegistry.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 but patches.json couldn'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.Remove now persists before mutating _byKey (build the post-removal snapshot, call Save, 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 in try/catch so 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 failing Remove ran, 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

12 May 01:27

Choose a tag to compare

[0.7.2] - 2026-05-12

Changed (Legacy package-id fallback)

  • The literal com.roslyn-repl folder probe in ReplPackagePaths.ResolvePackageRoot is gone. Path resolution now relies entirely on PackageInfo.FindForAssembly — Unity resolves embedded packages by the name field in package.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).
  • IsBundledRoslynAssemblyPath now matches the bundled-DLL location through both PackageInfo.assetPath (AssetDatabase form, e.g. Packages/com.youngchan.roslyn-repl) and PackageInfo.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 under Library/PackageCache/. Without the resolvedPath leg, OpenUPM-installed copies — whose Assembly.Location never 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 Contains checks (/Editor/Plugins/Roslyn/ AND /<root>) to a single anchored segment <root>/Editor/Plugins/Roslyn/ (with leading / boundary or path-start anchor), so a folder named com.roslyn-repl-old/ no longer substring-matches a root of com.roslyn-repl/. PR-review followup.
  • Soft nudge for embedded checkouts: when PackageInfo.assetPath ends in a folder name other than com.youngchan.roslyn-repl, the resolver emits a single Debug.LogWarning per 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 Getters toggle 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 in RoslynRepl.Editor.Core.OutputSettings.IncludeProperties so other surfaces can read or flip it programmatically.
  • Existing SimpleObjectSerializer.Options.IncludeProperties default stays at true so direct callers don't quietly change behaviour; the Output panel routes through a new BuildOutputTreeOptions() helper that consults the setting. README updated. Closes #28.

Changed (Apply-to-file backups moved out of Assets/Packages)

  • Apply to file no longer drops Foo.cs.bak next to the edited Foo.cs. Unity used to import the backup as a regular asset (.meta generated, Project window pollution, easy to accidentally commit inside Assets/ or Packages/). Backups now land under <project>/Library/RoslynRepl/Backups/<yyyyMMdd_HHmmss_fff>_<name>.cs.bakLibrary/ is the Unity-reserved derived-data folder that the asset importer skips and the stock .gitignore template excludes.
  • The atomic-write pipeline is unchanged in spirit (same File.Replace swap with a same-directory dot-prefixed temp), only the destination of the pre-write backup moved. The Library/ path is passed to File.Replace as its destinationBackupFileName — Replace requires a non-null backup parameter, and the step-1 File.Copy already 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.GetFullPath on the source) so File.Replace doesn't trip on the Unity-style asset paths it would otherwise see mixed with the absolute Library/ path; AssetDatabase.ImportAsset further 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 delete Library/ 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/*.json instead of EditorPrefs. 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, and UserSettings/ 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/.gitignore containing a single * rule, so files in here never show up in git status — even on consumer projects whose top-level .gitignore doesn't already cover UserSettings/. 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.UserSettingsStorage is the single I/O helper every store routes through. Writes go through the same atomic File.Replace pipeline 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.Delete returns bool indicating whether the post-call state is "file does not exist", and routes any failure (locked file, missing permission) through Debug.LogWarning with the path. The four file-backed Clear() methods (SnippetStore, RunHistoryStore, WatchStore, PatchPersistence — and PatchRegistry.Clear which delegates to it) now propagate that flag, and Tools / Roslyn REPL / Reset Project Data aggregates 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 next Load() would silently resurrect the supposedly-cleared sensitive data and the user had no idea. PR-review followup.
  • SnippetStore.HasAny / RunHistoryStore.HasAny / WatchStore.HasAny mirror the existing PatchPersistence.HasAny so 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 locked runHistory.json would 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 version field so future format bumps can be detected; payload shapes are {version, items} for the three list stores and a SpecDto mirror of MethodPatchSpec (with an explicit HasOriginalBody flag so null vs "" 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.ParameterTypes now uses ; as the per-parameter separator instead of ,. Closed-generic CLR Type.FullName output 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.ResolveParamTypes shredded 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 / SplitParamTypes helpers centralize the encoding. Browse → Fill form and 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 ...

Read more

v0.7.1

11 May 06:02

Choose a tag to compare

[0.7.1] - 2026-05-11

Changed (OpenUPM package id)

  • Package id renamed from com.roslyn-repl to com.youngchan.roslyn-repl so 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-repl fallback 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 to openupm/openupm/data/packages/com.youngchan.roslyn-repl.yml when submitting the registry PR.

v0.7.0

11 May 05:38

Choose a tag to compare

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.