diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs index f0ab0dfbb98cc..1a71118b9cebc 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs @@ -1064,14 +1064,18 @@ private SolutionCompilationState ComputeFrozenSnapshot(CancellationToken cancell var newIdToProjectStateMapBuilder = this.SolutionState.ProjectStates.ToBuilder(); var newIdToTrackerMapBuilder = _projectIdToTrackerMap.ToBuilder(); - using var _1 = ArrayBuilder.GetInstance(out var documentsToRemove); - using var _2 = ArrayBuilder.GetInstance(out var documentsToAdd); + var filePathToDocumentIdsMapBuilder = this.SolutionState.FilePathToDocumentIdsMap.ToBuilder(); + var filePathToDocumentIdsMapChanged = false; foreach (var projectId in this.SolutionState.ProjectIds) { cancellationToken.ThrowIfCancellationRequested(); - // if we don't have one or it is stale, create a new partial solution + // Definitely do nothing for non-C#/VB projects. We have nothing to freeze in that case. + var oldProjectState = this.SolutionState.GetRequiredProjectState(projectId); + if (!oldProjectState.SupportsCompilation) + continue; + var oldTracker = GetCompilationTracker(projectId); var newTracker = oldTracker.FreezePartialState(cancellationToken); if (oldTracker == newTracker) @@ -1079,7 +1083,6 @@ private SolutionCompilationState ComputeFrozenSnapshot(CancellationToken cancell Contract.ThrowIfFalse(newIdToProjectStateMapBuilder.ContainsKey(projectId)); - var oldProjectState = this.SolutionState.GetRequiredProjectState(projectId); var newProjectState = newTracker.ProjectState; newIdToProjectStateMapBuilder[projectId] = newProjectState; @@ -1110,9 +1113,10 @@ private SolutionCompilationState ComputeFrozenSnapshot(CancellationToken cancell var newIdToProjectStateMap = newIdToProjectStateMapBuilder.ToImmutable(); var newIdToTrackerMap = newIdToTrackerMapBuilder.ToImmutable(); - var filePathToDocumentIdsMap = this.SolutionState.CreateFilePathToDocumentIdsMapWithAddedAndRemovedDocuments( - documentsToAdd: documentsToAdd, - documentsToRemove: documentsToRemove); + var filePathToDocumentIdsMap = filePathToDocumentIdsMapChanged + ? filePathToDocumentIdsMapBuilder.ToImmutable() + : null; + var dependencyGraph = SolutionState.CreateDependencyGraph(this.SolutionState.ProjectIds, newIdToProjectStateMap); var newState = this.SolutionState.Branch( @@ -1132,12 +1136,22 @@ void CheckDocumentStates( TextDocumentStates oldStates, TextDocumentStates newStates) where TDocumentState : TextDocumentState { + if (oldStates.Equals(newStates)) + return; + // Get the trivial sets of documents that are present in one set but not the other. + foreach (var documentId in newStates.GetAddedStateIds(oldStates)) - documentsToAdd.Add(newStates.GetRequiredState(documentId)); + { + filePathToDocumentIdsMapChanged = true; + SolutionState.AddDocumentFilePath(newStates.GetRequiredState(documentId), filePathToDocumentIdsMapBuilder); + } foreach (var documentId in newStates.GetRemovedStateIds(oldStates)) - documentsToRemove.Add(oldStates.GetRequiredState(documentId)); + { + filePathToDocumentIdsMapChanged = true; + SolutionState.RemoveDocumentFilePath(oldStates.GetRequiredState(documentId), filePathToDocumentIdsMapBuilder); + } // Now go through the states that are in both sets. We have to check these all as it is possible for // document to change its file path without its id changing. @@ -1147,8 +1161,9 @@ void CheckDocumentStates( oldDocumentState != newDocumentState && oldDocumentState.FilePath != newDocumentState.FilePath) { - documentsToRemove.Remove(oldDocumentState); - documentsToAdd.Add(newDocumentState); + filePathToDocumentIdsMapChanged = true; + SolutionState.RemoveDocumentFilePath(oldDocumentState, filePathToDocumentIdsMapBuilder); + SolutionState.AddDocumentFilePath(newDocumentState, filePathToDocumentIdsMapBuilder); } } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs index 8a6fb868502e4..5e26a3538c837 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs @@ -220,6 +220,8 @@ public SolutionState WithNewWorkspace( _lazyAnalyzers); } + public ImmutableDictionary> FilePathToDocumentIdsMap => _filePathToDocumentIdsMap; + /// /// The version of the most recently modified project. /// @@ -404,25 +406,6 @@ public SolutionState RemoveProject(ProjectId projectId) dependencyGraph: newDependencyGraph); } - public ImmutableDictionary> CreateFilePathToDocumentIdsMapWithAddedAndRemovedDocuments( - ArrayBuilder documentsToAdd, - ArrayBuilder documentsToRemove) - { - if (documentsToRemove.Count == 0 && documentsToAdd.Count == 0) - return _filePathToDocumentIdsMap; - - var builder = _filePathToDocumentIdsMap.ToBuilder(); - - // Add first, then remove. This helps avoid the case where a filepath now sees no documents, so we remove - // the entry entirely for it in the dictionary, only to add it back in. Adding then removing will at least - // keep the entry, but increase the docs for it, then lower it back down. - - AddDocumentFilePaths(documentsToAdd, builder); - RemoveDocumentFilePaths(documentsToRemove, builder); - - return builder.ToImmutable(); - } - public ImmutableDictionary> CreateFilePathToDocumentIdsMapWithAddedDocuments(IEnumerable documentStates) { var builder = _filePathToDocumentIdsMap.ToBuilder(); @@ -433,16 +416,17 @@ public ImmutableDictionary> CreateFilePathToD private static void AddDocumentFilePaths(IEnumerable documentStates, ImmutableDictionary>.Builder builder) { foreach (var documentState in documentStates) - { - var filePath = documentState.FilePath; + AddDocumentFilePath(documentState, builder); + } - if (RoslynString.IsNullOrEmpty(filePath)) - { - continue; - } + public static void AddDocumentFilePath(TextDocumentState documentState, ImmutableDictionary>.Builder builder) + { + var filePath = documentState.FilePath; - builder.MultiAdd(filePath, documentState.Id); - } + if (RoslynString.IsNullOrEmpty(filePath)) + return; + + builder.MultiAdd(filePath, documentState.Id); } public ImmutableDictionary> CreateFilePathToDocumentIdsMapWithRemovedDocuments(IEnumerable documentStates) @@ -455,21 +439,19 @@ public ImmutableDictionary> CreateFilePathToD private static void RemoveDocumentFilePaths(IEnumerable documentStates, ImmutableDictionary>.Builder builder) { foreach (var documentState in documentStates) - { - var filePath = documentState.FilePath; + RemoveDocumentFilePath(documentState, builder); + } - if (RoslynString.IsNullOrEmpty(filePath)) - { - continue; - } + public static void RemoveDocumentFilePath(TextDocumentState documentState, ImmutableDictionary>.Builder builder) + { + var filePath = documentState.FilePath; + if (RoslynString.IsNullOrEmpty(filePath)) + return; - if (!builder.TryGetValue(filePath, out var documentIdsWithPath) || !documentIdsWithPath.Contains(documentState.Id)) - { - throw new ArgumentException($"The given documentId was not found in '{nameof(_filePathToDocumentIdsMap)}'."); - } + if (!builder.TryGetValue(filePath, out var documentIdsWithPath) || !documentIdsWithPath.Contains(documentState.Id)) + throw new ArgumentException($"The given documentId was not found in '{nameof(_filePathToDocumentIdsMap)}'."); - builder.MultiRemove(filePath, documentState.Id); - } + builder.MultiRemove(filePath, documentState.Id); } private ImmutableDictionary> CreateFilePathToDocumentIdsMapWithFilePath(DocumentId documentId, string? oldFilePath, string? newFilePath) diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentStates.cs b/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentStates.cs index 2df39ead6024b..723edf2ac17fc 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentStates.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentStates.cs @@ -11,7 +11,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Serialization; using Roslyn.Utilities; @@ -217,6 +216,15 @@ private static IEnumerable Except(ImmutableList ids, Imm public bool HasAnyStateChanges(TextDocumentStates oldStates) => !_map.Values.SequenceEqual(oldStates._map.Values); + public override bool Equals(object? obj) + => obj is TextDocumentStates other && Equals(other); + + public override int GetHashCode() + => throw new NotSupportedException(); + + public bool Equals(TextDocumentStates other) + => _map == other._map && _ids == other.Ids; + private sealed class DocumentIdComparer : IComparer { public static readonly IComparer Instance = new DocumentIdComparer(); diff --git a/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs b/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs index f99467d9ab0b0..bb890884334b4 100644 --- a/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs +++ b/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs @@ -4719,5 +4719,21 @@ public async Task TestFrozenPartialSolution7(bool freeze) Assert.Equal("// source2", forkedGeneratedDocuments.Single().GetTextSynchronously(CancellationToken.None).ToString()); } } + + [Fact] + public async Task TestFrozenPartialSolutionOtherLanguage() + { + using var workspace = WorkspaceTestUtilities.CreateWorkspaceWithPartialSemantics(); + var project = workspace.CurrentSolution.AddProject("TypeScript", "TypeScript", "TypeScript"); + project = project.AddDocument("Extra.ts", SourceText.From("class Extra { }")).Project; + + // Freeze should have no impact on non-c#/vb projects. + var frozenSolution = project.Solution.WithFrozenPartialCompilations(CancellationToken.None); + var frozenProject = frozenSolution.Projects.Single(); + Assert.Single(frozenProject.Documents); + + var frozenCompilation = await frozenProject.GetCompilationAsync(); + Assert.Null(frozenCompilation); + } } }