// 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.Text;
using System.Threading;
using System.Threading.Tasks;
using EnvDTE;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.EditAndContinue;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.ProjectSystem;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Telemetry;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Workspaces.ProjectSystem;
using Microsoft.VisualStudio.ComponentModelHost;
using Microsoft.VisualStudio.Composition;
using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.LanguageServices.Implementation.CodeModel;
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.Extensions;
using Microsoft.VisualStudio.LanguageServices.Implementation.TaskList;
using Microsoft.VisualStudio.LanguageServices.Implementation.Venus;
using Microsoft.VisualStudio.LanguageServices.Telemetry;
using Microsoft.VisualStudio.LanguageServices.Utilities;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Telemetry;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Projection;
using Microsoft.VisualStudio.Threading;
using Roslyn.Utilities;
using VSLangProj;
using VSLangProj140;
using IAsyncServiceProvider = Microsoft.VisualStudio.Shell.IAsyncServiceProvider;
using OleInterop = Microsoft.VisualStudio.OLE.Interop;
using Task = System.Threading.Tasks.Task;

namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;

/// <summary>
/// The Workspace for running inside Visual Studio.
/// </summary>
internal abstract partial class VisualStudioWorkspaceImpl : VisualStudioWorkspace
{
    private static readonly IntPtr s_docDataExisting_Unknown = new(-1);
    private const string AppCodeFolderName = "App_Code";

    private readonly IThreadingContext _threadingContext;
    private readonly IAsyncServiceProvider _asyncServiceProvider;
    private readonly ITextBufferFactoryService _textBufferFactoryService;
    private readonly IProjectionBufferFactoryService _projectionBufferFactoryService;
    private readonly IGlobalOptionService _globalOptions;

    private readonly ITextBufferCloneService _textBufferCloneService;

    /// <summary>
    /// Guards any updates to the maps here that aren't updated via interlocked updates.
    /// </summary>
    // Our usage of SemaphoreSlim is fine.  We don't perform blocking waits for it on the UI thread.
#pragma warning disable RS0030 // Do not use banned APIs
    private readonly SemaphoreSlim _gate = new(initialCount: 1);
#pragma warning restore RS0030 // Do not use banned APIs

    private ImmutableDictionary<ProjectId, IVsHierarchy?> _projectToHierarchyMap = ImmutableDictionary<ProjectId, IVsHierarchy?>.Empty;
    private ImmutableDictionary<ProjectId, Guid> _projectToGuidMap = ImmutableDictionary<ProjectId, Guid>.Empty;

    /// <summary>
    /// A map to fetch the path to a rule set file for a project. This right now is only used to implement
    /// <see cref="TryGetRuleSetPathForProject(ProjectId)"/> and any other use is extremely suspicious, since direct use of this is out of
    /// sync with the Workspace if there is active batching happening.
    /// </summary>
    /// <remarks>Should be updated with <see cref="ImmutableInterlocked"/>.</remarks>
    private ImmutableDictionary<ProjectId, Func<string?>> _projectToRuleSetFilePath = ImmutableDictionary<ProjectId, Func<string?>>.Empty;

    private readonly Dictionary<string, List<ProjectSystemProject>> _projectSystemNameToProjectsMap = [];

    /// <summary>
    /// Mapping from language name to an existing UIContext's active state.
    /// Only access when holding <see cref="_gate"/>
    /// </summary>
    private readonly Dictionary<string, bool> _languageToProjectExistsUIContextState = [];

    /// <summary>
    /// Joinable task collection to await to ensure language ui contexts are updated.
    /// </summary>
    private readonly JoinableTaskCollection _updateUIContextJoinableTasks;

    private readonly OpenFileTracker _openFileTracker;
    private readonly UIContext _solutionClosingContext;

    internal IFileChangeWatcher FileChangeWatcher { get; }

    internal ProjectSystemProjectFactory ProjectSystemProjectFactory { get; }

    private readonly Lazy<IProjectCodeModelFactory> _projectCodeModelFactory;
    private readonly Lazy<ExternalErrorDiagnosticUpdateSource> _lazyExternalErrorDiagnosticUpdateSource;
    private readonly IAsynchronousOperationListener _workspaceListener;

    /// <summary>
    /// Only read/written on the UI thread.
    /// </summary>
    private bool _isShowingDocumentChangeErrorInfoBar = false;
    private bool _ignoreDocumentTextChangeErrors;

    public VisualStudioWorkspaceImpl(ExportProvider exportProvider, IAsyncServiceProvider asyncServiceProvider)
        : base(VisualStudioMefHostServices.Create(exportProvider))
    {
        _threadingContext = exportProvider.GetExportedValue<IThreadingContext>();
        _asyncServiceProvider = asyncServiceProvider;
        _globalOptions = exportProvider.GetExportedValue<IGlobalOptionService>();
        _textBufferCloneService = exportProvider.GetExportedValue<ITextBufferCloneService>();
        _textBufferFactoryService = exportProvider.GetExportedValue<ITextBufferFactoryService>();
        _projectionBufferFactoryService = exportProvider.GetExportedValue<IProjectionBufferFactoryService>();
        _projectCodeModelFactory = exportProvider.GetExport<IProjectCodeModelFactory>();
        _workspaceListener = Services.GetRequiredService<IWorkspaceAsynchronousOperationListenerProvider>().GetListener();

        _textBufferFactoryService.TextBufferCreated += AddTextBufferCloneServiceToBuffer;
        _projectionBufferFactoryService.ProjectionBufferCreated += AddTextBufferCloneServiceToBuffer;

        FileChangeWatcher = exportProvider.GetExportedValue<FileChangeWatcherProvider>().Watcher;

        ProjectSystemProjectFactory = new ProjectSystemProjectFactory(
            this, FileChangeWatcher, CheckForAddedFileBeingOpenMaybeAsync, RemoveProjectFromMaps, _threadingContext.DisposalToken);

        _solutionClosingContext = UIContext.FromUIContextGuid(VSConstants.UICONTEXT.SolutionClosing_guid);
        _solutionClosingContext.UIContextChanged += SolutionClosingContext_UIContextChanged;

        _openFileTracker = new OpenFileTracker(
            this,
            ProjectSystemProjectFactory,
            exportProvider.GetExport<Microsoft.VisualStudio.Text.Editor.IEditorOptionsFactoryService>(),
            _workspaceListener,
            exportProvider.GetExportedValue<OpenTextBufferProvider>());

        _lazyExternalErrorDiagnosticUpdateSource = exportProvider.GetExport<ExternalErrorDiagnosticUpdateSource>();

        _updateUIContextJoinableTasks = new JoinableTaskCollection(_threadingContext.JoinableTaskContext);

        // Set up our telemetry session and log an event for the version
        var logDelta = _globalOptions.GetOption(DiagnosticOptionsStorage.LogTelemetryForBackgroundAnalyzerExecution);
        var telemetryService = (VisualStudioWorkspaceTelemetryService)Services.GetRequiredService<IWorkspaceTelemetryService>();
        telemetryService.InitializeTelemetrySession(TelemetryService.DefaultSession, logDelta);

        Logger.Log(FunctionId.Run_Environment, KeyValueLogMessage.Create(
            static m => m["Version"] = FileVersionInfo.GetVersionInfo(typeof(VisualStudioWorkspace).Assembly.Location).FileVersion));

        SubscribeToSourceGeneratorImpactingEvents();
    }

    private void SolutionClosingContext_UIContextChanged(object sender, UIContextChangedEventArgs e)
    {
        ProjectSystemProjectFactory.SolutionClosing = e.Activated;
    }

    internal ExternalErrorDiagnosticUpdateSource ExternalErrorDiagnosticUpdateSource => _lazyExternalErrorDiagnosticUpdateSource.Value;

    public Task CheckForAddedFileBeingOpenMaybeAsync(bool useAsync, ImmutableArray<string> newFileNames)
        => _openFileTracker.CheckForAddedFileBeingOpenMaybeAsync(useAsync, newFileNames);

