// 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.

#nullable disable

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.BackgroundWorkIndicator;
using Microsoft.CodeAnalysis.Editor.Host;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Editor.Undo;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Notification;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Rename;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Threading;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename;

internal sealed partial class InlineRenameSession : IInlineRenameSession, IFeatureController
{
    private readonly ITextBufferAssociatedViewService _textBufferAssociatedViewService;
    private readonly ITextBufferFactoryService _textBufferFactoryService;
    private readonly ITextBufferCloneService _textBufferCloneService;

    private readonly IFeatureService _featureService;
    private readonly IFeatureDisableToken _completionDisabledToken;
    private readonly IAsynchronousOperationListener _asyncListener;
    private readonly Solution _baseSolution;
    private readonly ITextView _triggerView;
    private readonly IDisposable _inlineRenameSessionDurationLogBlock;
    private readonly IThreadingContext _threadingContext;
    private readonly WorkspaceEventRegistration _workspaceChangedDisposer;

    public readonly InlineRenameService RenameService;

    private bool _dismissed;
    private bool _isApplyingEdit;
    private readonly Dictionary<ITextBuffer, OpenTextBufferManager> _openTextBuffers = [];

    /// <summary>
    /// The original <see cref="Document"/> where rename was triggered
    /// </summary>
    public Document TriggerDocument { get; }

    /// <summary>
    /// The original <see cref="SnapshotSpan"/> for the identifier that rename was triggered on
    /// </summary>
    public SnapshotSpan TriggerSpan { get; }

    /// <summary>
    /// If non-null, the current text of the replacement. Linked spans added will automatically be updated with this
    /// text.
    /// </summary>
    public string ReplacementText
    {
        get;
        private set
        {
            field = value;
            ReplacementTextChanged?.Invoke(this, EventArgs.Empty);
        }
    }

    /// <summary>
    /// Information about whether a file rename should be allowed as part
    /// of the rename operation, as determined by the language
    /// </summary>
    public InlineRenameFileRenameInfo FileRenameInfo { get; }

    /// <summary>
    /// Information about this rename session.
    /// </summary>
    public IInlineRenameInfo RenameInfo { get; }

    /// <summary>
    /// The task which computes the main rename locations against the original workspace
    /// snapshot.
    /// </summary>
    public JoinableTask<IInlineRenameLocationSet> AllRenameLocationsTask { get; private set; }

    /// <summary>
    /// Keep-alive session held alive with the OOP server.  This allows us to pin the initial solution snapshot over on
    /// the oop side, which is valuable for preventing it from constantly being dropped/synced on every conflict
    /// resolution step.
    /// </summary>
    private readonly RemoteKeepAliveSession _keepAliveSession;

    /// <summary>
    /// The cancellation token for most work being done by the inline rename session. This
    /// includes the <see cref="AllRenameLocationsTask"/> tasks.
    /// </summary>
    private readonly CancellationTokenSource _cancellationTokenSource = new();

    /// <summary>
    /// This task is a continuation of the <see cref="AllRenameLocationsTask"/> that is the result of computing
    /// the resolutions of the rename spans for the current replacementText.
    /// </summary>
    private JoinableTask<IInlineRenameReplacementInfo> _conflictResolutionTask;

    /// <summary>
    /// The cancellation source for <see cref="_conflictResolutionTask"/>.
    /// </summary>
    private CancellationTokenSource _conflictResolutionTaskCancellationSource = new();

    /// <summary>
    /// The initial text being renamed.
    /// </summary>
    private readonly string _initialRenameText;

