File: RenameTracking\RenameTrackingTaggerProvider.RenameTrackingCommitter.cs
Web Access
Project: src\src\EditorFeatures\Core\Microsoft.CodeAnalysis.EditorFeatures.csproj (Microsoft.CodeAnalysis.EditorFeatures)
// 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.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Editor.Undo;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Notification;
using Microsoft.CodeAnalysis.Rename;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Operations;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Editor.Implementation.RenameTracking;
 
internal sealed partial class RenameTrackingTaggerProvider
{
    private class RenameTrackingCommitter
    {
        private readonly StateMachine _stateMachine;
        private readonly SnapshotSpan _snapshotSpan;
        private readonly IEnumerable<IRefactorNotifyService> _refactorNotifyServices;
        private readonly ITextUndoHistoryRegistry _undoHistoryRegistry;
        private readonly string _displayText;
        private readonly AsyncLazy<RenameTrackingSolutionSet> _renameSymbolResultGetter;
 
        public RenameTrackingCommitter(
            StateMachine stateMachine,
            SnapshotSpan snapshotSpan,
            IEnumerable<IRefactorNotifyService> refactorNotifyServices,
            ITextUndoHistoryRegistry undoHistoryRegistry,
            string displayText)
        {
            _stateMachine = stateMachine;
            _snapshotSpan = snapshotSpan;
            _refactorNotifyServices = refactorNotifyServices;
            _undoHistoryRegistry = undoHistoryRegistry;
            _displayText = displayText;
            _renameSymbolResultGetter = AsyncLazy.Create(
                static (self, c) => self.RenameSymbolWorkerAsync(c),
                arg: this);
        }
 
        /// <summary>
        /// Returns non-null error message if renaming fails.
        /// </summary>
        public async Task<(NotificationSeverity severity, string message)?> TryCommitAsync(CancellationToken cancellationToken)
        {
            _stateMachine.ThreadingContext.ThrowIfNotOnUIThread();
 
            try
            {
                return await TryApplyChangesToWorkspaceAsync(cancellationToken).ConfigureAwait(false);
            }
            finally
            {
                // Clear the state machine so that future updates to the same token work, and any text changes
                // caused by this update are not interpreted as potential renames.  Intentionally pass
                // CancellationToken.None.  We must clear this state out.
                await _stateMachine.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(CancellationToken.None);
                _stateMachine.ClearTrackingSession();
            }
        }
 
        public async Task<RenameTrackingSolutionSet> RenameSymbolAsync(CancellationToken cancellationToken)
            => await _renameSymbolResultGetter.GetValueAsync(cancellationToken).ConfigureAwait(false);
 
        private async Task<RenameTrackingSolutionSet> RenameSymbolWorkerAsync(CancellationToken cancellationToken)
        {
            var document = _snapshotSpan.Snapshot.GetOpenDocumentInCurrentContextWithChanges();
            var newName = _snapshotSpan.GetText();
 
            Contract.ThrowIfNull(document, "Invoked rename tracking smart tag but cannot find the document for the snapshot span.");
 
            // Get copy of solution with the original name in the place of the renamed name
            var solutionWithOriginalName = await CreateSolutionWithOriginalNameAsync(
                document, cancellationToken).ConfigureAwait(false);
 
            var symbol = await TryGetSymbolAsync(solutionWithOriginalName, document.Id, cancellationToken).ConfigureAwait(false);
            Contract.ThrowIfNull(symbol, "Invoked rename tracking smart tag but cannot find the symbol.");
 
            var options = new SymbolRenameOptions(RenameOverloads: _stateMachine.TrackingSession.ForceRenameOverloads);
            var renamedSolution = await Renamer.RenameSymbolAsync(solutionWithOriginalName, symbol, options, newName, cancellationToken).ConfigureAwait(false);
            return new RenameTrackingSolutionSet(symbol, solutionWithOriginalName, renamedSolution);
        }
 
