Files
2026-09-21 12:15:57 +03:00

367 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Reflex.Core;
using Reflex.Extensions;
using Reflex.Injectors;
using Reflex.Resolvers;
using UnityEditor;
using UnityEditor.IMGUI.Controls;
#if UNITY_6000_2_OR_NEWER
using TreeViewState = UnityEditor.IMGUI.Controls.TreeViewState<int>;
#endif
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Reflex.Editor.DebuggingWindow
{
public class ReflexDebuggerWindow : EditorWindow
{
private const string ContainerIcon = "PreMatCylinder"; // d_PrefabModel On Icon, "PreMatCylinder"
private const string ResolverIcon = "d_NetworkAnimator Icon"; // "d_eyeDropper.Large", "AnimatorStateTransition Icon", "RelativeJoint2D Icon"
private const string InstanceIcon = "d_Prefab Icon"; // "d_Prefab Icon", "d_Prefab On Icon"
[NonSerialized] private bool _isInitialized;
[SerializeField] private TreeViewState _treeViewState; // Serialized in the window layout file so it survives assembly reloading
[SerializeField] private MultiColumnHeaderState _multiColumnHeaderState;
private int _id = -1;
private SearchField _searchField;
private Vector2 _bindingStackTraceScrollPosition;
private MultiColumnTreeView TreeView { get; set; }
private Rect SearchBarRect => new Rect(20f, 10f, position.width - 40f, 20f);
private Rect MultiColumnTreeViewRect => new Rect(20, 30, position.width - 40, position.height - 200);
private void OnEnable()
{
SceneManager.sceneLoaded += OnSceneLoaded;
SceneManager.sceneUnloaded += OnSceneUnloaded;
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
}
private void OnDisable()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
SceneManager.sceneUnloaded -= OnSceneUnloaded;
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
}
private async void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
await Task.Yield();
Refresh();
}
private async void OnSceneUnloaded(Scene scene)
{
await Task.Yield();
Refresh();
}
private async void OnPlayModeStateChanged(PlayModeStateChange playModeStateChange)
{
await Task.Yield();
Refresh();
}
private void InitIfNeeded()
{
if (!_isInitialized)
{
// Check if it already exists (deserialized from window layout file or scriptable object)
if (_treeViewState == null)
_treeViewState = new TreeViewState();
bool firstInit = _multiColumnHeaderState == null;
var headerState = MultiColumnTreeView.CreateDefaultMultiColumnHeaderState();
if (MultiColumnHeaderState.CanOverwriteSerializedFields(_multiColumnHeaderState, headerState))
MultiColumnHeaderState.OverwriteSerializedFields(_multiColumnHeaderState, headerState);
_multiColumnHeaderState = headerState;
var multiColumnHeader = new MultiColumnHeader(headerState)
{
canSort = false,
height = MultiColumnHeader.DefaultGUI.minimumHeight
};
if (firstInit)
multiColumnHeader.ResizeToFit();
var treeModel = new TreeModel<MyTreeElement>(GetData());
TreeView = new MultiColumnTreeView(_treeViewState, multiColumnHeader, treeModel);
TreeView.ExpandAll();
_searchField = new SearchField();
_searchField.downOrUpArrowKeyPressed += TreeView.SetFocusAndEnsureSelectedItem;
_isInitialized = true;
}
}
private static List<(IResolver, Type[])> BuildMatrix(Container container)
{
var resolvers = container.ResolversByContract.Values
.SelectMany(r => r)
.ToHashSet();
if (ReflexEditorSettings.ShowInternalBindings == false)
{
if (container.TryGetResolver(typeof(Container), out var containerResolver))
{
resolvers.Remove(containerResolver);
}
}
if (ReflexEditorSettings.ShowInheritedBindings == false && container.Parent != null)
{
var parentResolvers = container.Parent.ResolversByContract.Values
.SelectMany(r => r)
.Distinct();
foreach (var parentResolver in parentResolvers)
{
resolvers.Remove(parentResolver);
}
}
return resolvers.Select(resolver => (resolver, GetContracts(resolver, container))).ToList();
}
private static Type[] GetContracts(IResolver resolver, Container container)
{
var result = new List<Type>();
foreach (var pair in container.ResolversByContract)
{
if (pair.Value.Contains(resolver))
{
result.Add(pair.Key);
}
}
return result.ToArray();
}
private void BuildDataRecursively(MyTreeElement parent, Container container)
{
if (container == null)
{
return;
}
var containerTreeElement = new MyTreeElement(container.Name, parent.Depth + 1, ++_id, ContainerIcon, () => string.Empty, Array.Empty<string>(), string.Empty, container.GetDebugProperties().BuildCallsite, kind: string.Empty);
parent.Children.Add(containerTreeElement);
containerTreeElement.Parent = parent;
foreach (var pair in BuildMatrix(container))
{
var resolverTreeElement = new MyTreeElement(
string.Join(", ", pair.Item2.Select(x => x.GetName())), // In this case Name is not used for rendering, but for searching
containerTreeElement.Depth + 1,
++_id,
ResolverIcon,
() => pair.Item1.GetDebugProperties().Resolutions.ToString(),
pair.Item2.Select(x => x.GetName()).OrderBy(x => x).ToArray(),
pair.Item1.Lifetime.ToString(),
pair.Item1.GetDebugProperties().BindingCallsite,
kind: pair.Item1.GetType().Name.Replace("Singleton", string.Empty).Replace("Transient", string.Empty).Replace("Scoped", string.Empty).Replace("Resolver", string.Empty)
);
foreach (var (instance, callsite) in pair.Item1.GetDebugProperties().Instances.Where(tuple => tuple.Item1.IsAlive).Select(tuple => (tuple.Item1.Target, tuple.Item2)))
{
var instanceTreeElement = new MyTreeElement(
$"{instance.GetType().GetName()} <color=#3D99ED>({SHA1.ShortHash(instance.GetHashCode())})</color>",
resolverTreeElement.Depth + 1,
++_id,
InstanceIcon,
() => string.Empty,
Array.Empty<string>(),
string.Empty,
callsite,
string.Empty
);
instanceTreeElement.SetParent(resolverTreeElement);
}
resolverTreeElement.SetParent(containerTreeElement);
}
foreach (var scopedContainer in container.Children)
{
BuildDataRecursively(containerTreeElement, scopedContainer);
}
}
private IList<MyTreeElement> GetData()
{
var root = new MyTreeElement("Root", -1, ++_id, ContainerIcon, () => string.Empty, Array.Empty<string>(), string.Empty, null, string.Empty);
foreach (var rootContainer in Container.RootContainers)
{
BuildDataRecursively(root, rootContainer);
}
var list = new List<MyTreeElement>();
TreeElementUtility.TreeToList(root, list);
return list;
}
private void OnGUI()
{
Repaint();
InitIfNeeded();
if (UnityScriptingDefineSymbols.IsDefined("REFLEX_DEBUG"))
{
PresentDebuggerEnabled();
}
else
{
PresentDebuggerDisabled();
}
GUILayout.FlexibleSpace();
PresentStatusBar();
}
private void Refresh(PlayModeStateChange _ = default)
{
_isInitialized = false;
InitIfNeeded();
}
private void SearchBar(Rect rect)
{
TreeView.searchString = _searchField.OnGUI(rect, TreeView.searchString);
GUILayoutUtility.GetRect(rect.width, rect.height);
}
private void DoTreeView(Rect rect)
{
TreeView.OnGUI(rect);
GUILayoutUtility.GetRect(rect.width, rect.height);
}
private static void PresentDebuggerDisabled()
{
GUILayout.FlexibleSpace();
GUILayout.Label("To start debugging, enable the Reflex Debug Mode in the status bar.", Styles.LabelHorizontallyCentered);
GUILayout.Label("Keep in mind that enabling Reflex Debug Mode will impact performance.", Styles.LabelHorizontallyCentered);
}
private void PresentDebuggerEnabled()
{
SearchBar(SearchBarRect);
DoTreeView(MultiColumnTreeViewRect);
GUILayout.Space(16);
using (new GUILayout.HorizontalScope())
{
GUILayout.Space(16);
using (new GUILayout.VerticalScope())
{
_bindingStackTraceScrollPosition = GUILayout.BeginScrollView(_bindingStackTraceScrollPosition);
PresentCallSite();
GUILayout.EndScrollView();
GUILayout.Space(16);
}
GUILayout.Space(16);
}
}
private void PresentHierarchyFilters()
{
EditorGUI.BeginChangeCheck();
ReflexEditorSettings.ShowInternalBindings = GUILayout.Toggle(ReflexEditorSettings.ShowInternalBindings, "Show Internal Bindings ");
ReflexEditorSettings.ShowInheritedBindings = GUILayout.Toggle(ReflexEditorSettings.ShowInheritedBindings, "Show Inherited Bindings ");
if (EditorGUI.EndChangeCheck())
{
Refresh();
}
}
private void PresentStatusBar()
{
using (new EditorGUILayout.HorizontalScope(Styles.AppToolbar))
{
GUILayout.FlexibleSpace();
PresentHierarchyFilters();
var refreshIcon = EditorGUIUtility.IconContent("d_TreeEditor.Refresh");
refreshIcon.tooltip = "Forces Tree View to Refresh";
if (GUILayout.Button(refreshIcon, Styles.StatusBarIcon, GUILayout.Width(25)))
{
Refresh();
}
var debuggerIcon = UnityScriptingDefineSymbols.IsDefined("REFLEX_DEBUG")
? EditorGUIUtility.IconContent("d_DebuggerEnabled")
: EditorGUIUtility.IconContent("d_DebuggerDisabled");
debuggerIcon.tooltip = UnityScriptingDefineSymbols.IsDefined("REFLEX_DEBUG")
? "Reflex Debugger Enabled"
: "Reflex Debugger Disabled";
if (GUILayout.Button(debuggerIcon, Styles.StatusBarIcon, GUILayout.Width(25)))
{
UnityScriptingDefineSymbols.Toggle("REFLEX_DEBUG", EditorUserBuildSettings.selectedBuildTargetGroup);
}
}
}
private void PresentCallSite()
{
var selection = TreeView.GetSelection();
if (selection == null || selection.Count == 0)
{
return;
}
var item = TreeView.Find(selection.Single());
if (item == null || item.Callsite == null)
{
return;
}
foreach (var callSite in item.Callsite)
{
PresentStackFrame(callSite.ClassName, callSite.FunctionName, callSite.Path, callSite.Line);
}
}
private static void PresentStackFrame(string className, string functionName, string path, int line)
{
using (new GUILayout.HorizontalScope())
{
GUILayout.Label($"{className}:{functionName}() →", Styles.StackTrace);
if (PresentLinkButton($"{path}:{line}"))
{
UnityEditorInternal.InternalEditorUtility.OpenFileAtLineExternal(path, line);
}
}
}
private static bool PresentLinkButton(string label, params GUILayoutOption[] options)
{
var position = GUILayoutUtility.GetRect(new GUIContent(label), Styles.Hyperlink, options);
position.y -= 3;
Handles.color = Styles.Hyperlink.normal.textColor;
Handles.DrawLine(new Vector3(position.xMin + (float)EditorStyles.linkLabel.padding.left, position.yMax), new Vector3(position.xMax - (float)EditorStyles.linkLabel.padding.right, position.yMax));
Handles.color = Color.white;
EditorGUIUtility.AddCursorRect(position, MouseCursor.Link);
return GUI.Button(position, label, Styles.Hyperlink);
}
}
}