// // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. // // This file contain implementations details that are subject to change without notice. // Use at your own risk. // using System; using System.Collections.Generic; using System.Globalization; using Microsoft.VisualStudio.Utilities; namespace Microsoft.VisualStudio.Text.Operations.Standalone { internal class UndoHistoryImpl : ITextUndoHistory2 { public event EventHandler UndoRedoHappened; public event EventHandler UndoTransactionCompleted; #region Private Fields private UndoTransactionImpl currentTransaction; private Stack undoStack; private Stack redoStack; private DelegatedUndoPrimitiveImpl activeUndoOperationPrimitive; internal TextUndoHistoryState state; private PropertyCollection properties; #endregion internal UndoHistoryRegistryImpl UndoHistoryRegistry; public UndoHistoryImpl(UndoHistoryRegistryImpl undoHistoryRegistry) { this.currentTransaction = null; this.UndoHistoryRegistry = undoHistoryRegistry; this.undoStack = new Stack(); this.redoStack = new Stack(); this.activeUndoOperationPrimitive = null; this.state = TextUndoHistoryState.Idle; } /// /// The full undo stack for this history. Does not include any currently opened or redo transactions. /// public IEnumerable UndoStack { get { return this.undoStack; } } /// /// The full redo stack for this history. Does not include any currently opened or undo transactions. /// public IEnumerable RedoStack { get { return this.redoStack; } } /// /// It returns most recently pushed (topmost) item of the or if the stack is /// empty it returns null. /// public ITextUndoTransaction LastUndoTransaction { get { if (this.undoStack.Count != 0) { return this.undoStack.Peek(); } return null; } } /// /// It returns most recently pushed (topmost) item of the or if the stack is /// empty it returns null. /// public ITextUndoTransaction LastRedoTransaction { get { if (this.redoStack.Count != 0) { return this.redoStack.Peek(); } return null; } } /// /// Whether a single undo is permissible (corresponds to the most recent visible undo UndoTransaction's CanUndo). /// /// /// If there are hidden transactions on top of the visible transaction, this property returns true only they are /// undoable as well. /// public bool CanUndo { get { if (this.undoStack.Count > 0) { return this.undoStack.Peek().CanUndo; } else { return false; } } } /// /// Whether a single redo is permissible (corresponds to the most recent visible redo UndoTransaction's CanRedo). /// /// /// If there are hidden transactions on top of the visible transaction, this property returns true only they are /// redoable as well. /// public bool CanRedo { get { if (this.redoStack.Count > 0) { return this.redoStack.Peek().CanRedo; } else { return false; } } } /// /// The most recent visible undo UndoTransactions's Description. /// public string UndoDescription { get { if (this.undoStack.Count > 0) { return this.undoStack.Peek().Description; } else { return "Strings.HistoryCantUndo"; } } } /// /// The most recent visible redo UndoTransaction's Description. /// public string RedoDescription { get { if (this.undoStack.Count > 0) { return this.redoStack.Peek().Description; } else { return "Strings.HistoryCantRedo"; } } } /// /// The current UndoTransaction in progress. /// public ITextUndoTransaction CurrentTransaction { get { return this.currentTransaction; } } /// /// /// public TextUndoHistoryState State { get { return this.state; } } public ITextUndoTransaction CreateInvisibleTransaction(string description) { // Standalone undo doesn't support invisible transactions so simply return // a normal transaction. return this.CreateTransaction(description); } /// /// Creates a new transaction, nests it in the previously current transaction, and marks it current. /// If there is a redo stack, it gets cleared. /// UNDONE: should the redo-clearing happen now or when the new transaction is committed? /// /// A string description for the transaction. /// The new transaction. /// public ITextUndoTransaction CreateTransaction(string description) { if (string.IsNullOrEmpty(description)) { throw new ArgumentNullException(nameof(description)); } // If there is a pending transaction that has already been completed, we should not be permitted // to open a new transaction, since it cannot later be added to its parent. if ((this.currentTransaction != null) && (this.currentTransaction.State != UndoTransactionState.Open)) { throw new InvalidOperationException("Strings.CannotCreateTransactionWhenCurrentTransactionNotOpen"); } // new transactions that are visible should clear the redo stack. if (this.currentTransaction == null) { foreach (UndoTransactionImpl redoTransaction in this.redoStack) { redoTransaction.Invalidate(); } this.redoStack.Clear(); } UndoTransactionImpl newTransaction = new UndoTransactionImpl(this, this.currentTransaction, description); this.currentTransaction = newTransaction; return this.currentTransaction; } /// /// Performs requested amount of undo operation and places the transactions on the redo stack. /// UNDONE: What if there is a currently opened transaction? /// /// The number of undo operations to perform. At the end of the operation, requested number of visible /// transactions are undone. Hence actual number of transactions undone might be more than this number if there are some /// hidden transactions adjacent to (on top of or at the bottom of) the visible ones. /// /// /// After the last visible transaction is undone, hidden transactions left on top the stack are undone as well until a /// visible or linked transaction is encountered or stack is emptied totally. /// public void Undo(int count) { if (count <= 0) { throw new ArgumentOutOfRangeException(nameof(count)); } if (!IsThereEnoughVisibleTransactions(this.undoStack, count)) { throw new InvalidOperationException("Cannot undo more transactions than exist"); } TextUndoHistoryState originalState = this.state; this.state = TextUndoHistoryState.Undoing; using (new AutoEnclose(delegate { this.state = originalState; })) { while (count > 0) { if (!this.undoStack.Peek().CanUndo) { throw new InvalidOperationException("Strings.CannotUndoRequestedPrimitiveFromHistoryUndo"); } ITextUndoTransaction ut = this.undoStack.Pop(); ut.Undo(); this.redoStack.Push(ut); RaiseUndoRedoHappened(this.state, ut); --count; } } } /// /// Performs an undo operation and places the primitives on the redo stack, up until (and /// including) the transaction indicated. This is called by the linked undo transaction that /// is aware of the linking relationship between transactions, and it does not call back into /// the transactions' public Undo(). /// /// public void UndoInIsolation(UndoTransactionImpl transaction) { TextUndoHistoryState originalState = this.state; this.state = TextUndoHistoryState.Undoing; using (new AutoEnclose(delegate { this.state = originalState; })) { if (this.undoStack.Contains(transaction)) { UndoTransactionImpl undone = null; while (undone != transaction) { UndoTransactionImpl ut = this.undoStack.Pop() as UndoTransactionImpl; ut.Undo(); this.redoStack.Push(ut); RaiseUndoRedoHappened(this.state, ut); undone = ut; } } } } /// /// Performs requested amount of redo operation and places the transactions on the undo stack. /// UNDONE: What if there is a currently opened transaction? /// /// The number of redo operations to perform. At the end of the operation, requested number of visible /// transactions are redone. Hence actual number of transactions redone might be more than this number if there are some /// hidden transactions adjacent to (on top of or at the bottom of) the visible ones. /// /// /// After the last visible transaction is redone, hidden transactions left on top the stack are redone as well until a /// visible or linked transaction is encountered or stack is emptied totally. /// public void Redo(int count) { if (count <= 0) { throw new ArgumentOutOfRangeException(nameof(count)); } if (!IsThereEnoughVisibleTransactions(this.redoStack, count)) { throw new InvalidOperationException("Cannot redo more transactions than exist"); } TextUndoHistoryState originalState = this.state; this.state = TextUndoHistoryState.Redoing; using (new AutoEnclose(delegate { this.state = originalState; })) { while (count > 0) { if (!this.redoStack.Peek().CanRedo) { throw new InvalidOperationException("Strings.CannotRedoRequestedPrimitiveFromHistoryRedo"); } ITextUndoTransaction ut = this.redoStack.Pop(); ut.Do(); this.undoStack.Push(ut); RaiseUndoRedoHappened(this.state, ut); --count; } } } /// /// Performs a redo operation and places the primitives on the redo stack, up until (and /// including) the transaction indicated. This is called by the linked undo transaction that /// is aware of the linking relationship between transactions, and it does not call back into /// the transactions' public Redo(). /// /// public void RedoInIsolation(UndoTransactionImpl transaction) { TextUndoHistoryState originalState = this.state; this.state = TextUndoHistoryState.Redoing; using (new AutoEnclose(delegate { this.state = originalState; })) { if (this.redoStack.Contains(transaction)) { UndoTransactionImpl redone = null; while (redone != transaction) { UndoTransactionImpl ut = this.redoStack.Pop() as UndoTransactionImpl; ut.Do(); this.undoStack.Push(ut); RaiseUndoRedoHappened(this.state, ut); redone = ut; } } } } /// /// This method is called from the DelegatedUndoPrimitive just as it starts a do or undo, so that this /// history knows to forward any new UndoableOperations to the primitive. This and its pair EndForward... only manage /// the state of the activeUndoOperationPrimitive. /// /// The delegated primitive to be marked active public void ForwardToUndoOperation(DelegatedUndoPrimitiveImpl primitive) { if (this.activeUndoOperationPrimitive != null) { throw new InvalidOperationException(); } this.activeUndoOperationPrimitive = primitive; } /// /// This method ends the lifetime of the activeUndoOperationPrimitive and should be called after ForwardToUndoOperation. /// /// The previously active delegated primitive--used for sanity check. public void EndForwardToUndoOperation(DelegatedUndoPrimitiveImpl primitive) { if (this.activeUndoOperationPrimitive != primitive) { throw new InvalidOperationException(); } this.activeUndoOperationPrimitive = null; } /// /// This is how the transactions alert their containing history that they have finished /// (likely from the Dispose() method). /// /// This is the transaction that's finishing. It should match the history's current transaction. /// If it does not match, then the current transaction will be discarded and an exception will be thrown. public void EndTransaction(ITextUndoTransaction transaction) { if (this.currentTransaction != transaction) { this.currentTransaction = null; throw new InvalidOperationException("Strings.EndTransactionOutOfOrder"); } // Note that the VS undo history actually "pops" the nested undo stack on the Complete/Cancel // (instead of in the Dispose). This shouldn't affect anything but we should consider adapting // this code to follow the model in VS undo. this.currentTransaction = (UndoTransactionImpl)(transaction.Parent); // only add completed transactions to their parents (or the stack) if (transaction.State == UndoTransactionState.Completed) { if (transaction.Parent == null) // stack bottomed out! { MergeOrPushToUndoStack((UndoTransactionImpl)transaction); } } } /// /// This does two different things, depending on the MergeUndoTransactionPolicys in question. /// It either simply pushes the current transaction to the undo stack, OR it merges it with /// the most recent item in the stack. /// private void MergeOrPushToUndoStack(UndoTransactionImpl transaction) { ITextUndoTransaction transactionAdded; TextUndoTransactionCompletionResult transactionResult; UndoTransactionImpl utPrevious = this.undoStack.Count > 0 ? this.undoStack.Peek() as UndoTransactionImpl : null; if (utPrevious != null && ProceedWithMerge(transaction, utPrevious)) { // Temporarily make utPrevious non-read-only, during merge. utPrevious.IsReadOnly = false; try { transaction.MergePolicy.PerformTransactionMerge(utPrevious, transaction); } finally { utPrevious.IsReadOnly = true; } // utPrevious is already on the undo stack, so we don't need to add it; but report // it as the added transaction in the UndoTransactionCompleted event. transactionAdded = utPrevious; transactionResult = TextUndoTransactionCompletionResult.TransactionMerged; } else { this.undoStack.Push(transaction); transactionAdded = transaction; transactionResult = TextUndoTransactionCompletionResult.TransactionAdded; } RaiseUndoTransactionCompleted(transactionAdded, transactionResult); } public bool ValidTransactionForMarkers(ITextUndoTransaction transaction) { return transaction == null // you can put a marker on the null transaction || this.currentTransaction == transaction // you can put a marker on the currently active transaction || (transaction.History == this && !(transaction.State == UndoTransactionState.Invalid)); // and you can put a marker on any transaction in this history. } public static bool IsThereEnoughVisibleTransactions(Stack stack, int visibleCount) { if (visibleCount <= 0) { return true; } foreach (ITextUndoTransaction transaction in stack) { visibleCount--; if (visibleCount <= 0) { return true; } } return false; } private bool ProceedWithMerge(UndoTransactionImpl transaction1, UndoTransactionImpl transaction2) { UndoHistoryRegistryImpl registry = UndoHistoryRegistry; return transaction1.MergePolicy != null && transaction2.MergePolicy != null && transaction1.MergePolicy.TestCompatiblePolicy(transaction2.MergePolicy) && transaction1.MergePolicy.CanMerge(transaction1, transaction2); } private void RaiseUndoRedoHappened(TextUndoHistoryState state, ITextUndoTransaction transaction) { EventHandler undoRedoHappened = UndoRedoHappened; if (undoRedoHappened != null) { undoRedoHappened(this, new TextUndoRedoEventArgs(state, transaction)); } } private void RaiseUndoTransactionCompleted(ITextUndoTransaction transaction, TextUndoTransactionCompletionResult result) { EventHandler undoTransactionAdded = UndoTransactionCompleted; if (undoTransactionAdded != null) { undoTransactionAdded(this, new TextUndoTransactionCompletedEventArgs(transaction, result)); } } public PropertyCollection Properties { get { if (this.properties == null) { this.properties = new PropertyCollection(); } return this.properties; } } } }