using System;
using System.Collections.Generic;
using System.Diagnostics;
using Uigenx.Protocol;
using UnityEditor;
using UnityEditor.Compilation;
namespace Uigenx.Editor
{
///
/// Collects compiler diagnostics for the whole compilation and reports one `compile.result`
/// when it finishes. Diagnostics arrive per assembly, so they are accumulated in between.
///
[InitializeOnLoad]
public static class UgxCompileReporter
{
/// Diagnostics kept per compilation. Beyond this only the count is reported.
public const int MaxDiagnostics = 200;
/// A compilation ends in a domain reload, so these two keys carry state across it.
const string RequestKey = "Uigenx.Editor.Compile.RequestId";
const string ResultKey = "Uigenx.Editor.Compile.PendingResult";
static readonly List Errors = new List(16);
static readonly List Warnings = new List(16);
static Stopwatch _stopwatch;
/// Raised on the main thread when a compilation finishes.
public static event Action Reported;
static UgxCompileReporter()
{
CompilationPipeline.compilationStarted += OnCompilationStarted;
CompilationPipeline.assemblyCompilationFinished += OnAssemblyFinished;
CompilationPipeline.compilationFinished += OnCompilationFinished;
}
/// Correlates the next `compile.result` with a `compile.request`.
public static void ExpectRequest(string requestId) => SessionState.SetString(RequestKey, requestId ?? string.Empty);
///
/// Takes the result of a compilation that finished while nothing was listening, which is
/// what happens when the bridge disconnects for the domain reload the compilation triggers.
///
public static bool TryTakePending(out UgxCompileResult result)
{
result = null;
var json = SessionState.GetString(ResultKey, string.Empty);
if (string.IsNullOrEmpty(json)) return false;
SessionState.EraseString(ResultKey);
return UgxJson.TryDeserialize(json, out result, out _) && result != null;
}
/// Reports the last known state without waiting for a compilation.
public static UgxCompileResult Snapshot(string requestId = null)
=> new UgxCompileResult
{
RequestId = requestId,
Ok = Errors.Count == 0 && !EditorUtility.scriptCompilationFailed,
Errors = new List(Errors),
Warnings = new List(Warnings),
DurationMs = 0,
};
static void OnCompilationStarted(object context)
{
Errors.Clear();
Warnings.Clear();
_stopwatch = Stopwatch.StartNew();
}
static void OnAssemblyFinished(string assemblyPath, CompilerMessage[] messages)
{
if (messages == null) return;
foreach (var message in messages)
{
var target = message.type == CompilerMessageType.Error ? Errors : Warnings;
if (message.type == CompilerMessageType.Info) continue;
if (target.Count >= MaxDiagnostics) continue;
target.Add(new UgxCompileDiagnostic
{
File = message.file ?? string.Empty,
Line = message.line,
Column = message.column,
Code = ExtractCode(message.message),
Message = message.message ?? string.Empty,
});
}
}
static void OnCompilationFinished(object context)
{
var requestId = SessionState.GetString(RequestKey, string.Empty);
var result = new UgxCompileResult
{
RequestId = string.IsNullOrEmpty(requestId) ? null : requestId,
Ok = Errors.Count == 0,
Errors = new List(Errors),
Warnings = new List(Warnings),
DurationMs = _stopwatch?.Elapsed.TotalMilliseconds ?? 0d,
};
SessionState.EraseString(RequestKey);
_stopwatch = null;
// The reload that follows this callback tears the bridge down, so the result is parked
// for the reconnect rather than dropped with the socket.
SessionState.SetString(ResultKey, UgxJson.Serialize(result));
Reported?.Invoke(result);
}
/// Pulls `CS0246` out of `error CS0246: The type or namespace ...`.
public static string ExtractCode(string message)
{
if (string.IsNullOrEmpty(message)) return string.Empty;
int cs = message.IndexOf("CS", StringComparison.Ordinal);
if (cs < 0) return string.Empty;
int end = cs + 2;
while (end < message.Length && char.IsDigit(message[end])) end++;
return end > cs + 2 ? message.Substring(cs, end - cs) : string.Empty;
}
}
}