File: Shared\Utilities\HACK_TextUndoTransactionThatRollsBackProperly.cs
Web Access
Project: ..\..\..\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.
 
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.Text.Operations;
 
namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities
{
    /// <summary>
    /// An implementation of <see cref="ITextUndoTransaction" /> that wraps another
    /// <see cref="ITextUndoTransaction" />. Some undo implementations (notably the VS implementation)
    /// violate the specified contract for Cancel(), which states that cancelling an active transaction
    /// should undo the primitives that we already added. This works around that problem; calling Cancel()
    /// on this forwards the cancellation to the inner transaction, and if it failed to roll back we
    /// do it ourselves.
    /// </summary>
    internal sealed class HACK_TextUndoTransactionThatRollsBackProperly : ITextUndoTransaction
    {
        private readonly ITextUndoTransaction _innerTransaction;
        private readonly RollbackDetectingUndoPrimitive _undoPrimitive = new();
 
        private bool _transactionOpen = true;
 
        public HACK_TextUndoTransactionThatRollsBackProperly(ITextUndoTransaction innerTransaction)
        {
            _innerTransaction = innerTransaction;
        }
 
        public bool CanRedo => _innerTransaction.CanRedo;
 
        public bool CanUndo => _innerTransaction.CanUndo;
 
        public string Description
        {
            get
            {
                return _innerTransaction.Description;
            }
 
            set
            {
                _innerTransaction.Description = value;
            }
        }
 
        public ITextUndoHistory History => _innerTransaction.History;
 
        public IMergeTextUndoTransactionPolicy MergePolicy
        {
            get
            {
                return _innerTransaction.MergePolicy;
            }
 
            set
            {
                _innerTransaction.MergePolicy = value;
            }
        }
 
        public ITextUndoTransaction Parent => _innerTransaction.Parent;
 
        public UndoTransactionState State => _innerTransaction.State;
 
        public IList<ITextUndoPrimitive> UndoPrimitives => _innerTransaction.UndoPrimitives;
 
        public void AddUndo(ITextUndoPrimitive undo)
            => _innerTransaction.AddUndo(undo);
 
        public void Cancel()
        {
            var transactionWasOpen = _transactionOpen;
            _transactionOpen = false;
 
            // First, add an undo primitive so we can detect whether or not undo gets called
            if (transactionWasOpen)
            {
                _innerTransaction.AddUndo(_undoPrimitive);
            }
 
            _innerTransaction.Cancel();
 
            if (transactionWasOpen && !_undoPrimitive.UndoCalled)
            {
                // Undo each of the primitives in reverse order to clean up. This is slimy.
                foreach (var primitive in _innerTransaction.UndoPrimitives.Reverse())
                {
                    primitive.Undo();
                }
            }
        }
 
        public void Complete()
        {
            _transactionOpen = false;
            _innerTransaction.Complete();
        }
 
        public void Dispose()
        {
            if (_transactionOpen)
            {
                // Call our cancel method first to ensure we handle it properly
                Cancel();
            }
 
            _innerTransaction.Dispose();
        }
 
        public void Do()
            => _innerTransaction.Do();
 
        public void Undo()
            => _innerTransaction.Undo();
 
        private class RollbackDetectingUndoPrimitive : ITextUndoPrimitive
        {
            internal bool UndoCalled = false;
 
            public bool CanRedo => true;
 
            public bool CanUndo => true;
 
            public ITextUndoTransaction? Parent { get; set; }
 
            public bool CanMerge(ITextUndoPrimitive older)
                => false;
 
            public void Do()
            {
            }
 
            public ITextUndoPrimitive Merge(ITextUndoPrimitive older)
                => throw new NotSupportedException();
 
            public void Undo()
                => UndoCalled = true;
        }
    }
}