    public InlineRenameSession(
        IThreadingContext threadingContext,
        InlineRenameService renameService,
        Workspace workspace,
        SnapshotSpan triggerSpan,
        IInlineRenameInfo renameInfo,
        SymbolRenameOptions options,
        bool previewChanges,
        ITextBufferAssociatedViewService textBufferAssociatedViewService,
        ITextBufferFactoryService textBufferFactoryService,
        ITextBufferCloneService textBufferCloneService,
        IFeatureServiceFactory featureServiceFactory,
        IAsynchronousOperationListener asyncListener)
    {
        // This should always be touching a symbol since we verified that upon invocation
        _threadingContext = threadingContext;
        RenameInfo = renameInfo;

        TriggerSpan = triggerSpan;
        TriggerDocument = triggerSpan.Snapshot.GetOpenDocumentInCurrentContextWithChanges();
        if (TriggerDocument == null)
        {
            throw new InvalidOperationException(EditorFeaturesResources.The_triggerSpan_is_not_included_in_the_given_workspace);
        }

        _inlineRenameSessionDurationLogBlock = Logger.LogBlock(FunctionId.Rename_InlineSession, CancellationToken.None);

        Workspace = workspace;

        // Requires the main thread due to OnWorkspaceChanged calling the Cancel method
        _workspaceChangedDisposer = workspace.RegisterWorkspaceChangedHandler(OnWorkspaceChanged, WorkspaceEventOptions.RequiresMainThreadOptions);

        _textBufferFactoryService = textBufferFactoryService;
        _textBufferCloneService = textBufferCloneService;
        _textBufferAssociatedViewService = textBufferAssociatedViewService;
        _textBufferAssociatedViewService.SubjectBuffersConnected += OnSubjectBuffersConnected;

        // Disable completion when an inline rename session starts
        _featureService = featureServiceFactory.GlobalFeatureService;
        _completionDisabledToken = _featureService.Disable(PredefinedEditorFeatureNames.Completion, this);
        RenameService = renameService;
        _asyncListener = asyncListener;
        _triggerView = textBufferAssociatedViewService.GetAssociatedTextViews(triggerSpan.Snapshot.TextBuffer).FirstOrDefault(v => v.HasAggregateFocus) ??
            textBufferAssociatedViewService.GetAssociatedTextViews(triggerSpan.Snapshot.TextBuffer).First();

        Options = options;
        PreviewChanges = previewChanges;

        _initialRenameText = triggerSpan.GetText();
        this.ReplacementText = _initialRenameText;

        _baseSolution = TriggerDocument.Project.Solution;
        this.UndoManager = workspace.Services.GetService<IInlineRenameUndoManager>();

        FileRenameInfo = RenameInfo.GetFileRenameInfo();

        // Open a session to oop, syncing our solution to it and pinning it there.  The connection will close once
        // _cancellationTokenSource is canceled (which we always do when the session is finally ended).
        _keepAliveSession = RemoteKeepAliveSession.Create(_baseSolution, asyncListener);
        InitializeOpenBuffers(triggerSpan);
    }

    public string OriginalSymbolName => RenameInfo.DisplayName;

    // Used to aid the investigation of https://github.com/dotnet/roslyn/issues/7364
    private sealed class NullTextBufferException(Document document, SourceText text) : Exception("Cannot retrieve textbuffer from document.")
    {
#pragma warning disable IDE0052 // Remove unread private members
        private readonly Document _document = document;
        private readonly SourceText _text = text;
    }

    private void InitializeOpenBuffers(SnapshotSpan triggerSpan)
    {
        using (Logger.LogBlock(FunctionId.Rename_CreateOpenTextBufferManagerForAllOpenDocs, CancellationToken.None))
        {
            var openBuffers = new HashSet<ITextBuffer>();
            foreach (var d in Workspace.GetOpenDocumentIds())
            {
                var document = _baseSolution.GetDocument(d);
                if (document == null)
                {
                    continue;
                }

                Contract.ThrowIfFalse(document.TryGetText(out var text));
                Contract.ThrowIfNull(text);

                var textSnapshot = text.FindCorrespondingEditorTextSnapshot();
                if (textSnapshot == null)
                {
                    FatalError.ReportAndCatch(new NullTextBufferException(document, text));
                    continue;
                }

                Contract.ThrowIfNull(textSnapshot.TextBuffer);

                openBuffers.Add(textSnapshot.TextBuffer);
            }

            foreach (var buffer in openBuffers)
            {
                TryPopulateOpenTextBufferManagerForBuffer(buffer);
            }
        }

        var startingSpan = triggerSpan.Span;

        // Select this span if we didn't already have something selected
        var selections = _triggerView.Selection.GetSnapshotSpansOnBuffer(triggerSpan.Snapshot.TextBuffer);
        if (!selections.Any() ||
            selections.First().IsEmpty ||
            !startingSpan.Contains(selections.First()))
        {
            _triggerView.SetSelection(new SnapshotSpan(triggerSpan.Snapshot, startingSpan));
        }

        this.UndoManager.CreateInitialState(this.ReplacementText, _triggerView.Selection, new SnapshotSpan(triggerSpan.Snapshot, startingSpan));
        _openTextBuffers[triggerSpan.Snapshot.TextBuffer].SetReferenceSpans([startingSpan.ToTextSpan()]);

        UpdateReferenceLocationsTask();

        RenameTrackingDismisser.DismissRenameTracking(Workspace, Workspace.GetOpenDocumentIds());
    }

    private bool TryPopulateOpenTextBufferManagerForBuffer(ITextBuffer buffer)
    {
        _threadingContext.ThrowIfNotOnUIThread();
        VerifyNotDismissed();

        if (Workspace.Kind == WorkspaceKind.Interactive)
        {
            Debug.Assert(buffer.GetRelatedDocuments().Count() == 1);
            Debug.Assert(buffer.IsReadOnly(0) == buffer.IsReadOnly(VisualStudio.Text.Span.FromBounds(0, buffer.CurrentSnapshot.Length))); // All or nothing.
            if (buffer.IsReadOnly(0))
            {
                return false;
            }
        }

        if (!_openTextBuffers.ContainsKey(buffer) && buffer.SupportsRename())
        {
            _openTextBuffers[buffer] = new OpenTextBufferManager(this, Workspace, _textBufferFactoryService, _textBufferCloneService, buffer);
            return true;
        }

        return _openTextBuffers.ContainsKey(buffer);
    }

