using Microsoft.CodeAnalysis.Threading; 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 CompletionChange // Apply only in memory, as well as notify tabs of new content } public class FileChangedService(RoslynAnalysis roslynAnalysis, IdeOpenTabsFileManager openTabsFileManager) { private readonly RoslynAnalysis _roslynAnalysis = roslynAnalysis; private readonly IdeOpenTabsFileManager _openTabsFileManager = openTabsFileManager; public SharpIdeSolutionModel SolutionModel { get; set; } = null!; public async Task SharpIdeFileRenamed(SharpIdeFile file, string oldFilePath) { if (file.IsRoslynWorkspaceFile) { await HandleWorkspaceFileRenamed(file, oldFilePath); } // TODO: handle csproj moved } public async Task SharpIdeFileMoved(SharpIdeFile file, string oldFilePath) { if (file.IsRoslynWorkspaceFile) { await HandleWorkspaceFileMoved(file, oldFilePath); } // TODO: handle csproj moved } public async Task SharpIdeFileAdded(SharpIdeFile file, string content) { if (file.IsRoslynWorkspaceFile) { await HandleWorkspaceFileAdded(file, content); } // TODO: handle csproj added } public async Task SharpIdeFileRemoved(SharpIdeFile file) { await file.FileDeleted.InvokeParallelAsync(); if (file.IsRoslynWorkspaceFile) { await HandleWorkspaceFileRemoved(file); } } // All file changes should go via this service public async Task SharpIdeFileChanged(SharpIdeFile file, string newContents, FileChangeType changeType, SharpIdeFileLinePosition? linePosition = null) { if (changeType is FileChangeType.ExternalChange) { // Disk is already up to date // Update any open tabs // update in memory await _openTabsFileManager.UpdateFileTextInMemoryIfOpen(file, newContents); file.FileContentsChangedExternally.InvokeParallelFireAndForget(linePosition); } else if (changeType is FileChangeType.CodeActionChange) { // update in memory, tabs and save to disk await _openTabsFileManager.UpdateInMemoryIfOpenAndSaveAsync(file, newContents); file.FileContentsChangedExternally.InvokeParallelFireAndForget(linePosition); } else if (changeType is FileChangeType.CompletionChange) { // update in memory, tabs await _openTabsFileManager.UpdateFileTextInMemory(file, newContents); file.FileContentsChangedExternally.InvokeParallelFireAndForget(linePosition); } 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 CancellationSeries _updateSolutionDiagnosticsCtSeries = new(); private async Task HandleCsprojChanged(SharpIdeFile file) { var project = SolutionModel.AllProjects.SingleOrDefault(p => p.FilePath == file.Path); if (project is null) return; var newCts = _updateSolutionDiagnosticsCtSeries.CreateNext(); await ProjectEvaluation.ReloadProject(file.Path); await _roslynAnalysis.ReloadProject(project, CancellationToken.None); GlobalEvents.Instance.SolutionAltered.InvokeParallelFireAndForget(); await _roslynAnalysis.UpdateSolutionDiagnostics(newCts); } private async Task HandleWorkspaceFileChanged(SharpIdeFile file, string newContents) { var newCts = _updateSolutionDiagnosticsCtSeries.CreateNext(); await _roslynAnalysis.UpdateDocument(file, newContents); GlobalEvents.Instance.SolutionAltered.InvokeParallelFireAndForget(); await _roslynAnalysis.UpdateSolutionDiagnostics(newCts); } private async Task HandleWorkspaceFileAdded(SharpIdeFile file, string contents) { var newCts = _updateSolutionDiagnosticsCtSeries.CreateNext(); await _roslynAnalysis.AddDocument(file, contents); GlobalEvents.Instance.SolutionAltered.InvokeParallelFireAndForget(); await _roslynAnalysis.UpdateSolutionDiagnostics(newCts); } private async Task HandleWorkspaceFileRemoved(SharpIdeFile file) { var newCts = _updateSolutionDiagnosticsCtSeries.CreateNext(); await _roslynAnalysis.RemoveDocument(file); GlobalEvents.Instance.SolutionAltered.InvokeParallelFireAndForget(); await _roslynAnalysis.UpdateSolutionDiagnostics(newCts); } private async Task HandleWorkspaceFileMoved(SharpIdeFile file, string oldFilePath) { var newCts = _updateSolutionDiagnosticsCtSeries.CreateNext(); await _roslynAnalysis.MoveDocument(file, oldFilePath); GlobalEvents.Instance.SolutionAltered.InvokeParallelFireAndForget(); await _roslynAnalysis.UpdateSolutionDiagnostics(newCts); } private async Task HandleWorkspaceFileRenamed(SharpIdeFile file, string oldFilePath) { var newCts = _updateSolutionDiagnosticsCtSeries.CreateNext(); await _roslynAnalysis.RenameDocument(file, oldFilePath); GlobalEvents.Instance.SolutionAltered.InvokeParallelFireAndForget(); await _roslynAnalysis.UpdateSolutionDiagnostics(newCts); } }