undo.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import Plugin from '../core/plugin.js';
  6. import UndoEngine from './undoengine.js';
  7. import ButtonView from '../ui/button/buttonview.js';
  8. /**
  9. * The undo feature. It introduces the Undo and Redo buttons to the editor.
  10. *
  11. * Below is the explanation of the undo mechanism working together with {@link engine.model.History History}:
  12. *
  13. * Whenever a {@link engine.model.Delta delta} is applied to the {@link engine.model.Document document}, it is saved to
  14. * `History` as is. The {@link engine.model.Batch batch} that owns that delta is also saved, in {@link undo.UndoCommand},
  15. * together with the selection that was present in the document before the delta was applied. A batch is saved instead of the delta
  16. * because changes are undone batch-by-batch, not delta-by-delta and a batch is seen as one undo step.
  17. *
  18. * After some changes happen to the document, the `History` and `UndoCommand` stack can be represented as follows:
  19. *
  20. * History Undo stack
  21. * =========== ==================================
  22. * [delta A1] [batch A with selection before A1]
  23. * [delta B1] [batch B with selection before B1]
  24. * [delta B2] [batch C with selection before C1]
  25. * [delta C1]
  26. * [delta C2]
  27. * [delta B3]
  28. * [delta C3]
  29. *
  30. * Where deltas starting with the same letter are from same batch.
  31. *
  32. * Undoing a batch means that a set of deltas which will reverse the effects of that batch needs to be generated. For example, if a batch
  33. * added several letters, undoing the batch should remove them. It is important to apply undoing deltas in the reversed order,
  34. * so if a batch has delta `X`, `Y`, `Z`, reversed deltas `Zr`, `Yr` and `Xr` need to be applied. Otherwise reversed delta
  35. * `Xr` would operate on a wrong document state, because delta `X` does not know that deltas `Y` and `Z` happened.
  36. *
  37. * After deltas from an undone batch got {@link engine.model.Delta#getReversed reversed}, one needs to make sure if they are
  38. * ready to be applied. In the scenario above, delta `C3` is the last delta and `C3r` bases on up-to-date document state, so
  39. * it can be applied to the document.
  40. *
  41. * History Undo stack
  42. * =========== ==================================
  43. * [delta A1 ] [batch A with selection before A1]
  44. * [delta B1 ] [batch B with selection before B1]
  45. * [delta B2 ] [ processing undoing batch C ]
  46. * [delta C1 ]
  47. * [delta C2 ]
  48. * [delta B3 ]
  49. * [delta C3 ]
  50. * [delta C3r]
  51. *
  52. * Next is delta `C2`, reversed to `C2r`. `C2r` bases on `C2`, so it bases on the wrong document state. It needs to be
  53. * transformed by deltas from history that happened after it, so it "knows" about them. Let us assume that `C2' = C2r * B3 * C3 * C3r`,
  54. * where `*` means "transformed by". As can be seen, `C2r` is transformed by a delta which is undone afterwards anyway.
  55. * This brings two problems: lower effectiveness (obvious) and incorrect results. Bad results come from the fact that
  56. * operational transformation algorithms assume there is no connection between two transformed operations when resolving
  57. * conflicts, which is true for example for collaborative editing, but is not true for the undo algorithm.
  58. *
  59. * To prevent both problems, `History` introduces an API to {@link engine.model.History#removeDelta remove}
  60. * deltas from history. It is used to remove undone and undoing deltas after they are applied. It feels right — since when a
  61. * delta is undone or reversed, it is "removed" and there should be no sign of it in the history (fig. 1).
  62. *
  63. * Notes:
  64. *
  65. * * `---` symbolizes a removed delta.
  66. * * `'` symbolizes a reversed delta that was later transformed.
  67. *
  68. * History (fig. 1) History (fig. 2) History (fig. 3)
  69. * ================ ================ ================
  70. * [delta A1] [delta A1] [delta A1]
  71. * [delta B1] [delta B1] [delta B1]
  72. * [delta B2] [delta B2] [delta B2]
  73. * [delta C1] [delta C1] [---C1---]
  74. * [delta C2] [---C2---] [---C2---]
  75. * [delta B3] [delta B3] [delta B3]
  76. * [---C3---] [---C3---] [---C3---]
  77. * [---C3r--] [---C3r--] [---C3r--]
  78. * [---C2'--] [---C2'--]
  79. * [---C1'--]
  80. *
  81. * `C2r` can now be transformed only by `B3` and both `C2'` and `C2` can be removed (fig. 2). Same with `C1` (fig. 3).
  82. *
  83. * But what about that selection? For batch `C`, undo feature remembers the selection just before `C1` was applied. It can be
  84. * visualized between delta `B2` and `B3` (see fig. 3). As can be seen, some operations were applied to the document since the selection
  85. * state was remembered. Setting the document selection as it was remembered would be incorrect. It feels natural that
  86. * the selection state should also be transformed by deltas from history. The same pattern applies as with transforming deltas —
  87. * ranges should not be transformed by undone and undoing deltas. Thankfully, those deltas are already removed from history.
  88. *
  89. * Unfortunately, a problem appears with delta `B3`. It still remembers the context of deltas `C2` and `C1` on which it bases.
  90. * It is an obvious error — transforming by that delta would lead to incorrect results or "repeating" history would
  91. * produce a different document than the actual one.
  92. *
  93. * To prevent this situation, `B3` needs to also be {@link engine.model.History#updateDelta updated} in history.
  94. * It should be kept in a state that "does not remember" deltas that were removed from history. It is easily
  95. * achieved while transforming the reversed delta. For example, when `C2r` is transformed by `B3`, at the same time `B3` is
  96. * transformed by `C2r`. Transforming `B3` that remembers `C2` by a delta reversing `C2` effectively makes `B3` "forget" about `C2`.
  97. * By doing these transformations you effectively make `B3` base on `B2` which is the correct state of history (fig. 4).
  98. *
  99. * History (fig. 4) History (fig. 5)
  100. * =========================== ===============================
  101. * [delta A1] [---A1---]
  102. * [delta B1] [delta B1 "without A1"]
  103. * [delta B2] [delta B2 "without A1"]
  104. * [---C1---] [---C1---]
  105. * [---C2---] [---C2---]
  106. * [delta B3 "without C2, C1"] [delta B3 "without C2, C1, A1"]
  107. * [---C3---] [---C3---]
  108. * [---C3r--] [---C3r--]
  109. * [---C2'--] [---C2'--]
  110. * [---C1'--] [---C1'--]
  111. * [---A1'--]
  112. *
  113. * Selective undo works on the same basis, however, instead of undoing the last batch in the undo stack, any batch can be undone.
  114. * The same algorithm applies: deltas from a batch (i.e. `A1`) are reversed and then transformed by deltas stored in history,
  115. * simultaneously updating them. Then deltas are applied to the document and removed from history (fig. 5).
  116. *
  117. * @memberOf undo
  118. * @extends core.Feature
  119. */
  120. export default class Undo extends Plugin {
  121. /**
  122. * @inheritDoc
  123. */
  124. static get requires() {
  125. return [ UndoEngine ];
  126. }
  127. /**
  128. * @inheritDoc
  129. */
  130. init() {
  131. const editor = this.editor;
  132. const t = editor.t;
  133. this._addButton( 'undo', t( 'Undo' ), 'CTRL+Z' );
  134. this._addButton( 'redo', t( 'Redo' ), 'CTRL+Y' );
  135. editor.keystrokes.set( 'CTRL+Z', 'undo' );
  136. editor.keystrokes.set( 'CTRL+Y', 'redo' );
  137. editor.keystrokes.set( 'CTRL+SHIFT+Z', 'redo' );
  138. }
  139. /**
  140. * Creates a button for the specified command.
  141. *
  142. * @private
  143. * @param {String} name Command name.
  144. * @param {String} label Button label.
  145. * @param {String} keystroke Command keystroke.
  146. */
  147. _addButton( name, label, keystroke ) {
  148. const editor = this.editor;
  149. const command = editor.commands.get( name );
  150. editor.ui.componentFactory.add( name, ( locale ) => {
  151. const view = new ButtonView( locale );
  152. view.set( {
  153. label: label,
  154. icon: name,
  155. keystroke
  156. } );
  157. view.bind( 'isEnabled' ).to( command, 'isEnabled' );
  158. this.listenTo( view, 'execute', () => editor.execute( name ) );
  159. return view;
  160. } );
  161. }
  162. }