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