File: Workspace\ProjectSystem\ProjectSystemProject.cs
Web Access
Project: ..\..\..\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.ProjectSystem;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Telemetry;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Workspaces.ProjectSystem
{
    internal sealed partial class ProjectSystemProject
    {
        private static readonly char[] s_directorySeparator = { Path.DirectorySeparatorChar };
        private static readonly ImmutableArray<MetadataReferenceProperties> s_defaultMetadataReferenceProperties = ImmutableArray.Create(default(MetadataReferenceProperties));
 
        private readonly ProjectSystemProjectFactory _projectSystemProjectFactory;
        private readonly ProjectSystemHostInfo _hostInfo;
 
        /// <summary>
        /// A semaphore taken for all mutation of any mutable field in this type.
        /// </summary>
        /// <remarks>This is, for now, intentionally pessimistic. There are no doubt ways that we could allow more to run in parallel,
        /// but the current tradeoff is for simplicity of code and "obvious correctness" than something that is subtle, fast, and wrong.</remarks>
        private readonly SemaphoreSlim _gate = new SemaphoreSlim(initialCount: 1);
 
        /// <summary>
        /// The number of active batch scopes. If this is zero, we are not batching, non-zero means we are batching.
        /// </summary>
        private int _activeBatchScopes = 0;
 
        private readonly List<(string path, MetadataReferenceProperties properties)> _metadataReferencesAddedInBatch = new();
        private readonly List<(string path, MetadataReferenceProperties properties)> _metadataReferencesRemovedInBatch = new();
        private readonly List<ProjectReference> _projectReferencesAddedInBatch = new();
        private readonly List<ProjectReference> _projectReferencesRemovedInBatch = new();
 
        private readonly Dictionary<string, ProjectAnalyzerReference> _analyzerPathsToAnalyzers = new();
        private readonly List<ProjectAnalyzerReference> _analyzersAddedInBatch = new();
 
        /// <summary>
        /// The list of <see cref="ProjectAnalyzerReference"/> that will be removed in this batch. They have not yet
        /// been disposed, and will be disposed once the batch is applied.
        /// </summary>
        private readonly List<ProjectAnalyzerReference> _analyzersRemovedInBatch = new();
 
        private readonly List<Action<SolutionChangeAccumulator>> _projectPropertyModificationsInBatch = new();
 
        private string _assemblyName;
        private string _displayName;
        private string? _filePath;
        private CompilationOptions? _compilationOptions;
        private ParseOptions? _parseOptions;
        private SourceHashAlgorithm _checksumAlgorithm = SourceHashAlgorithms.Default;
        private bool _hasAllInformation = true;
        private string? _compilationOutputAssemblyFilePath;
        private string? _outputFilePath;
        private string? _outputRefFilePath;
        private string? _defaultNamespace;
 
        /// <summary>
        /// If this project is the 'primary' project the project system cares about for a group of Roslyn projects that
        /// correspond to different configurations of a single project system project. <see langword="true"/> by
        /// default.
        /// </summary>
        internal bool IsPrimary { get; set; } = true;
 
        // Actual property values for 'RunAnalyzers' and 'RunAnalyzersDuringLiveAnalysis' properties from the project file.
        // Both these properties can be used to configure running analyzers, with RunAnalyzers overriding RunAnalyzersDuringLiveAnalysis.
        private bool? _runAnalyzersPropertyValue;
        private bool? _runAnalyzersDuringLiveAnalysisPropertyValue;
 
        // Effective boolean value to determine if analyzers should be executed based on _runAnalyzersPropertyValue and _runAnalyzersDuringLiveAnalysisPropertyValue.
        private bool _runAnalyzers = true;
 
        /// <summary>
        /// The full list of all metadata references this project has. References that have internally been converted to project references
        /// will still be in this.
        /// </summary>
        private readonly Dictionary<string, ImmutableArray<MetadataReferenceProperties>> _allMetadataReferences = new();
 
        /// <summary>
        /// The file watching tokens for the documents in this project. We get the tokens even when we're in a batch, so the files here
        /// may not be in the actual workspace yet.
        /// </summary>
        private readonly Dictionary<DocumentId, IWatchedFile> _documentWatchedFiles = new();
 
        /// <summary>
        /// A file change context used to watch source files, additional files, and analyzer config files for this project. It's automatically set to watch the user's project
        /// directory so we avoid file-by-file watching.
        /// </summary>
        private readonly IFileChangeContext _documentFileChangeContext;
 
        /// <summary>
        /// track whether we have been subscribed to <see cref="IDynamicFileInfoProvider.Updated"/> event
        /// </summary>
        private readonly HashSet<IDynamicFileInfoProvider> _eventSubscriptionTracker = new();
 
        /// <summary>
        /// Map of the original dynamic file path to the <see cref="DynamicFileInfo.FilePath"/> that was associated with it.
        ///
        /// For example, the key is something like Page.cshtml which is given to us from the project system calling
        /// <see cref="AddDynamicSourceFile(string, ImmutableArray{string})"/>. The value of the map is a generated file that
        /// corresponds to the original path, say Page.g.cs. If we were given a file by the project system but no
        /// <see cref="IDynamicFileInfoProvider"/> provided a file for it, we will record the value as null so we still can track
        /// the addition of the .cshtml file for a later call to <see cref="RemoveDynamicSourceFile(string)"/>.
        ///
        /// The workspace snapshot will only have a document with  <see cref="DynamicFileInfo.FilePath"/> (the value) but not the
        /// original dynamic file path (the key).
        /// </summary>
        /// <remarks>
        /// We use the same string comparer as in the <see cref="BatchingDocumentCollection"/> used by _sourceFiles, below, as these
        /// files are added to that collection too.
        /// </remarks>
        private readonly Dictionary<string, string?> _dynamicFilePathMaps = new(StringComparer.OrdinalIgnoreCase);
 
        private readonly BatchingDocumentCollection _sourceFiles;
        private readonly BatchingDocumentCollection _additionalFiles;
        private readonly BatchingDocumentCollection _analyzerConfigFiles;
 
        private readonly AsyncBatchingWorkQueue<string> _fileChangesToProcess;
        private readonly CancellationTokenSource _asynchronousFileChangeProcessingCancellationTokenSource = new();
 
        public ProjectId Id { get; }
        public string Language { get; }
 
        internal ProjectSystemProject(
            ProjectSystemProjectFactory projectSystemProjectFactory,
            ProjectSystemHostInfo hostInfo,
            ProjectId id,
            string displayName,
            string language,
            string assemblyName,
            CompilationOptions? compilationOptions,
            string? filePath,
            ParseOptions? parseOptions)
        {
            _projectSystemProjectFactory = projectSystemProjectFactory;
            _hostInfo = hostInfo;
 
            Id = id;
            Language = language;
            _displayName = displayName;
 
            _sourceFiles = new BatchingDocumentCollection(
                this,
                documentAlreadyInWorkspace: (s, d) => s.ContainsDocument(d),
                documentAddAction: (w, d) => w.OnDocumentAdded(d),
                documentRemoveAction: (w, documentId) => w.OnDocumentRemoved(documentId),
                documentTextLoaderChangedAction: (s, d, loader) => s.WithDocumentTextLoader(d, loader, PreservationMode.PreserveValue),
                documentChangedWorkspaceKind: WorkspaceChangeKind.DocumentChanged);
 
            _additionalFiles = new BatchingDocumentCollection(this,
                (s, d) => s.ContainsAdditionalDocument(d),
                (w, d) => w.OnAdditionalDocumentAdded(d),
                (w, documentId) => w.OnAdditionalDocumentRemoved(documentId),
                documentTextLoaderChangedAction: (s, d, loader) => s.WithAdditionalDocumentTextLoader(d, loader, PreservationMode.PreserveValue),
                documentChangedWorkspaceKind: WorkspaceChangeKind.AdditionalDocumentChanged);
 
            _analyzerConfigFiles = new BatchingDocumentCollection(this,
                (s, d) => s.ContainsAnalyzerConfigDocument(d),
                (w, d) => w.OnAnalyzerConfigDocumentAdded(d),
                (w, documentId) => w.OnAnalyzerConfigDocumentRemoved(documentId),
                documentTextLoaderChangedAction: (s, d, loader) => s.WithAnalyzerConfigDocumentTextLoader(d, loader, PreservationMode.PreserveValue),
                documentChangedWorkspaceKind: WorkspaceChangeKind.AnalyzerConfigDocumentChanged);
 
            _fileChangesToProcess = new AsyncBatchingWorkQueue<string>(
                TimeSpan.FromMilliseconds(200), // 200 chosen with absolutely no evidence whatsoever
                ProcessFileChangesAsync,
                StringComparer.Ordinal,
                _projectSystemProjectFactory.WorkspaceListener,
                _asynchronousFileChangeProcessingCancellationTokenSource.Token);
 
            _assemblyName = assemblyName;
            _compilationOptions = compilationOptions;
            _filePath = filePath;
            _parseOptions = parseOptions;
 
            var fileExtensionToWatch = language switch { LanguageNames.CSharp => ".cs", LanguageNames.VisualBasic => ".vb", _ => null };
 
            if (filePath != null && fileExtensionToWatch != null)
            {
                // Since we have a project directory, we'll just watch all the files under that path; that'll avoid extra overhead of
                // having to add explicit file watches everywhere.
                var projectDirectoryToWatch = new WatchedDirectory(Path.GetDirectoryName(filePath)!, fileExtensionToWatch);
                _documentFileChangeContext = _projectSystemProjectFactory.FileChangeWatcher.CreateContext(projectDirectoryToWatch);
            }
            else
            {
                _documentFileChangeContext = _projectSystemProjectFactory.FileChangeWatcher.CreateContext();
            }
 
            _documentFileChangeContext.FileChanged += DocumentFileChangeContext_FileChanged;
        }
 
        private void ChangeProjectProperty<T>(ref T field, T newValue, Func<Solution, Solution> updateSolution, bool logThrowAwayTelemetry = false)
        {
            ChangeProjectProperty(
                ref field,
                newValue,
                (solutionChanges, oldValue) => solutionChanges.UpdateSolutionForProjectAction(Id, updateSolution(solutionChanges.Solution)),
                logThrowAwayTelemetry);
        }
 
        private void ChangeProjectProperty<T>(ref T field, T newValue, Action<SolutionChangeAccumulator, T> updateSolution, bool logThrowAwayTelemetry = false)
        {
            using (_gate.DisposableWait())
            {
                // If nothing is changing, we can skip entirely
                if (object.Equals(field, newValue))
                {
                    return;
                }
 
                var oldValue = field;
 
                field = newValue;
 
                if (logThrowAwayTelemetry)
                {
                    var telemetryService = _projectSystemProjectFactory.Workspace.Services.GetService<IWorkspaceTelemetryService>();
 
                    if (telemetryService?.HasActiveSession == true)
                    {
                        var workspaceStatusService = _projectSystemProjectFactory.Workspace.Services.GetService<IWorkspaceStatusService>();
 
                        // We only log telemetry during solution open
 
                        // Importantly, we do not await/wait on the fullyLoadedStateTask.  We do not want to ever be waiting on work
                        // that may end up touching the UI thread (As we can deadlock if GetTagsSynchronous waits on us).  Instead,
                        // we only check if the Task is completed.  Prior to that we will assume we are still loading.  Once this
                        // task is completed, we know that the WaitUntilFullyLoadedAsync call will have actually finished and we're
                        // fully loaded.
                        var isFullyLoadedTask = workspaceStatusService?.IsFullyLoadedAsync(CancellationToken.None);
                        var isFullyLoaded = isFullyLoadedTask is { IsCompleted: true } && isFullyLoadedTask.GetAwaiter().GetResult();
 
                        if (!isFullyLoaded)
                        {
                            TryReportCompilationThrownAway(_projectSystemProjectFactory.Workspace.CurrentSolution.State, Id);
                        }
                    }
                }
 
                if (_activeBatchScopes > 0)
                {
                    _projectPropertyModificationsInBatch.Add(solutionChanges => updateSolution(solutionChanges, oldValue));
                }
                else
                {
                    _projectSystemProjectFactory.ApplyBatchChangeToWorkspace(solutionChanges =>
                    {
                        updateSolution(solutionChanges, oldValue);
                    });
                }
            }
        }
 
        /// <summary>
        /// Reports a telemetry event if compilation information is being thrown away after being previously computed
        /// </summary>
        private static void TryReportCompilationThrownAway(SolutionState solutionState, ProjectId projectId)
        {
            // We log the number of syntax trees that have been parsed even if there was no compilation created yet
            var projectState = solutionState.GetRequiredProjectState(projectId);
            var parsedTrees = 0;
            foreach (var (_, documentState) in projectState.DocumentStates.States)
            {
                if (documentState.TryGetSyntaxTree(out _))
                {
                    parsedTrees++;
                }
            }
 
            // But we also want to know if a compilation was created
            var hadCompilation = solutionState.TryGetCompilation(projectId, out _);
 
            if (parsedTrees > 0 || hadCompilation)
            {
                Logger.Log(FunctionId.Workspace_Project_CompilationThrownAway, KeyValueLogMessage.Create(m =>
                {
                    // Note: Not using our project Id. This is the same ProjectGuid that the project system uses
                    // so data can be correlated
                    m["ProjectGuid"] = projectState.ProjectInfo.Attributes.TelemetryId.ToString("B");
                    m["SyntaxTreesParsed"] = parsedTrees;
                    m["HadCompilation"] = hadCompilation;
                }));
            }
        }
 
        private void ChangeProjectOutputPath(ref string? field, string? newValue, Func<Solution, Solution> withNewValue)
        {
            ChangeProjectProperty(ref field, newValue, (solutionChanges, oldValue) =>
            {
                // First, update the property itself that's exposed on the Project.
                solutionChanges.UpdateSolutionForProjectAction(Id, withNewValue(solutionChanges.Solution));
 
                if (oldValue != null)
                {
                    _projectSystemProjectFactory.RemoveProjectOutputPath_NoLock(solutionChanges, Id, oldValue);
                }
 
                if (newValue != null)
                {
                    _projectSystemProjectFactory.AddProjectOutputPath_NoLock(solutionChanges, Id, newValue);
                }
            });
        }
 
        public string AssemblyName
        {
            get => _assemblyName;
            set => ChangeProjectProperty(ref _assemblyName, value, s => s.WithProjectAssemblyName(Id, value), logThrowAwayTelemetry: true);
        }
 
        // The property could be null if this is a non-C#/VB language and we don't have one for it. But we disallow assigning null, because C#/VB cannot end up null
        // again once they already had one.
        [DisallowNull]
        public CompilationOptions? CompilationOptions
        {
            get => _compilationOptions;
            set => ChangeProjectProperty(ref _compilationOptions, value, s => s.WithProjectCompilationOptions(Id, value), logThrowAwayTelemetry: true);
        }
 
        // The property could be null if this is a non-C#/VB language and we don't have one for it. But we disallow assigning null, because C#/VB cannot end up null
        // again once they already had one.
        [DisallowNull]
        public ParseOptions? ParseOptions
        {
            get => _parseOptions;
            set => ChangeProjectProperty(ref _parseOptions, value, s => s.WithProjectParseOptions(Id, value), logThrowAwayTelemetry: true);
        }
 
        /// <summary>
        /// The path to the output in obj.
        /// </summary>
        internal string? CompilationOutputAssemblyFilePath
        {
            get => _compilationOutputAssemblyFilePath;
            set => ChangeProjectOutputPath(
                       ref _compilationOutputAssemblyFilePath,
                       value,
                       s => s.WithProjectCompilationOutputInfo(Id, s.GetRequiredProject(Id).CompilationOutputInfo.WithAssemblyPath(value)));
        }
 
        public string? OutputFilePath
        {
            get => _outputFilePath;
            set => ChangeProjectOutputPath(ref _outputFilePath, value, s => s.WithProjectOutputFilePath(Id, value));
        }
 
        public string? OutputRefFilePath
        {
            get => _outputRefFilePath;
            set => ChangeProjectOutputPath(ref _outputRefFilePath, value, s => s.WithProjectOutputRefFilePath(Id, value));
        }
 
        public string? FilePath
        {
            get => _filePath;
            set => ChangeProjectProperty(ref _filePath, value, s => s.WithProjectFilePath(Id, value));
        }
 
        public string DisplayName
        {
            get => _displayName;
            set => ChangeProjectProperty(ref _displayName, value, s => s.WithProjectName(Id, value));
        }
 
        public SourceHashAlgorithm ChecksumAlgorithm
        {
            get => _checksumAlgorithm;
            set => ChangeProjectProperty(ref _checksumAlgorithm, value, s => s.WithProjectChecksumAlgorithm(Id, value));
        }
 
        // internal to match the visibility of the Workspace-level API -- this is something
        // we use but we haven't made officially public yet.
        internal bool HasAllInformation
        {
            get => _hasAllInformation;
            set => ChangeProjectProperty(ref _hasAllInformation, value, s => s.WithHasAllInformation(Id, value));
        }
 
        internal bool? RunAnalyzers
        {
            get => _runAnalyzersPropertyValue;
            set
            {
                _runAnalyzersPropertyValue = value;
                UpdateRunAnalyzers();
            }
        }
 
        internal bool? RunAnalyzersDuringLiveAnalysis
        {
            get => _runAnalyzersDuringLiveAnalysisPropertyValue;
            set
            {
                _runAnalyzersDuringLiveAnalysisPropertyValue = value;
                UpdateRunAnalyzers();
            }
        }
 
        private void UpdateRunAnalyzers()
        {
            // Property RunAnalyzers overrides RunAnalyzersDuringLiveAnalysis, and default when both properties are not set is 'true'.
            var runAnalyzers = _runAnalyzersPropertyValue ?? _runAnalyzersDuringLiveAnalysisPropertyValue ?? true;
            ChangeProjectProperty(ref _runAnalyzers, runAnalyzers, s => s.WithRunAnalyzers(Id, runAnalyzers));
        }
 
        /// <summary>
        /// The default namespace of the project.
        /// </summary>
        /// <remarks>
        /// In C#, this is defined as the value of "rootnamespace" msbuild property. Right now VB doesn't 
        /// have the concept of "default namespace", but we conjure one in workspace by assigning the value
        /// of the project's root namespace to it. So various features can choose to use it for their own purpose.
        /// 
        /// In the future, we might consider officially exposing "default namespace" for VB project
        /// (e.g.through a "defaultnamespace" msbuild property)
        /// </remarks>
        internal string? DefaultNamespace
        {
            get => _defaultNamespace;
            set => ChangeProjectProperty(ref _defaultNamespace, value, s => s.WithProjectDefaultNamespace(Id, value));
        }
 
        /// <summary>
        /// The max language version supported for this project, if applicable. Useful to help indicate what 
        /// language version features should be suggested to a user, as well as if they can be upgraded. 
        /// </summary>
        internal string? MaxLangVersion
        {
            set => _projectSystemProjectFactory.SetMaxLanguageVersion(Id, value);
        }
 
        internal string DependencyNodeTargetIdentifier
        {
            set => _projectSystemProjectFactory.SetDependencyNodeTargetIdentifier(Id, value);
        }
 
        private bool HasBeenRemoved => !_projectSystemProjectFactory.Workspace.CurrentSolution.ContainsProject(Id);
 
        #region Batching
 
        public BatchScope CreateBatchScope()
        {
            using (_gate.DisposableWait())
            {
                _activeBatchScopes++;
                return new BatchScope(this);
            }
        }
 
        public sealed class BatchScope : IDisposable, IAsyncDisposable
        {
            private readonly ProjectSystemProject _project;
 
            /// <summary>
            /// Flag to control if this has already been disposed. Not a boolean only so it can be used with Interlocked.CompareExchange.
            /// </summary>
            private volatile int _disposed = 0;
 
            internal BatchScope(ProjectSystemProject visualStudioProject)
                => _project = visualStudioProject;
 
            public void Dispose()
            {
                if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 0)
                {
                    _project.OnBatchScopeDisposedMaybeAsync(useAsync: false).VerifyCompleted();
                }
            }
 
            public async ValueTask DisposeAsync()
            {
                if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 0)
                {
                    await _project.OnBatchScopeDisposedMaybeAsync(useAsync: true).ConfigureAwait(false);
                }
            }
        }
 
        private async Task OnBatchScopeDisposedMaybeAsync(bool useAsync)
        {
            using (useAsync ? await _gate.DisposableWaitAsync().ConfigureAwait(false) : _gate.DisposableWait())
            {
                _activeBatchScopes--;
 
                if (_activeBatchScopes > 0)
                {
                    return;
                }
 
                // If the project was already removed, we'll just ignore any further requests to complete batches.
                if (HasBeenRemoved)
                {
                    return;
                }
 
                var documentFileNamesAdded = ImmutableArray.CreateBuilder<string>();
                var documentsToOpen = new List<(DocumentId documentId, SourceTextContainer textContainer)>();
                var additionalDocumentsToOpen = new List<(DocumentId documentId, SourceTextContainer textContainer)>();
                var analyzerConfigDocumentsToOpen = new List<(DocumentId documentId, SourceTextContainer textContainer)>();
 
                await _projectSystemProjectFactory.ApplyBatchChangeToWorkspaceMaybeAsync(useAsync, solutionChanges =>
                {
                    _sourceFiles.UpdateSolutionForBatch(
                        solutionChanges,
                        documentFileNamesAdded,
                        documentsToOpen,
                        (s, documents) => s.AddDocuments(documents),
                        WorkspaceChangeKind.DocumentAdded,
                        (s, ids) => s.RemoveDocuments(ids),
                        WorkspaceChangeKind.DocumentRemoved);
 
                    _additionalFiles.UpdateSolutionForBatch(
                        solutionChanges,
                        documentFileNamesAdded,
                        additionalDocumentsToOpen,
                        (s, documents) =>
                        {
                            foreach (var document in documents)
                            {
                                s = s.AddAdditionalDocument(document);
                            }
 
                            return s;
                        },
                        WorkspaceChangeKind.AdditionalDocumentAdded,
                        (s, ids) => s.RemoveAdditionalDocuments(ids),
                        WorkspaceChangeKind.AdditionalDocumentRemoved);
 
                    _analyzerConfigFiles.UpdateSolutionForBatch(
                        solutionChanges,
                        documentFileNamesAdded,
                        analyzerConfigDocumentsToOpen,
                        (s, documents) => s.AddAnalyzerConfigDocuments(documents),
                        WorkspaceChangeKind.AnalyzerConfigDocumentAdded,
                        (s, ids) => s.RemoveAnalyzerConfigDocuments(ids),
                        WorkspaceChangeKind.AnalyzerConfigDocumentRemoved);
 
                    // Metadata reference removing. Do this before adding in case this removes a project reference that
                    // we are also going to add in the same batch. This could happen if case is changing, or we're targeting
                    // a different output path (say bin vs. obj vs. ref).
                    foreach (var (path, properties) in _metadataReferencesRemovedInBatch)
                    {
                        var projectReference = _projectSystemProjectFactory.TryRemoveConvertedProjectReference_NoLock(Id, path, properties);
 
                        if (projectReference != null)
                        {
                            solutionChanges.UpdateSolutionForProjectAction(
                                Id,
                                solutionChanges.Solution.RemoveProjectReference(Id, projectReference));
                        }
                        else
                        {
                            // TODO: find a cleaner way to fetch this
                            var metadataReference = _projectSystemProjectFactory.Workspace.CurrentSolution.GetRequiredProject(Id).MetadataReferences.Cast<PortableExecutableReference>()
                                                                                    .Single(m => m.FilePath == path && m.Properties == properties);
 
                            _projectSystemProjectFactory.FileWatchedReferenceFactory.StopWatchingReference(metadataReference);
 
                            solutionChanges.UpdateSolutionForProjectAction(
                                Id,
                                newSolution: solutionChanges.Solution.RemoveMetadataReference(Id, metadataReference));
                        }
                    }
 
                    ClearAndZeroCapacity(_metadataReferencesRemovedInBatch);
 
                    // Metadata reference adding...
                    if (_metadataReferencesAddedInBatch.Count > 0)
                    {
                        var projectReferencesCreated = new List<ProjectReference>();
                        var metadataReferencesCreated = new List<MetadataReference>();
 
                        foreach (var (path, properties) in _metadataReferencesAddedInBatch)
                        {
                            var projectReference = _projectSystemProjectFactory.TryCreateConvertedProjectReference_NoLock(Id, path, properties);
 
                            if (projectReference != null)
                            {
                                projectReferencesCreated.Add(projectReference);
                            }
                            else
                            {
                                var metadataReference = _projectSystemProjectFactory.FileWatchedReferenceFactory.CreateReferenceAndStartWatchingFile(path, properties);
                                metadataReferencesCreated.Add(metadataReference);
                            }
                        }
 
                        solutionChanges.UpdateSolutionForProjectAction(
                            Id,
                            solutionChanges.Solution.AddProjectReferences(Id, projectReferencesCreated)
                                                    .AddMetadataReferences(Id, metadataReferencesCreated));
 
                        ClearAndZeroCapacity(_metadataReferencesAddedInBatch);
                    }
 
                    // Project reference adding...
                    solutionChanges.UpdateSolutionForProjectAction(
                        Id,
                        newSolution: solutionChanges.Solution.AddProjectReferences(Id, _projectReferencesAddedInBatch));
                    ClearAndZeroCapacity(_projectReferencesAddedInBatch);
 
                    // Project reference removing...
                    foreach (var projectReference in _projectReferencesRemovedInBatch)
                    {
                        solutionChanges.UpdateSolutionForProjectAction(
                            Id,
                            newSolution: solutionChanges.Solution.RemoveProjectReference(Id, projectReference));
                    }
 
                    ClearAndZeroCapacity(_projectReferencesRemovedInBatch);
 
                    // Analyzer reference adding...
                    solutionChanges.UpdateSolutionForProjectAction(
                        Id,
                        newSolution: solutionChanges.Solution.AddAnalyzerReferences(Id, _analyzersAddedInBatch.Select(a => a.GetReference())));
                    ClearAndZeroCapacity(_analyzersAddedInBatch);
 
                    // Analyzer reference removing...
                    foreach (var analyzerReference in _analyzersRemovedInBatch)
                    {
                        solutionChanges.UpdateSolutionForProjectAction(
                            Id,
                            newSolution: solutionChanges.Solution.RemoveAnalyzerReference(Id, analyzerReference.GetReference()));
 
                        analyzerReference.Dispose();
                    }
 
                    ClearAndZeroCapacity(_analyzersRemovedInBatch);
 
                    // Other property modifications...
                    foreach (var propertyModification in _projectPropertyModificationsInBatch)
                    {
                        propertyModification(solutionChanges);
                    }
 
                    ClearAndZeroCapacity(_projectPropertyModificationsInBatch);
                }).ConfigureAwait(false);
 
                foreach (var (documentId, textContainer) in documentsToOpen)
                {
                    await _projectSystemProjectFactory.ApplyChangeToWorkspaceMaybeAsync(useAsync, w => w.OnDocumentOpened(documentId, textContainer)).ConfigureAwait(false);
                }
 
                foreach (var (documentId, textContainer) in additionalDocumentsToOpen)
                {
                    await _projectSystemProjectFactory.ApplyChangeToWorkspaceMaybeAsync(useAsync, w => w.OnAdditionalDocumentOpened(documentId, textContainer)).ConfigureAwait(false);
                }
 
                foreach (var (documentId, textContainer) in analyzerConfigDocumentsToOpen)
                {
                    await _projectSystemProjectFactory.ApplyChangeToWorkspaceMaybeAsync(useAsync, w => w.OnAnalyzerConfigDocumentOpened(documentId, textContainer)).ConfigureAwait(false);
                }
 
                // Give the host the opportunity to check if those files are open
                if (documentFileNamesAdded.Count > 0)
                {
                    await _projectSystemProjectFactory.RaiseOnDocumentsAddedMaybeAsync(useAsync, documentFileNamesAdded.ToImmutable()).ConfigureAwait(false);
                }
            }
        }
 
        #endregion
 
        #region Source File Addition/Removal
 
        public void AddSourceFile(string fullPath, SourceCodeKind sourceCodeKind = SourceCodeKind.Regular, ImmutableArray<string> folders = default)
            => _sourceFiles.AddFile(fullPath, sourceCodeKind, folders);
 
        /// <summary>
        /// Adds a source file to the project from a text container (eg, a Visual Studio Text buffer)
        /// </summary>
        /// <param name="textContainer">The text container that contains this file.</param>
        /// <param name="fullPath">The file path of the document.</param>
        /// <param name="sourceCodeKind">The kind of the source code.</param>
        /// <param name="folders">The names of the logical nested folders the document is contained in.</param>
        /// <param name="designTimeOnly">Whether the document is used only for design time (eg. completion) or also included in a compilation.</param>
        /// <param name="documentServiceProvider">A <see cref="IDocumentServiceProvider"/> associated with this document</param>
        public DocumentId AddSourceTextContainer(
            SourceTextContainer textContainer,
            string fullPath,
            SourceCodeKind sourceCodeKind = SourceCodeKind.Regular,
            ImmutableArray<string> folders = default,
            bool designTimeOnly = false,
            IDocumentServiceProvider? documentServiceProvider = null)
        {
            return _sourceFiles.AddTextContainer(textContainer, fullPath, sourceCodeKind, folders, designTimeOnly, documentServiceProvider);
        }
 
        public bool ContainsSourceFile(string fullPath)
            => _sourceFiles.ContainsFile(fullPath);
 
        public void RemoveSourceFile(string fullPath)
            => _sourceFiles.RemoveFile(fullPath);
 
        public void RemoveSourceTextContainer(SourceTextContainer textContainer)
            => _sourceFiles.RemoveTextContainer(textContainer);
 
        #endregion
 
        #region Additional File Addition/Removal
 
        // TODO: should AdditionalFiles have source code kinds?
        public void AddAdditionalFile(string fullPath, SourceCodeKind sourceCodeKind = SourceCodeKind.Regular)
            => _additionalFiles.AddFile(fullPath, sourceCodeKind, folders: default);
 
        public bool ContainsAdditionalFile(string fullPath)
            => _additionalFiles.ContainsFile(fullPath);
 
        public void RemoveAdditionalFile(string fullPath)
            => _additionalFiles.RemoveFile(fullPath);
 
        #endregion
 
        #region Analyzer Config File Addition/Removal
 
        public void AddAnalyzerConfigFile(string fullPath)
        {
            // TODO: do we need folders for analyzer config files?
            _analyzerConfigFiles.AddFile(fullPath, SourceCodeKind.Regular, folders: default);
        }
 
        public bool ContainsAnalyzerConfigFile(string fullPath)
            => _analyzerConfigFiles.ContainsFile(fullPath);
 
        public void RemoveAnalyzerConfigFile(string fullPath)
            => _analyzerConfigFiles.RemoveFile(fullPath);
 
        #endregion
 
        #region Non Source File Addition/Removal
 
        public void AddDynamicSourceFile(string dynamicFilePath, ImmutableArray<string> folders)
        {
            DynamicFileInfo? fileInfo = null;
            IDynamicFileInfoProvider? providerForFileInfo = null;
 
            var extension = FileNameUtilities.GetExtension(dynamicFilePath)?.TrimStart('.');
            if (extension?.Length == 0)
            {
                fileInfo = null;
            }
            else
            {
                foreach (var provider in _hostInfo.DynamicFileInfoProviders)
                {
                    // skip unrelated providers
                    if (!provider.Metadata.Extensions.Any(e => string.Equals(e, extension, StringComparison.OrdinalIgnoreCase)))
                    {
                        continue;
                    }
 
                    // Don't get confused by _filePath and filePath.
                    // VisualStudioProject._filePath points to csproj/vbproj of the project
                    // and the parameter filePath points to dynamic file such as ASP.NET .g.cs files.
                    // 
                    // Also, provider is free-threaded. so fine to call Wait rather than JTF.
                    fileInfo = provider.Value.GetDynamicFileInfoAsync(
                        projectId: Id, projectFilePath: _filePath, filePath: dynamicFilePath, CancellationToken.None).WaitAndGetResult_CanCallOnBackground(CancellationToken.None);
 
                    if (fileInfo != null)
                    {
                        fileInfo = FixUpDynamicFileInfo(fileInfo, dynamicFilePath);
                        providerForFileInfo = provider.Value;
                        break;
                    }
                }
            }
 
            using (_gate.DisposableWait())
            {
                if (_dynamicFilePathMaps.ContainsKey(dynamicFilePath))
                {
                    // TODO: if we have a duplicate, we are not calling RemoveDynamicFileInfoAsync since we
                    // don't want to call with that under a lock. If we are getting duplicates we have bigger problems
                    // at that point since our workspace is generally out of sync with the project system.
                    // Given we're taking this as a late fix prior to a release, I don't think it's worth the added
                    // risk to handle a case that wasn't handled before either.
                    throw new ArgumentException($"{dynamicFilePath} has already been added to this project.");
                }
 
                // Record the mapping from the dynamic file path to the source file it generated. We will record
                // 'null' if no provider was able to produce a source file for this input file. That could happen
                // if the provider (say ASP.NET Razor) doesn't recognize the file, or the wrong type of file
                // got passed through the system. That's not a failure from the project system's perspective:
                // adding dynamic files is a hint at best that doesn't impact it.
                _dynamicFilePathMaps.Add(dynamicFilePath, fileInfo?.FilePath);
 
                if (fileInfo != null)
                {
                    // If fileInfo is not null, that means we found a provider so this should be not-null as well
                    // since we had to go through the earlier assignment.
                    Contract.ThrowIfNull(providerForFileInfo);
                    _sourceFiles.AddDynamicFile_NoLock(providerForFileInfo, fileInfo, folders);
                }
            }
        }
 
        private static DynamicFileInfo FixUpDynamicFileInfo(DynamicFileInfo fileInfo, string filePath)
        {
            // we might change contract and just throw here. but for now, we keep existing contract where one can return null for DynamicFileInfo.FilePath.
            // In this case we substitute the file being generated from so we still have some path.
            if (string.IsNullOrEmpty(fileInfo.FilePath))
            {
                return new DynamicFileInfo(filePath, fileInfo.SourceCodeKind, fileInfo.TextLoader, fileInfo.DesignTimeOnly, fileInfo.DocumentServiceProvider);
            }
 
            return fileInfo;
        }
 
        public void RemoveDynamicSourceFile(string dynamicFilePath)
        {
            IDynamicFileInfoProvider provider;
 
            using (_gate.DisposableWait())
            {
                if (!_dynamicFilePathMaps.TryGetValue(dynamicFilePath, out var sourceFilePath))
                {
                    throw new ArgumentException($"{dynamicFilePath} wasn't added by a previous call to {nameof(AddDynamicSourceFile)}");
                }
 
                _dynamicFilePathMaps.Remove(dynamicFilePath);
 
                // If we got a null path back, it means we never had a source file to add. In that case,
                // we're done
                if (sourceFilePath == null)
                {
                    return;
                }
 
                provider = _sourceFiles.RemoveDynamicFile_NoLock(sourceFilePath);
            }
 
            // provider is free-threaded. so fine to call Wait rather than JTF
            provider.RemoveDynamicFileInfoAsync(
                projectId: Id, projectFilePath: _filePath, filePath: dynamicFilePath, CancellationToken.None).Wait(CancellationToken.None);
        }
 
        private void OnDynamicFileInfoUpdated(object? sender, string dynamicFilePath)
        {
            string? fileInfoPath;
 
            using (_gate.DisposableWait())
            {
                if (!_dynamicFilePathMaps.TryGetValue(dynamicFilePath, out fileInfoPath))
                {
                    // given file doesn't belong to this project. 
                    // this happen since the event this is handling is shared between all projects
                    return;
                }
            }
 
            if (fileInfoPath != null)
            {
                _sourceFiles.ProcessDynamicFileChange(dynamicFilePath, fileInfoPath);
            }
        }
 
        #endregion
 
        #region Analyzer Addition/Removal
 
        public void AddAnalyzerReference(string fullPath)
        {
            CompilerPathUtilities.RequireAbsolutePath(fullPath, nameof(fullPath));
 
            var mappedPaths = GetMappedAnalyzerPaths(fullPath);
 
            using (_gate.DisposableWait())
            {
                // check all mapped paths first, so that all analyzers are either added or not
                foreach (var mappedFullPath in mappedPaths)
                {
                    if (_analyzerPathsToAnalyzers.ContainsKey(mappedFullPath))
                    {
                        throw new ArgumentException($"'{fullPath}' has already been added to this project.", nameof(fullPath));
                    }
                }
 
                foreach (var mappedFullPath in mappedPaths)
                {
                    // Are we adding one we just recently removed? If so, we can just keep using that one, and avoid removing
                    // it once we apply the batch
                    var analyzerPendingRemoval = _analyzersRemovedInBatch.FirstOrDefault(a => a.FullPath == mappedFullPath);
                    if (analyzerPendingRemoval != null)
                    {
                        _analyzersRemovedInBatch.Remove(analyzerPendingRemoval);
                        _analyzerPathsToAnalyzers.Add(mappedFullPath, analyzerPendingRemoval);
                    }
                    else
                    {
                        // Nope, we actually need to make a new one.
                        var visualStudioAnalyzer = new ProjectAnalyzerReference(
                            mappedFullPath,
                            _hostInfo.DiagnosticSource,
                            Id,
                            Language);
 
                        _analyzerPathsToAnalyzers.Add(mappedFullPath, visualStudioAnalyzer);
 
                        if (_activeBatchScopes > 0)
                        {
                            _analyzersAddedInBatch.Add(visualStudioAnalyzer);
                        }
                        else
                        {
                            _projectSystemProjectFactory.ApplyChangeToWorkspace(w => w.OnAnalyzerReferenceAdded(Id, visualStudioAnalyzer.GetReference()));
                        }
                    }
                }
            }
        }
 
        public void RemoveAnalyzerReference(string fullPath)
        {
            if (string.IsNullOrEmpty(fullPath))
            {
                throw new ArgumentException("message", nameof(fullPath));
            }
 
            var mappedPaths = GetMappedAnalyzerPaths(fullPath);
 
            using (_gate.DisposableWait())
            {
                // check all mapped paths first, so that all analyzers are either removed or not
                foreach (var mappedFullPath in mappedPaths)
                {
                    if (!_analyzerPathsToAnalyzers.ContainsKey(mappedFullPath))
                    {
                        throw new ArgumentException($"'{fullPath}' is not an analyzer of this project.", nameof(fullPath));
                    }
                }
 
                foreach (var mappedFullPath in mappedPaths)
                {
                    var visualStudioAnalyzer = _analyzerPathsToAnalyzers[mappedFullPath];
 
                    _analyzerPathsToAnalyzers.Remove(mappedFullPath);
 
                    if (_activeBatchScopes > 0)
                    {
                        // This analyzer may be one we've just added in the same batch; in that case, just don't add
                        // it in the first place.
                        if (_analyzersAddedInBatch.Remove(visualStudioAnalyzer))
                        {
                            // Nothing is holding onto this analyzer now, so get rid of it
                            visualStudioAnalyzer.Dispose();
                        }
                        else
                        {
                            _analyzersRemovedInBatch.Add(visualStudioAnalyzer);
                        }
                    }
                    else
                    {
                        _projectSystemProjectFactory.ApplyChangeToWorkspace(w => w.OnAnalyzerReferenceRemoved(Id, visualStudioAnalyzer.GetReference()));
 
                        visualStudioAnalyzer.Dispose();
                    }
                }
            }
        }
 
        private const string RazorVsixExtensionId = "Microsoft.VisualStudio.RazorExtension";
        private static readonly string s_razorSourceGeneratorSdkDirectory = Path.Combine("Sdks", "Microsoft.NET.Sdk.Razor", "source-generators") + PathUtilities.DirectorySeparatorStr;
        private static readonly string s_razorSourceGeneratorMainAssemblyRootedFileName = PathUtilities.DirectorySeparatorStr + "Microsoft.NET.Sdk.Razor.SourceGenerators.dll";
 
        private OneOrMany<string> GetMappedAnalyzerPaths(string fullPath)
        {
            // Map all files in the SDK directory that contains the Razor source generator to source generator files loaded from VSIX.
            // Include the generator and all its dependencies shipped in VSIX, discard the generator and all dependencies in the SDK
            if (fullPath.LastIndexOf(s_razorSourceGeneratorSdkDirectory, StringComparison.OrdinalIgnoreCase) + s_razorSourceGeneratorSdkDirectory.Length - 1 ==
                fullPath.LastIndexOf(Path.DirectorySeparatorChar))
            {
                var vsixRazorAnalyzers = _hostInfo.HostDiagnosticAnalyzerProvider.GetAnalyzerReferencesInExtensions().SelectAsArray(
                    predicate: item => item.extensionId == RazorVsixExtensionId,
                    selector: item => item.reference.FullPath);
 
                if (!vsixRazorAnalyzers.IsEmpty)
                {
                    if (fullPath.EndsWith(s_razorSourceGeneratorMainAssemblyRootedFileName, StringComparison.OrdinalIgnoreCase))
                    {
                        return OneOrMany.Create(vsixRazorAnalyzers);
                    }
 
                    return OneOrMany.Create(ImmutableArray<string>.Empty);
                }
            }
 
            return OneOrMany.Create(fullPath);
        }
 
        #endregion
 
        private void DocumentFileChangeContext_FileChanged(object? sender, string fullFilePath)
        {
            _fileChangesToProcess.AddWork(fullFilePath);
        }
 
        private async ValueTask ProcessFileChangesAsync(ImmutableSegmentedList<string> filePaths, CancellationToken cancellationToken)
        {
            await _sourceFiles.ProcessRegularFileChangesAsync(filePaths).ConfigureAwait(false);
            await _additionalFiles.ProcessRegularFileChangesAsync(filePaths).ConfigureAwait(false);
            await _analyzerConfigFiles.ProcessRegularFileChangesAsync(filePaths).ConfigureAwait(false);
        }
 
        #region Metadata Reference Addition/Removal
 
        public void AddMetadataReference(string fullPath, MetadataReferenceProperties properties)
        {
            if (string.IsNullOrEmpty(fullPath))
            {
                throw new ArgumentException($"{nameof(fullPath)} isn't a valid path.", nameof(fullPath));
            }
 
            using (_gate.DisposableWait())
            {
                if (ContainsMetadataReference_NoLock(fullPath, properties))
                {
                    throw new InvalidOperationException("The metadata reference has already been added to the project.");
                }
 
                _allMetadataReferences.MultiAdd(fullPath, properties, s_defaultMetadataReferenceProperties);
 
                if (_activeBatchScopes > 0)
                {
                    if (!_metadataReferencesRemovedInBatch.Remove((fullPath, properties)))
                    {
                        _metadataReferencesAddedInBatch.Add((fullPath, properties));
                    }
                }
                else
                {
                    _projectSystemProjectFactory.ApplyChangeToWorkspace(w =>
                    {
                        var projectReference = _projectSystemProjectFactory.TryCreateConvertedProjectReference_NoLock(Id, fullPath, properties);
 
                        if (projectReference != null)
                        {
                            w.OnProjectReferenceAdded(Id, projectReference);
                        }
                        else
                        {
                            var metadataReference = _projectSystemProjectFactory.FileWatchedReferenceFactory.CreateReferenceAndStartWatchingFile(fullPath, properties);
                            w.OnMetadataReferenceAdded(Id, metadataReference);
                        }
                    });
                }
            }
        }
 
        public bool ContainsMetadataReference(string fullPath, MetadataReferenceProperties properties)
        {
            using (_gate.DisposableWait())
            {
                return ContainsMetadataReference_NoLock(fullPath, properties);
            }
        }
 
        private bool ContainsMetadataReference_NoLock(string fullPath, MetadataReferenceProperties properties)
        {
            Debug.Assert(_gate.CurrentCount == 0);
 
            return _allMetadataReferences.TryGetValue(fullPath, out var propertiesList) && propertiesList.Contains(properties);
        }
 
        /// <summary>
        /// Returns the properties being used for the current metadata reference added to this project. May return multiple properties if
        /// the reference has been added multiple times with different properties.
        /// </summary>
        public ImmutableArray<MetadataReferenceProperties> GetPropertiesForMetadataReference(string fullPath)
        {
            using (_gate.DisposableWait())
            {
                return _allMetadataReferences.TryGetValue(fullPath, out var list) ? list : ImmutableArray<MetadataReferenceProperties>.Empty;
            }
        }
 
        public void RemoveMetadataReference(string fullPath, MetadataReferenceProperties properties)
        {
            if (string.IsNullOrEmpty(fullPath))
            {
                throw new ArgumentException($"{nameof(fullPath)} isn't a valid path.", nameof(fullPath));
            }
 
            using (_gate.DisposableWait())
            {
                if (!ContainsMetadataReference_NoLock(fullPath, properties))
                {
                    throw new InvalidOperationException("The metadata reference does not exist in this project.");
                }
 
                _allMetadataReferences.MultiRemove(fullPath, properties);
 
                if (_activeBatchScopes > 0)
                {
                    if (!_metadataReferencesAddedInBatch.Remove((fullPath, properties)))
                    {
                        _metadataReferencesRemovedInBatch.Add((fullPath, properties));
                    }
                }
                else
                {
                    _projectSystemProjectFactory.ApplyChangeToWorkspace(w =>
                    {
                        var projectReference = _projectSystemProjectFactory.TryRemoveConvertedProjectReference_NoLock(Id, fullPath, properties);
 
                        // If this was converted to a project reference, we have now recorded the removal -- let's remove it here too
                        if (projectReference != null)
                        {
                            w.OnProjectReferenceRemoved(Id, projectReference);
                        }
                        else
                        {
                            // TODO: find a cleaner way to fetch this
                            var metadataReference = w.CurrentSolution.GetRequiredProject(Id).MetadataReferences.Cast<PortableExecutableReference>()
                                                                                            .Single(m => m.FilePath == fullPath && m.Properties == properties);
 
                            _projectSystemProjectFactory.FileWatchedReferenceFactory.StopWatchingReference(metadataReference);
                            w.OnMetadataReferenceRemoved(Id, metadataReference);
                        }
                    });
                }
            }
        }
 
        #endregion
 
        #region Project Reference Addition/Removal
 
        public void AddProjectReference(ProjectReference projectReference)
        {
            if (projectReference == null)
            {
                throw new ArgumentNullException(nameof(projectReference));
            }
 
            using (_gate.DisposableWait())
            {
                if (ContainsProjectReference_NoLock(projectReference))
                {
                    throw new ArgumentException("The project reference has already been added to the project.");
                }
 
                if (_activeBatchScopes > 0)
                {
                    if (!_projectReferencesRemovedInBatch.Remove(projectReference))
                    {
                        _projectReferencesAddedInBatch.Add(projectReference);
                    }
                }
                else
                {
                    _projectSystemProjectFactory.ApplyChangeToWorkspace(w => w.OnProjectReferenceAdded(Id, projectReference));
                }
            }
        }
 
        public bool ContainsProjectReference(ProjectReference projectReference)
        {
            if (projectReference == null)
            {
                throw new ArgumentNullException(nameof(projectReference));
            }
 
            using (_gate.DisposableWait())
            {
                return ContainsProjectReference_NoLock(projectReference);
            }
        }
 
        private bool ContainsProjectReference_NoLock(ProjectReference projectReference)
        {
            Debug.Assert(_gate.CurrentCount == 0);
 
            if (_projectReferencesRemovedInBatch.Contains(projectReference))
            {
                return false;
            }
 
            if (_projectReferencesAddedInBatch.Contains(projectReference))
            {
                return true;
            }
 
            return _projectSystemProjectFactory.Workspace.CurrentSolution.GetRequiredProject(Id).AllProjectReferences.Contains(projectReference);
        }
 
        public IReadOnlyList<ProjectReference> GetProjectReferences()
        {
            using (_gate.DisposableWait())
            {
                // If we're not batching, then this is cheap: just fetch from the workspace and we're done
                var projectReferencesInWorkspace = _projectSystemProjectFactory.Workspace.CurrentSolution.GetRequiredProject(Id).AllProjectReferences;
 
                if (_activeBatchScopes == 0)
                {
                    return projectReferencesInWorkspace;
                }
 
                // Not, so we get to compute a new list instead
                var newList = projectReferencesInWorkspace.ToList();
                newList.AddRange(_projectReferencesAddedInBatch);
                newList.RemoveAll(p => _projectReferencesRemovedInBatch.Contains(p));
 
                return newList;
            }
        }
 
        public void RemoveProjectReference(ProjectReference projectReference)
        {
            if (projectReference == null)
            {
                throw new ArgumentNullException(nameof(projectReference));
            }
 
            using (_gate.DisposableWait())
            {
                if (!ContainsProjectReference_NoLock(projectReference))
                {
                    throw new ArgumentException("The project does not contain that project reference.");
                }
 
                if (_activeBatchScopes > 0)
                {
                    if (!_projectReferencesAddedInBatch.Remove(projectReference))
                    {
                        _projectReferencesRemovedInBatch.Add(projectReference);
                    }
                }
                else
                {
                    _projectSystemProjectFactory.ApplyChangeToWorkspace(w => w.OnProjectReferenceRemoved(Id, projectReference));
                }
            }
        }
 
        #endregion
 
        public void RemoveFromWorkspace()
        {
            using (_gate.DisposableWait())
            {
                if (!_projectSystemProjectFactory.Workspace.CurrentSolution.ContainsProject(Id))
                {
                    throw new InvalidOperationException("The project has already been removed.");
                }
 
                _asynchronousFileChangeProcessingCancellationTokenSource.Cancel();
 
                // clear tracking to external components
                foreach (var provider in _eventSubscriptionTracker)
                {
                    provider.Updated -= OnDynamicFileInfoUpdated;
                }
 
                _eventSubscriptionTracker.Clear();
            }
 
            _documentFileChangeContext.Dispose();
 
            IReadOnlyList<MetadataReference>? remainingMetadataReferences = null;
 
            _projectSystemProjectFactory.ApplyChangeToWorkspace(w =>
            {
                // Acquire the remaining metadata references inside the workspace lock. This is critical
                // as another project being removed at the same time could result in project to project
                // references being converted to metadata references (or vice versa) and we might either
                // miss stopping a file watcher or might end up double-stopping a file watcher.
                remainingMetadataReferences = w.CurrentSolution.GetRequiredProject(Id).MetadataReferences;
                _projectSystemProjectFactory.RemoveProjectFromTrackingMaps_NoLock(Id);
 
                // If this is our last project, clear the entire solution.
                if (w.CurrentSolution.ProjectIds.Count == 1)
                {
                    _projectSystemProjectFactory.RemoveSolution_NoLock();
                }
                else
                {
                    _projectSystemProjectFactory.Workspace.OnProjectRemoved(Id);
                }
            });
 
            Contract.ThrowIfNull(remainingMetadataReferences);
 
            foreach (PortableExecutableReference reference in remainingMetadataReferences)
            {
                _projectSystemProjectFactory.FileWatchedReferenceFactory.StopWatchingReference(reference);
            }
 
            // Dispose of any analyzers that might still be around to remove their load diagnostics
            foreach (var visualStudioAnalyzer in _analyzerPathsToAnalyzers.Values.Concat(_analyzersRemovedInBatch))
            {
                visualStudioAnalyzer.Dispose();
            }
        }
 
        public void ReorderSourceFiles(ImmutableArray<string> filePaths)
            => _sourceFiles.ReorderFiles(filePaths);
 
        /// <summary>
        /// Clears a list and zeros out the capacity. The lists we use for batching are likely to get large during an initial load, but after
        /// that point should never get that large again.
        /// </summary>
        private static void ClearAndZeroCapacity<T>(List<T> list)
        {
            list.Clear();
            list.Capacity = 0;
        }
 
        /// <summary>
        /// Clears a list and zeros out the capacity. The lists we use for batching are likely to get large during an initial load, but after
        /// that point should never get that large again.
        /// </summary>
        private static void ClearAndZeroCapacity<T>(ImmutableArray<T>.Builder list)
        {
            list.Clear();
            list.Capacity = 0;
        }
    }
}