using Microsoft.CodeAnalysis; namespace Editor; internal class CodeCompileNotice : NoticeWidget { CompileGroup CompilerGroup; public CodeCompileNotice() { } public CodeCompileNotice( CompileGroup compiler ) { Icon = "build_circle"; Position = 10; CompilerGroup = compiler; Reset(); } protected override Vector2 SizeHint() { return 1000; } /// /// Called when it's about to be re-used by a new compiler /// public override void Reset() { base.Reset(); IsRunning = true; Tick(); SetBodyWidget( null ); FixedWidth = 300; FixedHeight = 76; Title = $"Compile '{CompilerGroup.Name}'"; BorderColor = Theme.Primary; isErrored = false; } bool isErrored = false; private readonly List files = []; public override bool WantsVisible { get { if ( EditorPreferences.CompileNotifications == EditorPreferences.NotificationLevel.ShowAlways ) return true; if ( EditorPreferences.CompileNotifications == EditorPreferences.NotificationLevel.ShowOnError ) return isErrored; return false; } } public override void Tick() { if ( !IsRunning ) return; var buildCount = CompilerGroup.Compilers.Count( x => x.IsBuilding ); var finishCount = CompilerGroup.Compilers.Count( x => !x.IsBuilding ); var warnings = CompilerGroup.Compilers.Where( x => x.Diagnostics != null ) .SelectMany( x => x.Diagnostics ) .Where( x => x.Severity >= DiagnosticSeverity.Warning ) .Count(); IsRunning = CompilerGroup.IsBuilding; Subtitle = $"{buildCount} pending, {finishCount} completed"; if ( warnings > 0 ) Subtitle += $", {warnings} warnings"; if ( IsRunning ) return; bool success = CompilerGroup.Compilers.All( x => x.BuildSuccess ); files.Clear(); if ( success ) { isErrored = false; BorderColor = Theme.Green; NoticeManager.Remove( this, 1 ); if ( EditorPreferences.NotificationSounds ) EditorUtility.PlayRawSound( "sounds/editor/success.wav" ); } else { isErrored = true; BorderColor = Theme.Red; Subtitle = ""; NoticeManager.Remove( this, EditorPreferences.ErrorNotificationTimeout ); AddDiagnostics(); if ( EditorPreferences.NotificationSounds ) EditorUtility.PlayRawSound( "sounds/editor/fail.wav" ); } } protected override void OnPaint() { if ( !EditorPreferences.NotificationPopups ) return; base.OnPaint(); } protected override void OnMousePress( MouseEvent e ) { base.OnMousePress( e ); if ( e.LeftMouseButton && e.HasShift ) { foreach ( var file in files ) { CodeEditor.OpenFile( file ); } e.Accepted = true; return; } } private void AddDiagnostics() { var diagnostics = CompilerGroup.Compilers .Where( x => x.Diagnostics != null ) .SelectMany( x => x.Diagnostics ) .Where( x => x.Severity >= DiagnosticSeverity.Error ) .OrderByDescending( x => (int)x.Severity ) .Take( 5 ) .ToArray(); if ( diagnostics.Length == 0 ) return; var bodyWidget = new Widget( this ); bodyWidget.Layout = Layout.Column(); foreach ( var diag in diagnostics ) { var btn = bodyWidget.Layout.Add( new DiagnosticWidget( diag ) ); files.Add( btn.FilePath ); } SetBodyWidget( bodyWidget ); } [Event( "compile.started" )] public static void OnCompileStarted( CompileGroup compiler ) { // find an old notice to replace var notice = NoticeManager.All.OfType().FirstOrDefault( x => x.CompilerGroup.Name == compiler.Name ); if ( !notice.IsValid() ) notice = new CodeCompileNotice( compiler ); notice.CompilerGroup = compiler; notice.Reset(); } } internal class DiagnosticWidget : Widget { private Diagnostic diag; public string FilePath { get; private set; } public int LineNumber { get; private set; } public int CharNumber { get; private set; } public string Message { get; private set; } public DiagnosticWidget( Diagnostic diag ) : base( null ) { this.diag = diag; FixedHeight = 18; MinimumWidth = 400; Cursor = CursorShape.Finger; ToolTip = $"{diag.GetMessage()}\n{diag.Location}"; var span = diag.Location.GetLineSpan(); var mappedSpan = diag.Location.GetMappedLineSpan(); Message = diag.GetMessage(); // Path can be null if the spans are not valid (not related to a file) FilePath = mappedSpan.HasMappedPath ? mappedSpan.Path : span.Path; LineNumber = mappedSpan.Span.Start.Line + 1; CharNumber = mappedSpan.Span.Start.Character + 1; } protected override void DoLayout() { base.DoLayout(); } protected override void OnPaint() { base.OnPaint(); Paint.Antialiasing = true; Paint.TextAntialiasing = true; var color = Theme.Red; if ( diag.Severity == DiagnosticSeverity.Warning ) color = Theme.Yellow; var bareFilename = System.IO.Path.GetFileName( FilePath ); var rect = LocalRect; var textColor = Color.Lerp( Color.White, color, 0.5f ); if ( IsUnderMouse ) { Paint.ClearPen(); Paint.SetBrush( color.WithAlpha( 0.1f ) ); Paint.DrawRect( rect ); } var measureText = $"{diag.Id}: {Message} {bareFilename}:{LineNumber}"; var size = Paint.MeasureText( new Rect( 0, 1000 ), measureText, TextFlag.LeftCenter ); rect = rect.Shrink( 4, 0 ); Paint.SetPen( textColor.WithAlpha( 0.8f ) ); var fileRect = Paint.DrawText( rect, $"{bareFilename}:{LineNumber}", TextFlag.RightCenter ); rect.Right -= fileRect.Width; rect.Right -= 8; Paint.SetPen( textColor.WithAlpha( 0.5f ) ); Paint.DrawText( rect.Shrink( 16, 0, 0, 0 ), $"{diag.Id}: {Message}", TextFlag.LeftCenter ); Paint.ClearPen(); Paint.SetBrush( color ); Paint.DrawCircle( new Rect( 0, 16 ).Shrink( 2 ) ); } protected override void OnMouseClick( MouseEvent e ) { if ( e.LeftMouseButton ) { // Can be null if our diagnostic spans are not valid if ( FilePath != null ) { CodeEditor.OpenFile( FilePath, LineNumber, CharNumber ); } e.Accepted = true; return; } base.OnMouseClick( e ); } }