diff --git a/src/VisualStudio/Core/Def/Implementation/ProjectSystem/SolutionChangeAccumulator.cs b/src/VisualStudio/Core/Def/Implementation/ProjectSystem/SolutionChangeAccumulator.cs index d8978c4761789a3af2eec8f35b7154338b0702b8..bd88465eae2c511781feb642b36ab70c70771530 100644 --- a/src/VisualStudio/Core/Def/Implementation/ProjectSystem/SolutionChangeAccumulator.cs +++ b/src/VisualStudio/Core/Def/Implementation/ProjectSystem/SolutionChangeAccumulator.cs @@ -2,6 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +#nullable enable + +using System; using System.Collections.Generic; using Microsoft.CodeAnalysis; @@ -19,6 +22,7 @@ internal class SolutionChangeAccumulator /// if we can't give a more precise type. /// private WorkspaceChangeKind? _workspaceChangeKind; + private readonly List _documentIdsRemoved = new List(); public SolutionChangeAccumulator(Solution startingSolution) { @@ -26,12 +30,13 @@ public SolutionChangeAccumulator(Solution startingSolution) } public Solution Solution { get; private set; } + public IEnumerable DocumentIdsRemoved => _documentIdsRemoved; public bool HasChange => _workspaceChangeKind.HasValue; - public WorkspaceChangeKind WorkspaceChangeKind => _workspaceChangeKind.Value; + public WorkspaceChangeKind WorkspaceChangeKind => _workspaceChangeKind!.Value; - public ProjectId WorkspaceChangeProjectId { get; private set; } - public DocumentId WorkspaceChangeDocumentId { get; private set; } + public ProjectId? WorkspaceChangeProjectId { get; private set; } + public DocumentId? WorkspaceChangeDocumentId { get; private set; } public void UpdateSolutionForDocumentAction(Solution newSolution, WorkspaceChangeKind changeKind, IEnumerable documentIds) { @@ -75,6 +80,17 @@ public void UpdateSolutionForDocumentAction(Solution newSolution, WorkspaceChang } } + /// + /// The same as but also records + /// the removed documents into . + /// + public void UpdateSolutionForRemovedDocumentAction(Solution solution, WorkspaceChangeKind removeDocumentChangeKind, IEnumerable documentIdsRemoved) + { + UpdateSolutionForDocumentAction(solution, removeDocumentChangeKind, documentIdsRemoved); + + _documentIdsRemoved.AddRange(documentIdsRemoved); + } + /// /// Should be called to update the solution if there isn't a specific document change kind that should be /// given to diff --git a/src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioProject.cs b/src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioProject.cs index ae7037c173507a4a49e168b576131cf5f4d88311..22c1a19e04c3a1c51c7081ea87943cb1c93b57b5 100644 --- a/src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioProject.cs +++ b/src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioProject.cs @@ -422,13 +422,7 @@ private void OnBatchScopeDisposed() documentsToOpen, (s, documents) => s.AddDocuments(documents), WorkspaceChangeKind.DocumentAdded, - (s, id) => - { - // Clear any document-specific data now (like open file trackers, etc.). If we called OnRemoveDocument directly this is - // called, but since we're doing this in one large batch we need to do it now. - _workspace.ClearDocumentData(id); - return s.RemoveDocument(id); - }, + (s, ids) => s.RemoveDocuments(ids), WorkspaceChangeKind.DocumentRemoved); _additionalFiles.UpdateSolutionForBatch( @@ -445,13 +439,7 @@ private void OnBatchScopeDisposed() return s; }, WorkspaceChangeKind.AdditionalDocumentAdded, - (s, id) => - { - // Clear any document-specific data now (like open file trackers, etc.). If we called OnRemoveDocument directly this is - // called, but since we're doing this in one large batch we need to do it now. - _workspace.ClearDocumentData(id); - return s.RemoveAdditionalDocument(id); - }, + (s, ids) => s.RemoveAdditionalDocuments(ids), WorkspaceChangeKind.AdditionalDocumentRemoved); _analyzerConfigFiles.UpdateSolutionForBatch( @@ -460,13 +448,7 @@ private void OnBatchScopeDisposed() analyzerConfigDocumentsToOpen, (s, documents) => s.AddAnalyzerConfigDocuments(documents), WorkspaceChangeKind.AnalyzerConfigDocumentAdded, - (s, id) => - { - // Clear any document-specific data now (like open file trackers, etc.). If we called OnRemoveAnalyzerConfigDocument directly this is - // called, but since we're doing this in one large batch we need to do it now. - _workspace.ClearDocumentData(id); - return s.RemoveAnalyzerConfigDocument(id); - }, + (s, ids) => s.RemoveAnalyzerConfigDocuments(ids), WorkspaceChangeKind.AnalyzerConfigDocumentRemoved); // Metadata reference adding... @@ -1570,7 +1552,7 @@ public void ReorderFiles(ImmutableArray filePaths) List<(DocumentId documentId, SourceTextContainer textContainer)> documentsToOpen, Func, Solution> addDocuments, WorkspaceChangeKind addDocumentChangeKind, - Func removeDocument, + Func, Solution> removeDocuments, WorkspaceChangeKind removeDocumentChangeKind) { // Document adding... @@ -1592,12 +1574,9 @@ public void ReorderFiles(ImmutableArray filePaths) ClearAndZeroCapacity(_documentsAddedInBatch); // Document removing... - foreach (var documentId in _documentsRemovedInBatch) - { - solutionChanges.UpdateSolutionForDocumentAction(removeDocument(solutionChanges.Solution, documentId), - removeDocumentChangeKind, - SpecializedCollections.SingletonEnumerable(documentId)); - } + solutionChanges.UpdateSolutionForRemovedDocumentAction(removeDocuments(solutionChanges.Solution, _documentsRemovedInBatch.ToImmutableArray()), + removeDocumentChangeKind, + _documentsRemovedInBatch); ClearAndZeroCapacity(_documentsRemovedInBatch); diff --git a/src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioWorkspaceImpl.cs b/src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioWorkspaceImpl.cs index afa356cf0172600ade5d7a5f4dee81de4057d26a..44c81d65f6871152693d0605f147ddcf78e2f78b 100644 --- a/src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioWorkspaceImpl.cs +++ b/src/VisualStudio/Core/Def/Implementation/ProjectSystem/VisualStudioWorkspaceImpl.cs @@ -1468,6 +1468,8 @@ internal override bool CanAddProjectReference(ProjectId referencingProject, Proj return result != (uint)__VSREFERENCEQUERYRESULT.REFERENCE_DENY; } +#nullable enable + /// /// Applies a single operation to the workspace. should be a call to one of the protected Workspace.On* methods. /// @@ -1496,6 +1498,11 @@ public void ApplyBatchChangeToWorkspace(Func _projectReferenceInfoMap = new Dictionary(); private ProjectReferenceInformation GetReferenceInfo_NoLock(ProjectId projectId) diff --git a/src/Workspaces/Core/Portable/PublicAPI.Unshipped.txt b/src/Workspaces/Core/Portable/PublicAPI.Unshipped.txt index b6abe4720320cc6060b6eadd57dce112b0458efc..fc046ba72a8ab8c08a938baf214283dcfe696d08 100644 --- a/src/Workspaces/Core/Portable/PublicAPI.Unshipped.txt +++ b/src/Workspaces/Core/Portable/PublicAPI.Unshipped.txt @@ -1,6 +1,10 @@ *REMOVED*Microsoft.CodeAnalysis.TextDocument.Project.set -> void *REMOVED*Microsoft.CodeAnalysis.TextDocument.TextDocument() -> void Microsoft.CodeAnalysis.Options.DocumentOptionSet.WithChangedOption(Microsoft.CodeAnalysis.Options.PerLanguageOption option, T value) -> Microsoft.CodeAnalysis.Options.DocumentOptionSet +Microsoft.CodeAnalysis.Project.RemoveDocuments(System.Collections.Immutable.ImmutableArray documentIds) -> Microsoft.CodeAnalysis.Project +Microsoft.CodeAnalysis.Solution.RemoveAdditionalDocuments(System.Collections.Immutable.ImmutableArray documentIds) -> Microsoft.CodeAnalysis.Solution +Microsoft.CodeAnalysis.Solution.RemoveAnalyzerConfigDocuments(System.Collections.Immutable.ImmutableArray documentIds) -> Microsoft.CodeAnalysis.Solution +Microsoft.CodeAnalysis.Solution.RemoveDocuments(System.Collections.Immutable.ImmutableArray documentIds) -> Microsoft.CodeAnalysis.Solution Microsoft.CodeAnalysis.Solution.WithOptions(Microsoft.CodeAnalysis.Options.OptionSet options) -> Microsoft.CodeAnalysis.Solution static Microsoft.CodeAnalysis.Formatting.Formatter.OrganizeImportsAsync(Microsoft.CodeAnalysis.Document document, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task static Microsoft.CodeAnalysis.Simplification.Simplifier.AddImportsAnnotation.get -> Microsoft.CodeAnalysis.SyntaxAnnotation diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs index fc94400de51d8655fdcf6da399ac3d484409ea26..84abc817f33eb3b1d9bbe41a7919d6b1d32aac96 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs @@ -14,6 +14,7 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; using Roslyn.Collections.Immutable; using Roslyn.Utilities; @@ -599,9 +600,28 @@ public TextDocument AddAnalyzerConfigDocument(string name, SourceText text, IEnu /// public Project RemoveDocument(DocumentId documentId) { + // NOTE: the method isn't checking if documentId belongs to the project. This probably should be done, but may be a compat change. + // https://github.com/dotnet/roslyn/issues/41211 tracks this investigation. return this.Solution.RemoveDocument(documentId).GetProject(this.Id)!; } + /// + /// Creates a new instance of this project updated to no longer include the specified documents. + /// + public Project RemoveDocuments(ImmutableArray documentIds) + { + foreach (var documentId in documentIds) + { + // Handling of null entries is handled by Solution.RemoveDocuments. + if (documentId?.ProjectId != this.Id) + { + throw new ArgumentException(string.Format(WorkspacesResources._0_is_in_a_different_project, documentId)); + } + } + + return this.Solution.RemoveDocuments(documentIds).GetRequiredProject(this.Id); + } + /// /// Creates a new instance of this project updated to no longer include the specified additional document. /// diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs index 962eb68f1a9859a2578627b7b21686b4d24dfdd9..2ea5742a7ad0d363148d50ab2d389f7616b3c514 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs @@ -784,31 +784,25 @@ private ProjectState CreateNewStateForChangedAnalyzerConfigDocuments(ImmutableSo analyzerConfigSet: newAnalyzerConfigSet); } - public ProjectState RemoveDocument(DocumentId documentId) + public ProjectState RemoveDocuments(ImmutableArray documentIds) { - Debug.Assert(this.DocumentStates.ContainsKey(documentId)); - return this.With( projectInfo: this.ProjectInfo.WithVersion(this.Version.GetNewerVersion()), - documentIds: _documentIds.Remove(documentId), - documentStates: _documentStates.Remove(documentId)); + documentIds: _documentIds.RemoveRange(documentIds), + documentStates: _documentStates.RemoveRange(documentIds)); } - public ProjectState RemoveAdditionalDocument(DocumentId documentId) + public ProjectState RemoveAdditionalDocuments(ImmutableArray documentIds) { - Debug.Assert(this.AdditionalDocumentStates.ContainsKey(documentId)); - return this.With( projectInfo: this.ProjectInfo.WithVersion(this.Version.GetNewerVersion()), - additionalDocumentIds: _additionalDocumentIds.Remove(documentId), - additionalDocumentStates: _additionalDocumentStates.Remove(documentId)); + additionalDocumentIds: _additionalDocumentIds.RemoveRange(documentIds), + additionalDocumentStates: _additionalDocumentStates.RemoveRange(documentIds)); } - public ProjectState RemoveAnalyzerConfigDocument(DocumentId documentId) + public ProjectState RemoveAnalyzerConfigDocuments(ImmutableArray documentIds) { - Debug.Assert(_analyzerConfigDocumentStates.ContainsKey(documentId)); - - var newAnalyzerConfigDocumentStates = _analyzerConfigDocumentStates.Remove(documentId); + var newAnalyzerConfigDocumentStates = _analyzerConfigDocumentStates.RemoveRange(documentIds); return CreateNewStateForChangedAnalyzerConfigDocuments(newAnalyzerConfigDocumentStates); } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs index 1d1ee85e737880f2b07140676da2802fadec7641..7316afd69e0c8a1ce332432ac860ab016178f04b 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs @@ -891,7 +891,20 @@ public Solution AddAnalyzerConfigDocuments(ImmutableArray document /// public Solution RemoveDocument(DocumentId documentId) { - var newState = _state.RemoveDocument(documentId); + if (documentId == null) + { + throw new ArgumentNullException(nameof(documentId)); + } + + return RemoveDocuments(ImmutableArray.Create(documentId)); + } + + /// + /// Creates a new solution instance that no longer includes the specified documents. + /// + public Solution RemoveDocuments(ImmutableArray documentIds) + { + var newState = _state.RemoveDocuments(documentIds); if (newState == _state) { return this; @@ -905,7 +918,20 @@ public Solution RemoveDocument(DocumentId documentId) /// public Solution RemoveAdditionalDocument(DocumentId documentId) { - var newState = _state.RemoveAdditionalDocument(documentId); + if (documentId == null) + { + throw new ArgumentNullException(nameof(documentId)); + } + + return RemoveAdditionalDocuments(ImmutableArray.Create(documentId)); + } + + /// + /// Creates a new solution instance that no longer includes the specified additional documents. + /// + public Solution RemoveAdditionalDocuments(ImmutableArray documentIds) + { + var newState = _state.RemoveAdditionalDocuments(documentIds); if (newState == _state) { return this; @@ -914,9 +940,25 @@ public Solution RemoveAdditionalDocument(DocumentId documentId) return new Solution(newState); } + /// + /// Creates a new solution instance that no longer includes the specified . + /// public Solution RemoveAnalyzerConfigDocument(DocumentId documentId) { - var newState = _state.RemoveAnalyzerConfigDocument(documentId); + if (documentId == null) + { + throw new ArgumentNullException(nameof(documentId)); + } + + return RemoveAnalyzerConfigDocuments(ImmutableArray.Create(documentId)); + } + + /// + /// Creates a new solution instance that no longer includes the specified s. + /// + public Solution RemoveAnalyzerConfigDocuments(ImmutableArray documentIds) + { + var newState = _state.RemoveAnalyzerConfigDocuments(documentIds); if (newState == _state) { return this; diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTranslationAction.Actions.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTranslationAction.Actions.cs index 11de84429eddafe819dc6cb3541002d3e090219a..fb921f24fec03011a13d9d210ab82c536676ff61 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTranslationAction.Actions.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CompilationTranslationAction.Actions.cs @@ -33,14 +33,25 @@ public override Task InvokeAsync(Compilation oldCompilation, Cancel public DocumentId DocumentId => _newState.Attributes.Id; } - internal sealed class RemoveDocumentAction : SimpleCompilationTranslationAction + internal sealed class RemoveDocumentsAction : CompilationTranslationAction { - private static readonly Func> s_action = - async (o, d, c) => o.RemoveSyntaxTrees(await d.GetSyntaxTreeAsync(c).ConfigureAwait(false)); + private readonly ImmutableArray _documents; - public RemoveDocumentAction(DocumentState document) - : base(document, s_action) + public RemoveDocumentsAction(ImmutableArray documents) { + _documents = documents; + } + + public override async Task InvokeAsync(Compilation oldCompilation, CancellationToken cancellationToken) + { + var syntaxTrees = new List(_documents.Length); + foreach (var document in _documents) + { + cancellationToken.ThrowIfCancellationRequested(); + syntaxTrees.Add(await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false)); + } + + return oldCompilation.RemoveSyntaxTrees(syntaxTrees); } } @@ -55,9 +66,10 @@ public AddDocumentsAction(ImmutableArray documents) public override async Task InvokeAsync(Compilation oldCompilation, CancellationToken cancellationToken) { - var syntaxTrees = new List(); + var syntaxTrees = new List(capacity: _documents.Length); foreach (var document in _documents) { + cancellationToken.ThrowIfCancellationRequested(); syntaxTrees.Add(await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false)); } @@ -88,42 +100,33 @@ public override async Task InvokeAsync(Compilation oldCompilation, } } - internal sealed class ProjectCompilationOptionsAction : SimpleCompilationTranslationAction + internal sealed class ProjectCompilationOptionsAction : CompilationTranslationAction { - private static readonly Func> s_action = - (o, d, c) => Task.FromResult(o.WithOptions(d)); + private readonly CompilationOptions _options; - public ProjectCompilationOptionsAction(CompilationOptions option) - : base(option, s_action) + public ProjectCompilationOptionsAction(CompilationOptions options) { + _options = options; } - } - internal sealed class ProjectAssemblyNameAction : SimpleCompilationTranslationAction - { - private static readonly Func> s_action = - (o, d, c) => Task.FromResult(o.WithAssemblyName(d)); - - public ProjectAssemblyNameAction(string assemblyName) - : base(assemblyName, s_action) + public override Task InvokeAsync(Compilation oldCompilation, CancellationToken cancellationToken) { + return Task.FromResult(oldCompilation.WithOptions(_options)); } } - internal class SimpleCompilationTranslationAction : CompilationTranslationAction + internal sealed class ProjectAssemblyNameAction : CompilationTranslationAction { - private readonly T _data; - private readonly Func> _action; + private readonly string _assemblyName; - public SimpleCompilationTranslationAction(T data, Func> action) + public ProjectAssemblyNameAction(string assemblyName) { - _data = data; - _action = action; + _assemblyName = assemblyName; } public override Task InvokeAsync(Compilation oldCompilation, CancellationToken cancellationToken) { - return _action(oldCompilation, _data, cancellationToken); + return Task.FromResult(oldCompilation.WithAssemblyName(_assemblyName)); } } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs index d65c4707dcc0ba59a29fff4adc2e4ef4cf8a35bf..87b14dc7112e794519a24f6989a35dfbc88640d3 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs @@ -1217,23 +1217,18 @@ public SolutionState AddDocuments(ImmutableArray documentInfos) foreach (var documentInfosInProject in documentInfosByProjectId) { CheckContainsProject(documentInfosInProject.Key); - var oldProject = this.GetProjectState(documentInfosInProject.Key); - - if (oldProject == null) - { - throw new InvalidOperationException(string.Format(WorkspacesResources._0_is_not_part_of_the_workspace, documentInfosInProject.Key)); - } + var oldProjectState = this.GetProjectState(documentInfosInProject.Key)!; var newDocumentStatesForProjectBuilder = ArrayBuilder.GetInstance(); foreach (var documentInfo in documentInfosInProject) { - newDocumentStatesForProjectBuilder.Add(createDocumentState(documentInfo, oldProject)); + newDocumentStatesForProjectBuilder.Add(createDocumentState(documentInfo, oldProjectState)); } var newDocumentStatesForProject = newDocumentStatesForProjectBuilder.ToImmutableAndFree(); - var (newProjectState, compilationTranslationAction) = addDocumentsToProjectState(oldProject, newDocumentStatesForProject); + var (newProjectState, compilationTranslationAction) = addDocumentsToProjectState(oldProjectState, newDocumentStatesForProject); newSolutionState = newSolutionState.ForkProject(newProjectState, compilationTranslationAction, @@ -1263,51 +1258,85 @@ public SolutionState AddAnalyzerConfigDocuments(ImmutableArray doc }); } - public SolutionState RemoveAnalyzerConfigDocument(DocumentId documentId) + public SolutionState RemoveAnalyzerConfigDocuments(ImmutableArray documentIds) { - CheckContainsAnalyzerConfigDocument(documentId); - - var oldProject = this.GetProjectState(documentId.ProjectId)!; - var newProject = oldProject.RemoveAnalyzerConfigDocument(documentId); - var removedDocumentStates = SpecializedCollections.SingletonEnumerable(oldProject.GetAnalyzerConfigDocumentState(documentId)!); - - return this.ForkProject( - newProject, - new CompilationTranslationAction.ReplaceAllSyntaxTreesAction(newProject), - newFilePathToDocumentIdsMap: CreateFilePathToDocumentIdsMapWithRemovedDocuments(removedDocumentStates)); + return RemoveDocumentsFromMultipleProjects(documentIds, + (projectState, documentId) => { CheckContainsAnalyzerConfigDocument(documentId); return projectState.GetAnalyzerConfigDocumentState(documentId)!; }, + (oldProject, documentIds, _) => + { + var newProject = oldProject.RemoveAnalyzerConfigDocuments(documentIds); + return (newProject, new CompilationTranslationAction.ReplaceAllSyntaxTreesAction(newProject)); + }); } /// /// Creates a new solution instance that no longer includes the specified document. /// - public SolutionState RemoveDocument(DocumentId documentId) + public SolutionState RemoveDocuments(ImmutableArray documentIds) { - CheckContainsDocument(documentId); + return RemoveDocumentsFromMultipleProjects(documentIds, + (projectState, documentId) => { CheckContainsDocument(documentId); return projectState.GetDocumentState(documentId)!; }, + (projectState, documentIds, documentStates) => (projectState.RemoveDocuments(documentIds), new CompilationTranslationAction.RemoveDocumentsAction(documentStates))); + } - var oldProject = this.GetProjectState(documentId.ProjectId)!; - var oldDocument = oldProject.GetDocumentState(documentId); - var newProject = oldProject.RemoveDocument(documentId); - var removedDocumentStates = SpecializedCollections.SingletonEnumerable(oldProject.GetDocumentState(documentId)!); + private SolutionState RemoveDocumentsFromMultipleProjects( + ImmutableArray documentIds, + Func getExistingTextDocumentState, + Func, ImmutableArray, (ProjectState newState, CompilationTranslationAction? translationAction)> removeDocumentsFromProjectState) + where T : TextDocumentState + { + if (documentIds.IsDefault) + { + throw new ArgumentNullException(nameof(documentIds)); + } - return this.ForkProject( - newProject, - new CompilationTranslationAction.RemoveDocumentAction(oldDocument), - newFilePathToDocumentIdsMap: CreateFilePathToDocumentIdsMapWithRemovedDocuments(removedDocumentStates)); + if (documentIds.IsEmpty) + { + return this; + } + + // The documents might be contributing to multiple different projects; split them by project and then we'll process + // project-at-a-time. + var documentIdsByProjectId = documentIds.ToLookup(id => id.ProjectId); + + var newSolutionState = this; + + foreach (var documentIdsInProject in documentIdsByProjectId) + { + var oldProjectState = this.GetProjectState(documentIdsInProject.Key); + + if (oldProjectState == null) + { + throw new InvalidOperationException(string.Format(WorkspacesResources._0_is_not_part_of_the_workspace, documentIdsInProject.Key)); + } + + var removedDocumentStatesBuilder = ArrayBuilder.GetInstance(); + + foreach (var documentId in documentIdsInProject) + { + removedDocumentStatesBuilder.Add(getExistingTextDocumentState(oldProjectState, documentId)); + } + + var removedDocumentStatesForProject = removedDocumentStatesBuilder.ToImmutableAndFree(); + + var (newProjectState, compilationTranslationAction) = removeDocumentsFromProjectState(oldProjectState, documentIdsInProject.ToImmutableArray(), removedDocumentStatesForProject); + + newSolutionState = newSolutionState.ForkProject(newProjectState, + compilationTranslationAction, + newFilePathToDocumentIdsMap: CreateFilePathToDocumentIdsMapWithRemovedDocuments(removedDocumentStatesForProject)); + } + + return newSolutionState; } /// - /// Creates a new solution instance that no longer includes the specified additional document. + /// Creates a new solution instance that no longer includes the specified additional documents. /// - public SolutionState RemoveAdditionalDocument(DocumentId documentId) + public SolutionState RemoveAdditionalDocuments(ImmutableArray documentIds) { - CheckContainsAdditionalDocument(documentId); - - var oldProject = this.GetProjectState(documentId.ProjectId)!; - var newProject = oldProject.RemoveAdditionalDocument(documentId); - var documentStates = SpecializedCollections.SingletonEnumerable(GetAdditionalDocumentState(documentId)!); - - return this.ForkProject(newProject, - newFilePathToDocumentIdsMap: CreateFilePathToDocumentIdsMapWithRemovedDocuments(documentStates)); + return RemoveDocumentsFromMultipleProjects(documentIds, + (projectState, documentId) => { CheckContainsAdditionalDocument(documentId); return projectState.GetAdditionalDocumentState(documentId)!; }, + (projectState, documentIds, documentStates) => (projectState.RemoveAdditionalDocuments(documentIds), translationAction: null)); } /// diff --git a/src/Workspaces/Core/Portable/WorkspacesResources.resx b/src/Workspaces/Core/Portable/WorkspacesResources.resx index ad6e613cb2f1412fc772abe663417462740e6e71..3e4e842b9567638871da0e091dfa1028fa4963f4 100644 --- a/src/Workspaces/Core/Portable/WorkspacesResources.resx +++ b/src/Workspaces/Core/Portable/WorkspacesResources.resx @@ -1440,4 +1440,7 @@ Zero-width positive lookbehind assertions are typically used at the beginning of Document does not support syntax trees + + {0} is in a different project. + \ No newline at end of file diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.cs.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.cs.xlf index 146360cf4ebbd6235c2c833e783439b29584d46d..a68e4436df4d5211f1a1ebcadc1d6ddc009a9ce8 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.cs.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.cs.xlf @@ -1317,6 +1317,11 @@ Pozitivní kontrolní výrazy zpětného vyhledávání s nulovou délkou se obv Pracovní prostor není platný. + + {0} is in a different project. + {0} is in a different project. + + '{0}' is not part of the workspace. '{0} není součástí pracovního prostoru. diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.de.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.de.xlf index efb386dc5d18821f7c14d7c73bd5ed43c806f6e7..819124831c936255199563bc299478c8d1775533 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.de.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.de.xlf @@ -1317,6 +1317,11 @@ Positive Lookbehindassertionen mit Nullbreite werden normalerweise am Anfang reg Arbeitsbereich ist nicht leer. + + {0} is in a different project. + {0} is in a different project. + + '{0}' is not part of the workspace. '"{0}" ist nicht Teil des Arbeitsbereichs. diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.es.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.es.xlf index 9466a1b6ce311e54b8f81b5466d05f732c6cd12c..ab2fcb5faaa79250a816c41a5aa16f38e4686e64 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.es.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.es.xlf @@ -1317,6 +1317,11 @@ Las aserciones de búsqueda retrasada (lookbehind) positivas de ancho cero se us El área de trabajo no está vacía. + + {0} is in a different project. + {0} is in a different project. + + '{0}' is not part of the workspace. '{0}' no es parte del área de trabajo. diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.fr.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.fr.xlf index 55c52acf13516fc65c0b55b28f082d465193b039..7590c9cead4dbd38b52dc2cbd60b302c2dc7264a 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.fr.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.fr.xlf @@ -1317,6 +1317,11 @@ Les assertions arrière positives de largeur nulle sont généralement utilisée L'espace de travail n'est pas vide. + + {0} is in a different project. + {0} is in a different project. + + '{0}' is not part of the workspace. '{0}' ne fait pas partie de l'espace de travail. diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.it.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.it.xlf index a9ddff31ae528ea236ff45770d5f089915337456..2b966a9cd331b0019e10d0bded995a5a82f3dae7 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.it.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.it.xlf @@ -1317,6 +1317,11 @@ Le asserzioni lookbehind positive di larghezza zero vengono usate in genere all' L'area di lavoro non è vuota. + + {0} is in a different project. + {0} is in a different project. + + '{0}' is not part of the workspace. '{0}' non fa parte dell'area di lavoro. diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ja.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ja.xlf index f4cb399917fee052b4fdbe937c7ab05b52be2aed..2ff80ddea60fa257d2d2ea143eb898e31ae026b9 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ja.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ja.xlf @@ -1317,6 +1317,11 @@ Zero-width positive lookbehind assertions are typically used at the beginning of ワークスペースが空ではありません。 + + {0} is in a different project. + {0} is in a different project. + + '{0}' is not part of the workspace. '{0}' はワークスペースの一部ではありません。 diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ko.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ko.xlf index 38d531b6a3a347c6d99b89be9a1eeb2ed51f7311..38f0b09f76ffe97cd9245fc84bf18d50f159695d 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ko.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ko.xlf @@ -1317,6 +1317,11 @@ Zero-width positive lookbehind assertions are typically used at the beginning of 작업 영역이 비어 있지 않습니다. + + {0} is in a different project. + {0} is in a different project. + + '{0}' is not part of the workspace. '{0}'은(는) 작업 영역의 일부가 아닙니다. diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pl.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pl.xlf index 4ecb1ab8a590150270f6d6bd579fa62cdb234960..f758fde8f43efcd2b6a31649aead1bd28b4f4ef5 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pl.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pl.xlf @@ -1317,6 +1317,11 @@ Pozytywne asercje wsteczne o zerowej szerokości są zwykle używane na początk Obszar roboczy nie jest pusty. + + {0} is in a different project. + {0} is in a different project. + + '{0}' is not part of the workspace. 'Element „{0}” nie jest częścią obszaru roboczego. diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pt-BR.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pt-BR.xlf index c8bacbfd8336a63c89c4e4ed7af7fc0ab422ace6..2b3acbafc8ba44f64b1fb09b512b788f5f273097 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pt-BR.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pt-BR.xlf @@ -1317,6 +1317,11 @@ As declarações de lookbehind positivas de largura zero normalmente são usadas Workspace não está vazio. + + {0} is in a different project. + {0} is in a different project. + + '{0}' is not part of the workspace. "{0}" não é parte do workspace. diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ru.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ru.xlf index 3c4608aea3c2d161c40032edd4dbcdfec6193650..8ea82d46c7282b1ee05003b75f1fd0e1237bc31b 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ru.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ru.xlf @@ -1317,6 +1317,11 @@ Zero-width positive lookbehind assertions are typically used at the beginning of Рабочая область не пуста. + + {0} is in a different project. + {0} is in a different project. + + '{0}' is not part of the workspace. '"{0}" не является частью рабочей области. diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.tr.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.tr.xlf index 0ed61e67a1b623fd9e685a62d8a9a182019b4dff..c48cba68bc3ec270f46418d182ae74fbb118b792 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.tr.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.tr.xlf @@ -1317,6 +1317,11 @@ Sıfır genişlikli pozitif geri yönlü onaylamalar genellikle normal ifadeleri Çalışma alanı boş değil. + + {0} is in a different project. + {0} is in a different project. + + '{0}' is not part of the workspace. '{0}' çalışma alanının parçası değildir. diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hans.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hans.xlf index 35870bf0a8fc4a03cfce57a6bd4655400d8a0619..9ccd320134ae628ddcc01b2a2c7a2e87d4aa6b61 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hans.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hans.xlf @@ -1317,6 +1317,11 @@ Zero-width positive lookbehind assertions are typically used at the beginning of 工作区不为空。 + + {0} is in a different project. + {0} is in a different project. + + '{0}' is not part of the workspace. '“{0}”不是工作区的一部分。 diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hant.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hant.xlf index 8fdaeb1929e2b92a6564d154068492319196124f..901ebc095268f5d0b6f6aa2bacb660aec7956a0e 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hant.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hant.xlf @@ -1317,6 +1317,11 @@ Zero-width positive lookbehind assertions are typically used at the beginning of 工作區不是空的。 + + {0} is in a different project. + {0} is in a different project. + + '{0}' is not part of the workspace. '{0}' 不是工作區的一部分。 diff --git a/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs b/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs index f1aeb7f3ee5389a1519667a4e8c45033c17115ef..7339e2cbac926d78fa7b865c402fb209e8bab15b 100644 --- a/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs +++ b/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs @@ -193,6 +193,72 @@ public void AddTwoDocumentsWithMissingProject() Assert.ThrowsAny(() => solution.AddDocuments(ImmutableArray.Create(documentInfo1, documentInfo2))); } + [Fact, Trait(Traits.Feature, Traits.Features.Workspace)] + public void RemoveZeroDocuments() + { + var solution = CreateSolution(); + + Assert.Same(solution, solution.RemoveDocuments(ImmutableArray.Empty)); + } + + [Fact, Trait(Traits.Feature, Traits.Features.Workspace)] + public async Task RemoveTwoDocuments() + { + var projectId = ProjectId.CreateNewId(); + + var documentInfo1 = DocumentInfo.Create(DocumentId.CreateNewId(projectId), "file1.cs"); + var documentInfo2 = DocumentInfo.Create(DocumentId.CreateNewId(projectId), "file2.cs"); + + var solution = CreateSolution() + .AddProject(projectId, "project1", "project1.dll", LanguageNames.CSharp) + .AddDocuments(ImmutableArray.Create(documentInfo1, documentInfo2)); + + solution = solution.RemoveDocuments(ImmutableArray.Create(documentInfo1.Id, documentInfo2.Id)); + + var finalProject = solution.Projects.Single(); + Assert.Empty(finalProject.Documents); + Assert.Empty((await finalProject.GetCompilationAsync()).SyntaxTrees); + } + + [Fact, Trait(Traits.Feature, Traits.Features.Workspace)] + public void RemoveTwoDocumentsFromDifferentProjects() + { + var projectId1 = ProjectId.CreateNewId(); + var projectId2 = ProjectId.CreateNewId(); + + var documentInfo1 = DocumentInfo.Create(DocumentId.CreateNewId(projectId1), "file1.cs"); + var documentInfo2 = DocumentInfo.Create(DocumentId.CreateNewId(projectId2), "file2.cs"); + + var solution = CreateSolution() + .AddProject(projectId1, "project1", "project1.dll", LanguageNames.CSharp) + .AddProject(projectId2, "project2", "project2.dll", LanguageNames.CSharp) + .AddDocuments(ImmutableArray.Create(documentInfo1, documentInfo2)); + + Assert.All(solution.Projects, p => Assert.Single(p.Documents)); + + solution = solution.RemoveDocuments(ImmutableArray.Create(documentInfo1.Id, documentInfo2.Id)); + + Assert.All(solution.Projects, p => Assert.Empty(p.Documents)); + } + + [Fact, Trait(Traits.Feature, Traits.Features.Workspace)] + public void RemoveDocumentFromUnrelatedProject() + { + var projectId1 = ProjectId.CreateNewId(); + var projectId2 = ProjectId.CreateNewId(); + + var documentInfo1 = DocumentInfo.Create(DocumentId.CreateNewId(projectId1), "file1.cs"); + + var solution = CreateSolution() + .AddProject(projectId1, "project1", "project1.dll", LanguageNames.CSharp) + .AddProject(projectId2, "project2", "project2.dll", LanguageNames.CSharp) + .AddDocument(documentInfo1); + + // This should throw if we're removing one document from the wrong project. Right now we don't test the RemoveDocument + // API due to https://github.com/dotnet/roslyn/issues/41211. + Assert.Throws(() => solution.GetProject(projectId2).RemoveDocuments(ImmutableArray.Create(documentInfo1.Id))); + } + [Fact, Trait(Traits.Feature, Traits.Features.Workspace)] public async Task TestOneCSharpProjectAsync() {