    internal void AddProjectToInternalMaps(ProjectSystemProject project, IVsHierarchy? hierarchy, Guid guid, string projectSystemName)
    {
        using (_gate.DisposableWait())
        {
            _projectToHierarchyMap = _projectToHierarchyMap.Add(project.Id, hierarchy);
            _projectToGuidMap = _projectToGuidMap.Add(project.Id, guid);
            _projectSystemNameToProjectsMap.MultiAdd(projectSystemName, project);
        }
    }

    internal void AddProjectRuleSetFileToInternalMaps(ProjectSystemProject project, Func<string?> ruleSetFilePathFunc)
    {
        Contract.ThrowIfFalse(ImmutableInterlocked.TryAdd(ref _projectToRuleSetFilePath, project.Id, ruleSetFilePathFunc));
    }

    internal ProjectSystemProject? GetProjectWithHierarchyAndName(IVsHierarchy hierarchy, string projectName)
    {
        using (_gate.DisposableWait())
        {
            return GetProjectWithHierarchyAndName_NoLock(hierarchy, projectName);
        }
    }

    private ProjectSystemProject? GetProjectWithHierarchyAndName_NoLock(IVsHierarchy hierarchy, string projectName)
    {
        if (_projectSystemNameToProjectsMap.TryGetValue(projectName, out var projects))
        {
            foreach (var project in projects)
            {
                if (_projectToHierarchyMap.TryGetValue(project.Id, out var projectHierarchy))
                {
                    if (projectHierarchy == hierarchy)
                    {
                        return project;
                    }
                }
            }
        }

        return null;
    }

    // TODO: consider whether this should be going to the project system directly to get this path. This is only called on interactions from the
    // Solution Explorer in the SolutionExplorerShim, where if we just could more directly get to the rule set file it'd simplify this.
    internal override string? TryGetRuleSetPathForProject(ProjectId projectId)
    {
        // _projectToRuleSetFilePath is immutable, so can be used outside of locks
        if (_projectToRuleSetFilePath.TryGetValue(projectId, out var ruleSetPathFunc))
        {
            return ruleSetPathFunc();
        }
        else
        {
            return null;
        }
    }

    public override EnvDTE.FileCodeModel GetFileCodeModel(DocumentId documentId)
    {
        if (documentId == null)
        {
            throw new ArgumentNullException(nameof(documentId));
        }

        var document = _threadingContext.JoinableTaskFactory.Run(() => CurrentSolution.GetDocumentAsync(documentId, includeSourceGenerated: true).AsTask());
        if (document == null)
        {
            throw new ArgumentException(ServicesVSResources.The_given_DocumentId_did_not_come_from_the_Visual_Studio_workspace, nameof(documentId));
        }

        if (document is SourceGeneratedDocument sourceGeneratedDocument)
        {
            return _projectCodeModelFactory.Value.CreateFileCodeModel(sourceGeneratedDocument);
        }
        else
        {
            return _projectCodeModelFactory.Value.GetOrCreateFileCodeModel(documentId.ProjectId, document.FilePath!);
        }
    }

    internal override bool TryApplyChanges(
        Microsoft.CodeAnalysis.Solution newSolution,
        IProgress<CodeAnalysisProgress> progressTracker)
    {
        if (!_threadingContext.JoinableTaskContext.IsOnMainThread)
        {
            throw new InvalidOperationException(ServicesVSResources.VisualStudioWorkspace_TryApplyChanges_cannot_be_called_from_a_background_thread);
        }

        var currentSolution = this.CurrentSolution;
        var projectChanges = newSolution.GetChanges(currentSolution).GetProjectChanges().ToList();

        // first make sure we can edit the document we will be updating (check them out from source control, etc)
        var changedDocs = projectChanges.SelectMany(pd => pd.GetChangedDocuments(onlyGetDocumentsWithTextChanges: true).Concat(pd.GetChangedAdditionalDocuments()))
                                        .Where(CanApplyChange).ToList();
        if (changedDocs.Count > 0)
        {
            this.EnsureEditableDocuments(changedDocs);
        }

        return base.TryApplyChanges(newSolution, progressTracker);

        bool CanApplyChange(DocumentId documentId)
        {
            var document = newSolution.GetDocument(documentId) ?? currentSolution.GetDocument(documentId);
            if (document == null)
            {
                // we can have null if documentId is for additional files
                return true;
            }

            return document.CanApplyChange();
        }
    }

    public override bool CanOpenDocuments
    {
        get
        {
            return true;
        }
    }

    internal override bool CanChangeActiveContextDocument
    {
        get
        {
            return true;
        }
    }

    internal bool IsCPSProject(CodeAnalysis.Project project)
        => IsCPSProject(project.Id);

    internal bool IsCPSProject(ProjectId projectId)
    {
        _threadingContext.ThrowIfNotOnUIThread();

        if (this.TryGetHierarchy(projectId, out var hierarchy))
        {
            return hierarchy.IsCapabilityMatch("CPS");
        }

        return false;
    }

    internal bool IsPrimaryProject(ProjectId projectId)
    {
        using (_gate.DisposableWait())
        {
            foreach (var (_, projects) in _projectSystemNameToProjectsMap)
            {
                foreach (var project in projects)
                {
                    if (project.Id == projectId)
                        return project.IsPrimary;
                }
            }
        }

        return true;
    }

    public override bool CanApplyCompilationOptionChange(CompilationOptions oldOptions, CompilationOptions newOptions, CodeAnalysis.Project project)
        => project.Services.GetRequiredService<ICompilationOptionsChangingService>().CanApplyChange(oldOptions, newOptions);

    public override bool CanApplyParseOptionChange(ParseOptions oldOptions, ParseOptions newOptions, CodeAnalysis.Project project)
    {
        var maxSupportLangVersion = ProjectSystemProjectFactory.TryGetMaxSupportedLanguageVersion(project.Id);

        return project.Services.GetRequiredService<IParseOptionsChangingService>().CanApplyChange(oldOptions, newOptions, maxSupportLangVersion);
    }

    private void AddTextBufferCloneServiceToBuffer(object sender, TextBufferCreatedEventArgs e)
        => e.TextBuffer.Properties.AddProperty(typeof(ITextBufferCloneService), _textBufferCloneService);

    public override bool CanApplyChange(ApplyChangesKind feature)
    {
        switch (feature)
        {
            case ApplyChangesKind.AddDocument:
            case ApplyChangesKind.RemoveDocument:
            case ApplyChangesKind.ChangeDocument:
            case ApplyChangesKind.AddMetadataReference:
            case ApplyChangesKind.RemoveMetadataReference:
            case ApplyChangesKind.AddProjectReference:
            case ApplyChangesKind.RemoveProjectReference:
            case ApplyChangesKind.AddAnalyzerReference:
            case ApplyChangesKind.RemoveAnalyzerReference:
            case ApplyChangesKind.AddAdditionalDocument:
            case ApplyChangesKind.RemoveAdditionalDocument:
            case ApplyChangesKind.ChangeAdditionalDocument:
            case ApplyChangesKind.ChangeCompilationOptions:
            case ApplyChangesKind.ChangeParseOptions:
            case ApplyChangesKind.ChangeDocumentInfo:
            case ApplyChangesKind.AddAnalyzerConfigDocument:
            case ApplyChangesKind.RemoveAnalyzerConfigDocument:
            case ApplyChangesKind.ChangeAnalyzerConfigDocument:
            case ApplyChangesKind.AddSolutionAnalyzerReference:
            case ApplyChangesKind.RemoveSolutionAnalyzerReference:
                return true;

            default:
                return false;
        }
    }

    private bool TryGetProjectData(ProjectId projectId, [NotNullWhen(returnValue: true)] out IVsHierarchy? hierarchy, [NotNullWhen(returnValue: true)] out EnvDTE.Project? project)
    {
        project = null;

        return
            this.TryGetHierarchy(projectId, out hierarchy) &&
            hierarchy.TryGetProject(out project);
    }

    internal void GetProjectData(ProjectId projectId, out IVsHierarchy hierarchy, out EnvDTE.Project project)
    {
        if (!TryGetProjectData(projectId, out hierarchy!, out project!))
        {
            throw new ArgumentException(string.Format(ServicesVSResources.Could_not_find_project_0, projectId));
        }
    }