        /// <summary>
        /// Returns non-null error message if renaming fails.
        /// </summary>
        private async Task<(NotificationSeverity, string)?> TryApplyChangesToWorkspaceAsync(CancellationToken cancellationToken)
        {
            // Now that the necessary work has been done to create the intermediate and final
            // solutions during PreparePreview, check one more time for cancellation before making all of the
            // workspace changes.
            cancellationToken.ThrowIfCancellationRequested();
 
            // Undo must backtrack to the state with the original identifier before the state
            // with the user-edited identifier. For example,
            // 
            //   1. Original:                           void M() { M(); }
            //   2. User types:                         void Method() { M(); }
            //   3. Invoke rename:                      void Method() { Method(); }
            // 
            // The undo process should be as follows
            //   1. Back to original name everywhere:   void M() { M(); }       // No tracking session
            //   2. Back to state 2 above:              void Method() { M(); }  // Resume tracking session
            //   3. Finally, start undoing typing:      void M() { M(); }
            //
            // As far as the user can see, undo state 1 never actually existed so we must insert
            // a state here to facilitate the undo. Do the work to obtain the intermediate and
            // final solution without updating the workspace, and then finally disallow
            // cancellation and update the workspace twice.
 
            var renameTrackingSolutionSet = await RenameSymbolAsync(cancellationToken).ConfigureAwait(false);
 
            var document = _snapshotSpan.Snapshot.GetOpenDocumentInCurrentContextWithChanges();
            var newName = _snapshotSpan.GetText();
 
            var workspace = document.Project.Solution.Workspace;
 
            // Since the state machine is only watching buffer changes, it will interpret the 
            // text changes caused by undo and redo actions as potential renames, so carefully
            // update the state machine after undo/redo actions. 
 
            var changedDocuments = renameTrackingSolutionSet.RenamedSolution.GetChangedDocuments(renameTrackingSolutionSet.OriginalSolution);
            try
            {
                // When this action is undone (the user has undone twice), restore the state
                // machine to so that they can continue their original rename tracking session.
 
                await _stateMachine.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
                var trackingSessionId = _stateMachine.StoreCurrentTrackingSessionAndGenerateId();
                var result = TryUpdateWorkspaceForResetOfTypedIdentifier(workspace, renameTrackingSolutionSet.OriginalSolution, trackingSessionId);
                if (result is not null)
                    return result;
 
                // Now that the solution is back in its original state, notify third parties about
                // the coming rename operation.
                if (!_refactorNotifyServices.TryOnBeforeGlobalSymbolRenamed(workspace, changedDocuments, renameTrackingSolutionSet.Symbol, newName, throwOnFailure: false))
                    return (NotificationSeverity.Error, EditorFeaturesResources.Rename_operation_was_cancelled_or_is_not_valid);
 
                // move all changes to final solution based on the workspace's current solution, since the current solution
                // got updated when we reset it above.
                var finalSolution = workspace.CurrentSolution;
                foreach (var docId in changedDocuments)
                {
                    // because changes have already been made to the workspace (UpdateWorkspaceForResetOfTypedIdentifier() above),
                    // these calls can't be cancelled and must be allowed to complete.
                    var root = await renameTrackingSolutionSet.RenamedSolution.GetDocument(docId).GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
                    finalSolution = finalSolution.WithDocumentSyntaxRoot(docId, root);
                }
 
                // Undo/redo on this action must always clear the state machine
                await _stateMachine.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
                return TryUpdateWorkspaceForGlobalIdentifierRename(
                    workspace,
                    finalSolution,
                    _displayText,
                    changedDocuments,
                    renameTrackingSolutionSet.Symbol,
                    newName,
                    trackingSessionId);
            }
            finally
            {
                // Explicit CancellationToken.None here.  We must clean up our state no matter what.
                await _stateMachine.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(CancellationToken.None);
                RenameTrackingDismisser.DismissRenameTracking(workspace, changedDocuments);
            }
        }
 
        private async Task<Solution> CreateSolutionWithOriginalNameAsync(
            Document document, CancellationToken cancellationToken)
        {
            var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
            var fullText = syntaxTree.GetText(cancellationToken);
            var textChange = new TextChange(new TextSpan(_snapshotSpan.Start, _snapshotSpan.Length), _stateMachine.TrackingSession.OriginalName);
 
            var newFullText = fullText.WithChanges(textChange);
#if DEBUG
            var syntaxTreeWithOriginalName = syntaxTree.WithChangedText(newFullText);
            var documentWithOriginalName = document.WithSyntaxRoot(syntaxTreeWithOriginalName.GetRoot(cancellationToken));
 
            Debug.Assert(newFullText.ToString() == documentWithOriginalName.GetTextSynchronously(cancellationToken).ToString());
#endif
 
            // Apply the original name to all linked documents to construct a consistent solution
            var solution = document.Project.Solution;
            var finalSolution = solution.WithDocumentTexts(
                document.GetLinkedDocumentIds().Add(document.Id).SelectAsArray(id => (id, newFullText)));
 
            return finalSolution;
        }
 
