diff --git a/Directory.Packages.props b/Directory.Packages.props index bee1208..f657780 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,8 +2,8 @@ true true - - $(NoWarn);NU1507 + + $(NoWarn);NU1507 @@ -12,6 +12,7 @@ + @@ -24,6 +25,7 @@ + @@ -46,14 +48,14 @@ - - - - - - + + + + + + - + \ No newline at end of file diff --git a/src/SharpIDE.Application/Features/FileWatching/IdeFileWatcher.cs b/src/SharpIDE.Application/Features/FileWatching/IdeFileWatcher.cs new file mode 100644 index 0000000..88b204d --- /dev/null +++ b/src/SharpIDE.Application/Features/FileWatching/IdeFileWatcher.cs @@ -0,0 +1,93 @@ +using FileWatcherEx; +using Microsoft.Extensions.FileSystemGlobbing; +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.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) + { + + Console.WriteLine($"FileSystemWatcher: Renamed - {oldFullPath}, {fullPath}"); + } + + private void HandleDeleted(string fullPath) + { + Console.WriteLine($"FileSystemWatcher: Deleted - {fullPath}"); + } + + private void HandleCreated(string 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; + // TODO: Handle updating the content of open files in editors + Console.WriteLine($"FileSystemWatcher: Changed - {fullPath}"); + } + + public void Dispose() + { + StopWatching(); + } +} diff --git a/src/SharpIDE.Application/Features/SolutionDiscovery/VsPersistence/SharpIdeModels.cs b/src/SharpIDE.Application/Features/SolutionDiscovery/VsPersistence/SharpIdeModels.cs index 47e4aef..0bda2c8 100644 --- a/src/SharpIDE.Application/Features/SolutionDiscovery/VsPersistence/SharpIdeModels.cs +++ b/src/SharpIDE.Application/Features/SolutionDiscovery/VsPersistence/SharpIdeModels.cs @@ -34,6 +34,7 @@ public class SharpIdeSolutionModel : ISharpIdeNode, IExpandableSharpIdeNode { public required string Name { get; set; } public required string FilePath { get; set; } + public required string DirectoryPath { get; set; } public required List Projects { get; set; } public required List Folders { get; set; } public required HashSet AllProjects { get; set; } @@ -48,6 +49,7 @@ public class SharpIdeSolutionModel : ISharpIdeNode, IExpandableSharpIdeNode var allFiles = new ConcurrentBag(); Name = solutionName; FilePath = solutionFilePath; + DirectoryPath = Path.GetDirectoryName(solutionFilePath)!; Projects = intermediateModel.Projects.Select(s => new SharpIdeProjectModel(s, allProjects, allFiles, this)).ToList(); Folders = intermediateModel.SolutionFolders.Select(s => new SharpIdeSolutionFolder(s, allProjects, allFiles, this)).ToList(); AllProjects = allProjects.ToHashSet(); diff --git a/src/SharpIDE.Application/SharpIDE.Application.csproj b/src/SharpIDE.Application/SharpIDE.Application.csproj index fdf55ef..9b3149b 100644 --- a/src/SharpIDE.Application/SharpIDE.Application.csproj +++ b/src/SharpIDE.Application/SharpIDE.Application.csproj @@ -19,7 +19,9 @@ + + diff --git a/src/SharpIDE.Godot/IdeRoot.cs b/src/SharpIDE.Godot/IdeRoot.cs index 5dbc4b7..228a27d 100644 --- a/src/SharpIDE.Godot/IdeRoot.cs +++ b/src/SharpIDE.Godot/IdeRoot.cs @@ -4,6 +4,8 @@ using Microsoft.Extensions.Hosting; using SharpIDE.Application.Features.Analysis; using SharpIDE.Application.Features.Build; using SharpIDE.Application.Features.Events; +using SharpIDE.Application.Features.FileWatching; +using SharpIDE.Application.Features.Run; using SharpIDE.Application.Features.SolutionDiscovery; using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence; using SharpIDE.Godot.Features.BottomPanel; @@ -39,6 +41,10 @@ public partial class IdeRoot : Control { GodotGlobalEvents.Instance = new GodotGlobalEvents(); GlobalEvents.Instance = new GlobalEvents(); + Singletons.RunService = new RunService(); + Singletons.BuildService = new BuildService(); + Singletons.FileWatcher?.Dispose(); + Singletons.FileWatcher = new IdeFileWatcher(); } public override void _Ready() diff --git a/src/SharpIDE.Godot/Singletons.cs b/src/SharpIDE.Godot/Singletons.cs index e130ff1..618cc1a 100644 --- a/src/SharpIDE.Godot/Singletons.cs +++ b/src/SharpIDE.Godot/Singletons.cs @@ -1,4 +1,5 @@ using SharpIDE.Application.Features.Build; +using SharpIDE.Application.Features.FileWatching; using SharpIDE.Application.Features.Run; using SharpIDE.Godot.Features.IdeSettings; @@ -6,7 +7,8 @@ namespace SharpIDE.Godot; public static class Singletons { - public static RunService RunService { get; } = new RunService(); - public static BuildService BuildService { get; } = new BuildService(); + public static RunService RunService { get; set; } = null!; + public static BuildService BuildService { get; set; } = null!; + public static IdeFileWatcher FileWatcher { get; set; } = null!; public static AppState AppState { get; set; } = null!; } \ No newline at end of file