Repository navigation
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 resolver instead of silently picking the wrong overload. -
New
MethodPatchSpec.NormalizeParamTypes(string)runs at every registry boundary —Keyed,AddOrUpdate, andLoadFromPersistence— so a legacy comma-joined entry persisted on 0.7.1 lands at the same registry slot as a freshly Browsed semicolon-joined entry for the same method. Without itApplycouldn't see the previously-installed Harmony prefix and would stack a second one. Caught in PR review for #41. -
The Params field tooltip and the active-list row label now reflect the new semicolon-separated form. The row label still renders with
,between entries for readability — the raw semicolon form is only the on-disk encoding. Closes #41.
Fixed (Generic-aware C# type renderer for patch wrappers)
- New
RoslynRepl.Editor.Patches.CSharpTypeName.Render(Type)is the single renderer the patch wrapper uses for both thePrefixdeclaring-type / parameter-type emission and everytypeof()reference inside generated helper bodies. Handles closed generics (List<int>/Dictionary<string, int>), nested types (the historic+→.), nested generics with per-level introduced-arity attribution (Outer<int>.Inner/Outer<int>.Inner<string>), arrays of any rank, andNullable<T>(rendered as theT?short form). The historictarget.DeclaringType.FullName.Replace('+', '.')only worked for plain non-nested non-generic types — anything with a backtick / bracket in the FullName produced CLR-internal arity markers in the wrapper source and the Roslyn compile failed even after the spec resolved correctly to a real method. PatchSyntaxRewriter.TypeRefandPatchSyntaxRewriter.RenderCSharpTypeWithSubstitutionnow route throughCSharpTypeName.Rendertoo. The rewriter previously carried its own copy of the renderer with the same backtick-truncation bug — so even after the wrapper signature was fixed, a body that read aprivate Outer<int>.Inner<string>member through__get<T>/__call<T>/typeof(...)would still emit invalid C# for that member's type and the wrapper would fail to compile on the next Apply.CSharpTypeName.Rendergained an internal(Type, Func<Type, string>)overload so the rewriter's substitution callback (method-level generic argument → userTypeSyntax) can plug into the same nested-chain walk; the duplicate buggy renderer is gone. Reported in PR review.- Open generic types (generic type definitions, generic parameters) raise
NotSupportedExceptionat the renderer;PatchEngine.ResolveTargetMethodcatches the upstream form (type.IsGenericTypeDefinition) and rejects with a friendly "specify a closed type, e.g.Foo<...>" message before the wrapper renderer ever runs. Closes #42.
Fixed (Watch fallback discovery cache)
- Watch evaluation no longer re-runs the global instance discovery sweep for every fallback expression in a Refresh. The earlier shape made each unqualified or owner-qualified fallback call
InstanceLocator.Find(InstanceCategory.All, …)directly, andFindrebuilt the pool from scratch every time —Resources.FindObjectsOfTypeAll<MonoBehaviour>()+Resources.FindObjectsOfTypeAll<ScriptableObject>()+SingletonScanner.Find()for each call, twice per fallback expression (qualified pass + unqualified pass). N fallback watches paid the discovery cost 2N times in a single Run, which felt slow on projects with many loaded scenes. - New
WatchEvaluationScopeopens once perWatchEvaluator.RefreshAlland lazily snapshots the global pool the first time any fallback expression needs it. Every later fallback call in the same refresh reuses the snapshot throughInstanceLocator.FindFromSnapshot, which keeps the per-call filter + cap behaviour identical to the originalFindpath. The scope is disposed in afinallyso destroyed Unity objects don't get pinned past the refresh — references are only held for the lifetime of one Refresh, never across refreshes.EvaluateOnecalled outside a Refresh (one-off API surface) opens its own throwaway scope so the standalone semantics survive. Closes #43.