using System; using System.Reflection; using System.Text.RegularExpressions; using JetBrains.Annotations; namespace Editor; /// /// Marks a method as a custom handler for stack trace lines matching a certain pattern. /// The method must take in a parameter, and return /// a (or null). /// [AttributeUsage( AttributeTargets.Method )] public sealed class StackLineHandlerAttribute : Attribute { [RegexPattern] public string Regex { get; } public int Order { get; set; } public StackLineHandlerAttribute( [RegexPattern] string regex ) { Regex = regex; } } internal class StackTraceProperty : Widget { private delegate StackRow StackLineHandler( Match match ); private record struct StackLineHandlerInfo ( Regex Regex, StackLineHandler Handler, int Order ) : IComparable { public int CompareTo( StackLineHandlerInfo other ) => Order - other.Order; } [SkipHotload] private static List StackLineHandlers { get; set; } [EditorEvent.Hotload] private static void OnHotload() { StackLineHandlers = null; } private static void UpdateStackLineHandlers() { if ( StackLineHandlers != null ) { return; } StackLineHandlers = new List(); var methods = EditorTypeLibrary.GetMethodsWithAttribute(); foreach ( var (method, attrib) in methods ) { if ( method.Parameters.Length != 1 ) { continue; } try { var regex = new Regex( attrib.Regex ); var handler = (StackLineHandler)Delegate.CreateDelegate( typeof( StackLineHandler ), (MethodInfo)method.MemberInfo ); StackLineHandlers.Add( new( regex, handler, attrib.Order ) ); } catch ( Exception e ) { Log.Warning( $"{method.MemberInfo.ToSimpleString()}: {e}" ); } } StackLineHandlers.Sort(); } public StackTraceProperty( Widget parent, LogEvent e ) : base( parent ) { Layout = Layout.Column(); MinimumWidth = 400; var scroller = new ScrollArea( this ); Layout.Add( scroller, 1 ); scroller.Canvas = new Widget( scroller ); scroller.Canvas.Layout = Layout.Column(); var row = scroller.Canvas.Layout.AddRow(); var message = row.Add( new Label( e.Message, this ), 1 ); message.Margin = 8; message.WordWrap = true; message.Cursor = CursorShape.Finger; message.Name = "Message"; message.ToolTip = "Copy this message to your clipboard"; message.MouseClick = () => { var message = e.Message; message += "\n"; message += e.Stack; EditorUtility.Clipboard.Copy( message ); }; var lines = (e.Stack ?? "").Split( '\n', '\r' ); var index = 0; foreach ( var line in lines ) { if ( AddStackLine( line, scroller.Canvas.Layout ) is { } addedRow ) { addedRow.Index = index++; } } scroller.Canvas.Layout.AddStretchCell(); } [StackLineHandler( @"^at (.+?)( in (.+):line (.+))?$", Order = 1000 )] public static StackRow DefaultStackLineHandler( Match match ) { var hasFile = match.Groups[3].Success; var isGenerated = match.Groups[3].Value.StartsWith( "Sandbox.Generator." ); var functionName = match.Groups[1].Value; if ( functionName.IndexOf( '(' ) > 0 ) functionName = functionName.Substring( 0, functionName.IndexOf( '(' ) ); if ( !hasFile ) { return UnknownStackLineHandler( match ); } var fileName = match.Groups[3].Value; var fileLine = match.Groups[4].Value; var row = new StackRow( functionName, $"{fileName}:{fileLine}" ) { IsFromEngine = match.Value.Contains( "\\engine\\Sandbox." ) }; row.MouseClick += () => CodeEditor.OpenFile( fileName, fileLine.ToInt() ); return row; } [StackLineHandler( @".+", Order = 1001 )] public static StackRow UnknownStackLineHandler( Match match ) { return new StackRow( match.Value, null ) { IsFromEngine = match.Value.Contains( "\\engine\\Sandbox." ) }; } private StackRow AddStackLine( string line, Layout target ) { if ( string.IsNullOrWhiteSpace( line ) ) return null; UpdateStackLineHandlers(); line = line.Trim(); foreach ( var (regex, handler, _) in StackLineHandlers ) { var match = regex.Match( line ); if ( !match.Success ) continue; if ( handler( match ) is not { } row ) return null; target.Add( row ); return row; } return null; } } public class StackRow : Frame { string FunctionName; string FileName; public bool IsFromEngine; public bool IsFromAction; public int Index; public StackRow( string functionName, string fileName ) : base( null ) { FunctionName = functionName; FileName = fileName; MinimumSize = FileName != null ? 48 : 24; Cursor = CursorShape.Finger; } protected override void OnPaint() { var r = new Rect( 0, Size ); var c = IsFromEngine ? Theme.Blue : IsFromAction ? Theme.Yellow : Theme.Green; if ( FileName == null ) c = Theme.Blue.Darken( 0.3f ).Desaturate( 0.5f ); if ( IsUnderMouse ) c = c.Lighten( 0.3f ); bool hasFileName = FileName != null; // Push message rect up a bit if ( hasFileName ) r.Top = r.Top - 16.0f; Paint.ClearPen(); Paint.SetBrush( c.Darken( 0.8f + (Index & 1) * 0.02f ).Desaturate( 0.3f ) ); Paint.DrawRect( r ); r = r.Grow( -8.0f, 0 ); Paint.SetPen( c ); var textSize = Paint.DrawText( r, FunctionName, TextFlag.LeftCenter ); if ( hasFileName ) { r.Top = textSize.Bottom - 4; r = r.Shrink( 0, 0, 0, 8 ); Paint.SetPen( c.Darken( 0.3f ) ); Paint.DrawText( r, FileName, TextFlag.LeftBottom ); } } }