#if UNITY_EDITOR using System; using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEditor.Compilation; using UnityEngine; namespace Dianoetic.LiveSync { [Serializable] class SyncState { public string status = "idle"; public long finished; public string unity; public string project; public List errors = new List(); public List warnings = new List(); public List console = new List(); } [InitializeOnLoad] static class LiveSync { const string EnabledKey = "Dianoetic.LiveSync.Enabled"; static readonly string[] Watched = { ".cs", ".asmdef", ".asmref", ".shader", ".hlsl", ".cginc", ".compute", ".uxml", ".uss" }; static readonly string Root = Path.GetDirectoryName(Application.dataPath); static readonly string Folder = Path.Combine(Root, "DianoeticSync"); static readonly string StatePath = Path.Combine(Folder, "compile.json"); static FileSystemWatcher watcher; static volatile bool pending; static double refreshedAt = -1; static bool compiledSinceRefresh; static bool consoleDirty; static readonly List errors = new List(); static readonly List warnings = new List(); static readonly List consoleLines = new List(); public static bool Enabled { get => EditorPrefs.GetBool(EnabledKey, true); set { EditorPrefs.SetBool(EnabledKey, value); Setup(); } } public static SyncState Current { get; private set; } = new SyncState(); static LiveSync() { Current = Load() ?? new SyncState(); consoleLines.AddRange(Current.console); CompilationPipeline.compilationStarted += OnCompilationStarted; CompilationPipeline.assemblyCompilationFinished += OnAssemblyFinished; CompilationPipeline.compilationFinished += OnCompilationFinished; Application.logMessageReceivedThreaded += OnLog; EditorApplication.update += Tick; AssemblyReloadEvents.beforeAssemblyReload += Teardown; Setup(); if (Current.status == "compiling") Finish(Current.errors, Current.warnings); } static void Setup() { Teardown(); if (!Enabled) return; try { Directory.CreateDirectory(Folder); var ignore = Path.Combine(Folder, ".gitignore"); if (!File.Exists(ignore)) File.WriteAllText(ignore, "*\n"); watcher = new FileSystemWatcher(Application.dataPath) { IncludeSubdirectories = true, NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.Size, }; watcher.Changed += OnFileEvent; watcher.Created += OnFileEvent; watcher.Deleted += OnFileEvent; watcher.Renamed += OnFileEvent; watcher.EnableRaisingEvents = true; if (!File.Exists(StatePath)) Finish(errors, warnings); } catch (Exception e) { Debug.LogWarning($"Dianoetic Live Sync couldn't start: {e.Message}"); } } static void Teardown() { if (watcher == null) return; watcher.EnableRaisingEvents = false; watcher.Dispose(); watcher = null; } static void OnFileEvent(object sender, FileSystemEventArgs e) { var ext = Path.GetExtension(e.FullPath).ToLowerInvariant(); if (Array.IndexOf(Watched, ext) >= 0) pending = true; } static void Tick() { if (!Enabled) return; if (pending && !EditorApplication.isCompiling && !EditorApplication.isUpdating && !EditorApplication.isPlayingOrWillChangePlaymode) { pending = false; compiledSinceRefresh = false; refreshedAt = EditorApplication.timeSinceStartup; AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); } if (refreshedAt >= 0 && EditorApplication.timeSinceStartup - refreshedAt > 1.5) { refreshedAt = -1; if (!compiledSinceRefresh && !EditorApplication.isCompiling) Finish(Current.errors, Current.warnings); } if (consoleDirty) { consoleDirty = false; lock (consoleLines) Current.console = new List(consoleLines); Save(); } } static void OnCompilationStarted(object context) { compiledSinceRefresh = true; errors.Clear(); warnings.Clear(); Current.status = "compiling"; Save(); } static void OnAssemblyFinished(string assembly, CompilerMessage[] messages) { foreach (var m in messages) { var line = $"{Relative(m.file)}({m.line},{m.column}): {m.message}"; if (m.type == CompilerMessageType.Error) { if (!errors.Contains(line)) errors.Add(line); } else if (warnings.Count < 50 && !warnings.Contains(line)) warnings.Add(line); } } static void OnCompilationFinished(object context) => Finish(errors, warnings); static void Finish(List errs, List warns) { Current.status = errs.Count > 0 ? "errors" : "ok"; Current.finished = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); Current.errors = new List(errs); Current.warnings = new List(warns); Save(); } static void OnLog(string message, string stack, LogType type) { if (type != LogType.Error && type != LogType.Exception && type != LogType.Assert) return; var first = stack?.Split('\n')[0]?.Trim(); var line = string.IsNullOrEmpty(first) ? message : $"{message} ({first})"; lock (consoleLines) { consoleLines.Add(line.Length > 400 ? line.Substring(0, 400) : line); if (consoleLines.Count > 30) consoleLines.RemoveAt(0); } consoleDirty = true; } static string Relative(string file) { if (string.IsNullOrEmpty(file)) return "(unknown)"; var full = Path.GetFullPath(Path.IsPathRooted(file) ? file : Path.Combine(Root, file)); return full.StartsWith(Root, StringComparison.OrdinalIgnoreCase) ? full.Substring(Root.Length + 1).Replace('\\', '/') : file; } static SyncState Load() { try { return File.Exists(StatePath) ? JsonUtility.FromJson(File.ReadAllText(StatePath)) : null; } catch { return null; } } static void Save() { if (!Enabled) return; try { Current.unity = Application.unityVersion; Current.project = Application.productName; Directory.CreateDirectory(Folder); var temp = StatePath + ".tmp"; File.WriteAllText(temp, JsonUtility.ToJson(Current, true)); if (File.Exists(StatePath)) File.Delete(StatePath); File.Move(temp, StatePath); } catch (IOException) { } } public static void ClearConsole() { lock (consoleLines) consoleLines.Clear(); Current.console.Clear(); Save(); } } class LiveSyncWindow : EditorWindow { Vector2 scroll; [MenuItem("Window/Dianoetic Live Sync")] static void Open() => GetWindow("Dianoetic Live Sync"); void OnEnable() => EditorApplication.update += Repaint; void OnDisable() => EditorApplication.update -= Repaint; void OnGUI() { var s = LiveSync.Current; EditorGUILayout.Space(); var on = EditorGUILayout.ToggleLeft("Sync changes from Dianoetic Code", LiveSync.Enabled); if (on != LiveSync.Enabled) LiveSync.Enabled = on; EditorGUILayout.HelpBox(on ? "Scripts Dianoetic edits are imported right away, even when Unity isn't the active window. Compile errors are shared back so Dianoetic can fix them." : "Live Sync is off. Unity will only pick up changes when you click back into the Editor.", MessageType.None); var label = EditorApplication.isCompiling ? "Compiling…" : s.status == "errors" ? $"{s.errors.Count} compile error(s)" : s.status == "ok" ? "Compiled, no errors" : "Waiting for changes"; EditorGUILayout.LabelField("Status", label, EditorStyles.boldLabel); if (s.finished > 0) EditorGUILayout.LabelField("Last compile", DateTimeOffset.FromUnixTimeMilliseconds(s.finished).ToLocalTime().ToString("t")); scroll = EditorGUILayout.BeginScrollView(scroll); foreach (var e in s.errors) EditorGUILayout.HelpBox(e, MessageType.Error); if (s.console.Count > 0) { EditorGUILayout.LabelField("Recent Console errors", EditorStyles.miniBoldLabel); foreach (var c in s.console) EditorGUILayout.LabelField(c, EditorStyles.wordWrappedMiniLabel); } EditorGUILayout.EndScrollView(); using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("Clear Console list")) LiveSync.ClearConsole(); if (GUILayout.Button("Open dianoetic.tech")) Application.OpenURL("https://dianoetic.tech/code"); } } } } #endif