diff --git a/src/SharpIDE.Application/Features/Events/GlobalEvents.cs b/src/SharpIDE.Application/Features/Events/GlobalEvents.cs index 9333b0e..20d9a13 100644 --- a/src/SharpIDE.Application/Features/Events/GlobalEvents.cs +++ b/src/SharpIDE.Application/Features/Events/GlobalEvents.cs @@ -13,4 +13,17 @@ public class GlobalEvents public EventWrapper ProjectStartedRunning { get; } = new(_ => Task.CompletedTask); public EventWrapper ProjectStoppedRunning { get; } = new(_ => Task.CompletedTask); public EventWrapper DebuggerExecutionStopped { get; } = new(_ => Task.CompletedTask); + + public FileSystemWatcherInternal FileSystemWatcherInternal { get; } = new(); +} + +public class FileSystemWatcherInternal +{ + public EventWrapper DirectoryCreated { get; } = new(_ => Task.CompletedTask); + public EventWrapper DirectoryDeleted { get; } = new(_ => Task.CompletedTask); + public EventWrapper DirectoryRenamed { get; } = new((_, _) => Task.CompletedTask); + public EventWrapper FileCreated { get; } = new(_ => Task.CompletedTask); + public EventWrapper FileDeleted { get; } = new(_ => Task.CompletedTask); + public EventWrapper FileRenamed { get; } = new((_, _) => Task.CompletedTask); + public EventWrapper FileChanged { get; } = new(_ => Task.CompletedTask); } diff --git a/src/SharpIDE.Application/Features/FilePersistence/IdeFileManager.cs b/src/SharpIDE.Application/Features/FilePersistence/IdeFileManager.cs index 4bc2b15..8a9abda 100644 --- a/src/SharpIDE.Application/Features/FilePersistence/IdeFileManager.cs +++ b/src/SharpIDE.Application/Features/FilePersistence/IdeFileManager.cs @@ -37,6 +37,20 @@ public class IdeFileManager } } + public async Task ReloadFileFromDisk(SharpIdeFile file) + { + if (!_openFiles.ContainsKey(file)) throw new InvalidOperationException("File is not open in memory."); + + var newTextTaskLazy = new Lazy>(() => File.ReadAllTextAsync(file.Path)); + _openFiles[file] = newTextTaskLazy; + var textTask = newTextTaskLazy.Value; + if (file.IsRoslynWorkspaceFile) + { + var text = await textTask; + RoslynAnalysis.UpdateDocument(file, text); + } + } + public async Task SaveFileAsync(SharpIdeFile file) { if (!_openFiles.ContainsKey(file)) throw new InvalidOperationException("File is not open in memory."); diff --git a/src/SharpIDE.Application/Features/FileWatching/IdeFileChangeHandler.cs b/src/SharpIDE.Application/Features/FileWatching/IdeFileChangeHandler.cs new file mode 100644 index 0000000..c005dc6 --- /dev/null +++ b/src/SharpIDE.Application/Features/FileWatching/IdeFileChangeHandler.cs @@ -0,0 +1,21 @@ +using SharpIDE.Application.Features.Events; +using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence; + +namespace SharpIDE.Application.Features.FileWatching; + +public class IdeFileChangeHandler +{ + public SharpIdeSolutionModel SolutionModel { get; set; } = null!; + public IdeFileChangeHandler() + { + GlobalEvents.Instance.FileSystemWatcherInternal.FileChanged.Subscribe(OnFileChanged); + } + + private async Task OnFileChanged(string arg) + { + var sharpIdeFile = SolutionModel.AllFiles.SingleOrDefault(f => f.Path == arg); + if (sharpIdeFile is null) return; + // TODO: Suppress if SharpIDE changed the file + await sharpIdeFile.FileContentsChangedExternallyFromDisk.InvokeParallelAsync(); + } +} diff --git a/src/SharpIDE.Application/Features/FileWatching/IdeFileWatcher.cs b/src/SharpIDE.Application/Features/FileWatching/IdeFileWatcher.cs index 0774c08..747fb45 100644 --- a/src/SharpIDE.Application/Features/FileWatching/IdeFileWatcher.cs +++ b/src/SharpIDE.Application/Features/FileWatching/IdeFileWatcher.cs @@ -1,5 +1,6 @@ using FileWatcherEx; using Microsoft.Extensions.FileSystemGlobbing; +using SharpIDE.Application.Features.Events; using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence; namespace SharpIDE.Application.Features.FileWatching; @@ -62,7 +63,6 @@ public sealed class IdeFileWatcher : IDisposable private void HandleRenamed(string? oldFullPath, string fullPath) { - Console.WriteLine($"FileSystemWatcher: Renamed - {oldFullPath}, {fullPath}"); } @@ -83,8 +83,8 @@ public sealed class IdeFileWatcher : IDisposable 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}"); + GlobalEvents.Instance.FileSystemWatcherInternal.FileChanged.InvokeParallelFireAndForget(fullPath); } public void Dispose() diff --git a/src/SharpIDE.Application/Features/SolutionDiscovery/SharpIdeFile.cs b/src/SharpIDE.Application/Features/SolutionDiscovery/SharpIdeFile.cs index 3d3a03d..215d2c6 100644 --- a/src/SharpIDE.Application/Features/SolutionDiscovery/SharpIdeFile.cs +++ b/src/SharpIDE.Application/Features/SolutionDiscovery/SharpIdeFile.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using R3; +using SharpIDE.Application.Features.Events; using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence; namespace SharpIDE.Application.Features.SolutionDiscovery; @@ -15,6 +16,8 @@ public class SharpIdeFile : ISharpIdeNode, IChildSharpIdeNode public bool IsCsharpFile => Path.EndsWith(".cs", StringComparison.OrdinalIgnoreCase); public bool IsRoslynWorkspaceFile => IsCsharpFile || IsRazorFile || IsCshtmlFile; public required ReactiveProperty IsDirty { get; set; } + public EventWrapper FileContentsChangedExternallyFromDisk { get; } = new(() => Task.CompletedTask); + public EventWrapper FileContentsChangedExternally { get; } = new(() => Task.CompletedTask); [SetsRequiredMembers] internal SharpIdeFile(string fullPath, string name, IExpandableSharpIdeNode parent, ConcurrentBag allFiles) diff --git a/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs b/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs index 8651c53..0c55b47 100644 --- a/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs +++ b/src/SharpIDE.Godot/Features/CodeEditor/SharpIdeCodeEdit.cs @@ -48,6 +48,12 @@ public partial class SharpIdeCodeEdit : CodeEdit SymbolLookup += OnSymbolLookup; } + public override void _ExitTree() + { + _currentFile.FileContentsChangedExternallyFromDisk.Unsubscribe(OnFileChangedExternallyFromDisk); + _currentFile.FileContentsChangedExternally.Unsubscribe(OnFileChangedExternallyInMemory); + } + private void OnBreakpointToggled(long line) { if (_fileChangingSuppressBreakpointToggleEvent) return; @@ -118,8 +124,7 @@ public partial class SharpIdeCodeEdit : CodeEdit GD.Print($"Code fix selected: {id}"); var codeAction = _currentCodeActionsInPopup[(int)id]; if (codeAction is null) return; - var currentCaretPosition = GetCaretPosition(); - var vScroll = GetVScroll(); + _ = Task.GodotRun(async () => { var affectedFiles = await RoslynAnalysis.ApplyCodeActionAsync(codeAction); @@ -127,25 +132,35 @@ public partial class SharpIdeCodeEdit : CodeEdit foreach (var (affectedFile, updatedText) in affectedFiles) { await Singletons.FileManager.UpdateInMemoryIfOpenAndSaveAsync(affectedFile, updatedText); + affectedFile.FileContentsChangedExternally.InvokeParallelFireAndForget(); } - var fileContents = await Singletons.FileManager.GetFileTextAsync(_currentFile); - var syntaxHighlighting = await RoslynAnalysis.GetDocumentSyntaxHighlighting(_currentFile); - var razorSyntaxHighlighting = await RoslynAnalysis.GetRazorDocumentSyntaxHighlighting(_currentFile); - var diagnostics = await RoslynAnalysis.GetDocumentDiagnostics(_currentFile); - Callable.From(() => - { - BeginComplexOperation(); - SetText(fileContents); - SetSyntaxHighlightingModel(syntaxHighlighting, razorSyntaxHighlighting); - SetDiagnosticsModel(diagnostics); - SetCaretLine(currentCaretPosition.line); - SetCaretColumn(currentCaretPosition.col); - SetVScroll(vScroll); - EndComplexOperation(); - }).CallDeferred(); }); } - + + private async Task OnFileChangedExternallyInMemory() + { + var fileContents = await Singletons.FileManager.GetFileTextAsync(_currentFile); + var syntaxHighlighting = RoslynAnalysis.GetDocumentSyntaxHighlighting(_currentFile); + var razorSyntaxHighlighting = RoslynAnalysis.GetRazorDocumentSyntaxHighlighting(_currentFile); + var diagnostics = RoslynAnalysis.GetDocumentDiagnostics(_currentFile); + var slnDiagnostics = RoslynAnalysis.UpdateSolutionDiagnostics(); + await Task.WhenAll(syntaxHighlighting, razorSyntaxHighlighting, diagnostics); + Callable.From(() => + { + var currentCaretPosition = GetCaretPosition(); + var vScroll = GetVScroll(); + BeginComplexOperation(); + SetText(fileContents); + SetSyntaxHighlightingModel(syntaxHighlighting.Result, razorSyntaxHighlighting.Result); + SetDiagnosticsModel(diagnostics.Result); + SetCaretLine(currentCaretPosition.line); + SetCaretColumn(currentCaretPosition.col); + SetVScroll(vScroll); + EndComplexOperation(); + }).CallDeferred(); + await slnDiagnostics; + } + public void SetFileLinePosition(SharpIdeFileLinePosition fileLinePosition) { var line = fileLinePosition.Line - 1; @@ -162,6 +177,8 @@ public partial class SharpIdeCodeEdit : CodeEdit await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); // get off the UI thread _currentFile = file; var readFileTask = Singletons.FileManager.GetFileTextAsync(file); + _currentFile.FileContentsChangedExternally.Subscribe(OnFileChangedExternallyInMemory); + _currentFile.FileContentsChangedExternallyFromDisk.Subscribe(OnFileChangedExternallyFromDisk); var syntaxHighlighting = RoslynAnalysis.GetDocumentSyntaxHighlighting(_currentFile); var razorSyntaxHighlighting = RoslynAnalysis.GetRazorDocumentSyntaxHighlighting(_currentFile); @@ -176,7 +193,13 @@ public partial class SharpIdeCodeEdit : CodeEdit SetSyntaxHighlightingModel(await syntaxHighlighting, await razorSyntaxHighlighting); SetDiagnosticsModel(await diagnostics); } - + + private async Task OnFileChangedExternallyFromDisk() + { + await Singletons.FileManager.ReloadFileFromDisk(_currentFile); + await OnFileChangedExternallyInMemory(); + } + public void UnderlineRange(int line, int caretStartCol, int caretEndCol, Color color, float thickness = 1.5f) { if (line < 0 || line >= GetLineCount()) diff --git a/src/SharpIDE.Godot/IdeRoot.cs b/src/SharpIDE.Godot/IdeRoot.cs index 32d6821..5beb4fe 100644 --- a/src/SharpIDE.Godot/IdeRoot.cs +++ b/src/SharpIDE.Godot/IdeRoot.cs @@ -47,6 +47,7 @@ public partial class IdeRoot : Control Singletons.FileWatcher?.Dispose(); Singletons.FileWatcher = new IdeFileWatcher(); Singletons.FileManager = new IdeFileManager(); + Singletons.FileChangeHandler = new IdeFileChangeHandler(); } public override void _Ready() @@ -121,6 +122,7 @@ public partial class IdeRoot : Control _codeEditorPanel.Solution = solutionModel; _bottomPanelManager.Solution = solutionModel; _searchWindow.Solution = solutionModel; + Singletons.FileChangeHandler.SolutionModel = solutionModel; Callable.From(_solutionExplorerPanel.RepopulateTree).CallDeferred(); RoslynAnalysis.StartSolutionAnalysis(solutionModel); Singletons.FileWatcher.StartWatching(solutionModel); diff --git a/src/SharpIDE.Godot/Singletons.cs b/src/SharpIDE.Godot/Singletons.cs index a3a6fe8..0ffb6c4 100644 --- a/src/SharpIDE.Godot/Singletons.cs +++ b/src/SharpIDE.Godot/Singletons.cs @@ -12,5 +12,6 @@ public static class Singletons public static BuildService BuildService { get; set; } = null!; public static IdeFileWatcher FileWatcher { get; set; } = null!; public static IdeFileManager FileManager { get; set; } = null!; + public static IdeFileChangeHandler FileChangeHandler { get; set; } = null!; public static AppState AppState { get; set; } = null!; } \ No newline at end of file