    private void OnSubjectBuffersConnected(object sender, SubjectBuffersConnectedEventArgs e)
    {
        _threadingContext.ThrowIfNotOnUIThread();
        foreach (var buffer in e.SubjectBuffers)
        {
            if (buffer.GetWorkspace() == Workspace)
            {
                if (TryPopulateOpenTextBufferManagerForBuffer(buffer))
                {
                    _openTextBuffers[buffer].ConnectToView(e.TextView);
                }
            }
        }
    }

    private void UpdateReferenceLocationsTask()
    {
        _threadingContext.ThrowIfNotOnUIThread();

        var asyncToken = _asyncListener.BeginAsyncOperation("UpdateReferencesTask");

        var currentOptions = Options;
        var currentRenameLocationsTask = AllRenameLocationsTask;
        var cancellationToken = _cancellationTokenSource.Token;

        AllRenameLocationsTask = _threadingContext.JoinableTaskFactory.RunAsync(async () =>
        {
            // Join prior work before proceeding, since it performs a required state update.
            // https://github.com/dotnet/roslyn/pull/34254#discussion_r267024593
            if (currentRenameLocationsTask != null)
                await AllRenameLocationsTask.JoinAsync(cancellationToken).ConfigureAwait(false);

            await TaskScheduler.Default;
            var inlineRenameLocations = await RenameInfo.FindRenameLocationsAsync(currentOptions, cancellationToken).ConfigureAwait(false);

            // It's unfortunate that _allRenameLocationsTask has a UI thread dependency (prevents continuations
            // from running prior to the completion of the UI operation), but the implementation does not currently
            // follow the originally-intended design.
            // https://github.com/dotnet/roslyn/issues/40890
            await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken);

            RaiseSessionSpansUpdated([.. inlineRenameLocations.Locations]);

            return inlineRenameLocations;
        });

        AllRenameLocationsTask.Task.CompletesAsyncOperation(asyncToken);