    internal EnvDTE.Project? TryGetDTEProject(ProjectId projectId)
        => TryGetProjectData(projectId, out var _, out var project) ? project : null;

    internal bool TryAddReferenceToProject(ProjectId projectId, string assemblyName)
    {
        if (!TryGetProjectData(projectId, out _, out var project))
        {
            return false;
        }

        var vsProject = (VSProject)project.Object;
        try
        {
            vsProject.References.Add(assemblyName);
        }
        catch (Exception)
        {
            return false;
        }

        return true;
    }

    private static string? GetAnalyzerPath(AnalyzerReference analyzerReference)
        => analyzerReference.FullPath;

    protected override void ApplyCompilationOptionsChanged(ProjectId projectId, CompilationOptions options)
    {
        if (projectId == null)
        {
            throw new ArgumentNullException(nameof(projectId));
        }

        if (options == null)
        {
            throw new ArgumentNullException(nameof(options));
        }

        var originalProject = CurrentSolution.GetRequiredProject(projectId);
        var compilationOptionsService = originalProject.Services.GetRequiredService<ICompilationOptionsChangingService>();
        var storage = ProjectPropertyStorage.Create(TryGetDTEProject(projectId)!, ServiceProvider.GlobalProvider);
        compilationOptionsService.Apply(originalProject.CompilationOptions!, options, storage);
    }

    protected override void ApplyParseOptionsChanged(ProjectId projectId, ParseOptions options)
    {
        if (projectId == null)
        {
            throw new ArgumentNullException(nameof(projectId));
        }

        if (options == null)
        {
            throw new ArgumentNullException(nameof(options));
        }

        var parseOptionsService = CurrentSolution.GetRequiredProject(projectId).Services.GetRequiredService<IParseOptionsChangingService>();
        var storage = ProjectPropertyStorage.Create(TryGetDTEProject(projectId)!, ServiceProvider.GlobalProvider);
        parseOptionsService.Apply(options, storage);
    }

    protected override void ApplyAnalyzerReferenceAdded(ProjectId projectId, AnalyzerReference analyzerReference)
    {
        if (projectId == null)
        {
            throw new ArgumentNullException(nameof(projectId));
        }

        if (analyzerReference == null)
        {
            throw new ArgumentNullException(nameof(analyzerReference));
        }

        GetProjectData(projectId, out _, out var project);

        var filePath = GetAnalyzerPath(analyzerReference);
        if (filePath != null)
        {
            var vsProject = (VSProject3)project.Object;
            vsProject.AnalyzerReferences.Add(filePath);
        }
    }

    protected override void ApplyAnalyzerReferenceRemoved(ProjectId projectId, AnalyzerReference analyzerReference)
    {
        if (projectId == null)
        {
            throw new ArgumentNullException(nameof(projectId));
        }

        if (analyzerReference == null)
        {
            throw new ArgumentNullException(nameof(analyzerReference));
        }

        GetProjectData(projectId, out _, out var project);

        var filePath = GetAnalyzerPath(analyzerReference);
        if (filePath != null)
        {
            var vsProject = (VSProject3)project.Object;
            vsProject.AnalyzerReferences.Remove(filePath);
        }
    }

    private static string? GetMetadataPath(MetadataReference metadataReference)
    {
        if (metadataReference is PortableExecutableReference fileMetadata)
        {
            return fileMetadata.FilePath;
        }

        return null;
    }

    protected override void ApplyMetadataReferenceAdded(
        ProjectId projectId, MetadataReference metadataReference)
    {
        if (projectId == null)
        {
            throw new ArgumentNullException(nameof(projectId));
        }

        if (metadataReference == null)
        {
            throw new ArgumentNullException(nameof(metadataReference));
        }

        GetProjectData(projectId, out _, out var project);

        var filePath = GetMetadataPath(metadataReference);
        if (filePath != null)
        {
            var vsProject = (VSProject)project.Object;
            vsProject.References.Add(filePath);

            var undoManager = TryGetUndoManager();
            undoManager?.Add(new RemoveMetadataReferenceUndoUnit(this, projectId, filePath));
        }
    }

    protected override void ApplyMetadataReferenceRemoved(
        ProjectId projectId, MetadataReference metadataReference)
    {
        if (projectId == null)
        {
            throw new ArgumentNullException(nameof(projectId));
        }

        if (metadataReference == null)
        {
            throw new ArgumentNullException(nameof(metadataReference));
        }

        GetProjectData(projectId, out _, out var project);

        var filePath = GetMetadataPath(metadataReference);
        if (filePath != null)
        {
            var vsProject = (VSProject)project.Object;
            foreach (Reference reference in vsProject.References)
            {
                if (StringComparer.OrdinalIgnoreCase.Equals(reference.Path, filePath))
                {
                    reference.Remove();
                    var undoManager = TryGetUndoManager();
                    undoManager?.Add(new AddMetadataReferenceUndoUnit(this, projectId, filePath));
                    break;
                }
            }
        }
    }

