From 679eb75b9839f7f36602b564eb6244d0a1f1828b Mon Sep 17 00:00:00 2001 From: C Date: Mon, 17 Nov 2025 22:51:38 +0200 Subject: [PATCH 1/2] Add stack trace display options - How the namespace/method name is displayed. - How the method parameters are displayed. - How the file/line link is displayed. --- package/Editor/Modules/SyntaxHighlighting.cs | 23 +++++++- package/Editor/NeedleConsole.cs | 53 ++++++++++++++++++- .../Editor/Settings/NeedleConsoleSettings.cs | 25 +++++++++ .../Settings/NeedleConsoleSettingsProvider.cs | 24 ++++++--- 4 files changed, 116 insertions(+), 9 deletions(-) diff --git a/package/Editor/Modules/SyntaxHighlighting.cs b/package/Editor/Modules/SyntaxHighlighting.cs index 0dfb19f..64bb9ba 100644 --- a/package/Editor/Modules/SyntaxHighlighting.cs +++ b/package/Editor/Modules/SyntaxHighlighting.cs @@ -204,7 +204,28 @@ string Eval(Match m) if (colorDict.TryGetValue("link", out var col)) { var displayUrl = link.Value.Substring(linkPrefix.Length).TrimEnd(')'); - line += $" in {displayUrl}"; + + // Trim the preceding path from the filename. + if (NeedleConsoleSettings.instance.StacktraceFilenameMode == NeedleConsoleSettings.StacktraceFilename.Compact) + { + var lastSlashIndex = displayUrl.LastIndexOf("/", StringComparison.Ordinal); + if (lastSlashIndex != -1) + { + displayUrl = displayUrl[(lastSlashIndex + 1)..]; + } + } + + var lineIndex = link.Groups["line"].Value; + line = NeedleConsoleSettings.instance.StacktraceFilenameMode switch + { + NeedleConsoleSettings.StacktraceFilename.Full + => $"{line} in {displayUrl}", + NeedleConsoleSettings.StacktraceFilename.Compact + => $"{line} {displayUrl}", + NeedleConsoleSettings.StacktraceFilename.Inline + => $"{line}:{lineIndex}", + _ => throw new ArgumentOutOfRangeException(), + }; } else line += link.Value; diff --git a/package/Editor/NeedleConsole.cs b/package/Editor/NeedleConsole.cs index 5a36442..2060099 100644 --- a/package/Editor/NeedleConsole.cs +++ b/package/Editor/NeedleConsole.cs @@ -1,5 +1,6 @@ -using System.IO; +using System; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Unity.Profiling; using UnityEditor; @@ -10,6 +11,13 @@ namespace Needle.Console { public static class NeedleConsole { + private static readonly Regex namespaceCompactNoReturnTypeRegex = new Regex(@"(.+?)\(", RegexOptions.Compiled); + private static readonly Regex namespaceCompactRegex = new Regex(@" ([^ ]+?)\(", RegexOptions.Compiled); + private static readonly Regex paramsRegex = new Regex(@"\((?!at)(.*?)\)", RegexOptions.Compiled); + private static readonly Regex paramsArgumentRegex = new Regex(@"([ (])([^),]+?) (.+?)([\),])", RegexOptions.Compiled); + private static readonly Regex paramsRefRegex = new Regex(@"\b ?ref ?\b", RegexOptions.Compiled); + private static readonly MatchEvaluator namespaceReplacer = NamespaceReplacer; + private static readonly MatchEvaluator paramReplacer = ParamReplacer; [HyperlinkCallback(Href = "OpenNeedleConsoleSettings")] private static void OpenNeedleConsoleUserPreferences() @@ -120,6 +128,20 @@ public static void Apply(ref string stacktrace) } } + if (foundPrefix) + { + if (settings.StacktraceNamespaceMode == NeedleConsoleSettings.StacktraceNamespace.Compact) + line = namespaceCompactRegex.Replace(line, namespaceReplacer); + else if (settings.StacktraceNamespaceMode == NeedleConsoleSettings.StacktraceNamespace.CompactNoReturnType) + line = namespaceCompactNoReturnTypeRegex.Replace(line, namespaceReplacer); + + if (settings.StacktraceParamsMode != NeedleConsoleSettings.StacktraceParams.Full) + { + line = paramsRefRegex.Replace(line, ""); + line = paramsRegex.Replace(line, paramReplacer); + } + } + if (foundPrefix && settings.UseSyntaxHighlighting) SyntaxHighlighting.AddSyntaxHighlighting(ref line); @@ -152,5 +174,34 @@ public static void Apply(ref string stacktrace) // ignore } } + + private static string NamespaceReplacer(Match match) + { + var lastDotIndex = match.Value.LastIndexOf(".", StringComparison.Ordinal); + if (lastDotIndex == -1) + return match.Value; + + // Remove everything but the last two parts leaving only "Class.Method". + var secondLastDotIndex = match.Value.LastIndexOf(".", lastDotIndex - 1, StringComparison.Ordinal); + var result = secondLastDotIndex != -1 + ? match.Value[(secondLastDotIndex + 1)..] + : match.Value[(lastDotIndex + 1)..]; + + var plusIndex = result.LastIndexOf("+", StringComparison.Ordinal); + return plusIndex != -1 + ? " " + result[(plusIndex + 1)..] + : " " + result; + } + + private static string ParamReplacer(Match match) + { + return NeedleConsoleSettings.instance.StacktraceParamsMode switch + { + NeedleConsoleSettings.StacktraceParams.TypesOnly => paramsArgumentRegex.Replace(match.Value, "$1$2$4"), + NeedleConsoleSettings.StacktraceParams.NamesOnly => paramsArgumentRegex.Replace(match.Value, "$1$3$4"), + NeedleConsoleSettings.StacktraceParams.Compact => "()", + _ => throw new ArgumentOutOfRangeException(), + }; + } } } diff --git a/package/Editor/Settings/NeedleConsoleSettings.cs b/package/Editor/Settings/NeedleConsoleSettings.cs index c9976d0..67cca34 100644 --- a/package/Editor/Settings/NeedleConsoleSettings.cs +++ b/package/Editor/Settings/NeedleConsoleSettings.cs @@ -18,6 +18,28 @@ internal enum StacktraceOrientations Horizontal, Auto, } + [Serializable, Flags] + internal enum StacktraceNamespace + { + Full, + Compact, + CompactNoReturnType, + } + [Serializable, Flags] + internal enum StacktraceParams + { + Full, + TypesOnly, + NamesOnly, + Compact, + } + [Serializable] + internal enum StacktraceFilename + { + Full, + Compact, + Inline, + } [SerializeField] internal bool Enabled = true; @@ -129,6 +151,9 @@ public void UpdateCurrentTheme() public Font CustomLogEntryFont; public StacktraceOrientations StacktraceOrientation = StacktraceOrientations.Vertical; + public StacktraceNamespace StacktraceNamespaceMode = StacktraceNamespace.Full; + public StacktraceParams StacktraceParamsMode = StacktraceParams.Full; + public StacktraceFilename StacktraceFilenameMode = StacktraceFilename.Full; public float StacktraceOrientationAutoHeight = 300; public bool UseStacktraceIgnoreFilters = true; public string[] StacktraceIgnoreFilters = new string[] { }; diff --git a/package/Editor/Settings/NeedleConsoleSettingsProvider.cs b/package/Editor/Settings/NeedleConsoleSettingsProvider.cs index 0b22794..a30ad3f 100644 --- a/package/Editor/Settings/NeedleConsoleSettingsProvider.cs +++ b/package/Editor/Settings/NeedleConsoleSettingsProvider.cs @@ -137,13 +137,23 @@ public override void OnGUI(string searchContext) if (_scope.changed) NeedleConsoleProjectSettings.RaiseColorsChangedEvent(); } - settings.StacktraceOrientation = (NeedleConsoleSettings.StacktraceOrientations)EditorGUILayout.EnumPopup("Stacktrace Orientation", settings.StacktraceOrientation); - using (new EditorGUI.DisabledScope(settings.StacktraceOrientation != NeedleConsoleSettings.StacktraceOrientations.Auto)) - { - var windowHeight = Math.Max(300, Screen.height); - EditorGUI.indentLevel++; - settings.StacktraceOrientationAutoHeight = EditorGUILayout.IntSlider(new GUIContent("Auto Height (in pixel)", "The height at which the stacktrace orientation will switch from Vertical to Horizontal when Stacktrace Orientation is set to Auto."), (int)settings.StacktraceOrientationAutoHeight, 100, windowHeight); - EditorGUI.indentLevel--; + GUILayout.Space(10); + EditorGUILayout.LabelField("Experimental > Stacktrace", EditorStyles.boldLabel); + using (var _scope = new EditorGUI.ChangeCheckScope()) { + settings.StacktraceOrientation = (NeedleConsoleSettings.StacktraceOrientations)EditorGUILayout.EnumPopup("Orientation", settings.StacktraceOrientation); + using (new EditorGUI.DisabledScope(settings.StacktraceOrientation != NeedleConsoleSettings.StacktraceOrientations.Auto)) + { + var windowHeight = Math.Max(300, Screen.height); + EditorGUI.indentLevel++; + settings.StacktraceOrientationAutoHeight = EditorGUILayout.IntSlider(new GUIContent("Auto Height (in pixel)", "The height at which the stacktrace orientation will switch from Vertical to Horizontal when Stacktrace Orientation is set to Auto."), (int)settings.StacktraceOrientationAutoHeight, 100, windowHeight); + EditorGUI.indentLevel--; + } + + settings.StacktraceNamespaceMode = (NeedleConsoleSettings.StacktraceNamespace)EditorGUILayout.EnumPopup("Namespace", settings.StacktraceNamespaceMode); + settings.StacktraceParamsMode = (NeedleConsoleSettings.StacktraceParams)EditorGUILayout.EnumPopup("Parameters", settings.StacktraceParamsMode); + settings.StacktraceFilenameMode = (NeedleConsoleSettings.StacktraceFilename)EditorGUILayout.EnumPopup("Filename", settings.StacktraceFilenameMode); + + if (_scope.changed) ThemeEditedOrChanged?.Invoke(); } // using (new GUILayout.HorizontalScope()) From cb68ae6a6ffbb07d210708e6ace44234eddba848 Mon Sep 17 00:00:00 2001 From: C Date: Sun, 23 Nov 2025 19:43:24 +0200 Subject: [PATCH 2/2] Fix namespace simplification This should now also handle generic types inside "<" and ">". --- package/Editor/NeedleConsole.cs | 71 +++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 16 deletions(-) diff --git a/package/Editor/NeedleConsole.cs b/package/Editor/NeedleConsole.cs index 2060099..b9fcc58 100644 --- a/package/Editor/NeedleConsole.cs +++ b/package/Editor/NeedleConsole.cs @@ -11,14 +11,22 @@ namespace Needle.Console { public static class NeedleConsole { - private static readonly Regex namespaceCompactNoReturnTypeRegex = new Regex(@"(.+?)\(", RegexOptions.Compiled); - private static readonly Regex namespaceCompactRegex = new Regex(@" ([^ ]+?)\(", RegexOptions.Compiled); + /// + /// Matches namespaces - "XX.YY.ZZ" is split into the groups "XX.YY" and "ZZ".
+ /// Also handles generics inside "<" and ">". + ///
+ private static readonly Regex namespaceCompactRegex = new Regex(@"([^<>.\s]+(?:\.[^<>.\s]+)*)(\.)([^<>.(]+)", RegexOptions.Compiled); private static readonly Regex paramsRegex = new Regex(@"\((?!at)(.*?)\)", RegexOptions.Compiled); private static readonly Regex paramsArgumentRegex = new Regex(@"([ (])([^),]+?) (.+?)([\),])", RegexOptions.Compiled); private static readonly Regex paramsRefRegex = new Regex(@"\b ?ref ?\b", RegexOptions.Compiled); private static readonly MatchEvaluator namespaceReplacer = NamespaceReplacer; private static readonly MatchEvaluator paramReplacer = ParamReplacer; + private static int namespaceLinkStartIndex; + private static int namespaceMatchIndex; + private static string namespaceStored; + private static int namespaceStartIndex; + [HyperlinkCallback(Href = "OpenNeedleConsoleSettings")] private static void OpenNeedleConsoleUserPreferences() { @@ -130,10 +138,39 @@ public static void Apply(ref string stacktrace) if (foundPrefix) { - if (settings.StacktraceNamespaceMode == NeedleConsoleSettings.StacktraceNamespace.Compact) + if ( + settings.StacktraceNamespaceMode == NeedleConsoleSettings.StacktraceNamespace.Compact || + settings.StacktraceNamespaceMode == NeedleConsoleSettings.StacktraceNamespace.CompactNoReturnType) + { + namespaceLinkStartIndex = line.IndexOf(" (at ", StringComparison.Ordinal); + namespaceMatchIndex = 0; + namespaceStartIndex = -1; + namespaceStored = ""; + line = namespaceCompactRegex.Replace(line, namespaceReplacer); - else if (settings.StacktraceNamespaceMode == NeedleConsoleSettings.StacktraceNamespace.CompactNoReturnType) - line = namespaceCompactNoReturnTypeRegex.Replace(line, namespaceReplacer); + + // If the class name was stripped out along with the namespaces, add + // it back in here. + int methodIndexEndIndex = line.IndexOf('('); + if (methodIndexEndIndex != -1 && line.IndexOf('.', 0, methodIndexEndIndex) == -1) + { + int classIndex = namespaceStored.LastIndexOf('.'); + if (classIndex != -1) + { + namespaceStored = namespaceStored[(classIndex + 1)..]; + } + line = line.Insert(namespaceStartIndex, $"{namespaceStored}."); + } + + // Remove the return type. + if (settings.StacktraceNamespaceMode == NeedleConsoleSettings.StacktraceNamespace.CompactNoReturnType) + { + if (namespaceStartIndex != -1) + { + line = line[namespaceStartIndex..]; + } + } + } if (settings.StacktraceParamsMode != NeedleConsoleSettings.StacktraceParams.Full) { @@ -177,20 +214,22 @@ public static void Apply(ref string stacktrace) private static string NamespaceReplacer(Match match) { - var lastDotIndex = match.Value.LastIndexOf(".", StringComparison.Ordinal); - if (lastDotIndex == -1) + namespaceMatchIndex++; + + // Prevent replacing stuff in the filename link. + if (namespaceLinkStartIndex != -1 && match.Index >= namespaceLinkStartIndex) return match.Value; - // Remove everything but the last two parts leaving only "Class.Method". - var secondLastDotIndex = match.Value.LastIndexOf(".", lastDotIndex - 1, StringComparison.Ordinal); - var result = secondLastDotIndex != -1 - ? match.Value[(secondLastDotIndex + 1)..] - : match.Value[(lastDotIndex + 1)..]; + // At this point there's no wa to differentiate between a generic type parameter and "Class.Method", + // causing the class name to be stripped which we don't want. + // Store this so that the class name can be added back at the start after all replacements have happened. + if (namespaceMatchIndex == 1) + { + namespaceStartIndex = match.Index; + namespaceStored = match.Groups[1].Value; + } - var plusIndex = result.LastIndexOf("+", StringComparison.Ordinal); - return plusIndex != -1 - ? " " + result[(plusIndex + 1)..] - : " " + result; + return match.Groups[3].Value; } private static string ParamReplacer(Match match)