refactor file update handling
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
using Microsoft.CodeAnalysis.CodeActions;
|
||||
using SharpIDE.Application.Features.FileWatching;
|
||||
|
||||
namespace SharpIDE.Application.Features.Analysis;
|
||||
|
||||
public class CodeActionService(RoslynAnalysis roslynAnalysis, FileChangedService fileChangedService)
|
||||
{
|
||||
private readonly RoslynAnalysis _roslynAnalysis = roslynAnalysis;
|
||||
private readonly FileChangedService _fileChangedService = fileChangedService;
|
||||
|
||||
public async Task ApplyCodeAction(CodeAction codeAction)
|
||||
{
|
||||
var affectedFiles = await _roslynAnalysis.GetCodeActionApplyChanges(codeAction);
|
||||
foreach (var (affectedFile, updatedText) in affectedFiles)
|
||||
{
|
||||
await _fileChangedService.SharpIdeFileChanged(affectedFile, updatedText, FileChangeType.CodeActionChange);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,6 +296,7 @@ public class RoslynAnalysis
|
||||
{
|
||||
using var _ = SharpIdeOtel.Source.StartActivity($"{nameof(RoslynAnalysis)}.{nameof(GetProjectDiagnosticsForFile)}");
|
||||
await _solutionLoadedTcs.Task;
|
||||
if (sharpIdeFile.IsRoslynWorkspaceFile is false) return [];
|
||||
var cancellationToken = CancellationToken.None;
|
||||
var project = _workspace!.CurrentSolution.Projects.Single(s => s.FilePath == ((IChildSharpIdeNode)sharpIdeFile).GetNearestProjectNode()!.FilePath);
|
||||
var compilation = await project.GetCompilationAsync(cancellationToken);
|
||||
@@ -316,6 +317,7 @@ public class RoslynAnalysis
|
||||
if (fileModel.IsRoslynWorkspaceFile is false) return [];
|
||||
using var _ = SharpIdeOtel.Source.StartActivity($"{nameof(RoslynAnalysis)}.{nameof(GetDocumentDiagnostics)}");
|
||||
await _solutionLoadedTcs.Task;
|
||||
if (fileModel.IsRoslynWorkspaceFile is false) return [];
|
||||
|
||||
var document = await GetDocumentForSharpIdeFile(fileModel);
|
||||
Guard.Against.Null(document, nameof(document));
|
||||
@@ -575,14 +577,15 @@ public class RoslynAnalysis
|
||||
return completions;
|
||||
}
|
||||
|
||||
/// Returns the list of files modified by applying the code action
|
||||
public async Task<List<(SharpIdeFile File, string UpdatedText)>> ApplyCodeActionAsync(CodeAction codeAction)
|
||||
/// Returns the list of files that would be modified by applying the code action. Does not apply the changes to the workspace sln
|
||||
public async Task<List<(SharpIdeFile File, string UpdatedText)>> GetCodeActionApplyChanges(CodeAction codeAction)
|
||||
{
|
||||
using var _ = SharpIdeOtel.Source.StartActivity($"{nameof(RoslynAnalysis)}.{nameof(ApplyCodeActionAsync)}");
|
||||
using var _ = SharpIdeOtel.Source.StartActivity($"{nameof(RoslynAnalysis)}.{nameof(GetCodeActionApplyChanges)}");
|
||||
await _solutionLoadedTcs.Task;
|
||||
var cancellationToken = CancellationToken.None;
|
||||
var operations = await codeAction.GetOperationsAsync(cancellationToken);
|
||||
var changedDocumentIds = new List<DocumentId>();
|
||||
var originalSolution = _workspace!.CurrentSolution;
|
||||
foreach (var operation in operations)
|
||||
{
|
||||
if (operation is ApplyChangesOperation applyChangesOperation)
|
||||
@@ -616,6 +619,8 @@ public class RoslynAnalysis
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
_workspace.TryApplyChanges(originalSolution);
|
||||
|
||||
return changedFilesWithText;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ public class GlobalEvents
|
||||
public EventWrapper<SharpIdeProjectModel, Task> ProjectStartedRunning { get; } = new(_ => Task.CompletedTask);
|
||||
public EventWrapper<SharpIdeProjectModel, Task> ProjectStoppedRunning { get; } = new(_ => Task.CompletedTask);
|
||||
public EventWrapper<ExecutionStopInfo, Task> DebuggerExecutionStopped { get; } = new(_ => Task.CompletedTask);
|
||||
public EventWrapper<SharpIdeFile, Task> IdeFileSavedToDisk { get; } = new(_ => Task.CompletedTask);
|
||||
/// A document changed, project was reloaded etc. Document changes include unsaved changes in the IDE.
|
||||
public EventWrapper<Task> SolutionAltered { get; } = new(() => Task.CompletedTask);
|
||||
|
||||
|
||||
@@ -69,7 +69,6 @@ public class IdeOpenTabsFileManager(RoslynAnalysis roslynAnalysis)
|
||||
var text = await GetFileTextAsync(file);
|
||||
await WriteAllText(file, text);
|
||||
file.IsDirty.Value = false;
|
||||
GlobalEvents.Instance.IdeFileSavedToDisk.InvokeParallelFireAndForget(file);
|
||||
}
|
||||
|
||||
public async Task UpdateInMemoryIfOpenAndSaveAsync(SharpIdeFile file, string newText)
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
using Microsoft.VisualStudio.SolutionPersistence.Model;
|
||||
using SharpIDE.Application.Features.Analysis;
|
||||
using SharpIDE.Application.Features.Evaluation;
|
||||
using SharpIDE.Application.Features.Events;
|
||||
using SharpIDE.Application.Features.FilePersistence;
|
||||
using SharpIDE.Application.Features.SolutionDiscovery;
|
||||
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
|
||||
|
||||
namespace SharpIDE.Application.Features.FileWatching;
|
||||
|
||||
public enum FileChangeType
|
||||
{
|
||||
IdeSaveToDisk, // Apply to disk
|
||||
IdeUnsavedChange, // Apply only in memory
|
||||
ExternalChange, // Apply to disk, as well as in memory
|
||||
CodeActionChange // Apply to disk, as well as in memory
|
||||
}
|
||||
|
||||
public class FileChangedService(RoslynAnalysis roslynAnalysis, IdeOpenTabsFileManager openTabsFileManager)
|
||||
{
|
||||
private readonly RoslynAnalysis _roslynAnalysis = roslynAnalysis;
|
||||
private readonly IdeOpenTabsFileManager _openTabsFileManager = openTabsFileManager;
|
||||
|
||||
public SharpIdeSolutionModel SolutionModel { get; set; } = null!;
|
||||
|
||||
// All file changes should go via this service
|
||||
public async Task SharpIdeFileChanged(SharpIdeFile file, string newContents, FileChangeType changeType)
|
||||
{
|
||||
if (changeType is FileChangeType.ExternalChange)
|
||||
{
|
||||
// Disk is already up to date
|
||||
// Update any open tabs
|
||||
// update in memory
|
||||
await _openTabsFileManager.UpdateFileTextInMemory(file, newContents);
|
||||
file.FileContentsChangedExternally.InvokeParallelFireAndForget();
|
||||
}
|
||||
else if (changeType is FileChangeType.CodeActionChange)
|
||||
{
|
||||
// update in memory, tabs and save to disk
|
||||
await _openTabsFileManager.UpdateInMemoryIfOpenAndSaveAsync(file, newContents);
|
||||
file.FileContentsChangedExternally.InvokeParallelFireAndForget();
|
||||
}
|
||||
else if (changeType is FileChangeType.IdeSaveToDisk)
|
||||
{
|
||||
// save to disk
|
||||
// We technically don't need to update in memory here. TODO review
|
||||
await _openTabsFileManager.UpdateInMemoryIfOpenAndSaveAsync(file, newContents);
|
||||
}
|
||||
else if (changeType is FileChangeType.IdeUnsavedChange)
|
||||
{
|
||||
// update in memory only
|
||||
await _openTabsFileManager.UpdateFileTextInMemory(file, newContents);
|
||||
}
|
||||
var afterSaveTask = (file, changeType) switch
|
||||
{
|
||||
({ IsRoslynWorkspaceFile: true }, _) => HandleWorkspaceFileChanged(file, newContents),
|
||||
({ IsCsprojFile: true }, FileChangeType.IdeSaveToDisk or FileChangeType.ExternalChange) => HandleCsprojChanged(file),
|
||||
({ IsCsprojFile: true }, _) => Task.CompletedTask,
|
||||
_ => throw new InvalidOperationException("Unknown file change type.")
|
||||
};
|
||||
await afterSaveTask;
|
||||
}
|
||||
|
||||
private async Task HandleCsprojChanged(SharpIdeFile file)
|
||||
{
|
||||
var project = SolutionModel.AllProjects.SingleOrDefault(p => p.FilePath == file.Path);
|
||||
if (project is null) return;
|
||||
await ProjectEvaluation.ReloadProject(file.Path);
|
||||
await _roslynAnalysis.ReloadProject(project);
|
||||
GlobalEvents.Instance.SolutionAltered.InvokeParallelFireAndForget();
|
||||
await _roslynAnalysis.UpdateSolutionDiagnostics();
|
||||
}
|
||||
|
||||
private async Task HandleWorkspaceFileChanged(SharpIdeFile file, string newContents)
|
||||
{
|
||||
await _roslynAnalysis.UpdateDocument(file, newContents);
|
||||
GlobalEvents.Instance.SolutionAltered.InvokeParallelFireAndForget();
|
||||
await _roslynAnalysis.UpdateSolutionDiagnostics();
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
using SharpIDE.Application.Features.Analysis;
|
||||
using SharpIDE.Application.Features.Evaluation;
|
||||
using SharpIDE.Application.Features.Events;
|
||||
using SharpIDE.Application.Features.Events;
|
||||
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
|
||||
|
||||
namespace SharpIDE.Application.Features.FileWatching;
|
||||
|
||||
public class IdeFileExternalChangeHandler
|
||||
{
|
||||
private readonly FileChangedService _fileChangedService;
|
||||
public SharpIdeSolutionModel SolutionModel { get; set; } = null!;
|
||||
public IdeFileExternalChangeHandler()
|
||||
public IdeFileExternalChangeHandler(FileChangedService fileChangedService)
|
||||
{
|
||||
_fileChangedService = fileChangedService;
|
||||
GlobalEvents.Instance.FileSystemWatcherInternal.FileChanged.Subscribe(OnFileChanged);
|
||||
}
|
||||
|
||||
@@ -31,8 +31,7 @@ public class IdeFileExternalChangeHandler
|
||||
var file = SolutionModel.AllFiles.SingleOrDefault(f => f.Path == filePath);
|
||||
if (file is not null)
|
||||
{
|
||||
await file.FileContentsChangedExternallyFromDisk.InvokeParallelAsync();
|
||||
await GlobalEvents.Instance.IdeFileSavedToDisk.InvokeParallelAsync(file);
|
||||
await _fileChangedService.SharpIdeFileChanged(file, await File.ReadAllTextAsync(file.Path), FileChangeType.ExternalChange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
using SharpIDE.Application.Features.Analysis;
|
||||
using SharpIDE.Application.Features.Evaluation;
|
||||
using SharpIDE.Application.Features.Events;
|
||||
using SharpIDE.Application.Features.FilePersistence;
|
||||
using SharpIDE.Application.Features.SolutionDiscovery;
|
||||
using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence;
|
||||
|
||||
namespace SharpIDE.Application.Features.FileWatching;
|
||||
|
||||
public class IdeFileSavedToDiskHandler
|
||||
{
|
||||
private readonly IdeOpenTabsFileManager _openTabsFileManager;
|
||||
private readonly RoslynAnalysis _roslynAnalysis;
|
||||
public SharpIdeSolutionModel SolutionModel { get; set; } = null!;
|
||||
|
||||
public IdeFileSavedToDiskHandler(IdeOpenTabsFileManager openTabsFileManager, RoslynAnalysis roslynAnalysis)
|
||||
{
|
||||
_openTabsFileManager = openTabsFileManager;
|
||||
_roslynAnalysis = roslynAnalysis;
|
||||
GlobalEvents.Instance.IdeFileSavedToDisk.Subscribe(HandleIdeFileChanged);
|
||||
}
|
||||
|
||||
private async Task HandleIdeFileChanged(SharpIdeFile file)
|
||||
{
|
||||
if (file.IsCsprojFile)
|
||||
{
|
||||
await HandleCsprojChanged(file);
|
||||
}
|
||||
else if (file.IsRoslynWorkspaceFile)
|
||||
{
|
||||
await HandleWorkspaceFileChanged(file);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleCsprojChanged(SharpIdeFile file)
|
||||
{
|
||||
var project = SolutionModel.AllProjects.SingleOrDefault(p => p.FilePath == file.Path);
|
||||
if (project is null) return;
|
||||
await ProjectEvaluation.ReloadProject(file.Path);
|
||||
await _roslynAnalysis.ReloadProject(project);
|
||||
await _roslynAnalysis.UpdateSolutionDiagnostics();
|
||||
}
|
||||
|
||||
private async Task HandleWorkspaceFileChanged(SharpIdeFile file)
|
||||
{
|
||||
// TODO: Don't reload from disk if we raised the change event ourselves (e.g. save from IDE). Cleanup this whole disaster
|
||||
var wasOpenAndUpdated = await _openTabsFileManager.ReloadFileFromDiskIfOpenInEditor(file);
|
||||
if (file.IsRoslynWorkspaceFile)
|
||||
{
|
||||
var fileText = wasOpenAndUpdated ?
|
||||
await _openTabsFileManager.GetFileTextAsync(file) :
|
||||
await File.ReadAllTextAsync(file.Path);
|
||||
await _roslynAnalysis.UpdateDocument(file, fileText);
|
||||
GlobalEvents.Instance.SolutionAltered.InvokeParallelFireAndForget();
|
||||
}
|
||||
await _roslynAnalysis.UpdateSolutionDiagnostics();
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ public class SharpIdeFile : ISharpIdeNode, IChildSharpIdeNode
|
||||
public required ReactiveProperty<bool> IsDirty { get; init; }
|
||||
public required bool SuppressDiskChangeEvents { get; set; } // probably has concurrency issues
|
||||
public required DateTimeOffset? LastIdeWriteTime { get; set; }
|
||||
public EventWrapper<Task> FileContentsChangedExternallyFromDisk { get; } = new(() => Task.CompletedTask); // Refactor to global event - this currently doesn't handle updating un-opened files
|
||||
public EventWrapper<Task> FileContentsChangedExternally { get; } = new(() => Task.CompletedTask);
|
||||
|
||||
[SetsRequiredMembers]
|
||||
|
||||
Reference in New Issue
Block a user