    internal override void ApplyMappedFileChanges(SolutionChanges solutionChanges)
    {
        // Get the original text changes from all documents and call the span mapping service to get span mappings for the text changes.
        // Create mapped text changes using the mapped spans and original text changes' text.

        // Mappings for opened razor files are retrieved via the LSP client making a request to the razor server.
        // If we wait for the result on the UI thread, we will hit a bug in the LSP client that brings us to a code path
        // using ConfigureAwait(true).  This deadlocks as it then attempts to return to the UI thread which is already blocked by us.
        // Instead, we invoke this in JTF run which will mitigate deadlocks when the ConfigureAwait(true)
        // tries to switch back to the main thread in the LSP client.
        // Link to LSP client bug for ConfigureAwait(true) - https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1216657
        var mappedChanges = _threadingContext.JoinableTaskFactory.Run(() => GetMappedTextChangesAsync(solutionChanges, CancellationToken.None));

        // Group the mapped text changes by file, then apply all mapped text changes for the file.
        foreach (var changesForFile in mappedChanges)
        {
            // It doesn't matter which of the file's projectIds we pass to the invisible editor, so just pick the first.
            var projectId = changesForFile.Value.First().ProjectId;
            // Make sure we only take distinct changes - we'll have duplicates from different projects for linked files or multi-targeted files.
            var distinctTextChanges = changesForFile.Value.Select(change => change.TextChange).Distinct().ToImmutableArray();
            using var invisibleEditor = new InvisibleEditor(ServiceProvider.GlobalProvider, changesForFile.Key, GetHierarchy(projectId), needsSave: true, needsUndoDisabled: false);
            TextEditApplication.UpdateText(distinctTextChanges, invisibleEditor.TextBuffer, EditOptions.None);
        }

        return;

        async Task<MultiDictionary<string, (TextChange TextChange, ProjectId ProjectId)>> GetMappedTextChangesAsync(SolutionChanges solutionChanges, CancellationToken cancellationToken)
        {
            var filePathToMappedTextChanges = new MultiDictionary<string, (TextChange TextChange, ProjectId ProjectId)>();
            foreach (var projectChanges in solutionChanges.GetProjectChanges())
            {
                foreach (var changedDocumentId in projectChanges.GetChangedDocuments())
                {
                    var oldDocument = projectChanges.OldProject.GetRequiredDocument(changedDocumentId);
                    if (!ShouldApplyChangesToMappedDocuments(oldDocument, out var mappingService))
                    {
                        continue;
                    }

                    var newDocument = projectChanges.NewProject.GetRequiredDocument(changedDocumentId);
                    var mappedTextChanges = await mappingService.GetMappedTextChangesAsync(
                        oldDocument, newDocument, cancellationToken).ConfigureAwait(true);
                    foreach (var (filePath, textChange) in mappedTextChanges)
                    {
                        filePathToMappedTextChanges.Add(filePath, (textChange, projectChanges.ProjectId));
                    }
                }
            }

            var sourceGeneratedDocumentMappingService = Services.GetService<ISourceGeneratedDocumentSpanMappingService>();
            if (sourceGeneratedDocumentMappingService is not null)
            {
                // Since we're mapping changes to source generated documents, we have to ensure the old solution has run the generators
                // so the mapper has something to compare to.
                foreach (var (docId, state) in solutionChanges.NewSolution.CompilationState.FrozenSourceGeneratedDocumentStates.States)
                {
                    var document = await solutionChanges.OldSolution.GetRequiredDocumentAsync(docId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(true);
                    Contract.ThrowIfFalse(document.IsRazorSourceGeneratedDocument());
                }

                foreach (var docId in solutionChanges.GetExplicitlyChangedSourceGeneratedDocuments())
                {
                    var oldDocument = solutionChanges.OldSolution.GetRequiredSourceGeneratedDocumentForAlreadyGeneratedId(docId);
                    var newDocument = solutionChanges.NewSolution.GetRequiredSourceGeneratedDocumentForAlreadyGeneratedId(docId);
                    var mappedTextChanges = await sourceGeneratedDocumentMappingService.GetMappedTextChangesAsync(oldDocument, newDocument, cancellationToken).ConfigureAwait(true);
                    foreach (var (filePath, textChange) in mappedTextChanges)
                    {
                        filePathToMappedTextChanges.Add(filePath, (textChange, docId.ProjectId));
                    }
                }
            }

            return filePathToMappedTextChanges;
        }

        bool ShouldApplyChangesToMappedDocuments(CodeAnalysis.Document document, [NotNullWhen(true)] out ISpanMappingService? spanMappingService)
        {
            spanMappingService = document.DocumentServiceProvider.GetService<ISpanMappingService>();
            // Only consider files that are mapped and that we are unable to apply changes to.
            // TODO - refactor how this is determined - https://github.com/dotnet/roslyn/issues/47908
            return spanMappingService != null && document?.CanApplyChange() == false;
        }
    }

    protected override void ApplyProjectReferenceAdded(
        ProjectId projectId, ProjectReference projectReference)
    {
        if (projectId == null)
        {
            throw new ArgumentNullException(nameof(projectId));
        }

        if (projectReference == null)
        {
            throw new ArgumentNullException(nameof(projectReference));
        }

        GetProjectData(projectId, out _, out var project);
        GetProjectData(projectReference.ProjectId, out _, out var refProject);

        var vsProject = (VSProject)project.Object;
        vsProject.References.AddProject(refProject);

        var undoManager = TryGetUndoManager();
        undoManager?.Add(new RemoveProjectReferenceUndoUnit(
            this, projectId, projectReference.ProjectId));
    }

    private OleInterop.IOleUndoManager? TryGetUndoManager()
    {
        var documentTrackingService = this.Services.GetRequiredService<IDocumentTrackingService>();
        var documentId = documentTrackingService.TryGetActiveDocument() ?? documentTrackingService.GetVisibleDocuments().FirstOrDefault();
        if (documentId != null)
        {
            var composition = (IComponentModel)ServiceProvider.GlobalProvider.GetService(typeof(SComponentModel));
            var exportProvider = composition.DefaultExportProvider;
            var editorAdaptersService = exportProvider.GetExportedValue<IVsEditorAdaptersFactoryService>();

            return editorAdaptersService.TryGetUndoManager(this, documentId, CancellationToken.None);
        }

        return null;
    }

    protected override void ApplyProjectReferenceRemoved(
        ProjectId projectId, ProjectReference projectReference)
    {
        if (projectId == null)
        {
            throw new ArgumentNullException(nameof(projectId));
        }

        if (projectReference == null)
        {
            throw new ArgumentNullException(nameof(projectReference));
        }

        GetProjectData(projectId, out _, out var project);
        GetProjectData(projectReference.ProjectId, out _, out var refProject);

        var vsProject = (VSProject)project.Object;
        foreach (Reference reference in vsProject.References)
        {
            if (reference.SourceProject == refProject)
            {
                reference.Remove();
                var undoManager = TryGetUndoManager();
                undoManager?.Add(new AddProjectReferenceUndoUnit(this, projectId, projectReference.ProjectId));
            }
        }
    }

    protected override void ApplyDocumentAdded(DocumentInfo info, SourceText text)
        => AddDocumentCore(info, text, TextDocumentKind.Document);

    protected override void ApplyAdditionalDocumentAdded(DocumentInfo info, SourceText text)
        => AddDocumentCore(info, text, TextDocumentKind.AdditionalDocument);

    protected override void ApplyAnalyzerConfigDocumentAdded(DocumentInfo info, SourceText text)
    {
        if (!TryAddEditorConfigToSolutionItems(info, text))
            AddDocumentCore(info, text, TextDocumentKind.AnalyzerConfigDocument);
    }

    private void AddDocumentCore(DocumentInfo info, SourceText initialText, TextDocumentKind documentKind)
    {
        GetProjectData(info.Id.ProjectId, out _, out var project);

        // If the first namespace name matches the name of the project, then we don't want to
        // generate a folder for that.  The project is implicitly a folder with that name.
        var folders = info.Folders.AsEnumerable();
        if (folders.FirstOrDefault() == project.Name)
        {
            folders = folders.Skip(1);
        }

        folders = FilterFolderForProjectType(project, folders);

        if (IsWebsite(project))
        {
            AddDocumentToFolder(project, info.Id, [AppCodeFolderName], info.Name, documentKind, initialText, info.FilePath);
        }
        else if (folders.Any())
        {
            AddDocumentToFolder(project, info.Id, folders, info.Name, documentKind, initialText, info.FilePath);
        }
        else
        {
            AddDocumentToProject(project, info.Id, info.Name, documentKind, initialText, info.FilePath);
        }

        var undoManager = TryGetUndoManager();

        switch (documentKind)
        {
            case TextDocumentKind.AdditionalDocument:
                undoManager?.Add(new RemoveAdditionalDocumentUndoUnit(this, info.Id));
                break;

            case TextDocumentKind.AnalyzerConfigDocument:
                undoManager?.Add(new RemoveAnalyzerConfigDocumentUndoUnit(this, info.Id));
                break;

            case TextDocumentKind.Document:
                undoManager?.Add(new RemoveDocumentUndoUnit(this, info.Id));
                break;

            default:
                throw ExceptionUtilities.UnexpectedValue(documentKind);
        }
    }

    private static bool IsWebsite(EnvDTE.Project project)
        => project.Kind == VsWebSite.PrjKind.prjKindVenusProject;

    private static IEnumerable<string> FilterFolderForProjectType(EnvDTE.Project project, IEnumerable<string> folders)
    {
        foreach (var folder in folders)
        {
            var items = GetAllItems(project.ProjectItems);
            var folderItem = items.FirstOrDefault(p => StringComparer.OrdinalIgnoreCase.Compare(p.Name, folder) == 0);
            if (folderItem == null || folderItem.Kind != EnvDTE.Constants.vsProjectItemKindPhysicalFile)
            {
                yield return folder;
            }
        }
    }

    private static IEnumerable<ProjectItem> GetAllItems(ProjectItems projectItems)
    {
        if (projectItems == null)
            return [];

        var items = projectItems.OfType<ProjectItem>();
        return items.Concat(items.SelectMany(i => GetAllItems(i.ProjectItems)));
    }

#if false
    protected override void AddExistingDocument(DocumentId documentId, string filePath, IEnumerable<string> folders)
    {
        IVsHierarchy hierarchy;
        EnvDTE.Project project;
        IVisualStudioHostProject hostProject;
        GetProjectData(documentId.ProjectId, out hostProject, out hierarchy, out project);

        // If the first namespace name matches the name of the project, then we don't want to
        // generate a folder for that.  The project is implicitly a folder with that name.
        if (folders.FirstOrDefault() == project.Name)
        {
            folders = folders.Skip(1);
        }

        var name = Path.GetFileName(filePath);

        if (folders.Any())
        {
            AddDocumentToFolder(hostProject, project, documentId, folders, name, SourceCodeKind.Regular, initialText: null, filePath: filePath);
        }
        else
        {
            AddDocumentToProject(hostProject, project, documentId, name, SourceCodeKind.Regular, initialText: null, filePath: filePath);
        }
    }
#endif

    private void AddDocumentToProject(
        EnvDTE.Project project,
        DocumentId documentId,
        string documentName,
        TextDocumentKind documentKind,
        SourceText? initialText = null,
        string? filePath = null)
    {
        string? folderPath = null;
        if (filePath == null && !project.TryGetFullPath(out folderPath))
        {
            // TODO(cyrusn): Throw an appropriate exception here.
            throw new Exception(ServicesVSResources.Could_not_find_location_of_folder_on_disk);
        }

        AddDocumentToProjectItems(project.ProjectItems, documentId, folderPath, documentName, initialText, filePath, documentKind);
    }

    private void AddDocumentToFolder(
        EnvDTE.Project project,
        DocumentId documentId,
        IEnumerable<string> folders,
        string documentName,
        TextDocumentKind documentKind,
        SourceText? initialText = null,
        string? filePath = null)
    {
        var folder = project.FindOrCreateFolder(folders);

        string? folderPath = null;
        if (filePath == null && !folder.TryGetFullPath(out folderPath))
        {
            // TODO(cyrusn): Throw an appropriate exception here.
            throw new Exception(ServicesVSResources.Could_not_find_location_of_folder_on_disk);
        }

        AddDocumentToProjectItems(folder.ProjectItems, documentId, folderPath, documentName, initialText, filePath, documentKind);
    }

    private bool TryAddEditorConfigToSolutionItems(
        DocumentInfo documentInfo,
        SourceText text)
    {
        // We're going to see if this is an .editorconfig being added at the solution level, and if so process it specially; so verify first that it is one
        if (CurrentSolution.FilePath is null || documentInfo.FilePath is null)
            return false;

        if (PathUtilities.GetFileName(documentInfo.FilePath) != ".editorconfig")
            return false;

        if (IOUtilities.PerformIO(() => File.Exists(documentInfo.FilePath)))
            return false;

        var solutionDirectory = PathUtilities.GetDirectoryName(this.CurrentSolution.FilePath);
        if (solutionDirectory != PathUtilities.GetDirectoryName(documentInfo.FilePath))
            return false;

        // Double check too that this isn't a case of the .csproj being in the same folder of the .sln, at which point it's reasonable
        // just to add this to the project file.
        if (PathUtilities.GetDirectoryName(CurrentSolution.GetProject(documentInfo.Id.ProjectId)?.FilePath) == solutionDirectory)
            return false;

        // All checks pass, so let's treat this special.
        var dte = _threadingContext.JoinableTaskFactory.Run(() => _asyncServiceProvider.GetServiceAsync<SDTE, EnvDTE.DTE>(_threadingContext.DisposalToken));

        const string SolutionItemsFolderName = "Solution Items";

        var projects = dte.Solution.Projects.OfType<EnvDTE.Project>();
        var solutionItemsFolder = projects.FirstOrDefault(static p => p.Kind == EnvDTE.Constants.vsProjectKindSolutionItems && p.Name == SolutionItemsFolderName);

        if (solutionItemsFolder != null)
        {
            foreach (ProjectItem projectItem in solutionItemsFolder.ProjectItems)
            {
                if (projectItem.Name == documentInfo.Name)
                {
                    // It's already added to the solution folder, we just need to write the text and be done
                    using var writer = new StreamWriter(documentInfo.FilePath, append: false, encoding: text.Encoding ?? Encoding.UTF8);
                    text.Write(writer);
                    return true;
                }
            }
        }
        else
        {
            solutionItemsFolder = ((EnvDTE80.Solution2)dte.Solution).AddSolutionFolder(SolutionItemsFolderName);
        }

        AddDocumentToProjectItems(
            solutionItemsFolder.ProjectItems,
            documentInfo.Id,
            folderPath: null,
            documentInfo.Name,
            text,
            documentInfo.FilePath,
            TextDocumentKind.AnalyzerConfigDocument);
        dte.Solution.SaveAs(dte.Solution.FileName);

        return true;
    }

    private void AddDocumentToProjectItems(
        ProjectItems projectItems,
        DocumentId documentId,
        string? folderPath,
        string documentName,
        SourceText? initialText,
        string? filePath,
        TextDocumentKind documentKind)
    {
        if (filePath == null)
        {
            Contract.ThrowIfNull(folderPath, "If we didn't have a file path, then we expected a folder path to generate the file path from.");
            var baseName = Path.GetFileNameWithoutExtension(documentName);
            var extension = documentKind == TextDocumentKind.Document ? GetPreferredExtension(documentId) : Path.GetExtension(documentName);
            var uniqueName = projectItems.GetUniqueName(baseName, extension);
            filePath = Path.Combine(folderPath, uniqueName);
        }

        if (initialText != null)
        {
            using var writer = new StreamWriter(filePath, append: false, encoding: initialText.Encoding ?? Encoding.UTF8);
            initialText.Write(writer);
        }

        // TODO: restore document ID hinting -- we previously ensured that the AddFromFile will introduce the document ID being used here.
        // (tracked by https://devdiv.visualstudio.com/DevDiv/_workitems/edit/677956)
        projectItems.AddFromFile(filePath);
    }

    private void RemoveDocumentCore(DocumentId documentId, TextDocumentKind documentKind)
    {
        if (documentId == null)
        {
            throw new ArgumentNullException(nameof(documentId));
        }

        var document = this.CurrentSolution.GetTextDocument(documentId);
        if (document != null)
        {
            if (!VisualStudioWorkspaceUtilities.TryGetVsHierarchyAndItemId(
                    document, out var hierarchy, out var itemId))
            {
                // it is no longer part of the solution
                return;
            }

            var text = document.GetTextSynchronously(CancellationToken.None);

            var project = (IVsProject3)hierarchy;
            project.RemoveItem(0, itemId, out _);

            var undoManager = TryGetUndoManager();
            var docInfo = CreateDocumentInfoWithoutText(document);

            switch (documentKind)
            {
                case TextDocumentKind.AdditionalDocument:
                    undoManager?.Add(new AddAdditionalDocumentUndoUnit(this, docInfo, text));
                    break;

                case TextDocumentKind.AnalyzerConfigDocument:
                    undoManager?.Add(new AddAnalyzerConfigDocumentUndoUnit(this, docInfo, text));
                    break;

                case TextDocumentKind.Document:
                    undoManager?.Add(new AddDocumentUndoUnit(this, docInfo, text));
                    break;

                default:
                    throw ExceptionUtilities.UnexpectedValue(documentKind);
            }
        }
    }

    protected override void ApplyDocumentRemoved(DocumentId documentId)
        => RemoveDocumentCore(documentId, TextDocumentKind.Document);

    protected override void ApplyAdditionalDocumentRemoved(DocumentId documentId)
        => RemoveDocumentCore(documentId, TextDocumentKind.AdditionalDocument);

    protected override void ApplyAnalyzerConfigDocumentRemoved(DocumentId documentId)
        => RemoveDocumentCore(documentId, TextDocumentKind.AnalyzerConfigDocument);

    public override void OpenDocument(DocumentId documentId, bool activate = true)
        => OpenDocumentCore(documentId, activate);

    public override void OpenAdditionalDocument(DocumentId documentId, bool activate = true)
        => OpenDocumentCore(documentId, activate);

    public override void OpenAnalyzerConfigDocument(DocumentId documentId, bool activate = true)
        => OpenDocumentCore(documentId, activate);

    public override void CloseDocument(DocumentId documentId)
        => CloseDocumentCore(documentId);

    public override void CloseAdditionalDocument(DocumentId documentId)
        => CloseDocumentCore(documentId);

    public override void CloseAnalyzerConfigDocument(DocumentId documentId)
        => CloseDocumentCore(documentId);

    public void OpenDocumentCore(DocumentId documentId, bool activate = true)
    {
        if (documentId == null)
        {
            throw new ArgumentNullException(nameof(documentId));
        }

        if (!_threadingContext.JoinableTaskContext.IsOnMainThread)
        {
            throw new InvalidOperationException(ServicesVSResources.This_workspace_only_supports_opening_documents_on_the_UI_thread);
        }

        var document = this.CurrentSolution.GetTextDocument(documentId);
        if (document != null)
        {
            OpenDocumentFromPath(document.FilePath, document.Project.Id, activate);
        }
    }

    internal void OpenDocumentFromPath(string? filePath, ProjectId projectId, bool activate = true)
    {
        if (TryGetFrame(filePath, projectId, out var frame))
        {
            if (activate)
            {
                frame.Show();
            }
            else
            {
                frame.ShowNoActivate();
            }
        }
    }

    /// <summary>
    /// Opens a file and retrieves the window frame.
    /// </summary>
    /// <param name="filePath">the file path of the file to open.</param>
    /// <param name="projectId">used to retrieve the IVsHierarchy to ensure the file is opened in a matching context.</param>
    /// <param name="frame">the window frame.</param>
    private bool TryGetFrame(string? filePath, ProjectId projectId, [NotNullWhen(returnValue: true)] out IVsWindowFrame? frame)
    {
        frame = null;

        if (filePath == null)
        {
            return false;
        }

        var hierarchy = GetHierarchy(projectId);
        var itemId = hierarchy?.TryGetItemId(filePath) ?? (uint)VSConstants.VSITEMID.Nil;
        if (itemId == (uint)VSConstants.VSITEMID.Nil)
        {
            // If the ItemId is Nil, then IVsProject would not be able to open the
            // document using its ItemId. Thus, we must use OpenDocumentViaProject, which only
            // depends on the file path.

            var openDocumentService = ServiceProvider.GlobalProvider.GetServiceOnMainThread<SVsUIShellOpenDocument, IVsUIShellOpenDocument>();
            return ErrorHandler.Succeeded(openDocumentService.OpenDocumentViaProject(
                filePath,
                VSConstants.LOGVIEWID.TextView_guid,
                out _,
                out _,
                out _,
                out frame));
        }
        else
        {
            // If the ItemId is not Nil, then we should not call IVsUIShellDocument
            // .OpenDocumentViaProject here because that simply takes a file path and opens the
            // file within the context of the first project it finds. That would cause problems
            // if the document we're trying to open is actually a linked file in another
            // project. So, we get the project's hierarchy and open the document using its item
            // ID.

            // It's conceivable that IVsHierarchy might not implement IVsProject. However,
            // OpenDocumentViaProject itself relies upon this QI working, so it should be OK to
            // use here.

            return hierarchy is IVsProject vsProject &&
                ErrorHandler.Succeeded(vsProject.OpenItem(itemId, VSConstants.LOGVIEWID.TextView_guid, s_docDataExisting_Unknown, out frame));
        }
    }

    public void CloseDocumentCore(DocumentId documentId)
    {
        if (documentId == null)
        {
            throw new ArgumentNullException(nameof(documentId));
        }

        if (this.IsDocumentOpen(documentId))
        {
            var filePath = this.GetFilePath(documentId);
            if (filePath != null)
            {
                var openDocumentService = ServiceProvider.GlobalProvider.GetServiceOnMainThread<SVsUIShellOpenDocument, IVsUIShellOpenDocument>();
                if (ErrorHandler.Succeeded(openDocumentService.IsDocumentOpen(null, 0, filePath, Guid.Empty, 0, out _, null, out var frame, out _)))
                {
                    // TODO: do we need save argument for CloseDocument?
                    frame.CloseFrame((uint)__FRAMECLOSE.FRAMECLOSE_NoSave);
                }
            }
        }
    }

    protected override void ApplyDocumentTextChanged(DocumentId documentId, SourceText newText)
        => ApplyTextDocumentChange(documentId, newText);

    protected override void ApplyAdditionalDocumentTextChanged(DocumentId documentId, SourceText newText)
        => ApplyTextDocumentChange(documentId, newText);

    protected override void ApplyAnalyzerConfigDocumentTextChanged(DocumentId documentId, SourceText newText)
        => ApplyTextDocumentChange(documentId, newText);

    private void ApplyTextDocumentChange(DocumentId documentId, SourceText newText)
    {
        this._threadingContext.ThrowIfNotOnUIThread();

        CodeAnalysis.TextDocument? document = null;

        try
        {
            document = this.CurrentSolution.GetRequiredTextDocument(documentId);

            var containedDocument = ContainedDocument.TryGetContainedDocument(documentId);
            if (containedDocument != null)
            {
                containedDocument.UpdateText(newText);
            }
            else
            {
                if (IsDocumentOpen(documentId))
                {
                    var textBuffer = document.GetTextSynchronously(CancellationToken.None).Container.TryGetTextBuffer();
                    if (textBuffer != null)
                    {
                        TextEditApplication.UpdateText(newText, textBuffer, EditOptions.DefaultMinimalChange);
                        return;
                    }
                }

                // The document wasn't open in a normal way, so invisible editor time
                using var invisibleEditor = OpenInvisibleEditor(documentId);
                TextEditApplication.UpdateText(newText, invisibleEditor.TextBuffer, EditOptions.None);
            }
        }
        catch (Exception ex) when (FatalError.ReportAndCatch(ex))
        {
            ReportErrorChangingDocumentText(ex);
            return;
        }

        void ReportErrorChangingDocumentText(Exception exception)
        {
            // Don't spam the info bar.  If the user already has a message up, leave it at that.  Also,
            // if they've asked to not be notified about future doc change issue, respect that flag.
            if (_ignoreDocumentTextChangeErrors || _isShowingDocumentChangeErrorInfoBar)
                return;

            var documentName = document?.Name ?? documentId.ToString();

            var errorReportingService = this.Services.GetRequiredService<IErrorReportingService>();
            errorReportingService.ShowGlobalErrorInfo(
                message: string.Format(ServicesVSResources.Error_encountered_updating_0, documentName),
                TelemetryFeatureName.Workspace,
                exception,
                // 'Show stack trace' will not dismiss the info bar.
                new(WorkspacesResources.Show_Stack_Trace, InfoBarUI.UIKind.HyperLink,
                    () => errorReportingService.ShowDetailedErrorInfo(exception), closeAfterAction: false),
                // 'Ignore' just closes the info bar, but allows future errors to show up.
                new(ServicesVSResources.Ignore, InfoBarUI.UIKind.Button, GetDefaultDismissAction()),
                // 'Ignore (including future errors) closes the info bar, but also sets the flag so the user gets no more messages
                // in the current session.
                new(ServicesVSResources.Ignore_including_future_errors, InfoBarUI.UIKind.Button, GetDefaultDismissAction(
                    () => _ignoreDocumentTextChangeErrors = true)),
                // Close button is the same as 'ignore'.  It closes the info bar, but allows future errors to show up.
                new(string.Empty, InfoBarUI.UIKind.Close, GetDefaultDismissAction()));

            // Mark that we're showing the info bar at this point.
            _isShowingDocumentChangeErrorInfoBar = true;
        }

        Action GetDefaultDismissAction(Action? additionalAction = null)
            => () =>
            {
                additionalAction?.Invoke();

                // All info bar actions (except for 'show stack trace') dismiss the info bar, putting us back in the
                // "we're not showing the user anything" state.
                _isShowingDocumentChangeErrorInfoBar = false;
            };
    }

    protected override void ApplyDocumentInfoChanged(DocumentId documentId, DocumentInfo updatedInfo)
    {
        var document = CurrentSolution.GetRequiredDocument(documentId);

        FailIfDocumentInfoChangesNotSupported(document, updatedInfo);

        if (document.Name != updatedInfo.Name)
        {
            GetProjectData(updatedInfo.Id.ProjectId, out var _, out var dteProject);

            if (document.FilePath == null)
            {
                FatalError.ReportAndCatch(new Exception("Attempting to change the information of a document without a file path."));
                return;
            }

            var projectItemForDocument = dteProject.FindItemByPath(document.FilePath, StringComparer.OrdinalIgnoreCase);

            if (projectItemForDocument == null)
            {
                // TODO(https://github.com/dotnet/roslyn/issues/34276):
                Debug.Fail("Attempting to change the name of a file in a Shared Project");
                return;
            }

            // Must save the document first for things like Breakpoints to be preserved.
            // WORKAROUND: Check if the document needs to be saved before calling save. 
            // Should remove the if below and just call save() once 
            // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1163405
            // is fixed
            if (!projectItemForDocument.Saved)
            {
                projectItemForDocument.Save();
            }

            var uniqueName = projectItemForDocument.Collection
                .GetUniqueNameIgnoringProjectItem(
                    projectItemForDocument,
                    Path.GetFileNameWithoutExtension(updatedInfo.Name),
                    Path.GetExtension(updatedInfo.Name));

            // Get the current undoManager before any file renames/documentId changes happen
            var undoManager = TryGetUndoManager();

            // By setting this property, Visual Studio will perform the file rename, which 
            // will cause the workspace's current solution to update and will fire the 
            // necessary workspace changed events.
            projectItemForDocument.Name = uniqueName;

            if (projectItemForDocument.TryGetFullPath(out var newPath))
            {
                undoManager?.Add(new RenameDocumentUndoUnit(this, uniqueName, document.Name, newPath));
            }
        }
    }

    /// <summary>
    /// The <see cref="VisualStudioWorkspace"/> currently supports only a subset of <see cref="DocumentInfo"/> 
    /// changes.
    /// </summary>
    private static void FailIfDocumentInfoChangesNotSupported(CodeAnalysis.Document document, DocumentInfo updatedInfo)
    {
        if (document.SourceCodeKind != updatedInfo.SourceCodeKind)
        {
            throw new InvalidOperationException(
                $"This Workspace does not support changing a document's {nameof(document.SourceCodeKind)}.");
        }

        if (document.FilePath != updatedInfo.FilePath)
        {
            throw new InvalidOperationException(
                $"This Workspace does not support changing a document's {nameof(document.FilePath)}.");
        }

        if (document.Id != updatedInfo.Id)
        {
            throw new InvalidOperationException(
                $"This Workspace does not support changing a document's {nameof(document.Id)}.");
        }

        if (document.Folders != updatedInfo.Folders && !document.Folders.SequenceEqual(updatedInfo.Folders))
        {
            throw new InvalidOperationException(
                $"This Workspace does not support changing a document's {nameof(document.Folders)}.");
        }

        if (document.State.Attributes.IsGenerated != updatedInfo.IsGenerated)
        {
            throw new InvalidOperationException(
                $"This Workspace does not support changing a document's {nameof(document.State.Attributes.IsGenerated)} state.");
        }
    }

    private string GetPreferredExtension(DocumentId documentId)
    {
        // No extension was provided.  Pick a good one based on the type of host project.
        return CurrentSolution.GetRequiredProject(documentId.ProjectId).Language switch
        {
            // TODO: uncomment when fixing https://github.com/dotnet/roslyn/issues/5325
            //return sourceCodeKind == SourceCodeKind.Regular ? ".cs" : ".csx";
            LanguageNames.CSharp => ".cs",

            // TODO: uncomment when fixing https://github.com/dotnet/roslyn/issues/5325
            //return sourceCodeKind == SourceCodeKind.Regular ? ".vb" : ".vbx";
            LanguageNames.VisualBasic => ".vb",
            _ => throw new InvalidOperationException(),
        };
    }

    public override IVsHierarchy? GetHierarchy(ProjectId projectId)
    {
        // This doesn't take a lock since _projectToHierarchyMap is immutable
        return _projectToHierarchyMap.GetValueOrDefault(projectId, defaultValue: null);
    }

    internal override Guid GetProjectGuid(ProjectId projectId)
    {
        // This doesn't take a lock since _projectToGuidMap is immutable
        return _projectToGuidMap.GetValueOrDefault(projectId, defaultValue: Guid.Empty);
    }

    internal string? TryGetDependencyNodeTargetIdentifier(ProjectId projectId)
    {
        return ProjectSystemProjectFactory.TryGetDependencyNodeTargetIdentifier(projectId);
    }

    internal override void SetDocumentContext(DocumentId documentId)
    {
        _threadingContext.ThrowIfNotOnUIThread();

        // Note: this method does not actually call into any workspace code here to change the workspace's context. The assumption is updating the running document table or
        // IVsHierarchies will raise the appropriate events which we are subscribed to.

        var hierarchy = GetHierarchy(documentId.ProjectId);
        if (hierarchy == null)
        {
            // If we don't have a hierarchy then there's nothing we can do
            return;
        }

        // The hierarchy might be supporting multitargeting; in that case, let's update the context. Unfortunately the IVsHierarchies that support this
        // don't necessarily let us read it first, so we have to fire-and-forget here.
        string? projectSystemNameForProjectId = null;

        using (_gate.DisposableWait())
        {
            foreach (var (projectSystemName, projects) in _projectSystemNameToProjectsMap)
            {
                if (projects.Any(p => p.Id == documentId.ProjectId))
                {
                    projectSystemNameForProjectId = projectSystemName;
                }
            }
        }

        if (projectSystemNameForProjectId is null)
        {
            // Project must have been removed asynchronously
            return;
        }

        // The hierarchy might be supporting multitargeting; in that case, let's update the context. Unfortunately the IVsHierarchies that support this
        // don't necessarily let us read it first, so we have to fire-and-forget here.
        hierarchy.SetProperty(VSConstants.VSITEMID_ROOT, (int)__VSHPROPID8.VSHPROPID_ActiveIntellisenseProjectContext, projectSystemNameForProjectId);

        var filePath = GetFilePath(documentId);
        if (filePath == null)
        {
            return;
        }

        var itemId = hierarchy.TryGetItemId(filePath);
        if (itemId != VSConstants.VSITEMID_NIL)
        {
            // Is this owned by a shared asset project? If so, we need to put the shared asset project into the running document table, and need to set the
            // current hierarchy as the active context of that shared hierarchy. This is kept as a loop that we do multiple times in the case that you
            // have multiple pointers. This used to be the case for multitargeting projects, but that was now handled by setting the active context property
            // above. Some project systems out there might still be supporting it, so we'll support it too.
            while (SharedProjectUtilities.TryGetItemInSharedAssetsProject(hierarchy, itemId, out var sharedHierarchy, out var sharedItemId) &&
                   hierarchy != sharedHierarchy)
            {
                // Ensure the shared context is set correctly
                if (sharedHierarchy.GetActiveProjectContext() != hierarchy)
                {
                    ErrorHandler.ThrowOnFailure(sharedHierarchy.SetActiveProjectContext(hierarchy));
                }

                // We now need to ensure the outer project is also set up
                hierarchy = sharedHierarchy;
                itemId = sharedItemId;
            }
        }

        // Update the ownership of the file in the Running Document Table
        var project = (IVsProject3)hierarchy;
        project.TransferItem(filePath, filePath, punkWindowFrame: null);
    }

    internal bool TryGetHierarchy(ProjectId projectId, [NotNullWhen(returnValue: true)] out IVsHierarchy? hierarchy)
    {
        hierarchy = this.GetHierarchy(projectId);
        return hierarchy != null;
    }

    protected override void Dispose(bool finalize)
    {
        if (!finalize)
        {
            _textBufferFactoryService.TextBufferCreated -= AddTextBufferCloneServiceToBuffer;
            _projectionBufferFactoryService.ProjectionBufferCreated -= AddTextBufferCloneServiceToBuffer;

            // UIContext.FromUIContextGuid internally has a map from the GUID to the context object itself that is stored in a static;
            // if we don't unsubscribe, it will leak our workspace object which can cause memory leaks in tests that create a whole MEF container
            // per test.
            _solutionClosingContext?.UIContextChanged -= SolutionClosingContext_UIContextChanged;
        }

        base.Dispose(finalize);
    }

    public virtual void EnsureEditableDocuments(IEnumerable<DocumentId> documents)
    {
        var queryEdit = (IVsQueryEditQuerySave2)ServiceProvider.GlobalProvider.GetService(typeof(SVsQueryEditQuerySave));

        // make sure given document id actually exist in current solution and the file is marked as supporting modifications
        // and actually has non null file path
        var fileNames = documents.Select(GetFilePath).ToArray();

        // TODO: meditate about the flags we can pass to this and decide what is most appropriate for Roslyn
        var result = queryEdit.QueryEditFiles(
            rgfQueryEdit: 0,
            cFiles: fileNames.Length,
            rgpszMkDocuments: fileNames,
            rgrgf: new uint[fileNames.Length],
            rgFileInfo: new VSQEQS_FILE_ATTRIBUTE_DATA[fileNames.Length],
            pfEditVerdict: out var editVerdict,
            prgfMoreInfo: out var editResultFlags);

        if (ErrorHandler.Failed(result) ||
            editVerdict != (uint)tagVSQueryEditResult.QER_EditOK)
        {
            throw new Exception("Unable to check out the files from source control.");
        }

        if ((editResultFlags & (uint)(tagVSQueryEditResultFlags2.QER_Changed | tagVSQueryEditResultFlags2.QER_Reloaded)) != 0)
        {
            throw new Exception("A file was reloaded during the source control checkout.");
        }
    }

    internal override bool CanAddProjectReference(ProjectId referencingProject, ProjectId referencedProject)
    {
        _threadingContext.ThrowIfNotOnUIThread();

        if (!TryGetHierarchy(referencingProject, out var referencingHierarchy) ||
            !TryGetHierarchy(referencedProject, out var referencedHierarchy))
        {
            // Couldn't even get a hierarchy for this project. So we have to assume
            // that adding a reference is disallowed.
            return false;
        }

        // First we have to see if either project disallows the reference being added.
        const int ContextFlags = (int)__VSQUERYFLAVORREFERENCESCONTEXT.VSQUERYFLAVORREFERENCESCONTEXT_RefreshReference;

        var canAddProjectReference = (uint)__VSREFERENCEQUERYRESULT.REFERENCE_UNKNOWN;
        var canBeReferenced = (uint)__VSREFERENCEQUERYRESULT.REFERENCE_UNKNOWN;

        if (referencingHierarchy is IVsProjectFlavorReferences3 referencingProjectFlavor3)
        {
            if (ErrorHandler.Failed(referencingProjectFlavor3.QueryAddProjectReferenceEx(referencedHierarchy, ContextFlags, out canAddProjectReference, out _)))
            {
                // Something went wrong even trying to see if the reference would be allowed.
                // Assume it won't be allowed.
                return false;
            }

            if (canAddProjectReference == (uint)__VSREFERENCEQUERYRESULT.REFERENCE_DENY)
            {
                // Adding this project reference is not allowed.
                return false;
            }
        }

        if (referencedHierarchy is IVsProjectFlavorReferences3 referencedProjectFlavor3)
        {
            if (ErrorHandler.Failed(referencedProjectFlavor3.QueryCanBeReferencedEx(referencingHierarchy, ContextFlags, out canBeReferenced, out _)))
            {
                // Something went wrong even trying to see if the reference would be allowed.
                // Assume it won't be allowed.
                return false;
            }

            if (canBeReferenced == (uint)__VSREFERENCEQUERYRESULT.REFERENCE_DENY)
            {
                // Adding this project reference is not allowed.
                return false;
            }
        }

        // Neither project denied the reference being added.  At this point, if either project
        // allows the reference to be added, and the other doesn't block it, then we can add
        // the reference.
        if (canAddProjectReference == (int)__VSREFERENCEQUERYRESULT.REFERENCE_ALLOW ||
            canBeReferenced == (int)__VSREFERENCEQUERYRESULT.REFERENCE_ALLOW)
        {
            return true;
        }

        // In both directions things are still unknown.  Fallback to the reference manager
        // to make the determination here.
        var referenceManager = (IVsReferenceManager)ServiceProvider.GlobalProvider.GetService(typeof(SVsReferenceManager));
        if (referenceManager == null)
        {
            // Couldn't get the reference manager.  Have to assume it's not allowed.
            return false;
        }

        // As long as the reference manager does not deny things, then we allow the
        // reference to be added.
        var result = referenceManager.QueryCanReferenceProject(referencingHierarchy, referencedHierarchy);
        return result != (uint)__VSREFERENCEQUERYRESULT.REFERENCE_DENY;
    }

    internal void RemoveProjectFromMaps(CodeAnalysis.Project project)
    {
        foreach (var (projectName, projects) in _projectSystemNameToProjectsMap)
        {
            if (projects.RemoveAll(p => p.Id == project.Id) > 0)
            {
                if (projects.Count == 0)
                {
                    _projectSystemNameToProjectsMap.Remove(projectName);
                }

                break;
            }
        }

        _projectToHierarchyMap = _projectToHierarchyMap.Remove(project.Id);
        _projectToGuidMap = _projectToGuidMap.Remove(project.Id);

        ImmutableInterlocked.TryRemove(ref _projectToRuleSetFilePath, project.Id, out _);

        // Try to update the UI context info.  But cancel that work if we're shutting down.
        _threadingContext.RunWithShutdownBlockAsync(async cancellationToken =>
        {
            using var asyncToken = _workspaceListener.BeginAsyncOperation(nameof(RefreshProjectExistsUIContextForLanguageAsync));
            await RefreshProjectExistsUIContextForLanguageAsync(project.Language, cancellationToken).ConfigureAwait(false);
        });
    }

    internal async Task RefreshProjectExistsUIContextForLanguageAsync(string language, CancellationToken cancellationToken)
    {
        using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
        {
            var isContextActive = _languageToProjectExistsUIContextState.GetOrAdd(language, false);

            // Determine if there is a project with a matching language. Uses _projectSystemNameToProjectsMap as 
            // that data structure is updated before calling into this method, whereas CurrentSolution may not be.
            var projectExistsWithLanguage = _projectSystemNameToProjectsMap.Any(projects => projects.Value.Any(project => project.Language == language));
            if (projectExistsWithLanguage != isContextActive)
            {
                _languageToProjectExistsUIContextState[language] = projectExistsWithLanguage;

                // Create a task to update the UI context, and add it to the task collection that all callers to 
                // this method will wait on before returning.
                var joinableTask = _threadingContext.JoinableTaskFactory.RunAsync(() => UpdateUIContextAsync(language, cancellationToken));

                _updateUIContextJoinableTasks.Add(joinableTask);
            }
        }

        // Ensure any pending ui context updates have occurred before returning
        await _updateUIContextJoinableTasks.JoinTillEmptyAsync(cancellationToken).ConfigureAwait(false);
    }

    private async Task UpdateUIContextAsync(string language, CancellationToken cancellationToken)
    {
        await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);

        var uiContext = Services.GetLanguageServices(language).GetService<IProjectExistsUIContextProviderLanguageService>()?.GetUIContext();

        // UIContexts can be "zombied" if UIContexts aren't supported because we're in a command line build or in
        // other scenarios.
        if (uiContext == null || uiContext.IsZombie)
            return;

        // Note: it's safe to read CurrentSolution here outside of any sort of lock.  We do all work here on the UI
        // thread, so that acts as a natural ordering mechanism here.  If, say, a BG piece of work was mutating this
        // solution (either adding or removing a project) then that work will also have enqueued the next refresh
        // operation on the UI thread.  So we'll always eventually reach a fixed point where the task for that
        // language will check the latest CurrentSolution we have and will set the IsActive bit accordingly. We
        // don't use the isContextActive value here specifically for this case as it may not reflect the desired
        // value after the main thread switch.
        uiContext.IsActive = this.CurrentSolution.Projects.Any(p => p.Language == language);
    }

    internal void PreloadProjectSystemComponents(string languageName)
    {
        // Ensure we have any listeners for WellKnownEventListeners.Workspace warmed up, since these are otherwise created
        // the first time we make a change to the CurrentSolution
        base.EnsureEventListeners();

        // Load up the command line parser and warm it up. This generally ensures we have our language specific binaries loaded
        // and we have the command line parser ready to go, since those tend to be more expensive things to JIT.
        var commandLineParserService = Services.GetRequiredLanguageService<ICommandLineParserService>(languageName);
        commandLineParserService.Parse([], null, isInteractive: false, sdkDirectory: null);
    }
}
