undo.js 8.4 KB

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