using FileWatcherEx; using Microsoft.Extensions.FileSystemGlobbing; using SharpIDE.Application.Features.Events; using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence; namespace SharpIDE.Application.Features.FileWatching; public sealed class IdeFileWatcher : IDisposable { private Matcher? _matcher; private FileSystemWatcherEx? _fileWatcher; private SharpIdeSolutionModel? _solution; public void StartWatching(SharpIdeSolutionModel solution) { _solution = solution; var matcher = new Matcher(); //matcher.AddIncludePatterns(["**/*.cs", "**/*.csproj", "**/*.sln"]); matcher.AddIncludePatterns(["**/*"]); matcher.AddExcludePatterns(["**/bin", "**/obj", "**/node_modules", "**/.vs", "**/.git", "**/.idea", "**/.vscode"]); _matcher = matcher; var fileWatcher = new FileSystemWatcherEx(); fileWatcher.FolderPath = solution.DirectoryPath; //fileWatcher.Filters.AddRange(["*"]); fileWatcher.IncludeSubdirectories = true; fileWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName; fileWatcher.OnChanged += OnEvent; fileWatcher.OnCreated += OnEvent; fileWatcher.OnDeleted += OnEvent; fileWatcher.OnRenamed += OnEvent; fileWatcher.OnError += static (s, e) => Console.WriteLine($"FileSystemWatcher: Error - {e.GetException().Message}"); fileWatcher.Start(); _fileWatcher = fileWatcher; } public void StopWatching() { if (_fileWatcher is not null) { _fileWatcher.Stop(); _fileWatcher.Dispose(); _fileWatcher = null!; } } // TODO: Put events on a queue and process them in the background to avoid filling the buffer? FileSystemWatcherEx might already handle this private void OnEvent(object? sender, FileChangedEvent e) { var matchResult = _matcher!.Match(_solution!.DirectoryPath, e.FullPath); if (!matchResult.HasMatches) return; switch (e.ChangeType) { case ChangeType.CHANGED: HandleChanged(e.FullPath); break; case ChangeType.CREATED: HandleCreated(e.FullPath); break; case ChangeType.DELETED: HandleDeleted(e.FullPath); break; case ChangeType.RENAMED: HandleRenamed(e.OldFullPath!, e.FullPath); break; default: throw new ArgumentOutOfRangeException(); } } private void HandleRenamed(string oldFullPath, string fullPath) { var isDirectory = Path.HasExtension(fullPath) is false; if (isDirectory) { GlobalEvents.Instance.FileSystemWatcherInternal.DirectoryRenamed.InvokeParallelFireAndForget(oldFullPath, fullPath); } else { GlobalEvents.Instance.FileSystemWatcherInternal.FileRenamed.InvokeParallelFireAndForget(oldFullPath, fullPath); } //Console.WriteLine($"FileSystemWatcher: Renamed - {oldFullPath}, {fullPath}"); } private void HandleDeleted(string fullPath) { var isDirectory = Path.HasExtension(fullPath) is false; if (isDirectory) { GlobalEvents.Instance.FileSystemWatcherInternal.DirectoryDeleted.InvokeParallelFireAndForget(fullPath); } else { GlobalEvents.Instance.FileSystemWatcherInternal.FileDeleted.InvokeParallelFireAndForget(fullPath); } //Console.WriteLine($"FileSystemWatcher: Deleted - {fullPath}"); } private void HandleCreated(string fullPath) { var isDirectory = Path.HasExtension(fullPath) is false; if (isDirectory) { GlobalEvents.Instance.FileSystemWatcherInternal.DirectoryCreated.InvokeParallelFireAndForget(fullPath); } else { GlobalEvents.Instance.FileSystemWatcherInternal.FileCreated.InvokeParallelFireAndForget(fullPath); } //Console.WriteLine($"FileSystemWatcher: Created - {fullPath}"); } // The only changed event we care about is files, not directories // We will naively assume that if the file name does not have an extension, it's a directory // This may not always be true, but it lets us avoid reading the file system to check // TODO: Make a note to users that they should not use files without extensions private void HandleChanged(string fullPath) { if (Path.HasExtension(fullPath) is false) return; // we don't care about directory changes //Console.WriteLine($"FileSystemWatcher: Changed - {fullPath}"); GlobalEvents.Instance.FileSystemWatcherInternal.FileChanged.InvokeParallelFireAndForget(fullPath); } public void Dispose() { StopWatching(); } }