From 36a2acbdf7d1410285831647431acc1d115e91b9 Mon Sep 17 00:00:00 2001 From: Matt Parker <61717342+MattParkerDev@users.noreply.github.com> Date: Mon, 20 Oct 2025 19:06:47 +1000 Subject: [PATCH] more file operations --- .../Features/Analysis/RoslynAnalysis.cs | 36 ++++++++++++++++ .../FileWatching/FileChangedService.cs | 19 +++++++++ .../SharpIdeSolutionModificationService.cs | 42 ++++++++++++++++--- 3 files changed, 91 insertions(+), 6 deletions(-) diff --git a/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs b/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs index ebaeed5..f435094 100644 --- a/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs +++ b/src/SharpIDE.Application/Features/Analysis/RoslynAnalysis.cs @@ -749,6 +749,17 @@ public class RoslynAnalysis var project = _workspace!.CurrentSolution.Projects.Single(s => s.FilePath == ((IChildSharpIdeNode)fileModel).GetNearestProjectNode()!.FilePath); + var existingDocument = fileModel switch + { + { IsRazorFile: true } => project.AdditionalDocuments.SingleOrDefault(s => s.FilePath == fileModel.Path), + { IsCsharpFile: true } => project.Documents.SingleOrDefault(s => s.FilePath == fileModel.Path), + _ => throw new InvalidOperationException("AddDocument failed: File is not a workspace file") + }; + if (existingDocument is not null) + { + throw new InvalidOperationException($"AddDocument failed: Document '{fileModel.Path}' already exists in workspace"); + } + var sourceText = SourceText.From(content, Encoding.UTF8); var newSolution = fileModel switch @@ -760,4 +771,29 @@ public class RoslynAnalysis _workspace.TryApplyChanges(newSolution); } + + public async Task RemoveDocument(SharpIdeFile fileModel) + { + using var _ = SharpIdeOtel.Source.StartActivity($"{nameof(RoslynAnalysis)}.{nameof(AddDocument)}"); + await _solutionLoadedTcs.Task; + Guard.Against.Null(fileModel, nameof(fileModel)); + + var project = _workspace!.CurrentSolution.Projects.Single(s => s.FilePath == ((IChildSharpIdeNode)fileModel).GetNearestProjectNode()!.FilePath); + + var document = fileModel switch + { + { IsRazorFile: true } => project.AdditionalDocuments.Single(s => s.FilePath == fileModel.Path), + { IsCsharpFile: true } => project.Documents.Single(s => s.FilePath == fileModel.Path), + _ => throw new InvalidOperationException("UpdateDocument failed: File is not in workspace") + }; + + var newSolution = fileModel switch + { + { IsRazorFile: true } => _workspace.CurrentSolution.RemoveAdditionalDocument(document.Id), + { IsCsharpFile: true } => _workspace.CurrentSolution.RemoveDocument(document.Id), + _ => throw new InvalidOperationException("AddDocument failed: File is not in workspace") + }; + + _workspace.TryApplyChanges(newSolution); + } } diff --git a/src/SharpIDE.Application/Features/FileWatching/FileChangedService.cs b/src/SharpIDE.Application/Features/FileWatching/FileChangedService.cs index ad4a44c..4409cd6 100644 --- a/src/SharpIDE.Application/Features/FileWatching/FileChangedService.cs +++ b/src/SharpIDE.Application/Features/FileWatching/FileChangedService.cs @@ -32,6 +32,14 @@ public class FileChangedService(RoslynAnalysis roslynAnalysis, IdeOpenTabsFileMa // TODO: handle csproj added } + public async Task SharpIdeFileRemoved(SharpIdeFile file) + { + if (file.IsRoslynWorkspaceFile) + { + await HandleWorkspaceFileRemoved(file); + } + } + // All file changes should go via this service public async Task SharpIdeFileChanged(SharpIdeFile file, string newContents, FileChangeType changeType) { @@ -106,4 +114,15 @@ public class FileChangedService(RoslynAnalysis roslynAnalysis, IdeOpenTabsFileMa GlobalEvents.Instance.SolutionAltered.InvokeParallelFireAndForget(); await _roslynAnalysis.UpdateSolutionDiagnostics(newCts.Token); } + + private async Task HandleWorkspaceFileRemoved(SharpIdeFile file) + { + var newCts = new CancellationTokenSource(); + var oldCts = Interlocked.Exchange(ref _updateSolutionDiagnosticsCts, newCts); + await oldCts.CancelAsync(); + oldCts.Dispose(); + await _roslynAnalysis.RemoveDocument(file); + GlobalEvents.Instance.SolutionAltered.InvokeParallelFireAndForget(); + await _roslynAnalysis.UpdateSolutionDiagnostics(newCts.Token); + } } diff --git a/src/SharpIDE.Application/Features/FileWatching/SharpIdeSolutionModificationService.cs b/src/SharpIDE.Application/Features/FileWatching/SharpIdeSolutionModificationService.cs index df69956..bd70138 100644 --- a/src/SharpIDE.Application/Features/FileWatching/SharpIdeSolutionModificationService.cs +++ b/src/SharpIDE.Application/Features/FileWatching/SharpIdeSolutionModificationService.cs @@ -1,21 +1,27 @@ -using SharpIDE.Application.Features.SolutionDiscovery; +using System.Collections.Concurrent; +using Microsoft.CodeAnalysis; +using SharpIDE.Application.Features.SolutionDiscovery; using SharpIDE.Application.Features.SolutionDiscovery.VsPersistence; namespace SharpIDE.Application.Features.FileWatching; /// Does not do any file system operations, only modifies the in-memory solution model -public class SharpIdeSolutionModificationService +public class SharpIdeSolutionModificationService(FileChangedService fileChangedService) { + private readonly FileChangedService _fileChangedService = fileChangedService; + public SharpIdeSolutionModel SolutionModel { get; set; } = null!; /// The directory must already exist on disk public async Task AddDirectory(SharpIdeFolder parentFolder, string directoryName) { - // Passing [] to allFiles and allFolders, as we assume that a brand new folder has no subfolders or files yet var addedDirectoryPath = Path.Combine(parentFolder.Path, directoryName); - var sharpIdeFolder = new SharpIdeFolder(new DirectoryInfo(addedDirectoryPath), parentFolder, [], []); + var allFiles = new ConcurrentBag(); + var allFolders = new ConcurrentBag(); + var sharpIdeFolder = new SharpIdeFolder(new DirectoryInfo(addedDirectoryPath), parentFolder, allFiles, allFolders); parentFolder.Folders.Add(sharpIdeFolder); - SolutionModel.AllFolders.Add(sharpIdeFolder); + SolutionModel.AllFolders.AddRange((IEnumerable)[sharpIdeFolder, ..allFolders]); + SolutionModel.AllFiles.AddRange(allFiles); return sharpIdeFolder; } @@ -23,6 +29,30 @@ public class SharpIdeSolutionModificationService { var parentFolderOrProject = (IFolderOrProject)folder.Parent; parentFolderOrProject.Folders.Remove(folder); - SolutionModel.AllFolders.Remove(folder); + + // Also remove all child files and folders from SolutionModel.AllFiles and AllFolders + var foldersToRemove = new List(); + + var stack = new Stack(); + stack.Push(folder); + while (stack.Count > 0) + { + var current = stack.Pop(); + foldersToRemove.Add(current); + + foreach (var subfolder in current.Folders) + { + stack.Push(subfolder); + } + } + + var filesToRemove = foldersToRemove.SelectMany(f => f.Files).ToList(); + + SolutionModel.AllFiles.RemoveRange(filesToRemove); + SolutionModel.AllFolders.RemoveRange(foldersToRemove); + foreach (var file in filesToRemove) + { + await _fileChangedService.SharpIdeFileRemoved(file); + } } }