Skip to content

Instantly share code, notes, and snippets.

@elringus
Created March 30, 2021 14:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save elringus/7c0fcf0fdcaa3d0ffa4a5408209d8f10 to your computer and use it in GitHub Desktop.
Save elringus/7c0fcf0fdcaa3d0ffa4a5408209d8f10 to your computer and use it in GitHub Desktop.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
namespace Naninovel
{
/// <summary>
/// Uses file system watcher to track changes to `.nani` files in the project directory.
/// </summary>
public static class ScriptFileWatcher
{
/// <summary>
/// Invoked when a <see cref="Script"/> asset is created or modified; returns modified script asset path.
/// </summary>
public static event Action<string> OnModified;
private static ConcurrentQueue<string> modifiedScriptPaths = new ConcurrentQueue<string>();
private static ConcurrentStack<FileSystemWatcher> runningWatchers = new ConcurrentStack<FileSystemWatcher>();
[InitializeOnLoadMethod]
public static void Initialize ()
{
StopWatching();
var config = ProjectConfigurationProvider.LoadOrDefault<ScriptsConfiguration>();
if (config.WatchScripts) StartWatching(config);
}
private static void StartWatching (ScriptsConfiguration config)
{
EditorApplication.update += Update;
foreach (var path in FindDirectoriesWithScripts(config))
WatchDirectory(path);
}
private static void StopWatching ()
{
EditorApplication.update -= Update;
foreach (var watcher in runningWatchers)
watcher?.Dispose();
runningWatchers.Clear();
}
private static void Update ()
{
if (modifiedScriptPaths.Count == 0) return;
if (!modifiedScriptPaths.TryDequeue(out var fullPath)) return;
if (!File.Exists(fullPath)) return;
var assetPath = PathUtils.AbsoluteToAssetPath(fullPath);
AssetDatabase.ImportAsset(assetPath);
OnModified?.Invoke(assetPath);
// Required to rebuild script when editor is not in focus, because script view
// delays rebuild, but delayed call is not invoked while editor is not in focus.
if (!InternalEditorUtility.isApplicationActive)
EditorApplication.delayCall?.Invoke();
}
private static IReadOnlyCollection<string> FindDirectoriesWithScripts (ScriptsConfiguration config)
{
var result = new List<string>();
var dataPath = string.IsNullOrEmpty(config.WatchedDirectory) || !Directory.Exists(config.WatchedDirectory) ? Application.dataPath : config.WatchedDirectory;
if (ContainsScripts(dataPath)) result.Add(dataPath);
foreach (var path in Directory.GetDirectories(dataPath, "*", SearchOption.AllDirectories))
if (ContainsScripts(path))
result.Add(path);
return result;
bool ContainsScripts (string path) => Directory.GetFiles(path, "*.nani", SearchOption.TopDirectoryOnly).Length > 0;
}
private static void WatchDirectory (string path)
{
Task.Run(AddWatcher).ContinueWith(DisposeWatcher, TaskScheduler.FromCurrentSynchronizationContext());
FileSystemWatcher AddWatcher ()
{
var watcher = CreateWatcher(path);
runningWatchers.Push(watcher);
return watcher;
}
}
private static FileSystemWatcher CreateWatcher (string path)
{
var watcher = new FileSystemWatcher();
watcher.Path = path;
watcher.IncludeSubdirectories = false;
watcher.NotifyFilter = NotifyFilters.LastWrite;
watcher.Filter = "*.nani";
watcher.Changed += (_, e) => modifiedScriptPaths.Enqueue(e.FullPath);
watcher.EnableRaisingEvents = true;
return watcher;
}
private static void DisposeWatcher (Task<FileSystemWatcher> startTask)
{
try
{
var watcher = startTask.Result;
AppDomain.CurrentDomain.DomainUnload += (EventHandler)((_, __) => { watcher.Dispose(); });
}
catch (Exception e)
{
Debug.LogError($"Failed to stop script file watcher: {e.Message}");
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment