Skip to content

Instantly share code, notes, and snippets.

Created March 30, 2021 14:56
Show Gist options
  • 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>();
public static void Initialize ()
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))
private static void StopWatching ()
EditorApplication.update -= Update;
foreach (var watcher in runningWatchers)
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);
// 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)
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))
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);
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)
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