        UpdateConflictResolutionTask();
        QueueApplyReplacements();
    }

    public Workspace Workspace { get; }
    public SymbolRenameOptions Options { get; private set; }
    public bool PreviewChanges { get; private set; }
    public bool HasRenameOverloads => RenameInfo.HasOverloads;
    public bool MustRenameOverloads => RenameInfo.MustRenameOverloads;
    public IInlineRenameUndoManager UndoManager { get; }
    public bool IsCommitInProgress { get; private set; } = false;

    public event EventHandler<ImmutableArray<InlineRenameLocation>> ReferenceLocationsChanged;
    public event EventHandler<IInlineRenameReplacementInfo> ReplacementsComputed;
    public event EventHandler ReplacementTextChanged;
    public event EventHandler CommitStateChange;

    internal OpenTextBufferManager GetBufferManager(ITextBuffer buffer)
        => _openTextBuffers[buffer];

    internal bool TryGetBufferManager(ITextBuffer buffer, out OpenTextBufferManager bufferManager)
        => _openTextBuffers.TryGetValue(buffer, out bufferManager);

    public void RefreshRenameSessionWithOptionsChanged(SymbolRenameOptions newOptions)
    {
        if (Options == newOptions)
        {
            return;
        }

        _threadingContext.ThrowIfNotOnUIThread();
        VerifyNotDismissed();

        Options = newOptions;
        UpdateReferenceLocationsTask();
    }

    public void SetPreviewChanges(bool value)
    {
        _threadingContext.ThrowIfNotOnUIThread();
        VerifyNotDismissed();

        PreviewChanges = value;
    }

    private void VerifyNotDismissed()
    {
        if (_dismissed)
        {
            throw new InvalidOperationException(EditorFeaturesResources.This_session_has_already_been_dismissed);
        }
    }

    private void OnWorkspaceChanged(WorkspaceChangeEventArgs args)
    {
        if (args.Kind != WorkspaceChangeKind.DocumentChanged)
        {
            // Make sure only call Cancel() when there is real document changes.
            // Sometimes, WorkspaceChangeKind.SolutionChanged might be triggered because of SourceGeneratorVersion get updated.
            // We don't want to cancel rename when there is no changed documents.
            var changedDocuments = args.NewSolution.GetChangedDocuments(args.OldSolution);
            if (changedDocuments.Any())
            {
                Logger.Log(FunctionId.Rename_InlineSession_Cancel_NonDocumentChangedWorkspaceChange, KeyValueLogMessage.Create(static (m, args) =>
                {
                    m["Kind"] = Enum.GetName(typeof(WorkspaceChangeKind), args.Kind);
                }, args));

                Cancel();
            }
        }
    }

    private void RaiseSessionSpansUpdated(ImmutableArray<InlineRenameLocation> locations)
    {
        _threadingContext.ThrowIfNotOnUIThread();
        SetReferenceLocations(locations);

        // It's OK to call SetReferenceLocations with all documents, including unchangeable ones,
        // because they can't be opened, so the _openTextBuffers loop won't matter. In fact, the entire
        // inline rename is oblivious to unchangeable documents, we just need to filter out references
        // in them to avoid displaying them in the UI.
        // https://github.com/dotnet/roslyn/issues/41242
        if (Workspace.IgnoreUnchangeableDocumentsWhenApplyingChanges)
        {
            locations = locations.WhereAsArray(l => l.Document.CanApplyChange());
        }

        ReferenceLocationsChanged?.Invoke(this, locations);
    }

    private void SetReferenceLocations(ImmutableArray<InlineRenameLocation> locations)
    {
        _threadingContext.ThrowIfNotOnUIThread();

        var locationsByDocument = locations.ToLookup(l => l.Document.Id);

        _isApplyingEdit = true;
        foreach (var textBuffer in _openTextBuffers.Keys)
        {
            var documents = textBuffer.AsTextContainer().GetRelatedDocuments();

            if (!documents.Any(static (d, locationsByDocument) => locationsByDocument.Contains(d.Id), locationsByDocument))
            {
                _openTextBuffers[textBuffer].SetReferenceSpans([]);
            }
            else
            {
                var spans = documents.SelectMany(d => locationsByDocument[d.Id]).Select(l => l.TextSpan).Distinct();
                _openTextBuffers[textBuffer].SetReferenceSpans(spans);
            }
        }

        _isApplyingEdit = false;
    }

    /// <summary>
    /// Updates the replacement text for the rename session and propagates it to all live buffers.
    /// </summary>
    internal void ApplyReplacementText(string replacementText, bool propagateEditImmediately, bool updateSelection = true)
    {
        _threadingContext.ThrowIfNotOnUIThread();
        VerifyNotDismissed();
        this.ReplacementText = RenameInfo.GetFinalSymbolName(replacementText);

        var asyncToken = _asyncListener.BeginAsyncOperation(nameof(ApplyReplacementText));

        Action propagateEditAction = delegate
        {
            _threadingContext.ThrowIfNotOnUIThread();

            if (_dismissed)
            {
                asyncToken.Dispose();
                return;
            }

            _isApplyingEdit = true;
            using (Logger.LogBlock(FunctionId.Rename_ApplyReplacementText, replacementText, _cancellationTokenSource.Token))
            {
                foreach (var openBuffer in _openTextBuffers.Values)
                {
                    openBuffer.ApplyReplacementText(updateSelection);
                }
            }

            _isApplyingEdit = false;

            // We already kicked off UpdateConflictResolutionTask below (outside the delegate).
            // Now that we are certain the replacement text has been propagated to all of the
            // open buffers, it is safe to actually apply the replacements it has calculated.
            // See https://devdiv.visualstudio.com/DevDiv/_workitems?_a=edit&id=227513
            QueueApplyReplacements();

            asyncToken.Dispose();
        };

        // Start the conflict resolution task but do not apply the results immediately. The
        // buffer changes performed in propagateEditAction can cause source control modal
        // dialogs to show. Those dialogs pump, and yield the UI thread to whatever work is
        // waiting to be done there, including our ApplyReplacements work. If ApplyReplacements
        // starts running on the UI thread while propagateEditAction is still updating buffers
        // on the UI thread, we crash because we try to enumerate the undo stack while an undo
        // transaction is still in process. Therefore, we defer QueueApplyReplacements until
        // after the buffers have been edited, and any modal dialogs have been completed.
        // In addition to avoiding the crash, this also ensures that the resolved conflict text
        // is applied after the simple text change is propagated.
        // See https://devdiv.visualstudio.com/DevDiv/_workitems?_a=edit&id=227513
        UpdateConflictResolutionTask();

        if (propagateEditImmediately)
        {
            propagateEditAction();
        }
        else
        {
            // When responding to a text edit, we delay propagating the edit until the first transaction completes.
            _threadingContext.JoinableTaskFactory.RunAsync(async () =>
            {
                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true);
                propagateEditAction();
            });
        }
    }

    private void UpdateConflictResolutionTask()
    {
        _threadingContext.ThrowIfNotOnUIThread();

        _conflictResolutionTaskCancellationSource.Cancel();
        _conflictResolutionTaskCancellationSource = new CancellationTokenSource();

        // If the replacement text is empty, we do not update the results of the conflict
        // resolution task. We instead wait for a non-empty identifier.
        if (this.ReplacementText == string.Empty)
        {
            return;
        }

        var replacementText = this.ReplacementText;
        var options = Options;
        var cancellationToken = _conflictResolutionTaskCancellationSource.Token;

        var asyncToken = _asyncListener.BeginAsyncOperation(nameof(UpdateConflictResolutionTask));

        _conflictResolutionTask = _threadingContext.JoinableTaskFactory.RunAsync(async () =>
        {
            // Join prior work before proceeding, since it performs a required state update.
            // https://github.com/dotnet/roslyn/pull/34254#discussion_r267024593
            //
            // If cancellation of the conflict resolution task is requested before the rename locations task
            // completes, we do not need to wait for rename before cancelling. The next conflict resolution task
            // will wait on the latest rename location task if/when necessary.
            var result = await AllRenameLocationsTask.JoinAsync(cancellationToken).ConfigureAwait(false);
            await TaskScheduler.Default;

            return await result.GetReplacementsAsync(replacementText, options, cancellationToken).ConfigureAwait(false);
        });

        _conflictResolutionTask.Task.CompletesAsyncOperation(asyncToken);
    }

    [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "False positive in methods using JTF: https://github.com/dotnet/roslyn-analyzers/issues/4283")]
    private void QueueApplyReplacements()
    {
        // If the replacement text is empty, we do not update the results of the conflict
        // resolution task. We instead wait for a non-empty identifier.
        if (this.ReplacementText == string.Empty)
        {
            return;
        }

        var cancellationToken = _conflictResolutionTaskCancellationSource.Token;
        var asyncToken = _asyncListener.BeginAsyncOperation(nameof(QueueApplyReplacements));
        var replacementOperation = _threadingContext.JoinableTaskFactory.RunAsync(async () =>
        {
            var replacementInfo = await _conflictResolutionTask.JoinAsync(CancellationToken.None).ConfigureAwait(false);
            if (replacementInfo == null || cancellationToken.IsCancellationRequested)
            {
                return;
            }

            // Switch to a background thread for expensive work
            await TaskScheduler.Default;
            var computedMergeResult = await ComputeMergeResultAsync(replacementInfo, cancellationToken).ConfigureAwait(false);
            await ApplyReplacementsAsync(
                computedMergeResult.replacementInfo, computedMergeResult.mergeResult, cancellationToken).ConfigureAwait(true);
        });
        replacementOperation.Task.CompletesAsyncOperation(asyncToken);
    }

    private async Task<(IInlineRenameReplacementInfo replacementInfo, LinkedFileMergeSessionResult mergeResult)> ComputeMergeResultAsync(IInlineRenameReplacementInfo replacementInfo, CancellationToken cancellationToken)
    {
        var diffMergingSession = new LinkedFileDiffMergingSession(_baseSolution, replacementInfo.NewSolution, replacementInfo.NewSolution.GetChanges(_baseSolution));
        var mergeResult = await diffMergingSession.MergeDiffsAsync(cancellationToken).ConfigureAwait(false);
        return (replacementInfo, mergeResult);
    }

    private async Task ApplyReplacementsAsync(
        IInlineRenameReplacementInfo replacementInfo, LinkedFileMergeSessionResult mergeResult, CancellationToken cancellationToken)
    {
        await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

        RaiseReplacementsComputed(replacementInfo);

        _isApplyingEdit = true;
        foreach (var textBuffer in _openTextBuffers.Keys)
        {
            var documents = textBuffer.CurrentSnapshot.GetRelatedDocumentsWithChanges();
            if (documents.Any())
            {
                var textBufferManager = _openTextBuffers[textBuffer];
                await textBufferManager.ApplyConflictResolutionEditsAsync(
                    replacementInfo, mergeResult, documents, cancellationToken).ConfigureAwait(true);
            }
        }

        _isApplyingEdit = false;
    }

    private void RaiseReplacementsComputed(IInlineRenameReplacementInfo resolution)
    {
        _threadingContext.ThrowIfNotOnUIThread();
        ReplacementsComputed?.Invoke(this, resolution);
    }

    private void LogRenameSession(RenameLogMessage.UserActionOutcome outcome, bool previewChanges)
    {
        if (_conflictResolutionTask == null)
        {
            return;
        }

        var conflictResolutionFinishedComputing = _conflictResolutionTask.Task.Status == TaskStatus.RanToCompletion;

        if (conflictResolutionFinishedComputing)
        {
            var result = _conflictResolutionTask.Task.Result;
            var replacementKinds = result.GetAllReplacementKinds().ToList();

            Logger.Log(FunctionId.Rename_InlineSession_Session, RenameLogMessage.Create(
                Options,
                outcome,
                conflictResolutionFinishedComputing,
                previewChanges,
                replacementKinds));
        }
        else
        {
            Debug.Assert(outcome.HasFlag(RenameLogMessage.UserActionOutcome.Canceled));
            Logger.Log(FunctionId.Rename_InlineSession_Session, RenameLogMessage.Create(
                Options,
                outcome,
                conflictResolutionFinishedComputing,
                previewChanges,
                replacementKinds: []));
        }
    }

    public void Cancel()
    {
        _threadingContext.ThrowIfNotOnUIThread();

        DismissUIAndRollbackEditsAndEndRenameSession_MustBeCalledOnUIThread(
            RenameLogMessage.UserActionOutcome.Canceled, previewChanges: false);
    }

    /// <summary>
    /// Dismisses the UI, rolls back any edits, and ends the rename session.
    /// </summary>
    private void DismissUIAndRollbackEditsAndEndRenameSession_MustBeCalledOnUIThread(
        RenameLogMessage.UserActionOutcome outcome,
        bool previewChanges,
        Action finalCommitAction = null)
    {
        // Note: this entire sequence of steps is not cancellable.  We must perform it all to get back to a correct
        // state for all the editors the user is interacting with.
        var cancellationToken = CancellationToken.None;
        _threadingContext.ThrowIfNotOnUIThread();

        if (_dismissed)
            return;

        _dismissed = true;

        // Remove all our adornments and restore all buffer texts to their initial state.
        DismissUIAndRollbackEdits();

        // We're about to perform the final commit action.  No need to do any of our BG work to find-refs or compute conflicts.
        _cancellationTokenSource.Cancel();
        _conflictResolutionTaskCancellationSource.Cancel();

        // Close the keep alive session we have open with OOP, allowing it to release the solution it is holding onto.
        _keepAliveSession.Dispose();

        // Perform the actual commit step if we've been asked to.
        finalCommitAction?.Invoke();

        // Log the result so we know how well rename is going in practice.
        LogRenameSession(outcome, previewChanges);

        // Remove all our rename trackers from the text buffer properties.
        RenameTrackingDismisser.DismissRenameTracking(Workspace, Workspace.GetOpenDocumentIds());

        // Log how long the full rename took.
        _inlineRenameSessionDurationLogBlock.Dispose();

        return;

        void DismissUIAndRollbackEdits()
        {
            _workspaceChangedDisposer.Dispose();

            _textBufferAssociatedViewService.SubjectBuffersConnected -= OnSubjectBuffersConnected;

            // Reenable completion now that the inline rename session is done
            _completionDisabledToken.Dispose();

            foreach (var textBuffer in _openTextBuffers.Keys)
            {
                var document = textBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
                var isClosed = document == null;

                var openBuffer = _openTextBuffers[textBuffer];
                openBuffer.DisconnectAndRollbackEdits(isClosed);
            }

            this.UndoManager.Disconnect();

            if (_triggerView != null && !_triggerView.IsClosed)
            {
                _triggerView.Selection.Clear();
            }

            RenameService.ActiveSession = null;
        }
    }

    /// <remarks>
    /// Caller should pass in the IUIThreadOperationContext if it is called from editor so rename commit operation could set up its own context correctly.
    /// When the editorOperationContext is not null it will take ownership of the UI thread and try to create a BackgroundWorkIndicator
    /// </remarks>
    public async Task CommitAsync(bool previewChanges, IUIThreadOperationContext editorOperationContext)
    {
        await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync();
        VerifyNotDismissed();

        // If the identifier was deleted (or didn't change at all) then cancel the operation.
        // Note: an alternative approach would be for the work we're doing (like detecting
        // conflicts) to quickly bail in the case of no change.  However, that involves deeper
        // changes to the system and is less easy to validate that nothing happens.
        //
        // The only potential downside here would be if there was a language that wanted to
        // still 'rename' even if the identifier went away (or was unchanged).  But that isn't
        // a case we're aware of, so it's fine to be opinionated here that we can quickly bail
        // in these cases.
        if (this.ReplacementText == string.Empty ||
            this.ReplacementText == _initialRenameText)
        {
            Cancel();
            return;
        }

        // Don't dup commit.
        if (this.IsCommitInProgress)
        {
            return;
        }

        previewChanges = previewChanges || PreviewChanges;

        // Prevent Editor's typing responsiveness auto canceling the rename operation.
        // InlineRenameSession will call IUIThreadOperationExecutor to sets up our own IUIThreadOperationContext
        editorOperationContext?.TakeOwnership();

        try
        {
            // Notify the UI commit starts.
            this.IsCommitInProgress = true;
            this.CommitStateChange?.Invoke(this, EventArgs.Empty);

            // We do not cancel on edit because as part of the rename system we have asynchronous work still
            // occurring that itself may be asynchronously editing the buffer (for example, updating reference
            // locations with the final renamed text).  Ideally though, once we start comitting, we would cancel
            // any of that work and then only have the work of rolling back to the original state of the world
            // and applying the desired edits ourselves.
            var factory = Workspace.Services.GetRequiredService<IBackgroundWorkIndicatorFactory>();
            using var context = factory.Create(
                _triggerView, TriggerSpan, EditorFeaturesResources.Computing_Rename_information,
                cancelOnEdit: false, cancelOnFocusLost: false);

            // .ConfigureAwait(true); so we can return to the UI thread to dispose the operation context.  It
            // has a non-JTF threading dependency on the main thread.  So it can deadlock if you call it on a BG
            // thread when in a blocking JTF call.
            await CommitCoreAsync(context, previewChanges).ConfigureAwait(true);
        }
        catch (OperationCanceledException)
        {
            // We've used CA(true) consistently in this method.  So we should always be on the UI thread.
            DismissUIAndRollbackEditsAndEndRenameSession_MustBeCalledOnUIThread(
                RenameLogMessage.UserActionOutcome.Canceled | RenameLogMessage.UserActionOutcome.Committed, previewChanges);
            return;
        }
        finally
        {
            this.IsCommitInProgress = false;
            this.CommitStateChange?.Invoke(this, EventArgs.Empty);
        }

        return;
    }

    private async Task CommitCoreAsync(IUIThreadOperationContext operationContext, bool previewChanges)
    {
        var cancellationToken = operationContext.UserCancellationToken;
        var eventName = previewChanges ? FunctionId.Rename_CommitCoreWithPreview : FunctionId.Rename_CommitCore;
        using (Logger.LogBlock(eventName, KeyValueLogMessage.Create(LogType.UserAction), cancellationToken))
        {
            var info = await _conflictResolutionTask.JoinAsync(cancellationToken).ConfigureAwait(true);
            var newSolution = info.NewSolution;

            if (previewChanges)
            {
                var previewService = Workspace.Services.GetService<IPreviewDialogService>();

                // The preview service needs to be called from the UI thread, since it's doing COM calls underneath.
                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
                newSolution = previewService.PreviewChanges(
                    string.Format(EditorFeaturesResources.Preview_Changes_0, EditorFeaturesResources.Rename),
                    "vs.csharp.refactoring.rename",
                    string.Format(EditorFeaturesResources.Rename_0_to_1_colon, this.OriginalSymbolName, this.ReplacementText),
                    RenameInfo.FullDisplayName,
                    RenameInfo.Glyph,
                    newSolution,
                    TriggerDocument.Project.Solution);

                if (newSolution == null)
                {
                    // User clicked cancel.
                    return;
                }
            }

            // The user hasn't canceled by now, so we're done waiting for them. Off to rename!
            using var _1 = operationContext.AddScope(allowCancellation: true, EditorFeaturesResources.Updating_files);

            await TaskScheduler.Default;

            // While on the background, attempt to figure out the actual changes to make to the workspace's current solution.
            var documentChanges = await CalculateFinalDocumentChangesAsync(newSolution, cancellationToken).ConfigureAwait(false);

            // Now jump back to the UI thread to actually apply those changes.
            await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

            // We're about to make irrevocable changes to the workspace and UI.  We're no longer cancellable at this point.
            using var _2 = operationContext.AddScope(allowCancellation: false, EditorFeaturesResources.Updating_files);
            cancellationToken = CancellationToken.None;

            // Dismiss the rename UI and rollback any linked edits made.
            DismissUIAndRollbackEditsAndEndRenameSession_MustBeCalledOnUIThread(
                RenameLogMessage.UserActionOutcome.Committed, previewChanges,
                () =>
                {
                    // Now try to apply that change we computed to the workspace.
                    var error = TryApplyRename(documentChanges);
                    if (error is not null)
                    {
                        var notificationService = Workspace.Services.GetService<INotificationService>();
                        notificationService.SendNotification(
                            error.Value.message, EditorFeaturesResources.Rename_Symbol, error.Value.severity);
                    }
                });
        }

        async Task<ImmutableArray<(DocumentId documentId, string newName, SyntaxNode newRoot, SourceText newText)>> CalculateFinalDocumentChangesAsync(
            Solution newSolution, CancellationToken cancellationToken)
        {
            var changes = newSolution.GetChanges(_baseSolution);
            var changedDocumentIDs = changes.GetProjectChanges().SelectManyAsArray(c => c.GetChangedDocuments());

            using var _ = PooledObjects.ArrayBuilder<(DocumentId documentId, string newName, SyntaxNode newRoot, SourceText newText)>.GetInstance(out var result);

            foreach (var documentId in changes.GetProjectChanges().SelectMany(c => c.GetChangedDocuments()))
            {
                // If the document supports syntax tree, then create the new solution from the updated syntax root.
                // This should ensure that annotations are preserved, and prevents the solution from having to reparse
                // documents when we already have the trees for them.  If we don't support syntax, then just use the
                // text of the document.
                var newDocument = newSolution.GetDocument(documentId);

                result.Add(newDocument.SupportsSyntaxTree
                    ? (documentId, newDocument.Name, await newDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false), newText: null)
                    : (documentId, newDocument.Name, newRoot: null, await newDocument.GetTextAsync(cancellationToken).ConfigureAwait(false)));
            }

            foreach (var documentId in changes.GetExplicitlyChangedSourceGeneratedDocuments())
            {
                var newDocument = newSolution.GetRequiredSourceGeneratedDocumentForAlreadyGeneratedId(documentId);
                result.Add((documentId, newDocument.Name, newRoot: null, await newDocument.GetTextAsync(cancellationToken).ConfigureAwait(false)));
            }

            return result.ToImmutableAndClear();
        }
    }

    // Returns non-null error message if renaming fails.
    private (NotificationSeverity severity, string message)? TryApplyRename(
        ImmutableArray<(DocumentId documentId, string newName, SyntaxNode newRoot, SourceText newText)> documentChanges)
    {
        _threadingContext.ThrowIfNotOnUIThread();

        using var undoTransaction = Workspace.OpenGlobalUndoTransaction(EditorFeaturesResources.Inline_Rename);

        if (!RenameInfo.TryOnBeforeGlobalSymbolRenamed(Workspace, documentChanges.SelectAsArray(t => t.documentId), this.ReplacementText))
            return (NotificationSeverity.Error, EditorFeaturesResources.Rename_operation_was_cancelled_or_is_not_valid);

        var finalSolution = _threadingContext.JoinableTaskFactory.Run(() => GetFinalSolutionAsync(documentChanges));

        // Now actually go and apply the changes to the workspace.  We expect this to succeed as we're on the UI
        // thread, and nothing else should have been able to make a change to workspace since we we grabbed its
        // current solution and forked it.
        if (!Workspace.TryApplyChanges(finalSolution))
            return (NotificationSeverity.Error, EditorFeaturesResources.Rename_operation_could_not_complete_due_to_external_change_to_workspace);

        try
        {
            // Since rename can apply file changes as well, and those file
            // changes can generate new document ids, include added documents
            // as well as changed documents. This also ensures that any document
            // that was removed is not included
            var finalChanges = Workspace.CurrentSolution.GetChanges(_baseSolution);

            var finalChangedIds = finalChanges
                .GetProjectChanges()
                .SelectManyAsArray(c => c.GetChangedDocuments().Concat(c.GetAddedDocuments()));

            if (!RenameInfo.TryOnAfterGlobalSymbolRenamed(Workspace, finalChangedIds, this.ReplacementText))
                return (NotificationSeverity.Information, EditorFeaturesResources.Rename_operation_was_not_properly_completed_Some_file_might_not_have_been_updated);

            return null;
        }
        finally
        {
            // If we successfully updated the workspace then make sure the undo transaction is committed and is
            // always able to undo anything any other external listener did.
            undoTransaction.Commit();
        }
    }

    private async Task<Solution> GetFinalSolutionAsync(ImmutableArray<(DocumentId, string, SyntaxNode, SourceText)> documentChanges)
    {
        // Grab the workspace's current solution, and make the document changes to it we computed.
        var finalSolution = Workspace.CurrentSolution;
        foreach (var (documentId, newName, newRoot, newText) in documentChanges)
        {
            if (documentId.IsSourceGenerated)
            {
                var document = await finalSolution.GetDocumentAsync(documentId, includeSourceGenerated: true, CancellationToken.None).ConfigureAwait(true);
                Contract.ThrowIfFalse(document.IsRazorSourceGeneratedDocument());
            }

            finalSolution = newRoot != null
                ? finalSolution.WithDocumentSyntaxRoot(documentId, newRoot)
                : finalSolution.WithDocumentText(documentId, newText);

            // WithDocumentName doesn't support source generated documents, and there should be no circumstance were they'd
            // be renamed anyway. Given rename only supports Razor generated documents, and those are always named for the Razor
            // file they come from, and nothing in Roslyn knows to rename those documents, we can safely skip this step. Razor
            // may process the results of this rename and rename the document if it needs to later.
            if (!documentId.IsSourceGenerated)
            {
                finalSolution = finalSolution.WithDocumentName(documentId, newName);
            }
        }

        return finalSolution;
    }

    internal bool TryGetContainingEditableSpan(SnapshotPoint point, out SnapshotSpan editableSpan)
    {
        editableSpan = default;
        if (!_openTextBuffers.TryGetValue(point.Snapshot.TextBuffer, out var bufferManager))
        {
            return false;
        }

        foreach (var span in bufferManager.GetEditableSpansForSnapshot(point.Snapshot))
        {
            if (span.Contains(point) || span.End == point)
            {
                editableSpan = span;
                return true;
            }
        }

        return false;
    }

    internal bool IsInOpenTextBuffer(SnapshotPoint point)
        => _openTextBuffers.ContainsKey(point.Snapshot.TextBuffer);
}
