From 34d7bb30f4fa1ee5f8a951f733fd5585a50952db Mon Sep 17 00:00:00 2001 From: dichotomy-corp Date: Sat, 14 Mar 2026 14:11:35 +0900 Subject: [PATCH 1/2] Update .gitignore to filter Jetbrains cache directory --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 3f9a3e8..b7453a6 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,9 @@ # Gradle cache directory .gradle/ +# Jetbrains cache directory +.idea/ + # Autogenerated VS/MD/Consulo solution and project files ExportedObj/ .consulo/ From be39689cc83f9ce694a01356314c4485ed1023d5 Mon Sep 17 00:00:00 2001 From: dichotomy-corp Date: Sat, 14 Mar 2026 14:13:23 +0900 Subject: [PATCH 2/2] feat: Add constructed generic candidate support for SubclassSelector Add support for discovering and constructing closed generic candidate types (e.g. ConstantValueProvider) when the field type is a closed generic interface or base class (Unity 2023.2+). Previously, only non-generic concrete classes that explicitly closed the type parameter were shown in the popup. Changes: - Relax intrinsic type policy to allow constructed generics through - Infer type arguments and construct closed types via MakeGenericType - Add generic-aware display names for popup menu and inline labels - Merge dual assembly scans into a single pass - Remove redundant filtering in TypeCandiateService - Cache attribute lookups, type paths, and nicified names - Pre-sort type array once at construction instead of on every Show --- .../Editor/AdvancedTypePopup.cs | 9 +- .../Editor/SubclassSelectorDrawer.cs | 4 +- .../Editor/TypeMenuUtility.cs | 95 ++++++++++++-- .../DefaultIntrinsicTypePolicy.cs | 2 +- ...ity_2023_2_OrNewer_TypeCandiateProvider.cs | 124 ++++++++++++++++-- .../Editor/TypeSearch/TypeCandiateService.cs | 14 +- .../Editor/TypeSearch/TypeSearchService.cs | 2 +- 7 files changed, 214 insertions(+), 36 deletions(-) diff --git a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/AdvancedTypePopup.cs b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/AdvancedTypePopup.cs index 49ed756..b9435bb 100644 --- a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/AdvancedTypePopup.cs +++ b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/AdvancedTypePopup.cs @@ -39,7 +39,7 @@ public static void AddTo (AdvancedDropdownItem root, IEnumerable types) }; root.AddChild(nullItem); - Type[] typeArray = types.OrderByType().ToArray(); + Type[] typeArray = types as Type[] ?? types.ToArray(); // Single namespace if the root has one namespace and the nest is unbranched. bool isSingleNamespace = true; @@ -111,7 +111,8 @@ public static void AddTo (AdvancedDropdownItem root, IEnumerable types) } // Add type item. - var item = new AdvancedTypePopupItem(type, ObjectNames.NicifyVariableName(splittedTypePath[splittedTypePath.Length - 1])) + var item = new AdvancedTypePopupItem(type, TypeMenuUtility.CachedNicifyVariableName(splittedTypePath[splittedTypePath.Length - 1])) + { id = itemCount++ }; @@ -145,7 +146,7 @@ public AdvancedTypePopup (IEnumerable types, int maxLineCount, AdvancedDro public void SetTypes (IEnumerable types) { - this.types = types.ToArray(); + this.types = types.OrderByType().ToArray(); } protected override AdvancedDropdownItem BuildRoot () @@ -163,6 +164,6 @@ protected override void ItemSelected (AdvancedDropdownItem item) OnItemSelected?.Invoke(typePopupItem); } } - + } } \ No newline at end of file diff --git a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/SubclassSelectorDrawer.cs b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/SubclassSelectorDrawer.cs index d4b787f..2838416 100644 --- a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/SubclassSelectorDrawer.cs +++ b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/SubclassSelectorDrawer.cs @@ -207,13 +207,13 @@ private GUIContent GetTypeName (SerializedProperty property) typeName = typeMenu.GetTypeNameWithoutPath(); if (!string.IsNullOrWhiteSpace(typeName)) { - typeName = ObjectNames.NicifyVariableName(typeName); + typeName = TypeMenuUtility.CachedNicifyVariableName(typeName); } } if (string.IsNullOrWhiteSpace(typeName)) { - typeName = ObjectNames.NicifyVariableName(type.Name); + typeName = TypeMenuUtility.CachedNicifyVariableName(TypeMenuUtility.GetNiceGenericName(type)); } GUIContent result = new GUIContent(typeName); diff --git a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/TypeMenuUtility.cs b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/TypeMenuUtility.cs index 497ca62..11ea47f 100644 --- a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/TypeMenuUtility.cs +++ b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/TypeMenuUtility.cs @@ -10,33 +10,61 @@ public static class TypeMenuUtility public const string NullDisplayName = ""; + private static readonly Dictionary splittedTypePathCache = new Dictionary(); + private static readonly Dictionary attributeCache = new Dictionary(); + private static readonly Dictionary nicifyCache = new Dictionary(); + + public static AddTypeMenuAttribute GetAttribute (Type type) { - return Attribute.GetCustomAttribute(type, typeof(AddTypeMenuAttribute)) as AddTypeMenuAttribute; - } + if (type == null) + { + return null; + } + + if (attributeCache.TryGetValue(type, out AddTypeMenuAttribute cached)) + { + return cached; + } - public static string[] GetSplittedTypePath (Type type) + var result = Attribute.GetCustomAttribute(type, typeof(AddTypeMenuAttribute)) as AddTypeMenuAttribute; + attributeCache.Add(type, result); + return result; + } + + public static string[] GetSplittedTypePath(Type type) { + if (splittedTypePathCache.TryGetValue(type, out string[] cached)) + { + return cached; + } + + string[] result; + AddTypeMenuAttribute typeMenu = GetAttribute(type); if (typeMenu != null) { - return typeMenu.GetSplittedMenuName(); + result = typeMenu.GetSplittedMenuName(); } else { - int splitIndex = type.FullName.LastIndexOf('.'); + string fullName = GetNiceGenericFullName(type); + int splitIndex = fullName.LastIndexOf('.'); if (splitIndex >= 0) { - return new string[] { type.FullName.Substring(0, splitIndex), type.FullName.Substring(splitIndex + 1) }; + result = new string[] { fullName.Substring(0, splitIndex), fullName.Substring(splitIndex + 1) }; } else { - return new string[] { type.Name }; + result = new string[] { GetNiceGenericName(type) }; } } + + splittedTypePathCache.Add(type, result); + return result; } - public static IEnumerable OrderByType (this IEnumerable source) + public static IEnumerable OrderByType(this IEnumerable source) { return source.OrderBy(type => { @@ -44,6 +72,7 @@ public static IEnumerable OrderByType (this IEnumerable source) { return -999; } + return GetAttribute(type)?.Order ?? 0; }).ThenBy(type => { @@ -51,9 +80,57 @@ public static IEnumerable OrderByType (this IEnumerable source) { return null; } - return GetAttribute(type)?.MenuName ?? type.Name; + + return GetAttribute(type)?.MenuName ?? GetNiceGenericName(type); }); } + public static string GetNiceGenericName(Type type) + { + if (!type.IsGenericType) + { + return type.Name; + } + + string baseName = type.Name; + int backtickIndex = baseName.IndexOf('`'); + if (backtickIndex > 0) + { + baseName = baseName.Substring(0, backtickIndex); + } + + Type[] args = type.GetGenericArguments(); + string argsJoined = string.Join(", ", args.Select(a => GetNiceGenericName(a))); + return $"{baseName}<{argsJoined}>"; + } + + private static string GetNiceGenericFullName(Type type) + { + if (!type.IsGenericType) + { + return type.FullName ?? type.Name; + } + + string ns = type.Namespace; + string niceName = GetNiceGenericName(type); + return string.IsNullOrEmpty(ns) ? niceName : $"{ns}.{niceName}"; + } + + public static string CachedNicifyVariableName (string name) + { + if (string.IsNullOrEmpty(name)) + { + return name; + } + + if (nicifyCache.TryGetValue(name, out string cached)) + { + return cached; + } + + string result = ObjectNames.NicifyVariableName(name); + nicifyCache.Add(name, result); + return result; + } } } \ No newline at end of file diff --git a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/TypeSearch/IntrinsicTypePolicy/DefaultIntrinsicTypePolicy.cs b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/TypeSearch/IntrinsicTypePolicy/DefaultIntrinsicTypePolicy.cs index d7158ae..9ac49e0 100644 --- a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/TypeSearch/IntrinsicTypePolicy/DefaultIntrinsicTypePolicy.cs +++ b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/TypeSearch/IntrinsicTypePolicy/DefaultIntrinsicTypePolicy.cs @@ -12,7 +12,7 @@ public bool IsAllowed (Type candiateType) return (candiateType.IsPublic || candiateType.IsNestedPublic || candiateType.IsNestedPrivate) && !candiateType.IsAbstract && - !candiateType.IsGenericType && + !candiateType.ContainsGenericParameters && !candiateType.IsPrimitive && !candiateType.IsEnum && !typeof(UnityEngine.Object).IsAssignableFrom(candiateType) && diff --git a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/TypeSearch/TypeCandiateProvider/Unity_2023_2_OrNewer_TypeCandiateProvider.cs b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/TypeSearch/TypeCandiateProvider/Unity_2023_2_OrNewer_TypeCandiateProvider.cs index 4ed3c3c..6f48d0b 100644 --- a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/TypeSearch/TypeCandiateProvider/Unity_2023_2_OrNewer_TypeCandiateProvider.cs +++ b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/TypeSearch/TypeCandiateProvider/Unity_2023_2_OrNewer_TypeCandiateProvider.cs @@ -46,22 +46,32 @@ private IEnumerable GetTypesWithGeneric (Type baseType) result = new List(); - IEnumerable types = EnumerateAllTypesSafely(); - foreach (Type type in types) + // Prepare generic inference data upfront + bool baseIsConstructedGeneric = baseType.IsGenericType && !baseType.IsGenericTypeDefinition && !baseType.ContainsGenericParameters; + Type baseGenericDef = baseIsConstructedGeneric ? baseType.GetGenericTypeDefinition() : null; + Type[] baseTypeArgs = baseIsConstructedGeneric ? baseType.GetGenericArguments() : null; + + // Single pass over all types + foreach (Type type in EnumerateAllTypesSafely()) { - if (!intrinsicTypePolicy.IsAllowed(type)) + // Existing: check closed/non-generic candidates + if (intrinsicTypePolicy.IsAllowed(type) && typeCompatibilityPolicy.IsCompatible(baseType, type)) { + result.Add(type); continue; } - if (!typeCompatibilityPolicy.IsCompatible(baseType, type)) + + // New: try to close open generic candidates + if (baseIsConstructedGeneric && type.IsGenericTypeDefinition && Attribute.IsDefined(type, typeof(SerializableAttribute))) { - continue; + Type closedType = TryCloseGenericType(type, baseGenericDef, baseTypeArgs); + if (closedType != null && intrinsicTypePolicy.IsAllowed(closedType)) + { + result.Add(closedType); + } } - - result.Add(type); } - // Include the base type itself if allowed if (intrinsicTypePolicy.IsAllowed(baseType) && typeCompatibilityPolicy.IsCompatible(baseType, baseType)) { result.Add(baseType); @@ -99,6 +109,104 @@ private static IEnumerable EnumerateAllTypesSafely () } } } + private static Type TryCloseGenericType (Type openCandidateType, Type baseGenericDef, Type[] baseTypeArgs) + { + // openCandidateType is e.g. ConstantValueProvider + // baseGenericDef is e.g. IValueProvider<> + // baseTypeArgs is e.g. [int] + + Type[] candidateGenericParams = openCandidateType.GetGenericArguments(); + Type[] resolvedArgs = new Type[candidateGenericParams.Length]; + + // Walk the candidate's interfaces to find one matching the base generic definition + foreach (Type iface in openCandidateType.GetInterfaces()) + { + if (!iface.IsGenericType) continue; + if (iface.GetGenericTypeDefinition() != baseGenericDef) continue; + + Type[] ifaceArgs = iface.GetGenericArguments(); + if (ifaceArgs.Length != baseTypeArgs.Length) continue; + + if (TryMapTypeArguments(candidateGenericParams, ifaceArgs, baseTypeArgs, resolvedArgs)) + { + return TryMakeGenericTypeSafe(openCandidateType, resolvedArgs); + } + } + + // Walk base class chain + for (Type t = openCandidateType.BaseType; t != null && t != typeof(object); t = t.BaseType) + { + if (!t.IsGenericType) continue; + if (t.GetGenericTypeDefinition() != baseGenericDef) continue; + + Type[] tArgs = t.GetGenericArguments(); + if (tArgs.Length != baseTypeArgs.Length) continue; + + if (TryMapTypeArguments(candidateGenericParams, tArgs, baseTypeArgs, resolvedArgs)) + { + return TryMakeGenericTypeSafe(openCandidateType, resolvedArgs); + } + } + + return null; + } + + private static bool TryMapTypeArguments (Type[] candidateGenericParams, Type[] ifaceArgs, Type[] baseTypeArgs, Type[] resolvedArgs) + { + // Reset + for (int i = 0; i < resolvedArgs.Length; i++) + { + resolvedArgs[i] = null; + } + + // For each type argument in the interface/base, map it back to the candidate's generic parameter + // e.g. IValueProvider has ifaceArgs=[T], baseTypeArgs=[int] + // We need to find that T is candidateGenericParams[0], so resolvedArgs[0] = int + for (int i = 0; i < ifaceArgs.Length; i++) + { + Type ifaceArg = ifaceArgs[i]; + Type targetArg = baseTypeArgs[i]; + + if (ifaceArg.IsGenericParameter) + { + int position = ifaceArg.GenericParameterPosition; + if (position < 0 || position >= resolvedArgs.Length) return false; + + if (resolvedArgs[position] != null && resolvedArgs[position] != targetArg) + { + // Conflicting mapping for the same parameter + return false; + } + resolvedArgs[position] = targetArg; + } + else + { + // The interface argument is already concrete — it must match exactly + if (ifaceArg != targetArg) return false; + } + } + + // Check all parameters were resolved + for (int i = 0; i < resolvedArgs.Length; i++) + { + if (resolvedArgs[i] == null) return false; + } + + return true; + } + + private static Type TryMakeGenericTypeSafe (Type openType, Type[] typeArgs) + { + try + { + return openType.MakeGenericType(typeArgs); + } + catch (ArgumentException) + { + // Constraint violation (e.g. where T : struct but we passed a class) + return null; + } + } } } #endif diff --git a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/TypeSearch/TypeCandiateService.cs b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/TypeSearch/TypeCandiateService.cs index 31c63d6..4da968f 100644 --- a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/TypeSearch/TypeCandiateService.cs +++ b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/TypeSearch/TypeCandiateService.cs @@ -8,16 +8,11 @@ public sealed class TypeCandiateService { private readonly ITypeCandiateProvider typeCandiateProvider; - private readonly IIntrinsicTypePolicy intrinsicTypePolicy; - private readonly ITypeCompatibilityPolicy typeCompatibilityPolicy; - private readonly Dictionary typeCache = new Dictionary(); - public TypeCandiateService (ITypeCandiateProvider typeCandiateProvider, IIntrinsicTypePolicy intrinsicTypePolicy, ITypeCompatibilityPolicy typeCompatibilityPolicy) + public TypeCandiateService (ITypeCandiateProvider typeCandiateProvider) { this.typeCandiateProvider = typeCandiateProvider ?? throw new ArgumentNullException(nameof(typeCandiateProvider)); - this.intrinsicTypePolicy = intrinsicTypePolicy ?? throw new ArgumentNullException(nameof(intrinsicTypePolicy)); - this.typeCompatibilityPolicy = typeCompatibilityPolicy ?? throw new ArgumentNullException(nameof(typeCompatibilityPolicy)); } public IReadOnlyList GetDisplayableTypes (Type baseType) @@ -30,11 +25,8 @@ public IReadOnlyList GetDisplayableTypes (Type baseType) { return cachedTypes; } - - var candiateTypes = typeCandiateProvider.GetTypeCandidates(baseType); - var result = candiateTypes - .Where(intrinsicTypePolicy.IsAllowed) - .Where(t => typeCompatibilityPolicy.IsCompatible(baseType, t)) + + var result = typeCandiateProvider.GetTypeCandidates(baseType) .Distinct() .ToArray(); diff --git a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/TypeSearch/TypeSearchService.cs b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/TypeSearch/TypeSearchService.cs index 2e85c40..29dc730 100644 --- a/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/TypeSearch/TypeSearchService.cs +++ b/Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/TypeSearch/TypeSearchService.cs @@ -20,7 +20,7 @@ static TypeSearchService () TypeCandiateProvider = DefaultTypeCandiateProvider.Instance; #endif - TypeCandiateService = new TypeCandiateService(TypeCandiateProvider, IntrinsicTypePolicy, TypeCompatibilityPolicy); + TypeCandiateService = new TypeCandiateService(TypeCandiateProvider); } } } \ No newline at end of file