Files
sbox-public/engine/Sandbox.Tools/Editor/StackTraceProperty.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

233 lines
5.4 KiB
C#

using System;
using System.Reflection;
using System.Text.RegularExpressions;
using JetBrains.Annotations;
namespace Editor;
/// <summary>
/// Marks a method as a custom handler for stack trace lines matching a certain pattern.
/// The method must take in a <see cref="Match"/> parameter, and return
/// a <see cref="StackRow"/> (or null).
/// </summary>
[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<StackLineHandlerInfo>
{
public int CompareTo( StackLineHandlerInfo other ) => Order - other.Order;
}
[SkipHotload]
private static List<StackLineHandlerInfo> StackLineHandlers { get; set; }
[EditorEvent.Hotload]
private static void OnHotload()
{
StackLineHandlers = null;
}
private static void UpdateStackLineHandlers()
{
if ( StackLineHandlers != null )
{
return;
}
StackLineHandlers = new List<StackLineHandlerInfo>();
var methods = EditorTypeLibrary.GetMethodsWithAttribute<StackLineHandlerAttribute>();
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 );
}
}
}