PageRenderTime 24ms CodeModel.GetById 1ms app.highlight 15ms RepoModel.GetById 2ms app.codeStats 0ms

/AvalonEdit/ICSharpCode.AvalonEdit/Document/UndoStack.cs

http://github.com/icsharpcode/ILSpy
C# | 458 lines | 289 code | 42 blank | 127 comment | 79 complexity | 0230202c095a48e2b4a9c456d9436ba2 MD5 | raw file
  1// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
  2// 
  3// Permission is hereby granted, free of charge, to any person obtaining a copy of this
  4// software and associated documentation files (the "Software"), to deal in the Software
  5// without restriction, including without limitation the rights to use, copy, modify, merge,
  6// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
  7// to whom the Software is furnished to do so, subject to the following conditions:
  8// 
  9// The above copyright notice and this permission notice shall be included in all copies or
 10// substantial portions of the Software.
 11// 
 12// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
 13// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
 14// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
 15// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 16// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 17// DEALINGS IN THE SOFTWARE.
 18
 19using System;
 20using System.Collections.Generic;
 21using System.ComponentModel;
 22using System.Diagnostics;
 23using ICSharpCode.AvalonEdit.Utils;
 24
 25namespace ICSharpCode.AvalonEdit.Document
 26{
 27	/// <summary>
 28	/// Undo stack implementation.
 29	/// </summary>
 30	[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix")]
 31	public sealed class UndoStack : INotifyPropertyChanged
 32	{
 33		/// undo stack is listening for changes
 34		internal const int StateListen = 0;
 35		/// undo stack is reverting/repeating a set of changes
 36		internal const int StatePlayback = 1;
 37		// undo stack is reverting/repeating a set of changes and modifies the document to do this
 38		internal const int StatePlaybackModifyDocument = 2;
 39		/// state is used for checking that noone but the UndoStack performs changes
 40		/// during Undo events
 41		internal int state = StateListen;
 42		
 43		Deque<IUndoableOperation> undostack = new Deque<IUndoableOperation>();
 44		Deque<IUndoableOperation> redostack = new Deque<IUndoableOperation>();
 45		int sizeLimit = int.MaxValue;
 46		
 47		int undoGroupDepth;
 48		int actionCountInUndoGroup;
 49		int optionalActionCount;
 50		object lastGroupDescriptor;
 51		bool allowContinue;
 52		
 53		#region IsOriginalFile implementation
 54		// implements feature request SD2-784 - File still considered dirty after undoing all changes
 55		
 56		/// <summary>
 57		/// Number of times undo must be executed until the original state is reached.
 58		/// Negative: number of times redo must be executed until the original state is reached.
 59		/// Special case: int.MinValue == original state is unreachable
 60		/// </summary>
 61		int elementsOnUndoUntilOriginalFile;
 62		
 63		bool isOriginalFile = true;
 64		
 65		/// <summary>
 66		/// Gets whether the document is currently in its original state (no modifications).
 67		/// </summary>
 68		public bool IsOriginalFile {
 69			get { return isOriginalFile; }
 70		}
 71		
 72		void RecalcIsOriginalFile()
 73		{
 74			bool newIsOriginalFile = (elementsOnUndoUntilOriginalFile == 0);
 75			if (newIsOriginalFile != isOriginalFile) {
 76				isOriginalFile = newIsOriginalFile;
 77				NotifyPropertyChanged("IsOriginalFile");
 78			}
 79		}
 80		
 81		/// <summary>
 82		/// Marks the current state as original. Discards any previous "original" markers.
 83		/// </summary>
 84		public void MarkAsOriginalFile()
 85		{
 86			elementsOnUndoUntilOriginalFile = 0;
 87			RecalcIsOriginalFile();
 88		}
 89		
 90		/// <summary>
 91		/// Discards the current "original" marker.
 92		/// </summary>
 93		public void DiscardOriginalFileMarker()
 94		{
 95			elementsOnUndoUntilOriginalFile = int.MinValue;
 96			RecalcIsOriginalFile();
 97		}
 98		
 99		void FileModified(int newElementsOnUndoStack)
100		{
101			if (elementsOnUndoUntilOriginalFile == int.MinValue)
102				return;
103			
104			elementsOnUndoUntilOriginalFile += newElementsOnUndoStack;
105			if (elementsOnUndoUntilOriginalFile > undostack.Count)
106				elementsOnUndoUntilOriginalFile = int.MinValue;
107			
108			// don't call RecalcIsOriginalFile(): wait until end of undo group
109		}
110		#endregion
111		
112		/// <summary>
113		/// Gets if the undo stack currently accepts changes.
114		/// Is false while an undo action is running.
115		/// </summary>
116		public bool AcceptChanges {
117			get { return state == StateListen; }
118		}
119		
120		/// <summary>
121		/// Gets if there are actions on the undo stack.
122		/// Use the PropertyChanged event to listen to changes of this property.
123		/// </summary>
124		public bool CanUndo {
125			get { return undostack.Count > 0; }
126		}
127		
128		/// <summary>
129		/// Gets if there are actions on the redo stack.
130		/// Use the PropertyChanged event to listen to changes of this property.
131		/// </summary>
132		public bool CanRedo {
133			get { return redostack.Count > 0; }
134		}
135		
136		/// <summary>
137		/// Gets/Sets the limit on the number of items on the undo stack.
138		/// </summary>
139		/// <remarks>The size limit is enforced only on the number of stored top-level undo groups.
140		/// Elements within undo groups do not count towards the size limit.</remarks>
141		public int SizeLimit {
142			get { return sizeLimit; }
143			set {
144				if (value < 0)
145					ThrowUtil.CheckNotNegative(value, "value");
146				if (sizeLimit != value) {
147					sizeLimit = value;
148					NotifyPropertyChanged("SizeLimit");
149					if (undoGroupDepth == 0)
150						EnforceSizeLimit();
151				}
152			}
153		}
154		
155		void EnforceSizeLimit()
156		{
157			Debug.Assert(undoGroupDepth == 0);
158			while (undostack.Count > sizeLimit)
159				undostack.PopFront();
160			while (redostack.Count > sizeLimit)
161				redostack.PopFront();
162		}
163		
164		/// <summary>
165		/// If an undo group is open, gets the group descriptor of the current top-level
166		/// undo group.
167		/// If no undo group is open, gets the group descriptor from the previous undo group.
168		/// </summary>
169		/// <remarks>The group descriptor can be used to join adjacent undo groups:
170		/// use a group descriptor to mark your changes, and on the second action,
171		/// compare LastGroupDescriptor and use <see cref="StartContinuedUndoGroup"/> if you
172		/// want to join the undo groups.</remarks>
173		public object LastGroupDescriptor {
174			get { return lastGroupDescriptor; }
175		}
176		
177		/// <summary>
178		/// Starts grouping changes.
179		/// Maintains a counter so that nested calls are possible.
180		/// </summary>
181		public void StartUndoGroup()
182		{
183			StartUndoGroup(null);
184		}
185		
186		/// <summary>
187		/// Starts grouping changes.
188		/// Maintains a counter so that nested calls are possible.
189		/// </summary>
190		/// <param name="groupDescriptor">An object that is stored with the undo group.
191		/// If this is not a top-level undo group, the parameter is ignored.</param>
192		public void StartUndoGroup(object groupDescriptor)
193		{
194			if (undoGroupDepth == 0) {
195				actionCountInUndoGroup = 0;
196				optionalActionCount = 0;
197				lastGroupDescriptor = groupDescriptor;
198			}
199			undoGroupDepth++;
200			//Util.LoggingService.Debug("Open undo group (new depth=" + undoGroupDepth + ")");
201		}
202		
203		/// <summary>
204		/// Starts grouping changes, continuing with the previously closed undo group if possible.
205		/// Maintains a counter so that nested calls are possible.
206		/// If the call to StartContinuedUndoGroup is a nested call, it behaves exactly
207		/// as <see cref="StartUndoGroup()"/>, only top-level calls can continue existing undo groups.
208		/// </summary>
209		/// <param name="groupDescriptor">An object that is stored with the undo group.
210		/// If this is not a top-level undo group, the parameter is ignored.</param>
211		public void StartContinuedUndoGroup(object groupDescriptor = null)
212		{
213			if (undoGroupDepth == 0) {
214				actionCountInUndoGroup = (allowContinue && undostack.Count > 0) ? 1 : 0;
215				optionalActionCount = 0;
216				lastGroupDescriptor = groupDescriptor;
217			}
218			undoGroupDepth++;
219			//Util.LoggingService.Debug("Continue undo group (new depth=" + undoGroupDepth + ")");
220		}
221		
222		/// <summary>
223		/// Stops grouping changes.
224		/// </summary>
225		public void EndUndoGroup()
226		{
227			if (undoGroupDepth == 0) throw new InvalidOperationException("There are no open undo groups");
228			undoGroupDepth--;
229			//Util.LoggingService.Debug("Close undo group (new depth=" + undoGroupDepth + ")");
230			if (undoGroupDepth == 0) {
231				Debug.Assert(state == StateListen || actionCountInUndoGroup == 0);
232				allowContinue = true;
233				if (actionCountInUndoGroup == optionalActionCount) {
234					// only optional actions: don't store them
235					for (int i = 0; i < optionalActionCount; i++) {
236						undostack.PopBack();
237					}
238					allowContinue = false;
239				} else if (actionCountInUndoGroup > 1) {
240					// combine all actions within the group into a single grouped action
241					undostack.PushBack(new UndoOperationGroup(undostack, actionCountInUndoGroup));
242					FileModified(-actionCountInUndoGroup + 1 + optionalActionCount);
243				}
244				//if (state == StateListen) {
245				EnforceSizeLimit();
246				RecalcIsOriginalFile(); // can raise event
247				//}
248			}
249		}
250		
251		/// <summary>
252		/// Throws an InvalidOperationException if an undo group is current open.
253		/// </summary>
254		void ThrowIfUndoGroupOpen()
255		{
256			if (undoGroupDepth != 0) {
257				undoGroupDepth = 0;
258				throw new InvalidOperationException("No undo group should be open at this point");
259			}
260			if (state != StateListen) {
261				throw new InvalidOperationException("This method cannot be called while an undo operation is being performed");
262			}
263		}
264		
265		List<TextDocument> affectedDocuments;
266		
267		internal void RegisterAffectedDocument(TextDocument document)
268		{
269			if (affectedDocuments == null)
270				affectedDocuments = new List<TextDocument>();
271			if (!affectedDocuments.Contains(document)) {
272				affectedDocuments.Add(document);
273				document.BeginUpdate();
274			}
275		}
276		
277		void CallEndUpdateOnAffectedDocuments()
278		{
279			if (affectedDocuments != null) {
280				foreach (TextDocument doc in affectedDocuments) {
281					doc.EndUpdate();
282				}
283				affectedDocuments = null;
284			}
285		}
286		
287		/// <summary>
288		/// Call this method to undo the last operation on the stack
289		/// </summary>
290		public void Undo()
291		{
292			ThrowIfUndoGroupOpen();
293			if (undostack.Count > 0) {
294				// disallow continuing undo groups after undo operation
295				lastGroupDescriptor = null; allowContinue = false;
296				// fetch operation to undo and move it to redo stack
297				IUndoableOperation uedit = undostack.PopBack();
298				redostack.PushBack(uedit);
299				state = StatePlayback;
300				try {
301					RunUndo(uedit);
302				} finally {
303					state = StateListen;
304					FileModified(-1);
305					CallEndUpdateOnAffectedDocuments();
306				}
307				RecalcIsOriginalFile();
308				if (undostack.Count == 0)
309					NotifyPropertyChanged("CanUndo");
310				if (redostack.Count == 1)
311					NotifyPropertyChanged("CanRedo");
312			}
313		}
314		
315		internal void RunUndo(IUndoableOperation op)
316		{
317			IUndoableOperationWithContext opWithCtx = op as IUndoableOperationWithContext;
318			if (opWithCtx != null)
319				opWithCtx.Undo(this);
320			else
321				op.Undo();
322		}
323		
324		/// <summary>
325		/// Call this method to redo the last undone operation
326		/// </summary>
327		public void Redo()
328		{
329			ThrowIfUndoGroupOpen();
330			if (redostack.Count > 0) {
331				lastGroupDescriptor = null; allowContinue = false;
332				IUndoableOperation uedit = redostack.PopBack();
333				undostack.PushBack(uedit);
334				state = StatePlayback;
335				try {
336					RunRedo(uedit);
337				} finally {
338					state = StateListen;
339					FileModified(1);
340					CallEndUpdateOnAffectedDocuments();
341				}
342				RecalcIsOriginalFile();
343				if (redostack.Count == 0)
344					NotifyPropertyChanged("CanRedo");
345				if (undostack.Count == 1)
346					NotifyPropertyChanged("CanUndo");
347			}
348		}
349		
350		internal void RunRedo(IUndoableOperation op)
351		{
352			IUndoableOperationWithContext opWithCtx = op as IUndoableOperationWithContext;
353			if (opWithCtx != null)
354				opWithCtx.Redo(this);
355			else
356				op.Redo();
357		}
358		
359		/// <summary>
360		/// Call this method to push an UndoableOperation on the undostack.
361		/// The redostack will be cleared if you use this method.
362		/// </summary>
363		public void Push(IUndoableOperation operation)
364		{
365			Push(operation, false);
366		}
367		
368		/// <summary>
369		/// Call this method to push an UndoableOperation on the undostack.
370		/// However, the operation will be only stored if the undo group contains a
371		/// non-optional operation.
372		/// Use this method to store the caret position/selection on the undo stack to
373		/// prevent having only actions that affect only the caret and not the document.
374		/// </summary>
375		public void PushOptional(IUndoableOperation operation)
376		{
377			if (undoGroupDepth == 0)
378				throw new InvalidOperationException("Cannot use PushOptional outside of undo group");
379			Push(operation, true);
380		}
381		
382		void Push(IUndoableOperation operation, bool isOptional)
383		{
384			if (operation == null) {
385				throw new ArgumentNullException("operation");
386			}
387			
388			if (state == StateListen && sizeLimit > 0) {
389				bool wasEmpty = undostack.Count == 0;
390				
391				bool needsUndoGroup = undoGroupDepth == 0;
392				if (needsUndoGroup) StartUndoGroup();
393				undostack.PushBack(operation);
394				actionCountInUndoGroup++;
395				if (isOptional)
396					optionalActionCount++;
397				else
398					FileModified(1);
399				if (needsUndoGroup) EndUndoGroup();
400				if (wasEmpty)
401					NotifyPropertyChanged("CanUndo");
402				ClearRedoStack();
403			}
404		}
405		
406		/// <summary>
407		/// Call this method, if you want to clear the redo stack
408		/// </summary>
409		public void ClearRedoStack()
410		{
411			if (redostack.Count != 0) {
412				redostack.Clear();
413				NotifyPropertyChanged("CanRedo");
414				// if the "original file" marker is on the redo stack: remove it
415				if (elementsOnUndoUntilOriginalFile < 0)
416					elementsOnUndoUntilOriginalFile = int.MinValue;
417			}
418		}
419		
420		/// <summary>
421		/// Clears both the undo and redo stack.
422		/// </summary>
423		public void ClearAll()
424		{
425			ThrowIfUndoGroupOpen();
426			actionCountInUndoGroup = 0;
427			optionalActionCount = 0;
428			if (undostack.Count != 0) {
429				lastGroupDescriptor = null;
430				allowContinue = false;
431				undostack.Clear();
432				NotifyPropertyChanged("CanUndo");
433			}
434			ClearRedoStack();
435		}
436		
437		internal void Push(TextDocument document, DocumentChangeEventArgs e)
438		{
439			if (state == StatePlayback)
440				throw new InvalidOperationException("Document changes during undo/redo operations are not allowed.");
441			if (state == StatePlaybackModifyDocument)
442				state = StatePlayback; // allow only 1 change per expected modification
443			else
444				Push(new DocumentChangeOperation(document, e));
445		}
446		
447		/// <summary>
448		/// Is raised when a property (CanUndo, CanRedo) changed.
449		/// </summary>
450		public event PropertyChangedEventHandler PropertyChanged;
451		
452		void NotifyPropertyChanged(string propertyName)
453		{
454			if (PropertyChanged != null)
455				PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
456		}
457	}
458}