        private async Task<ISymbol> TryGetSymbolAsync(Solution solutionWithOriginalName, DocumentId documentId, CancellationToken cancellationToken)
        {
            var documentWithOriginalName = solutionWithOriginalName.GetDocument(documentId);
            var syntaxTreeWithOriginalName = await documentWithOriginalName.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
 
            var syntaxFacts = documentWithOriginalName.GetLanguageService<ISyntaxFactsService>();
            var semanticFacts = documentWithOriginalName.GetLanguageService<ISemanticFactsService>();
            var semanticModel = await documentWithOriginalName.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
 
            var token = await syntaxTreeWithOriginalName.GetTouchingWordAsync(_snapshotSpan.Start, syntaxFacts, cancellationToken).ConfigureAwait(false);
            var tokenRenameInfo = RenameUtilities.GetTokenRenameInfo(semanticFacts, semanticModel, token, cancellationToken);
 
            return tokenRenameInfo.HasSymbols ? tokenRenameInfo.Symbols.First() : null;
        }
 
        /// <summary>
        /// Returns non-null error message if renaming fails.
        /// </summary>
        private (NotificationSeverity, string)? TryUpdateWorkspaceForResetOfTypedIdentifier(Workspace workspace, Solution newSolution, int trackingSessionId)
        {
            _stateMachine.ThreadingContext.ThrowIfNotOnUIThread();
 
            // Update document in an ITextUndoTransaction with custom behaviors on undo/redo to
            // deal with the state machine.
 
            var undoHistory = _undoHistoryRegistry.RegisterHistory(_stateMachine.Buffer);
            using var localUndoTransaction = undoHistory.CreateTransaction(EditorFeaturesResources.Text_Buffer_Change);
 
            var undoPrimitiveBefore = new UndoPrimitive(_stateMachine.Buffer, trackingSessionId, shouldRestoreStateOnUndo: true);
            localUndoTransaction.AddUndo(undoPrimitiveBefore);
 
            if (!workspace.TryApplyChanges(newSolution))
                return (NotificationSeverity.Error, EditorFeaturesResources.Rename_operation_could_not_complete_due_to_external_change_to_workspace);
 
            // 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.
 
            // Never resume tracking session on redo
            var undoPrimitiveAfter = new UndoPrimitive(_stateMachine.Buffer, trackingSessionId, shouldRestoreStateOnUndo: false);
            localUndoTransaction.AddUndo(undoPrimitiveAfter);
 
            localUndoTransaction.Complete();
 
            return null;
        }
 
        /// <summary>
        /// Returns non-null error message if renaming fails.
        /// </summary>
        private (NotificationSeverity, string)? TryUpdateWorkspaceForGlobalIdentifierRename(
            Workspace workspace,
            Solution newSolution,
            string undoName,
            IEnumerable<DocumentId> changedDocuments,
            ISymbol symbol,
            string newName,
            int trackingSessionId)
        {
            _stateMachine.ThreadingContext.ThrowIfNotOnUIThread();
 
            // Perform rename in a workspace undo action so that undo will revert all 
            // references. It should also be performed in an ITextUndoTransaction to handle 
 
            var undoHistory = _undoHistoryRegistry.RegisterHistory(_stateMachine.Buffer);
 
            using var workspaceUndoTransaction = workspace.OpenGlobalUndoTransaction(undoName);
            using var localUndoTransaction = undoHistory.CreateTransaction(undoName);
 
            var undoPrimitiveBefore = new UndoPrimitive(_stateMachine.Buffer, trackingSessionId, shouldRestoreStateOnUndo: false);
            localUndoTransaction.AddUndo(undoPrimitiveBefore);
 
            if (!workspace.TryApplyChanges(newSolution))
                return (NotificationSeverity.Error, EditorFeaturesResources.Rename_operation_could_not_complete_due_to_external_change_to_workspace);
 
            try
            {
                if (!_refactorNotifyServices.TryOnAfterGlobalSymbolRenamed(workspace, changedDocuments, symbol, newName, throwOnFailure: false))
                    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.
 
                // Never resume tracking session on redo
                var undoPrimitiveAfter = new UndoPrimitive(_stateMachine.Buffer, trackingSessionId, shouldRestoreStateOnUndo: false);
                localUndoTransaction.AddUndo(undoPrimitiveAfter);
 
                localUndoTransaction.Complete();
                workspaceUndoTransaction.Commit();
            }
        }
    }
}