aboutsummaryrefslogtreecommitdiffstats
path: root/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/UndoStack.cs
diff options
context:
space:
mode:
authorRoy Ben Shabat <Roy.mail.net@gmail.com>2019-04-09 01:47:48 +0300
committerRoy Ben Shabat <Roy.mail.net@gmail.com>2019-04-09 01:47:48 +0300
commit080f1697e97e13461ec6df4d31c8924d01257a1b (patch)
treeb1fe0285de7bc9bc52e9e2195e66fe022bf8f5b3 /Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/UndoStack.cs
parent1608e69a417bc5e40a607c3958c4a60f19f66f1a (diff)
downloadTango-080f1697e97e13461ec6df4d31c8924d01257a1b.tar.gz
Tango-080f1697e97e13461ec6df4d31c8924d01257a1b.zip
MERGE
Diffstat (limited to 'Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/UndoStack.cs')
-rw-r--r--Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/UndoStack.cs450
1 files changed, 450 insertions, 0 deletions
diff --git a/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/UndoStack.cs b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/UndoStack.cs
new file mode 100644
index 000000000..f0a759b23
--- /dev/null
+++ b/Software/Visual_Studio/Scripting/Tango.Scripting.Editors/Document/UndoStack.cs
@@ -0,0 +1,450 @@
+// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
+// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics;
+using Tango.Scripting.Editors.Utils;
+
+namespace Tango.Scripting.Editors.Document
+{
+ /// <summary>
+ /// Undo stack implementation.
+ /// </summary>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix")]
+ public sealed class UndoStack : INotifyPropertyChanged
+ {
+ /// undo stack is listening for changes
+ internal const int StateListen = 0;
+ /// undo stack is reverting/repeating a set of changes
+ internal const int StatePlayback = 1;
+ // undo stack is reverting/repeating a set of changes and modifies the document to do this
+ internal const int StatePlaybackModifyDocument = 2;
+ /// state is used for checking that noone but the UndoStack performs changes
+ /// during Undo events
+ internal int state = StateListen;
+
+ Deque<IUndoableOperation> undostack = new Deque<IUndoableOperation>();
+ Deque<IUndoableOperation> redostack = new Deque<IUndoableOperation>();
+ int sizeLimit = int.MaxValue;
+
+ int undoGroupDepth;
+ int actionCountInUndoGroup;
+ int optionalActionCount;
+ object lastGroupDescriptor;
+ bool allowContinue;
+
+ #region IsOriginalFile implementation
+ // implements feature request SD2-784 - File still considered dirty after undoing all changes
+
+ /// <summary>
+ /// Number of times undo must be executed until the original state is reached.
+ /// Negative: number of times redo must be executed until the original state is reached.
+ /// Special case: int.MinValue == original state is unreachable
+ /// </summary>
+ int elementsOnUndoUntilOriginalFile;
+
+ bool isOriginalFile = true;
+
+ /// <summary>
+ /// Gets whether the document is currently in its original state (no modifications).
+ /// </summary>
+ public bool IsOriginalFile {
+ get { return isOriginalFile; }
+ }
+
+ void RecalcIsOriginalFile()
+ {
+ bool newIsOriginalFile = (elementsOnUndoUntilOriginalFile == 0);
+ if (newIsOriginalFile != isOriginalFile) {
+ isOriginalFile = newIsOriginalFile;
+ NotifyPropertyChanged("IsOriginalFile");
+ }
+ }
+
+ /// <summary>
+ /// Marks the current state as original. Discards any previous "original" markers.
+ /// </summary>
+ public void MarkAsOriginalFile()
+ {
+ elementsOnUndoUntilOriginalFile = 0;
+ RecalcIsOriginalFile();
+ }
+
+ /// <summary>
+ /// Discards the current "original" marker.
+ /// </summary>
+ public void DiscardOriginalFileMarker()
+ {
+ elementsOnUndoUntilOriginalFile = int.MinValue;
+ RecalcIsOriginalFile();
+ }
+
+ void FileModified(int newElementsOnUndoStack)
+ {
+ if (elementsOnUndoUntilOriginalFile == int.MinValue)
+ return;
+
+ elementsOnUndoUntilOriginalFile += newElementsOnUndoStack;
+ if (elementsOnUndoUntilOriginalFile > undostack.Count)
+ elementsOnUndoUntilOriginalFile = int.MinValue;
+
+ // don't call RecalcIsOriginalFile(): wait until end of undo group
+ }
+ #endregion
+
+ /// <summary>
+ /// Gets if the undo stack currently accepts changes.
+ /// Is false while an undo action is running.
+ /// </summary>
+ public bool AcceptChanges {
+ get { return state == StateListen; }
+ }
+
+ /// <summary>
+ /// Gets if there are actions on the undo stack.
+ /// Use the PropertyChanged event to listen to changes of this property.
+ /// </summary>
+ public bool CanUndo {
+ get { return undostack.Count > 0; }
+ }
+
+ /// <summary>
+ /// Gets if there are actions on the redo stack.
+ /// Use the PropertyChanged event to listen to changes of this property.
+ /// </summary>
+ public bool CanRedo {
+ get { return redostack.Count > 0; }
+ }
+
+ /// <summary>
+ /// Gets/Sets the limit on the number of items on the undo stack.
+ /// </summary>
+ /// <remarks>The size limit is enforced only on the number of stored top-level undo groups.
+ /// Elements within undo groups do not count towards the size limit.</remarks>
+ public int SizeLimit {
+ get { return sizeLimit; }
+ set {
+ if (value < 0)
+ ThrowUtil.CheckNotNegative(value, "value");
+ if (sizeLimit != value) {
+ sizeLimit = value;
+ NotifyPropertyChanged("SizeLimit");
+ if (undoGroupDepth == 0)
+ EnforceSizeLimit();
+ }
+ }
+ }
+
+ void EnforceSizeLimit()
+ {
+ Debug.Assert(undoGroupDepth == 0);
+ while (undostack.Count > sizeLimit)
+ undostack.PopFront();
+ while (redostack.Count > sizeLimit)
+ redostack.PopFront();
+ }
+
+ /// <summary>
+ /// If an undo group is open, gets the group descriptor of the current top-level
+ /// undo group.
+ /// If no undo group is open, gets the group descriptor from the previous undo group.
+ /// </summary>
+ /// <remarks>The group descriptor can be used to join adjacent undo groups:
+ /// use a group descriptor to mark your changes, and on the second action,
+ /// compare LastGroupDescriptor and use <see cref="StartContinuedUndoGroup"/> if you
+ /// want to join the undo groups.</remarks>
+ public object LastGroupDescriptor {
+ get { return lastGroupDescriptor; }
+ }
+
+ /// <summary>
+ /// Starts grouping changes.
+ /// Maintains a counter so that nested calls are possible.
+ /// </summary>
+ public void StartUndoGroup()
+ {
+ StartUndoGroup(null);
+ }
+
+ /// <summary>
+ /// Starts grouping changes.
+ /// Maintains a counter so that nested calls are possible.
+ /// </summary>
+ /// <param name="groupDescriptor">An object that is stored with the undo group.
+ /// If this is not a top-level undo group, the parameter is ignored.</param>
+ public void StartUndoGroup(object groupDescriptor)
+ {
+ if (undoGroupDepth == 0) {
+ actionCountInUndoGroup = 0;
+ optionalActionCount = 0;
+ lastGroupDescriptor = groupDescriptor;
+ }
+ undoGroupDepth++;
+ //Util.LoggingService.Debug("Open undo group (new depth=" + undoGroupDepth + ")");
+ }
+
+ /// <summary>
+ /// Starts grouping changes, continuing with the previously closed undo group if possible.
+ /// Maintains a counter so that nested calls are possible.
+ /// If the call to StartContinuedUndoGroup is a nested call, it behaves exactly
+ /// as <see cref="StartUndoGroup()"/>, only top-level calls can continue existing undo groups.
+ /// </summary>
+ /// <param name="groupDescriptor">An object that is stored with the undo group.
+ /// If this is not a top-level undo group, the parameter is ignored.</param>
+ public void StartContinuedUndoGroup(object groupDescriptor)
+ {
+ if (undoGroupDepth == 0) {
+ actionCountInUndoGroup = (allowContinue && undostack.Count > 0) ? 1 : 0;
+ optionalActionCount = 0;
+ lastGroupDescriptor = groupDescriptor;
+ }
+ undoGroupDepth++;
+ //Util.LoggingService.Debug("Continue undo group (new depth=" + undoGroupDepth + ")");
+ }
+
+ /// <summary>
+ /// Stops grouping changes.
+ /// </summary>
+ public void EndUndoGroup()
+ {
+ if (undoGroupDepth == 0) return;
+ undoGroupDepth--;
+ //Util.LoggingService.Debug("Close undo group (new depth=" + undoGroupDepth + ")");
+ if (undoGroupDepth == 0) {
+ Debug.Assert(state == StateListen || actionCountInUndoGroup == 0);
+ if (actionCountInUndoGroup == optionalActionCount) {
+ // only optional actions: don't store them
+ for (int i = 0; i < optionalActionCount; i++) {
+ undostack.PopBack();
+ }
+ } else if (actionCountInUndoGroup > 1) {
+ // combine all actions within the group into a single grouped action
+ undostack.PushBack(new UndoOperationGroup(undostack, actionCountInUndoGroup));
+ FileModified(-actionCountInUndoGroup + 1 + optionalActionCount);
+ }
+ //if (state == StateListen) {
+ EnforceSizeLimit();
+ allowContinue = true;
+ RecalcIsOriginalFile(); // can raise event
+ //}
+ }
+ }
+
+ /// <summary>
+ /// Throws an InvalidOperationException if an undo group is current open.
+ /// </summary>
+ void ThrowIfUndoGroupOpen()
+ {
+ try
+ {
+ if (undoGroupDepth != 0)
+ {
+ undoGroupDepth = 0;
+ throw new InvalidOperationException("No undo group should be open at this point");
+ }
+ if (state != StateListen)
+ {
+ throw new InvalidOperationException("This method cannot be called while an undo operation is being performed");
+ }
+ }
+ catch
+ {
+ }
+ }
+
+ List<TextDocument> affectedDocuments;
+
+ internal void RegisterAffectedDocument(TextDocument document)
+ {
+ if (affectedDocuments == null)
+ affectedDocuments = new List<TextDocument>();
+ if (!affectedDocuments.Contains(document)) {
+ affectedDocuments.Add(document);
+ document.BeginUpdate();
+ }
+ }
+
+ void CallEndUpdateOnAffectedDocuments()
+ {
+ if (affectedDocuments != null) {
+ foreach (TextDocument doc in affectedDocuments) {
+ doc.EndUpdate();
+ }
+ affectedDocuments = null;
+ }
+ }
+
+ /// <summary>
+ /// Call this method to undo the last operation on the stack
+ /// </summary>
+ public void Undo()
+ {
+ ThrowIfUndoGroupOpen();
+ if (undostack.Count > 0) {
+ // disallow continuing undo groups after undo operation
+ lastGroupDescriptor = null; allowContinue = false;
+ // fetch operation to undo and move it to redo stack
+ IUndoableOperation uedit = undostack.PopBack();
+ redostack.PushBack(uedit);
+ state = StatePlayback;
+ try {
+ RunUndo(uedit);
+ } finally {
+ state = StateListen;
+ FileModified(-1);
+ CallEndUpdateOnAffectedDocuments();
+ }
+ RecalcIsOriginalFile();
+ if (undostack.Count == 0)
+ NotifyPropertyChanged("CanUndo");
+ if (redostack.Count == 1)
+ NotifyPropertyChanged("CanRedo");
+ }
+ }
+
+ internal void RunUndo(IUndoableOperation op)
+ {
+ IUndoableOperationWithContext opWithCtx = op as IUndoableOperationWithContext;
+ if (opWithCtx != null)
+ opWithCtx.Undo(this);
+ else
+ op.Undo();
+ }
+
+ /// <summary>
+ /// Call this method to redo the last undone operation
+ /// </summary>
+ public void Redo()
+ {
+ ThrowIfUndoGroupOpen();
+ if (redostack.Count > 0) {
+ lastGroupDescriptor = null; allowContinue = false;
+ IUndoableOperation uedit = redostack.PopBack();
+ undostack.PushBack(uedit);
+ state = StatePlayback;
+ try {
+ RunRedo(uedit);
+ } finally {
+ state = StateListen;
+ FileModified(1);
+ CallEndUpdateOnAffectedDocuments();
+ }
+ RecalcIsOriginalFile();
+ if (redostack.Count == 0)
+ NotifyPropertyChanged("CanRedo");
+ if (undostack.Count == 1)
+ NotifyPropertyChanged("CanUndo");
+ }
+ }
+
+ internal void RunRedo(IUndoableOperation op)
+ {
+ IUndoableOperationWithContext opWithCtx = op as IUndoableOperationWithContext;
+ if (opWithCtx != null)
+ opWithCtx.Redo(this);
+ else
+ op.Redo();
+ }
+
+ /// <summary>
+ /// Call this method to push an UndoableOperation on the undostack.
+ /// The redostack will be cleared if you use this method.
+ /// </summary>
+ public void Push(IUndoableOperation operation)
+ {
+ Push(operation, false);
+ }
+
+ /// <summary>
+ /// Call this method to push an UndoableOperation on the undostack.
+ /// However, the operation will be only stored if the undo group contains a
+ /// non-optional operation.
+ /// Use this method to store the caret position/selection on the undo stack to
+ /// prevent having only actions that affect only the caret and not the document.
+ /// </summary>
+ public void PushOptional(IUndoableOperation operation)
+ {
+ if (undoGroupDepth == 0)
+ throw new InvalidOperationException("Cannot use PushOptional outside of undo group");
+ Push(operation, true);
+ }
+
+ void Push(IUndoableOperation operation, bool isOptional)
+ {
+ if (operation == null) {
+ throw new ArgumentNullException("operation");
+ }
+
+ if (state == StateListen && sizeLimit > 0) {
+ bool wasEmpty = undostack.Count == 0;
+
+ bool needsUndoGroup = undoGroupDepth == 0;
+ if (needsUndoGroup) StartUndoGroup();
+ undostack.PushBack(operation);
+ actionCountInUndoGroup++;
+ if (isOptional)
+ optionalActionCount++;
+ else
+ FileModified(1);
+ if (needsUndoGroup) EndUndoGroup();
+ if (wasEmpty)
+ NotifyPropertyChanged("CanUndo");
+ ClearRedoStack();
+ }
+ }
+
+ /// <summary>
+ /// Call this method, if you want to clear the redo stack
+ /// </summary>
+ public void ClearRedoStack()
+ {
+ if (redostack.Count != 0) {
+ redostack.Clear();
+ NotifyPropertyChanged("CanRedo");
+ // if the "original file" marker is on the redo stack: remove it
+ if (elementsOnUndoUntilOriginalFile < 0)
+ elementsOnUndoUntilOriginalFile = int.MinValue;
+ }
+ }
+
+ /// <summary>
+ /// Clears both the undo and redo stack.
+ /// </summary>
+ public void ClearAll()
+ {
+ ThrowIfUndoGroupOpen();
+ actionCountInUndoGroup = 0;
+ optionalActionCount = 0;
+ if (undostack.Count != 0) {
+ lastGroupDescriptor = null;
+ allowContinue = false;
+ undostack.Clear();
+ NotifyPropertyChanged("CanUndo");
+ }
+ ClearRedoStack();
+ }
+
+ internal void Push(TextDocument document, DocumentChangeEventArgs e)
+ {
+ if (state == StatePlayback)
+ throw new InvalidOperationException("Document changes during undo/redo operations are not allowed.");
+ if (state == StatePlaybackModifyDocument)
+ state = StatePlayback; // allow only 1 change per expected modification
+ else
+ Push(new DocumentChangeOperation(document, e));
+ }
+
+ /// <summary>
+ /// Is raised when a property (CanUndo, CanRedo) changed.
+ /// </summary>
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ void NotifyPropertyChanged(string propertyName)
+ {
+ if (PropertyChanged != null)
+ PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+}