8
0
Просмотр исходного кода

Merge pull request #1459 from ckeditor/t/new-ot-w-tests

Other: Removed concept of deltas. Added new operations. Rewritten OT algorithms. Closes #1162.

BREAKING CHANGE: Everything connected with deltas was removed from the editor.
BREAKING CHANGE: `model.Writer#setAttribute` (and `AttributeOperation`) now applies attribute only to the top-level nodes in the `range` (instead of all the nodes in the range).
Piotr Jasiun 7 лет назад
Родитель
Сommit
9da576a9ee
100 измененных файлов с 3695 добавлено и 10859 удалено
  1. 0 143
      packages/ckeditor5-engine/src/dev-utils/deltareplayer.js
  2. 18 150
      packages/ckeditor5-engine/src/dev-utils/enableenginedebug.js
  3. 24 5
      packages/ckeditor5-engine/src/dev-utils/model.js
  4. 140 0
      packages/ckeditor5-engine/src/dev-utils/operationreplayer.js
  5. 18 30
      packages/ckeditor5-engine/src/model/batch.js
  6. 0 111
      packages/ckeditor5-engine/src/model/delta/attributedelta.js
  7. 0 23
      packages/ckeditor5-engine/src/model/delta/basic-deltas.js
  8. 0 627
      packages/ckeditor5-engine/src/model/delta/basic-transformations.js
  9. 0 167
      packages/ckeditor5-engine/src/model/delta/delta.js
  10. 0 72
      packages/ckeditor5-engine/src/model/delta/deltafactory.js
  11. 0 74
      packages/ckeditor5-engine/src/model/delta/insertdelta.js
  12. 0 46
      packages/ckeditor5-engine/src/model/delta/markerdelta.js
  13. 0 67
      packages/ckeditor5-engine/src/model/delta/mergedelta.js
  14. 0 84
      packages/ckeditor5-engine/src/model/delta/movedelta.js
  15. 0 28
      packages/ckeditor5-engine/src/model/delta/removedelta.js
  16. 0 42
      packages/ckeditor5-engine/src/model/delta/renamedelta.js
  17. 0 30
      packages/ckeditor5-engine/src/model/delta/rootattributedelta.js
  18. 0 83
      packages/ckeditor5-engine/src/model/delta/splitdelta.js
  19. 0 664
      packages/ckeditor5-engine/src/model/delta/transform.js
  20. 0 62
      packages/ckeditor5-engine/src/model/delta/unwrapdelta.js
  21. 0 28
      packages/ckeditor5-engine/src/model/delta/weakinsertdelta.js
  22. 0 88
      packages/ckeditor5-engine/src/model/delta/wrapdelta.js
  23. 48 0
      packages/ckeditor5-engine/src/model/differ.js
  24. 1 3
      packages/ckeditor5-engine/src/model/document.js
  25. 2 2
      packages/ckeditor5-engine/src/model/documentselection.js
  26. 56 117
      packages/ckeditor5-engine/src/model/history.js
  27. 9 83
      packages/ckeditor5-engine/src/model/liveposition.js
  28. 28 62
      packages/ckeditor5-engine/src/model/liverange.js
  29. 1 26
      packages/ckeditor5-engine/src/model/model.js
  30. 11 2
      packages/ckeditor5-engine/src/model/operation/attributeoperation.js
  31. 21 2
      packages/ckeditor5-engine/src/model/operation/insertoperation.js
  32. 166 0
      packages/ckeditor5-engine/src/model/operation/mergeoperation.js
  33. 6 25
      packages/ckeditor5-engine/src/model/operation/moveoperation.js
  34. 8 7
      packages/ckeditor5-engine/src/model/operation/operation.js
  35. 8 0
      packages/ckeditor5-engine/src/model/operation/operationfactory.js
  36. 1 0
      packages/ckeditor5-engine/src/model/operation/renameoperation.js
  37. 175 0
      packages/ckeditor5-engine/src/model/operation/splitoperation.js
  38. 1622 530
      packages/ckeditor5-engine/src/model/operation/transform.js
  39. 153 0
      packages/ckeditor5-engine/src/model/operation/unwrapoperation.js
  40. 4 4
      packages/ckeditor5-engine/src/model/operation/utils.js
  41. 179 0
      packages/ckeditor5-engine/src/model/operation/wrapoperation.js
  42. 191 17
      packages/ckeditor5-engine/src/model/position.js
  43. 139 155
      packages/ckeditor5-engine/src/model/range.js
  44. 3 0
      packages/ckeditor5-engine/src/model/treewalker.js
  45. 2 1
      packages/ckeditor5-engine/src/model/utils/deletecontent.js
  46. 6 2
      packages/ckeditor5-engine/src/model/utils/insertcontent.js
  47. 112 147
      packages/ckeditor5-engine/src/model/writer.js
  48. 4 4
      packages/ckeditor5-engine/tests/controller/datacontroller.js
  49. 0 203
      packages/ckeditor5-engine/tests/dev-utils/deltareplayer.js
  50. 32 500
      packages/ckeditor5-engine/tests/dev-utils/enableenginedebug.js
  51. 2 2
      packages/ckeditor5-engine/tests/dev-utils/model.js
  52. 197 0
      packages/ckeditor5-engine/tests/dev-utils/operationreplayer.js
  53. 0 23
      packages/ckeditor5-engine/tests/model/_utils/utils.js
  54. 23 53
      packages/ckeditor5-engine/tests/model/batch.js
  55. 0 131
      packages/ckeditor5-engine/tests/model/delta/attributedelta.js
  56. 0 276
      packages/ckeditor5-engine/tests/model/delta/delta.js
  57. 0 168
      packages/ckeditor5-engine/tests/model/delta/deltafactory.js
  58. 0 93
      packages/ckeditor5-engine/tests/model/delta/insertdelta.js
  59. 0 55
      packages/ckeditor5-engine/tests/model/delta/markerdelta.js
  60. 0 84
      packages/ckeditor5-engine/tests/model/delta/mergedelta.js
  61. 0 99
      packages/ckeditor5-engine/tests/model/delta/movedelta.js
  62. 0 12
      packages/ckeditor5-engine/tests/model/delta/removedelta.js
  63. 0 62
      packages/ckeditor5-engine/tests/model/delta/renamedelta.js
  64. 0 12
      packages/ckeditor5-engine/tests/model/delta/rootattributedelta.js
  65. 0 125
      packages/ckeditor5-engine/tests/model/delta/splitdelta.js
  66. 0 254
      packages/ckeditor5-engine/tests/model/delta/transform/_utils/utils.js
  67. 0 406
      packages/ckeditor5-engine/tests/model/delta/transform/attributedelta.js
  68. 0 85
      packages/ckeditor5-engine/tests/model/delta/transform/delta.js
  69. 0 251
      packages/ckeditor5-engine/tests/model/delta/transform/insertdelta.js
  70. 0 255
      packages/ckeditor5-engine/tests/model/delta/transform/markerdelta.js
  71. 0 365
      packages/ckeditor5-engine/tests/model/delta/transform/mergedelta.js
  72. 0 191
      packages/ckeditor5-engine/tests/model/delta/transform/movedelta.js
  73. 0 242
      packages/ckeditor5-engine/tests/model/delta/transform/removedelta.js
  74. 0 203
      packages/ckeditor5-engine/tests/model/delta/transform/renamedelta.js
  75. 0 1091
      packages/ckeditor5-engine/tests/model/delta/transform/splitdelta.js
  76. 0 569
      packages/ckeditor5-engine/tests/model/delta/transform/transform.js
  77. 0 185
      packages/ckeditor5-engine/tests/model/delta/transform/unwrapdelta.js
  78. 0 132
      packages/ckeditor5-engine/tests/model/delta/transform/weakinsertdelta.js
  79. 0 226
      packages/ckeditor5-engine/tests/model/delta/transform/wrapdelta.js
  80. 0 87
      packages/ckeditor5-engine/tests/model/delta/unwrapdelta.js
  81. 0 12
      packages/ckeditor5-engine/tests/model/delta/weakinsertdelta.js
  82. 0 116
      packages/ckeditor5-engine/tests/model/delta/wrapdelta.js
  83. 17 27
      packages/ckeditor5-engine/tests/model/differ.js
  84. 4 9
      packages/ckeditor5-engine/tests/model/document.js
  85. 40 78
      packages/ckeditor5-engine/tests/model/documentselection.js
  86. 92 113
      packages/ckeditor5-engine/tests/model/history.js
  87. 5 3
      packages/ckeditor5-engine/tests/model/liveposition.js
  88. 12 13
      packages/ckeditor5-engine/tests/model/liverange.js
  89. 3 44
      packages/ckeditor5-engine/tests/model/model.js
  90. 21 62
      packages/ckeditor5-engine/tests/model/operation/attributeoperation.js
  91. 2 2
      packages/ckeditor5-engine/tests/model/operation/detachoperation.js
  92. 20 18
      packages/ckeditor5-engine/tests/model/operation/insertoperation.js
  93. 15 15
      packages/ckeditor5-engine/tests/model/operation/markeroperation.js
  94. 11 35
      packages/ckeditor5-engine/tests/model/operation/moveoperation.js
  95. 2 2
      packages/ckeditor5-engine/tests/model/operation/nooperation.js
  96. 15 14
      packages/ckeditor5-engine/tests/model/operation/operation.js
  97. 3 8
      packages/ckeditor5-engine/tests/model/operation/reinsertoperation.js
  98. 6 18
      packages/ckeditor5-engine/tests/model/operation/removeoperation.js
  99. 4 4
      packages/ckeditor5-engine/tests/model/operation/renameoperation.js
  100. 13 13
      packages/ckeditor5-engine/tests/model/operation/rootattributeoperation.js

+ 0 - 143
packages/ckeditor5-engine/src/dev-utils/deltareplayer.js

@@ -1,143 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/dev-utils/deltareplayer
- */
-
-/* global setTimeout */
-
-import DeltaFactory from '../model/delta/deltafactory';
-
-/**
- * Delta replayer is a development tool created for easy replaying of operations on the document from stringified deltas.
- */
-export default class DeltaReplayer {
-	/**
-	 * @param {module:engine/model/model~Model} model Data model.
-	 * @param {String} logSeparator Separator between deltas.
-	 * @param {String} stringifiedDeltas Deltas to replay.
-	 */
-	constructor( model, logSeparator, stringifiedDeltas ) {
-		this._model = model;
-		this._logSeparator = logSeparator;
-		this.setStringifiedDeltas( stringifiedDeltas );
-	}
-
-	/**
-	 * Parses the given string containing stringified deltas and sets parsed deltas as deltas to replay.
-	 *
-	 * @param {String} stringifiedDeltas Stringified deltas to replay.
-	 */
-	setStringifiedDeltas( stringifiedDeltas ) {
-		if ( stringifiedDeltas === '' ) {
-			this._deltasToReplay = [];
-
-			return;
-		}
-
-		this._deltasToReplay = stringifiedDeltas
-			.split( this._logSeparator )
-			.map( stringifiedDelta => JSON.parse( stringifiedDelta ) );
-	}
-
-	/**
-	 * Returns deltas to replay.
-	 *
-	 * @returns {Array.<module:engine/model/delta/delta~Delta>}
-	 */
-	getDeltasToReplay() {
-		return this._deltasToReplay;
-	}
-
-	/**
-	 * Applies all deltas with a delay between actions.
-	 *
-	 * @param {Number} timeInterval Time between applying deltas.
-	 * @returns {Promise}
-	 */
-	play( timeInterval = 1000 ) {
-		const deltaReplayer = this; // eslint-disable-line consistent-this
-
-		return new Promise( ( res, rej ) => {
-			play();
-
-			function play() {
-				deltaReplayer.applyNextDelta().then( isFinished => {
-					if ( isFinished ) {
-						return res();
-					}
-
-					setTimeout( play, timeInterval );
-				} ).catch( err => {
-					rej( err );
-				} );
-			}
-		} );
-	}
-
-	/**
-	 * Applies `numberOfDeltas` deltas, beginning after the last applied delta (or first delta, if no deltas were applied).
-	 *
-	 * @param {Number} numberOfDeltas The number of deltas to apply.
-	 * @returns {Promise}
-	 */
-	applyDeltas( numberOfDeltas ) {
-		if ( numberOfDeltas <= 0 ) {
-			return;
-		}
-
-		return this.applyNextDelta()
-			.then( isFinished => {
-				if ( !isFinished ) {
-					return this.applyDeltas( numberOfDeltas - 1 );
-				}
-			} );
-	}
-
-	/**
-	 * Applies all deltas to replay at once.
-	 *
-	 * @returns {Promise}
-	 */
-	applyAllDeltas() {
-		return this.applyNextDelta()
-			.then( isFinished => {
-				if ( !isFinished ) {
-					return this.applyAllDeltas();
-				}
-			} );
-	}
-
-	/**
-	 * Applies the next delta to replay. Returns a promise with the `isFinished` parameter that is `true` if the last
-	 * delta in the replayer has been applied, `false` otherwise.
-	 *
-	 * @returns {Promise.<Boolean>}
-	 */
-	applyNextDelta() {
-		const model = this._model;
-
-		return new Promise( res => {
-			model.enqueueChange( writer => {
-				const jsonDelta = this._deltasToReplay.shift();
-
-				if ( !jsonDelta ) {
-					return res( true );
-				}
-
-				const delta = DeltaFactory.fromJSON( jsonDelta, model.document );
-
-				writer.batch.addDelta( delta );
-
-				for ( const operation of delta.operations ) {
-					model.applyOperation( operation );
-				}
-
-				res( false );
-			} );
-		} );
-	}
-}

+ 18 - 150
packages/ckeditor5-engine/src/dev-utils/enableenginedebug.js

@@ -9,7 +9,7 @@
 
 /* global console */
 
-import DeltaReplayer from './deltareplayer';
+import OperationReplayer from './operationreplayer';
 
 import ModelPosition from '../model/position';
 import ModelRange from '../model/range';
@@ -25,18 +25,7 @@ import MoveOperation from '../model/operation/moveoperation';
 import NoOperation from '../model/operation/nooperation';
 import RenameOperation from '../model/operation/renameoperation';
 import RootAttributeOperation from '../model/operation/rootattributeoperation';
-import Delta from '../model/delta/delta';
-import AttributeDelta from '../model/delta/attributedelta';
-import InsertDelta from '../model/delta/insertdelta';
-import MarkerDelta from '../model/delta/markerdelta';
-import MergeDelta from '../model/delta/mergedelta';
-import MoveDelta from '../model/delta/movedelta';
-import RenameDelta from '../model/delta/renamedelta';
-import RootAttributeDelta from '../model/delta/rootattributedelta';
-import SplitDelta from '../model/delta/splitdelta';
-import UnwrapDelta from '../model/delta/unwrapdelta';
-import WrapDelta from '../model/delta/wrapdelta';
-import deltaTransform from '../model/delta/transform';
+import transform from '../model/operation/transform';
 import Model from '../model/model';
 import ModelDocument from '../model/document';
 import ModelDocumentFragment from '../model/documentfragment';
@@ -51,8 +40,6 @@ import ViewDocumentFragment from '../view/documentfragment';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Editor from '@ckeditor/ckeditor5-core/src/editor/editor';
 
-import clone from '@ckeditor/ckeditor5-utils/src/lib/lodash/clone';
-
 // Sandbox class allows creating mocks of the functions and restoring these mocks to the original values.
 class Sandbox {
 	constructor() {
@@ -98,7 +85,7 @@ const treeDump = Symbol( '_treeDump' );
 // Maximum number of stored states of model and view document.
 const maxTreeDumpLength = 20;
 
-// Separator used to separate stringified deltas
+// Separator used to separate stringified operations
 const LOG_SEPARATOR = '-------';
 
 // Specified whether debug tools were already enabled.
@@ -122,7 +109,6 @@ let logger = console;
  * * {@link module:engine/model/documentfragment~DocumentFragment model.DocumentFragment},
  * * {@link module:engine/model/document~Document model.Document},
  * * all {@link module:engine/model/operation/operation~Operation operations}
- * * all {@link module:engine/model/delta/delta~Delta deltas},
  * * {@link module:engine/view/element~Element view.Element},
  * * {@link module:engine/view/documentfragment~DocumentFragment view.DocumentFragment},
  * * {@link module:engine/view/document~Document view.Document}.
@@ -130,8 +116,7 @@ let logger = console;
  * Additionally, the following logging utility methods are added:
  * * {@link module:engine/model/text~Text model.Text} `logExtended`,
  * * {@link module:engine/model/element~Element model.Element} `logExtended`,
- * * {@link module:engine/model/element~Element model.Element} `logAll`,
- * * {@link module:engine/model/delta/delta~Delta model.Delta} `logAll`.
+ * * {@link module:engine/model/element~Element model.Element} `logAll`.
  *
  * Additionally, the following classes are expanded with `logTree` and `printTree` methods:
  * * {@link module:engine/model/element~Element model.Element},
@@ -364,136 +349,24 @@ function enableLoggingTools() {
 			`"${ this.key }": ${ JSON.stringify( this.oldValue ) } -> ${ JSON.stringify( this.newValue ) }, ${ this.root.rootName }`;
 	} );
 
-	sandbox.mock( Delta.prototype, 'log', function() {
-		logger.log( this.toString() );
-	} );
-
-	sandbox.mock( Delta.prototype, 'logAll', function() {
-		logger.log( '--------------------' );
-
-		this.log();
-
-		for ( const op of this.operations ) {
-			op.log();
-		}
-	} );
-
-	sandbox.mock( Delta.prototype, '_saveHistory', function( itemToSave ) {
-		const history = itemToSave.before.history ? itemToSave.before.history : [];
-
-		itemToSave.before = clone( itemToSave.before );
-		delete itemToSave.before.history;
-		itemToSave.before = JSON.stringify( itemToSave.before );
-
-		itemToSave.transformedBy = clone( itemToSave.transformedBy );
-		delete itemToSave.transformedBy.history;
-		itemToSave.transformedBy = JSON.stringify( itemToSave.transformedBy );
-
-		this.history = history.concat( itemToSave );
-	} );
-
-	const _deltaTransformTransform = deltaTransform.transform;
+	const _transformTransform = transform.transform;
 
-	sandbox.mock( deltaTransform, 'transform', function( a, b, context ) {
+	sandbox.mock( transform, 'transform', function( a, b, context ) {
 		let results;
 
 		try {
-			results = _deltaTransformTransform( a, b, context );
+			results = _transformTransform( a, b, context );
 		} catch ( e ) {
-			logger.error( 'Error during delta transformation!' );
+			logger.error( 'Error during operation transformation!' );
 			logger.error( a.toString() + ( context.isStrong ? ' (important)' : '' ) );
 			logger.error( b.toString() + ( context.isStrong ? '' : ' (important)' ) );
 
 			throw e;
 		}
 
-		for ( let i = 0; i < results.length; i++ ) {
-			results[ i ]._saveHistory( {
-				before: a,
-				transformedBy: b,
-				wasImportant: !!context.isStrong,
-				resultIndex: i,
-				resultsTotal: results.length
-			} );
-		}
-
 		return results;
 	} );
 
-	sandbox.mock( AttributeDelta.prototype, 'toString', function() {
-		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
-			`"${ this.key }": -> ${ JSON.stringify( this.value ) }, ${ this.range }, ${ this.operations.length } ops`;
-	} );
-
-	sandbox.mock( InsertDelta.prototype, 'toString', function() {
-		const op = this._insertOperation;
-		const nodeString = op.nodes.length > 1 ? `[ ${ op.nodes.length } ]` : op.nodes.getNode( 0 );
-
-		return getClassName( this ) + `( ${ this.baseVersion } ): ${ nodeString } -> ${ op.position }`;
-	} );
-
-	sandbox.mock( MarkerDelta.prototype, 'toString', function() {
-		const op = this.operations[ 0 ];
-
-		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
-			`"${ op.name }": ${ op.oldRange } -> ${ op.newRange }`;
-	} );
-
-	sandbox.mock( MergeDelta.prototype, 'toString', function() {
-		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
-			( this.position ?
-				this.position.toString() :
-				`(move from ${ this.operations[ 0 ].sourcePosition })`
-			);
-	} );
-
-	sandbox.mock( MoveDelta.prototype, 'toString', function() {
-		const opStrings = [];
-
-		for ( const op of this.operations ) {
-			const range = ModelRange.createFromPositionAndShift( op.sourcePosition, op.howMany );
-
-			opStrings.push( `${ range } -> ${ op.targetPosition }` );
-		}
-
-		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
-			opStrings.join( '; ' );
-	} );
-
-	sandbox.mock( RenameDelta.prototype, 'toString', function() {
-		const op = this.operations[ 0 ];
-
-		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
-			`${ op.position }: "${ op.oldName }" -> "${ op.newName }"`;
-	} );
-
-	sandbox.mock( RootAttributeDelta.prototype, 'toString', function() {
-		const op = this.operations[ 0 ];
-
-		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
-			`"${ op.key }": ${ JSON.stringify( op.oldValue ) } -> ${ JSON.stringify( op.newValue ) }, ${ op.root.rootName }`;
-	} );
-
-	sandbox.mock( SplitDelta.prototype, 'toString', function() {
-		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
-			( this.position ?
-				this.position.toString() :
-				`(clone to ${ this._cloneOperation.position || this._cloneOperation.targetPosition })`
-			);
-	} );
-
-	sandbox.mock( UnwrapDelta.prototype, 'toString', function() {
-		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
-			this.position.toString();
-	} );
-
-	sandbox.mock( WrapDelta.prototype, 'toString', function() {
-		const wrapElement = this._insertOperation.nodes.getNode( 0 );
-
-		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
-			`${ this.range } -> ${ wrapElement }`;
-	} );
-
 	sandbox.mock( ViewText.prototype, 'toString', function() {
 		return `#${ this.data }`;
 	} );
@@ -569,30 +442,25 @@ function enableReplayerTools() {
 	const _modelApplyOperation = Model.prototype.applyOperation;
 
 	sandbox.mock( Model.prototype, 'applyOperation', function( operation ) {
-		if ( !this._lastDelta ) {
-			this._appliedDeltas = [];
-		} else if ( this._lastDelta !== operation.delta ) {
-			this._appliedDeltas.push( this._lastDelta.toJSON() );
+		if ( !this._appliedOperations ) {
+			this._appliedOperations = [];
 		}
 
-		this._lastDelta = operation.delta;
+		this._appliedOperations.push( operation.toJSON() );
 
 		return _modelApplyOperation.call( this, operation );
 	} );
 
-	sandbox.mock( Model.prototype, 'getAppliedDeltas', function() {
-		// No deltas has been applied yet, return empty string.
-		if ( !this._lastDelta ) {
+	sandbox.mock( Model.prototype, 'getAppliedOperations', function() {
+		if ( !this._appliedOperations ) {
 			return '';
 		}
 
-		const appliedDeltas = this._appliedDeltas.concat( this._lastDelta );
-
-		return appliedDeltas.map( JSON.stringify ).join( LOG_SEPARATOR );
+		return this._appliedOperations.map( JSON.stringify ).join( LOG_SEPARATOR );
 	} );
 
-	sandbox.mock( Model.prototype, 'createReplayer', function( stringifiedDeltas ) {
-		return new DeltaReplayer( this, LOG_SEPARATOR, stringifiedDeltas );
+	sandbox.mock( Model.prototype, 'createReplayer', function( stringifiedOperations ) {
+		return new OperationReplayer( this, LOG_SEPARATOR, stringifiedOperations );
 	} );
 }
 
@@ -694,8 +562,8 @@ function dumpTrees( document, version ) {
 	}
 }
 
-// Helper function, returns the class name of a given `Delta` or `Operation`.
-// @param {module:engine/model/delta/delta~Delta|module:engine/model/operation/operation~Operation}
+// Helper function, returns the class name of a given `Operation`.
+// @param {module:engine/model/operation/operation~Operation}
 // @returns {String} Class name.
 function getClassName( obj ) {
 	const path = obj.constructor.className.split( '.' );

+ 24 - 5
packages/ckeditor5-engine/src/dev-utils/model.js

@@ -32,7 +32,7 @@ import {
 	convertRangeSelection,
 	convertCollapsedSelection,
 } from '../conversion/downcast-selection-converters';
-import { insertText, insertElement, wrap } from '../conversion/downcast-converters';
+import { insertText, insertElement, wrap, insertUIElement } from '../conversion/downcast-converters';
 
 import isPlainObject from '@ckeditor/ckeditor5-utils/src/lib/lodash/isPlainObject';
 import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
@@ -50,6 +50,7 @@ import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
  * not be included in the returned string.
  * @param {String} [options.rootName='main'] The name of the root from which the data should be stringified. If not provided,
  * the default `main` name will be used.
+ * @param {Boolean} [options.convertMarkers=false] Whether to include markers in the returned string.
  * @returns {String} The stringified data.
  */
 export function getData( model, options = {} ) {
@@ -57,11 +58,14 @@ export function getData( model, options = {} ) {
 		throw new TypeError( 'Model needs to be an instance of module:engine/model/model~Model.' );
 	}
 
-	const withoutSelection = options.withoutSelection;
 	const rootName = options.rootName || 'main';
 	const root = model.document.getRoot( rootName );
 
-	return withoutSelection ? getData._stringify( root ) : getData._stringify( root, model.document.selection );
+	return getData._stringify(
+		root,
+		options.withoutSelection ? null : model.document.selection,
+		options.convertMarkers ? model.markers : null
+	);
 }
 
 // Set stringify as getData private method - needed for testing/spying.
@@ -155,9 +159,10 @@ setData._parse = parse;
  * A selection instance whose ranges will be included in the returned string data. If a range instance is provided, it will be
  * converted to a selection containing this range. If a position instance is provided, it will be converted to a selection
  * containing one range collapsed at this position.
+ * @param {Iterable.<module:engine/model/markercollection~Marker>|null} markers Markers to include.
  * @returns {String} An HTML-like string representing the model.
  */
-export function stringify( node, selectionOrPositionOrRange = null ) {
+export function stringify( node, selectionOrPositionOrRange = null, markers = null ) {
 	const model = new Model();
 	const mapper = new Mapper();
 	let selection, range;
@@ -227,6 +232,11 @@ export function stringify( node, selectionOrPositionOrRange = null ) {
 	} ) );
 	downcastDispatcher.on( 'selection', convertRangeSelection() );
 	downcastDispatcher.on( 'selection', convertCollapsedSelection() );
+	downcastDispatcher.on( 'addMarker', insertUIElement( ( data, writer ) => {
+		const name = data.markerName + ':' + ( data.isOpening ? 'start' : 'end' );
+
+		return writer.createUIElement( name );
+	} ) );
 
 	// Convert model to view.
 	const writer = view._writer;
@@ -234,7 +244,16 @@ export function stringify( node, selectionOrPositionOrRange = null ) {
 
 	// Convert model selection to view selection.
 	if ( selection ) {
-		downcastDispatcher.convertSelection( selection, model.markers, writer );
+		downcastDispatcher.convertSelection( selection, markers || model.markers, writer );
+	}
+
+	if ( markers ) {
+		// To provide stable results, sort markers by name.
+		markers = Array.from( markers ).sort( ( a, b ) => a.name < b.name );
+
+		for ( const marker of markers ) {
+			downcastDispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer );
+		}
 	}
 
 	// Parse view to data string.

+ 140 - 0
packages/ckeditor5-engine/src/dev-utils/operationreplayer.js

@@ -0,0 +1,140 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/dev-utils/replayer
+ */
+
+/* global setTimeout */
+
+import OperationFactory from '../model/operation/operationfactory';
+
+/**
+ * Operation replayer is a development tool created for easy replaying of operations on the document from stringified operations.
+ */
+export default class OperationReplayer {
+	/**
+	 * @param {module:engine/model/model~Model} model Data model.
+	 * @param {String} logSeparator Separator between operations.
+	 * @param {String} stringifiedOperations Operations to replay.
+	 */
+	constructor( model, logSeparator, stringifiedOperations ) {
+		this._model = model;
+		this._logSeparator = logSeparator;
+		this.setStringifiedOperations( stringifiedOperations );
+	}
+
+	/**
+	 * Parses the given string containing stringified operations and sets parsed operations as operations to replay.
+	 *
+	 * @param {String} stringifiedOperations Stringified operations to replay.
+	 */
+	setStringifiedOperations( stringifiedOperations ) {
+		if ( stringifiedOperations === '' ) {
+			this._operationsToReplay = [];
+
+			return;
+		}
+
+		this._operationsToReplay = stringifiedOperations
+			.split( this._logSeparator )
+			.map( stringifiedOperation => JSON.parse( stringifiedOperation ) );
+	}
+
+	/**
+	 * Returns operations to replay.
+	 *
+	 * @returns {Array.<module:engine/model/operation/operation~Operation>}
+	 */
+	getOperationsToReplay() {
+		return this._operationsToReplay;
+	}
+
+	/**
+	 * Applies all operations with a delay between actions.
+	 *
+	 * @param {Number} timeInterval Time between applying operations.
+	 * @returns {Promise}
+	 */
+	play( timeInterval = 1000 ) {
+		const operationReplayer = this; // eslint-disable-line consistent-this
+
+		return new Promise( ( res, rej ) => {
+			play();
+
+			function play() {
+				operationReplayer.applyNextOperation().then( isFinished => {
+					if ( isFinished ) {
+						return res();
+					}
+
+					setTimeout( play, timeInterval );
+				} ).catch( err => {
+					rej( err );
+				} );
+			}
+		} );
+	}
+
+	/**
+	 * Applies `numberOfOperations` operations, beginning after the last applied operation (or first, if no operations were applied).
+	 *
+	 * @param {Number} numberOfOperations The number of operations to apply.
+	 * @returns {Promise}
+	 */
+	applyOperations( numberOfOperations ) {
+		if ( numberOfOperations <= 0 ) {
+			return;
+		}
+
+		return this.applyNextOperation()
+			.then( isFinished => {
+				if ( !isFinished ) {
+					return this.applyOperations( numberOfOperations - 1 );
+				}
+			} );
+	}
+
+	/**
+	 * Applies all operations to replay at once.
+	 *
+	 * @returns {Promise}
+	 */
+	applyAllOperations() {
+		return this.applyNextOperation()
+			.then( isFinished => {
+				if ( !isFinished ) {
+					return this.applyAllOperations();
+				}
+			} );
+	}
+
+	/**
+	 * Applies the next operation to replay. Returns a promise with the `isFinished` parameter that is `true` if the last
+	 * operation in the replayer has been applied, `false` otherwise.
+	 *
+	 * @returns {Promise.<Boolean>}
+	 */
+	applyNextOperation() {
+		const model = this._model;
+
+		return new Promise( res => {
+			model.enqueueChange( writer => {
+				const operationJson = this._operationsToReplay.shift();
+
+				if ( !operationJson ) {
+					return res( true );
+				}
+
+				const operation = OperationFactory.fromJSON( operationJson, model.document );
+
+				writer.batch.addOperation( operation );
+				model.applyOperation( operation );
+
+				res( false );
+			} );
+		} );
+	}
+}

+ 18 - 30
packages/ckeditor5-engine/src/model/batch.js

@@ -8,9 +8,9 @@
  */
 
 /**
- * A batch instance groups model changes ({@link module:engine/model/delta/delta~Delta deltas}). All deltas grouped in a single batch
- * can be reverted together, so you can think about a batch as of a single undo step. If you want to extend a given undo step, you
- * can add more changes to the batch using {@link module:engine/model/model~Model#enqueueChange}:
+ * A batch instance groups model changes ({@link module:engine/model/operation/operation~Operation operations}). All operations
+ * grouped in a single batch can be reverted together, so you can also think about a batch as of a single undo step. If you want
+ * to extend a given undo step, you can add more changes to the batch using {@link module:engine/model/model~Model#enqueueChange}:
  *
  *		model.enqueueChange( batch, writer => {
  *			writer.insertText( 'foo', paragraph, 'end' );
@@ -29,12 +29,12 @@ export default class Batch {
 	 */
 	constructor( type = 'default' ) {
 		/**
-		 * An array of deltas that compose this batch.
+		 * An array of operations that compose this batch.
 		 *
 		 * @readonly
-		 * @type {Array.<module:engine/model/delta/delta~Delta>}
+		 * @type {Array.<module:engine/model/operation/operation~Operation>}
 		 */
-		this.deltas = [];
+		this.operations = [];
 
 		/**
 		 * The type of the batch.
@@ -51,16 +51,16 @@ export default class Batch {
 	}
 
 	/**
-	 * Returns the base version of this batch, which is equal to the base version of the first delta (which has the base version set)
-	 * in the batch. If there are no deltas in the batch or neither delta has the base version set, it returns `null`.
+	 * Returns the base version of this batch, which is equal to the base version of the first operation in the batch.
+	 * If there are no operations in the batch or neither operation has the base version set, it returns `null`.
 	 *
 	 * @readonly
 	 * @type {Number|null}
 	 */
 	get baseVersion() {
-		for ( const delta of this.deltas ) {
-			if ( delta.baseVersion !== null ) {
-				return delta.baseVersion;
+		for ( const op of this.operations ) {
+			if ( op.baseVersion !== null ) {
+				return op.baseVersion;
 			}
 		}
 
@@ -68,27 +68,15 @@ export default class Batch {
 	}
 
 	/**
-	 * Adds a delta to the batch instance. All modification methods (insert, remove, split, etc.) use this method
-	 * to add created deltas.
+	 * Adds an operation to the batch instance.
 	 *
-	 * @param {module:engine/model/delta/delta~Delta} delta A delta to add.
-	 * @returns {module:engine/model/delta/delta~Delta} An added delta.
+	 * @param {module:engine/model/operation/operation~Operation} operation An operation to add.
+	 * @returns {module:engine/model/operation/operation~Operation} The added operation.
 	 */
-	addDelta( delta ) {
-		delta.batch = this;
-		this.deltas.push( delta );
+	addOperation( operation ) {
+		operation.batch = this;
+		this.operations.push( operation );
 
-		return delta;
-	}
-
-	/**
-	 * Gets an iterable collection of operations.
-	 *
-	 * @returns {Iterable.<module:engine/model/operation/operation~Operation>}
-	 */
-	* getOperations() {
-		for ( const delta of this.deltas ) {
-			yield* delta.operations;
-		}
+		return operation;
 	}
 }

+ 0 - 111
packages/ckeditor5-engine/src/model/delta/attributedelta.js

@@ -1,111 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/model/delta/attributedelta
- */
-
-import Delta from './delta';
-import DeltaFactory from './deltafactory';
-import NoOperation from '../operation/nooperation';
-import Range from '../range';
-
-/**
- * To provide specific OT behavior and better collisions solving, methods to change attributes
- * ({@link module:engine/model/writer~Writer#setAttribute} and {@link module:engine/model/writer~Writer#removeAttribute})
- * use `AttributeDelta` class which inherits from the `Delta` class and may overwrite some methods.
- *
- * @extends module:engine/model/delta/delta~Delta
- */
-export default class AttributeDelta extends Delta {
-	/**
-	 * @inheritDoc
-	 */
-	get type() {
-		return 'attribute';
-	}
-
-	/**
-	 * The attribute key that is changed by the delta or `null` if the delta has no operations.
-	 *
-	 * @readonly
-	 * @type {String|null}
-	 */
-	get key() {
-		return this.operations[ 0 ] ? this.operations[ 0 ].key : null;
-	}
-
-	/**
-	 * The attribute value that is set by the delta or `null` if the delta has no operations.
-	 *
-	 * @readonly
-	 * @type {*|null}
-	 */
-	get value() {
-		return this.operations[ 0 ] ? this.operations[ 0 ].newValue : null;
-	}
-
-	/**
-	 * The range on which delta operates or `null` if the delta has no operations.
-	 *
-	 * @readonly
-	 * @type {module:engine/model/range~Range|null}
-	 */
-	get range() {
-		// Check if it is cached.
-		if ( this._range ) {
-			return this._range;
-		}
-
-		let start = null;
-		let end = null;
-
-		for ( const operation of this.operations ) {
-			if ( operation instanceof NoOperation ) {
-				continue;
-			}
-
-			if ( start === null || start.isAfter( operation.range.start ) ) {
-				start = operation.range.start;
-			}
-
-			if ( end === null || end.isBefore( operation.range.end ) ) {
-				end = operation.range.end;
-			}
-		}
-
-		if ( start && end ) {
-			this._range = new Range( start, end );
-
-			return this._range;
-		}
-
-		return null;
-	}
-
-	get _reverseDeltaClass() {
-		return AttributeDelta;
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	toJSON() {
-		const json = super.toJSON();
-
-		delete json._range;
-
-		return json;
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	static get className() {
-		return 'engine.model.delta.AttributeDelta';
-	}
-}
-
-DeltaFactory.register( AttributeDelta );

+ 0 - 23
packages/ckeditor5-engine/src/model/delta/basic-deltas.js

@@ -1,23 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/model/delta/basic-deltas
- */
-
-// Deltas require `register` method that require `Batch` class and is defined in batch-base.js.
-// We would like to group all deltas files in one place, so we would only have to include batch.js
-// which would already have all default deltas registered.
-
-// Import default suite of deltas so a feature have to include only Batch class file.
-import './attributedelta';
-import './mergedelta';
-import './movedelta';
-import './removedelta';
-import './renamedelta';
-import './splitdelta';
-import './unwrapdelta';
-import './weakinsertdelta';
-import './wrapdelta';

+ 0 - 627
packages/ckeditor5-engine/src/model/delta/basic-transformations.js

@@ -1,627 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/model/delta/basic-transformations
- */
-
-import deltaTransform from './transform';
-const addTransformationCase = deltaTransform.addTransformationCase;
-const defaultTransform = deltaTransform.defaultTransform;
-
-import Range from '../range';
-import Position from '../position';
-
-import NoOperation from '../operation/nooperation';
-import AttributeOperation from '../operation/attributeoperation';
-import InsertOperation from '../operation/insertoperation';
-import ReinsertOperation from '../operation/reinsertoperation';
-
-import Delta from './delta';
-import AttributeDelta from './attributedelta';
-import InsertDelta from './insertdelta';
-import MarkerDelta from './markerdelta';
-import MergeDelta from './mergedelta';
-import MoveDelta from './movedelta';
-import SplitDelta from './splitdelta';
-import WeakInsertDelta from './weakinsertdelta';
-import WrapDelta from './wrapdelta';
-import UnwrapDelta from './unwrapdelta';
-import RenameDelta from './renamedelta';
-import RemoveDelta from './removedelta';
-
-import compareArrays from '@ckeditor/ckeditor5-utils/src/comparearrays';
-
-// Provide transformations for default deltas.
-
-// Add special case for AttributeDelta x WeakInsertDelta transformation.
-addTransformationCase( AttributeDelta, WeakInsertDelta, ( a, b, context ) => {
-	// If nodes are weak-inserted into attribute delta range, we need to apply changes from attribute delta on them.
-	// So first we do the normal transformation and if this special cases happens, we will add an extra delta.
-	const deltas = defaultTransform( a, b, context );
-
-	if ( a.range.containsPosition( b.position ) ) {
-		deltas.push( _getComplementaryAttrDelta( b, a ) );
-	}
-
-	return deltas;
-} );
-
-// Add special case for AttributeDelta x SplitDelta transformation.
-addTransformationCase( AttributeDelta, SplitDelta, ( a, b, context ) => {
-	// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
-	if ( !b.position ) {
-		return defaultTransform( a, b, context );
-	}
-
-	const undoMode = context.undoMode;
-	const splitPosition = new Position( b.position.root, b.position.path.slice( 0, -1 ) );
-
-	const deltas = defaultTransform( a, b, context );
-
-	// Special case applies only if undo is not a context and only if `SplitDelta` has `InsertOperation` (not `ReinsertOperation`).
-	if ( undoMode || !( b._cloneOperation instanceof InsertOperation ) ) {
-		return deltas;
-	}
-
-	for ( const operation of a.operations ) {
-		// If a node that has been split has it's attribute updated, we should also update attribute of
-		// the node created during splitting.
-		if ( operation.range.containsPosition( splitPosition ) || operation.range.start.isEqual( splitPosition ) ) {
-			const additionalAttributeDelta = new AttributeDelta();
-
-			const rangeStart = splitPosition.getShiftedBy( 1 );
-			const rangeEnd = Position.createFromPosition( rangeStart );
-			rangeEnd.path.push( 0 );
-
-			const oldValue = b._cloneOperation.nodes.getNode( 0 ).getAttribute( operation.key );
-
-			additionalAttributeDelta.addOperation( new AttributeOperation(
-				new Range( rangeStart, rangeEnd ),
-				operation.key,
-				oldValue === undefined ? null : oldValue,
-				operation.newValue,
-				0
-			) );
-
-			deltas.push( additionalAttributeDelta );
-
-			break;
-		}
-	}
-
-	return deltas;
-} );
-
-// Add special case for InsertDelta x MergeDelta transformation.
-addTransformationCase( InsertDelta, MergeDelta, ( a, b, context ) => {
-	// Do not apply special transformation case if `MergeDelta` has `NoOperation` as the second operation.
-	if ( !b.position ) {
-		return defaultTransform( a, b, context );
-	}
-
-	const undoMode = context.undoMode;
-
-	// If insert is applied at the same position where merge happened, we reverse the merge (we treat it like it
-	// didn't happen) and then apply the original insert operation. This is "mirrored" in MergeDelta x InsertDelta
-	// transformation below, where we simply do not apply MergeDelta.
-	if ( !undoMode && a.position.isEqual( b.position ) ) {
-		return [
-			b.getReversed(),
-			a.clone()
-		];
-	}
-
-	return defaultTransform( a, b, context );
-} );
-
-function transformMarkerDelta( a, b ) {
-	const transformedDelta = a.clone();
-	const transformedOp = transformedDelta.operations[ 0 ];
-
-	if ( transformedOp.oldRange ) {
-		transformedOp.oldRange = transformedOp.oldRange.getTransformedByDelta( b )[ 0 ];
-	}
-
-	if ( transformedOp.newRange ) {
-		transformedOp.newRange = transformedOp.newRange.getTransformedByDelta( b )[ 0 ];
-	}
-
-	return [ transformedDelta ];
-}
-
-addTransformationCase( MarkerDelta, SplitDelta, transformMarkerDelta );
-addTransformationCase( MarkerDelta, MergeDelta, transformMarkerDelta );
-addTransformationCase( MarkerDelta, WrapDelta, transformMarkerDelta );
-addTransformationCase( MarkerDelta, UnwrapDelta, transformMarkerDelta );
-addTransformationCase( MarkerDelta, MoveDelta, transformMarkerDelta );
-addTransformationCase( MarkerDelta, RenameDelta, transformMarkerDelta );
-
-// Add special case for MoveDelta x MergeDelta transformation.
-addTransformationCase( MoveDelta, MergeDelta, ( a, b, context ) => {
-	const undoMode = context.undoMode;
-
-	// Do not apply special transformation case in undo mode or if `MergeDelta` has `NoOperation` as the second operation.
-	if ( undoMode || !b.position ) {
-		return defaultTransform( a, b, context );
-	}
-
-	// If move delta is supposed to move a node that has been merged, we reverse the merge (we treat it like it
-	// didn't happen) and then apply the original move operation. This is "mirrored" in MergeDelta x MoveDelta
-	// transformation below, where we simply do not apply MergeDelta.
-	const operateInSameParent =
-		a.sourcePosition.root == b.position.root &&
-		compareArrays( a.sourcePosition.getParentPath(), b.position.getParentPath() ) === 'same';
-
-	const mergeInsideMoveRange = a.sourcePosition.offset <= b.position.offset && a.sourcePosition.offset + a.howMany > b.position.offset;
-
-	if ( operateInSameParent && mergeInsideMoveRange ) {
-		return [
-			b.getReversed(),
-			a.clone()
-		];
-	}
-
-	return defaultTransform( a, b, context );
-} );
-
-// Add special case for MergeDelta x InsertDelta transformation.
-addTransformationCase( MergeDelta, InsertDelta, ( a, b, context ) => {
-	// Do not apply special transformation case if `MergeDelta` has `NoOperation` as the second operation.
-	if ( !a.position ) {
-		return defaultTransform( a, b, context );
-	}
-
-	const undoMode = context.undoMode;
-
-	// If merge is applied at the same position where we inserted a range of nodes we cancel the merge as it's results
-	// may be unexpected and very weird. Even if we do some "magic" we don't know what really are users' expectations.
-	if ( !undoMode && a.position.isEqual( b.position ) ) {
-		return [ noDelta() ];
-	}
-
-	return defaultTransform( a, b, context );
-} );
-
-// Add special case for MergeDelta x MoveDelta transformation.
-addTransformationCase( MergeDelta, MoveDelta, ( a, b, context ) => {
-	const undoMode = context.undoMode;
-
-	// Do not apply special transformation case in undo mode or if `MergeDelta` has `NoOperation` as the second operation.
-	if ( undoMode || !a.position ) {
-		return defaultTransform( a, b, context );
-	}
-
-	// If merge is applied at the position between moved nodes we cancel the merge as it's results may be unexpected and
-	// very weird. Even if we do some "magic" we don't know what really are users' expectations.
-	const operateInSameParent =
-		a.position.root == b.sourcePosition.root &&
-		compareArrays( a.position.getParentPath(), b.sourcePosition.getParentPath() ) === 'same';
-
-	const mergeInsideMoveRange = b.sourcePosition.offset <= a.position.offset && b.sourcePosition.offset + b.howMany > a.position.offset;
-
-	if ( operateInSameParent && mergeInsideMoveRange ) {
-		return [ noDelta() ];
-	}
-
-	return defaultTransform( a, b, context );
-} );
-
-addTransformationCase( SplitDelta, SplitDelta, ( a, b, context ) => {
-	const undoMode = context.undoMode;
-
-	// Do not apply special transformation case if transformation is in undo mode.
-	if ( undoMode ) {
-		return defaultTransform( a, b, context );
-	}
-
-	// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
-	if ( !a.position || !b.position ) {
-		return defaultTransform( a, b, context );
-	}
-
-	const pathA = a.position.getParentPath();
-	const pathB = b.position.getParentPath();
-
-	// The special case is for splits inside the same parent.
-	if ( a.position.root == b.position.root && compareArrays( pathA, pathB ) == 'same' ) {
-		a = a.clone();
-
-		if ( a.position.offset < b.position.offset || ( a.position.offset == b.position.offset && context.isStrong ) ) {
-			// If both first operations are `ReinsertOperation`s, we might need to transform `a._cloneOperation`,
-			// so it will take correct node from graveyard.
-			if (
-				a._cloneOperation instanceof ReinsertOperation && b._cloneOperation instanceof ReinsertOperation &&
-				a._cloneOperation.sourcePosition.offset > b._cloneOperation.sourcePosition.offset
-			) {
-				a._cloneOperation.sourcePosition.offset--;
-			}
-
-			// `a` splits closer or at same offset.
-			// Change how many nodes are moved. Do not move nodes that were moved by delta `b`.
-			const aRange = Range.createFromPositionAndShift( a.position, a._moveOperation.howMany );
-			const bRange = Range.createFromPositionAndShift( b.position, b._moveOperation.howMany );
-
-			const diff = aRange.getDifference( bRange );
-
-			let newHowMany = 0;
-
-			for ( const range of diff ) {
-				newHowMany += range.end.offset - range.start.offset;
-			}
-
-			if ( newHowMany === 0 ) {
-				a.operations.pop(); // Remove last operation (`MoveOperation`).
-				a.addOperation( new NoOperation( a.operations[ 0 ].baseVersion + 1 ) ); // Add `NoOperation` instead.
-			} else {
-				a.operations[ 1 ].howMany = newHowMany;
-			}
-
-			return [ a ];
-		} else {
-			// `a` splits further.
-			// This is more complicated case, thankfully we can solve it using default transformation and setting proper context.
-			const newContext = Object.assign( {}, context );
-			newContext.isStrong = true;
-			newContext.insertBefore = true;
-
-			return defaultTransform( a, b, newContext );
-		}
-	}
-
-	return defaultTransform( a, b, context );
-} );
-
-// Add special case for SplitDelta x UnwrapDelta transformation.
-addTransformationCase( SplitDelta, UnwrapDelta, ( a, b, context ) => {
-	// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
-	if ( !a.position ) {
-		return defaultTransform( a, b, context );
-	}
-
-	// If incoming split delta tries to split a node that just got unwrapped, there is actually nothing to split,
-	// so we discard that delta.
-	if ( a.position.root == b.position.root && compareArrays( b.position.path, a.position.getParentPath() ) === 'same' ) {
-		return [ noDelta() ];
-	}
-
-	return defaultTransform( a, b, context );
-} );
-
-// Add special case for SplitDelta x WrapDelta transformation.
-addTransformationCase( SplitDelta, WrapDelta, ( a, b, context ) => {
-	// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
-	if ( !a.position ) {
-		return defaultTransform( a, b, context );
-	}
-
-	// If split is applied at the position between wrapped nodes, we cancel the split as it's results may be unexpected and
-	// very weird. Even if we do some "magic" we don't know what really are users' expectations.
-
-	const sameRoot = a.position.root == b.range.start.root;
-	const operateInSameParent = sameRoot && compareArrays( a.position.getParentPath(), b.range.start.getParentPath() ) === 'same';
-	const splitInsideWrapRange = b.range.start.offset < a.position.offset && b.range.end.offset >= a.position.offset;
-
-	if ( operateInSameParent && splitInsideWrapRange ) {
-		return [ noDelta() ];
-	} else if ( sameRoot && compareArrays( a.position.getParentPath(), b.range.end.getShiftedBy( -1 ).path ) === 'same' ) {
-		// Split position is directly inside the last node from wrap range.
-		// If that's the case, we manually change split delta so it will "target" inside the wrapping element.
-		// By doing so we will be inserting split node right to the original node which feels natural and is a good UX.
-		const delta = a.clone();
-
-		// 1. Fix insert operation position.
-		// Node to split is the last children of the wrapping element.
-		// Wrapping element is the element inserted by WrapDelta (re)insert operation.
-		// It is inserted after the wrapped range, but the wrapped range will be moved inside it.
-		// Having this in mind, it is correct to use wrapped range start position as the position before wrapping element.
-		const splitNodePos = Position.createFromPosition( b.range.start );
-		// Now, `splitNodePos` points before wrapping element.
-		// To get a position before last children of that element, we expand position's `path` member by proper offset.
-		splitNodePos.path.push( b.howMany - 1 );
-
-		// SplitDelta insert operation position should be right after the node we split.
-		const insertPos = splitNodePos.getShiftedBy( 1 );
-		delta._cloneOperation.position = insertPos;
-
-		// 2. Fix move operation source position.
-		// Nodes moved by SplitDelta will be moved from new position, modified by WrapDelta.
-		// To obtain that new position, `splitNodePos` will be used, as this is the node we are extracting children from.
-		const sourcePos = Position.createFromPosition( splitNodePos );
-		// Nothing changed inside split node so it is correct to use the original split position offset.
-		sourcePos.path.push( a.position.offset );
-		delta._moveOperation.sourcePosition = sourcePos;
-
-		// 3. Fix move operation target position.
-		// SplitDelta move operation target position should be inside the node inserted by operation above.
-		// Since the node is empty, we will insert at offset 0.
-		const targetPos = Position.createFromPosition( insertPos );
-		targetPos.path.push( 0 );
-		delta._moveOperation.targetPosition = targetPos;
-
-		return [ delta ];
-	}
-
-	return defaultTransform( a, b, context );
-} );
-
-// Add special case for SplitDelta x WrapDelta transformation.
-addTransformationCase( SplitDelta, AttributeDelta, ( a, b, context ) => {
-	// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
-	if ( !a.position ) {
-		return defaultTransform( a, b, context );
-	}
-
-	a = a.clone();
-
-	const undoMode = context.undoMode;
-	const splitPosition = new Position( a.position.root, a.position.path.slice( 0, -1 ) );
-
-	// Special case applies only if undo is not a context and only if `SplitDelta` has `InsertOperation` (not `ReinsertOperation`).
-	if ( undoMode || !( a._cloneOperation instanceof InsertOperation ) ) {
-		return [ a ];
-	}
-
-	// If element to split had it's attribute changed, we have to reflect this change in an element
-	// that is in SplitDelta's InsertOperation.
-	for ( const operation of b.operations ) {
-		if ( operation.range.containsPosition( splitPosition ) || operation.range.start.isEqual( splitPosition ) ) {
-			if ( operation.newValue !== null ) {
-				a._cloneOperation.nodes.getNode( 0 )._setAttribute( operation.key, operation.newValue );
-			} else {
-				a._cloneOperation.nodes.getNode( 0 )._removeAttribute( operation.key );
-			}
-
-			break;
-		}
-	}
-
-	return [ a ];
-} );
-
-// Add special case for UnwrapDelta x SplitDelta transformation.
-addTransformationCase( UnwrapDelta, SplitDelta, ( a, b, context ) => {
-	// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
-	if ( !b.position ) {
-		return defaultTransform( a, b, context );
-	}
-
-	// If incoming unwrap delta tries to unwrap node that got split we should unwrap the original node and the split copy.
-	// This can be achieved either by reverting split and applying unwrap to singular node, or creating additional unwrap delta.
-	if ( a.position.root == b.position.root && compareArrays( a.position.path, b.position.getParentPath() ) === 'same' ) {
-		return [
-			b.getReversed(),
-			a.clone()
-		];
-	}
-
-	return defaultTransform( a, b, context );
-} );
-
-// Add special case for WeakInsertDelta x AttributeDelta transformation.
-addTransformationCase( WeakInsertDelta, AttributeDelta, ( a, b ) => {
-	// If nodes are weak-inserted into attribute delta range, we need to apply changes from attribute delta on them.
-	const deltas = [ a.clone() ];
-
-	if ( b.range.containsPosition( a.position ) ) {
-		deltas.push( _getComplementaryAttrDelta( a, b ) );
-	}
-
-	return deltas;
-} );
-
-// Add special case for WrapDelta x SplitDelta transformation.
-addTransformationCase( WrapDelta, SplitDelta, ( a, b, context ) => {
-	// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
-	if ( !b.position ) {
-		return defaultTransform( a, b, context );
-	}
-
-	// If incoming wrap delta tries to wrap range that contains split position, we have to cancel the split and apply
-	// the wrap. Since split was already applied, we have to revert it.
-	const sameRoot = a.range.start.root == b.position.root;
-	const operateInSameParent = sameRoot && compareArrays( a.range.start.getParentPath(), b.position.getParentPath() ) === 'same';
-	const splitInsideWrapRange = a.range.start.offset < b.position.offset && a.range.end.offset >= b.position.offset;
-
-	if ( operateInSameParent && splitInsideWrapRange ) {
-		return [
-			b.getReversed(),
-			a.clone()
-		];
-	} else if ( sameRoot && compareArrays( b.position.getParentPath(), a.range.end.getShiftedBy( -1 ).path ) === 'same' ) {
-		const delta = a.clone();
-
-		// Move wrapping element insert position one node further so it is after the split node insertion.
-		delta._insertOperation.position.offset++;
-
-		// Include the split node copy.
-		delta._moveOperation.howMany++;
-
-		// Change the path to wrapping element in move operation.
-		delta._moveOperation.targetPosition.path[ delta._moveOperation.targetPosition.path.length - 2 ]++;
-
-		return [ delta ];
-	}
-
-	return defaultTransform( a, b, context );
-} );
-
-// Add special case for RenameDelta x SplitDelta transformation.
-addTransformationCase( RenameDelta, SplitDelta, ( a, b, context ) => {
-	const undoMode = context.undoMode;
-	const deltas = defaultTransform( a, b, context );
-
-	// Special case applies only if undo is not a context and only if `SplitDelta` has `InsertOperation` (not `ReinsertOperation`).
-	if ( undoMode || !( b._cloneOperation instanceof InsertOperation ) ) {
-		return deltas;
-	}
-
-	const insertPosition = b._cloneOperation.position.getShiftedBy( -1 );
-
-	if ( insertPosition && a.operations[ 0 ].position.isEqual( insertPosition ) ) {
-		// If a node that has been split has it's name changed, we should also change name of
-		// the node created during splitting.
-		const additionalRenameDelta = a.clone();
-		additionalRenameDelta.operations[ 0 ].position = insertPosition.getShiftedBy( 1 );
-
-		deltas.push( additionalRenameDelta );
-	}
-
-	return deltas;
-} );
-
-// Add special case for SplitDelta x RenameDelta transformation.
-addTransformationCase( SplitDelta, RenameDelta, ( a, b, context ) => {
-	a = a.clone();
-
-	const undoMode = context.undoMode;
-
-	// Special case applies only if undo is not a context and only if `SplitDelta` has `InsertOperation` (not `ReinsertOperation`).
-	if ( undoMode || !( a._cloneOperation instanceof InsertOperation ) ) {
-		return [ a ];
-	}
-
-	const insertPosition = a._cloneOperation.position.getShiftedBy( -1 );
-
-	// If element to split had it's name changed, we have to reflect this by creating additional rename operation.
-	if ( insertPosition && !undoMode && b.operations[ 0 ].position.isEqual( insertPosition ) ) {
-		const additionalRenameDelta = b.clone();
-		additionalRenameDelta.operations[ 0 ].position = insertPosition.getShiftedBy( 1 );
-		additionalRenameDelta.operations[ 0 ].oldName = a._cloneOperation.nodes.getNode( 0 ).name;
-
-		return [ a, additionalRenameDelta ];
-	}
-
-	return [ a ];
-} );
-
-// Add special case for RemoveDelta x SplitDelta transformation.
-addTransformationCase( RemoveDelta, SplitDelta, ( a, b, context ) => {
-	const deltas = defaultTransform( a, b, context );
-	// The "clone operation" may be InsertOperation, ReinsertOperation, MoveOperation or NoOperation.
-	const insertPosition = b._cloneOperation.position || b._cloneOperation.targetPosition;
-
-	// NoOperation.
-	if ( !insertPosition ) {
-		return defaultTransform( a, b, context );
-	}
-
-	const undoMode = context.undoMode;
-
-	// Special case applies only if undo is not a context.
-	if ( undoMode ) {
-		return deltas;
-	}
-
-	// In case if `defaultTransform` returned more than one delta.
-	for ( const delta of deltas ) {
-		// "No delta" may be returned in some cases.
-		if ( delta instanceof RemoveDelta ) {
-			const operation = delta._moveOperation;
-			const rangeEnd = operation.sourcePosition.getShiftedBy( operation.howMany );
-
-			if ( rangeEnd.isEqual( insertPosition ) ) {
-				operation.howMany += 1;
-			}
-		}
-	}
-
-	return deltas;
-} );
-
-// Add special case for SplitDelta x RemoveDelta transformation.
-addTransformationCase( SplitDelta, RemoveDelta, ( a, b, context ) => {
-	const undoMode = context.undoMode;
-
-	// Special case applies only if undo is not a context.
-	if ( undoMode ) {
-		return defaultTransform( a, b, context );
-	}
-
-	// This case is very trickily solved.
-	// Instead of fixing `a` delta, we change `b` delta for a while and fire default transformation with fixed `b` delta.
-	// Thanks to that fixing `a` delta will be differently (correctly) transformed.
-	//
-	// The "clone operation" may be InsertOperation, ReinsertOperation, MoveOperation or NoOperation.
-	const insertPosition = a._cloneOperation.position || a._cloneOperation.targetPosition;
-
-	// NoOperation.
-	if ( !insertPosition ) {
-		return defaultTransform( a, b, context );
-	}
-
-	b = b.clone();
-	const operation = b._moveOperation;
-	const rangeEnd = operation.sourcePosition.getShiftedBy( operation.howMany );
-
-	if ( rangeEnd.isEqual( insertPosition ) ) {
-		operation.howMany += 1;
-	}
-
-	return defaultTransform( a, b, context );
-} );
-
-// Helper function for `AttributeDelta` class transformations.
-// Creates an attribute delta that sets attribute from given `attributeDelta` on nodes from given `weakInsertDelta`.
-function _getComplementaryAttrDelta( weakInsertDelta, attributeDelta ) {
-	const complementaryAttrDelta = new AttributeDelta();
-	const nodes = weakInsertDelta.nodes;
-
-	// At the beginning we store the attribute value from the first node on `weakInsertDelta` node list.
-	let val = nodes.getNode( 0 ).getAttribute( attributeDelta.key );
-
-	// This stores the last index of `weakInsertDelta` node list where the attribute value was different
-	// than in the previous node. We need it to create separate `AttributeOperation`s for nodes with different attributes.
-	let lastOffset = 0;
-	// Sum of offsets of already processed nodes.
-	let offsetSum = nodes.getNode( 0 ).offsetSize;
-
-	for ( let i = 1; i < nodes.length; i++ ) {
-		const node = nodes.getNode( i );
-		const nodeAttrVal = node.getAttribute( attributeDelta.key );
-
-		// If previous node has different attribute value, we will create an operation to the point before current node.
-		// So all nodes with the same attributes up to this point will be included in one `AttributeOperation`.
-		if ( nodeAttrVal != val ) {
-			// New operation is created only when it is needed. If given node already has proper value for this
-			// attribute we simply skip it without adding a new operation.
-			if ( val != attributeDelta.value ) {
-				addOperation();
-			}
-
-			val = nodeAttrVal;
-			lastOffset = offsetSum;
-		}
-
-		offsetSum = offsetSum + node.offsetSize;
-	}
-
-	// At the end we have to add additional `AttributeOperation` for the last part of node list. If all nodes on the
-	// node list had same attributes, this will be the only operation added to the delta.
-	addOperation();
-
-	return complementaryAttrDelta;
-
-	function addOperation() {
-		const range = new Range(
-			weakInsertDelta.position.getShiftedBy( lastOffset ),
-			weakInsertDelta.position.getShiftedBy( offsetSum )
-		);
-
-		const attrOperation = new AttributeOperation( range, attributeDelta.key, val, attributeDelta.value, 0 );
-		complementaryAttrDelta.addOperation( attrOperation );
-	}
-}
-
-// This is "no-op" delta, it has no type and only no-operation, it basically does nothing.
-// It is used when we don't want to apply changes but still we need to return a delta.
-function noDelta() {
-	const noDelta = new Delta();
-
-	// BaseVersion will be fixed later anyway.
-	noDelta.addOperation( new NoOperation( 0 ) );
-
-	return noDelta;
-}

+ 0 - 167
packages/ckeditor5-engine/src/model/delta/delta.js

@@ -1,167 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/model/delta/delta
- */
-
-import clone from '@ckeditor/ckeditor5-utils/src/lib/lodash/clone';
-import DeltaFactory from './deltafactory';
-
-/**
- * Base class for all deltas.
- *
- * Delta is a single, from the user action point of view, change in the editable document, like insert, split or
- * rename element. Delta is composed of operations, which are unit changes needed to be done to execute user action.
- *
- * Multiple deltas are grouped into a single {@link module:engine/model/batch~Batch}.
- */
-export default class Delta {
-	/**
-	 * Creates a delta instance.
-	 */
-	constructor() {
-		/**
-		 * {@link module:engine/model/batch~Batch} which delta is a part of. This property is null by default and set by the
-		 * {@link module:engine/model/batch~Batch#addDelta} method.
-		 *
-		 * @readonly
-		 * @member {module:engine/model/batch~Batch} module:engine/model/delta/delta~Delta#batch
-		 */
-		this.batch = null;
-
-		/**
-		 * Array of operations which compose delta.
-		 *
-		 * @readonly
-		 * @member {module:engine/model/operation/operation~Operation[]} module:engine/model/delta/delta~Delta#operations
-		 */
-		this.operations = [];
-	}
-
-	/**
-	 * Returns delta base version which is equal to the base version of the first operation in delta. If there
-	 * are no operations in delta, returns `null`.
-	 *
-	 * @see module:engine/model/document~Document
-	 * @type {Number|null}
-	 */
-	get baseVersion() {
-		if ( this.operations.length > 0 ) {
-			return this.operations[ 0 ].baseVersion;
-		}
-
-		return null;
-	}
-
-	/**
-	 * @param {Number} baseVersion
-	 */
-	set baseVersion( baseVersion ) {
-		for ( const operation of this.operations ) {
-			operation.baseVersion = baseVersion++;
-		}
-	}
-
-	/**
-	 * A class that will be used when creating reversed delta.
-	 *
-	 * @private
-	 * @type {Function}
-	 */
-	get _reverseDeltaClass() {
-		return Delta;
-	}
-
-	/**
-	 * Delta type.
-	 *
-	 * @readonly
-	 * @member {String} #type
-	 */
-
-	/**
-	 * Add operation to the delta.
-	 *
-	 * @param {module:engine/model/operation/operation~Operation} operation Operation instance.
-	 */
-	addOperation( operation ) {
-		operation.delta = this;
-		this.operations.push( operation );
-
-		return operation;
-	}
-
-	/**
-	 * Creates and returns a delta that has the same parameters as this delta.
-	 *
-	 * @returns {module:engine/model/delta/delta~Delta} Clone of this delta.
-	 */
-	clone() {
-		const delta = new this.constructor();
-
-		for ( const op of this.operations ) {
-			delta.addOperation( op.clone() );
-		}
-
-		return delta;
-	}
-
-	/**
-	 * Creates and returns a reverse delta. Reverse delta when executed right after the original delta will bring back
-	 * tree model state to the point before the original delta execution. In other words, it reverses changes done
-	 * by the original delta.
-	 *
-	 * Keep in mind that tree model state may change since executing the original delta, so reverse delta may be "outdated".
-	 * In that case you will need to {@link module:engine/model/delta/transform~transform} it by all deltas that were executed after
-	 * the original delta.
-	 *
-	 * @returns {module:engine/model/delta/delta~Delta} Reversed delta.
-	 */
-	getReversed() {
-		const delta = new this._reverseDeltaClass();
-
-		for ( const op of this.operations ) {
-			delta.addOperation( op.getReversed() );
-		}
-
-		delta.operations.reverse();
-
-		for ( let i = 0; i < delta.operations.length; i++ ) {
-			delta.operations[ i ].baseVersion = this.operations[ this.operations.length - 1 ].baseVersion + i + 1;
-		}
-
-		return delta;
-	}
-
-	/**
-	 * Custom toJSON method to make deltas serializable.
-	 *
-	 * @returns {Object} Clone of this delta with added class name.
-	 */
-	toJSON() {
-		const json = clone( this );
-
-		json.__className = this.constructor.className;
-
-		// Remove parent batch to avoid circular dependencies.
-		delete json.batch;
-
-		return json;
-	}
-
-	/**
-	 * Delta class name. Used by {@link #toJSON} method for serialization and
-	 * {@link module:engine/model/delta/deltafactory~DeltaFactory.fromJSON} during deserialization.
-	 *
-	 * @type {String}
-	 * @readonly
-	 */
-	static get className() {
-		return 'engine.model.delta.Delta';
-	}
-}
-
-DeltaFactory.register( Delta );

+ 0 - 72
packages/ckeditor5-engine/src/model/delta/deltafactory.js

@@ -1,72 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/model/delta/deltafactory
- */
-
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-
-import OperationFactory from '../operation/operationfactory';
-
-const deserializers = new Map();
-
-/**
- * A factory class for creating operations.
- *
- * Delta is a single, from the user action point of view, change in the editable document, like insert, split or
- * rename element. Delta is composed of operations, which are unit changes needed to be done to execute user action.
- *
- * Multiple deltas are grouped into a single {@link module:engine/model/batch~Batch}.
- */
-export default class DeltaFactory {
-	/**
-	 * Creates InsertDelta from deserialized object, i.e. from parsed JSON string.
-	 *
-	 * @param {Object} json
-	 * @param {module:engine/model/document~Document} doc Document on which this delta will be applied.
-	 * @returns {module:engine/model/delta/insertdelta~InsertDelta}
-	 */
-	static fromJSON( json, doc ) {
-		if ( !deserializers.has( json.__className ) ) {
-			/**
-			 * This delta has no defined deserializer.
-			 *
-			 * @error delta-fromjson-no-deserializer
-			 * @param {String} name
-			 */
-			throw new CKEditorError(
-				'delta-fromjson-no-deserializer: This delta has no defined deserializer',
-				{ name: json.__className }
-			);
-		}
-
-		const Delta = deserializers.get( json.__className );
-
-		const delta = new Delta();
-
-		for ( const operation of json.operations ) {
-			delta.addOperation( OperationFactory.fromJSON( operation, doc ) );
-		}
-
-		// Rewrite all other properties.
-		for ( const prop in json ) {
-			if ( prop != '__className' && delta[ prop ] === undefined ) {
-				delta[ prop ] = json[ prop ];
-			}
-		}
-
-		return delta;
-	}
-
-	/**
-	 * Registers a class for delta factory.
-	 *
-	 * @param {Function} Delta A delta class to register.
-	 */
-	static register( Delta ) {
-		deserializers.set( Delta.className, Delta );
-	}
-}

+ 0 - 74
packages/ckeditor5-engine/src/model/delta/insertdelta.js

@@ -1,74 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/model/delta/insertdelta
- */
-
-import Delta from './delta';
-import RemoveDelta from './removedelta';
-import DeltaFactory from './deltafactory';
-
-/**
- * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/writer~Writer#insert Batch#insert} method
- * uses the `InsertDelta` class which inherits from the `Delta` class and may overwrite some methods.
- *
- * @extends module:engine/model/delta/delta~Delta
- */
-export default class InsertDelta extends Delta {
-	/**
-	 * @inheritDoc
-	 */
-	get type() {
-		return 'insert';
-	}
-
-	/**
-	 * Position where the delta inserts nodes or `null` if there are no operations in the delta.
-	 *
-	 * @readonly
-	 * @type {module:engine/model/position~Position|null}
-	 */
-	get position() {
-		return this._insertOperation ? this._insertOperation.position : null;
-	}
-
-	/**
-	 * Node list containing all the nodes inserted by the delta or `null` if there are no operations in the delta.
-	 *
-	 * @readonly
-	 * @type {module:engine/model/nodelist~NodeList|null}
-	 */
-	get nodes() {
-		return this._insertOperation ? this._insertOperation.nodes : null;
-	}
-
-	/**
-	 * Insert operation that is saved in this delta or `null` if there are no operations in the delta.
-	 *
-	 * @readonly
-	 * @protected
-	 * @type {module:engine/model/operation/insertoperation~InsertOperation|null}
-	 */
-	get _insertOperation() {
-		return this.operations[ 0 ] || null;
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	get _reverseDeltaClass() {
-		return RemoveDelta;
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	static get className() {
-		return 'engine.model.delta.InsertDelta';
-	}
-}
-
-DeltaFactory.register( InsertDelta );

+ 0 - 46
packages/ckeditor5-engine/src/model/delta/markerdelta.js

@@ -1,46 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/model/delta/markerdelta
- */
-
-import Delta from './delta';
-import DeltaFactory from './deltafactory';
-
-/**
- * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/writer~Writer#addMarker Writer#addMarker}
- * and {@link module:engine/model/writer~Writer#removeMarker Writer#removeMarker} methods use the `MarkerDelta` class which inherits
- * from the `Delta` class and may overwrite some methods.
- *
- * @extends module:engine/model/delta/delta~Delta
- */
-export default class MarkerDelta extends Delta {
-	/**
-	 * @inheritDoc
-	 */
-	get type() {
-		return 'marker';
-	}
-
-	/**
-	 * A class that will be used when creating reversed delta.
-	 *
-	 * @private
-	 * @type {Function}
-	 */
-	get _reverseDeltaClass() {
-		return MarkerDelta;
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	static get className() {
-		return 'engine.model.delta.MarkerDelta';
-	}
-}
-
-DeltaFactory.register( MarkerDelta );

+ 0 - 67
packages/ckeditor5-engine/src/model/delta/mergedelta.js

@@ -1,67 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/model/delta/mergedelta
- */
-
-import Delta from './delta';
-import DeltaFactory from './deltafactory';
-import SplitDelta from './splitdelta';
-
-/**
- * To provide specific OT behavior and better collisions solving, {@link module:engine/model/writer~Writer#merge} method
- * uses the `MergeDelta` class which inherits from the `Delta` class and may overwrite some methods.
- *
- * @extends module:engine/model/delta/delta~Delta
- */
-export default class MergeDelta extends Delta {
-	/**
-	 * @inheritDoc
-	 */
-	get type() {
-		return 'merge';
-	}
-
-	/**
-	 * Position between to merged nodes or `null` if the delta has no operations.
-	 *
-	 * @readonly
-	 * @type {module:engine/model/position~Position|null}
-	 */
-	get position() {
-		return this._removeOperation ? this._removeOperation.sourcePosition : null;
-	}
-
-	/**
-	 * Operation in this delta that removes the node after merge position (which will be empty at that point) or
-	 * `null` if the delta has no operations. Note, that after {@link module:engine/model/delta/transform~transform transformation}
-	 * this might be an instance of {@link module:engine/model/operation/moveoperation~MoveOperation} instead of
-	 * {@link module:engine/model/operation/removeoperation~RemoveOperation}.
-	 *
-	 * @readonly
-	 * @protected
-	 * @type {module:engine/model/operation/moveoperation~MoveOperation|null}
-	 */
-	get _removeOperation() {
-		return this.operations[ 1 ] || null;
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	get _reverseDeltaClass() {
-		return SplitDelta;
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	static get className() {
-		return 'engine.model.delta.MergeDelta';
-	}
-}
-
-DeltaFactory.register( MergeDelta );

+ 0 - 84
packages/ckeditor5-engine/src/model/delta/movedelta.js

@@ -1,84 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/model/delta/movedelta
- */
-
-import Delta from './delta';
-import DeltaFactory from './deltafactory';
-
-/**
- * To provide specific OT behavior and better collisions solving, {@link module:engine/model/writer~Writer#move} method
- * uses the `MoveDelta` class which inherits from the `Delta` class and may overwrite some methods.
- *
- * @extends module:engine/model/delta/delta~Delta
- */
-export default class MoveDelta extends Delta {
-	/**
-	 * @inheritDoc
-	 */
-	get type() {
-		return 'move';
-	}
-
-	/**
-	 * Offset size of moved range or `null` if there are no operations in the delta.
-	 *
-	 * @type {Number|null}
-	 */
-	get howMany() {
-		return this._moveOperation ? this._moveOperation.howMany : null;
-	}
-
-	/**
-	 * {@link module:engine/model/delta/movedelta~MoveDelta#_moveOperation Move operation}
-	 * {@link module:engine/model/operation/moveoperation~MoveOperation#sourcePosition source position} or `null` if there are
-	 * no operations in the delta.
-	 *
-	 * @type {module:engine/model/position~Position|null}
-	 */
-	get sourcePosition() {
-		return this._moveOperation ? this._moveOperation.sourcePosition : null;
-	}
-
-	/**
-	 * {@link module:engine/model/delta/movedelta~MoveDelta#_moveOperation Move operation}
-	 * {@link module:engine/model/operation/moveoperation~MoveOperation#targetPosition target position} or `null` if there are
-	 * no operations in the delta.
-	 *
-	 * @type {module:engine/model/position~Position|null}
-	 */
-	get targetPosition() {
-		return this._moveOperation ? this._moveOperation.targetPosition : null;
-	}
-
-	/**
-	 * {@link module:engine/model/delta/movedelta~MoveDelta#_moveOperation Move operation} that is saved in this delta or `null`
-	 * if there are no operations in the delta.
-	 *
-	 * @protected
-	 * @type {module:engine/model/operation/moveoperation~MoveOperation|null}
-	 */
-	get _moveOperation() {
-		return this.operations[ 0 ] || null;
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	get _reverseDeltaClass() {
-		return MoveDelta;
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	static get className() {
-		return 'engine.model.delta.MoveDelta';
-	}
-}
-
-DeltaFactory.register( MoveDelta );

+ 0 - 28
packages/ckeditor5-engine/src/model/delta/removedelta.js

@@ -1,28 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/model/delta/removedelta
- */
-
-import MoveDelta from './movedelta';
-import DeltaFactory from './deltafactory';
-
-/**
- * To provide specific OT behavior and better collisions solving, {@link module:engine/model/writer~Writer#remove} method
- * uses the `RemoveDelta` class which inherits from the `Delta` class and may overwrite some methods.
- *
- * @extends module:engine/model/delta/delta~Delta
- */
-export default class RemoveDelta extends MoveDelta {
-	/**
-	 * @inheritDoc
-	 */
-	static get className() {
-		return 'engine.model.delta.RemoveDelta';
-	}
-}
-
-DeltaFactory.register( RemoveDelta );

+ 0 - 42
packages/ckeditor5-engine/src/model/delta/renamedelta.js

@@ -1,42 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/model/delta/renamedelta
- */
-
-import Delta from './delta';
-import DeltaFactory from './deltafactory';
-
-/**
- * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/writer~Writer#rename Batch#rename} method
- * uses the `RenameDelta` class which inherits from the `Delta` class and may overwrite some methods.
- *
- * @extends module:engine/model/delta/delta~Delta
- */
-export default class RenameDelta extends Delta {
-	/**
-	 * @inheritDoc
-	 */
-	get type() {
-		return 'rename';
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	get _reverseDeltaClass() {
-		return RenameDelta;
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	static get className() {
-		return 'engine.model.delta.RenameDelta';
-	}
-}
-
-DeltaFactory.register( RenameDelta );

+ 0 - 30
packages/ckeditor5-engine/src/model/delta/rootattributedelta.js

@@ -1,30 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/model/delta/rootattributedelta
- */
-
-import Delta from './delta';
-import DeltaFactory from './deltafactory';
-
-/**
- * To provide specific OT behavior and better collisions solving, methods to change attributes
- * ({@link module:engine/model/writer~Writer#setAttribute} and {@link module:engine/model/writer~Writer#removeAttribute})
- * use `RootAttributeDelta` class which inherits from the `Delta` class and may
- * overwrite some methods.
- *
- * @extends module:engine/model/delta/delta~Delta
- */
-export default class RootAttributeDelta extends Delta {
-	/**
-	 * @inheritDoc
-	 */
-	static get className() {
-		return 'engine.model.delta.RootAttributeDelta';
-	}
-}
-
-DeltaFactory.register( RootAttributeDelta );

+ 0 - 83
packages/ckeditor5-engine/src/model/delta/splitdelta.js

@@ -1,83 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/model/delta/splitdelta
- */
-
-import Delta from './delta';
-import DeltaFactory from './deltafactory';
-import MoveOperation from '../operation/moveoperation';
-import MergeDelta from '../delta/mergedelta';
-
-/**
- * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/writer~Writer#split} method
- * uses `SplitDelta` class which inherits from the `Delta` class and may overwrite some methods.
- *
- * @extends module:engine/model/delta/delta~Delta
- */
-export default class SplitDelta extends Delta {
-	/**
-	 * @inheritDoc
-	 */
-	get type() {
-		return 'split';
-	}
-
-	/**
-	 * Position of split or `null` if there are no operations in the delta.
-	 *
-	 * @type {module:engine/model/position~Position|null}
-	 */
-	get position() {
-		return this._moveOperation ? this._moveOperation.sourcePosition : null;
-	}
-
-	/**
-	 * Operation in the delta that adds to model an element into which split nodes will be moved, or `null` if
-	 * there are no operations in the delta.
-	 *
-	 * Most commonly this will be {@link module:engine/model/operation/insertoperation~InsertOperation an insert operation},
-	 * as `SplitDelta` has to create a new node. If `SplitDelta` was created through
-	 * {@link module:engine/model/delta/delta~Delta#getReversed reversing}
-	 * a {@link module:engine/model/delta/mergedelta~MergeDelta merge delta},
-	 * this will be a {@link module:engine/model/operation/reinsertoperation~ReinsertOperation reinsert operation},
-	 * as we will want to re-insert the exact element that was removed by that merge delta.
-	 *
-	 * @protected
-	 * @type {module:engine/model/operation/insertoperation~InsertOperation|
-	 * module:engine/model/operation/reinsertoperation~ReinsertOperation|null}
-	 */
-	get _cloneOperation() {
-		return this.operations[ 0 ] || null;
-	}
-
-	/**
-	 * Operation in the delta that moves model items, that are after split position, to their new parent or `null`
-	 * if there are no operations in the delta.
-	 *
-	 * @protected
-	 * @type {module:engine/model/operation/moveoperation~MoveOperation|null}
-	 */
-	get _moveOperation() {
-		return this.operations[ 1 ] && this.operations[ 1 ] instanceof MoveOperation ? this.operations[ 1 ] : null;
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	get _reverseDeltaClass() {
-		return MergeDelta;
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	static get className() {
-		return 'engine.model.delta.SplitDelta';
-	}
-}
-
-DeltaFactory.register( SplitDelta );

+ 0 - 664
packages/ckeditor5-engine/src/model/delta/transform.js

@@ -1,664 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @protected
- * @module engine/model/delta/transform
- */
-
-import Delta from './delta';
-import MoveDelta from './movedelta';
-import RemoveDelta from './removedelta';
-import MergeDelta from './mergedelta';
-import SplitDelta from './splitdelta';
-import WrapDelta from './wrapdelta';
-import UnwrapDelta from './unwrapdelta';
-import RenameDelta from './renamedelta';
-import AttributeDelta from './attributedelta';
-import operationTransform from '../operation/transform';
-import NoOperation from '../operation/nooperation';
-import MoveOperation from '../operation/moveoperation';
-import RemoveOperation from '../operation/removeoperation';
-import arrayUtils from '@ckeditor/ckeditor5-utils/src/lib/lodash/array';
-import compareArrays from '@ckeditor/ckeditor5-utils/src/comparearrays';
-
-const specialCases = new Map();
-
-/**
- * @namespace
- */
-const transform = {
-	/**
-	 * Transforms given {@link module:engine/model/delta/delta~Delta delta} by another {@link module:engine/model/delta/delta~Delta delta}
-	 * and returns the result of that transformation as an array containing one or more {@link module:engine/model/delta/delta~Delta delta}
-	 * instances.
-	 *
-	 * Delta transformations heavily base on {@link module:engine/model/operation/transform~transform operational transformations}. Since
-	 * delta is a list of operations most situations can be handled thanks to operational transformation. Unfortunately,
-	 * deltas are more complicated than operations and have they semantic meaning, as they represent user's editing intentions.
-	 *
-	 * Sometimes, simple operational transformation on deltas' operations might result in some unexpected results. Those
-	 * results would be fine from OT point of view, but would not reflect user's intentions. Because of such conflicts
-	 * we need to handle transformations in special cases in a custom way.
-	 *
-	 * The function itself looks whether two given delta types have a special case function registered. If so, the deltas are
-	 * transformed using that function. If not,
-	 * {@link module:engine/model/delta/transform~transform.defaultTransform default transformation algorithm} is used.
-	 *
-	 * @param {module:engine/model/delta/delta~Delta} a Delta that will be transformed.
-	 * @param {module:engine/model/delta/delta~Delta} b Delta to transform by.
-	 * @param {module:engine/model/delta/transform~transformationContext} context Transformation context object.
-	 * @returns {Array.<module:engine/model/delta/delta~Delta>} Result of the transformation.
-	 */
-	transform( a, b, context ) {
-		const transformAlgorithm = transform.getTransformationCase( a, b ) || transform.defaultTransform;
-
-		// Make new instance of context object, so all changes done during transformation are not saved in original object.
-		const transformed = transformAlgorithm( a, b, Object.assign( {}, context ) );
-		const baseVersion = arrayUtils.last( b.operations ).baseVersion;
-
-		return updateBaseVersion( baseVersion, transformed );
-	},
-
-	/**
-	 * The default delta transformation function. It is used for those deltas that are not in special case conflict.
-	 *
-	 * This algorithm is similar to a popular `dOPT` algorithm used in operational transformation, as we are in fact
-	 * transforming two sets of operations by each other.
-	 *
-	 * @param {module:engine/model/delta/delta~Delta} a Delta that will be transformed.
-	 * @param {module:engine/model/delta/delta~Delta} b Delta to transform by.
-	 * @param {module:engine/model/delta/transform~transformationContext} context Transformation context object.
-	 * @returns {Array.<module:engine/model/delta/delta~Delta>} Result of the transformation.
-	 */
-	defaultTransform( a, b, context ) {
-		// This will hold operations from delta `a` that will be transformed by operations from delta `b`.
-		// Eventually, those operations will be used to create result delta(s).
-		const transformed = [];
-
-		// Array containing operations that we will transform by. At the beginning these are just operations from
-		let byOps = b.operations;
-
-		// This array is storing operations from `byOps` which got transformed by operation from delta `a`.
-		let newByOps = [];
-
-		// We take each operation from original set of operations to transform.
-		for ( const opA of a.operations ) {
-			// We wrap the operation in the array. This is important, because operation transformation algorithm returns
-			// an array of operations so we need to make sure that our algorithm is ready to handle arrays.
-			const ops = [ opA ];
-
-			// Now the real algorithm takes place.
-			for ( const opB of byOps ) {
-				// For each operation that we need transform by...
-				for ( let i = 0; i < ops.length; i++ ) {
-					// We take each operation to transform...
-					const op = ops[ i ];
-
-					// And transform both of them by themselves.
-
-					// The result of transforming operation from delta B by operation from delta A is saved in
-					// `newByOps` array. We will use that array for transformations in next loops. We need delta B
-					// operations after transformed by delta A operations to get correct results of transformations
-					// of next operations from delta A.
-					//
-					// It's like this because 2nd operation from delta A assumes that 1st operation from delta A
-					// is "already applied". When we transform 2nd operation from delta A by operations from delta B
-					// we have to be sure that operations from delta B are in a state that acknowledges 1st operation
-					// from delta A.
-					//
-					// This can be easier understood when operations sets to transform are represented by diamond diagrams:
-					// http://www.codecommit.com/blog/java/understanding-and-applying-operational-transformation
-
-					// Transform operation from delta A by operation from delta B.
-					const results = operationTransform( op, opB, context );
-
-					// We replace currently processed operation from `ops` array by the results of transformation.
-					// Note, that we process single operation but `operationTransform` result is an array, so we
-					// might have to splice-in more than one operation. Save them in `ops` array and move `i` pointer by a proper offset.
-					Array.prototype.splice.apply( ops, [ i, 1 ].concat( results ) );
-
-					i += results.length - 1;
-
-					// Then, transform operation from delta B by operation from delta A.
-					// Since this is a "mirror" transformation, first, we "mirror" some of context values.
-					const reverseContext = Object.assign( {}, context );
-					reverseContext.isStrong = !context.isStrong;
-					reverseContext.insertBefore = context.insertBefore !== undefined ? !context.insertBefore : undefined;
-
-					// Transform operations.
-					const updatedOpB = operationTransform( opB, op, reverseContext );
-
-					// Update `newByOps` by transformed, updated `opB`.
-					// Using push.apply because `operationTransform` returns an array with one or multiple results.
-					Array.prototype.push.apply( newByOps, updatedOpB );
-				}
-
-				// At this point a single operation from delta A got transformed by a single operation from delta B.
-				// The transformation result is in `ops` array and it may be one or more operations. This was just the first step.
-				// Operation from delta A has to be further transformed by the other operations from delta B.
-				// So in next iterator loop we will take another operation from delta B and use transformed delta A (`ops`)
-				// to transform it further.
-			}
-
-			// We got through all delta B operations and have a final transformed state of an operation from delta A.
-
-			// As previously mentioned, we substitute operations from delta B by their transformed equivalents.
-			byOps = newByOps;
-			newByOps = [];
-
-			// We add transformed operation from delta A to newly created delta.
-			// Remember that transformed operation from delta A may consist of multiple operations.
-			for ( const op of ops ) {
-				transformed.push( op );
-			}
-
-			// In next loop, we will take another operation from delta A and transform it through (transformed) operations
-			// from delta B...
-		}
-
-		return getNormalizedDeltas( a.constructor, transformed );
-	},
-
-	/**
-	 * Adds a special case callback for given delta classes.
-	 *
-	 * @param {Function} A Delta constructor which instance will get transformed.
-	 * @param {Function} B Delta constructor which instance will be transformed by.
-	 * @param {Function} resolver A callback that will handle custom special case transformation for instances of given delta classes.
-	 */
-	addTransformationCase( A, B, resolver ) {
-		let casesA = specialCases.get( A );
-
-		if ( !casesA ) {
-			casesA = new Map();
-			specialCases.set( A, casesA );
-		}
-
-		casesA.set( B, resolver );
-	},
-
-	/**
-	 * Gets a special case callback which was previously {@link module:engine/model/delta/transform~transform.addTransformationCase added}.
-	 *
-	 * @param {module:engine/model/delta/delta~Delta} a Delta to transform.
-	 * @param {module:engine/model/delta/delta~Delta} b Delta to be transformed by.
-	 */
-	getTransformationCase( a, b ) {
-		let casesA = specialCases.get( a.constructor );
-
-		// If there are no special cases registered for class which `a` is instance of, we will
-		// check if there are special cases registered for any parent class.
-		if ( !casesA || !casesA.get( b.constructor ) ) {
-			const cases = specialCases.keys();
-
-			for ( const caseClass of cases ) {
-				if ( a instanceof caseClass && specialCases.get( caseClass ).get( b.constructor ) ) {
-					casesA = specialCases.get( caseClass );
-
-					break;
-				}
-			}
-		}
-
-		if ( casesA ) {
-			return casesA.get( b.constructor );
-		}
-
-		return undefined;
-	},
-
-	/**
-	 * Transforms two sets of deltas by themselves. Returns both transformed sets.
-	 *
-	 * @param {Array.<module:engine/model/delta/delta~Delta>} deltasA Array with the first set of deltas to transform. These
-	 * deltas are considered more important (than `deltasB`) when resolving conflicts.
-	 * @param {Array.<module:engine/model/delta/delta~Delta>} deltasB Array with the second set of deltas to transform. These
-	 * deltas are considered less important (than `deltasA`) when resolving conflicts.
-	 * @param {module:engine/model/document~Document} [document=null] If set, deltas will be transformed in "undo mode"
-	 * and given `document` will be used to determine relations between deltas. If not set (default), deltas will be
-	 * transforming without additional context information.
-	 * @returns {Object}
-	 * @returns {Array.<module:engine/model/delta/delta~Delta>} return.deltasA The first set of deltas transformed
-	 * by the second set of deltas.
-	 * @returns {Array.<module:engine/model/delta/delta~Delta>} return.deltasB The second set of deltas transformed
-	 * by the first set of deltas.
-	 */
-	transformDeltaSets( deltasA, deltasB, document = null ) {
-		const transformedDeltasA = Array.from( deltasA );
-		const transformedDeltasB = Array.from( deltasB );
-
-		const useAdditionalContext = document !== null;
-
-		const contextAB = {
-			isStrong: true
-		};
-
-		if ( useAdditionalContext ) {
-			contextAB.wasAffected = new Map();
-			contextAB.originalDelta = new Map();
-			contextAB.document = document;
-			contextAB.undoMode = true;
-
-			for ( const delta of transformedDeltasB ) {
-				contextAB.originalDelta.set( delta, delta );
-			}
-		}
-
-		for ( let i = 0; i < transformedDeltasA.length; i++ ) {
-			const deltaA = [ transformedDeltasA[ i ] ];
-
-			for ( let j = 0; j < transformedDeltasB.length; j++ ) {
-				const deltaB = [ transformedDeltasB[ j ] ];
-
-				for ( let k = 0; k < deltaA.length; k++ ) {
-					for ( let l = 0; l < deltaB.length; l++ ) {
-						if ( useAdditionalContext ) {
-							_setContext( deltaA[ k ], deltaB[ l ], contextAB );
-						}
-
-						const resultAB = transform.transform( deltaA[ k ], deltaB[ l ], {
-							insertBefore: contextAB.insertBefore,
-							forceNotSticky: contextAB.forceNotSticky,
-							isStrong: contextAB.isStrong,
-							forceWeakRemove: contextAB.forceWeakRemove,
-							undoMode: contextAB.undoMode
-						} );
-
-						const resultBA = transform.transform( deltaB[ l ], deltaA[ k ], {
-							insertBefore: !contextAB.insertBefore,
-							forceNotSticky: contextAB.forceNotSticky,
-							isStrong: !contextAB.isStrong,
-							forceWeakRemove: contextAB.forceWeakRemove,
-							undoMode: contextAB.undoMode
-						} );
-
-						if ( useAdditionalContext ) {
-							_updateContext( deltaA[ k ], resultAB, contextAB );
-
-							const originalDelta = contextAB.originalDelta.get( deltaB[ l ] );
-
-							for ( const deltaBA of resultBA ) {
-								contextAB.originalDelta.set( deltaBA, originalDelta );
-							}
-						}
-
-						deltaA.splice( k, 1, ...resultAB );
-						k += resultAB.length - 1;
-
-						deltaB.splice( l, 1, ...resultBA );
-						l += resultBA.length - 1;
-					}
-				}
-
-				transformedDeltasB.splice( j, 1, ...deltaB );
-				j += deltaB.length - 1;
-			}
-
-			transformedDeltasA.splice( i, 1, ...deltaA );
-			i += deltaA.length - 1;
-		}
-
-		const opsDiffA = getOpsCount( transformedDeltasA ) - getOpsCount( deltasA );
-		const opsDiffB = getOpsCount( transformedDeltasB ) - getOpsCount( deltasB );
-
-		if ( opsDiffB < opsDiffA ) {
-			padWithNoOps( transformedDeltasB, opsDiffA - opsDiffB );
-		} else if ( opsDiffA < opsDiffB ) {
-			padWithNoOps( transformedDeltasA, opsDiffB - opsDiffA );
-		}
-
-		return { deltasA: transformedDeltasA, deltasB: transformedDeltasB };
-	}
-};
-
-export default transform;
-
-// Updates base versions of operations inside deltas (which are the results of delta transformation).
-function updateBaseVersion( baseVersion, deltas ) {
-	for ( const delta of deltas ) {
-		for ( const op of delta.operations ) {
-			op.baseVersion = ++baseVersion;
-		}
-	}
-
-	return deltas;
-}
-
-// Returns number of operations in given array of deltas.
-function getOpsCount( deltas ) {
-	return deltas.reduce( ( current, delta ) => {
-		return current + delta.operations.length;
-	}, 0 );
-}
-
-// Adds a delta containing `howMany` `NoOperation` instances to given array with deltas.
-// Used to "synchronize" the number of operations in two delta sets.
-function padWithNoOps( deltas, howMany ) {
-	const lastDelta = deltas[ deltas.length - 1 ];
-	let baseVersion = lastDelta.operations.length + lastDelta.baseVersion;
-
-	const noDelta = new Delta();
-
-	for ( let i = 0; i < howMany; i++ ) {
-		noDelta.addOperation( new NoOperation( baseVersion++ ) );
-	}
-
-	deltas.push( noDelta );
-}
-
-// Sets context data before delta `a` by delta `b` transformation.
-// Using data given in `context` object, sets `context.insertBefore` and `context.forceNotSticky` flags.
-// Also updates `context.wasAffected`.
-function _setContext( a, b, context ) {
-	_setWasAffected( a, b, context );
-	_setInsertBeforeContext( a, b, context );
-	_setForceWeakRemove( b, context );
-	_setForceNotSticky( b, context );
-}
-
-// Sets `context.insertBefore` basing on `context.document` history for `a` by `b` transformation.
-//
-// Simply saying, if `b` is "undoing delta" it means that `a` might already be transformed by the delta
-// which was undone by `b` (let's call it `oldB`). If this is true, `a` by `b` transformation has to consider
-// how `a` was transformed by `oldB` to get an expected result.
-//
-// This is used to resolve conflict when two operations want to insert nodes at the same position. If the operations
-// are not related, it doesn't matter in what order operations insert those nodes. However if the operations are
-// related (for example, in undo) we need to keep the same order.
-//
-// For example, assume that editor has two letters: 'ab'. Then, both letters are removed, creating two operations:
-// (op. 1) REM [ 1 ] - [ 2 ] => (graveyard) [ 0 ]
-// (op. 2) REM [ 0 ] - [ 1 ] => (graveyard) [ 1 ]
-// Then, we undo operation 2:
-// REM [ 0 ] - [ 1 ] => (graveyard) [ 1 ] is reversed to REI (graveyard) [ 1 ] => [ 0 ] - [ 1 ] and is applied.
-// History stack is:
-// (op. 1) REM [ 1 ] - [ 2 ] => (graveyard) [ 0 ]
-// (op. 2) REM [ 0 ] - [ 1 ] => (graveyard) [ 1 ]
-// (op. 3) REI (graveyard) [ 1 ] => [ 0 ] - [ 1 ]
-// Then, we undo operation 1:
-// REM [ 1 ] - [ 2 ] => (graveyard) [ 0 ] is reversed to REI (graveyard) [ 0 ] => [ 1 ] - [ 2 ] then,
-// is transformed by (op. 2) REM [ 0 ] - [ 1 ] => (graveyard) [ 1 ] and becomes REI (graveyard) [ 0 ] => [ 0 ] - [ 1 ] then,
-// is transformed by (op. 3) REI (graveyard) [ 1 ] => [ 0 ] - [ 1 ] and we have a conflict because both operations
-// insert at the same position, but thanks to keeping the context, we know that in this case, the transformed operation should
-// insert the node after operation 3.
-//
-// Keep in mind, that `context.insertBefore` may be either `Boolean` or `undefined`. If it is `Boolean` then the order is
-// known (deltas are related and `a` should insert nodes before or after `b`). However, if deltas were not related,
-// `context.isBefore` is `undefined` and other factors will be taken into consideration when resolving the order
-// (this, however, happens in operational transformation algorithms).
-//
-// This affects both `MoveOperation` (and its derivatives) and `InsertOperation`.
-function _setInsertBeforeContext( a, b, context ) {
-	// If `b` is a delta that undoes other delta...
-	const originalDelta = context.originalDelta.get( b );
-
-	if ( context.document.history.isUndoingDelta( originalDelta ) ) {
-		// Get the undone delta...
-		const undoneDelta = context.document.history.getUndoneDelta( originalDelta );
-		// Get a map with deltas related to `a` delta...
-		const aWasAffectedBy = context.wasAffected.get( a );
-		// And check if the undone delta is related with delta `a`.
-		const affected = aWasAffectedBy.get( undoneDelta );
-
-		if ( affected !== undefined ) {
-			// If deltas are related, set `context.insertBefore` basing on whether `a` was affected by the undone delta.
-			context.insertBefore = affected;
-		}
-	}
-}
-
-// Sets `context.forceNotSticky` basing on `context.document` history for transformation by `b` delta.
-//
-// `MoveOperation` may be "sticky" which means, that anything that was inserted at the boundary of moved range, should
-// also be moved. This is particularly helpful for actions like splitting or merging a node. However, this behavior
-// sometimes leads to an error, for example in undo.
-//
-// Simply saying, if delta is going to be transformed by delta `b`, stickiness should not be taken into consideration
-// if delta `b` was already undone or if delta `b` is an undoing delta.
-//
-// This affects `MoveOperation` (and its derivatives).
-function _setForceNotSticky( b, context ) {
-	const originalDelta = context.originalDelta.get( b );
-	const history = context.document.history;
-
-	context.forceNotSticky = history.isUndoneDelta( originalDelta ) || history.isUndoingDelta( originalDelta );
-}
-
-// Sets `context.forceWeakRemove` basing on `context.document` history for transformation by `b` delta.
-//
-// When additional context is not used, default `MoveOperation` x `RemoveOperation` transformation
-// always treats `RemoveOperation` as a stronger one, no matter how `context.isStrong` is set. It is like this
-// to provide better results when transformations happen.
-//
-// This, however, works fine only when additional context is not used.
-//
-// When additional context is used, we need a better way to decide whether `RemoveOperation` is "dominating" (or in other
-// words, whether nodes removed by given operation should stay in graveyard if other operation wants to move them).
-//
-// The answer to this is easy: if `RemoveOperation` has been already undone, we are not forcing given nodes to stay
-// in graveyard. In such scenario, we set `context.forceWeakRemove` to `true`. However, if the `RemoveOperation` has
-// not been undone, we set `context.forceWeakRemove` to `false` because we want the operation to be "dominating".
-function _setForceWeakRemove( b, context ) {
-	const history = context.document.history;
-	const originalB = context.originalDelta.get( b );
-
-	// If `b` delta has not been undone yet, forceWeakRemove should be `false`.
-	// It should be `true`, in any other case, if additional context is used.
-	context.forceWeakRemove = history.isUndoneDelta( originalB );
-}
-
-// Sets `context.wasAffected` which holds context information about how transformed deltas are related. `context.wasAffected`
-// is used by `_setInsertBeforeContext` helper function.
-function _setWasAffected( a, b, context ) {
-	if ( !context.wasAffected.get( a ) ) {
-		// Create a new map with relations for `a` delta.
-		context.wasAffected.set( a, new Map() );
-	}
-
-	const originalDelta = context.originalDelta.get( b );
-	let wasAffected = !!context.wasAffected.get( a ).get( originalDelta );
-
-	// Cross-check all operations from both deltas...
-	for ( const opA of a.operations ) {
-		for ( const opB of b.operations ) {
-			if ( opA instanceof MoveOperation && opB instanceof MoveOperation ) {
-				if ( _isOperationAffected( opA, opB ) ) {
-					// If any of them are move operations that affect each other, set the relation accordingly.
-					wasAffected = true;
-
-					break;
-				}
-			}
-		}
-
-		// Break both loops if affecting pair has been found.
-		if ( wasAffected ) {
-			break;
-		}
-	}
-
-	context.wasAffected.get( a ).set( originalDelta, wasAffected );
-}
-
-// Checks whether `opA` is affected by `opB`. It is assumed that both operations are `MoveOperation`.
-// Operation is affected only if the other operation's source range is before that operation's source range.
-function _isOperationAffected( opA, opB ) {
-	const target = opA.targetPosition;
-	const source = opB.sourcePosition;
-
-	const cmpResult = compareArrays( source.getParentPath(), target.getParentPath() );
-
-	if ( target.root != source.root ) {
-		return false;
-	}
-
-	return cmpResult == 'same' && source.offset < target.offset;
-}
-
-// Updates `context` object after delta by delta transformation is done.
-//
-// This means two things:
-// 1. Some information are removed from context (those that apply only to the transformation that just happened).
-// 2. `context.wasAffected` is updated because `oldDelta` has been transformed to one or many `newDeltas` and we
-// need to update entries in `context.wasAffected`. Basically, anything that was in `context.wasAffected` under
-// `oldDelta` key should be rewritten to `newDeltas`. This way in next transformation steps, `newDeltas` "remember"
-// the context of `oldDelta`.
-function _updateContext( oldDelta, newDeltas, context ) {
-	delete context.insertBefore;
-	delete context.forceNotSticky;
-	delete context.forceWeakRemove;
-
-	const wasAffected = context.wasAffected.get( oldDelta );
-
-	context.wasAffected.delete( oldDelta );
-
-	for ( const delta of newDeltas ) {
-		context.wasAffected.set( delta, new Map( wasAffected ) );
-	}
-}
-
-// Takes base delta class (`DeltaClass`) and a set of `operations` that are transformation results and creates
-// one or more deltas, acknowledging that the result is a transformation of a delta that is of `DeltaClass`.
-//
-// The normalization ensures that each delta has it's "normal" state, that is, for example, `MoveDelta` has
-// just one `MoveOperation`, `SplitDelta` has just two operations of which first is `InsertOperation` and second
-// is `MoveOperation` or `NoOperation`, etc.
-function getNormalizedDeltas( DeltaClass, operations ) {
-	let deltas = [];
-	let delta = null;
-	let attributeOperationIndex;
-
-	switch ( DeltaClass ) {
-		case MoveDelta:
-		case RemoveDelta:
-			// Normal MoveDelta has just one MoveOperation.
-			// Take all operations and create MoveDelta for each of them.
-			for ( const o of operations ) {
-				if ( o instanceof NoOperation ) {
-					// An operation may be instance of NoOperation and this may be correct.
-					// If that's the case, do not create a MoveDelta with singular NoOperation.
-					// Create "no delta" instead, that is Delta instance with NoOperation.
-					delta = new Delta();
-				} else {
-					if ( o instanceof RemoveOperation ) {
-						delta = new RemoveDelta();
-					} else {
-						delta = new MoveDelta();
-					}
-				}
-
-				delta.addOperation( o );
-				deltas.push( delta );
-			}
-
-			// Return all created MoveDeltas.
-			return deltas;
-		case SplitDelta:
-		case WrapDelta:
-			// Normal SplitDelta and WrapDelta have two operations: first is InsertOperation and second is MoveOperation.
-			// The MoveOperation may be split into multiple MoveOperations.
-			// If that's the case, convert additional MoveOperations into MoveDeltas.
-			// First, create normal SplitDelta or WrapDelta, using first two operations.
-			delta = new DeltaClass();
-			delta.addOperation( operations[ 0 ] );
-			delta.addOperation( operations[ 1 ] );
-			// Then, take all but last two operations and use them to create normalized MoveDeltas.
-			deltas = getNormalizedDeltas( MoveDelta, operations.slice( 2 ) );
-
-			// Return all deltas as one array, in proper order.
-			return [ delta ].concat( deltas );
-		case MergeDelta:
-		case UnwrapDelta:
-			// Normal MergeDelta and UnwrapDelta have two operations: first is MoveOperation and second is RemoveOperation.
-			// The MoveOperation may be split into multiple MoveOperations.
-			// If that's the case, convert additional MoveOperations into MoveDeltas.
-			// Take all but last two operations and use them to create normalized MoveDeltas.
-			deltas = getNormalizedDeltas( MoveDelta, operations.slice( 0, -2 ) );
-			// Then, create normal MergeDelta or UnwrapDelta, using last two operations.
-			delta = new DeltaClass();
-			delta.addOperation( operations[ operations.length - 2 ] );
-			delta.addOperation( operations[ operations.length - 1 ] );
-
-			// Return all deltas as one array, in proper order.
-			return deltas.concat( delta );
-		case RenameDelta:
-			// RenameDelta may become a "no delta" if it's only operation is transformed to NoOperation.
-			// This may happen when RenameOperation is transformed by RenameOperation.
-			// Keep in mind that RenameDelta always have just one operation.
-			if ( operations[ 0 ] instanceof NoOperation ) {
-				delta = new Delta();
-			} else {
-				delta = new RenameDelta();
-			}
-
-			delta.addOperation( operations[ 0 ] );
-
-			return [ delta ];
-		case AttributeDelta:
-			// AttributeDelta is allowed to have multiple AttributeOperations and also NoOperations but
-			// the first operation has to be an AttributeOperation as it is used as a reference for deltas properties.
-			// Keep in mind that we cannot simply remove NoOperations cause that would mess up base versions.
-			// Find an index of first operation that is not a NoOperation.
-			for ( attributeOperationIndex = 0; attributeOperationIndex < operations.length; attributeOperationIndex++ ) {
-				if ( !( operations[ attributeOperationIndex ] instanceof NoOperation ) ) {
-					break;
-				}
-			}
-
-			// No AttributeOperations has been found. Convert AttributeDelta to "no delta".
-			if ( attributeOperationIndex == operations.length ) {
-				delta = new Delta();
-			}
-			// AttributeOperation found.
-			else {
-				delta = new AttributeDelta();
-
-				// AttributeOperation wasn't the first operation.
-				if ( attributeOperationIndex != 0 ) {
-					// Move AttributeOperation to the beginning.
-					operations.unshift( operations.splice( attributeOperationIndex, 1 )[ 0 ] );
-					// No need to update base versions - they are updated at the end of transformation algorithm anyway.
-				}
-			}
-
-			// Add all operations to the delta (even if it is just a couple of NoOperations we have to keep them all).
-			for ( const o of operations ) {
-				delta.addOperation( o );
-			}
-
-			return [ delta ];
-		default:
-			// For all other deltas no normalization is needed.
-			delta = new DeltaClass();
-
-			for ( const o of operations ) {
-				delta.addOperation( o );
-			}
-
-			return [ delta ];
-	}
-}
-
-/**
- * Object containing values and flags describing context of a transformation.
- *
- * @typedef {Object} module:engine/model/delta/transform~transformationContext
- * @property {Boolean} useAdditionalContext Whether additional context should be evaluated and used during transformations.
- * @property {Boolean} isStrong Whether transformed deltas are more (`true`) or less (`false`) important than deltas to transform by.
- * @property {module:engine/model/document~Document} [document] Model document which is a context for transformations.
- * Available only if `useAdditionalContext` is `true`.
- * @property {Boolean|undefined} forceWeakRemove Whether {@link module:engine/model/operation/removeoperation~RemoveOperation}
- * should be always more important than other operations. Available only if `useAdditionalContext` is `true`.
- * @property {Boolean|undefined} insertBefore Used when transforming {@link module:engine/model/operation/moveoperation~MoveOperation}s
- * If two `MoveOperation`s target to the same position, `insertBefore` is used to resolve such conflict. This flag
- * is set and used internally by transformation algorithms. Available only if `useAdditionalContext` is `true`.
- * @property {Boolean|undefined} forceNotSticky Used when transforming
- * {@link module:engine/model/operation/moveoperation~MoveOperation#isSticky sticky MoveOperation}. If set to `true`,
- * `isSticky` flag is discarded during transformations. This flag is set and used internally by transformation algorithms.
- * Available only if `useAdditionalContext` is `true`.
- * @property {Map|undefined} wasAffected Used to evaluate `insertBefore` flag. This map is set and used internally by
- * transformation algorithms. Available only if `useAdditionalContext` is `true`.
- */

+ 0 - 62
packages/ckeditor5-engine/src/model/delta/unwrapdelta.js

@@ -1,62 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/model/delta/unwrapdelta
- */
-
-import Delta from './delta';
-import DeltaFactory from './deltafactory';
-import WrapDelta from './wrapdelta';
-
-/**
- * To provide specific OT behavior and better collisions solving, {@link module:engine/model/writer~Writer#merge} method
- * uses the `UnwrapDelta` class which inherits from the `Delta` class and may overwrite some methods.
- *
- * @extends module:engine/model/delta/delta~Delta
- */
-export default class UnwrapDelta extends Delta {
-	/**
-	 * @inheritDoc
-	 */
-	get type() {
-		return 'unwrap';
-	}
-
-	/**
-	 * Position before unwrapped element or `null` if there are no operations in the delta.
-	 *
-	 * @type {module:engine/model/position~Position|null}
-	 */
-	get position() {
-		return this._moveOperation ? this._moveOperation.targetPosition : null;
-	}
-
-	/**
-	 * Operation in the delta that moves unwrapped nodes to their new parent or `null` if there are no operations in the delta.
-	 *
-	 * @protected
-	 * @type {module:engine/model/operation/moveoperation~MoveOperation|null}
-	 */
-	get _moveOperation() {
-		return this.operations[ 0 ] || null;
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	get _reverseDeltaClass() {
-		return WrapDelta;
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	static get className() {
-		return 'engine.model.delta.UnwrapDelta';
-	}
-}
-
-DeltaFactory.register( UnwrapDelta );

+ 0 - 28
packages/ckeditor5-engine/src/model/delta/weakinsertdelta.js

@@ -1,28 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/model/delta/weakinsertdelta
- */
-
-import InsertDelta from './insertdelta';
-import DeltaFactory from './deltafactory';
-
-/**
- * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/writer~Writer#insert} method
- * uses the `WeakInsertDelta` class which inherits from the `Delta` class and may overwrite some methods.
- *
- * @extends module:engine/model/delta/delta~Delta
- */
-export default class WeakInsertDelta extends InsertDelta {
-	/**
-	 * @inheritDoc
-	 */
-	static get className() {
-		return 'engine.model.delta.WeakInsertDelta';
-	}
-}
-
-DeltaFactory.register( WeakInsertDelta );

+ 0 - 88
packages/ckeditor5-engine/src/model/delta/wrapdelta.js

@@ -1,88 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module engine/model/delta/wrapdelta
- */
-
-import Delta from './delta';
-import DeltaFactory from './deltafactory';
-import UnwrapDelta from './unwrapdelta';
-import Range from '../range';
-
-/**
- * To provide specific OT behavior and better collisions solving, {@link module:engine/model/writer~Writer#merge} method
- * uses the `WrapDelta` class which inherits from the `Delta` class and may overwrite some methods.
- *
- * @extends module:engine/model/delta/delta~Delta
- */
-export default class WrapDelta extends Delta {
-	/**
-	 * @inheritDoc
-	 */
-	get type() {
-		return 'wrap';
-	}
-
-	/**
-	 * Range to wrap or `null` if there are no operations in the delta.
-	 *
-	 * @type {module:engine/model/range~Range|null}
-	 */
-	get range() {
-		const moveOp = this._moveOperation;
-
-		return moveOp ? Range.createFromPositionAndShift( moveOp.sourcePosition, moveOp.howMany ) : null;
-	}
-
-	/**
-	 * Offset size of range to wrap by the delta or `null` if there are no operations in delta.
-	 *
-	 * @type {Number}
-	 */
-	get howMany() {
-		const range = this.range;
-
-		return range ? range.end.offset - range.start.offset : 0;
-	}
-
-	/* eslint-disable max-len */
-	/**
-	 * Operation that inserts wrapping element or `null` if there are no operations in the delta.
-	 *
-	 * @protected
-	 * @type {module:engine/model/operation/insertoperation~InsertOperation|module:engine/model/operation/reinsertoperation~ReinsertOperation}
-	 */
-	/* eslint-enable max-len */
-	get _insertOperation() {
-		return this.operations[ 0 ] || null;
-	}
-
-	/**
-	 * Operation that moves wrapped nodes to their new parent or `null` if there are no operations in the delta.
-	 *
-	 * @protected
-	 * @type {module:engine/model/operation/moveoperation~MoveOperation|null}
-	 */
-	get _moveOperation() {
-		return this.operations[ 1 ] || null;
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	get _reverseDeltaClass() {
-		return UnwrapDelta;
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	static get className() {
-		return 'engine.model.delta.WrapDelta';
-	}
-}
-
-DeltaFactory.register( WrapDelta );

+ 48 - 0
packages/ckeditor5-engine/src/model/differ.js

@@ -174,6 +174,54 @@ export default class Differ {
 					this.bufferMarkerChange( marker.name, markerRange, markerRange, marker.affectsData );
 				}
 
+				break;
+			}
+			case 'split': {
+				const splitElement = operation.position.parent;
+				const howManyMoved = splitElement.maxOffset - operation.position.offset;
+
+				if ( !this._isInInsertedElement( splitElement ) ) {
+					this._markRemove( splitElement, operation.position.offset, howManyMoved );
+				}
+
+				if ( !this._isInInsertedElement( splitElement.parent ) ) {
+					this._markInsert( splitElement.parent, splitElement.startOffset + 1, 1 );
+				}
+
+				break;
+			}
+			case 'merge': {
+				const mergedElement = operation.sourcePosition.parent;
+				const mergedIntoElement = operation.targetPosition.parent;
+
+				if ( !this._isInInsertedElement( mergedElement.parent ) ) {
+					this._markRemove( mergedElement.parent, mergedElement.startOffset, 1 );
+				}
+
+				if ( !this._isInInsertedElement( mergedIntoElement ) ) {
+					this._markInsert( mergedIntoElement, operation.targetPosition.offset, mergedElement.maxOffset );
+				}
+
+				break;
+			}
+			case 'wrap': {
+				if ( !this._isInInsertedElement( operation.position.parent ) ) {
+					this._markRemove( operation.position.parent, operation.position.offset, operation.howMany );
+					this._markInsert( operation.position.parent, operation.position.offset, 1 );
+				}
+
+				break;
+			}
+			case 'unwrap': {
+				const elementToUnwrap = operation.position.parent;
+				const offset = elementToUnwrap.startOffset;
+				const parent = elementToUnwrap.parent;
+
+				if ( !this._isInInsertedElement( parent ) ) {
+					this._markRemove( parent, offset, 1 );
+					this._markInsert( parent, offset, elementToUnwrap.maxOffset );
+				}
+
 				break;
 			}
 		}

+ 1 - 3
packages/ckeditor5-engine/src/model/document.js

@@ -67,8 +67,6 @@ export default class Document {
 		/**
 		 * The document's history.
 		 *
-		 * **Note:** Be aware that deltas applied to the document might get removed or changed.
-		 *
 		 * @readonly
 		 * @member {module:engine/model/history~History}
 		 */
@@ -143,7 +141,7 @@ export default class Document {
 
 			if ( operation.isDocumentOperation ) {
 				this.version++;
-				this.history.addDelta( operation.delta );
+				this.history.addOperation( operation );
 			}
 		}, { priority: 'low' } );
 

+ 2 - 2
packages/ckeditor5-engine/src/model/documentselection.js

@@ -750,14 +750,14 @@ class LiveSelection extends Selection {
 
 		const liveRange = LiveRange.createFromRange( range );
 
-		liveRange.on( 'change:range', ( evt, oldRange, data ) => {
+		liveRange.on( 'change:range', ( evt, oldRange, operation ) => {
 			this._hasChangedRange = true;
 
 			// If `LiveRange` is in whole moved to the graveyard, save necessary data. It will be fixed on `Model#applyOperation` event.
 			if ( liveRange.root == this._document.graveyard ) {
 				this._fixGraveyardRangesData.push( {
 					liveRange,
-					sourcePosition: data.sourcePosition
+					sourcePosition: operation.sourcePosition
 				} );
 			}
 		} );

+ 56 - 117
packages/ckeditor5-engine/src/model/history.js

@@ -7,10 +7,8 @@
  * @module engine/model/history
  */
 
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-
 /**
- * `History` keeps the track of all the deltas applied to the {@link module:engine/model/document~Document document}.
+ * `History` keeps the track of all the operations applied to the {@link module:engine/model/document~Document document}.
  */
 export default class History {
 	/**
@@ -18,28 +16,19 @@ export default class History {
 	 */
 	constructor() {
 		/**
-		 * Deltas added to the history.
+		 * Operations added to the history.
 		 *
 		 * @protected
-		 * @member {Array.<module:engine/model/delta/delta~Delta>} module:engine/model/history~History#_deltas
+		 * @member {Array.<module:engine/model/operation/operation~Operation>} module:engine/model/history~History#_operations
 		 */
-		this._deltas = [];
+		this._operations = [];
 
 		/**
-		 * Helper structure that maps added delta's base version to the index in {@link module:engine/model/history~History#_deltas}
-		 * at which the delta was added.
+		 * Holds an information which {@link module:engine/model/operation/operation~Operation operation} undoes which
+		 * {@link module:engine/model/operation/operation~Operation operation}.
 		 *
-		 * @protected
-		 * @member {Map} module:engine/model/history~History#_historyPoints
-		 */
-		this._historyPoints = new Map();
-
-		/**
-		 * Holds an information which {@link module:engine/model/delta/delta~Delta delta} undoes which
-		 * {@link module:engine/model/delta/delta~Delta delta}.
-		 *
-		 * Keys of the map are "undoing deltas", that is deltas that undone some other deltas. For each key, the
-		 * value is a delta that has been undone by the "undoing delta".
+		 * Keys of the map are "undoing operations", that is operations that undone some other operations. For each key, the
+		 * value is an operation that has been undone by the "undoing operation".
 		 *
 		 * @private
 		 * @member {Map} module:engine/model/history~History#_undoPairs
@@ -47,145 +36,95 @@ export default class History {
 		this._undoPairs = new Map();
 
 		/**
-		 * Holds all undone deltas.
+		 * Holds all undone operations.
 		 *
 		 * @private
-		 * @member {Set.<module:engine/model/delta/delta~Delta>} module:engine/model/history~History#_undoneDeltas
+		 * @member {Set.<module:engine/model/operation/operation~Operation>} module:engine/model/history~History#_undoneOperations
 		 */
-		this._undoneDeltas = new Set();
-	}
-
-	/**
-	 * Adds delta to the history.
-	 *
-	 * @param {module:engine/model/delta/delta~Delta} delta Delta to add.
-	 */
-	addDelta( delta ) {
-		if ( delta.operations.length > 0 && !this._historyPoints.has( delta.baseVersion ) ) {
-			const index = this._deltas.length;
-
-			this._deltas[ index ] = delta;
-			this._historyPoints.set( delta.baseVersion, index );
-		}
+		this._undoneOperations = new Set();
 	}
 
 	/**
-	 * Returns deltas added to the history.
+	 * Adds an operation to the history.
 	 *
-	 * @param {Number} [from=0] Base version from which deltas should be returned (inclusive). Defaults to `0`, which means
-	 * that deltas from the first one will be returned.
-	 * @param {Number} [to=Number.POSITIVE_INFINITY] Base version up to which deltas should be returned (exclusive).
-	 * Defaults to `Number.POSITIVE_INFINITY` which means that deltas up to the last one will be returned.
-	 * @returns {Iterable.<module:engine/model/delta/delta~Delta>} Deltas added to the history from given base versions range.
+	 * @param {module:engine/model/operation/operation~Operation} operation Operation to add.
 	 */
-	* getDeltas( from = 0, to = Number.POSITIVE_INFINITY ) {
-		// No deltas added, nothing to yield.
-		if ( this._deltas.length === 0 ) {
+	addOperation( operation ) {
+		if ( this._operations.includes( operation ) ) {
 			return;
 		}
 
-		// Will throw if base version is incorrect.
-		let fromIndex = this._getIndex( from );
-
-		// Base version is too low or too high and is not found in history.
-		if ( fromIndex == -1 ) {
-			return;
-		}
-
-		// We have correct `fromIndex` so let's iterate starting from it.
-		while ( fromIndex < this._deltas.length ) {
-			const delta = this._deltas[ fromIndex++ ];
-
-			if ( delta.baseVersion >= to ) {
-				break;
-			}
-
-			yield delta;
-		}
+		this._operations.push( operation );
 	}
 
 	/**
-	 * Returns delta from history that bases on given `baseVersion`.
+	 * Returns operations added to the history.
 	 *
-	 * @param {Number} baseVersion Base version of the delta to get.
-	 * @returns {module:engine/model/delta/delta~Delta|null} Delta with given base version or `null` if there is no such delta in history.
+	 * @param {Number} [from=0] Base version from which operations should be returned (inclusive). Defaults to `0`, which means
+	 * that operations from the first one will be returned.
+	 * @param {Number} [to=Number.POSITIVE_INFINITY] Base version up to which operations should be returned (exclusive).
+	 * Defaults to `Number.POSITIVE_INFINITY` which means that operations up to the last one will be returned.
+	 * @returns {Iterable.<module:engine/model/operation/operation~Operation>} Operations added to the history.
 	 */
-	getDelta( baseVersion ) {
-		const index = this._historyPoints.get( baseVersion );
+	getOperations( from = 0, to = Number.POSITIVE_INFINITY ) {
+		if ( from < 0 ) {
+			return [];
+		}
 
-		return index === undefined ? null : this._deltas[ index ];
+		return this._operations.slice( from, to );
 	}
 
 	/**
-	 * Marks in history that one delta is a delta that is undoing the other delta. By marking deltas this way,
-	 * history is keeping more context information about deltas which helps in operational transformation.
+	 * Returns operation from the history that bases on given `baseVersion`.
 	 *
-	 * @param {module:engine/model/delta/delta~Delta} undoneDelta Delta which is undone by `undoingDelta`.
-	 * @param {module:engine/model/delta/delta~Delta} undoingDelta Delta which undoes `undoneDelta`.
+	 * @param {Number} baseVersion Base version of the operation to get.
+	 * @returns {module:engine/model/operation/operation~Operation|null} Operation with given base version or `null` if
+	 * there is no such operation in history.
 	 */
-	setDeltaAsUndone( undoneDelta, undoingDelta ) {
-		this._undoPairs.set( undoingDelta, undoneDelta );
-		this._undoneDeltas.add( undoneDelta );
+	getOperation( baseVersion ) {
+		return this._operations[ baseVersion ];
 	}
 
 	/**
-	 * Checks whether given `delta` is undoing by any other delta.
+	 * Marks in history that one operation is an operation that is undoing the other operation. By marking operation this way,
+	 * history is keeping more context information about operations, which helps in operational transformation.
 	 *
-	 * @param {module:engine/model/delta/delta~Delta} delta Delta to check.
-	 * @returns {Boolean} `true` if given `delta` is undoing any other delta, `false` otherwise.
+	 * @param {module:engine/model/operation/operation~Operation} undoneOperation Operation which is undone by `undoingOperation`.
+	 * @param {module:engine/model/operation/operation~Operation} undoingOperation Operation which undoes `undoneOperation`.
 	 */
-	isUndoingDelta( delta ) {
-		return this._undoPairs.has( delta );
+	setOperationAsUndone( undoneOperation, undoingOperation ) {
+		this._undoPairs.set( undoingOperation, undoneOperation );
+		this._undoneOperations.add( undoneOperation );
 	}
 
 	/**
-	 * Checks whether given `delta` has been undone by any other delta.
+	 * Checks whether given `operation` is undoing any other operation.
 	 *
-	 * @param {module:engine/model/delta/delta~Delta} delta Delta to check.
-	 * @returns {Boolean} `true` if given `delta` has been undone any other delta, `false` otherwise.
+	 * @param {module:engine/model/operation/operation~Operation} operation Operation to check.
+	 * @returns {Boolean} `true` if given `operation` is undoing any other operation, `false` otherwise.
 	 */
-	isUndoneDelta( delta ) {
-		return this._undoneDeltas.has( delta );
+	isUndoingOperation( operation ) {
+		return this._undoPairs.has( operation );
 	}
 
 	/**
-	 * For given `undoingDelta`, returns the delta which has been undone by it.
+	 * Checks whether given `operation` has been undone by any other operation.
 	 *
-	 * @param {module:engine/model/delta/delta~Delta} undoingDelta
-	 * @returns {module:engine/model/delta/delta~Delta|undefined} Delta that has been undone by given `undoingDelta` or `undefined`
-	 * if given `undoingDelta` is not undoing any other delta.
+	 * @param {module:engine/model/operation/operation~Operation} operation Operation to check.
+	 * @returns {Boolean} `true` if given `operation` has been undone any other operation, `false` otherwise.
 	 */
-	getUndoneDelta( undoingDelta ) {
-		return this._undoPairs.get( undoingDelta );
+	isUndoneOperation( operation ) {
+		return this._undoneOperations.has( operation );
 	}
 
 	/**
-	 * Gets an index in {@link module:engine/model/history~History#_deltas} where delta with given `baseVersion` is added.
+	 * For given `undoingOperation`, returns the operation which has been undone by it.
 	 *
-	 * @private
-	 * @param {Number} baseVersion Base version of delta.
+	 * @param {module:engine/model/operation/operation~Operation} undoingOperation
+	 * @returns {module:engine/model/operation/operation~Operation|undefined} Operation that has been undone by given
+	 * `undoingOperation` or `undefined` if given `undoingOperation` is not undoing any other operation.
 	 */
-	_getIndex( baseVersion ) {
-		const index = this._historyPoints.get( baseVersion );
-
-		// Base version not found - it is either too high or too low, or is in the middle of delta.
-		if ( index === undefined ) {
-			const lastDelta = this._deltas[ this._deltas.length - 1 ];
-			const nextBaseVersion = lastDelta.baseVersion + lastDelta.operations.length;
-
-			if ( baseVersion < 0 || baseVersion >= nextBaseVersion ) {
-				// Base version is too high or too low - it's acceptable situation.
-				return -1;
-			}
-
-			/**
-			 * Given base version points to the middle of a delta.
-			 *
-			 * @error history-wrong-version
-			 */
-			throw new CKEditorError( 'model-history-wrong-version: Given base version points to the middle of a delta.' );
-		}
-
-		return index;
+	getUndoneOperation( undoingOperation ) {
+		return this._undoPairs.get( undoingOperation );
 	}
 }

+ 9 - 83
packages/ckeditor5-engine/src/model/liveposition.js

@@ -8,7 +8,6 @@
  */
 
 import Position from './position';
-import Range from './range';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
@@ -35,11 +34,11 @@ export default class LivePosition extends Position {
 	 * @see module:engine/model/position~Position
 	 * @param {module:engine/model/rootelement~RootElement} root
 	 * @param {Array.<Number>} path
-	 * @param {module:engine/model/position~PositionStickiness} [stickiness] Defaults to `'sticksToNext'`.
+	 * @param {module:engine/model/position~PositionStickiness} [stickiness] Position stickiness. Defaults to `'toNone'`.
 	 * See {@link module:engine/model/liveposition~LivePosition#stickiness}.
 	 */
-	constructor( root, path, stickiness ) {
-		super( root, path );
+	constructor( root, path, stickiness = 'toNone' ) {
+		super( root, path, stickiness );
 
 		if ( !this.root.is( 'rootElement' ) ) {
 			/**
@@ -53,26 +52,10 @@ export default class LivePosition extends Position {
 		}
 
 		/**
-		 * Flag representing `LivePosition` stickiness. `LivePosition` might be sticking to previous node or next node.
-		 * Whenever some nodes are inserted at the same position as `LivePosition`, `stickiness` is checked to decide if
-		 * LivePosition should be moved. Similar applies when a range of nodes is moved and one of it's boundary
-		 * position is same as `LivePosition`.
-		 *
-		 * Examples:
-		 *
-		 *		Insert:
-		 *		Position is at | and we insert at the same position, marked as ^:
-		 *		- | sticks to previous node: `<p>f|^oo</p>` => `<p>f|baroo</p>`
-		 *		- | sticks to next node: `<p>f^|oo</p>` => `<p>fbar|oo</p>`
-		 *
-		 *		Move:
-		 *		Position is at | and range [ ] is moved to position ^:
-		 *		- | sticks to previous node: `<p>f|[oo]</p><p>b^ar</p>` => `<p>f|</p><p>booar</p>`
-		 *		- | sticks to next node: `<p>f|[oo]</p><p>b^ar</p>` => `<p>f</p><p>b|ooar</p>`
+		 * Position stickiness. See {@link module:engine/model/position~PositionStickiness}.
 		 *
 		 * @member {module:engine/model/position~PositionStickiness} module:engine/model/liveposition~LivePosition#stickiness
 		 */
-		this.stickiness = stickiness || 'sticksToNext';
 
 		bindWithDocument.call( this );
 	}
@@ -136,9 +119,6 @@ export default class LivePosition extends Position {
  * @method module:engine/model/liveposition~LivePosition.bindWithDocument
  */
 function bindWithDocument() {
-	// Operation types handled by LivePosition (these are operations that change model tree structure).
-	const supportedTypes = new Set( [ 'insert', 'move', 'remove', 'reinsert' ] );
-
 	this.listenTo(
 		this.root.document.model,
 		'applyOperation',
@@ -149,9 +129,7 @@ function bindWithDocument() {
 				return;
 			}
 
-			if ( supportedTypes.has( operation.type ) ) {
-				transform.call( this, operation );
-			}
+			transform.call( this, operation );
 		},
 		{ priority: 'low' }
 	);
@@ -166,68 +144,16 @@ function bindWithDocument() {
  * @param {module:engine/model/operation/operation~Operation} operation Executed operation.
  */
 function transform( operation ) {
-	/* eslint-disable no-case-declarations */
-	let range;
-	let position;
-
-	if ( operation.type == 'insert' ) {
-		range = Range.createFromPositionAndShift( operation.position, operation.nodes.maxOffset );
-	} else {
-		range = Range.createFromPositionAndShift( operation.getMovedRangeStart(), operation.howMany );
-		position = operation.sourcePosition;
-	}
-
-	const howMany = range.end.offset - range.start.offset;
-	let transformed;
-
-	switch ( operation.type ) {
-		case 'insert':
-			const insertBefore = this.stickiness == 'sticksToNext';
-			transformed = this._getTransformedByInsertion( range.start, howMany, insertBefore );
-			break;
-
-		case 'move':
-		case 'remove':
-		case 'reinsert':
-			const originalRange = Range.createFromPositionAndShift( position, howMany );
-
-			const gotMoved = originalRange.containsPosition( this ) ||
-				( originalRange.start.isEqual( this ) && this.stickiness == 'sticksToNext' ) ||
-				( originalRange.end.isEqual( this ) && this.stickiness == 'sticksToPrevious' );
+	const result = this.getTransformedByOperation( operation );
 
-			// We can't use ._getTransformedByMove() because we have a different if-condition.
-			if ( gotMoved ) {
-				transformed = this._getCombined( position, range.start );
-			} else {
-				const insertBefore = this.stickiness == 'sticksToNext';
-
-				// `Position._getTransformedByMove` is expecting `targetPosition` to be "before" move
-				// (before transformation). `range.start` is already after the move happened.
-				// We have to revert `targetPosition` to the state before the move.
-				const targetPosition = range.start._getTransformedByInsertion( position, howMany );
-
-				transformed = this._getTransformedByMove( position, targetPosition, howMany, insertBefore );
-			}
-			break;
-	}
-
-	if ( !this.isEqual( transformed ) ) {
+	if ( !this.isEqual( result ) ) {
 		const oldPosition = Position.createFromPosition( this );
 
-		this.path = transformed.path;
-		this.root = transformed.root;
+		this.path = result.path;
+		this.root = result.root;
 
 		this.fire( 'change', oldPosition );
 	}
-	/* eslint-enable no-case-declarations */
 }
 
 mix( LivePosition, EmitterMixin );
-
-/**
- * Enum representing how position is "sticking" with their neighbour nodes.
- * Possible values: `'sticksToNext'`, `'sticksToPrevious'`.
- *
- * @typedef {String} module:engine/model/position~PositionStickiness
- */
-

+ 28 - 62
packages/ckeditor5-engine/src/model/liverange.js

@@ -116,9 +116,6 @@ export default class LiveRange extends Range {
  * @method module:engine/model/liverange~LiveRange#bindWithDocument
  */
 function bindWithDocument() {
-	// Operation types that a range can be transformed by.
-	const supportedTypes = new Set( [ 'insert', 'move', 'remove', 'reinsert' ] );
-
 	this.listenTo(
 		this.root.document.model,
 		'applyOperation',
@@ -129,9 +126,7 @@ function bindWithDocument() {
 				return;
 			}
 
-			if ( supportedTypes.has( operation.type ) ) {
-				transform.call( this, operation );
-			}
+			transform.call( this, operation );
 		},
 		{ priority: 'low' }
 	);
@@ -146,71 +141,42 @@ function bindWithDocument() {
  * @param {module:engine/model/operation/operation~Operation} operation Executed operation.
  */
 function transform( operation ) {
-	const changeType = operation.type;
-	const batch = operation.delta.batch;
-
-	let targetRange;
-	let sourcePosition;
-
-	if ( changeType == 'insert' ) {
-		targetRange = Range.createFromPositionAndShift( operation.position, operation.nodes.maxOffset );
-	} else {
-		targetRange = Range.createFromPositionAndShift( operation.getMovedRangeStart(), operation.howMany );
-		sourcePosition = operation.sourcePosition;
-	}
-
-	const howMany = targetRange.end.offset - targetRange.start.offset;
-	let targetPosition = targetRange.start;
-
-	if ( changeType == 'move' || changeType == 'remove' || changeType == 'reinsert' ) {
-		// Range._getTransformedByDocumentChange is expecting `targetPosition` to be "before" move
-		// (before transformation). `targetRange.start` is already after the move happened.
-		// We have to revert `targetPosition` to the state before the move.
-		targetPosition = targetPosition._getTransformedByInsertion( sourcePosition, howMany );
-	}
-
-	const result = this._getTransformedByDocumentChange( changeType, operation.delta.type, targetPosition, howMany, sourcePosition );
-
-	// Decide whether moved part should be included in the range.
-	//
-	// First, this concerns only `move` change, because insert change includes inserted part always (changeType == 'move').
-	// Second, this is a case only if moved range was intersecting with this range and was inserted into this range (result.length == 3).
-	if ( ( changeType == 'move' || changeType == 'remove' || changeType == 'reinsert' ) && result.length == 3 ) {
-		// `result[ 2 ]` is a "common part" of this range and moved range. We substitute that common part with the whole
-		// `targetRange` because we want to include whole `targetRange` in this range.
-		result[ 2 ] = targetRange;
-	}
-
-	const updated = Range.createFromRanges( result );
-
-	const boundariesChanged = !updated.isEqual( this );
-
-	const rangeExpanded = this.containsPosition( targetPosition );
-	const rangeShrunk = sourcePosition && ( this.containsPosition( sourcePosition ) || this.start.isEqual( sourcePosition ) );
-	const contentChanged = rangeExpanded || rangeShrunk;
+	const ranges = this.getTransformedByOperation( operation );
+	const result = Range.createFromRanges( ranges );
+	const boundariesChanged = !result.isEqual( this );
+	const contentChanged = doesOperationChangeRangeContent( this, operation );
 
 	if ( boundariesChanged ) {
 		// If range boundaries have changed, fire `change:range` event.
 		const oldRange = Range.createFromRange( this );
 
-		this.start = updated.start;
-		this.end = updated.end;
+		this.start = result.start;
+		this.end = result.end;
 
-		this.fire( 'change:range', oldRange, {
-			type: changeType,
-			batch,
-			range: targetRange,
-			sourcePosition
-		} );
+		this.fire( 'change:range', oldRange, operation );
 	} else if ( contentChanged ) {
 		// If range boundaries have not changed, but there was change inside the range, fire `change:content` event.
-		this.fire( 'change:content', Range.createFromRange( this ), {
-			type: changeType,
-			batch,
-			range: targetRange,
-			sourcePosition
-		} );
+		this.fire( 'change:content', Range.createFromRange( this ), operation );
 	}
 }
 
+function doesOperationChangeRangeContent( range, operation ) {
+	switch ( operation.type ) {
+		case 'insert':
+		case 'split':
+		case 'wrap':
+		case 'unwrap':
+			return range.containsPosition( operation.position );
+		case 'move':
+		case 'remove':
+		case 'reinsert':
+		case 'merge':
+			return range.containsPosition( operation.sourcePosition ) ||
+				range.start.isEqual( operation.sourcePosition ) ||
+				range.containsPosition( operation.targetPosition );
+	}
+
+	return false;
+}
+
 mix( LiveRange, EmitterMixin );

+ 1 - 26
packages/ckeditor5-engine/src/model/model.js

@@ -7,10 +7,6 @@
  * @module engine/model/model
  */
 
-// Load all basic deltas and transformations, they register themselves.
-import './delta/basic-deltas';
-import './delta/basic-transformations';
-
 import Batch from './batch';
 import Writer from './writer';
 import Schema from './schema';
@@ -18,7 +14,6 @@ import Document from './document';
 import MarkerCollection from './markercollection';
 import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
-import deltaTransform from './delta/transform';
 import ModelElement from './element';
 import ModelRange from './range';
 
@@ -221,26 +216,6 @@ export default class Model {
 		operation._execute();
 	}
 
-	/**
-	 * Transforms two sets of deltas by themselves. Returns both transformed sets.
-	 *
-	 * @param {Array.<module:engine/model/delta/delta~Delta>} deltasA Array with the first set of deltas to transform. These
-	 * deltas are considered more important (than `deltasB`) when resolving conflicts.
-	 * @param {Array.<module:engine/model/delta/delta~Delta>} deltasB Array with the second set of deltas to transform. These
-	 * deltas are considered less important (than `deltasA`) when resolving conflicts.
-	 * @param {Boolean} [useContext=false] When set to `true`, transformation will store and use additional context
-	 * information to guarantee more expected results. Should be used whenever deltas related to already applied
-	 * deltas are transformed (for example when undoing changes).
-	 * @returns {Object}
-	 * @returns {Array.<module:engine/model/delta/delta~Delta>} return.deltasA The first set of deltas transformed
-	 * by the second set of deltas.
-	 * @returns {Array.<module:engine/model/delta/delta~Delta>} return.deltasB The second set of deltas transformed
-	 * by the first set of deltas.
-	 */
-	transformDeltas( deltasA, deltasB, useContext = false ) {
-		return deltaTransform.transformDeltaSets( deltasA, deltasB, useContext ? this.document : null );
-	}
-
 	/**
 	 * Inserts content into the editor (specified selection) as one would expect the paste
 	 * functionality to work.
@@ -268,7 +243,7 @@ export default class Model {
 	 * @fires deleteContent
 	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
 	 * Selection of which the content should be deleted.
-	 * @param {module:engine/model/batch~Batch} batch Batch to which the deltas will be added.
+	 * @param {module:engine/model/batch~Batch} batch Batch to which the operations will be added.
 	 * @param {Object} [options]
 	 * @param {Boolean} [options.leaveUnmerged=false] Whether to merge elements after removing the content of the selection.
 	 *

+ 11 - 2
packages/ckeditor5-engine/src/model/operation/attributeoperation.js

@@ -33,7 +33,7 @@ export default class AttributeOperation extends Operation {
 	 * If both `newValue` and `oldValue` are set, then the operation will change the attribute value. Note that all nodes in
 	 * operation's ranges must already have an attribute with given key and `oldValue` as value
 	 *
-	 * @param {module:engine/model/range~Range} range Range on which the operation should be applied.
+	 * @param {module:engine/model/range~Range} range Range on which the operation should be applied. Must be a flat range.
 	 * @param {String} key Key of an attribute to change or remove.
 	 * @param {*} oldValue Old value of the attribute with given key or `null`, if attribute was not set before.
 	 * @param {*} newValue New value of the attribute with given key or `null`, if operation should remove attribute.
@@ -111,7 +111,16 @@ export default class AttributeOperation extends Operation {
 	 * @inheritDoc
 	 */
 	_validate() {
-		for ( const item of this.range.getItems() ) {
+		if ( !this.range.isFlat ) {
+			/**
+			 * The range to change is not flat.
+			 *
+			 * @error attribute-operation-range-not-flat
+			 */
+			throw new CKEditorError( 'attribute-operation-range-not-flat: The range to change is not flat.' );
+		}
+
+		for ( const item of this.range.getItems( { shallow: true } ) ) {
 			if ( this.oldValue !== null && !isEqual( item.getAttribute( this.key ), this.oldValue ) ) {
 				/**
 				 * Changed node has different attribute value than operation's old attribute value.

+ 21 - 2
packages/ckeditor5-engine/src/model/operation/insertoperation.js

@@ -48,6 +48,15 @@ export default class InsertOperation extends Operation {
 		 * @member {module:engine/model/nodelist~NodeList} module:engine/model/operation/insertoperation~InsertOperation#nodeList
 		 */
 		this.nodes = new NodeList( _normalizeNodes( nodes ) );
+
+		/**
+		 * Flag deciding how the operation should be transformed. If set to `true`, nodes might get additional attributes
+		 * during operational transformation. This happens, when operation insertion position points to inside a
+		 * range which attributes have changed.
+		 *
+		 * @member {Boolean} module:engine/model/operation/insertoperation~InsertOperation#shouldReceiveAttributes
+		 */
+		this.shouldReceiveAttributes = false;
 	}
 
 	/**
@@ -57,6 +66,10 @@ export default class InsertOperation extends Operation {
 		return 'insert';
 	}
 
+	get howMany() {
+		return this.nodes.maxOffset;
+	}
+
 	/**
 	 * Creates and returns an operation that has the same parameters as this operation.
 	 *
@@ -64,8 +77,11 @@ export default class InsertOperation extends Operation {
 	 */
 	clone() {
 		const nodes = new NodeList( [ ...this.nodes ].map( node => node._clone( true ) ) );
+		const insert = new InsertOperation( this.position, nodes, this.baseVersion );
 
-		return new InsertOperation( this.position, nodes, this.baseVersion );
+		insert.shouldReceiveAttributes = this.shouldReceiveAttributes;
+
+		return insert;
 	}
 
 	/**
@@ -139,6 +155,9 @@ export default class InsertOperation extends Operation {
 			}
 		}
 
-		return new InsertOperation( Position.fromJSON( json.position, document ), children, json.baseVersion );
+		const insert = new InsertOperation( Position.fromJSON( json.position, document ), children, json.baseVersion );
+		insert.shouldReceiveAttributes = json.shouldReceiveAttributes;
+
+		return insert;
 	}
 }

+ 166 - 0
packages/ckeditor5-engine/src/model/operation/mergeoperation.js

@@ -0,0 +1,166 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/model/operation/mergeoperation
+ */
+
+import Operation from './operation';
+import SplitOperation from './splitoperation';
+import Position from '../position';
+import Range from '../range';
+import { _move } from './utils';
+
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
+/**
+ * Operation to merge two {@link module:engine/model/element~Element elements}. The merged elements are a parent of given
+ * {@link module:engine/model/position~Position position} and the next element.
+ *
+ * Technically, the content of the next element is moved at given {@link module:engine/model/position~Position position}
+ * and the element is removed.
+ *
+ * @extends module:engine/model/operation/operation~Operation
+ */
+export default class MergeOperation extends Operation {
+	/**
+	 * Creates a merge operation.
+	 *
+	 * @param {module:engine/model/position~Position} sourcePosition Position inside the merged element. All nodes from that
+	 * element after that position will be moved to {@link ~#targetPosition}.
+	 * @param {module:engine/model/position~Position} targetPosition Position which the nodes from the merged elements will be moved to.
+	 * @param {module:engine/model/position~Position} graveyardPosition Position in graveyard to which the merged element will be moved.
+	 * @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
+	 * can be applied or `null` if the operation operates on detached (non-document) tree.
+	 */
+	constructor( sourcePosition, targetPosition, graveyardPosition, baseVersion ) {
+		super( baseVersion );
+
+		/**
+		 * Position inside the merged element. All nodes from that element after that position will be moved to {@link ~#targetPosition}.
+		 *
+		 * @member {module:engine/model/position~Position} module:engine/model/operation/mergeoperation~MergeOperation#sourcePosition
+		 */
+		this.sourcePosition = Position.createFromPosition( sourcePosition );
+		this.sourcePosition.stickiness = 'toPrevious'; // This is, and should always remain, the first position in its parent.
+
+		/**
+		 * Position which the nodes from the merged elements will be moved to.
+		 *
+		 * @member {module:engine/model/position~Position} module:engine/model/operation/mergeoperation~MergeOperation#targetPosition
+		 */
+		this.targetPosition = Position.createFromPosition( targetPosition );
+		this.targetPosition.stickiness = 'toNext'; // This is, and should always remain, the last position in its parent.
+		// is it? think about reversed split operations, undo, etc.
+
+		this.graveyardPosition = Position.createFromPosition( graveyardPosition );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	get type() {
+		return 'merge';
+	}
+
+	/**
+	 * Position before the merged element (which will be removed). Calculated based on the split position.
+	 *
+	 * @readonly
+	 * @type {module:engine/model/position~Position}
+	 */
+	get deletionPosition() {
+		return new Position( this.sourcePosition.root, this.sourcePosition.path.slice( 0, -1 ) );
+	}
+
+	/**
+	 * Artificial range that contains all the nodes from the merged element that will be moved to {@link ~#sourcePosition}.
+	 * The range starts at {@link ~#sourcePosition} and ends in the same parent, at `POSITIVE_INFINITY` offset.
+	 *
+	 * @readonly
+	 * @type {module:engine/model/range~Range}
+	 */
+	get movedRange() {
+		const end = this.sourcePosition.getShiftedBy( Number.POSITIVE_INFINITY );
+
+		return new Range( this.sourcePosition, end );
+	}
+
+	/**
+	 * Creates and returns an operation that has the same parameters as this operation.
+	 *
+	 * @returns {module:engine/model/operation/mergeoperation~MergeOperation} Clone of this operation.
+	 */
+	clone() {
+		return new this.constructor( this.sourcePosition, this.targetPosition, this.graveyardPosition, this.baseVersion );
+	}
+
+	/**
+	 * See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
+	 *
+	 * @returns {module:engine/model/operation/splitoperation~SplitOperation}
+	 */
+	getReversed() {
+		return new SplitOperation( this.targetPosition, this.graveyardPosition, this.baseVersion + 1 );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	_validate() {
+		const sourceElement = this.sourcePosition.parent;
+		const targetElement = this.targetPosition.parent;
+
+		// Validate whether merge operation has correct parameters.
+		if ( !sourceElement || !sourceElement.is( 'element' ) ) {
+			/**
+			 * Merge source position is invalid.
+			 *
+			 * @error merge-operation-source-position-invalid
+			 */
+			throw new CKEditorError( 'merge-operation-source-position-invalid: Merge source position is invalid.' );
+		} else if ( !targetElement || !targetElement.is( 'element' ) ) {
+			/**
+			 * Merge target position is invalid.
+			 *
+			 * @error merge-operation-target-position-invalid
+			 */
+			throw new CKEditorError( 'merge-operation-target-position-invalid: Merge target position is invalid.' );
+		}
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	_execute() {
+		const mergedElement = this.sourcePosition.parent;
+		const sourceRange = Range.createIn( mergedElement );
+
+		_move( sourceRange, this.targetPosition );
+		_move( Range.createOn( mergedElement ), this.graveyardPosition );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get className() {
+		return 'engine.model.operation.MergeOperation';
+	}
+
+	/**
+	 * Creates `MergeOperation` object from deserilized object, i.e. from parsed JSON string.
+	 *
+	 * @param {Object} json Deserialized JSON object.
+	 * @param {module:engine/model/document~Document} document Document on which this operation will be applied.
+	 * @returns {module:engine/model/operation/mergeoperation~MergeOperation}
+	 */
+	static fromJSON( json, document ) {
+		const sourcePosition = Position.fromJSON( json.sourcePosition, document );
+		const targetPosition = Position.fromJSON( json.targetPosition, document );
+		const graveyardPosition = Position.fromJSON( json.graveyardPosition, document );
+
+		return new this( sourcePosition, targetPosition, graveyardPosition, json.baseVersion );
+	}
+}

+ 6 - 25
packages/ckeditor5-engine/src/model/operation/moveoperation.js

@@ -41,6 +41,8 @@ export default class MoveOperation extends Operation {
 		 * @member {module:engine/model/position~Position} module:engine/model/operation/moveoperation~MoveOperation#sourcePosition
 		 */
 		this.sourcePosition = Position.createFromPosition( sourcePosition );
+		this.sourcePosition.stickiness = 'toNext';
+		// maybe lets change sourcePosition + howMany to a range? flattness will be guaranteed by writer anyway
 
 		/**
 		 * Offset size of moved range.
@@ -55,16 +57,7 @@ export default class MoveOperation extends Operation {
 		 * @member {module:engine/model/position~Position} module:engine/model/operation/moveoperation~MoveOperation#targetPosition
 		 */
 		this.targetPosition = Position.createFromPosition( targetPosition );
-
-		/**
-		 * Defines whether `MoveOperation` is sticky. If `MoveOperation` is sticky, during
-		 * {@link module:engine/model/operation/transform~transform operational transformation} if there will be an operation that
-		 * inserts some nodes at the position equal to the boundary of this `MoveOperation`, that operation will
-		 * get their insertion path updated to the position where this `MoveOperation` moves the range.
-		 *
-		 * @member {Boolean} module:engine/model/operation/moveoperation~MoveOperation#isSticky
-		 */
-		this.isSticky = false;
+		this.targetPosition.stickiness = 'toNone';
 	}
 
 	/**
@@ -80,10 +73,7 @@ export default class MoveOperation extends Operation {
 	 * @returns {module:engine/model/operation/moveoperation~MoveOperation} Clone of this operation.
 	 */
 	clone() {
-		const op = new this.constructor( this.sourcePosition, this.howMany, this.targetPosition, this.baseVersion );
-		op.isSticky = this.isSticky;
-
-		return op;
+		return new this.constructor( this.sourcePosition, this.howMany, this.targetPosition, this.baseVersion );
 	}
 
 	/**
@@ -112,10 +102,7 @@ export default class MoveOperation extends Operation {
 	getReversed() {
 		const newTargetPosition = this.sourcePosition._getTransformedByInsertion( this.targetPosition, this.howMany );
 
-		const op = new this.constructor( this.getMovedRangeStart(), this.howMany, newTargetPosition, this.baseVersion + 1 );
-		op.isSticky = this.isSticky;
-
-		return op;
+		return new this.constructor( this.getMovedRangeStart(), this.howMany, newTargetPosition, this.baseVersion + 1 );
 	}
 
 	/**
@@ -200,12 +187,6 @@ export default class MoveOperation extends Operation {
 		const sourcePosition = Position.fromJSON( json.sourcePosition, document );
 		const targetPosition = Position.fromJSON( json.targetPosition, document );
 
-		const move = new this( sourcePosition, json.howMany, targetPosition, json.baseVersion );
-
-		if ( json.isSticky ) {
-			move.isSticky = true;
-		}
-
-		return move;
+		return new this( sourcePosition, json.howMany, targetPosition, json.baseVersion );
 	}
 }

+ 8 - 7
packages/ckeditor5-engine/src/model/operation/operation.js

@@ -40,6 +40,8 @@ export default class Operation {
 		 */
 		this.isDocumentOperation = this.baseVersion !== null;
 
+		this.batch = null;
+
 		/**
 		 * Operation type.
 		 *
@@ -48,11 +50,11 @@ export default class Operation {
 		 */
 
 		/**
-		 * {@link module:engine/model/delta/delta~Delta Delta} which the operation is a part of. This property is set by the
-		 * {@link module:engine/model/delta/delta~Delta delta} when the operations is added to it by the
-		 * {@link module:engine/model/delta/delta~Delta#addOperation} method.
+		 * {@link module:engine/model/operation/operation~Operation Operation} which the operation is a part of. This property is set by the
+		 * {@link module:engine/model/operation/operation~Operation operation} when the operations is added to it by the
+		 * {@link module:engine/model/operation/operation~Operation#addOperation} method.
 		 *
-		 * @member {module:engine/model/delta/delta~Delta} #delta
+		 * @member {module:engine/model/operation/operation~Operation} #operation
 		 */
 
 		/**
@@ -97,15 +99,14 @@ export default class Operation {
 	 * Custom toJSON method to solve child-parent circular dependencies.
 	 *
 	 * @method #toJSON
-	 * @returns {Object} Clone of this object with the delta property replaced with string.
+	 * @returns {Object} Clone of this object with the operation property replaced with string.
 	 */
 	toJSON() {
 		const json = clone( this, true );
 
 		json.__className = this.constructor.className;
 
-		// Remove parent delta to avoid circular dependencies.
-		delete json.delta;
+		delete json.batch;
 
 		// Only document operations are shared with other clients so it is not necessary to keep this information.
 		delete json.isDocumentOperation;

+ 8 - 0
packages/ckeditor5-engine/src/model/operation/operationfactory.js

@@ -17,6 +17,10 @@ import ReinsertOperation from '../operation/reinsertoperation';
 import RemoveOperation from '../operation/removeoperation';
 import RenameOperation from '../operation/renameoperation';
 import RootAttributeOperation from '../operation/rootattributeoperation';
+import SplitOperation from '../operation/splitoperation';
+import MergeOperation from '../operation/mergeoperation';
+import WrapOperation from '../operation/wrapoperation';
+import UnwrapOperation from '../operation/unwrapoperation';
 
 const operations = {};
 operations[ AttributeOperation.className ] = AttributeOperation;
@@ -29,6 +33,10 @@ operations[ ReinsertOperation.className ] = ReinsertOperation;
 operations[ RemoveOperation.className ] = RemoveOperation;
 operations[ RenameOperation.className ] = RenameOperation;
 operations[ RootAttributeOperation.className ] = RootAttributeOperation;
+operations[ SplitOperation.className ] = SplitOperation;
+operations[ MergeOperation.className ] = MergeOperation;
+operations[ WrapOperation.className ] = WrapOperation;
+operations[ UnwrapOperation.className ] = UnwrapOperation;
 
 /**
  * A factory class for creating operations.

+ 1 - 0
packages/ckeditor5-engine/src/model/operation/renameoperation.js

@@ -38,6 +38,7 @@ export default class RenameOperation extends Operation {
 		 * @member {module:engine/model/position~Position} module:engine/model/operation/renameoperation~RenameOperation#position
 		 */
 		this.position = position;
+		this.position.stickiness = 'toNext';
 
 		/**
 		 * Current name of the element.

+ 175 - 0
packages/ckeditor5-engine/src/model/operation/splitoperation.js

@@ -0,0 +1,175 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/model/operation/splitoperation
+ */
+
+import Operation from './operation';
+import MergeOperation from './mergeoperation';
+import Position from '../position';
+import Range from '../range';
+import { _insert, _move } from './utils';
+
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
+/**
+ * Operation to split {@link module:engine/model/element~Element an element} at
+ * given {@link module:engine/model/position~Position position} into two elements, both containing a part of the element's content.
+ *
+ * @extends module:engine/model/operation/operation~Operation
+ */
+export default class SplitOperation extends Operation {
+	/**
+	 * Creates a split operation.
+	 *
+	 * @param {module:engine/model/position~Position} position Position at which an element should be split.
+	 * @param {module:engine/model/position~Position|null} graveyardPosition Position in graveyard before the element which
+	 * should be used as a parent of the nodes after `position`. If it is not set, a copy of the the `position` parent will be used.
+	 * @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
+	 * can be applied or `null` if the operation operates on detached (non-document) tree.
+	 */
+	constructor( position, graveyardPosition, baseVersion ) {
+		super( baseVersion );
+
+		/**
+		 * Position at which an element should be split.
+		 *
+		 * @member {module:engine/model/position~Position} module:engine/model/operation/splitoperation~SplitOperation#position
+		 */
+		this.position = Position.createFromPosition( position );
+		this.position.stickiness = 'toNext';
+
+		this.graveyardPosition = graveyardPosition ? Position.createFromPosition( graveyardPosition ) : null;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	get type() {
+		return 'split';
+	}
+
+	/**
+	 * Position after the split element. This is a position at which the clone of split element will be inserted.
+	 * Calculated based on the split position.
+	 *
+	 * @readonly
+	 * @type {module:engine/model/position~Position}
+	 */
+	get insertionPosition() {
+		const path = this.position.path.slice( 0, -1 );
+		path[ path.length - 1 ]++;
+
+		return new Position( this.position.root, path );
+	}
+
+	/**
+	 * Position inside the new clone of a split element. This is a position where nodes from after the split position will
+	 * be moved to. Calculated based on the split position.
+	 *
+	 * @readonly
+	 * @type {module:engine/model/position~Position}
+	 */
+	get moveTargetPosition() {
+		const path = this.position.path.slice( 0, -1 );
+		path[ path.length - 1 ]++;
+		path.push( 0 );
+
+		return new Position( this.position.root, path );
+	}
+
+	/**
+	 * Artificial range that contains all the nodes from the split element that will be moved to the new element.
+	 * The range starts at {@link ~#position} and ends in the same parent, at `POSITIVE_INFINITY` offset.
+	 *
+	 * @readonly
+	 * @type {module:engine/model/range~Range}
+	 */
+	get movedRange() {
+		const end = this.position.getShiftedBy( Number.POSITIVE_INFINITY );
+
+		return new Range( this.position, end );
+	}
+
+	/**
+	 * Creates and returns an operation that has the same parameters as this operation.
+	 *
+	 * @returns {module:engine/model/operation/splitoperation~SplitOperation} Clone of this operation.
+	 */
+	clone() {
+		return new this.constructor( this.position, this.graveyardPosition, this.baseVersion );
+	}
+
+	/**
+	 * See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
+	 *
+	 * @returns {module:engine/model/operation/mergeoperation~MergeOperation}
+	 */
+	getReversed() {
+		const graveyard = this.position.root.document.graveyard;
+		const graveyardPosition = new Position( graveyard, [ 0 ] );
+
+		return new MergeOperation( this.moveTargetPosition, this.position, graveyardPosition, this.baseVersion + 1 );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	_validate() {
+		const element = this.position.parent;
+		const offset = this.position.offset;
+
+		// Validate whether split operation has correct parameters.
+		if ( !element || element.maxOffset < offset ) {
+			/**
+			 * Split position is invalid.
+			 *
+			 * @error split-operation-position-invalid
+			 */
+			throw new CKEditorError( 'split-operation-position-invalid: Split position is invalid.' );
+		}
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	_execute() {
+		const splitElement = this.position.parent;
+
+		if ( this.graveyardPosition ) {
+			_move( Range.createFromPositionAndShift( this.graveyardPosition, 1 ), this.insertionPosition );
+		} else {
+			const newElement = splitElement._clone();
+
+			_insert( this.insertionPosition, newElement );
+		}
+
+		const sourceRange = Range.createFromParentsAndOffsets( splitElement, this.position.offset, splitElement, splitElement.maxOffset );
+
+		_move( sourceRange, this.moveTargetPosition );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get className() {
+		return 'engine.model.operation.SplitOperation';
+	}
+
+	/**
+	 * Creates `SplitOperation` object from deserilized object, i.e. from parsed JSON string.
+	 *
+	 * @param {Object} json Deserialized JSON object.
+	 * @param {module:engine/model/document~Document} document Document on which this operation will be applied.
+	 * @returns {module:engine/model/operation/splitoperation~SplitOperation}
+	 */
+	static fromJSON( json, document ) {
+		const position = Position.fromJSON( json.position, document );
+		const graveyardPosition = json.graveyardPosition ? Position.fromJSON( json.graveyardPosition, document ) : null;
+
+		return new this( position, graveyardPosition, json.baseVersion );
+	}
+}

Разница между файлами не показана из-за своего большого размера
+ 1622 - 530
packages/ckeditor5-engine/src/model/operation/transform.js


+ 153 - 0
packages/ckeditor5-engine/src/model/operation/unwrapoperation.js

@@ -0,0 +1,153 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/model/operation/unwrapoperation
+ */
+
+import Operation from './operation';
+import WrapOperation from './wrapoperation';
+import Position from '../position';
+import Range from '../range';
+import { _move } from './utils';
+
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
+/**
+ * Operation to unwrap a {@link module:engine/model/element~Element model element}. In the result, the unwrapped element
+ * is removed and its children are moved in its place.
+ *
+ * @extends module:engine/model/operation/operation~Operation
+ */
+export default class UnwrapOperation extends Operation {
+	/**
+	 * Creates an unwrap operation.
+	 *
+	 * @param {module:engine/model/position~Position} position Position inside the element to unwrap.
+	 * @param {Number} howMany How many nodes are inside unwrapped element.
+	 * @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
+	 * @param {module:engine/model/position~Position} graveyardPosition Position in graveyard to which the unwrapped element will be moved.
+	 * can be applied or `null` if the operation operates on detached (non-document) tree.
+	 */
+	constructor( position, howMany, graveyardPosition, baseVersion ) {
+		super( baseVersion );
+
+		/**
+		 * Position inside the element to unwrap.
+		 *
+		 * @member {module:engine/model/position~Position} module:engine/model/operation/unwrapoperation~UnwrapOperation#position
+		 */
+		this.position = Position.createFromPosition( position );
+		this.position.stickiness = 'toPrevious'; // Keep the position always at the beginning of the element.
+
+		this.graveyardPosition = Position.createFromPosition( graveyardPosition );
+
+		/**
+		 * How many nodes are inside unwrapped element.
+		 *
+		 * This information is needed to properly reverse `UnwrapOperation` and to properly transform by `UnwrapOperation`.
+		 *
+		 * @member {Number} module:engine/model/operation/unwrapoperation~UnwrapOperation#_howMany
+		 */
+		this.howMany = howMany;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	get type() {
+		return 'unwrap';
+	}
+
+	/**
+	 * A range containing all nodes that will be unwrapped.
+	 *
+	 * @readonly
+	 * @type {module:engine/model/range~Range}
+	 */
+	get unwrappedRange() {
+		return Range.createFromPositionAndShift( this.position, this.howMany );
+	}
+
+	get targetPosition() {
+		const path = this.position.path.slice( 0, -1 );
+
+		return new Position( this.position.root, path );
+	}
+
+	/**
+	 * Creates and returns an operation that has the same parameters as this operation.
+	 *
+	 * @returns {module:engine/model/operation/unwrapoperation~UnwrapOperation} Clone of this operation.
+	 */
+	clone() {
+		return new this.constructor( this.position, this.howMany, this.graveyardPosition, this.baseVersion );
+	}
+
+	/**
+	 * See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
+	 *
+	 * @returns {module:engine/model/operation/wrapoperation~WrapOperation}
+	 */
+	getReversed() {
+		return new WrapOperation( this.targetPosition, this.howMany, this.graveyardPosition, this.baseVersion + 1 );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	_validate() {
+		const element = this.position.parent;
+
+		// Validate whether unwrap operation has correct parameters.
+		if ( !element || !element.is( 'element' ) ) {
+			/**
+			 * Unwrap position is invalid.
+			 *
+			 * @error unwrap-operation-position-invalid
+			 */
+			throw new CKEditorError( 'unwrap-operation-position-invalid: Unwrap position is invalid.' );
+		} else if ( element.maxOffset !== this.howMany ) {
+			/**
+			 * Operation specifies incorrect number of nodes to unwrap.
+			 *
+			 * @error unwrap-operation-incorrect-how-many
+			 */
+			throw new CKEditorError( 'unwrap-operation-incorrect-how-many: Operation specifies incorrect number of nodes to unwrap.' );
+		}
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	_execute() {
+		const elementToUnwrap = this.position.parent;
+		const targetPosition = Position.createAfter( elementToUnwrap );
+
+		_move( this.unwrappedRange, targetPosition );
+		_move( Range.createOn( elementToUnwrap ), this.graveyardPosition );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get className() {
+		return 'engine.model.operation.UnwrapOperation';
+	}
+
+	/**
+	 * Creates `UnwrapOperation` object from deserilized object, i.e. from parsed JSON string.
+	 *
+	 * @param {Object} json Deserialized JSON object.
+	 * @param {module:engine/model/document~Document} document Document on which this operation will be applied.
+	 * @returns {module:engine/model/operation/unwrapoperation~UnwrapOperation}
+	 */
+	static fromJSON( json, document ) {
+		const position = Position.fromJSON( json.position, document );
+		const graveyardPosition = Position.fromJSON( json.graveyardPosition, document );
+
+		return new this( position, json.howMany, graveyardPosition, json.baseVersion );
+	}
+}

+ 4 - 4
packages/ckeditor5-engine/src/model/operation/utils.js

@@ -119,11 +119,11 @@ export function _move( sourceRange, targetPosition ) {
 }
 
 /**
- * Sets given attribute on nodes in given range.
+ * Sets given attribute on nodes in given range. The attributes are only set on top-level nodes of the range, not on its children.
  *
  * @protected
  * @function module:engine/model/operation/utils~utils._setAttribute
- * @param {module:engine/model/range~Range} range Range containing nodes that should have the attribute set.
+ * @param {module:engine/model/range~Range} range Range containing nodes that should have the attribute set. Should be flat.
  * @param {String} key Key of attribute to set.
  * @param {*} value Attribute value.
  */
@@ -133,7 +133,7 @@ export function _setAttribute( range, key, value ) {
 	_splitNodeAtPosition( range.end );
 
 	// Iterate over all items in the range.
-	for ( const item of range.getItems() ) {
+	for ( const item of range.getItems( { shallow: true } ) ) {
 		// Iterator will return `TextProxy` instances but we know that those text proxies will
 		// always represent full text nodes (this is guaranteed thanks to splitting we did before).
 		// So, we can operate on those text proxies' text nodes.
@@ -191,7 +191,7 @@ export function _normalizeNodes( nodes ) {
 		const prev = normalized[ i - 1 ];
 
 		if ( node instanceof Text && prev instanceof Text && _haveSameAttributes( node, prev ) ) {
-			// Doing this instead changing prev.data because .data is readonly.
+			// Doing this instead changing `prev.data` because `data` is readonly.
 			normalized.splice( i - 1, 2, new Text( prev.data + node.data, prev.getAttributes() ) );
 			i--;
 		}

+ 179 - 0
packages/ckeditor5-engine/src/model/operation/wrapoperation.js

@@ -0,0 +1,179 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/model/operation/wrapoperation
+ */
+
+import Operation from './operation';
+import UnwrapOperation from './unwrapoperation';
+import Position from '../position';
+import Range from '../range';
+import Element from '../element';
+import { _insert, _move } from './utils';
+
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+
+/**
+ * Operation to wrap a range of {@link module:engine/model/item~Item model items} with
+ * a {@link module:engine/model/element~Element model element}.
+ *
+ * @extends module:engine/model/operation/operation~Operation
+ */
+export default class WrapOperation extends Operation {
+	/**
+	 * Creates a wrap operation.
+	 *
+	 * @param {module:engine/model/position~Position} position Position before
+	 * the first {@link module:engine/model/item~Item model item} to wrap.
+	 * @param {Number} howMany Offset size of wrapped range. Wrapped range will start at `position.offset` and end at
+	 * `position.offset + howMany`.
+	 * @param {module:engine/model/element~Element|module:engine/model/position~Position} elementOrPosition Element to
+	 * wrap with or position in graveyard before the element which should be used as a wrapper.
+	 * @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
+	 * can be applied or `null` if the operation operates on detached (non-document) tree.
+	 */
+	constructor( position, howMany, elementOrPosition, baseVersion ) {
+		super( baseVersion );
+
+		/**
+		 * Position before the first {@link module:engine/model/item~Item model item} to wrap.
+		 *
+		 * @member {module:engine/model/position~Position} module:engine/model/operation/wrapoperation~WrapOperation#position
+		 */
+		this.position = Position.createFromPosition( position );
+		this.position.stickiness = 'toNext';
+		// maybe change to a range
+
+		/**
+		 * Offset size of wrapped range.
+		 *
+		 * @member {Number} module:engine/model/operation/wrapoperation~WrapOperation#howMany
+		 */
+		this.howMany = howMany;
+
+		/**
+		 * Element to wrap with.
+		 *
+		 * @member {module:engine/model/element~Element} module:engine/model/operation/wrapoperation~WrapOperation#element
+		 */
+		this.element = elementOrPosition instanceof Element ? elementOrPosition : null;
+
+		this.graveyardPosition = elementOrPosition instanceof Element ? null : Position.createFromPosition( elementOrPosition );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	get type() {
+		return 'wrap';
+	}
+
+	/**
+	 * Position to which the wrapped elements will be moved. This is a position at the beginning of the wrapping element.
+	 *
+	 * @readonly
+	 * @type {module:engine/model/position~Position}
+	 */
+	get targetPosition() {
+		const path = this.position.path.slice();
+		path.push( 0 );
+
+		return new Position( this.position.root, path );
+	}
+
+	/**
+	 * A range containing all nodes that will be wrapped.
+	 *
+	 * @readonly
+	 * @type {module:engine/model/range~Range}
+	 */
+	get wrappedRange() {
+		return Range.createFromPositionAndShift( this.position, this.howMany );
+	}
+
+	/**
+	 * Creates and returns an operation that has the same parameters as this operation.
+	 *
+	 * @returns {module:engine/model/operation/wrapoperation~WrapOperation} Clone of this operation.
+	 */
+	clone() {
+		const elementOrPosition = this.element ? this.element._clone() : this.graveyardPosition;
+
+		return new this.constructor( this.position, this.howMany, elementOrPosition, this.baseVersion );
+	}
+
+	/**
+	 * See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
+	 *
+	 * @returns {module:engine/model/operation/unwrapoperation~UnwrapOperation}
+	 */
+	getReversed() {
+		const graveyard = this.position.root.document.graveyard;
+		const graveyardPosition = new Position( graveyard, [ 0 ] );
+
+		return new UnwrapOperation( this.targetPosition, this.howMany, graveyardPosition, this.baseVersion + 1 );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	_validate() {
+		const element = this.position.parent;
+		const offset = this.position.offset;
+
+		// Validate whether wrap operation has correct parameters.
+		if ( !element || offset + this.howMany > element.maxOffset ) {
+			/**
+			 * Wrap range is invalid.
+			 *
+			 * @error wrap-operation-range-invalid
+			 */
+			throw new CKEditorError( 'wrap-operation-range-invalid: Wrap range is invalid.' );
+		}
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	_execute() {
+		const wrappedRange = this.wrappedRange;
+
+		const insertPosition = Position.createFromPosition( wrappedRange.end );
+
+		const targetPath = insertPosition.path.slice();
+		targetPath.push( 0 );
+		const targetPosition = new Position( this.position.root, targetPath );
+
+		if ( this.element ) {
+			_insert( insertPosition, this.element._clone() );
+		} else {
+			_move( Range.createFromPositionAndShift( this.graveyardPosition, 1 ), insertPosition );
+		}
+
+		_move( wrappedRange, targetPosition );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get className() {
+		return 'engine.model.operation.WrapOperation';
+	}
+
+	/**
+	 * Creates `WrapOperation` object from deserilized object, i.e. from parsed JSON string.
+	 *
+	 * @param {Object} json Deserialized JSON object.
+	 * @param {module:engine/model/document~Document} document Document on which this operation will be applied.
+	 * @returns {module:engine/model/operation/wrapoperation~WrapOperation}
+	 */
+	static fromJSON( json, document ) {
+		const position = Position.fromJSON( json.position, document );
+		const elementOrPosition = json.element ? Element.fromJSON( json.element ) : Position.fromJSON( json.graveyardPosition, document );
+
+		return new this( position, json.howMany, elementOrPosition, json.baseVersion );
+	}
+}

+ 191 - 17
packages/ckeditor5-engine/src/model/position.js

@@ -41,8 +41,10 @@ export default class Position {
 	 *
 	 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} root Root of the position.
 	 * @param {Array.<Number>} path Position path. See {@link module:engine/model/position~Position#path}.
+	 * @param {module:engine/model/position~PositionStickiness} stickiness Position stickiness. Defaults to `'toNone'`.
+	 * See {@link module:engine/model/position~PositionStickiness}.
 	 */
-	constructor( root, path ) {
+	constructor( root, path, stickiness = 'toNone' ) {
 		if ( !root.is( 'element' ) && !root.is( 'documentFragment' ) ) {
 			/**
 			 * Position root is invalid.
@@ -108,6 +110,17 @@ export default class Position {
 		 * @member {Array.<Number>} module:engine/model/position~Position#path
 		 */
 		this.path = path;
+
+		/**
+		 * Position stickiness. See {@link module:engine/model/position~PositionStickiness}.
+		 *
+		 * Regular `Position` (not {@link module:engine/model/liveposition~LivePosition}) should always be not-sticky
+		 * (`'toNone'`), unless it is a {@link module:engine/model/range~Range} boundary. Note, that `Range`
+		 * automatically creates positions with correct stickiness.
+		 *
+		 * @member {module:engine/model/position~PositionStickiness} module:engine/model/position~Position#stickiness
+		 */
+		this.stickiness = stickiness;
 	}
 
 	/**
@@ -469,6 +482,134 @@ export default class Position {
 		}
 	}
 
+	hasSameParentAs( position ) {
+		if ( this.root !== position.root ) {
+			return false;
+		}
+
+		const thisParentPath = this.getParentPath();
+		const posParentPath = position.getParentPath();
+
+		return compareArrays( thisParentPath, posParentPath ) == 'same';
+	}
+
+	getTransformedByOperation( operation ) {
+		let result;
+
+		switch ( operation.type ) {
+			case 'insert':
+				result = this._getTransformedByInsertOperation( operation );
+				break;
+			case 'move':
+			case 'remove':
+			case 'reinsert':
+				result = this._getTransformedByMoveOperation( operation );
+				break;
+			case 'split':
+				result = this._getTransformedBySplitOperation( operation );
+				break;
+			case 'merge':
+				result = this._getTransformedByMergeOperation( operation );
+				break;
+			case 'wrap':
+				result = this._getTransformedByWrapOperation( operation );
+				break;
+			case 'unwrap':
+				result = this._getTransformedByUnwrapOperation( operation );
+				break;
+			default:
+				result = Position.createFromPosition( this );
+				break;
+		}
+
+		return result;
+	}
+
+	_getTransformedByInsertOperation( operation ) {
+		return this._getTransformedByInsertion( operation.position, operation.howMany );
+	}
+
+	_getTransformedByMoveOperation( operation ) {
+		return this._getTransformedByMove( operation.sourcePosition, operation.targetPosition, operation.howMany );
+	}
+
+	_getTransformedBySplitOperation( operation ) {
+		const movedRange = operation.movedRange;
+
+		const isContained = movedRange.containsPosition( this ) ||
+			( movedRange.start.isEqual( this ) && this.stickiness == 'toNext' );
+
+		if ( isContained ) {
+			return this._getCombined( operation.position, operation.moveTargetPosition );
+		} else {
+			if ( operation.graveyardPosition ) {
+				return this._getTransformedByMove( operation.graveyardPosition, operation.insertionPosition, 1 );
+			} else {
+				return this._getTransformedByInsertion( operation.insertionPosition, 1 );
+			}
+		}
+	}
+
+	_getTransformedByMergeOperation( operation ) {
+		const movedRange = operation.movedRange;
+		const isContained = movedRange.containsPosition( this ) || movedRange.start.isEqual( this );
+
+		let pos;
+
+		if ( isContained ) {
+			pos = this._getCombined( operation.sourcePosition, operation.targetPosition );
+
+			if ( operation.sourcePosition.isBefore( operation.targetPosition ) ) {
+				// Above happens during OT when the merged element is moved before the merged-to element.
+				pos = pos._getTransformedByDeletion( operation.deletionPosition, 1 );
+			}
+		} else {
+			pos = this._getTransformedByMove( operation.deletionPosition, operation.graveyardPosition, 1 );
+		}
+
+		return pos;
+	}
+
+	_getTransformedByWrapOperation( operation ) {
+		const wrappedRange = operation.wrappedRange;
+
+		const isContained = wrappedRange.containsPosition( this ) ||
+			( wrappedRange.start.isEqual( this ) && this.stickiness == 'toNext' ) ||
+			( wrappedRange.end.isEqual( this ) && this.stickiness == 'toPrevious' );
+
+		if ( isContained ) {
+			return this._getCombined( wrappedRange.start, operation.targetPosition );
+		} else if ( this.isEqual( operation.position ) ) {
+			return Position.createFromPosition( this );
+		} else {
+			if ( operation.graveyardPosition ) {
+				const pos = this._getTransformedByMove( operation.graveyardPosition, operation.position, 1 );
+
+				return pos._getTransformedByMove( operation.position.getShiftedBy( 1 ), operation.targetPosition, operation.howMany );
+			} else {
+				return this._getTransformedByDeletion( operation.position, operation.howMany - 1 );
+			}
+		}
+	}
+
+	_getTransformedByUnwrapOperation( operation ) {
+		const unwrappedRange = operation.unwrappedRange;
+
+		const isContained = unwrappedRange.containsPosition( this ) ||
+			unwrappedRange.start.isEqual( this ) ||
+			unwrappedRange.end.isEqual( this );
+
+		if ( isContained ) {
+			return this._getCombined( operation.position, operation.targetPosition );
+		} else if ( this.isEqual( operation.targetPosition ) ) {
+			return Position.createFromPosition( this );
+		} else {
+			const pos = this._getTransformedByInsertion( operation.targetPosition, operation.howMany - 1 );
+
+			return pos._getTransformedByInsertion( operation.graveyardPosition, 1 );
+		}
+	}
+
 	/**
 	 * Returns a copy of this position that is updated by removing `howMany` nodes starting from `deletePosition`.
 	 * It may happen that this position is in a removed node. If that is the case, `null` is returned instead.
@@ -524,12 +665,9 @@ export default class Position {
 	 * @protected
 	 * @param {module:engine/model/position~Position} insertPosition Position where nodes are inserted.
 	 * @param {Number} howMany How many nodes are inserted.
-	 * @param {Boolean} insertBefore Flag indicating whether nodes are inserted before or after `insertPosition`.
-	 * This is important only when `insertPosition` and this position are same. If that is the case and the flag is
-	 * set to `true`, this position will get transformed. If the flag is set to `false`, it won't.
 	 * @returns {module:engine/model/position~Position} Transformed position.
 	 */
-	_getTransformedByInsertion( insertPosition, howMany, insertBefore ) {
+	_getTransformedByInsertion( insertPosition, howMany ) {
 		const transformed = Position.createFromPosition( this );
 
 		// This position can't be affected if insertion was in a different root.
@@ -539,7 +677,7 @@ export default class Position {
 
 		if ( compareArrays( insertPosition.getParentPath(), this.getParentPath() ) == 'same' ) {
 			// If nodes are inserted in the node that is pointed by this position...
-			if ( insertPosition.offset < this.offset || ( insertPosition.offset == this.offset && insertBefore ) ) {
+			if ( insertPosition.offset < this.offset || ( insertPosition.offset == this.offset && this.stickiness != 'toPrevious' ) ) {
 				// And are inserted before an offset of that position...
 				// "Push" this positions offset.
 				transformed.offset += howMany;
@@ -565,28 +703,27 @@ export default class Position {
 	 * @param {module:engine/model/position~Position} sourcePosition Position before the first element to move.
 	 * @param {module:engine/model/position~Position} targetPosition Position where moved elements will be inserted.
 	 * @param {Number} howMany How many consecutive nodes to move, starting from `sourcePosition`.
-	 * @param {Boolean} insertBefore Flag indicating whether moved nodes are pasted before or after `insertPosition`.
-	 * This is important only when `targetPosition` and this position are same. If that is the case and the flag is
-	 * set to `true`, this position will get transformed by range insertion. If the flag is set to `false`, it won't.
-	 * @param {Boolean} [sticky] Flag indicating whether this position "sticks" to range, that is if it should be moved
-	 * with the moved range if it is equal to one of range's boundaries.
 	 * @returns {module:engine/model/position~Position} Transformed position.
 	 */
-	_getTransformedByMove( sourcePosition, targetPosition, howMany, insertBefore, sticky ) {
+	_getTransformedByMove( sourcePosition, targetPosition, howMany ) {
 		// Moving a range removes nodes from their original position. We acknowledge this by proper transformation.
 		let transformed = this._getTransformedByDeletion( sourcePosition, howMany );
 
 		// Then we update target position, as it could be affected by nodes removal too.
 		targetPosition = targetPosition._getTransformedByDeletion( sourcePosition, howMany );
 
-		if ( transformed === null || ( sticky && transformed.isEqual( sourcePosition ) ) ) {
+		const isMoved = transformed === null ||
+			( sourcePosition.isEqual( this ) && this.stickiness == 'toNext' ) ||
+			( sourcePosition.getShiftedBy( howMany ).isEqual( this ) && this.stickiness == 'toPrevious' );
+
+		if ( isMoved ) {
 			// This position is inside moved range (or sticks to it).
 			// In this case, we calculate a combination of this position, move source position and target position.
 			transformed = this._getCombined( sourcePosition, targetPosition );
 		} else {
 			// This position is not inside a removed range.
 			// In next step, we simply reflect inserting `howMany` nodes, which might further affect the position.
-			transformed = transformed._getTransformedByInsertion( targetPosition, howMany, insertBefore );
+			transformed = transformed._getTransformedByInsertion( targetPosition, howMany );
 		}
 
 		return transformed;
@@ -747,18 +884,25 @@ export default class Position {
 	 * @returns {module:engine/model/position~Position}
 	 */
 	static createFromPosition( position ) {
-		return new this( position.root, position.path.slice() );
+		const newPos = new this( position.root, position.path.slice() );
+		newPos.stickiness = position.stickiness;
+
+		return newPos;
 	}
 
 	/**
 	 * Creates a `Position` instance from given plain object (i.e. parsed JSON string).
 	 *
 	 * @param {Object} json Plain object to be converted to `Position`.
+	 * @param {module:engine/model/document~Document} doc Document object that will be position owner.
 	 * @returns {module:engine/model/position~Position} `Position` instance created using given plain object.
 	 */
 	static fromJSON( json, doc ) {
 		if ( json.root === '$graveyard' ) {
-			return new Position( doc.graveyard, json.path );
+			const pos = new Position( doc.graveyard, json.path );
+			pos.stickiness = json.stickiness;
+
+			return pos;
 		}
 
 		if ( !doc.getRoot( json.root ) ) {
@@ -774,7 +918,10 @@ export default class Position {
 			);
 		}
 
-		return new Position( doc.getRoot( json.root ), json.path );
+		const pos = new Position( doc.getRoot( json.root ), json.path );
+		pos.stickiness = json.stickiness;
+
+		return pos;
 	}
 }
 
@@ -784,3 +931,30 @@ export default class Position {
  *
  * @typedef {String} module:engine/model/position~PositionRelation
  */
+
+/**
+ * Represents how position is "sticking" with neighbour nodes. Used to define how position should be transformed (moved)
+ * in edge cases. Possible values: `'toNone'`, `'toNext'`, `'toPrevious'`.
+ *
+ * Examples:
+ *
+ *		Insert. Position is at | and nodes are inserted at the same position, marked as ^:
+ *
+ *		- sticks to none:           <p>f^|oo</p>  ->  <p>fbar|oo</p>
+ *		- sticks to next node:      <p>f^|oo</p>  ->  <p>fbar|oo</p>
+ *		- sticks to previous node:  <p>f|^oo</p>  ->  <p>f|baroo</p>
+ *
+ *
+ *		Move. Position is at | and range [oo] is moved to position ^:
+ *
+ *		- sticks to none:           <p>f|[oo]</p><p>b^ar</p>  ->  <p>f|</p><p>booar</p>
+ *		- sticks to none:           <p>f[oo]|</p><p>b^ar</p>  ->  <p>f|</p><p>booar</p>
+ *
+ *		- sticks to next node:      <p>f|[oo]</p><p>b^ar</p>  ->  <p>f</p><p>b|ooar</p>
+ *		- sticks to next node:      <p>f[oo]|</p><p>b^ar</p>  ->  <p>f|</p><p>booar</p>
+ *
+ *		- sticks to previous node:  <p>f|[oo]</p><p>b^ar</p>  ->  <p>f|</p><p>booar</p>
+ *		- sticks to previous node:  <p>f[oo]|</p><p>b^ar</p>  ->  <p>f</p><p>boo|ar</p>
+ *
+ * @typedef {String} module:engine/model/position~PositionStickiness
+ */

+ 139 - 155
packages/ckeditor5-engine/src/model/range.js

@@ -10,6 +10,7 @@
 import Position from './position';
 import TreeWalker from './treewalker';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import compareArrays from '@ckeditor/ckeditor5-utils/src/comparearrays';
 
 /**
  * Range class. Range is iterable.
@@ -39,6 +40,9 @@ export default class Range {
 		 * @member {module:engine/model/position~Position}
 		 */
 		this.end = end ? Position.createFromPosition( end ) : Position.createFromPosition( start );
+
+		this.start.stickiness = this.isCollapsed ? 'toNone' : 'toNext';
+		this.end.stickiness = this.isCollapsed ? 'toNone' : 'toPrevious';
 	}
 
 	/**
@@ -76,7 +80,10 @@ export default class Range {
 	 * @type {Boolean}
 	 */
 	get isFlat() {
-		return this.start.parent === this.end.parent;
+		const startParentPath = this.start.getParentPath();
+		const endParentPath = this.end.getParentPath();
+
+		return compareArrays( startParentPath, endParentPath ) == 'same';
 	}
 
 	/**
@@ -381,69 +388,52 @@ export default class Range {
 	}
 
 	/**
-	 * Returns a range that is a result of transforming this range by given `delta`.
+	 * Returns a range that is a result of transforming this range by given `operation`.
 	 *
-	 * **Note:** transformation may break one range into multiple ranges (e.g. when a part of the range is
+	 * **Note:** transformation may break one range into multiple ranges (for example, when a part of the range is
 	 * moved to a different part of document tree). For this reason, an array is returned by this method and it
 	 * may contain one or more `Range` instances.
 	 *
-	 * @param {module:engine/model/delta/delta~Delta} delta Delta to transform range by.
+	 * @param {module:engine/model/operation/operation~Operation} operation Operation to transform range by.
 	 * @returns {Array.<module:engine/model/range~Range>} Range which is the result of transformation.
 	 */
-	getTransformedByDelta( delta ) {
-		const ranges = [ Range.createFromRange( this ) ];
-
-		// Operation types that a range can be transformed by.
-		const supportedTypes = new Set( [ 'insert', 'move', 'remove', 'reinsert' ] );
-
-		for ( const operation of delta.operations ) {
-			if ( supportedTypes.has( operation.type ) ) {
-				for ( let i = 0; i < ranges.length; i++ ) {
-					let result;
-
-					if ( operation.type == 'insert' ) {
-						result = ranges[ i ]._getTransformedByDocumentChange(
-							operation.type,
-							delta.type,
-							operation.position,
-							operation.nodes.maxOffset
-						);
-					} else {
-						result = ranges[ i ]._getTransformedByDocumentChange(
-							operation.type,
-							delta.type,
-							operation.targetPosition,
-							operation.howMany,
-							operation.sourcePosition
-						);
-					}
-
-					ranges.splice( i, 1, ...result );
-
-					i += result.length - 1;
-				}
-			}
+	getTransformedByOperation( operation ) {
+		switch ( operation.type ) {
+			case 'insert':
+				return this._getTransformedByInsertOperation( operation );
+			case 'move':
+			case 'remove':
+			case 'reinsert':
+				return this._getTransformedByMoveOperation( operation );
+			case 'split':
+				return [ this._getTransformedBySplitOperation( operation ) ];
+			case 'merge':
+				return [ this._getTransformedByMergeOperation( operation ) ];
+			case 'wrap':
+				return [ this._getTransformedByWrapOperation( operation ) ];
+			case 'unwrap':
+				return [ this._getTransformedByUnwrapOperation( operation ) ];
 		}
 
-		return ranges;
+		return [ Range.createFromRange( this ) ];
 	}
 
 	/**
-	 * Returns a range that is a result of transforming this range by multiple `deltas`.
+	 * Returns a range that is a result of transforming this range by multiple `operations`.
 	 *
 	 * **Note:** transformation may break one range into multiple ranges (e.g. when a part of the range is
 	 * moved to a different part of document tree). For this reason, an array is returned by this method and it
 	 * may contain one or more `Range` instances.
 	 *
-	 * @param {Iterable.<module:engine/model/delta/delta~Delta>} deltas Deltas to transform the range by.
+	 * @param {Iterable.<module:engine/model/operation/operation~Operation>} operations Operations to transform the range by.
 	 * @returns {Array.<module:engine/model/range~Range>} Range which is the result of transformation.
 	 */
-	getTransformedByDeltas( deltas ) {
+	getTransformedByOperations( operations ) {
 		const ranges = [ Range.createFromRange( this ) ];
 
-		for ( const delta of deltas ) {
+		for ( const operation of operations ) {
 			for ( let i = 0; i < ranges.length; i++ ) {
-				const result = ranges[ i ].getTransformedByDelta( delta );
+				const result = ranges[ i ].getTransformedByOperation( operation );
 
 				ranges.splice( i, 1, ...result );
 				i += result.length - 1;
@@ -479,102 +469,57 @@ export default class Range {
 		return this.start.getCommonAncestor( this.end );
 	}
 
+	_getTransformedByInsertOperation( operation, spread = false ) {
+		return this._getTransformedByInsertion( operation.position, operation.howMany, spread );
+	}
+
 	/**
-	 * Returns a range that is a result of transforming this range by a change in the model document.
-	 *
 	 * @protected
-	 * @param {'insert'|'move'|'remove'|'reinsert'} type Change type.
-	 * @param {String} deltaType Type of delta that introduced the change.
-	 * @param {module:engine/model/position~Position} targetPosition Position before the first changed node.
-	 * @param {Number} howMany How many nodes has been changed.
-	 * @param {module:engine/model/position~Position} sourcePosition Source position of changes.
 	 * @returns {Array.<module:engine/model/range~Range>}
 	 */
-	_getTransformedByDocumentChange( type, deltaType, targetPosition, howMany, sourcePosition ) {
-		if ( type == 'insert' ) {
-			return this._getTransformedByInsertion( targetPosition, howMany, false, false );
-		} else {
-			const sourceRange = Range.createFromPositionAndShift( sourcePosition, howMany );
-
-			// Edge case for merge delta.
-			if (
-				deltaType == 'merge' &&
-				this.isCollapsed &&
-				( this.start.isEqual( sourceRange.start ) || this.start.isEqual( sourceRange.end ) )
-			) {
-				// Collapsed range is in merged element, at the beginning or at the end of it.
-				// Without fix, the range would end up in the graveyard, together with removed element.
-				// <p>foo</p><p>[]bar</p> -> <p>foobar</p><p>[]</p> -> <p>foobar</p> -> <p>foo[]bar</p>
-				// <p>foo</p><p>bar[]</p> -> <p>foobar</p><p>[]</p> -> <p>foobar</p> -> <p>foobar[]</p>
-				//
-				// In most cases, `sourceRange.start.offset` for merge delta's move operation would be 0,
-				// so this formula might look overcomplicated.
-				// However in some scenarios, after operational transformation, move operation might not
-				// in fact start from 0 and we need to properly count new offset.
-				// https://github.com/ckeditor/ckeditor5-engine/pull/1133#issuecomment-329080668.
-				const offset = this.start.offset - sourceRange.start.offset;
-
-				return [ new Range( targetPosition.getShiftedBy( offset ) ) ];
-			}
-			//
-			// Edge case for split delta.
-			//
-			if ( deltaType == 'split' && this.isCollapsed && this.end.isEqual( sourceRange.end ) ) {
-				// Collapsed range is at the end of split element.
-				// Without fix, the range would end up at the end of split (old) element instead of at the end of new element.
-				// That would happen because this range is not technically inside moved range. Last step below shows the fix.
-				// <p>foobar[]</p> -> <p>foobar[]</p><p></p> -> <p>foo[]</p><p>bar</p> -> <p>foo</p><p>bar[]</p>
-				return [ new Range( targetPosition.getShiftedBy( howMany ) ) ];
-			}
-			//
-			// Other edge cases:
+	_getTransformedByMoveOperation( operation, spread = false ) {
+		const sourcePosition = operation.sourcePosition;
+		const howMany = operation.howMany;
+		const targetPosition = operation.targetPosition;
+
+		return this._getTransformedByMove( sourcePosition, targetPosition, howMany, spread );
+	}
+
+	_getTransformedBySplitOperation( operation ) {
+		const start = this.start._getTransformedBySplitOperation( operation );
+		const end = this.end._getTransformedBySplitOperation( operation );
+
+		return new Range( start, end );
+	}
+
+	_getTransformedByMergeOperation( operation ) {
+		const start = this.start._getTransformedByMergeOperation( operation );
+		const end = this.end._getTransformedByMergeOperation( operation );
+
+		if ( start.isAfter( end ) ) {
+			// This happens in the following case:
 			//
-			// In all examples `[]` is `this` and `{}` is `sourceRange`, while `^` is move target position.
+			// <p>abc</p>{<p>x}yz</p> -> <p>abcx}yz</p>{
+			//                           <p>abcx{yz</p>}
 			//
-			// Example:
-			// <p>xx</p>^<w>{<p>a[b</p>}</w><p>c]d</p>   -->   <p>xx</p><p>a[b</p><w></w><p>c]d</p>
-			// ^<p>xx</p><w>{<p>a[b</p>}</w><p>c]d</p>   -->   <p>a[b</p><p>xx</p><w></w><p>c]d</p>  // Note <p>xx</p> inclusion.
-			// <w>{<p>a[b</p>}</w>^<p>c]d</p>            -->   <w></w><p>a[b</p><p>c]d</p>
-			if (
-				( sourceRange.containsPosition( this.start ) || sourceRange.start.isEqual( this.start ) ) &&
-				this.containsPosition( sourceRange.end ) &&
-				this.end.isAfter( targetPosition )
-			) {
-				const start = this.start._getCombined(
-					sourcePosition,
-					targetPosition._getTransformedByDeletion( sourcePosition, howMany )
-				);
-				const end = this.end._getTransformedByMove( sourcePosition, targetPosition, howMany, false, false );
+			return new Range( end, start );
+		}
 
-				return [ new Range( start, end ) ];
-			}
+		return new Range( start, end );
+	}
 
-			// Example:
-			// <p>c[d</p><w>{<p>a]b</p>}</w>^<p>xx</p>   -->   <p>c[d</p><w></w><p>a]b</p><p>xx</p>
-			// <p>c[d</p><w>{<p>a]b</p>}</w><p>xx</p>^   -->   <p>c[d</p><w></w><p>xx</p><p>a]b</p>  // Note <p>xx</p> inclusion.
-			// <p>c[d</p>^<w>{<p>a]b</p>}</w>            -->   <p>c[d</p><p>a]b</p><w></w>
-			if (
-				( sourceRange.containsPosition( this.end ) || sourceRange.end.isEqual( this.end ) ) &&
-				this.containsPosition( sourceRange.start ) &&
-				this.start.isBefore( targetPosition )
-			) {
-				const start = this.start._getTransformedByMove(
-					sourcePosition,
-					targetPosition,
-					howMany,
-					true,
-					false
-				);
-				const end = this.end._getCombined(
-					sourcePosition,
-					targetPosition._getTransformedByDeletion( sourcePosition, howMany )
-				);
+	_getTransformedByWrapOperation( operation ) {
+		const start = this.start._getTransformedByWrapOperation( operation );
+		const end = this.end._getTransformedByWrapOperation( operation );
 
-				return [ new Range( start, end ) ];
-			}
+		return new Range( start, end );
+	}
 
-			return this._getTransformedByMove( sourcePosition, targetPosition, howMany );
-		}
+	_getTransformedByUnwrapOperation( operation ) {
+		const start = this.start._getTransformedByUnwrapOperation( operation );
+		const end = this.end._getTransformedByUnwrapOperation( operation );
+
+		return new Range( start, end );
 	}
 
 	/**
@@ -597,22 +542,14 @@ export default class Range {
 	 *		transformed = range._getTransformedByInsertion( new Position( root, [ 3, 2 ] ), 4, true );
 	 *		// transformed array has two ranges: from [ 2, 7 ] to [ 3, 2 ] and from [ 3, 6 ] to [ 4, 0, 1 ]
 	 *
-	 *		transformed = range._getTransformedByInsertion( new Position( root, [ 4, 0, 1 ] ), 4, false, false );
-	 *		// transformed array has one range which is equal to original range because insertion is after the range boundary
-	 *
-	 *		transformed = range._getTransformedByInsertion( new Position( root, [ 4, 0, 1 ] ), 4, false, true );
-	 *		// transformed array has one range: from [ 2, 7 ] to [ 4, 0, 5 ] because range was expanded
-	 *
 	 * @protected
 	 * @param {module:engine/model/position~Position} insertPosition Position where nodes are inserted.
 	 * @param {Number} howMany How many nodes are inserted.
 	 * @param {Boolean} [spread] Flag indicating whether this {~Range range} should be spread if insertion
 	 * was inside the range. Defaults to `false`.
-	 * @param {Boolean} [isSticky] Flag indicating whether insertion should expand a range if it is in a place of
-	 * range boundary. Defaults to `false`.
 	 * @returns {Array.<module:engine/model/range~Range>} Result of the transformation.
 	 */
-	_getTransformedByInsertion( insertPosition, howMany, spread = false, isSticky = false ) {
+	_getTransformedByInsertion( insertPosition, howMany, spread = false ) {
 		if ( spread && this.containsPosition( insertPosition ) ) {
 			// Range has to be spread. The first part is from original start to the spread point.
 			// The other part is from spread point to the original end, but transformed by
@@ -621,18 +558,15 @@ export default class Range {
 			return [
 				new Range( this.start, insertPosition ),
 				new Range(
-					insertPosition._getTransformedByInsertion( insertPosition, howMany, true ),
-					this.end._getTransformedByInsertion( insertPosition, howMany, this.isCollapsed )
+					insertPosition.getShiftedBy( howMany ),
+					this.end._getTransformedByInsertion( insertPosition, howMany )
 				)
 			];
 		} else {
 			const range = Range.createFromRange( this );
 
-			const insertBeforeStart = !isSticky;
-			const insertBeforeEnd = range.isCollapsed ? true : isSticky;
-
-			range.start = range.start._getTransformedByInsertion( insertPosition, howMany, insertBeforeStart );
-			range.end = range.end._getTransformedByInsertion( insertPosition, howMany, insertBeforeEnd );
+			range.start = range.start._getTransformedByInsertion( insertPosition, howMany );
+			range.end = range.end._getTransformedByInsertion( insertPosition, howMany );
 
 			return [ range ];
 		}
@@ -646,18 +580,43 @@ export default class Range {
 	 * @param {module:engine/model/position~Position} sourcePosition Position from which nodes are moved.
 	 * @param {module:engine/model/position~Position} targetPosition Position to where nodes are moved.
 	 * @param {Number} howMany How many nodes are moved.
+	 * @param {Boolean} [spread=false]
 	 * @returns {Array.<module:engine/model/range~Range>} Result of the transformation.
 	 */
-	_getTransformedByMove( sourcePosition, targetPosition, howMany ) {
+	_getTransformedByMove( sourcePosition, targetPosition, howMany, spread = false ) {
+		// Special case for transforming a collapsed range. Just transform it like a position.
 		if ( this.isCollapsed ) {
-			const newPos = this.start._getTransformedByMove( sourcePosition, targetPosition, howMany, true, false );
+			const newPos = this.start._getTransformedByMove( sourcePosition, targetPosition, howMany );
 
 			return [ new Range( newPos ) ];
 		}
 
-		let result;
+		// Special case for transformation when a part of the range is moved towards the range.
+		//
+		// Examples:
+		//
+		// <div><p>ab</p><p>c[d</p></div><p>e]f</p> --> <div><p>ab</p></div><p>c[d</p><p>e]f</p>
+		// <p>e[f</p><div><p>a]b</p><p>cd</p></div> --> <p>e[f</p><p>a]b</p><div><p>cd</p></div>
+		//
+		// Without this special condition, the default algorithm leaves an "artifact" range from one of `differenceSet` parts:
+		//
+		// <div><p>ab</p><p>c[d</p></div><p>e]f</p> --> <div><p>ab</p>{</div>}<p>c[d</p><p>e]f</p>
+		//
+		// This special case is applied only if the range is to be kept together (not spread).
+		const moveRange = Range.createFromPositionAndShift( sourcePosition, howMany );
+		const insertPosition = targetPosition._getTransformedByDeletion( sourcePosition, howMany );
+
+		if ( this.containsPosition( targetPosition ) && !spread ) {
+			if ( moveRange.containsPosition( this.start ) || moveRange.containsPosition( this.end ) ) {
+				const start = this.start._getTransformedByMove( sourcePosition, targetPosition, howMany );
+				const end = this.end._getTransformedByMove( sourcePosition, targetPosition, howMany );
+
+				return [ new Range( start, end ) ];
+			}
+		}
 
-		const moveRange = new Range( sourcePosition, sourcePosition.getShiftedBy( howMany ) );
+		// Default algorithm.
+		let result;
 
 		const differenceSet = this.getDifference( moveRange );
 		let difference = null;
@@ -665,7 +624,7 @@ export default class Range {
 		const common = this.getIntersection( moveRange );
 
 		if ( differenceSet.length == 1 ) {
-			// `moveRange` and this range may intersect.
+			// `moveRange` and this range may intersect but may be separate.
 			difference = new Range(
 				differenceSet[ 0 ].start._getTransformedByDeletion( sourcePosition, howMany ),
 				differenceSet[ 0 ].end._getTransformedByDeletion( sourcePosition, howMany )
@@ -678,24 +637,49 @@ export default class Range {
 			);
 		} // else, `moveRange` contains this range.
 
-		const insertPosition = targetPosition._getTransformedByDeletion( sourcePosition, howMany );
-
 		if ( difference ) {
-			result = difference._getTransformedByInsertion( insertPosition, howMany, common !== null );
+			result = difference._getTransformedByInsertion( insertPosition, howMany, common !== null || spread );
 		} else {
 			result = [];
 		}
 
 		if ( common ) {
-			result.push( new Range(
+			const transformedCommon = new Range(
 				common.start._getCombined( moveRange.start, insertPosition ),
 				common.end._getCombined( moveRange.start, insertPosition )
-			) );
+			);
+
+			if ( result.length == 2 ) {
+				result.splice( 1, 0, transformedCommon );
+			} else if ( result.length > 0 && common.start.isBefore( result[ 0 ].start ) ) {
+				result.unshift( transformedCommon );
+			} else {
+				result.push( transformedCommon );
+			}
 		}
 
 		return result;
 	}
 
+	_getTransformedByDeletion( deletePosition, howMany ) {
+		let newStart = this.start._getTransformedByDeletion( deletePosition, howMany );
+		let newEnd = this.end._getTransformedByDeletion( deletePosition, howMany );
+
+		if ( newStart == null && newEnd == null ) {
+			return null;
+		}
+
+		if ( newStart == null ) {
+			newStart = deletePosition;
+		}
+
+		if ( newEnd == null ) {
+			newEnd = deletePosition;
+		}
+
+		return new Range( newStart, newEnd );
+	}
+
 	/**
 	 * Creates a new range, spreading from specified {@link module:engine/model/position~Position position} to a position moved by
 	 * given `shift`. If `shift` is a negative value, shifted position is treated as the beginning of the range.

+ 3 - 0
packages/ckeditor5-engine/src/model/treewalker.js

@@ -90,6 +90,9 @@ export default class TreeWalker {
 			this.position = Position.createFromPosition( this.boundaries[ this.direction == 'backward' ? 'end' : 'start' ] );
 		}
 
+		// Reset position stickiness in case it was set to other value.
+		this.position.stickiness = 'toNone';
+
 		/**
 		 * Flag indicating whether all consecutive characters with the same attributes should be
 		 * returned as one {@link module:engine/model/textproxy~TextProxy} (`true`) or one by one (`false`).

+ 2 - 1
packages/ckeditor5-engine/src/model/utils/deletecontent.js

@@ -24,7 +24,7 @@ import DocumentSelection from '../documentselection';
  * should be performed.
  * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
  * Selection of which the content should be deleted.
- * @param {module:engine/model/batch~Batch} batch Batch to which the deltas will be added.
+ * @param {module:engine/model/batch~Batch} batch Batch to which the operations will be added.
  * @param {Object} [options]
  * @param {Boolean} [options.leaveUnmerged=false] Whether to merge elements after removing the content of the selection.
  *
@@ -63,6 +63,7 @@ export default function deleteContent( model, selection, options = {} ) {
 		const selRange = selection.getFirstRange();
 		const startPos = selRange.start;
 		const endPos = LivePosition.createFromPosition( selRange.end );
+		endPos.stickiness = 'toNext';
 
 		// 2. Remove the content if there is any.
 		if ( !selRange.start.isTouching( selRange.end ) ) {

+ 6 - 2
packages/ckeditor5-engine/src/model/utils/insertcontent.js

@@ -90,7 +90,7 @@ class Insertion {
 		this.model = model;
 
 		/**
-		 * Batch to which deltas will be added.
+		 * Batch to which operations will be added.
 		 *
 		 * @member {module:engine/controller/writer~Batch} #writer
 		 */
@@ -261,6 +261,7 @@ class Insertion {
 		}
 
 		const livePos = LivePosition.createFromPosition( this.position );
+		livePos.stickiness = 'toNext';
 
 		this.writer.insert( node, this.position );
 
@@ -290,10 +291,13 @@ class Insertion {
 		const mergeLeft = this._canMergeLeft( node, context );
 		const mergeRight = this._canMergeRight( node, context );
 		const mergePosLeft = LivePosition.createBefore( node );
+		mergePosLeft.stickiness = 'toNext';
 		const mergePosRight = LivePosition.createAfter( node );
+		mergePosRight.stickiness = 'toNext';
 
 		if ( mergeLeft ) {
 			const position = LivePosition.createFromPosition( this.position );
+			position.stickiness = 'toNext';
 
 			this.writer.merge( mergePosLeft );
 
@@ -316,7 +320,7 @@ class Insertion {
 
 			// OK:  <p>xx[]</p> + <p>yy</p> => <p>xx[]yy</p> (when sticks to previous)
 			// NOK: <p>xx[]</p> + <p>yy</p> => <p>xxyy[]</p> (when sticks to next)
-			const position = new LivePosition( this.position.root, this.position.path, 'sticksToPrevious' );
+			const position = new LivePosition( this.position.root, this.position.path, 'toPrevious' );
 
 			this.writer.merge( mergePosRight );
 

+ 112 - 147
packages/ckeditor5-engine/src/model/writer.js

@@ -7,19 +7,6 @@
  * @module engine/model/writer
  */
 
-import AttributeDelta from './delta/attributedelta';
-import InsertDelta from './delta/insertdelta';
-import MarkerDelta from './delta/markerdelta';
-import MergeDelta from './delta/mergedelta';
-import MoveDelta from './delta/movedelta';
-import RemoveDelta from './delta/removedelta';
-import RenameDelta from './delta/renamedelta';
-import RootAttributeDelta from './delta/rootattributedelta';
-import SplitDelta from './delta/splitdelta';
-import UnwrapDelta from './delta/unwrapdelta';
-import WeakInsertDelta from './delta/weakinsertdelta';
-import WrapDelta from './delta/wrapdelta';
-
 import AttributeOperation from './operation/attributeoperation';
 import DetachOperation from './operation/detachoperation';
 import InsertOperation from './operation/insertoperation';
@@ -28,6 +15,10 @@ import MoveOperation from './operation/moveoperation';
 import RemoveOperation from './operation/removeoperation';
 import RenameOperation from './operation/renameoperation';
 import RootAttributeOperation from './operation/rootattributeoperation';
+import SplitOperation from './operation/splitoperation';
+import MergeOperation from './operation/mergeoperation';
+import WrapOperation from './operation/wrapoperation';
+import UnwrapOperation from './operation/unwrapoperation';
 
 import DocumentFragment from './documentfragment';
 import Text from './text';
@@ -166,9 +157,6 @@ export default class Writer {
 
 		const position = Position.createAt( itemOrPosition, offset );
 
-		// For text that has no parent we need to make a WeakInsert.
-		const delta = item instanceof Text && !item.parent ? new WeakInsertDelta() : new InsertDelta();
-
 		// If item has a parent already.
 		if ( item.parent ) {
 			// We need to check if item is going to be inserted within the same document.
@@ -195,8 +183,11 @@ export default class Writer {
 
 		const insert = new InsertOperation( position, item, version );
 
-		this.batch.addDelta( delta );
-		delta.addOperation( insert );
+		if ( item instanceof Text ) {
+			insert.shouldReceiveAttributes = true;
+		}
+
+		this.batch.addOperation( insert );
 		this.model.applyOperation( insert );
 
 		// When element is a DocumentFragment we need to move its markers to Document#markers.
@@ -346,7 +337,11 @@ export default class Writer {
 		this._assertWriterUsedCorrectly();
 
 		if ( itemOrRange instanceof Range ) {
-			setAttributeOnRange( this, key, value, itemOrRange );
+			const ranges = itemOrRange.getMinimalFlatRanges();
+
+			for ( const range of ranges ) {
+				setAttributeOnRange( this, key, value, range );
+			}
 		} else {
 			setAttributeOnItem( this, key, value, itemOrRange );
 		}
@@ -383,7 +378,11 @@ export default class Writer {
 		this._assertWriterUsedCorrectly();
 
 		if ( itemOrRange instanceof Range ) {
-			setAttributeOnRange( this, key, null, itemOrRange );
+			const ranges = itemOrRange.getMinimalFlatRanges();
+
+			for ( const range of ranges ) {
+				setAttributeOnRange( this, key, null, range );
+			}
 		} else {
 			setAttributeOnItem( this, key, null, itemOrRange );
 		}
@@ -473,13 +472,10 @@ export default class Writer {
 			throw new CKEditorError( 'writer-move-different-document: Range is going to be moved between different documents.' );
 		}
 
-		const delta = new MoveDelta();
-		this.batch.addDelta( delta );
-
 		const version = range.root.document ? range.root.document.version : null;
-
 		const operation = new MoveOperation( range.start, range.end.offset - range.start.offset, position, version );
-		delta.addOperation( operation );
+
+		this.batch.addOperation( operation );
 		this.model.applyOperation( operation );
 	}
 
@@ -491,24 +487,17 @@ export default class Writer {
 	remove( itemOrRange ) {
 		this._assertWriterUsedCorrectly();
 
-		const addRemoveDelta = ( position, howMany ) => {
-			const delta = new RemoveDelta();
-			this.batch.addDelta( delta );
-
-			applyRemoveOperation( position, howMany, delta, this.model );
-		};
-
 		if ( itemOrRange instanceof Range ) {
 			// The array is reversed, so the ranges to remove are in correct order and do not have to be updated.
 			const ranges = itemOrRange.getMinimalFlatRanges().reverse();
 
 			for ( const flat of ranges ) {
-				addRemoveDelta( flat.start, flat.end.offset - flat.start.offset );
+				applyRemoveOperation( flat.start, flat.end.offset - flat.start.offset, this.batch, this.model );
 			}
 		} else {
 			const howMany = itemOrRange.is( 'text' ) ? itemOrRange.offsetSize : 1;
 
-			addRemoveDelta( Position.createBefore( itemOrRange ), howMany );
+			applyRemoveOperation( Position.createBefore( itemOrRange ), howMany, this.batch, this.model );
 		}
 	}
 
@@ -518,14 +507,11 @@ export default class Writer {
 	 * Node before and after the position have to be an element. Otherwise `writer-merge-no-element-before` or
 	 * `writer-merge-no-element-after` error will be thrown.
 	 *
-	 * @param {module:engine/model/position~Position} position Position of merge.
+	 * @param {module:engine/model/position~Position} position Position between merged elements.
 	 */
 	merge( position ) {
 		this._assertWriterUsedCorrectly();
 
-		const delta = new MergeDelta();
-		this.batch.addDelta( delta );
-
 		const nodeBefore = position.nodeBefore;
 		const nodeAfter = position.nodeAfter;
 
@@ -547,23 +533,45 @@ export default class Writer {
 			throw new CKEditorError( 'writer-merge-no-element-after: Node after merge position must be an element.' );
 		}
 
-		const positionAfter = Position.createFromParentAndOffset( nodeAfter, 0 );
-		const positionBefore = Position.createFromParentAndOffset( nodeBefore, nodeBefore.maxOffset );
+		if ( !position.root.document ) {
+			this._mergeDetached( position );
+		} else {
+			this._merge( position );
+		}
+	}
+
+	/**
+	 * Performs merge action in a detached tree.
+	 *
+	 * @private
+	 */
+	_mergeDetached( position ) {
+		const nodeBefore = position.nodeBefore;
+		const nodeAfter = position.nodeAfter;
 
-		const moveVersion = position.root.document ? position.root.document.version : null;
+		this.move( Range.createIn( nodeAfter ), Position.createAt( nodeBefore, 'end' ) );
+		this.remove( nodeAfter );
+	}
+
+	/**
+	 * Performs merge action in a non-detached tree.
+	 *
+	 * @private
+	 * @param {module:engine/model/position~Position} position Position between merged elements.
+	 */
+	_merge( position ) {
+		const targetPosition = Position.createAt( position.nodeBefore, 'end' );
+		const sourcePosition = Position.createAt( position.nodeAfter, 0 );
 
-		const move = new MoveOperation(
-			positionAfter,
-			nodeAfter.maxOffset,
-			positionBefore,
-			moveVersion
-		);
+		const graveyard = position.root.document.graveyard;
+		const graveyardPosition = new Position( graveyard, [ 0 ] );
 
-		move.isSticky = true;
-		delta.addOperation( move );
-		this.model.applyOperation( move );
+		const version = position.root.document.version;
 
-		applyRemoveOperation( position, 1, delta, this.model );
+		const merge = new MergeOperation( sourcePosition, targetPosition, graveyardPosition, version );
+
+		this.batch.addOperation( merge );
+		this.model.applyOperation( merge );
 	}
 
 	/**
@@ -586,13 +594,10 @@ export default class Writer {
 			);
 		}
 
-		const delta = new RenameDelta();
-		this.batch.addDelta( delta );
-
 		const version = element.root.document ? element.root.document.version : null;
-
 		const renameOperation = new RenameOperation( Position.createBefore( element ), element.name, newName, version );
-		delta.addOperation( renameOperation );
+
+		this.batch.addOperation( renameOperation );
 		this.model.applyOperation( renameOperation );
 	}
 
@@ -639,41 +644,19 @@ export default class Writer {
 		let firstSplitElement, firstCopyElement;
 
 		do {
-			const delta = new SplitDelta();
-			this.batch.addDelta( delta );
-
-			const copy = new Element( splitElement.name, splitElement.getAttributes() );
-			const insertVersion = splitElement.root.document ? splitElement.root.document.version : null;
-
-			const insert = new InsertOperation(
-				Position.createAfter( splitElement ),
-				copy,
-				insertVersion
-			);
-
-			delta.addOperation( insert );
-			this.model.applyOperation( insert );
-
-			const moveVersion = insertVersion !== null ? insertVersion + 1 : null;
-
-			const move = new MoveOperation(
-				position,
-				splitElement.maxOffset - position.offset,
-				Position.createFromParentAndOffset( copy, 0 ),
-				moveVersion
-			);
-			move.isSticky = true;
+			const version = splitElement.root.document ? splitElement.root.document.version : null;
+			const split = new SplitOperation( position, null, version );
 
-			delta.addOperation( move );
-			this.model.applyOperation( move );
+			this.batch.addOperation( split );
+			this.model.applyOperation( split );
 
 			// Cache result of the first split.
 			if ( !firstSplitElement && !firstCopyElement ) {
 				firstSplitElement = splitElement;
-				firstCopyElement = copy;
+				firstCopyElement = position.parent.nextSibling;
 			}
 
-			position = Position.createBefore( copy );
+			position = Position.createAfter( position.parent );
 			splitElement = position.parent;
 		} while ( splitElement !== limitElement );
 
@@ -724,26 +707,11 @@ export default class Writer {
 			throw new CKEditorError( 'writer-wrap-element-attached: Element to wrap with is already attached to tree model.' );
 		}
 
-		const delta = new WrapDelta();
-		this.batch.addDelta( delta );
-
-		const insertVersion = range.root.document ? range.root.document.version : null;
-
-		const insert = new InsertOperation( range.end, element, insertVersion );
-		delta.addOperation( insert );
-		this.model.applyOperation( insert );
+		const version = range.root.document ? range.root.document.version : null;
+		const wrap = new WrapOperation( range.start, range.end.offset - range.start.offset, element, version );
 
-		const moveVersion = insertVersion !== null ? insertVersion + 1 : null;
-
-		const targetPosition = Position.createFromParentAndOffset( element, 0 );
-		const move = new MoveOperation(
-			range.start,
-			range.end.offset - range.start.offset,
-			targetPosition,
-			moveVersion
-		);
-		delta.addOperation( move );
-		this.model.applyOperation( move );
+		this.batch.addOperation( wrap );
+		this.model.applyOperation( wrap );
 	}
 
 	/**
@@ -764,24 +732,41 @@ export default class Writer {
 			throw new CKEditorError( 'writer-unwrap-element-no-parent: Trying to unwrap an element which has no parent.' );
 		}
 
-		const delta = new UnwrapDelta();
-		this.batch.addDelta( delta );
+		if ( !element.root.document ) {
+			this._unwrapDetached( element );
+		} else {
+			this._unwrap( element );
+		}
+	}
 
-		const sourcePosition = Position.createFromParentAndOffset( element, 0 );
-		const moveVersion = sourcePosition.root.document ? sourcePosition.root.document.version : null;
+	/**
+	 * Performs unwrap action in a detached tree.
+	 *
+	 * @private
+	 * @param {module:engine/model/element~Element} element Element to unwrap.
+	 */
+	_unwrapDetached( element ) {
+		this.move( Range.createIn( element ), Position.createAfter( element ) );
+		this.remove( element );
+	}
+
+	/**
+	 * Performs unwrap action in a non-detached tree.
+	 *
+	 * @private
+	 * @param {module:engine/model/element~Element} element Element to unwrap.
+	 */
+	_unwrap( element ) {
+		const position = Position.createAt( element, 0 );
+		const version = position.root.document.version;
 
-		const move = new MoveOperation(
-			sourcePosition,
-			element.maxOffset,
-			Position.createBefore( element ),
-			moveVersion
-		);
+		const graveyard = position.root.document.graveyard;
+		const graveyardPosition = new Position( graveyard, [ 0 ] );
 
-		move.isSticky = true;
-		delta.addOperation( move );
-		this.model.applyOperation( move );
+		const unwrap = new UnwrapOperation( position, element.maxOffset, graveyardPosition, version );
 
-		applyRemoveOperation( Position.createBefore( element ), 1, delta, this.model );
+		this.batch.addOperation( unwrap );
+		this.model.applyOperation( unwrap );
 	}
 
 	/**
@@ -1243,7 +1228,6 @@ export default class Writer {
 // @param {*} value Attribute new value.
 // @param {module:engine/model/range~Range} range Model range on which the attribute will be set.
 function setAttributeOnRange( writer, key, value, range ) {
-	const delta = new AttributeDelta();
 	const model = writer.model;
 	const doc = model.document;
 
@@ -1260,7 +1244,7 @@ function setAttributeOnRange( writer, key, value, range ) {
 	// Value after the currently position.
 	let valueAfter;
 
-	for ( const val of range ) {
+	for ( const val of range.getWalker( { shallow: true } ) ) {
 		valueAfter = val.item.getAttribute( key );
 
 		// At the first run of the iterator the position in undefined. We also do not have a valueBefore, but
@@ -1285,16 +1269,11 @@ function setAttributeOnRange( writer, key, value, range ) {
 	}
 
 	function addOperation() {
-		// Add delta to the batch only if there is at least operation in the delta. Add delta only once.
-		if ( delta.operations.length === 0 ) {
-			writer.batch.addDelta( delta );
-		}
-
 		const range = new Range( lastSplitPosition, position );
 		const version = range.root.document ? doc.version : null;
 		const operation = new AttributeOperation( range, key, valueBefore, value, version );
 
-		delta.addOperation( operation );
+		writer.batch.addOperation( operation );
 		model.applyOperation( operation );
 	}
 }
@@ -1315,37 +1294,25 @@ function setAttributeOnItem( writer, key, value, item ) {
 	if ( previousValue != value ) {
 		const isRootChanged = item.root === item;
 
-		const delta = isRootChanged ? new RootAttributeDelta() : new AttributeDelta();
-		writer.batch.addDelta( delta );
-
 		if ( isRootChanged ) {
 			// If we change attributes of root element, we have to use `RootAttributeOperation`.
 			const version = item.document ? doc.version : null;
 
 			operation = new RootAttributeOperation( item, key, previousValue, value, version );
 		} else {
-			if ( item.is( 'element' ) ) {
-				// If we change the attribute of the element, we do not want to change attributes of its children, so
-				// the end of the range cannot be after the closing tag, it should be inside that element, before any of
-				// it's children, so the range will contain only the opening tag.
-				range = new Range( Position.createBefore( item ), Position.createFromParentAndOffset( item, 0 ) );
-			} else {
-				// If `item` is text proxy, we create a range from the beginning to the end of that text proxy, to change
-				// all characters represented by it.
-				range = new Range( Position.createBefore( item ), Position.createAfter( item ) );
-			}
+			range = new Range( Position.createBefore( item ), Position.createAfter( item ) );
 
 			const version = range.root.document ? doc.version : null;
 
 			operation = new AttributeOperation( range, key, previousValue, value, version );
 		}
 
-		delta.addOperation( operation );
+		writer.batch.addOperation( operation );
 		model.applyOperation( operation );
 	}
 }
 
-// Creates and applies marker operation to {@link module:engine/model/delta/delta~Delta delta}.
+// Creates and applies marker operation to {@link module:engine/model/operation/operation~Operation operation}.
 //
 // @private
 // @param {module:engine/model/writer~Writer} writer
@@ -1356,24 +1323,22 @@ function setAttributeOnItem( writer, key, value, item ) {
 function applyMarkerOperation( writer, name, oldRange, newRange, affectsData ) {
 	const model = writer.model;
 	const doc = model.document;
-	const delta = new MarkerDelta();
 
 	const operation = new MarkerOperation( name, oldRange, newRange, model.markers, doc.version, affectsData );
 
-	writer.batch.addDelta( delta );
-	delta.addOperation( operation );
+	writer.batch.addOperation( operation );
 	model.applyOperation( operation );
 }
 
 // Creates `RemoveOperation` or `DetachOperation` that removes `howMany` nodes starting from `position`.
-// The operation will be applied on given model instance and added to given delta instance.
+// The operation will be applied on given model instance and added to given operation instance.
 //
 // @private
 // @param {module:engine/model/position~Position} position Position from which nodes are removed.
 // @param {Number} howMany Number of nodes to remove.
-// @param {module:engine/model/delta~Delta} delta Delta to add new operation to.
+// @param {Batch} batch
 // @param {module:engine/model/model~Model} model Model instance on which operation will be applied.
-function applyRemoveOperation( position, howMany, delta, model ) {
+function applyRemoveOperation( position, howMany, batch, model ) {
 	let operation;
 
 	if ( position.root.document ) {
@@ -1385,7 +1350,7 @@ function applyRemoveOperation( position, howMany, delta, model ) {
 		operation = new DetachOperation( position, howMany );
 	}
 
-	delta.addOperation( operation );
+	batch.addOperation( operation );
 	model.applyOperation( operation );
 }
 

+ 4 - 4
packages/ckeditor5-engine/tests/controller/datacontroller.js

@@ -193,7 +193,7 @@ describe( 'DataController', () => {
 			schema.extend( '$text', { allowIn: '$root' } );
 			data.init( 'foo' );
 
-			expect( count( modelDocument.history.getDeltas() ) ).to.equal( 1 );
+			expect( count( modelDocument.history.getOperations() ) ).to.equal( 1 );
 		} );
 
 		it( 'should cause firing change event', () => {
@@ -228,7 +228,7 @@ describe( 'DataController', () => {
 			schema.extend( '$text', { allowIn: '$root' } );
 			data.set( 'foo' );
 
-			expect( count( modelDocument.history.getDeltas() ) ).to.equal( 1 );
+			expect( count( modelDocument.history.getOperations() ) ).to.equal( 1 );
 		} );
 
 		it( 'should cause firing change event', () => {
@@ -250,7 +250,7 @@ describe( 'DataController', () => {
 			expect( getData( model, { withoutSelection: true, rootName: 'main' } ) ).to.equal( 'foo' );
 			expect( getData( model, { withoutSelection: true, rootName: 'title' } ) ).to.equal( 'Bar' );
 
-			expect( count( modelDocument.history.getDeltas() ) ).to.equal( 2 );
+			expect( count( modelDocument.history.getOperations() ) ).to.equal( 2 );
 		} );
 
 		it( 'should parse given data before set in a context of correct root', () => {
@@ -261,7 +261,7 @@ describe( 'DataController', () => {
 			expect( getData( model, { withoutSelection: true, rootName: 'main' } ) ).to.equal( '' );
 			expect( getData( model, { withoutSelection: true, rootName: 'title' } ) ).to.equal( 'Bar' );
 
-			expect( count( modelDocument.history.getDeltas() ) ).to.equal( 2 );
+			expect( count( modelDocument.history.getOperations() ) ).to.equal( 2 );
 		} );
 
 		// This case was added when order of params was different and it really didn't work. Let's keep it

+ 0 - 203
packages/ckeditor5-engine/tests/dev-utils/deltareplayer.js

@@ -1,203 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import DeltaReplayer from '../../src/dev-utils/deltareplayer';
-import Model from '../../src/model/model';
-
-describe( 'DeltaReplayer', () => {
-	describe( 'constructor()', () => {
-		it( 'should be able to initialize replayer without deltas', () => {
-			const model = getModel();
-			const stringifiedDeltas = '';
-			const deltaReplayer = new DeltaReplayer( model, '---', stringifiedDeltas );
-
-			expect( deltaReplayer.getDeltasToReplay() ).to.deep.equal( [] );
-		} );
-
-		it( 'should be able to initialize replayer with deltas', () => {
-			const model = getModel();
-			const delta = getFirstDelta();
-
-			const deltaReplayer = new DeltaReplayer( model, '---', JSON.stringify( delta ) );
-
-			expect( deltaReplayer.getDeltasToReplay() ).to.deep.equal( [ delta ] );
-		} );
-	} );
-
-	describe( 'applyNextDelta()', () => {
-		it( 'should remove first delta from stack', () => {
-			const model = getModel();
-			const delta = getFirstDelta();
-
-			const deltaReplayer = new DeltaReplayer( model, '---', JSON.stringify( delta ) );
-
-			return deltaReplayer.applyNextDelta().then( isFinished => {
-				expect( deltaReplayer.getDeltasToReplay() ).to.deep.equal( [] );
-				expect( isFinished ).to.equal( false );
-			} );
-		} );
-
-		it( 'should apply first delta on the document', () => {
-			const model = getModel();
-			const delta = getFirstDelta();
-
-			const deltaReplayer = new DeltaReplayer( model, '---', JSON.stringify( delta ) );
-
-			return deltaReplayer.applyNextDelta().then( () => {
-				expect( Array.from( model.document.getRoot().getChildren() ).length ).to.equal( 1 );
-			} );
-		} );
-
-		it( 'should resolve with true if 0 deltas are provided', () => {
-			const model = getModel();
-			const deltaReplayer = new DeltaReplayer( model, '---', '' );
-
-			return deltaReplayer.applyNextDelta().then( isFinished => {
-				expect( isFinished ).to.equal( true );
-			} );
-		} );
-	} );
-
-	describe( 'applyAllDeltas()', () => {
-		it( 'should apply all deltas on the document', () => {
-			const model = getModel();
-
-			const stringifiedDeltas = [ getFirstDelta(), getSecondDelta() ]
-				.map( d => JSON.stringify( d ) )
-				.join( '---' );
-
-			const deltaReplayer = new DeltaReplayer( model, '---', stringifiedDeltas );
-
-			return deltaReplayer.applyAllDeltas().then( () => {
-				expect( Array.from( model.document.getRoot().getChildren() ).length ).to.equal( 2 );
-				expect( deltaReplayer.getDeltasToReplay().length ).to.equal( 0 );
-			} );
-		} );
-	} );
-
-	describe( 'applyDeltas()', () => {
-		it( 'should apply certain number of deltas on the document', () => {
-			const model = getModel();
-
-			const stringifiedDeltas = [ getFirstDelta(), getSecondDelta() ]
-				.map( d => JSON.stringify( d ) )
-				.join( '---' );
-
-			const deltaReplayer = new DeltaReplayer( model, '---', stringifiedDeltas );
-
-			return deltaReplayer.applyDeltas( 1 ).then( () => {
-				expect( Array.from( model.document.getRoot().getChildren() ).length ).to.equal( 1 );
-				expect( deltaReplayer.getDeltasToReplay().length ).to.equal( 1 );
-			} );
-		} );
-
-		it( 'should not throw an error if the number of deltas is lower than number of expected deltas to replay', () => {
-			const model = getModel();
-
-			const stringifiedDeltas = [ getFirstDelta(), getSecondDelta() ]
-				.map( d => JSON.stringify( d ) )
-				.join( '---' );
-
-			const deltaReplayer = new DeltaReplayer( model, '---', stringifiedDeltas );
-
-			return deltaReplayer.applyDeltas( 3 ).then( () => {
-				expect( Array.from( model.document.getRoot().getChildren() ).length ).to.equal( 2 );
-				expect( deltaReplayer.getDeltasToReplay().length ).to.equal( 0 );
-			} );
-		} );
-	} );
-
-	describe( 'play()', () => {
-		it( 'should play deltas with time interval', () => {
-			const model = getModel();
-
-			const stringifiedDeltas = [ getFirstDelta(), getSecondDelta() ]
-				.map( d => JSON.stringify( d ) )
-				.join( '---' );
-
-			const deltaReplayer = new DeltaReplayer( model, '---', stringifiedDeltas );
-
-			return deltaReplayer.play( 0 ).then( () => {
-				expect( deltaReplayer.getDeltasToReplay().length ).to.equal( 0 );
-			} );
-		} );
-
-		it( 'should work with default time interval', () => {
-			const model = getModel();
-
-			const deltaReplayer = new DeltaReplayer( model, '---', '' );
-
-			return deltaReplayer.play();
-		} );
-
-		it( 'should correctly handle errors coming from the engine', () => {
-			const model = getModel();
-
-			const invalidDelta = getSecondDelta();
-			invalidDelta.operations[ 0 ].baseVersion = 3;
-
-			const stringifiedDeltas = [ getFirstDelta(), invalidDelta ]
-				.map( d => JSON.stringify( d ) )
-				.join( '---' );
-
-			const deltaReplayer = new DeltaReplayer( model, '---', stringifiedDeltas );
-
-			return deltaReplayer.play( 1 )
-				.then( () => {
-					throw new Error( 'It should throw an error' );
-				}, err => {
-					expect( err.message ).to.match( /^model-document-applyOperation-wrong-version:/ );
-				} );
-		} );
-	} );
-} );
-
-function getModel() {
-	const model = new Model();
-
-	model.document.createRoot();
-
-	return model;
-}
-
-function getFirstDelta() {
-	return {
-		operations: [ {
-			baseVersion: 0,
-			position: {
-				root: 'main',
-				path: [ 0 ]
-			},
-			nodes: [ {
-				name: 'heading1',
-				children: [ {
-					data: 'The great world of open Web standards'
-				} ]
-			} ],
-			__className: 'engine.model.operation.InsertOperation'
-		} ],
-		__className: 'engine.model.delta.InsertDelta'
-	};
-}
-
-function getSecondDelta() {
-	return {
-		operations: [ {
-			baseVersion: 1,
-			position: {
-				root: 'main',
-				path: [ 1 ]
-			},
-			nodes: [ {
-				name: 'heading2',
-				children: [ {
-					data: 'The great world of open Web standards'
-				} ]
-			} ],
-			__className: 'engine.model.operation.InsertOperation'
-		} ],
-		__className: 'engine.model.delta.InsertDelta'
-	};
-}

+ 32 - 500
packages/ckeditor5-engine/tests/dev-utils/enableenginedebug.js

@@ -22,19 +22,7 @@ import NoOperation from '../../src/model/operation/nooperation';
 import RenameOperation from '../../src/model/operation/renameoperation';
 import RootAttributeOperation from '../../src/model/operation/rootattributeoperation';
 import RemoveOperation from '../../src/model/operation/removeoperation';
-import DeltaFactory from '../../src/model/delta/deltafactory';
-import Delta from '../../src/model/delta/delta';
-import AttributeDelta from '../../src/model/delta/attributedelta';
-import InsertDelta from '../../src/model/delta/insertdelta';
-import MarkerDelta from '../../src/model/delta/markerdelta';
-import MergeDelta from '../../src/model/delta/mergedelta';
-import MoveDelta from '../../src/model/delta/movedelta';
-import RenameDelta from '../../src/model/delta/renamedelta';
-import RootAttributeDelta from '../../src/model/delta/rootattributedelta';
-import SplitDelta from '../../src/model/delta/splitdelta';
-import UnwrapDelta from '../../src/model/delta/unwrapdelta';
-import WrapDelta from '../../src/model/delta/wrapdelta';
-import deltaTransform from '../../src/model/delta/transform';
+import transform from '../../src/model/operation/transform';
 import Model from '../../src/model/model';
 import ModelDocumentFragment from '../../src/model/documentfragment';
 
@@ -77,7 +65,6 @@ describe( 'disableEngineDebug', () => {
 	it( 'restores modified stubs', () => {
 		expect( ModelPosition.prototype.log ).to.equal( undefined, 'Initial value (model/position)' );
 		expect( ModelElement.prototype.printTree ).to.equal( undefined, 'Initial value (model/element)' );
-		expect( Delta.prototype.log ).to.equal( undefined, 'Initial value (model/delta/delta)' );
 		expect( ViewElement.prototype.printTree ).to.equal( undefined, 'Initial value (view/element)' );
 		expect( Model.prototype.createReplayer ).to.equal( undefined, 'Initial value (model/document)' );
 		expect( Editor.prototype.logDocuments ).to.equal( undefined, 'Initial value (core~editor/editor)' );
@@ -86,7 +73,6 @@ describe( 'disableEngineDebug', () => {
 
 		expect( ModelPosition.prototype.log ).to.be.a( 'function', 'After enabling engine debug (model/position)' );
 		expect( ModelElement.prototype.printTree ).to.be.a( 'function', 'After enabling engine debug (model/element)' );
-		expect( Delta.prototype.log ).to.be.a( 'function', 'After enabling engine debug (model/delta/delta)' );
 		expect( ViewElement.prototype.printTree ).to.be.a( 'function', 'After enabling engine debug (view/element)' );
 		expect( Model.prototype.createReplayer ).to.be.a( 'function', 'After enabling engine debug (model/document)' );
 		expect( Editor.prototype.logDocuments ).to.be.a( 'function', 'After enabling engine debug (core~editor/editor)' );
@@ -95,7 +81,6 @@ describe( 'disableEngineDebug', () => {
 
 		expect( ModelPosition.prototype.log ).to.equal( undefined, 'After disabling engine debug (model/position)' );
 		expect( ModelElement.prototype.printTree ).to.equal( undefined, 'After disabling engine debug (model/element)' );
-		expect( Delta.prototype.log ).to.equal( undefined, 'After disabling engine debug (model/delta/delta)' );
 		expect( ViewElement.prototype.printTree ).to.equal( undefined, 'After disabling engine debug (view/element)' );
 		expect( Model.prototype.createReplayer ).to.equal( undefined, 'After disabling engine debug (model/document)' );
 		expect( Editor.prototype.logDocuments ).to.equal( undefined, 'After disabling engine debug (core~editor/editor)' );
@@ -366,261 +351,8 @@ describe( 'debug tools', () => {
 			} );
 		} );
 
-		describe( 'for deltas', () => {
-			it( 'Delta', () => {
-				const delta = new Delta();
-				const op = { log: sinon.spy() };
-				delta.addOperation( op );
-
-				sinon.spy( delta, 'log' );
-				delta.logAll();
-
-				expect( op.log.called ).to.be.true;
-				expect( delta.log.called ).to.be.true;
-			} );
-
-			it( 'AttributeDelta', () => {
-				modelRoot._appendChild( new ModelText( 'foobar' ) );
-
-				const delta = new AttributeDelta();
-				const op = new AttributeOperation( ModelRange.createIn( modelRoot ), 'key', null, { foo: 'bar' }, 0 );
-
-				delta.addOperation( op );
-
-				expect( delta.toString() ).to.equal( 'AttributeDelta( 0 ): "key": -> {"foo":"bar"}, main [ 0 ] - [ 6 ], 1 ops' );
-				delta.log();
-
-				expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
-			} );
-
-			it( 'InsertDelta (text node)', () => {
-				const delta = new InsertDelta();
-				const op = new InsertOperation( ModelPosition.createAt( modelRoot, 3 ), [ new ModelText( 'abc' ) ], 0 );
-
-				delta.addOperation( op );
-
-				expect( delta.toString() ).to.equal( 'InsertDelta( 0 ): #abc -> main [ 3 ]' );
-
-				delta.log();
-				expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
-			} );
-
-			it( 'InsertDelta (element)', () => {
-				const delta = new InsertDelta();
-				const op = new InsertOperation( ModelPosition.createAt( modelRoot, 3 ), [ new ModelElement( 'paragraph' ) ], 0 );
-
-				delta.addOperation( op );
-
-				expect( delta.toString() ).to.equal( 'InsertDelta( 0 ): <paragraph> -> main [ 3 ]' );
-
-				delta.log();
-				expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
-			} );
-
-			it( 'InsertDelta (multiple nodes)', () => {
-				const delta = new InsertDelta();
-				const nodes = [ new ModelText( 'x' ), new ModelElement( 'y' ), new ModelText( 'z' ) ];
-				const op = new InsertOperation( ModelPosition.createAt( modelRoot, 3 ), nodes, 0 );
-
-				delta.addOperation( op );
-
-				expect( delta.toString() ).to.equal( 'InsertDelta( 0 ): [ 3 ] -> main [ 3 ]' );
-
-				delta.log();
-				expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
-			} );
-
-			it( 'MarkerDelta', () => {
-				modelRoot._appendChild( new ModelText( 'foobar' ) );
-
-				const delta = new MarkerDelta();
-				const op = new MarkerOperation( 'marker', null, ModelRange.createIn( modelRoot ), modelDoc.markers, 0 );
-
-				delta.addOperation( op );
-
-				expect( delta.toString() ).to.equal( 'MarkerDelta( 0 ): "marker": null -> main [ 0 ] - [ 6 ]' );
-
-				delta.log();
-				expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
-			} );
-
-			it( 'MergeDelta', () => {
-				const otherRoot = modelDoc.createRoot( '$root', 'otherRoot' );
-				const firstEle = new ModelElement( 'paragraph' );
-				const removedEle = new ModelElement( 'paragraph', null, [ new ModelText( 'foo' ) ] );
-
-				otherRoot._appendChild( [ firstEle, removedEle ] );
-
-				const graveyard = modelDoc.graveyard;
-				const delta = new MergeDelta();
-				const move = new MoveOperation( ModelPosition.createAt( removedEle, 0 ), 3, ModelPosition.createAt( firstEle, 0 ), 0 );
-				const remove = new RemoveOperation(
-					ModelPosition.createBefore( removedEle ),
-					1,
-					ModelPosition.createAt( graveyard, 0 ),
-					1
-				);
-
-				delta.addOperation( move );
-				delta.addOperation( remove );
-
-				expect( delta.toString() ).to.equal( 'MergeDelta( 0 ): otherRoot [ 1 ]' );
-
-				delta.log();
-				expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
-			} );
-
-			it( 'MergeDelta - NoOperation as second operation', () => {
-				const otherRoot = modelDoc.createRoot( '$root', 'otherRoot' );
-				const firstEle = new ModelElement( 'paragraph' );
-				const removedEle = new ModelElement( 'paragraph', null, [ new ModelText( 'foo' ) ] );
-
-				otherRoot._appendChild( [ firstEle, removedEle ] );
-
-				const delta = new MergeDelta();
-				const move = new MoveOperation( ModelPosition.createAt( removedEle, 0 ), 3, ModelPosition.createAt( firstEle, 0 ), 0 );
-
-				delta.addOperation( move );
-				delta.addOperation( new NoOperation( 1 ) );
-
-				expect( delta.toString() ).to.equal( 'MergeDelta( 0 ): (move from otherRoot [ 1, 0 ])' );
-
-				delta.log();
-				expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
-			} );
-
-			it( 'MoveDelta', () => {
-				const delta = new MoveDelta();
-				const move1 = new MoveOperation( ModelPosition.createAt( modelRoot, 0 ), 1, ModelPosition.createAt( modelRoot, 3 ), 0 );
-				const move2 = new MoveOperation( ModelPosition.createAt( modelRoot, 1 ), 1, ModelPosition.createAt( modelRoot, 6 ), 0 );
-
-				delta.addOperation( move1 );
-				delta.addOperation( move2 );
-
-				expect( delta.toString() ).to.equal( 'MoveDelta( 0 ): main [ 0 ] - [ 1 ] -> main [ 3 ]; main [ 1 ] - [ 2 ] -> main [ 6 ]' );
-
-				delta.log();
-				expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
-			} );
-
-			it( 'RenameDelta', () => {
-				const delta = new RenameDelta();
-				const op = new RenameOperation( ModelPosition.createAt( modelRoot, 1 ), 'old', 'new', 0 );
-
-				delta.addOperation( op );
-
-				expect( delta.toString() ).to.equal( 'RenameDelta( 0 ): main [ 1 ]: "old" -> "new"' );
-
-				delta.log();
-				expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
-			} );
-
-			it( 'RootAttributeDelta', () => {
-				const delta = new RootAttributeDelta();
-				const op = new RootAttributeOperation( modelRoot, 'key', 'old', null, 0 );
-
-				delta.addOperation( op );
-
-				expect( delta.toString() ).to.equal( 'RootAttributeDelta( 0 ): "key": "old" -> null, main' );
-
-				delta.log();
-				expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
-			} );
-
-			it( 'SplitDelta', () => {
-				const otherRoot = modelDoc.createRoot( 'main', 'otherRoot' );
-				const splitEle = new ModelElement( 'paragraph', null, [ new ModelText( 'foo' ) ] );
-
-				otherRoot._appendChild( [ splitEle ] );
-
-				const delta = new SplitDelta();
-				const insert = new InsertOperation( ModelPosition.createAt( otherRoot, 1 ), [ new ModelElement( 'paragraph' ) ], 0 );
-				const move = new MoveOperation( ModelPosition.createAt( splitEle, 1 ), 2, new ModelPosition( otherRoot, [ 1, 0 ] ), 1 );
-
-				delta.addOperation( insert );
-				delta.addOperation( move );
-
-				expect( delta.toString() ).to.equal( 'SplitDelta( 0 ): otherRoot [ 0, 1 ]' );
-
-				delta.log();
-				expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
-			} );
-
-			it( 'SplitDelta - NoOperation as second operation', () => {
-				const otherRoot = modelDoc.createRoot( 'main', 'otherRoot' );
-				const splitEle = new ModelElement( 'paragraph', null, [ new ModelText( 'foo' ) ] );
-
-				otherRoot._appendChild( [ splitEle ] );
-
-				const delta = new SplitDelta();
-				const insert = new InsertOperation( ModelPosition.createAt( otherRoot, 1 ), [ new ModelElement( 'paragraph' ) ], 0 );
-				const move = new NoOperation( 1 );
-
-				delta.addOperation( insert );
-				delta.addOperation( move );
-
-				expect( delta.toString() ).to.equal( 'SplitDelta( 0 ): (clone to otherRoot [ 1 ])' );
-
-				delta.log();
-				expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
-			} );
-
-			it( 'SplitDelta - NoOperation as second operation, MoveOperation as first operation', () => {
-				const otherRoot = modelDoc.createRoot( 'main', 'otherRoot' );
-
-				const delta = new SplitDelta();
-				const insert = new MoveOperation( ModelPosition.createAt( modelRoot, 1 ), 1, ModelPosition.createAt( otherRoot, 1 ), 0 );
-				const move = new NoOperation( 1 );
-
-				delta.addOperation( insert );
-				delta.addOperation( move );
-
-				expect( delta.toString() ).to.equal( 'SplitDelta( 0 ): (clone to otherRoot [ 1 ])' );
-
-				delta.log();
-				expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
-			} );
-
-			it( 'UnwrapDelta', () => {
-				const otherRoot = modelDoc.createRoot( 'main', 'otherRoot' );
-				const unwrapEle = new ModelElement( 'paragraph', null, [ new ModelText( 'foo' ) ] );
-
-				otherRoot._appendChild( [ unwrapEle ] );
-
-				const graveyard = modelDoc.graveyard;
-				const delta = new UnwrapDelta();
-				const move = new MoveOperation( ModelPosition.createAt( unwrapEle, 0 ), 3, ModelPosition.createAt( otherRoot, 0 ), 0 );
-				const remove = new RemoveOperation( ModelPosition.createAt( otherRoot, 3 ), 1, ModelPosition.createAt( graveyard, 0 ), 1 );
-
-				delta.addOperation( move );
-				delta.addOperation( remove );
-
-				expect( delta.toString() ).to.equal( 'UnwrapDelta( 0 ): otherRoot [ 0 ]' );
-
-				delta.log();
-				expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
-			} );
-
-			it( 'WrapDelta', () => {
-				const delta = new WrapDelta();
-
-				const insert = new InsertOperation( ModelPosition.createAt( modelRoot, 6 ), new ModelElement( 'paragraph' ), 0 );
-				const move = new MoveOperation( ModelPosition.createAt( modelRoot, 0 ), 6, new ModelPosition( modelRoot, [ 1, 0 ] ), 1 );
-
-				delta.addOperation( insert );
-				delta.addOperation( move );
-
-				expect( delta.toString() ).to.equal( 'WrapDelta( 0 ): main [ 0 ] - [ 6 ] -> <paragraph>' );
-
-				delta.log();
-				expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
-			} );
-		} );
-
 		it( 'for applied operations', () => {
-			const delta = new InsertDelta();
 			const op = new InsertOperation( ModelPosition.createAt( modelRoot, 0 ), [ new ModelText( 'foo' ) ], 0 );
-			delta.addOperation( op );
 
 			model.applyOperation( op );
 
@@ -797,11 +529,11 @@ describe( 'debug tools', () => {
 
 			model.change( () => {
 				const insert = new InsertOperation( ModelPosition.createAt( modelRoot, 0 ), new ModelText( 'foobar' ), 0 );
-				model.applyOperation( wrapInDelta( insert ) );
+				model.applyOperation( insert );
 
 				const graveyard = modelDoc.graveyard;
 				const remove = new RemoveOperation( ModelPosition.createAt( modelRoot, 1 ), 2, ModelPosition.createAt( graveyard, 0 ), 1 );
-				model.applyOperation( wrapInDelta( remove ) );
+				model.applyOperation( remove );
 			} );
 
 			log.resetHistory();
@@ -889,7 +621,7 @@ describe( 'debug tools', () => {
 
 			for ( let i = 0; i < 25; i++ ) {
 				const insert = new InsertOperation( ModelPosition.createAt( modelRoot, 0 ), new ModelText( 'foobar' ), modelDoc.version );
-				model.applyOperation( wrapInDelta( insert ) );
+				model.applyOperation( insert );
 			}
 
 			modelDoc.log( 0 );
@@ -897,33 +629,24 @@ describe( 'debug tools', () => {
 		} );
 	} );
 
-	describe( 'should provide methods for delta replayer', () => {
-		it( 'getAppliedDeltas()', () => {
+	describe( 'should provide methods for operation replayer', () => {
+		it( 'getAppliedOperations()', () => {
 			const model = new Model();
 			const modelDoc = model.document;
 
-			expect( model.getAppliedDeltas() ).to.equal( '' );
+			expect( model.getAppliedOperations() ).to.equal( '' );
 
 			const otherRoot = modelDoc.createRoot( '$root', 'otherRoot' );
-			const firstEle = new ModelElement( 'paragraph' );
-			const removedEle = new ModelElement( 'paragraph', null, [ new ModelText( 'foo' ) ] );
-
-			otherRoot._appendChild( [ firstEle, removedEle ] );
+			const element = new ModelElement( 'paragraph' );
 
-			const delta = new MergeDelta();
-			const graveyard = modelDoc.graveyard;
-			const move = new MoveOperation( ModelPosition.createAt( removedEle, 0 ), 3, ModelPosition.createAt( firstEle, 0 ), 0 );
-			const remove = new RemoveOperation( ModelPosition.createBefore( removedEle ), 1, ModelPosition.createAt( graveyard, 0 ), 1 );
+			otherRoot._appendChild( element );
 
-			delta.addOperation( move );
-			delta.addOperation( remove );
+			const insert = new InsertOperation( ModelPosition.createAt( element, 0 ), new ModelText( 'foo' ), 0 );
+			model.applyOperation( insert );
 
-			model.applyOperation( move );
-			model.applyOperation( remove );
+			const stringifiedOperations = model.getAppliedOperations();
 
-			const stringifiedDeltas = model.getAppliedDeltas();
-
-			expect( stringifiedDeltas ).to.equal( JSON.stringify( delta.toJSON() ) );
+			expect( stringifiedOperations ).to.equal( JSON.stringify( insert.toJSON() ) );
 		} );
 
 		it( 'createReplayer()', () => {
@@ -931,28 +654,21 @@ describe( 'debug tools', () => {
 			const modelDoc = model.document;
 
 			const otherRoot = modelDoc.createRoot( '$root', 'otherRoot' );
-			const firstEle = new ModelElement( 'paragraph' );
-			const removedEle = new ModelElement( 'paragraph', null, [ new ModelText( 'foo' ) ] );
-
-			otherRoot._appendChild( [ firstEle, removedEle ] );
+			const element = new ModelElement( 'paragraph' );
 
-			const delta = new MergeDelta();
-			const graveyard = modelDoc.graveyard;
-			const move = new MoveOperation( ModelPosition.createAt( removedEle, 0 ), 3, ModelPosition.createAt( firstEle, 0 ), 0 );
-			const remove = new RemoveOperation( ModelPosition.createBefore( removedEle ), 1, ModelPosition.createAt( graveyard, 0 ), 1 );
+			otherRoot._appendChild( element );
 
-			delta.addOperation( move );
-			delta.addOperation( remove );
+			const insert = new InsertOperation( ModelPosition.createAt( element, 0 ), new ModelText( 'foo' ), 0 );
+			model.applyOperation( insert );
 
-			const stringifiedDeltas = JSON.stringify( delta.toJSON() );
+			const stringifiedOperations = model.getAppliedOperations();
+			const operationReplayer = model.createReplayer( stringifiedOperations );
 
-			const deltaReplayer = model.createReplayer( stringifiedDeltas );
-
-			expect( deltaReplayer.getDeltasToReplay() ).to.deep.equal( [ JSON.parse( stringifiedDeltas ) ] );
+			expect( operationReplayer.getOperationsToReplay() ).to.deep.equal( [ JSON.parse( stringifiedOperations ) ] );
 		} );
 	} );
 
-	describe( 'should provide debug tools for delta transformation', () => {
+	describe( 'should provide error logging for transformation', () => {
 		let model, document, root, otherRoot;
 
 		beforeEach( () => {
@@ -962,203 +678,26 @@ describe( 'debug tools', () => {
 			otherRoot = document.createRoot( 'other', 'other' );
 		} );
 
-		it( 'Delta#_saveHistory()', () => {
-			const insertDeltaA = new InsertDelta();
-			const insertOpA = new InsertOperation( ModelPosition.createAt( root, 0 ), new ModelText( 'a' ), 0 );
-			insertDeltaA.addOperation( insertOpA );
-
-			const insertDeltaB = new InsertDelta();
-			const insertOpB = new InsertOperation( ModelPosition.createAt( root, 0 ), new ModelText( 'a' ), 0 );
-			insertDeltaB.addOperation( insertOpB );
-
-			const insertDeltaFinalA = new InsertDelta();
-			const insertOpFinalA = new InsertOperation( ModelPosition.createAt( root, 0 ), new ModelText( 'a' ), 1 );
-			insertDeltaFinalA.addOperation( insertOpFinalA );
-			const insertDeltaFinalAJsonWithoutHistory = JSON.stringify( insertDeltaFinalA );
-			insertDeltaFinalA._saveHistory( {
-				before: insertDeltaA,
-				transformedBy: insertDeltaB,
-				wasImportant: true,
-				resultIndex: 0,
-				resultsTotal: 1
-			} );
-
-			const insertDeltaC = new InsertDelta();
-			const insertOpC = new InsertOperation( ModelPosition.createAt( root, 0 ), new ModelText( 'a' ), 1 );
-			insertDeltaC.addOperation( insertOpC );
-
-			const insertDeltaFinalB = new InsertDelta();
-			const insertOpFinalB = new InsertOperation( ModelPosition.createAt( root, 0 ), new ModelText( 'a' ), 2 );
-			insertDeltaFinalB.addOperation( insertOpFinalB );
-			insertDeltaFinalB._saveHistory( {
-				before: insertDeltaFinalA,
-				transformedBy: insertDeltaC,
-				wasImportant: false,
-				resultIndex: 1,
-				resultsTotal: 3
-			} );
-
-			expect( insertDeltaA.history ).to.be.undefined;
-			expect( insertDeltaB.history ).to.be.undefined;
-
-			expect( insertDeltaFinalA.history ).not.to.be.undefined;
-			expect( insertDeltaFinalA.history.length ).to.equal( 1 );
-			expect( insertDeltaFinalA.history[ 0 ] ).to.deep.equal( {
-				before: JSON.stringify( insertDeltaA ),
-				transformedBy: JSON.stringify( insertDeltaB ),
-				wasImportant: true,
-				resultIndex: 0,
-				resultsTotal: 1
-			} );
-
-			expect( insertDeltaFinalB.history ).not.to.be.undefined;
-			expect( insertDeltaFinalB.history.length ).to.equal( 2 );
-			expect( insertDeltaFinalB.history[ 0 ] ).to.deep.equal( {
-				before: JSON.stringify( insertDeltaA ),
-				transformedBy: JSON.stringify( insertDeltaB ),
-				wasImportant: true,
-				resultIndex: 0,
-				resultsTotal: 1
-			} );
-			expect( insertDeltaFinalB.history[ 1 ] ).to.deep.equal( {
-				before: insertDeltaFinalAJsonWithoutHistory,
-				transformedBy: JSON.stringify( insertDeltaC ),
-				wasImportant: false,
-				resultIndex: 1,
-				resultsTotal: 3
-			} );
-		} );
-
-		it( 'save delta history on deltaTransform.transform', () => {
-			const deltaA = new MoveDelta();
+		it.skip( 'with more important operation A', () => {
 			const opA = new MoveOperation( ModelPosition.createAt( root, 4 ), 4, ModelPosition.createAt( otherRoot, 4 ), 0 );
-			deltaA.addOperation( opA );
-
-			const deltaB = new InsertDelta();
 			const opB = new InsertOperation( ModelPosition.createAt( root, 0 ), new ModelText( 'a' ), 0 );
-			deltaB.addOperation( opB );
-
-			const deltaC = new MoveDelta();
-			const opC = new MoveOperation( ModelPosition.createAt( root, 4 ), 2, ModelPosition.createAt( root, 0 ), 1 );
-			deltaC.addOperation( opC );
-
-			let result = deltaTransform.transform( deltaA, deltaB, { document, wasAffected: new Map() } );
 
-			expect( result[ 0 ].history ).not.to.be.undefined;
-			expect( result[ 0 ].history.length ).to.equal( 1 );
-
-			expect( result[ 0 ].history[ 0 ] ).to.deep.equal( {
-				before: JSON.stringify( deltaA ),
-				transformedBy: JSON.stringify( deltaB ),
-				wasImportant: false,
-				resultIndex: 0,
-				resultsTotal: 1
-			} );
-
-			const firstResultWithoutHistory = result[ 0 ].clone();
-			delete firstResultWithoutHistory.history;
-
-			result = deltaTransform.transform( result[ 0 ], deltaC, {
-				isStrong: true,
-				document,
-				wasAffected: new Map()
-			} );
-			expect( result[ 0 ].history ).not.to.be.undefined;
-			expect( result[ 0 ].history.length ).to.equal( 2 );
-
-			expect( result[ 0 ].history[ 0 ] ).to.deep.equal( {
-				before: JSON.stringify( deltaA ),
-				transformedBy: JSON.stringify( deltaB ),
-				wasImportant: false,
-				resultIndex: 0,
-				resultsTotal: 1
-			} );
-
-			expect( result[ 0 ].history[ 1 ] ).to.deep.equal( {
-				before: JSON.stringify( firstResultWithoutHistory ),
-				transformedBy: JSON.stringify( deltaC ),
-				wasImportant: true,
-				resultIndex: 0,
-				resultsTotal: 2
-			} );
+			expect( () => {
+				transform.transform( opA, opB, { isStrong: true } );
+			} ).to.throw( Error );
+			expect( error.calledWith( opA.toString() + ' (important)' ) ).to.be.true;
+			expect( error.calledWith( opB.toString() ) ).to.be.true;
 		} );
 
-		it( 'recreate delta using Delta#history', () => {
-			const deltaA = new MoveDelta();
+		it.skip( 'with more important operation B', () => {
 			const opA = new MoveOperation( ModelPosition.createAt( root, 4 ), 4, ModelPosition.createAt( otherRoot, 4 ), 0 );
-			deltaA.addOperation( opA );
-
-			const deltaB = new InsertDelta();
 			const opB = new InsertOperation( ModelPosition.createAt( root, 0 ), new ModelText( 'a' ), 0 );
-			deltaB.addOperation( opB );
-
-			const deltaC = new MoveDelta();
-			const opC = new MoveOperation( ModelPosition.createAt( root, 4 ), 2, ModelPosition.createAt( root, 0 ), 1 );
-			deltaC.addOperation( opC );
-
-			let original = deltaTransform.transform( deltaA, deltaB, { document, wasAffected: new Map() } );
-			original = deltaTransform.transform( original[ 0 ], deltaC, {
-				isStrong: true,
-				document,
-				wasAffected: new Map()
-			} )[ 0 ];
-
-			const history = original.history;
-
-			let recreated = deltaTransform.transform(
-				DeltaFactory.fromJSON( JSON.parse( history[ 0 ].before ), document ),
-				DeltaFactory.fromJSON( JSON.parse( history[ 0 ].transformedBy ), document ),
-				{ isStrong: history[ 0 ].wasImportant, document, wasAffected: new Map() }
-			);
 
-			recreated = recreated[ history[ 0 ].resultIndex ];
+			testUtils.sinon.stub( transform, 'transform' ).throws( new Error() );
 
-			recreated = deltaTransform.transform(
-				recreated,
-				DeltaFactory.fromJSON( JSON.parse( history[ 1 ].transformedBy ), document ),
-				{ isStrong: history[ 1 ].wasImportant, document, wasAffected: new Map() }
-			);
-
-			recreated = recreated[ history[ 1 ].resultIndex ];
-
-			delete original.history;
-			delete recreated.history;
-
-			expect( JSON.stringify( recreated ) ).to.equal( JSON.stringify( original ) );
-		} );
-
-		describe( 'provide additional logging when transformation crashes', () => {
-			it( 'with more important delta A', () => {
-				const deltaA = new MoveDelta();
-				const opA = new MoveOperation( ModelPosition.createAt( root, 4 ), 4, ModelPosition.createAt( otherRoot, 4 ), 0 );
-				deltaA.addOperation( opA );
-
-				const deltaB = new InsertDelta();
-				const opB = new InsertOperation( ModelPosition.createAt( root, 0 ), new ModelText( 'a' ), 0 );
-				deltaB.addOperation( opB );
-
-				testUtils.sinon.stub( deltaTransform, 'defaultTransform' ).throws( new Error() );
-
-				expect( () => deltaTransform.transform( deltaA, deltaB, { isStrong: true } ) ).to.throw( Error );
-				expect( error.calledWith( deltaA.toString() + ' (important)' ) ).to.be.true;
-				expect( error.calledWith( deltaB.toString() ) ).to.be.true;
-			} );
-
-			it( 'with more important delta B', () => {
-				const deltaA = new MoveDelta();
-				const opA = new MoveOperation( ModelPosition.createAt( root, 4 ), 4, ModelPosition.createAt( otherRoot, 4 ), 0 );
-				deltaA.addOperation( opA );
-
-				const deltaB = new InsertDelta();
-				const opB = new InsertOperation( ModelPosition.createAt( root, 0 ), new ModelText( 'a' ), 0 );
-				deltaB.addOperation( opB );
-
-				testUtils.sinon.stub( deltaTransform, 'defaultTransform' ).throws( new Error() );
-
-				expect( () => deltaTransform.transform( deltaA, deltaB, { isStrong: false } ) ).to.throw( Error );
-				expect( error.calledWith( deltaA.toString() ) ).to.be.true;
-				expect( error.calledWith( deltaB.toString() + ' (important)' ) ).to.be.true;
-			} );
+			expect( () => transform.transform( opA, opB, { isStrong: true } ) ).to.throw( Error );
+			expect( error.calledWith( opA.toString() ) ).to.be.true;
+			expect( error.calledWith( opB.toString() + ' (important)' ) ).to.be.true;
 		} );
 	} );
 
@@ -1166,11 +705,4 @@ describe( 'debug tools', () => {
 		expect( log.calledWithExactly( expectedLogMsg ) ).to.be.true;
 		log.resetHistory();
 	}
-
-	function wrapInDelta( op ) {
-		const delta = new Delta();
-		delta.addOperation( op );
-
-		return op;
-	}
 } );

+ 2 - 2
packages/ckeditor5-engine/tests/dev-utils/model.js

@@ -61,7 +61,7 @@ describe( 'model test utils', () => {
 
 			expect( getData( model, { withoutSelection: true } ) ).to.equal( '<b>btext</b>' );
 			sinon.assert.calledOnce( stringifySpy );
-			sinon.assert.calledWithExactly( stringifySpy, root );
+			sinon.assert.calledWithExactly( stringifySpy, root, null, null );
 		} );
 
 		it( 'should use stringify method with selection', () => {
@@ -72,7 +72,7 @@ describe( 'model test utils', () => {
 			} );
 			expect( getData( model ) ).to.equal( '[<b>btext</b>]' );
 			sinon.assert.calledOnce( stringifySpy );
-			sinon.assert.calledWithExactly( stringifySpy, root, document.selection );
+			sinon.assert.calledWithExactly( stringifySpy, root, document.selection, null );
 		} );
 
 		it( 'should throw an error when passing invalid document', () => {

+ 197 - 0
packages/ckeditor5-engine/tests/dev-utils/operationreplayer.js

@@ -0,0 +1,197 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import OperationReplayer from '../../src/dev-utils/operationreplayer';
+import Model from '../../src/model/model';
+
+describe( 'OperationReplayer', () => {
+	describe( 'constructor()', () => {
+		it( 'should be able to initialize replayer without operations', () => {
+			const model = getModel();
+			const stringifiedOperations = '';
+			const operationReplayer = new OperationReplayer( model, '---', stringifiedOperations );
+
+			expect( operationReplayer.getOperationsToReplay() ).to.deep.equal( [] );
+		} );
+
+		it( 'should be able to initialize replayer with operations', () => {
+			const model = getModel();
+			const operation = getFirstOperation();
+
+			const operationReplayer = new OperationReplayer( model, '---', JSON.stringify( operation ) );
+
+			expect( operationReplayer.getOperationsToReplay() ).to.deep.equal( [ operation ] );
+		} );
+	} );
+
+	describe( 'applyNextOperation()', () => {
+		it( 'should remove first operation from stack', () => {
+			const model = getModel();
+			const operation = getFirstOperation();
+
+			const operationReplayer = new OperationReplayer( model, '---', JSON.stringify( operation ) );
+
+			return operationReplayer.applyNextOperation().then( isFinished => {
+				expect( operationReplayer.getOperationsToReplay() ).to.deep.equal( [] );
+				expect( isFinished ).to.equal( false );
+			} );
+		} );
+
+		it( 'should apply first operation on the document', () => {
+			const model = getModel();
+			const operation = getFirstOperation();
+
+			const operationReplayer = new OperationReplayer( model, '---', JSON.stringify( operation ) );
+
+			return operationReplayer.applyNextOperation().then( () => {
+				expect( Array.from( model.document.getRoot().getChildren() ).length ).to.equal( 1 );
+			} );
+		} );
+
+		it( 'should resolve with true if 0 operations are provided', () => {
+			const model = getModel();
+			const operationReplayer = new OperationReplayer( model, '---', '' );
+
+			return operationReplayer.applyNextOperation().then( isFinished => {
+				expect( isFinished ).to.equal( true );
+			} );
+		} );
+	} );
+
+	describe( 'applyAllOperations()', () => {
+		it( 'should apply all operations on the document', () => {
+			const model = getModel();
+
+			const stringifiedOperations = [ getFirstOperation(), getSecondOperation() ]
+				.map( d => JSON.stringify( d ) )
+				.join( '---' );
+
+			const operationReplayer = new OperationReplayer( model, '---', stringifiedOperations );
+
+			return operationReplayer.applyAllOperations().then( () => {
+				expect( Array.from( model.document.getRoot().getChildren() ).length ).to.equal( 2 );
+				expect( operationReplayer.getOperationsToReplay().length ).to.equal( 0 );
+			} );
+		} );
+	} );
+
+	describe( 'applyOperations()', () => {
+		it( 'should apply certain number of operations on the document', () => {
+			const model = getModel();
+
+			const stringifiedOperations = [ getFirstOperation(), getSecondOperation() ]
+				.map( d => JSON.stringify( d ) )
+				.join( '---' );
+
+			const operationReplayer = new OperationReplayer( model, '---', stringifiedOperations );
+
+			return operationReplayer.applyOperations( 1 ).then( () => {
+				expect( Array.from( model.document.getRoot().getChildren() ).length ).to.equal( 1 );
+				expect( operationReplayer.getOperationsToReplay().length ).to.equal( 1 );
+			} );
+		} );
+
+		it( 'should not throw an error if the number of operations is lower than number of expected operations to replay', () => {
+			const model = getModel();
+
+			const stringifiedOperations = [ getFirstOperation(), getSecondOperation() ]
+				.map( d => JSON.stringify( d ) )
+				.join( '---' );
+
+			const operationReplayer = new OperationReplayer( model, '---', stringifiedOperations );
+
+			return operationReplayer.applyOperations( 3 ).then( () => {
+				expect( Array.from( model.document.getRoot().getChildren() ).length ).to.equal( 2 );
+				expect( operationReplayer.getOperationsToReplay().length ).to.equal( 0 );
+			} );
+		} );
+	} );
+
+	describe( 'play()', () => {
+		it( 'should play operations with time interval', () => {
+			const model = getModel();
+
+			const stringifiedOperations = [ getFirstOperation(), getSecondOperation() ]
+				.map( d => JSON.stringify( d ) )
+				.join( '---' );
+
+			const operationReplayer = new OperationReplayer( model, '---', stringifiedOperations );
+
+			return operationReplayer.play( 0 ).then( () => {
+				expect( operationReplayer.getOperationsToReplay().length ).to.equal( 0 );
+			} );
+		} );
+
+		it( 'should work with default time interval', () => {
+			const model = getModel();
+
+			const operationReplayer = new OperationReplayer( model, '---', '' );
+
+			return operationReplayer.play();
+		} );
+
+		it( 'should correctly handle errors coming from the engine', () => {
+			const model = getModel();
+
+			const invalidOperation = getSecondOperation();
+			invalidOperation.baseVersion = 3;
+
+			const stringifiedOperations = [ getFirstOperation(), invalidOperation ]
+				.map( d => JSON.stringify( d ) )
+				.join( '---' );
+
+			const operationReplayer = new OperationReplayer( model, '---', stringifiedOperations );
+
+			return operationReplayer.play( 1 )
+				.then( () => {
+					throw new Error( 'It should throw an error' );
+				}, err => {
+					expect( err.message ).to.match( /^model-document-applyOperation-wrong-version/ );
+				} );
+		} );
+	} );
+} );
+
+function getModel() {
+	const model = new Model();
+
+	model.document.createRoot();
+
+	return model;
+}
+
+function getFirstOperation() {
+	return {
+		baseVersion: 0,
+		position: {
+			root: 'main',
+			path: [ 0 ]
+		},
+		nodes: [ {
+			name: 'heading1',
+			children: [ {
+				data: 'The great world of open Web standards'
+			} ]
+		} ],
+		__className: 'engine.model.operation.InsertOperation'
+	};
+}
+
+function getSecondOperation() {
+	return {
+		baseVersion: 1,
+		position: {
+			root: 'main',
+			path: [ 1 ]
+		},
+		nodes: [ {
+			name: 'heading2',
+			children: [ {
+				data: 'The great world of open Web standards'
+			} ]
+		} ],
+		__className: 'engine.model.operation.InsertOperation'
+	};
+}

+ 0 - 23
packages/ckeditor5-engine/tests/model/_utils/utils.js

@@ -7,8 +7,6 @@ import Range from '../../../src/model/range';
 import TreeWalker from '../../../src/model/treewalker';
 import Text from '../../../src/model/text';
 import TextProxy from '../../../src/model/textproxy';
-import Delta from '../../../src/model/delta/delta';
-import Batch from '../../../src/model/batch';
 
 /**
  * Returns tree structure as a simplified string. Elements are uppercase and characters are lowercase.
@@ -47,27 +45,6 @@ export function jsonParseStringify( object ) {
 	return JSON.parse( JSON.stringify( object ) );
 }
 
-/**
- * Adds given {@link module:engine/model/operation/operation~Operation operation} to a newly created
- * {@link module:engine/model/delta/delta~Delta delta}
- * and returns it back. Every operation, when applied, have to be added to a delta. This helper function is useful in those
- * tests which focus on operations, not deltas.
- *
- * @param {module:engine/model/operation/operation~Operation} operation Operation to wrap
- * @returns {module:engine/model/operation/operation~Operation}
- */
-export function wrapInDelta( operation ) {
-	const delta = new Delta();
-	// Batch() requires the document but only a few lines of code needs batch in `document#changes`
-	// so we may have an invalid batch instance for some tests.
-	const batch = new Batch();
-
-	delta.addOperation( operation );
-	batch.addDelta( delta );
-
-	return operation;
-}
-
 /**
  * Returns a {@link engine.model.Node} or if it starts at given offset, or {@link engine.model.TextProxy} with one
  * character, if given offset is occupied by a {@link engine.model.Text}.

+ 23 - 53
packages/ckeditor5-engine/tests/model/batch.js

@@ -4,90 +4,60 @@
  */
 
 import Batch from '../../src/model/batch';
-import Delta from '../../src/model/delta/delta';
 import Operation from '../../src/model/operation/operation';
 
 describe( 'Batch', () => {
 	describe( 'type', () => {
-		it( 'should default be "default"', () => {
+		it( 'should default to "default"', () => {
 			const batch = new Batch();
 
 			expect( batch.type ).to.equal( 'default' );
 		} );
 
 		it( 'should be set to the value set in constructor', () => {
-			const batch = new Batch( 'ignore' );
+			const batch = new Batch( 'transparent' );
 
-			expect( batch.type ).to.equal( 'ignore' );
+			expect( batch.type ).to.equal( 'transparent' );
+		} );
+	} );
+
+	describe( 'addOperation()', () => {
+		it( 'should add operation to the batch', () => {
+			const batch = new Batch();
+			const op = new Operation( 0 );
+
+			batch.addOperation( op );
+
+			expect( batch.operations.length ).to.equal( 1 );
+			expect( batch.operations[ 0 ] ).to.equal( op );
 		} );
 	} );
 
 	describe( 'baseVersion', () => {
-		it( 'should return base version of first delta from the batch', () => {
+		it( 'should return base version of the first operation from the batch', () => {
 			const batch = new Batch();
-			const delta = new Delta();
 			const operation = new Operation( 2 );
-			delta.addOperation( operation );
-			batch.addDelta( delta );
+			batch.addOperation( operation );
 
 			expect( batch.baseVersion ).to.equal( 2 );
 		} );
 
-		it( 'should return null if there are no deltas in batch', () => {
+		it( 'should return null if there are no operations in batch', () => {
 			const batch = new Batch();
 
 			expect( batch.baseVersion ).to.be.null;
 		} );
 
-		it( 'should return null if all deltas in batch have base version set to null', () => {
+		it( 'should return null if all operations in batch have base version set to null', () => {
 			const batch = new Batch();
 
-			const deltaA = new Delta();
-			deltaA.addOperation( new Operation( null ) );
+			const opA = new Operation( null );
+			const opB = new Operation( null );
 
-			const deltaB = new Delta();
-			deltaB.addOperation( new Operation( null ) );
-
-			batch.addDelta( deltaA );
-			batch.addDelta( deltaB );
+			batch.addOperation( opA );
+			batch.addOperation( opB );
 
 			expect( batch.baseVersion ).to.equal( null );
 		} );
 	} );
-
-	describe( 'addDelta()', () => {
-		it( 'should add delta to the batch', () => {
-			const batch = new Batch();
-			const deltaA = new Delta();
-			const deltaB = new Delta();
-			batch.addDelta( deltaA );
-			batch.addDelta( deltaB );
-
-			expect( batch.deltas.length ).to.equal( 2 );
-			expect( batch.deltas[ 0 ] ).to.equal( deltaA );
-			expect( batch.deltas[ 1 ] ).to.equal( deltaB );
-		} );
-	} );
-
-	describe( 'getOperations()', () => {
-		it( 'should return collection of operations from all deltas', () => {
-			const batch = new Batch();
-			const deltaA = new Delta();
-			const deltaB = new Delta();
-			const ops = [
-				new Operation( 0 ),
-				new Operation( 1 ),
-				new Operation( 2 )
-			];
-
-			batch.addDelta( deltaA );
-			deltaA.addOperation( ops[ 0 ] );
-			batch.addDelta( deltaB );
-			deltaA.addOperation( ops[ 1 ] );
-			deltaA.addOperation( ops[ 2 ] );
-
-			expect( Array.from( batch.getOperations() ) ).to.deep.equal( ops );
-			expect( batch.getOperations() ).to.have.property( 'next' );
-		} );
-	} );
 } );

+ 0 - 131
packages/ckeditor5-engine/tests/model/delta/attributedelta.js

@@ -1,131 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import Model from '../../../src/model/model';
-import Range from '../../../src/model/range';
-import Position from '../../../src/model/position';
-import AttributeDelta from '../../../src/model/delta/attributedelta';
-import AttributeOperation from '../../../src/model/operation/attributeoperation';
-import NoOperation from '../../../src/model/operation/nooperation';
-import { jsonParseStringify } from '../../../tests/model/_utils/utils';
-
-describe( 'AttributeDelta', () => {
-	let root, delta;
-
-	beforeEach( () => {
-		const model = new Model();
-
-		root = model.document.createRoot();
-		delta = new AttributeDelta();
-	} );
-
-	describe( 'type', () => {
-		it( 'should be equal to attribute', () => {
-			expect( delta.type ).to.equal( 'attribute' );
-		} );
-	} );
-
-	describe( 'key', () => {
-		it( 'should be null if there are no operations in delta', () => {
-			expect( delta.key ).to.be.null;
-		} );
-
-		it( 'should be equal to attribute operations key that are in delta', () => {
-			const range = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) );
-			delta.addOperation( new AttributeOperation( range, 'key', 'old', 'new', 0 ) );
-
-			expect( delta.key ).to.equal( 'key' );
-		} );
-	} );
-
-	describe( 'value', () => {
-		it( 'should be null if there are no operations in delta', () => {
-			expect( delta.value ).to.be.null;
-		} );
-
-		it( 'should be equal to the value set by the delta operations', () => {
-			const range = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) );
-			delta.addOperation( new AttributeOperation( range, 'key', 'old', 'new', 0 ) );
-
-			expect( delta.value ).to.equal( 'new' );
-		} );
-	} );
-
-	describe( 'range', () => {
-		it( 'should be null if there are no operations in delta', () => {
-			expect( delta.range ).to.be.null;
-		} );
-
-		it( 'start and end should be equal to first and last changed position', () => {
-			const rangeA = new Range( new Position( root, [ 2 ] ), new Position( root, [ 4 ] ) );
-			const rangeB = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) );
-			const rangeC = new Range( new Position( root, [ 5 ] ), new Position( root, [ 6 ] ) );
-
-			delta.addOperation( new AttributeOperation( rangeA, 'key', 'oldA', 'new', 0 ) );
-			delta.addOperation( new AttributeOperation( rangeB, 'key', 'oldB', 'new', 1 ) );
-			delta.addOperation( new AttributeOperation( rangeC, 'key', 'oldC', 'new', 2 ) );
-
-			expect( delta.range.start.path ).to.deep.equal( [ 1 ] );
-			expect( delta.range.end.path ).to.deep.equal( [ 6 ] );
-		} );
-
-		it( 'should return correct values when some operations are NoOperations', () => {
-			const rangeA = new Range( new Position( root, [ 2 ] ), new Position( root, [ 4 ] ) );
-			const rangeB = new Range( new Position( root, [ 5 ] ), new Position( root, [ 6 ] ) );
-
-			delta.addOperation( new AttributeOperation( rangeA, 'key', 'oldA', 'new', 0 ) );
-			delta.addOperation( new NoOperation( 1 ) );
-			delta.addOperation( new AttributeOperation( rangeB, 'key', 'oldC', 'new', 2 ) );
-
-			expect( delta.range.start.path ).to.deep.equal( [ 2 ] );
-			expect( delta.range.end.path ).to.deep.equal( [ 6 ] );
-		} );
-	} );
-
-	describe( 'getReversed', () => {
-		it( 'should return empty AttributeDelta if there are no operations in delta', () => {
-			const reversed = delta.getReversed();
-
-			expect( reversed ).to.be.instanceof( AttributeDelta );
-			expect( reversed.operations.length ).to.equal( 0 );
-		} );
-
-		it( 'should return correct AttributeDelta', () => {
-			const rangeA = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) );
-			const rangeB = new Range( new Position( root, [ 2 ] ), new Position( root, [ 4 ] ) );
-
-			delta.addOperation( new AttributeOperation( rangeA, 'key', 'oldA', 'new', 0 ) );
-			delta.addOperation( new AttributeOperation( rangeB, 'key', 'oldB', 'new', 1 ) );
-
-			const reversed = delta.getReversed();
-
-			expect( reversed ).to.be.instanceof( AttributeDelta );
-			expect( reversed.operations.length ).to.equal( 2 );
-
-			// Remember about reversed operations order.
-			expect( reversed.operations[ 0 ] ).to.be.instanceof( AttributeOperation );
-			expect( reversed.operations[ 0 ].range.isEqual( rangeB ) ).to.be.true;
-			expect( reversed.operations[ 0 ].key ).to.equal( 'key' );
-			expect( reversed.operations[ 0 ].oldValue ).to.equal( 'new' );
-			expect( reversed.operations[ 0 ].newValue ).to.equal( 'oldB' );
-
-			expect( reversed.operations[ 1 ] ).to.be.instanceof( AttributeOperation );
-			expect( reversed.operations[ 1 ].range.isEqual( rangeA ) ).to.be.true;
-			expect( reversed.operations[ 1 ].key ).to.equal( 'key' );
-			expect( reversed.operations[ 1 ].oldValue ).to.equal( 'new' );
-			expect( reversed.operations[ 1 ].newValue ).to.equal( 'oldA' );
-		} );
-	} );
-
-	it( 'should provide proper className', () => {
-		expect( AttributeDelta.className ).to.equal( 'engine.model.delta.AttributeDelta' );
-	} );
-
-	it( 'should not have _range property when converted to JSON', () => {
-		const json = jsonParseStringify( delta );
-
-		expect( json ).not.to.have.property( '_range' );
-	} );
-} );

+ 0 - 276
packages/ckeditor5-engine/tests/model/delta/delta.js

@@ -1,276 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import count from '@ckeditor/ckeditor5-utils/src/count';
-import Delta from '../../../src/model/delta/delta';
-import Operation from '../../../src/model/operation/operation';
-import AttributeOperation from '../../../src/model/operation/attributeoperation';
-import InsertOperation from '../../../src/model/operation/insertoperation';
-import MoveOperation from '../../../src/model/operation/moveoperation';
-import NoOperation from '../../../src/model/operation/nooperation';
-import ReinsertOperation from '../../../src/model/operation/reinsertoperation';
-import RemoveOperation from '../../../src/model/operation/removeoperation';
-import RootAttributeOperation from '../../../src/model/operation/rootattributeoperation';
-import DeltaFactory from '../../../src/model/delta/deltafactory';
-import Model from '../../../src/model/model';
-import Position from '../../../src/model/position';
-import Range from '../../../src/model/range';
-import { jsonParseStringify } from '../../../tests/model/_utils/utils';
-
-// Some test examples of operations.
-class FooOperation extends Operation {
-	constructor( string, baseVersion ) {
-		super( baseVersion );
-		this.string = string;
-	}
-
-	getReversed() {
-		return new BarOperation( this.string, this.baseVersion );
-	}
-}
-
-class BarOperation extends FooOperation {
-	getReversed() {
-		return new FooOperation( this.string, this.baseVersion );
-	}
-}
-
-class FooDelta extends Delta {
-	static get className() {
-		return 'tets.delta.foo';
-	}
-}
-
-describe( 'Delta', () => {
-	describe( 'constructor()', () => {
-		it( 'should create an delta with empty properties', () => {
-			const delta = new Delta();
-
-			expect( delta ).to.have.property( 'batch' ).that.is.null;
-			expect( delta ).to.have.property( 'operations' ).that.a( 'array' ).and.have.length( 0 );
-		} );
-	} );
-
-	describe( 'baseVersion', () => {
-		it( 'should return baseVersion of first operation in the delta', () => {
-			const delta = new Delta();
-
-			delta.addOperation( { baseVersion: 0 } );
-			delta.addOperation( { baseVersion: 1 } );
-			delta.addOperation( { baseVersion: 2 } );
-
-			expect( delta.baseVersion ).to.equal( 0 );
-		} );
-
-		it( 'should change baseVersion of it\'s operations', () => {
-			const delta = new Delta();
-
-			delta.addOperation( { baseVersion: 0 } );
-			delta.addOperation( { baseVersion: 1 } );
-			delta.addOperation( { baseVersion: 2 } );
-
-			delta.baseVersion = 10;
-
-			expect( delta.operations[ 0 ].baseVersion ).to.equal( 10 );
-			expect( delta.operations[ 1 ].baseVersion ).to.equal( 11 );
-			expect( delta.operations[ 2 ].baseVersion ).to.equal( 12 );
-		} );
-	} );
-
-	describe( 'addOperation', () => {
-		it( 'should add operation to the delta', () => {
-			const delta = new Delta();
-			const operation = {};
-
-			delta.addOperation( operation );
-
-			expect( delta.operations ).to.have.length( 1 );
-			expect( delta.operations[ 0 ] ).to.equal( operation );
-		} );
-
-		it( 'should add delta property to the operation', () => {
-			const delta = new Delta();
-			const operation = {};
-
-			delta.addOperation( operation );
-
-			expect( operation.delta ).to.equal( delta );
-		} );
-	} );
-
-	describe( 'iterator', () => {
-		it( 'should iterate over delta operations', () => {
-			const delta = new Delta();
-
-			delta.addOperation( {} );
-			delta.addOperation( {} );
-			delta.addOperation( {} );
-
-			const totalNumber = count( delta.operations );
-
-			expect( totalNumber ).to.equal( 3 );
-		} );
-	} );
-
-	describe( 'getReversed', () => {
-		it( 'should return empty Delta if there are no operations in delta', () => {
-			const delta = new Delta();
-			const reversed = delta.getReversed();
-
-			expect( reversed ).to.be.instanceof( Delta );
-			expect( reversed.operations.length ).to.equal( 0 );
-		} );
-
-		it( 'should return Delta with all operations reversed and their order reversed', () => {
-			const delta = new Delta();
-			delta.addOperation( new FooOperation( 'a', 1 ) );
-			delta.addOperation( new BarOperation( 'b', 2 ) );
-
-			const reversed = delta.getReversed();
-
-			expect( reversed ).to.be.instanceof( Delta );
-			expect( reversed.operations.length ).to.equal( 2 );
-
-			expect( reversed.operations[ 0 ] ).to.be.instanceOf( FooOperation );
-			expect( reversed.operations[ 0 ].string ).to.equal( 'b' );
-			expect( reversed.operations[ 1 ] ).to.be.instanceOf( BarOperation );
-			expect( reversed.operations[ 1 ].string ).to.equal( 'a' );
-		} );
-	} );
-
-	describe( 'toJSON', () => {
-		let delta, root, doc;
-
-		before( () => {
-			DeltaFactory.register( FooDelta );
-		} );
-
-		beforeEach( () => {
-			const model = new Model();
-
-			delta = new FooDelta();
-			doc = model.document;
-			root = doc.createRoot();
-		} );
-
-		it( 'should return JSON representation for empty delta', () => {
-			expect( jsonParseStringify( delta ) ).to.deep.equal( {
-				__className: FooDelta.className,
-				operations: []
-			} );
-		} );
-
-		it( 'should create delta with AttributeOperation', () => {
-			const operation = new AttributeOperation(
-				new Range( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) ),
-				'foo',
-				true,
-				null,
-				doc.version
-			);
-
-			delta.addOperation( operation );
-
-			expect( jsonParseStringify( delta ) ).to.deep.equal( {
-				__className: FooDelta.className,
-				operations: [ jsonParseStringify( operation ) ]
-			} );
-		} );
-
-		it( 'should create delta with InsertOperation', () => {
-			const operation = new InsertOperation(
-				new Position( root, [ 0 ] ),
-				'x',
-				doc.version
-			);
-
-			delta.addOperation( operation );
-
-			expect( jsonParseStringify( delta ) ).to.deep.equal( {
-				__className: FooDelta.className,
-				operations: [ jsonParseStringify( operation ) ]
-			} );
-		} );
-
-		it( 'should create delta with MoveOperation', () => {
-			const operation = new MoveOperation(
-				new Position( root, [ 0, 0 ] ),
-				1,
-				new Position( root, [ 1, 0 ] ),
-				doc.version
-			);
-
-			delta.addOperation( operation );
-
-			expect( jsonParseStringify( delta ) ).to.deep.equal( {
-				__className: FooDelta.className,
-				operations: [ jsonParseStringify( operation ) ]
-			} );
-		} );
-
-		it( 'should create delta with NoOperation', () => {
-			const operation = new NoOperation( 0 );
-
-			delta.addOperation( operation );
-
-			expect( jsonParseStringify( delta ) ).to.deep.equal( {
-				__className: FooDelta.className,
-				operations: [ jsonParseStringify( operation ) ]
-			} );
-		} );
-
-		it( 'should create delta with ReinsertOperation', () => {
-			const operation = new ReinsertOperation(
-				new Position( doc.graveyard, [ 0 ] ),
-				2,
-				new Position( root, [ 0 ] ),
-				doc.version
-			);
-
-			delta.addOperation( operation );
-
-			expect( jsonParseStringify( delta ) ).to.deep.equal( {
-				__className: FooDelta.className,
-				operations: [ jsonParseStringify( operation ) ]
-			} );
-		} );
-
-		it( 'should create delta with RemoveOperation', () => {
-			const operation = new RemoveOperation(
-				new Position( root, [ 2 ] ),
-				2,
-				new Position( doc.graveyard, [ 0 ] ),
-				doc.version
-			);
-
-			delta.addOperation( operation );
-
-			expect( jsonParseStringify( delta ) ).to.deep.equal( {
-				__className: FooDelta.className,
-				operations: [ jsonParseStringify( operation ) ]
-			} );
-		} );
-
-		it( 'should create delta with RootAttributeOperation', () => {
-			const operation = new RootAttributeOperation( root, 'key', null, 'newValue', doc.version );
-
-			delta.addOperation( operation );
-
-			expect( jsonParseStringify( delta ) ).to.deep.equal( {
-				__className: FooDelta.className,
-				operations: [ jsonParseStringify( operation ) ]
-			} );
-		} );
-
-		it( 'should remove batch reference', () => {
-			delta.batch = { foo: 'bar' };
-
-			expect( jsonParseStringify( delta ) ).to.deep.equal( {
-				__className: FooDelta.className,
-				operations: []
-			} );
-		} );
-	} );
-} );

+ 0 - 168
packages/ckeditor5-engine/tests/model/delta/deltafactory.js

@@ -1,168 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import Delta from '../../../src/model/delta/delta';
-import InsertDelta from '../../../src/model/delta/insertdelta';
-
-import AttributeOperation from '../../../src/model/operation/attributeoperation';
-import InsertOperation from '../../../src/model/operation/insertoperation';
-import MoveOperation from '../../../src/model/operation/moveoperation';
-import NoOperation from '../../../src/model/operation/nooperation';
-import ReinsertOperation from '../../../src/model/operation/reinsertoperation';
-import RemoveOperation from '../../../src/model/operation/removeoperation';
-import RootAttributeOperation from '../../../src/model/operation/rootattributeoperation';
-
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-
-import DeltaFactory from '../../../src/model/delta/deltafactory';
-
-import Model from '../../../src/model/model';
-import Position from '../../../src/model/position';
-import Range from '../../../src/model/range';
-import { jsonParseStringify } from '../../../tests/model/_utils/utils';
-
-class FooDelta extends Delta {
-	static get className() {
-		return 'tets.delta.foo';
-	}
-}
-
-class BarDelta extends Delta {
-	static get className() {
-		return 'tets.delta.bar';
-	}
-}
-
-describe( 'DeltaFactory', () => {
-	describe( 'fromJSON', () => {
-		let delta, root, doc;
-
-		before( () => {
-			DeltaFactory.register( FooDelta );
-		} );
-
-		beforeEach( () => {
-			const model = new Model();
-
-			delta = new FooDelta();
-			doc = model.document;
-			root = doc.createRoot();
-		} );
-
-		it( 'should throw error for unregistered delta', () => {
-			expect( () => {
-				DeltaFactory.fromJSON( jsonParseStringify( new BarDelta() ), doc );
-			} ).to.throw( CKEditorError, /^delta-fromjson-no-deserializer/ );
-		} );
-
-		it( 'should create delta with AttributeOperation', () => {
-			delta.addOperation( new AttributeOperation(
-				new Range( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) ),
-				'foo',
-				true,
-				null,
-				doc.version
-			) );
-
-			const deserialized = DeltaFactory.fromJSON( jsonParseStringify( delta ), doc );
-
-			expect( deserialized ).to.deep.equal( delta );
-		} );
-
-		it( 'should create delta with InsertOperation', () => {
-			delta.addOperation( new InsertOperation(
-				new Position( root, [ 0 ] ),
-				'x',
-				doc.version
-			) );
-
-			const deserialized = DeltaFactory.fromJSON( jsonParseStringify( delta ), doc );
-
-			expect( deserialized ).to.deep.equal( delta );
-		} );
-
-		it( 'should create delta with MoveOperation', () => {
-			delta.addOperation( new MoveOperation(
-				new Position( root, [ 0, 0 ] ),
-				1,
-				new Position( root, [ 1, 0 ] ),
-				doc.version
-			) );
-
-			const deserialized = DeltaFactory.fromJSON( jsonParseStringify( delta ), doc );
-
-			expect( deserialized ).to.deep.equal( delta );
-		} );
-
-		it( 'should create delta with NoOperation', () => {
-			delta.addOperation( new NoOperation( 0 ) );
-
-			const deserialized = DeltaFactory.fromJSON( jsonParseStringify( delta ), doc );
-
-			expect( deserialized ).to.deep.equal( delta );
-		} );
-
-		it( 'should create delta with ReinsertOperation', () => {
-			delta.addOperation( new ReinsertOperation(
-				new Position( doc.graveyard, [ 0 ] ),
-				2,
-				new Position( root, [ 0 ] ),
-				doc.version
-			) );
-
-			const deserialized = DeltaFactory.fromJSON( jsonParseStringify( delta ), doc );
-
-			expect( deserialized ).to.deep.equal( delta );
-		} );
-
-		it( 'should create delta with RemoveOperation', () => {
-			delta.addOperation( new RemoveOperation(
-				new Position( root, [ 2 ] ),
-				2,
-				new Position( doc.graveyard, [ 0 ] ),
-				doc.version
-			) );
-
-			const deserialized = DeltaFactory.fromJSON( jsonParseStringify( delta ), doc );
-
-			expect( deserialized ).to.deep.equal( delta );
-		} );
-
-		it( 'should create delta with RootAttributeOperation', () => {
-			delta.addOperation( new RootAttributeOperation( root, 'key', null, 'newValue', doc.version ) );
-
-			const deserialized = DeltaFactory.fromJSON( jsonParseStringify( delta ), doc );
-
-			expect( deserialized ).to.deep.equal( delta );
-		} );
-
-		it( 'should create InsertDelta instance from serialized JSON object', () => {
-			const insertDelta = new InsertDelta();
-			const serialized = jsonParseStringify( insertDelta );
-			const deserialized = DeltaFactory.fromJSON( serialized, doc );
-
-			expect( deserialized ).to.be.instanceOf( InsertDelta );
-			expect( deserialized.operations ).to.have.length( 0 );
-		} );
-	} );
-
-	describe( 'register', () => {
-		it( 'should add delta deserializer', done => {
-			class SomeDelta {
-				constructor() {
-					done();
-				}
-
-				static get className() {
-					return 'foo';
-				}
-			}
-
-			DeltaFactory.register( SomeDelta );
-
-			DeltaFactory.fromJSON( { __className: 'foo', operations: [] } );
-		} );
-	} );
-} );

+ 0 - 93
packages/ckeditor5-engine/tests/model/delta/insertdelta.js

@@ -1,93 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import Model from '../../../src/model/model';
-import Element from '../../../src/model/element';
-import Position from '../../../src/model/position';
-
-import InsertOperation from '../../../src/model/operation/insertoperation';
-import InsertDelta from '../../../src/model/delta/insertdelta';
-
-import RemoveDelta from '../../../src/model/delta/removedelta';
-import RemoveOperation from '../../../src/model/operation/removeoperation';
-
-describe( 'InsertDelta', () => {
-	let insertDelta, doc, root;
-
-	beforeEach( () => {
-		const model = new Model();
-
-		doc = model.document;
-		root = doc.createRoot();
-		insertDelta = new InsertDelta();
-	} );
-
-	describe( 'constructor()', () => {
-		it( 'should create insert delta with no operations added', () => {
-			expect( insertDelta.operations.length ).to.equal( 0 );
-		} );
-	} );
-
-	describe( 'type', () => {
-		it( 'should be equal to insert', () => {
-			expect( insertDelta.type ).to.equal( 'insert' );
-		} );
-	} );
-
-	describe( 'position', () => {
-		it( 'should be null if there are no operations in delta', () => {
-			expect( insertDelta.position ).to.be.null;
-		} );
-
-		it( 'should be equal to the position where node is inserted', () => {
-			insertDelta.operations.push( new InsertOperation( new Position( root, [ 1, 2, 3 ] ), new Element( 'x' ), 0 ) );
-
-			expect( insertDelta.position.root ).to.equal( root );
-			expect( insertDelta.position.path ).to.deep.equal( [ 1, 2, 3 ] );
-		} );
-	} );
-
-	describe( 'nodes', () => {
-		it( 'should be null if there are no operations in delta', () => {
-			expect( insertDelta.nodes ).to.be.null;
-		} );
-
-		it( 'should be equal to the nodes inserted by the delta', () => {
-			const elementX = new Element( 'x' );
-			insertDelta.operations.push( new InsertOperation( new Position( root, [ 1, 2, 3 ] ), elementX, 0 ) );
-
-			expect( insertDelta.nodes.length ).to.equal( 1 );
-			expect( insertDelta.nodes.getNode( 0 ) ).to.equal( elementX );
-		} );
-	} );
-
-	describe( 'getReversed', () => {
-		it( 'should return empty RemoveDelta if there are no operations in delta', () => {
-			const reversed = insertDelta.getReversed();
-
-			expect( reversed ).to.be.instanceof( RemoveDelta );
-			expect( reversed.operations.length ).to.equal( 0 );
-		} );
-
-		it( 'should return correct RemoveDelta', () => {
-			const position = new Position( root, [ 1, 2, 3 ] );
-			const elementX = new Element( 'x' );
-			insertDelta.operations.push( new InsertOperation( position, elementX, 0 ) );
-
-			const reversed = insertDelta.getReversed();
-
-			expect( reversed ).to.be.instanceof( RemoveDelta );
-			expect( reversed.operations.length ).to.equal( 1 );
-
-			expect( reversed.operations[ 0 ] ).to.be.instanceof( RemoveOperation );
-			expect( reversed.operations[ 0 ].sourcePosition.isEqual( position ) ).to.be.true;
-			expect( reversed.operations[ 0 ].howMany ).to.equal( 1 );
-		} );
-	} );
-
-	it( 'should provide proper className', () => {
-		expect( InsertDelta.className ).to.equal( 'engine.model.delta.InsertDelta' );
-	} );
-} );

+ 0 - 55
packages/ckeditor5-engine/tests/model/delta/markerdelta.js

@@ -1,55 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import Model from '../../../src/model/model';
-import Range from '../../../src/model/range';
-
-import MarkerDelta from '../../../src/model/delta/markerdelta';
-import MarkerOperation from '../../../src/model/operation/markeroperation';
-
-describe( 'MarkerDelta', () => {
-	let markerDelta, doc, root, range;
-
-	beforeEach( () => {
-		const model = new Model();
-
-		doc = model.document;
-		root = doc.createRoot();
-		range = Range.createIn( root );
-		markerDelta = new MarkerDelta();
-	} );
-
-	describe( 'constructor()', () => {
-		it( 'should create merge delta with no operations added', () => {
-			expect( markerDelta.operations.length ).to.equal( 0 );
-		} );
-	} );
-
-	describe( 'type', () => {
-		it( 'should be equal to marker', () => {
-			expect( markerDelta.type ).to.equal( 'marker' );
-		} );
-	} );
-
-	describe( 'getReversed', () => {
-		it( 'should return correct MarkerDelta', () => {
-			markerDelta.addOperation( new MarkerOperation( 'name', null, range, 0 ) );
-			const reversed = markerDelta.getReversed();
-
-			expect( reversed ).to.be.instanceof( MarkerDelta );
-			expect( reversed.operations.length ).to.equal( 1 );
-
-			const op = reversed.operations[ 0 ];
-
-			expect( op ).to.be.an.instanceof( MarkerOperation );
-			expect( op.oldRange.isEqual( range ) ).to.be.true;
-			expect( op.newRange ).to.be.null;
-		} );
-	} );
-
-	it( 'should provide proper className', () => {
-		expect( MarkerDelta.className ).to.equal( 'engine.model.delta.MarkerDelta' );
-	} );
-} );

+ 0 - 84
packages/ckeditor5-engine/tests/model/delta/mergedelta.js

@@ -1,84 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import Model from '../../../src/model/model';
-import Position from '../../../src/model/position';
-
-import MergeDelta from '../../../src/model/delta/mergedelta';
-import SplitDelta from '../../../src/model/delta/splitdelta';
-
-import MoveOperation from '../../../src/model/operation/moveoperation';
-import RemoveOperation from '../../../src/model/operation/removeoperation';
-import ReinsertOperation from '../../../src/model/operation/reinsertoperation';
-
-describe( 'MergeDelta', () => {
-	let mergeDelta, doc, root;
-
-	beforeEach( () => {
-		const model = new Model();
-
-		doc = model.document;
-		root = doc.createRoot();
-		mergeDelta = new MergeDelta();
-	} );
-
-	describe( 'constructor()', () => {
-		it( 'should create merge delta with no operations added', () => {
-			expect( mergeDelta.operations.length ).to.equal( 0 );
-		} );
-	} );
-
-	describe( 'type', () => {
-		it( 'should be equal to merge', () => {
-			expect( mergeDelta.type ).to.equal( 'merge' );
-		} );
-	} );
-
-	describe( 'position', () => {
-		it( 'should be null if there are no operations in delta', () => {
-			expect( mergeDelta.position ).to.be.null;
-		} );
-
-		it( 'should be equal to the position between merged nodes', () => {
-			mergeDelta.operations.push( new MoveOperation( new Position( root, [ 1, 2, 0 ] ), 4, new Position( root, [ 1, 1, 4 ] ) ) );
-			mergeDelta.operations.push( new RemoveOperation( new Position( root, [ 1, 2, 0 ] ), 1, new Position( doc.graveyard, [ 0 ] ) ) );
-
-			expect( mergeDelta.position.root ).to.equal( root );
-			expect( mergeDelta.position.path ).to.deep.equal( [ 1, 2, 0 ] );
-		} );
-	} );
-
-	describe( 'getReversed', () => {
-		it( 'should return empty SplitDelta if there are no operations in delta', () => {
-			const reversed = mergeDelta.getReversed();
-
-			expect( reversed ).to.be.instanceof( SplitDelta );
-			expect( reversed.operations.length ).to.equal( 0 );
-		} );
-
-		it( 'should return correct SplitDelta', () => {
-			mergeDelta.operations.push( new MoveOperation( new Position( root, [ 1, 2, 0 ] ), 4, new Position( root, [ 1, 1, 4 ] ) ) );
-			mergeDelta.operations.push( new RemoveOperation( new Position( root, [ 1, 2, 0 ] ), 1, new Position( doc.graveyard, [ 0 ] ) ) );
-
-			const reversed = mergeDelta.getReversed();
-
-			expect( reversed ).to.be.instanceof( SplitDelta );
-			expect( reversed.operations.length ).to.equal( 2 );
-
-			expect( reversed.operations[ 0 ] ).to.be.instanceof( ReinsertOperation );
-			expect( reversed.operations[ 0 ].howMany ).to.equal( 1 );
-			expect( reversed.operations[ 0 ].targetPosition.path ).to.deep.equal( [ 1, 2, 0 ] );
-
-			expect( reversed.operations[ 1 ] ).to.be.instanceof( MoveOperation );
-			expect( reversed.operations[ 1 ].sourcePosition.path ).to.deep.equal( [ 1, 1, 4 ] );
-			expect( reversed.operations[ 1 ].howMany ).to.equal( 4 );
-			expect( reversed.operations[ 1 ].targetPosition.path ).to.deep.equal( [ 1, 2, 0 ] );
-		} );
-	} );
-
-	it( 'should provide proper className', () => {
-		expect( MergeDelta.className ).to.equal( 'engine.model.delta.MergeDelta' );
-	} );
-} );

+ 0 - 99
packages/ckeditor5-engine/tests/model/delta/movedelta.js

@@ -1,99 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import Model from '../../../src/model/model';
-import Position from '../../../src/model/position';
-
-import MoveDelta from '../../../src/model/delta/movedelta';
-import MoveOperation from '../../../src/model/operation/moveoperation';
-
-describe( 'MoveDelta', () => {
-	let moveDelta, doc, root;
-
-	beforeEach( () => {
-		const model = new Model();
-
-		doc = model.document;
-		root = doc.createRoot();
-		moveDelta = new MoveDelta();
-	} );
-
-	describe( 'constructor()', () => {
-		it( 'should create move delta with no operations added', () => {
-			expect( moveDelta.operations.length ).to.equal( 0 );
-		} );
-	} );
-
-	describe( 'type', () => {
-		it( 'should be equal to move', () => {
-			expect( moveDelta.type ).to.equal( 'move' );
-		} );
-	} );
-
-	describe( 'sourcePosition', () => {
-		it( 'should be null if there are no operations in delta', () => {
-			expect( moveDelta.sourcePosition ).to.be.null;
-		} );
-
-		it( 'should be equal to the position where node is inserted', () => {
-			moveDelta.operations.push( new MoveOperation( new Position( root, [ 1, 1 ] ), 2, new Position( root, [ 2, 2 ] ), 0 ) );
-
-			expect( moveDelta.sourcePosition.root ).to.equal( root );
-			expect( moveDelta.sourcePosition.path ).to.deep.equal( [ 1, 1 ] );
-		} );
-	} );
-
-	describe( 'howMany', () => {
-		it( 'should be null if there are no operations in delta', () => {
-			expect( moveDelta.howMany ).to.be.null;
-		} );
-
-		it( 'should be equal to the position where node is inserted', () => {
-			moveDelta.operations.push( new MoveOperation( new Position( root, [ 1, 1 ] ), 2, new Position( root, [ 2, 2 ] ), 0 ) );
-
-			expect( moveDelta.howMany ).to.equal( 2 );
-		} );
-	} );
-
-	describe( 'targetPosition', () => {
-		it( 'should be null if there are no operations in delta', () => {
-			expect( moveDelta.targetPosition ).to.be.null;
-		} );
-
-		it( 'should be equal to the move operation\'s target position', () => {
-			moveDelta.operations.push( new MoveOperation( new Position( root, [ 1, 1 ] ), 2, new Position( root, [ 2, 2 ] ), 0 ) );
-
-			expect( moveDelta.targetPosition.root ).to.equal( root );
-			expect( moveDelta.targetPosition.path ).to.deep.equal( [ 2, 2 ] );
-		} );
-	} );
-
-	describe( 'getReversed', () => {
-		it( 'should return empty MoveDelta if there are no operations in delta', () => {
-			const reversed = moveDelta.getReversed();
-
-			expect( reversed ).to.be.instanceof( MoveDelta );
-			expect( reversed.operations.length ).to.equal( 0 );
-		} );
-
-		it( 'should return correct MoveDelta', () => {
-			moveDelta.operations.push( new MoveOperation( new Position( root, [ 1, 1 ] ), 2, new Position( root, [ 2, 2 ] ), 0 ) );
-
-			const reversed = moveDelta.getReversed();
-
-			expect( reversed ).to.be.instanceof( MoveDelta );
-			expect( reversed.operations.length ).to.equal( 1 );
-
-			expect( reversed.operations[ 0 ] ).to.be.instanceof( MoveOperation );
-			expect( reversed.operations[ 0 ].sourcePosition.path ).to.deep.equal( [ 2, 2 ] );
-			expect( reversed.operations[ 0 ].howMany ).to.equal( 2 );
-			expect( reversed.operations[ 0 ].targetPosition.path ).to.deep.equal( [ 1, 1 ] );
-		} );
-	} );
-
-	it( 'should provide proper className', () => {
-		expect( MoveDelta.className ).to.equal( 'engine.model.delta.MoveDelta' );
-	} );
-} );

+ 0 - 12
packages/ckeditor5-engine/tests/model/delta/removedelta.js

@@ -1,12 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import RemoveDelta from '../../../src/model/delta/removedelta';
-
-describe( 'RemoveDelta', () => {
-	it( 'should provide proper className', () => {
-		expect( RemoveDelta.className ).to.equal( 'engine.model.delta.RemoveDelta' );
-	} );
-} );

+ 0 - 62
packages/ckeditor5-engine/tests/model/delta/renamedelta.js

@@ -1,62 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import Model from '../../../src/model/model';
-import Element from '../../../src/model/element';
-import Text from '../../../src/model/text';
-
-import RenameDelta from '../../../src/model/delta/renamedelta';
-
-describe( 'RenameDelta', () => {
-	let renameDelta, model, doc, root;
-
-	beforeEach( () => {
-		model = new Model();
-		doc = model.document;
-		root = doc.createRoot();
-		renameDelta = new RenameDelta();
-	} );
-
-	describe( 'constructor()', () => {
-		it( 'should create rename delta with no operations added', () => {
-			expect( renameDelta.operations.length ).to.equal( 0 );
-		} );
-	} );
-
-	describe( 'type', () => {
-		it( 'should be equal to rename', () => {
-			expect( renameDelta.type ).to.equal( 'rename' );
-		} );
-	} );
-
-	describe( 'getReversed', () => {
-		it( 'should return instance of RenameDelta', () => {
-			const reversed = renameDelta.getReversed();
-
-			expect( reversed ).to.be.instanceof( RenameDelta );
-		} );
-
-		it( 'should return correct RenameDelta', () => {
-			root._appendChild( new Element( 'p', null, new Text( 'abc' ) ) );
-
-			model.change( writer => {
-				writer.rename( root.getChild( 0 ), 'h' );
-
-				const reversed = writer.batch.deltas[ 0 ].getReversed();
-
-				reversed.operations.forEach( operation => {
-					model.applyOperation( operation );
-				} );
-
-				expect( root.maxOffset ).to.equal( 1 );
-				expect( root.getChild( 0 ) ).to.have.property( 'name', 'p' );
-			} );
-		} );
-	} );
-
-	it( 'should provide proper className', () => {
-		expect( RenameDelta.className ).to.equal( 'engine.model.delta.RenameDelta' );
-	} );
-} );

+ 0 - 12
packages/ckeditor5-engine/tests/model/delta/rootattributedelta.js

@@ -1,12 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import RootAttributeDelta from '../../../src/model/delta/rootattributedelta';
-
-describe( 'RootAttributeDelta', () => {
-	it( 'should provide proper className', () => {
-		expect( RootAttributeDelta.className ).to.equal( 'engine.model.delta.RootAttributeDelta' );
-	} );
-} );

+ 0 - 125
packages/ckeditor5-engine/tests/model/delta/splitdelta.js

@@ -1,125 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import Model from '../../../src/model/model';
-import Position from '../../../src/model/position';
-import Element from '../../../src/model/element';
-
-import MergeDelta from '../../../src/model/delta/mergedelta';
-import SplitDelta from '../../../src/model/delta/splitdelta';
-
-import InsertOperation from '../../../src/model/operation/insertoperation';
-import MoveOperation from '../../../src/model/operation/moveoperation';
-import NoOperation from '../../../src/model/operation/nooperation';
-import RemoveOperation from '../../../src/model/operation/removeoperation';
-
-describe( 'SplitDelta', () => {
-	let splitDelta, doc, root;
-
-	beforeEach( () => {
-		const model = new Model();
-
-		doc = model.document;
-		root = doc.createRoot();
-		splitDelta = new SplitDelta();
-	} );
-
-	describe( 'constructor()', () => {
-		it( 'should create split delta with no operations added', () => {
-			expect( splitDelta.operations.length ).to.equal( 0 );
-		} );
-	} );
-
-	describe( 'type', () => {
-		it( 'should be equal to split', () => {
-			expect( splitDelta.type ).to.equal( 'split' );
-		} );
-	} );
-
-	describe( 'position', () => {
-		it( 'should be null if there are no operations in delta', () => {
-			expect( splitDelta.position ).to.be.null;
-		} );
-
-		it( 'should be equal to the position where node is split', () => {
-			splitDelta.operations.push( new InsertOperation( new Position( root, [ 1, 2 ] ), new Element( 'p' ), 0 ) );
-			splitDelta.operations.push( new MoveOperation( new Position( root, [ 1, 1, 4 ] ), 4, new Position( root, [ 1, 2, 0 ] ), 1 ) );
-
-			expect( splitDelta.position.root ).to.equal( root );
-			expect( splitDelta.position.path ).to.deep.equal( [ 1, 1, 4 ] );
-		} );
-	} );
-
-	describe( 'getReversed', () => {
-		it( 'should return empty MergeDelta if there are no operations in delta', () => {
-			const reversed = splitDelta.getReversed();
-
-			expect( reversed ).to.be.instanceof( MergeDelta );
-			expect( reversed.operations.length ).to.equal( 0 );
-		} );
-
-		it( 'should return correct SplitDelta', () => {
-			splitDelta.operations.push( new InsertOperation( new Position( root, [ 1, 2 ] ), new Element( 'p' ), 0 ) );
-			splitDelta.operations.push( new MoveOperation( new Position( root, [ 1, 1, 4 ] ), 4, new Position( root, [ 1, 2, 0 ] ), 1 ) );
-
-			const reversed = splitDelta.getReversed();
-
-			expect( reversed ).to.be.instanceof( MergeDelta );
-			expect( reversed.operations.length ).to.equal( 2 );
-
-			expect( reversed.operations[ 0 ] ).to.be.instanceof( MoveOperation );
-			expect( reversed.operations[ 0 ].sourcePosition.path ).to.deep.equal( [ 1, 2, 0 ] );
-			expect( reversed.operations[ 0 ].howMany ).to.equal( 4 );
-			expect( reversed.operations[ 0 ].targetPosition.path ).to.deep.equal( [ 1, 1, 4 ] );
-
-			expect( reversed.operations[ 1 ] ).to.be.instanceof( RemoveOperation );
-			expect( reversed.operations[ 1 ].sourcePosition.path ).to.deep.equal( [ 1, 2 ] );
-			expect( reversed.operations[ 1 ].howMany ).to.equal( 1 );
-		} );
-	} );
-
-	describe( '_cloneOperation', () => {
-		it( 'should return null if delta has no operations', () => {
-			expect( splitDelta._cloneOperation ).to.be.null;
-		} );
-
-		it( 'should return the first operation in the delta, which is InsertOperation or ReinsertOperation', () => {
-			const p = new Element( 'p' );
-			const insert = new InsertOperation( new Position( root, [ 1, 2 ] ), p, 0 );
-			splitDelta.operations.push( insert );
-			splitDelta.operations.push( new MoveOperation( new Position( root, [ 1, 1, 4 ] ), 4, new Position( root, [ 1, 2, 0 ] ), 1 ) );
-
-			expect( splitDelta._cloneOperation ).to.equal( insert );
-		} );
-	} );
-
-	describe( '_moveOperation', () => {
-		it( 'should return null if delta has no operations', () => {
-			expect( splitDelta._moveOperation ).to.be.null;
-		} );
-
-		it( 'should return null if second operation is NoOperation', () => {
-			const p = new Element( 'p' );
-			splitDelta.operations.push( new InsertOperation( new Position( root, [ 1, 2 ] ), p, 0 ) );
-			splitDelta.operations.push( new NoOperation( 1 ) );
-
-			expect( splitDelta._moveOperation ).to.be.null;
-		} );
-
-		it( 'should return second operation if it is MoveOperation', () => {
-			const p = new Element( 'p' );
-			const move = new MoveOperation( new Position( root, [ 1, 1, 4 ] ), 4, new Position( root, [ 1, 2, 0 ] ), 1 );
-			splitDelta.operations.push( new InsertOperation( new Position( root, [ 1, 2 ] ), p, 0 ) );
-			splitDelta.operations.push( move );
-
-			expect( splitDelta._moveOperation ).to.equal( move );
-		} );
-	} );
-
-	it( 'should provide proper className', () => {
-		expect( SplitDelta.className ).to.equal( 'engine.model.delta.SplitDelta' );
-	} );
-} );
-

+ 0 - 254
packages/ckeditor5-engine/tests/model/delta/transform/_utils/utils.js

@@ -1,254 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import Model from '../../../../../src/model/model';
-import Element from '../../../../../src/model/element';
-import Text from '../../../../../src/model/text';
-
-import Position from '../../../../../src/model/position';
-import Range from '../../../../../src/model/range';
-
-import Batch from '../../../../../src/model/batch';
-
-import AttributeDelta from '../../../../../src/model/delta/attributedelta';
-import InsertDelta from '../../../../../src/model/delta/insertdelta';
-import WeakInsertDelta from '../../../../../src/model/delta/weakinsertdelta';
-import RenameDelta from '../../../../../src/model/delta/renamedelta';
-import RemoveDelta from '../../../../../src/model/delta/removedelta';
-import MarkerDelta from '../../../../../src/model/delta/markerdelta';
-import MoveDelta from '../../../../../src/model/delta/movedelta';
-import MergeDelta from '../../../../../src/model/delta/mergedelta';
-import SplitDelta from '../../../../../src/model/delta/splitdelta';
-import WrapDelta from '../../../../../src/model/delta/wrapdelta';
-import UnwrapDelta from '../../../../../src/model/delta/unwrapdelta';
-
-import AttributeOperation from '../../../../../src/model/operation/attributeoperation';
-import InsertOperation from '../../../../../src/model/operation/insertoperation';
-import MarkerOperation from '../../../../../src/model/operation/markeroperation';
-import MoveOperation from '../../../../../src/model/operation/moveoperation';
-import RemoveOperation from '../../../../../src/model/operation/removeoperation';
-import RenameOperation from '../../../../../src/model/operation/renameoperation';
-
-export function getAttributeDelta( range, key, oldValue, newValue, version ) {
-	const delta = new AttributeDelta();
-	delta.addOperation( new AttributeOperation( range, key, oldValue, newValue, version ) );
-
-	return delta;
-}
-
-export function getInsertDelta( position, nodes, version ) {
-	const delta = new InsertDelta();
-	delta.addOperation( new InsertOperation( position, nodes, version ) );
-
-	wrapInBatch( delta );
-
-	return delta;
-}
-
-export function getWeakInsertDelta( position, nodes, version ) {
-	const delta = new WeakInsertDelta();
-	delta.addOperation( new InsertOperation( position, nodes, version ) );
-
-	wrapInBatch( delta );
-
-	return delta;
-}
-
-export function getMarkerDelta( name, oldRange, newRange, version ) {
-	const delta = new MarkerDelta();
-	delta.addOperation( new MarkerOperation( name, oldRange, newRange, version ) );
-
-	wrapInBatch( delta );
-
-	return delta;
-}
-
-export function getMergeDelta( position, howManyInPrev, howManyInNext, version ) {
-	const delta = new MergeDelta();
-
-	const sourcePosition = Position.createFromPosition( position );
-	sourcePosition.path.push( 0 );
-
-	const targetPosition = Position.createFromPosition( position );
-	targetPosition.offset--;
-	targetPosition.path.push( howManyInPrev );
-
-	const move = new MoveOperation( sourcePosition, howManyInNext, targetPosition, version );
-	move.isSticky = true;
-
-	delta.addOperation( move );
-
-	const gy = sourcePosition.root.document.graveyard;
-	const gyPos = Position.createAt( gy, 0 );
-
-	delta.addOperation( new RemoveOperation( position, 1, gyPos, version + 1 ) );
-
-	wrapInBatch( delta );
-
-	return delta;
-}
-
-export function getMoveDelta( sourcePosition, howMany, targetPosition, baseVersion ) {
-	const delta = new MoveDelta();
-
-	const move = new MoveOperation( sourcePosition, howMany, targetPosition, baseVersion );
-	delta.addOperation( move );
-
-	wrapInBatch( delta );
-
-	return delta;
-}
-
-export function getRemoveDelta( sourcePosition, howMany, baseVersion ) {
-	const delta = new RemoveDelta();
-
-	const gy = sourcePosition.root.document.graveyard;
-	const gyPos = Position.createAt( gy, 0 );
-
-	const remove = new RemoveOperation( sourcePosition, howMany, gyPos, baseVersion );
-	delta.addOperation( remove );
-
-	wrapInBatch( delta );
-
-	return delta;
-}
-
-export function getRenameDelta( position, oldName, newName, baseVersion ) {
-	const delta = new RenameDelta();
-
-	const rename = new RenameOperation( position, oldName, newName, baseVersion );
-	delta.addOperation( rename );
-
-	wrapInBatch( delta );
-
-	return delta;
-}
-
-export function getSplitDelta( position, nodeCopy, howManyMove, version ) {
-	const delta = new SplitDelta();
-
-	const insertPosition = Position.createFromPosition( position );
-	insertPosition.path = insertPosition.getParentPath();
-	insertPosition.offset++;
-
-	const targetPosition = Position.createFromPosition( insertPosition );
-	targetPosition.path.push( 0 );
-
-	delta.addOperation( new InsertOperation( insertPosition, [ nodeCopy ], version ) );
-
-	const move = new MoveOperation( position, howManyMove, targetPosition, version + 1 );
-	move.isSticky = true;
-
-	delta.addOperation( move );
-
-	wrapInBatch( delta );
-
-	return delta;
-}
-
-export function getWrapDelta( range, element, version ) {
-	const delta = new WrapDelta();
-
-	const insert = new InsertOperation( range.end, element, version );
-
-	const targetPosition = Position.createFromPosition( range.end );
-	targetPosition.path.push( 0 );
-	const move = new MoveOperation( range.start, range.end.offset - range.start.offset, targetPosition, version + 1 );
-
-	delta.addOperation( insert );
-	delta.addOperation( move );
-
-	wrapInBatch( delta );
-
-	return delta;
-}
-
-export function getUnwrapDelta( positionBefore, howManyChildren, version ) {
-	const delta = new UnwrapDelta();
-
-	const sourcePosition = Position.createFromPosition( positionBefore );
-	sourcePosition.path.push( 0 );
-
-	const move = new MoveOperation( sourcePosition, howManyChildren, positionBefore, version );
-	move.isSticky = true;
-
-	const removePosition = Position.createFromPosition( positionBefore );
-	removePosition.offset += howManyChildren;
-
-	const gy = sourcePosition.root.document.graveyard;
-	const gyPos = Position.createAt( gy, 0 );
-
-	const remove = new RemoveOperation( removePosition, 1, gyPos, version + 1 );
-
-	delta.addOperation( move );
-	delta.addOperation( remove );
-
-	wrapInBatch( delta );
-
-	return delta;
-}
-
-export function expectDelta( delta, expected ) {
-	expect( delta ).to.be.instanceof( expected.type );
-	expect( delta.operations.length ).to.equal( expected.operations.length );
-
-	for ( let i = 0; i < delta.operations.length; i++ ) {
-		expectOperation( delta.operations[ i ], expected.operations[ i ] );
-	}
-}
-
-export function expectOperation( op, params ) {
-	for ( const i in params ) {
-		if ( i == 'type' ) {
-			expect( op, 'operation type' ).to.be.instanceof( params[ i ] );
-		}
-		else if ( i == 'nodes' ) {
-			expect( Array.from( op.nodes ), 'nodes' ).to.deep.equal( params[ i ] );
-		} else if ( params[ i ] instanceof Position || params[ i ] instanceof Range ) {
-			expect( op[ i ].isEqual( params[ i ] ), 'property ' + i ).to.be.true;
-		} else {
-			expect( op[ i ], 'property ' + 1 ).to.equal( params[ i ] );
-		}
-	}
-}
-
-export function applyDelta( delta, document ) {
-	for ( const op of delta.operations ) {
-		document.model.applyOperation( op );
-	}
-}
-
-export function getFilledDocument() {
-	const model = new Model();
-	const doc = model.document;
-	const root = doc.createRoot();
-
-	root._insertChild( 0, [
-		new Element( 'x' ),
-		new Element( 'x' ),
-		new Element( 'x', [], new Text( 'a' ) ),
-		new Element( 'div', [], [
-			new Element( 'x' ),
-			new Element( 'x' ),
-			new Element( 'x', [], new Text( 'a' ) ),
-			new Element( 'div', [], [
-				new Element( 'x' ),
-				new Element( 'x' ),
-				new Element( 'x', [], new Text( 'abcd' ) ),
-				new Element( 'p', [], new Text( 'abcfoobarxyz' ) )
-			] )
-		] )
-	] );
-
-	return doc;
-}
-
-function wrapInBatch( delta ) {
-	// Batch() requires the document but only a few lines of code needs batch in `document#changes`
-	// so we may have an invalid batch instance for some tests.
-	const batch = new Batch();
-
-	batch.addDelta( delta );
-}

+ 0 - 406
packages/ckeditor5-engine/tests/model/delta/transform/attributedelta.js

@@ -1,406 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import transformations from '../../../../src/model/delta/basic-transformations'; // eslint-disable-line no-unused-vars
-
-import deltaTransform from '../../../../src/model/delta/transform';
-const transform = deltaTransform.transform;
-
-import Element from '../../../../src/model/element';
-import Text from '../../../../src/model/text';
-import Position from '../../../../src/model/position';
-import Range from '../../../../src/model/range';
-
-import AttributeDelta from '../../../../src/model/delta/attributedelta';
-import Delta from '../../../../src/model/delta/delta';
-import SplitDelta from '../../../../src/model/delta/splitdelta';
-import AttributeOperation from '../../../../src/model/operation/attributeoperation';
-import MoveOperation from '../../../../src/model/operation/moveoperation';
-import ReinsertOperation from '../../../../src/model/operation/reinsertoperation';
-import NoOperation from '../../../../src/model/operation/nooperation';
-
-import {
-	expectDelta,
-	getFilledDocument,
-	getAttributeDelta,
-	getWeakInsertDelta,
-	getSplitDelta
-} from '../../../../tests/model/delta/transform/_utils/utils';
-
-describe( 'transform', () => {
-	let doc, root, baseVersion, context;
-
-	beforeEach( () => {
-		doc = getFilledDocument();
-		root = doc.getRoot();
-		baseVersion = doc.version;
-		context = { isStrong: false };
-	} );
-
-	describe( 'AttributeDelta by', () => {
-		describe( 'WeakInsertDelta', () => {
-			it( 'weak insert inside attribute range should "fix" splitting the range', () => {
-				const attrRange = new Range( new Position( root, [ 3, 2 ] ), new Position( root, [ 3, 3, 3, 9 ] ) );
-				const attrDelta = getAttributeDelta( attrRange, 'key', 'old', 'new', baseVersion );
-
-				const insertPosition = new Position( root, [ 3, 3, 0 ] );
-				const insertDelta = getWeakInsertDelta(
-					insertPosition,
-					[
-						'a',
-						new Text( 'b', { key: 'new' } ),
-						new Text( 'c', { key: 'different' } ),
-						'de'
-					],
-					baseVersion
-				);
-
-				const transformed = transform( attrDelta, insertDelta, context );
-
-				expect( transformed.length ).to.equal( 2 );
-
-				baseVersion = insertDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: AttributeDelta,
-					operations: [
-						{
-							type: AttributeOperation,
-							range: new Range( new Position( root, [ 3, 3, 5 ] ), new Position( root, [ 3, 3, 8, 9 ] ) ),
-							key: 'key',
-							oldValue: 'old',
-							newValue: 'new',
-							baseVersion
-						},
-						{
-							type: AttributeOperation,
-							range: new Range( new Position( root, [ 3, 2 ] ), new Position( root, [ 3, 3, 0 ] ) ),
-							key: 'key',
-							oldValue: 'old',
-							newValue: 'new',
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-
-				expectDelta( transformed[ 1 ], {
-					type: AttributeDelta,
-					operations: [
-						{
-							type: AttributeOperation,
-							range: new Range( new Position( root, [ 3, 3, 0 ] ), new Position( root, [ 3, 3, 1 ] ) ),
-							key: 'key',
-							oldValue: null,
-							newValue: 'new',
-							baseVersion: baseVersion + 2
-						},
-						{
-							type: AttributeOperation,
-							range: new Range( new Position( root, [ 3, 3, 2 ] ), new Position( root, [ 3, 3, 3 ] ) ),
-							key: 'key',
-							oldValue: 'different',
-							newValue: 'new',
-							baseVersion: baseVersion + 3
-						},
-						{
-							type: AttributeOperation,
-							range: new Range( new Position( root, [ 3, 3, 3 ] ), new Position( root, [ 3, 3, 5 ] ) ),
-							key: 'key',
-							oldValue: null,
-							newValue: 'new',
-							baseVersion: baseVersion + 4
-						}
-					]
-				} );
-			} );
-
-			it( 'should be normally transformed if weak insert is not in the attribute range', () => {
-				const attrRange = new Range( new Position( root, [ 3, 2 ] ), new Position( root, [ 3, 3, 3, 9 ] ) );
-				const attrDelta = getAttributeDelta( attrRange, 'key', 'old', 'new', baseVersion );
-
-				const insertPosition = new Position( root, [ 5 ] );
-				const insertDelta = getWeakInsertDelta( insertPosition, 'abc', baseVersion );
-
-				const transformed = transform( attrDelta, insertDelta, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				baseVersion = insertDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: AttributeDelta,
-					operations: [
-						{
-							type: AttributeOperation,
-							range: new Range( new Position( root, [ 3, 2 ] ), new Position( root, [ 3, 3, 3, 9 ] ) ),
-							key: 'key',
-							oldValue: 'old',
-							newValue: 'new',
-							baseVersion
-						}
-					]
-				} );
-			} );
-		} );
-
-		describe( 'SplitDelta', () => {
-			it( 'change attribute of split element', () => {
-				const attrRange1 = Range.createFromParentsAndOffsets( root, 0, root, 1 );
-				const attrRange2 = new Range( new Position( root, [ 3, 2 ] ), new Position( root, [ 3, 4 ] ) );
-
-				const attrDelta = new AttributeDelta();
-				attrDelta.addOperation( new AttributeOperation( attrRange1, 'key', 'old', 'new', baseVersion ) );
-				attrDelta.addOperation( new AttributeOperation( attrRange2, 'key', 'old', 'new', baseVersion ) );
-
-				const splitPosition = new Position( root, [ 3, 3, 3, 3 ] );
-				const splitDelta = getSplitDelta( splitPosition, new Element( 'p' ), 9, baseVersion );
-
-				const transformed = transform( attrDelta, splitDelta, context );
-
-				expect( transformed.length ).to.equal( 2 );
-
-				baseVersion = splitDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: AttributeDelta,
-					operations: [
-						{
-							type: AttributeOperation,
-							range: new Range( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) ),
-							key: 'key',
-							oldValue: 'old',
-							newValue: 'new',
-							baseVersion
-						},
-						{
-							type: AttributeOperation,
-							range: new Range( new Position( root, [ 3, 3, 5 ] ), new Position( root, [ 3, 4 ] ) ),
-							key: 'key',
-							oldValue: 'old',
-							newValue: 'new',
-							baseVersion: baseVersion + 1
-						},
-						{
-							type: AttributeOperation,
-							range: new Range( new Position( root, [ 3, 2 ] ), new Position( root, [ 3, 3, 4 ] ) ),
-							key: 'key',
-							oldValue: 'old',
-							newValue: 'new',
-							baseVersion: baseVersion + 2
-						},
-						{
-							type: AttributeOperation,
-							range: new Range( new Position( root, [ 3, 3, 4, 0 ] ), new Position( root, [ 3, 3, 4, 9 ] ) ),
-							key: 'key',
-							oldValue: 'old',
-							newValue: 'new',
-							baseVersion: baseVersion + 3
-						}
-					]
-				} );
-
-				expectDelta( transformed[ 1 ], {
-					type: AttributeDelta,
-					operations: [
-						{
-							type: AttributeOperation,
-							range: new Range( new Position( root, [ 3, 3, 4 ] ), new Position( root, [ 3, 3, 4, 0 ] ) ),
-							key: 'key',
-							oldValue: null,
-							newValue: 'new',
-							baseVersion: baseVersion + 4
-						}
-					]
-				} );
-			} );
-
-			// A different case.
-			it( 'change attribute of split element #2', () => {
-				const attrRange = new Range( new Position( root, [ 3, 3, 3 ] ), new Position( root, [ 3, 3, 3, 0 ] ) );
-				const attrDelta = new AttributeDelta();
-				attrDelta.addOperation( new AttributeOperation( attrRange, 'foo', null, 'bar', baseVersion ) );
-
-				const splitPosition = new Position( root, [ 3, 3, 3, 3 ] );
-				const splitDelta = getSplitDelta( splitPosition, new Element( 'p', { foo: 'old' } ), 9, baseVersion );
-
-				const transformed = transform( attrDelta, splitDelta, context );
-
-				expect( transformed.length ).to.equal( 2 );
-
-				baseVersion = splitDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: AttributeDelta,
-					operations: [
-						{
-							type: AttributeOperation,
-							range: new Range( new Position( root, [ 3, 3, 3 ] ), new Position( root, [ 3, 3, 3, 0 ] ) ),
-							key: 'foo',
-							oldValue: null,
-							newValue: 'bar',
-							baseVersion
-						}
-					]
-				} );
-
-				expectDelta( transformed[ 1 ], {
-					type: AttributeDelta,
-					operations: [
-						{
-							type: AttributeOperation,
-							range: new Range( new Position( root, [ 3, 3, 4 ] ), new Position( root, [ 3, 3, 4, 0 ] ) ),
-							key: 'foo',
-							oldValue: 'old',
-							newValue: 'bar',
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-			} );
-
-			it( 'change attribute of split element that reinserts from graveyard', () => {
-				const gy = doc.graveyard;
-				const splitDelta = new SplitDelta();
-
-				splitDelta.operations[ 0 ] = new ReinsertOperation(
-					new Position( gy, [ 1 ] ),
-					1,
-					new Position( root, [ 2 ] ),
-					baseVersion
-				);
-
-				splitDelta.operations[ 1 ] = new MoveOperation(
-					new Position( root, [ 1, 4 ] ),
-					4,
-					new Position( root, [ 2, 0 ] ),
-					baseVersion
-				);
-
-				const attributeDelta = new AttributeDelta();
-
-				const range = new Range( new Position( root, [ 1 ] ), new Position( root, [ 1, 0 ] ) );
-
-				attributeDelta.addOperation( new AttributeOperation( range, 'key', 'oldValue', 'newValue', baseVersion ) );
-
-				// The split delta was undone.
-				context.undoMode = true;
-
-				const transformed = transform( attributeDelta, splitDelta, context );
-
-				baseVersion = attributeDelta.operations.length;
-				// We expect only one delta. Split delta should not affect attribute delta transformation. It was undone.
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: AttributeDelta,
-					operations: [
-						{
-							type: AttributeOperation,
-							range,
-							key: 'key',
-							oldValue: 'oldValue',
-							newValue: 'newValue',
-							baseVersion
-						}
-					]
-				} );
-			} );
-
-			it( 'should use default algorithm and not throw if split delta has NoOperation', () => {
-				const range = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2, 3 ] ) );
-				const attrDelta = getAttributeDelta( range, 'foo', null, 'bar', 0 );
-				const splitDelta = getSplitDelta( new Position( root, [ 0, 2 ] ), new Element( 'paragraph' ), 3, 0 );
-				splitDelta.operations[ 1 ] = new NoOperation( 1 );
-
-				const transformed = transform( attrDelta, splitDelta, context );
-
-				baseVersion = splitDelta.operations.length;
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: AttributeDelta,
-					operations: [
-						{
-							type: AttributeOperation,
-							range: new Range( new Position( root, [ 2 ] ), new Position( root, [ 3, 3 ] ) ),
-							key: 'foo',
-							oldValue: null,
-							newValue: 'bar',
-							baseVersion
-						}
-					]
-				} );
-			} );
-		} );
-
-		describe( 'AttributeDelta', () => {
-			it( 'should be converted to no delta if all operations are NoOperations', () => {
-				const rangeA = new Range( new Position( root, [ 2 ] ), new Position( root, [ 4 ] ) );
-				const rangeB = new Range( new Position( root, [ 4 ] ), new Position( root, [ 7 ] ) );
-
-				const attrDeltaA = new AttributeDelta();
-				attrDeltaA.addOperation( new AttributeOperation( rangeA, 'key', 'old', 'new', 0 ) );
-				attrDeltaA.addOperation( new AttributeOperation( rangeB, 'key', null, 'new', 1 ) );
-
-				const attrDeltaB = new AttributeDelta();
-				attrDeltaB.addOperation( new AttributeOperation( rangeA, 'key', 'old', 'other', 0 ) );
-				attrDeltaB.addOperation( new AttributeOperation( rangeB, 'key', null, 'other', 1 ) );
-
-				const transformed = transform( attrDeltaA, attrDeltaB, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: Delta,
-					operations: [
-						{
-							type: NoOperation,
-							baseVersion: 2
-						},
-						{
-							type: NoOperation,
-							baseVersion: 3
-						}
-					]
-				} );
-			} );
-
-			it( 'should put AttributeOperation as first operation in the delta', () => {
-				const rangeA = new Range( new Position( root, [ 2 ] ), new Position( root, [ 4 ] ) );
-				const rangeB = new Range( new Position( root, [ 4 ] ), new Position( root, [ 7 ] ) );
-
-				const attrDeltaA = new AttributeDelta();
-				attrDeltaA.addOperation( new AttributeOperation( rangeA, 'key', 'old', 'new', 0 ) );
-				attrDeltaA.addOperation( new AttributeOperation( rangeB, 'key', null, 'new', 1 ) );
-
-				const attrDeltaB = new AttributeDelta();
-				attrDeltaB.addOperation( new AttributeOperation( rangeA, 'key', 'old', 'other', 0 ) );
-
-				const transformed = transform( attrDeltaA, attrDeltaB, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: AttributeDelta,
-					operations: [
-						{
-							type: AttributeOperation,
-							range: rangeB,
-							key: 'key',
-							oldValue: null,
-							newValue: 'new',
-							baseVersion: 1
-						},
-						{
-							type: NoOperation,
-							baseVersion: 2
-						}
-					]
-				} );
-			} );
-		} );
-	} );
-} );

+ 0 - 85
packages/ckeditor5-engine/tests/model/delta/transform/delta.js

@@ -1,85 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import transformations from '../../../../src/model/delta/basic-transformations'; // eslint-disable-line no-unused-vars
-
-import deltaTransform from '../../../../src/model/delta/transform';
-const transform = deltaTransform.transform;
-
-import Position from '../../../../src/model/position';
-import MoveOperation from '../../../../src/model/operation/moveoperation';
-import Delta from '../../../../src/model/delta/delta';
-
-import {
-	expectDelta,
-	getFilledDocument,
-} from '../../../../tests/model/delta/transform/_utils/utils';
-
-describe( 'Delta', () => {
-	let doc, root, baseVersion;
-
-	beforeEach( () => {
-		doc = getFilledDocument();
-		root = doc.getRoot();
-		baseVersion = doc.version;
-	} );
-
-	it( 'should have baseVersion property, equal to the baseVersion of first operation in Delta or null', () => {
-		const deltaA = new Delta();
-
-		expect( deltaA.baseVersion ).to.be.null;
-
-		const version = 5;
-
-		deltaA.addOperation( new MoveOperation( new Position( root, [ 1, 2, 3 ] ), 4, new Position( root, [ 4, 0 ] ), version ) );
-
-		expect( deltaA.baseVersion ).to.equal( 5 );
-	} );
-
-	it( 'should be transformable by another Delta', () => {
-		const deltaA = new Delta();
-		const deltaB = new Delta();
-
-		const context = {
-			isStrong: false
-		};
-
-		deltaA.addOperation( new MoveOperation( new Position( root, [ 1, 2, 3 ] ), 4, new Position( root, [ 4, 0 ] ), baseVersion ) );
-		deltaB.addOperation( new MoveOperation( new Position( root, [ 1, 2, 0 ] ), 2, new Position( root, [ 4, 1 ] ), baseVersion ) );
-
-		const deltaAbyB = transform( deltaA, deltaB, context );
-		const deltaBbyA = transform( deltaB, deltaA, context );
-
-		expect( deltaAbyB.length ).to.equal( 1 );
-
-		expectDelta( deltaAbyB[ 0 ], {
-			type: Delta,
-			operations: [
-				{
-					type: MoveOperation,
-					sourcePosition: new Position( root, [ 1, 2, 1 ] ),
-					howMany: 4,
-					targetPosition: new Position( root, [ 4, 0 ] ),
-					baseVersion: 1
-				}
-			]
-		} );
-
-		expect( deltaBbyA.length ).to.equal( 1 );
-
-		expectDelta( deltaBbyA[ 0 ], {
-			type: Delta,
-			operations: [
-				{
-					type: MoveOperation,
-					sourcePosition: new Position( root, [ 1, 2, 0 ] ),
-					howMany: 2,
-					targetPosition: new Position( root, [ 4, 5 ] ),
-					baseVersion: 1
-				}
-			]
-		} );
-	} );
-} );

+ 0 - 251
packages/ckeditor5-engine/tests/model/delta/transform/insertdelta.js

@@ -1,251 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import transformations from '../../../../src/model/delta/basic-transformations'; // eslint-disable-line no-unused-vars
-
-import deltaTransform from '../../../../src/model/delta/transform';
-const transform = deltaTransform.transform;
-
-import Element from '../../../../src/model/element';
-import Position from '../../../../src/model/position';
-import Range from '../../../../src/model/range';
-
-import InsertDelta from '../../../../src/model/delta/insertdelta';
-import SplitDelta from '../../../../src/model/delta/splitdelta';
-
-import InsertOperation from '../../../../src/model/operation/insertoperation';
-import MoveOperation from '../../../../src/model/operation/moveoperation';
-import ReinsertOperation from '../../../../src/model/operation/reinsertoperation';
-import NoOperation from '../../../../src/model/operation/nooperation';
-
-import { getNodesAndText } from '../../../../tests/model/_utils/utils';
-
-import {
-	applyDelta,
-	expectDelta,
-	getFilledDocument,
-	getInsertDelta,
-	getMergeDelta
-} from '../../../../tests/model/delta/transform/_utils/utils';
-
-describe( 'transform', () => {
-	let doc, root, gy, baseVersion, context;
-
-	beforeEach( () => {
-		doc = getFilledDocument();
-		root = doc.getRoot();
-		gy = doc.graveyard;
-		baseVersion = doc.version;
-		context = {
-			isStrong: false
-		};
-	} );
-
-	describe( 'InsertDelta by', () => {
-		let insertDelta, insertPosition, nodeA, nodeB;
-
-		beforeEach( () => {
-			nodeA = new Element( 'a' );
-			nodeB = new Element( 'b' );
-
-			insertPosition = new Position( root, [ 3, 3, 3 ] );
-			insertDelta = getInsertDelta( insertPosition, [ nodeA, nodeB ], baseVersion );
-		} );
-
-		describe( 'InsertDelta', () => {
-			it( 'should be resolved in a same way as two insert operations', () => {
-				const insertPositionB = new Position( root, [ 3, 1 ] );
-				const insertDeltaB = getInsertDelta( insertPositionB, [ new Element( 'c' ), new Element( 'd' ) ], baseVersion );
-
-				const transformed = transform( insertDelta, insertDeltaB, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: InsertDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: new Position( root, [ 3, 5, 3 ] ),
-							nodes: [ nodeA, nodeB ],
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-			} );
-		} );
-
-		describe( 'MergeDelta', () => {
-			it( 'merge in same position as insert', () => {
-				const mergeDelta = getMergeDelta( insertPosition, 4, 12, baseVersion );
-				const transformed = transform( insertDelta, mergeDelta, context );
-
-				baseVersion = mergeDelta.operations.length;
-
-				// Expected: MergeOperation gets reversed (by special case of SplitDelta that has ReinsertOperation
-				// instead of InsertOperation. Then InsertDelta is applied as is.
-
-				expect( transformed.length ).to.equal( 2 );
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							type: ReinsertOperation,
-							sourcePosition: new Position( gy, [ 0 ] ),
-							howMany: 1,
-							targetPosition: new Position( root, [ 3, 3, 3 ] ),
-							baseVersion
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 2, 4 ] ),
-							howMany: 12,
-							targetPosition: new Position( root, [ 3, 3, 3, 0 ] ),
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-
-				expectDelta( transformed[ 1 ], {
-					type: InsertDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: Position.createFromPosition( insertPosition ),
-							nodes: [ nodeA, nodeB ],
-							baseVersion: baseVersion + 2
-						}
-					]
-				} );
-
-				// Test if deltas do what they should after applying transformed delta.
-
-				applyDelta( mergeDelta, doc );
-				applyDelta( transformed[ 0 ], doc );
-				applyDelta( transformed[ 1 ], doc );
-
-				const nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 0 ] ), 6 ) );
-
-				// Merge between X with "abcd" and P with "abcfoobarxyz" should be reversed and AB should be inserted between X and P.
-				expect( nodesAndText ).to.equal( 'XXXXXabcdXAABBPabcfoobarxyzP' );
-			} );
-
-			it( 'merge in same position as insert - undo mode', () => {
-				// In undo mode, default transformation algorithm should be used.
-				const mergeDelta = getMergeDelta( insertPosition, 4, 12, baseVersion );
-
-				context.undoMode = true;
-				const transformed = transform( insertDelta, mergeDelta, context );
-
-				baseVersion = mergeDelta.operations.length;
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: InsertDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: Position.createFromPosition( insertPosition ),
-							nodes: [ nodeA, nodeB ],
-							baseVersion
-						}
-					]
-				} );
-			} );
-
-			it( 'merge in same position as insert - undo mode', () => {
-				// If MergeDelta second operation is NoOperation, default transformation algorithm should be used.
-				const mergeDelta = getMergeDelta( insertPosition, 4, 12, baseVersion );
-				mergeDelta.operations[ 1 ] = new NoOperation( 1 );
-
-				const transformed = transform( insertDelta, mergeDelta, context );
-
-				baseVersion = mergeDelta.operations.length;
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: InsertDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: Position.createFromPosition( insertPosition ),
-							nodes: [ nodeA, nodeB ],
-							baseVersion
-						}
-					]
-				} );
-			} );
-
-			it( 'merge the node that is parent of insert position (sticky move test)', () => {
-				const mergeDelta = getMergeDelta( new Position( root, [ 3, 3 ] ), 1, 4, baseVersion );
-				const transformed = transform( insertDelta, mergeDelta, context );
-
-				baseVersion = mergeDelta.operations.length;
-
-				// Expected: InsertOperation in InsertDelta has it's path updated.
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: InsertDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: new Position( root, [ 3, 2, 4 ] ),
-							nodes: [ nodeA, nodeB ],
-							baseVersion
-						}
-					]
-				} );
-
-				// Test if deltas do what they should after applying transformed delta.
-
-				applyDelta( mergeDelta, doc );
-				applyDelta( transformed[ 0 ], doc );
-
-				const nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 2, 0 ] ), 7 ) );
-
-				// Merge between X with "a" and DIV should be applied. AB should be inserted in new, correct position.
-				expect( nodesAndText ).to.equal( 'aXXXXXabcdXAABBPabcfoobarxyzP' );
-			} );
-
-			it( 'merge at affected position but resolved by default OT', () => {
-				const mergeDelta = getMergeDelta( new Position( root, [ 3 ] ), 1, 4, baseVersion );
-				const transformed = transform( insertDelta, mergeDelta, context );
-
-				baseVersion = mergeDelta.operations.length;
-
-				// Expected: InsertOperation in InsertDelta has it's path updated.
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: InsertDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: new Position( root, [ 2, 4, 3 ] ),
-							nodes: [ nodeA, nodeB ],
-							baseVersion
-						}
-					]
-				} );
-
-				// Test if deltas do what they should after applying transformed delta.
-
-				applyDelta( mergeDelta, doc );
-				applyDelta( transformed[ 0 ], doc );
-
-				const nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 2, 0 ] ), 5 ) );
-
-				// Merge between X with "a" and DIV should be applied. AB should be inserted in new, correct position.
-				expect( nodesAndText ).to.equal( 'aXXXXXaXDIVXXXXXabcdXAABBPabcfoobarxyzPDIV' );
-			} );
-		} );
-	} );
-} );

+ 0 - 255
packages/ckeditor5-engine/tests/model/delta/transform/markerdelta.js

@@ -1,255 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import transformations from '../../../../src/model/delta/basic-transformations'; // eslint-disable-line no-unused-vars
-import deltaTransform from '../../../../src/model/delta/transform';
-
-const transform = deltaTransform.transform;
-
-import Element from '../../../../src/model/element';
-import Position from '../../../../src/model/position';
-import Range from '../../../../src/model/range';
-
-import MarkerDelta from '../../../../src/model/delta/markerdelta';
-import MarkerOperation from '../../../../src/model/operation/markeroperation';
-
-import {
-	expectDelta,
-	getFilledDocument,
-	getMarkerDelta,
-	getSplitDelta,
-	getMergeDelta,
-	getWrapDelta,
-	getUnwrapDelta
-} from '../../../model/delta/transform/_utils/utils';
-
-describe( 'transform', () => {
-	let doc, root, baseVersion, context;
-
-	beforeEach( () => {
-		doc = getFilledDocument();
-		root = doc.getRoot();
-		baseVersion = doc.version;
-		context = { isStrong: false };
-	} );
-
-	describe( 'MarkerDelta by', () => {
-		describe( 'SplitDelta', () => {
-			let markerDelta;
-
-			beforeEach( () => {
-				const oldRange = new Range( new Position( root, [ 3, 0 ] ), new Position( root, [ 3, 3 ] ) );
-				const newRange = new Range( new Position( root, [ 3, 3, 3, 2 ] ), new Position( root, [ 3, 3, 3, 6 ] ) );
-
-				markerDelta = getMarkerDelta( 'name', oldRange, newRange, baseVersion );
-			} );
-
-			it( 'split inside oldRange', () => {
-				const splitDelta = getSplitDelta( new Position( root, [ 3, 1 ] ), new Element( 'div' ), 3, baseVersion );
-				const transformed = transform( markerDelta, splitDelta, context );
-
-				baseVersion = splitDelta.operations.length;
-
-				expect( transformed.length ).to.equal( 1 );
-
-				const expectedOldRange = new Range( new Position( root, [ 3, 0 ] ), new Position( root, [ 4, 2 ] ) );
-				const expectedNewRange = new Range( new Position( root, [ 4, 2, 3, 2 ] ), new Position( root, [ 4, 2, 3, 6 ] ) );
-
-				expectDelta( transformed[ 0 ], {
-					type: MarkerDelta,
-					operations: [
-						{
-							type: MarkerOperation,
-							name: 'name',
-							oldRange: expectedOldRange,
-							newRange: expectedNewRange,
-							baseVersion
-						}
-					]
-				} );
-			} );
-
-			it( 'split inside newRange', () => {
-				const splitDelta = getSplitDelta( new Position( root, [ 3, 3, 3, 4 ] ), new Element( 'p' ), 8, baseVersion );
-				const transformed = transform( markerDelta, splitDelta, context );
-
-				baseVersion = splitDelta.operations.length;
-
-				expect( transformed.length ).to.equal( 1 );
-
-				const expectedOldRange = new Range( new Position( root, [ 3, 0 ] ), new Position( root, [ 3, 3 ] ) );
-				const expectedNewRange = new Range( new Position( root, [ 3, 3, 3, 2 ] ), new Position( root, [ 3, 3, 4, 2 ] ) );
-
-				expectDelta( transformed[ 0 ], {
-					type: MarkerDelta,
-					operations: [
-						{
-							type: MarkerOperation,
-							name: 'name',
-							oldRange: expectedOldRange,
-							newRange: expectedNewRange,
-							baseVersion
-						}
-					]
-				} );
-			} );
-		} );
-
-		describe( 'MergeDelta', () => {
-			it( 'collapsed marker in merged element', () => {
-				// MarkerDelta with collapsed range, which changes from the beginning of merged element to the end.
-				const oldRange = new Range( new Position( root, [ 3, 3, 3, 0 ] ) );
-				const newRange = new Range( new Position( root, [ 3, 3, 3, 12 ] ) );
-
-				const markerDelta = getMarkerDelta( 'name', oldRange, newRange, baseVersion );
-
-				// MergeDelta merges the element in which is collapsed marker range with the previous element.
-				const mergeDelta = getMergeDelta( new Position( root, [ 3, 3, 3 ] ), 4, 12, baseVersion );
-
-				const transformed = transform( markerDelta, mergeDelta, context );
-
-				// It is expected, that ranges in MarkerDelta got correctly transformed:
-				// from start of merged element to the place where merged nodes where moved in the previous element,
-				// from end of merged element to the end of previous element.
-				const expectedOldRange = new Range( new Position( root, [ 3, 3, 2, 4 ] ), new Position( root, [ 3, 3, 2, 4 ] ) );
-				const expectedNewRange = new Range( new Position( root, [ 3, 3, 2, 16 ] ), new Position( root, [ 3, 3, 2, 16 ] ) );
-
-				expectDelta( transformed[ 0 ], {
-					type: MarkerDelta,
-					operations: [
-						{
-							type: MarkerOperation,
-							name: 'name',
-							oldRange: expectedOldRange,
-							newRange: expectedNewRange,
-							baseVersion: baseVersion + 2
-						}
-					]
-				} );
-			} );
-		} );
-
-		describe( 'WrapDelta', () => {
-			it( 'ranges intersecting with wrapped range', () => {
-				// MarkerDelta with ranges that intersects with wrapped range.
-				const oldRange = new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 1, 2 ] ) );
-				const newRange = new Range( new Position( root, [ 1, 2 ] ), new Position( root, [ 2, 2 ] ) );
-
-				const markerDelta = getMarkerDelta( 'name', oldRange, newRange, baseVersion );
-
-				// Wrap delta wraps element on position ( root [ 1 ] ), which intersects with both `oldRange` and `newRange`.
-				const wrapElement = new Element( 'w' );
-				const wrapRange = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) );
-				const wrapDelta = getWrapDelta( wrapRange, wrapElement, baseVersion );
-
-				const transformed = transform( markerDelta, wrapDelta, context );
-
-				// It is expected, that ranges in MarkerDelta got correctly transformed:
-				// `oldRange` end is in wrapped element,
-				// `newRange` start is in wrapped element.
-				const expectedOldRange = new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 1, 0, 2 ] ) );
-				const expectedNewRange = new Range( new Position( root, [ 1, 0, 2 ] ), new Position( root, [ 2, 2 ] ) );
-
-				expectDelta( transformed[ 0 ], {
-					type: MarkerDelta,
-					operations: [
-						{
-							type: MarkerOperation,
-							name: 'name',
-							oldRange: expectedOldRange,
-							newRange: expectedNewRange,
-							baseVersion: baseVersion + 2
-						}
-					]
-				} );
-			} );
-		} );
-
-		describe( 'UnwrapDelta', () => {
-			it( 'ranges intersecting with unwrapped element', () => {
-				// MarkerDelta with ranges that intersects with unwrapped element.
-				const oldRange = new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 1, 0, 2 ] ) );
-				const newRange = new Range( new Position( root, [ 1, 0, 2 ] ), new Position( root, [ 2, 2 ] ) );
-
-				const markerDelta = getMarkerDelta( 'name', oldRange, newRange, baseVersion );
-
-				// Unwrap delta unwraps element on position ( root [ 1, 0 ] ), which intersects with both `oldRange` and `newRange`.
-				const unwrapPosition = new Position( root, [ 1, 0 ] );
-				const unwrapDelta = getUnwrapDelta( unwrapPosition, 4, baseVersion );
-
-				const transformed = transform( markerDelta, unwrapDelta, context );
-
-				// It is expected, that ranges in MarkerDelta got correctly transformed.
-				const expectedOldRange = new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 1, 2 ] ) );
-				const expectedNewRange = new Range( new Position( root, [ 1, 2 ] ), new Position( root, [ 2, 2 ] ) );
-
-				expectDelta( transformed[ 0 ], {
-					type: MarkerDelta,
-					operations: [
-						{
-							type: MarkerOperation,
-							name: 'name',
-							oldRange: expectedOldRange,
-							newRange: expectedNewRange,
-							baseVersion: baseVersion + 2
-						}
-					]
-				} );
-			} );
-
-			it( 'ranges intersecting with unwrapped element #2', () => {
-				// MarkerDelta with ranges that intersects with unwrapped element.
-				const oldRange = new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 1, 2 ] ) );
-				const newRange = new Range( new Position( root, [ 1, 2 ] ), new Position( root, [ 2, 2 ] ) );
-
-				const markerDelta = getMarkerDelta( 'name', oldRange, newRange, baseVersion );
-
-				// Unwrap delta unwraps element on position ( root [ 1 ] ), which intersects with both `oldRange` and `newRange`.
-				const unwrapPosition = new Position( root, [ 1 ] );
-				const unwrapDelta = getUnwrapDelta( unwrapPosition, 4, baseVersion );
-
-				const transformed = transform( markerDelta, unwrapDelta, context );
-
-				// It is expected, that ranges in MarkerDelta got correctly transformed.
-				const expectedOldRange = new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 3 ] ) );
-				const expectedNewRange = new Range( new Position( root, [ 3 ] ), new Position( root, [ 5, 2 ] ) );
-
-				expectDelta( transformed[ 0 ], {
-					type: MarkerDelta,
-					operations: [
-						{
-							type: MarkerOperation,
-							name: 'name',
-							oldRange: expectedOldRange,
-							newRange: expectedNewRange,
-							baseVersion: baseVersion + 2
-						}
-					]
-				} );
-			} );
-		} );
-	} );
-
-	it( 'null ranges of MarkerDelta should not be changed during transformation', () => {
-		const markerDelta = getMarkerDelta( 'name', null, null, baseVersion );
-
-		// Transform `markerDelta` by any other delta that has a special transformation case with `MarkerDelta`.
-		const wrapElement = new Element( 'w' );
-		const wrapRange = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) );
-		const wrapDelta = getWrapDelta( wrapRange, wrapElement, baseVersion );
-
-		const transformed = transform( markerDelta, wrapDelta, context );
-
-		expect( transformed.length ).to.equal( 1 );
-		expect( transformed[ 0 ].operations.length ).to.equal( 1 );
-
-		const transformedOp = transformed[ 0 ].operations[ 0 ];
-
-		expect( transformedOp ).to.be.instanceof( MarkerOperation );
-		expect( transformedOp.oldRange ).to.be.null;
-		expect( transformedOp.newRange ).to.be.null;
-		expect( transformedOp.name ).to.equal( 'name' );
-	} );
-} );

+ 0 - 365
packages/ckeditor5-engine/tests/model/delta/transform/mergedelta.js

@@ -1,365 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import transformations from '../../../../src/model/delta/basic-transformations'; // eslint-disable-line no-unused-vars
-
-import deltaTransform from '../../../../src/model/delta/transform';
-const transform = deltaTransform.transform;
-
-import Element from '../../../../src/model/element';
-import Position from '../../../../src/model/position';
-import Range from '../../../../src/model/range';
-
-import Delta from '../../../../src/model/delta/delta';
-import MergeDelta from '../../../../src/model/delta/mergedelta';
-
-import MoveOperation from '../../../../src/model/operation/moveoperation';
-import RemoveOperation from '../../../../src/model/operation/removeoperation';
-import NoOperation from '../../../../src/model/operation/nooperation';
-
-import { getNodesAndText } from '../../../../tests/model/_utils/utils';
-
-import {
-	applyDelta,
-	expectDelta,
-	getFilledDocument,
-	getInsertDelta,
-	getMergeDelta,
-	getMoveDelta
-} from '../../../../tests/model/delta/transform/_utils/utils';
-
-describe( 'transform', () => {
-	let doc, root, gy, baseVersion, context;
-
-	beforeEach( () => {
-		doc = getFilledDocument();
-		root = doc.getRoot();
-		gy = doc.graveyard;
-		baseVersion = doc.version;
-		context = { isStrong: false };
-	} );
-
-	describe( 'MergeDelta by', () => {
-		let mergeDelta, mergePosition;
-
-		beforeEach( () => {
-			mergePosition = new Position( root, [ 3, 3, 3 ] );
-			mergeDelta = getMergeDelta( mergePosition, 4, 12, baseVersion );
-		} );
-
-		describe( 'InsertDelta', () => {
-			let nodeA, nodeB;
-
-			beforeEach( () => {
-				nodeA = new Element( 'a' );
-				nodeB = new Element( 'b' );
-			} );
-
-			it( 'insert at same position as merge', () => {
-				const insertDelta = getInsertDelta( mergePosition, [ nodeA, nodeB ], baseVersion );
-				const transformed = transform( mergeDelta, insertDelta, context );
-
-				// Expected: MergeDelta gets ignored and is not applied.
-
-				baseVersion = insertDelta.operations.length;
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: Delta,
-					operations: [
-						{
-							type: NoOperation,
-							baseVersion
-						}
-					]
-				} );
-
-				// Test if deltas do what they should after applying transformed delta.
-
-				applyDelta( insertDelta, doc );
-				applyDelta( transformed[ 0 ], doc );
-
-				const nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 0 ] ), 6 ) );
-
-				// InsertDelta is applied. Merge between X and P is discarded.
-				expect( nodesAndText ).to.equal( 'XXXXXabcdXAABBPabcfoobarxyzP' );
-			} );
-
-			it( 'insert at same position as merge - undo mode', () => {
-				// In undo mode, default transformation algorithm should be used.
-				const insertDelta = getInsertDelta( mergePosition, [ nodeA, nodeB ], baseVersion );
-
-				context.undoMode = true;
-				const transformed = transform( mergeDelta, insertDelta, context );
-
-				baseVersion = insertDelta.operations.length;
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: MergeDelta,
-					operations: [
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 5, 0 ] ),
-							howMany: mergeDelta.operations[ 0 ].howMany,
-							targetPosition: mergeDelta.operations[ 0 ].targetPosition,
-							baseVersion
-						},
-						{
-							type: RemoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 5 ] ),
-							howMany: 1,
-							targetPosition: mergeDelta.operations[ 1 ].targetPosition,
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-			} );
-
-			it( 'insert at same position as merge - merge second operation is NoOperation', () => {
-				// If MergeDelta second operation is NoOperation, default transformation algorithm should be used.
-				mergeDelta.operations[ 1 ] = new NoOperation( 1 );
-				const insertDelta = getInsertDelta( mergePosition, [ nodeA, nodeB ], baseVersion );
-				const transformed = transform( mergeDelta, insertDelta, context );
-
-				baseVersion = insertDelta.operations.length;
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: MergeDelta,
-					operations: [
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 5, 0 ] ),
-							howMany: mergeDelta.operations[ 0 ].howMany,
-							targetPosition: mergeDelta.operations[ 0 ].targetPosition,
-							baseVersion
-						},
-						{
-							type: NoOperation,
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-			} );
-
-			it( 'insert inside merged node (sticky move test)', () => {
-				const insertDelta = getInsertDelta( new Position( root, [ 3, 3, 3, 12 ] ), [ nodeA, nodeB ], baseVersion );
-				const transformed = transform( mergeDelta, insertDelta, context );
-
-				baseVersion = insertDelta.operations.length;
-
-				expect( transformed.length ).to.equal( 1 );
-
-				// Expected: MoveOperation in MergeDelta has it's "range" expanded.
-
-				expectDelta( transformed[ 0 ], {
-					type: MergeDelta,
-					operations: [
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 3, 0 ] ),
-							howMany: 14,
-							targetPosition: new Position( root, [ 3, 3, 2, 4 ] ),
-							baseVersion
-						},
-						{
-							type: RemoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 3 ] ),
-							howMany: 1,
-							targetPosition: new Position( gy, [ 0 ] ),
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-
-				// Test if deltas do what they should after applying transformed delta.
-
-				applyDelta( insertDelta, doc );
-				applyDelta( transformed[ 0 ], doc );
-
-				const nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 0 ] ), 3 ) );
-
-				// InsertDelta is applied. Merge between X and P is discarded.
-				expect( nodesAndText ).to.equal( 'XXXXXabcdabcfoobarxyzAABBX' );
-			} );
-		} );
-
-		describe( 'MoveDelta', () => {
-			it( 'node on the right side of merge was moved', () => {
-				const moveDelta = getMoveDelta( new Position( root, [ 3, 3, 3 ] ), 1, new Position( root, [ 3, 3, 0 ] ), baseVersion );
-				const transformed = transform( mergeDelta, moveDelta, context );
-
-				baseVersion = moveDelta.operations.length;
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: Delta,
-					operations: [
-						{
-							type: NoOperation,
-							baseVersion
-						}
-					]
-				} );
-
-				// Test if deltas do what they should after applying transformed delta.
-				applyDelta( moveDelta, doc );
-				applyDelta( transformed[ 0 ], doc );
-
-				const nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3 ] ), 1 ) );
-
-				// MoveDelta is applied. MergeDelta is discarded.
-				expect( nodesAndText ).to.equal( 'DIVPabcfoobarxyzPXXXXXabcdXDIV' );
-			} );
-
-			it( 'node on the left side of merge was moved', () => {
-				const moveDelta = getMoveDelta( new Position( root, [ 3, 3, 2 ] ), 1, new Position( root, [ 3, 3, 0 ] ), baseVersion );
-				const transformed = transform( mergeDelta, moveDelta, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				baseVersion = moveDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: MergeDelta,
-					operations: [
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 3, 0 ] ),
-							howMany: 12,
-							targetPosition: new Position( root, [ 3, 3, 0, 4 ] ),
-							baseVersion
-						},
-						{
-							type: RemoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 3 ] ),
-							howMany: 1,
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-
-				// Test if deltas do what they should after applying transformed delta.
-				applyDelta( moveDelta, doc );
-				applyDelta( transformed[ 0 ], doc );
-
-				const nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3 ] ), 1 ) );
-
-				// MoveDelta is applied. MergeDelta is discarded.
-				expect( nodesAndText ).to.equal( 'DIVXabcdabcfoobarxyzXXXXXDIV' );
-			} );
-
-			it( 'node on the left side of merge was moved - second merge operation is NoOperation', () => {
-				// If second MergeDelta operation is NoOperation default algorithm should be used.
-				mergeDelta.operations[ 1 ] = new NoOperation( 1 );
-
-				const moveDelta = getMoveDelta( new Position( root, [ 3, 3, 2 ] ), 1, new Position( root, [ 3, 3, 0 ] ), baseVersion );
-				const transformed = transform( mergeDelta, moveDelta, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				baseVersion = moveDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: MergeDelta,
-					operations: [
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 3, 0 ] ),
-							howMany: 12,
-							targetPosition: new Position( root, [ 3, 3, 0, 4 ] ),
-							baseVersion
-						},
-						{
-							type: NoOperation,
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-			} );
-		} );
-
-		describe( 'MergeDelta', () => {
-			it( 'merge two consecutive elements, transformed merge is after', () => {
-				const mergeDeltaB = getMergeDelta( new Position( root, [ 3, 3, 2 ] ), 0, 4, baseVersion );
-				const transformed = transform( mergeDelta, mergeDeltaB, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				baseVersion = mergeDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: MergeDelta,
-					operations: [
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 2, 0 ] ),
-							howMany: 12,
-							targetPosition: new Position( root, [ 3, 3, 1, 4 ] ),
-							baseVersion
-						},
-						{
-							type: RemoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 2 ] ),
-							howMany: 1
-						}
-					]
-				} );
-
-				// Test if deltas do what they should after applying transformed delta.
-				applyDelta( mergeDeltaB, doc );
-				applyDelta( transformed[ 0 ], doc );
-
-				const nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 0 ] ), 2 ) );
-
-				// Both merge deltas are applied and merged nodes children are together in one node.
-				expect( nodesAndText ).to.equal( 'XXXabcdabcfoobarxyzX' );
-			} );
-
-			it( 'merge two consecutive elements, transformed merge is before', () => {
-				mergeDelta = getMergeDelta( new Position( root, [ 3, 3, 2 ] ), 0, 4, baseVersion );
-				const mergeDeltaB = getMergeDelta( new Position( root, [ 3, 3, 3 ] ), 4, 12, baseVersion );
-
-				const transformed = transform( mergeDelta, mergeDeltaB, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				baseVersion = mergeDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: MergeDelta,
-					operations: [
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 2, 0 ] ),
-							howMany: 16,
-							targetPosition: new Position( root, [ 3, 3, 1, 0 ] ),
-							baseVersion
-						},
-						{
-							type: RemoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 2 ] ),
-							howMany: 1
-						}
-					]
-				} );
-
-				// Test if deltas do what they should after applying transformed delta.
-				applyDelta( mergeDeltaB, doc );
-				applyDelta( transformed[ 0 ], doc );
-
-				const nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 0 ] ), 2 ) );
-
-				// Both merge deltas are applied and merged nodes children are together in one node.
-				expect( nodesAndText ).to.equal( 'XXXabcdabcfoobarxyzX' );
-			} );
-		} );
-	} );
-} );

+ 0 - 191
packages/ckeditor5-engine/tests/model/delta/transform/movedelta.js

@@ -1,191 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import transformations from '../../../../src/model/delta/basic-transformations'; // eslint-disable-line no-unused-vars
-
-import deltaTransform from '../../../../src/model/delta/transform';
-const transform = deltaTransform.transform;
-
-import Position from '../../../../src/model/position';
-import Range from '../../../../src/model/range';
-
-import MoveDelta from '../../../../src/model/delta/movedelta';
-import SplitDelta from '../../../../src/model/delta/splitdelta';
-
-import MoveOperation from '../../../../src/model/operation/moveoperation';
-import NoOperation from '../../../../src/model/operation/nooperation';
-
-import { getNodesAndText } from '../../../../tests/model/_utils/utils';
-
-import {
-	applyDelta,
-	expectDelta,
-	getFilledDocument,
-	getMergeDelta,
-	getMoveDelta
-} from '../../../../tests/model/delta/transform/_utils/utils';
-
-describe( 'transform', () => {
-	let doc, root, gy, baseVersion, context;
-
-	beforeEach( () => {
-		doc = getFilledDocument();
-		root = doc.getRoot();
-		gy = doc.graveyard;
-		baseVersion = doc.version;
-		context = { isStrong: false };
-	} );
-
-	describe( 'MoveDelta by', () => {
-		let moveDelta;
-
-		beforeEach( () => {
-			const sourcePosition = new Position( root, [ 3, 3, 3 ] );
-			const howMany = 1;
-			const targetPosition = new Position( root, [ 3, 3, 0 ] );
-
-			moveDelta = getMoveDelta( sourcePosition, howMany, targetPosition, baseVersion );
-		} );
-
-		describe( 'MergeDelta', () => {
-			it( 'node on the right side of merge was moved', () => {
-				const mergePosition = new Position( root, [ 3, 3, 3 ] );
-				const mergeDelta = getMergeDelta( mergePosition, 4, 12, baseVersion );
-
-				const transformed = transform( moveDelta, mergeDelta, context );
-
-				expect( transformed.length ).to.equal( 2 );
-
-				baseVersion = mergeDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							// This `SplitDelta` got created through reversing `MergeDelta`. It means that operations in
-							// a `MergeDelta` had been reversed. One of them is `RemoveOperation` which got reversed into
-							// `ReinsertOperation` because we want to get back the node from graveyard. `ReinsertOperation`
-							// is treated in OT as `MoveOperation` and might be converted to it. This is why we have to
-							// check whether the operation type is `MoveOperation`. This is all perfectly valid.
-							type: MoveOperation,
-							sourcePosition: new Position( gy, [ 0 ] ),
-							howMany: 1,
-							targetPosition: new Position( root, [ 3, 3, 3 ] ),
-							baseVersion
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 2, 4 ] ),
-							howMany: 12,
-							targetPosition: new Position( root, [ 3, 3, 3, 0 ] ),
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-
-				expectDelta( transformed[ 1 ], {
-					type: MoveDelta,
-					operations: [
-						{
-							type: MoveOperation,
-							sourcePosition: moveDelta._moveOperation.sourcePosition,
-							howMany: moveDelta._moveOperation.howMany,
-							targetPosition: moveDelta._moveOperation.targetPosition,
-							baseVersion: baseVersion + 2
-						}
-					]
-				} );
-
-				// Test if deltas do what they should after applying transformed delta.
-
-				applyDelta( mergeDelta, doc );
-				applyDelta( transformed[ 0 ], doc );
-				applyDelta( transformed[ 1 ], doc );
-
-				const nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3 ] ), 1 ) );
-
-				// MoveDelta is applied. MergeDelta is discarded.
-				expect( nodesAndText ).to.equal( 'DIVPabcfoobarxyzPXXXXXabcdXDIV' );
-			} );
-
-			it( 'move range in merged node #1', () => {
-				const mergePosition = new Position( root, [ 3, 3 ] );
-				const mergeDelta = getMergeDelta( mergePosition, 1, 4, baseVersion );
-
-				const transformed = transform( moveDelta, mergeDelta, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				baseVersion = mergeDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: MoveDelta,
-					operations: [
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 2, 4 ] ),
-							howMany: 1,
-							targetPosition: new Position( root, [ 3, 2, 1 ] ),
-							baseVersion
-						}
-					]
-				} );
-			} );
-
-			it( 'move range in merged node #2', () => {
-				moveDelta._moveOperation.sourcePosition.path = [ 3, 3, 1 ];
-				moveDelta._moveOperation.targetPosition.path = [ 3, 3, 4 ];
-
-				const mergePosition = new Position( root, [ 3, 3 ] );
-				const mergeDelta = getMergeDelta( mergePosition, 1, 4, baseVersion );
-
-				const transformed = transform( moveDelta, mergeDelta, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				baseVersion = mergeDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: MoveDelta,
-					operations: [
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 2, 2 ] ),
-							howMany: 1,
-							targetPosition: new Position( root, [ 3, 2, 5 ] ),
-							baseVersion
-						}
-					]
-				} );
-			} );
-
-			it( 'node on the right side of merge was moved - second merge operation is NoOperation', () => {
-				// If second MergeDelta operation is NoOperation default algorithm should be used.
-				const mergePosition = new Position( root, [ 3, 3, 3 ] );
-				const mergeDelta = getMergeDelta( mergePosition, 4, 12, baseVersion );
-				mergeDelta.operations[ 1 ] = new NoOperation( 1 );
-
-				const transformed = transform( moveDelta, mergeDelta, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				baseVersion = mergeDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: MoveDelta,
-					operations: [
-						{
-							type: MoveOperation,
-							sourcePosition: moveDelta._moveOperation.sourcePosition,
-							howMany: moveDelta._moveOperation.howMany,
-							targetPosition: moveDelta._moveOperation.targetPosition,
-							baseVersion
-						}
-					]
-				} );
-			} );
-		} );
-	} );
-} );

+ 0 - 242
packages/ckeditor5-engine/tests/model/delta/transform/removedelta.js

@@ -1,242 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import transformations from '../../../../src/model/delta/basic-transformations'; // eslint-disable-line no-unused-vars
-
-import deltaTransform from '../../../../src/model/delta/transform';
-const transform = deltaTransform.transform;
-
-import Element from '../../../../src/model/element';
-import Position from '../../../../src/model/position';
-import Range from '../../../../src/model/range';
-
-import Delta from '../../../../src/model/delta/delta';
-import RemoveDelta from '../../../../src/model/delta/removedelta';
-import SplitDelta from '../../../../src/model/delta/splitdelta';
-
-import NoOperation from '../../../../src/model/operation/nooperation';
-import MoveOperation from '../../../../src/model/operation/moveoperation';
-import RemoveOperation from '../../../../src/model/operation/removeoperation';
-
-import { getNodesAndText } from '../../../../tests/model/_utils/utils';
-
-import {
-	applyDelta,
-	expectDelta,
-	getFilledDocument,
-	getMergeDelta,
-	getRemoveDelta,
-	getSplitDelta
-} from '../../../../tests/model/delta/transform/_utils/utils';
-
-describe( 'transform', () => {
-	let doc, root, gy, baseVersion, context;
-
-	beforeEach( () => {
-		doc = getFilledDocument();
-		root = doc.getRoot();
-		gy = doc.graveyard;
-		baseVersion = doc.version;
-		context = { isStrong: false };
-	} );
-
-	describe( 'RemoveDelta by', () => {
-		describe( 'MergeDelta', () => {
-			it( 'node on the right side of merge was removed', () => {
-				// This special case should be handled by MoveDelta x MergeDelta special case.
-				const sourcePosition = new Position( root, [ 3, 3, 3 ] );
-				const removeDelta = getRemoveDelta( sourcePosition, 1, baseVersion );
-
-				const mergePosition = new Position( root, [ 3, 3, 3 ] );
-				const mergeDelta = getMergeDelta( mergePosition, 4, 12, baseVersion );
-
-				const transformed = transform( removeDelta, mergeDelta, context );
-
-				expect( transformed.length ).to.equal( 2 );
-
-				baseVersion = mergeDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( gy, [ 0 ] ),
-							howMany: 1,
-							targetPosition: new Position( root, [ 3, 3, 3 ] ),
-							baseVersion
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 2, 4 ] ),
-							howMany: 12,
-							targetPosition: new Position( root, [ 3, 3, 3, 0 ] ),
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-
-				expectDelta( transformed[ 1 ], {
-					type: RemoveDelta,
-					operations: [
-						{
-							type: MoveOperation,
-							sourcePosition: removeDelta._moveOperation.sourcePosition,
-							howMany: removeDelta._moveOperation.howMany,
-							baseVersion: baseVersion + 2
-						}
-					]
-				} );
-
-				// Test if deltas do what they should after applying transformed delta.
-				applyDelta( mergeDelta, doc );
-				applyDelta( transformed[ 0 ], doc );
-				applyDelta( transformed[ 1 ], doc );
-
-				const nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3 ] ), 1 ) );
-
-				// RemoveDelta is applied. MergeDelta is discarded.
-				expect( nodesAndText ).to.equal( 'DIVXXXXXabcdXDIV' );
-			} );
-		} );
-
-		describe( 'SplitDelta', () => {
-			it( 'node inside the removed range was a node that has been split', () => {
-				const sourcePosition = new Position( root, [ 3, 3, 1 ] );
-				const removeDelta = getRemoveDelta( sourcePosition, 3, baseVersion );
-
-				const splitPosition = new Position( root, [ 3, 3, 2, 2 ] );
-				const nodeCopy = new Element( 'x' );
-				const splitDelta = getSplitDelta( splitPosition, nodeCopy, 2, baseVersion );
-
-				const transformed = transform( removeDelta, splitDelta, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				baseVersion = splitDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: RemoveDelta,
-					operations: [
-						{
-							type: RemoveOperation,
-							sourcePosition,
-							howMany: 4,
-							baseVersion
-						}
-					]
-				} );
-			} );
-
-			it( 'removed nodes in split node, after split position, isStrong = false', () => {
-				const sourcePosition = new Position( root, [ 3, 3, 2, 4 ] );
-				const removeDelta = getRemoveDelta( sourcePosition, 3, baseVersion );
-
-				const splitPosition = new Position( root, [ 3, 3, 2, 2 ] );
-				const nodeCopy = new Element( 'x' );
-				const splitDelta = getSplitDelta( splitPosition, nodeCopy, 8, baseVersion );
-
-				const transformed = transform( removeDelta, splitDelta, {
-					isStrong: false,
-					forceWeakRemove: true
-				} );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				baseVersion = splitDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: Delta,
-					operations: [
-						{
-							type: NoOperation,
-							baseVersion
-						}
-					]
-				} );
-			} );
-
-			it( 'last node in the removed range was a node that has been split', () => {
-				const sourcePosition = new Position( root, [ 3, 2 ] );
-				const removeDelta = getRemoveDelta( sourcePosition, 2, baseVersion );
-
-				const splitPosition = new Position( root, [ 3, 3, 2 ] );
-				const nodeCopy = new Element( 'div' );
-				const splitDelta = getSplitDelta( splitPosition, nodeCopy, 2, baseVersion );
-
-				const transformed = transform( removeDelta, splitDelta, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				baseVersion = splitDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: RemoveDelta,
-					operations: [
-						{
-							type: RemoveOperation,
-							sourcePosition,
-							howMany: 3,
-							baseVersion
-						}
-					]
-				} );
-			} );
-
-			it( 'removed node has been split - undo context', () => {
-				const sourcePosition = new Position( root, [ 3, 3, 1 ] );
-				const removeDelta = getRemoveDelta( sourcePosition, 1, baseVersion );
-
-				const splitPosition = new Position( root, [ 3, 3, 1, 2 ] );
-				const nodeCopy = new Element( 'x' );
-				const splitDelta = getSplitDelta( splitPosition, nodeCopy, 2, baseVersion );
-
-				context.undoMode = true;
-
-				const transformed = transform( removeDelta, splitDelta, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				baseVersion = splitDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: RemoveDelta,
-					operations: [
-						{
-							type: RemoveOperation,
-							sourcePosition,
-							howMany: 1,
-							baseVersion
-						}
-					]
-				} );
-			} );
-
-			it( 'should not throw if clone operation is NoOperation and use default transformation in that case', () => {
-				const noOpSplitDelta = new SplitDelta();
-				noOpSplitDelta.addOperation( new NoOperation( 0 ) );
-				noOpSplitDelta.addOperation( new MoveOperation( new Position( root, [ 1, 2 ] ), 3, new Position( root, [ 2, 0 ] ), 1 ) );
-
-				const removeDelta = getRemoveDelta( new Position( root, [ 3 ] ), 1, 0 );
-
-				const transformed = transform( removeDelta, noOpSplitDelta, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: RemoveDelta,
-					operations: [
-						{
-							type: RemoveOperation,
-							sourcePosition: new Position( root, [ 3 ] ),
-							howMany: 1,
-							baseVersion: 2
-						}
-					]
-				} );
-			} );
-		} );
-	} );
-} );

+ 0 - 203
packages/ckeditor5-engine/tests/model/delta/transform/renamedelta.js

@@ -1,203 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import transformations from '../../../../src/model/delta/basic-transformations'; // eslint-disable-line no-unused-vars
-import deltaTransform from '../../../../src/model/delta/transform';
-const transform = deltaTransform.transform;
-
-import Element from '../../../../src/model/element';
-import Position from '../../../../src/model/position';
-
-import RenameDelta from '../../../../src/model/delta/renamedelta';
-import SplitDelta from '../../../../src/model/delta/splitdelta';
-import Delta from '../../../../src/model/delta/delta';
-import RenameOperation from '../../../../src/model/operation/renameoperation';
-import MoveOperation from '../../../../src/model/operation/moveoperation';
-import NoOperation from '../../../../src/model/operation/nooperation';
-
-import {
-	getFilledDocument,
-	expectDelta,
-	getSplitDelta
-} from '../../../../tests/model/delta/transform/_utils/utils';
-
-describe( 'transform', () => {
-	let doc, root, baseVersion, context;
-
-	beforeEach( () => {
-		doc = getFilledDocument();
-		root = doc.getRoot();
-		baseVersion = doc.version;
-		context = { isStrong: false };
-	} );
-
-	describe( 'RenameDelta by', () => {
-		describe( 'SplitDelta', () => {
-			it( 'split element is renamed', () => {
-				const renameDelta = new RenameDelta();
-				renameDelta.addOperation( new RenameOperation(
-					new Position( root, [ 3, 3 ] ),
-					'p',
-					'li',
-					baseVersion
-				) );
-
-				const splitPosition = new Position( root, [ 3, 3, 3 ] );
-				const splitDelta = getSplitDelta( splitPosition, new Element( 'p' ), 9, baseVersion );
-
-				const transformed = transform( renameDelta, splitDelta, context );
-
-				baseVersion = splitDelta.length;
-
-				expect( transformed.length ).to.equal( 2 );
-
-				expectDelta( transformed[ 0 ], {
-					type: RenameDelta,
-					operations: [
-						{
-							type: RenameOperation,
-							oldName: 'p',
-							newName: 'li',
-							position: new Position( root, [ 3, 3 ] )
-						}
-					]
-				} );
-
-				expectDelta( transformed[ 1 ], {
-					type: RenameDelta,
-					operations: [
-						{
-							type: RenameOperation,
-							oldName: 'p',
-							newName: 'li',
-							position: new Position( root, [ 3, 4 ] )
-						}
-					]
-				} );
-			} );
-
-			it( 'split element is renamed but split delta was undone', () => {
-				const renameDelta = new RenameDelta();
-				renameDelta.addOperation( new RenameOperation(
-					new Position( root, [ 3, 3 ] ),
-					'p',
-					'li',
-					baseVersion
-				) );
-
-				const splitPosition = new Position( root, [ 3, 3, 3 ] );
-				const splitDelta = getSplitDelta( splitPosition, new Element( 'p' ), 9, baseVersion );
-
-				context.undoMode = true;
-
-				const transformed = transform( renameDelta, splitDelta, context );
-
-				baseVersion = splitDelta.length;
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: RenameDelta,
-					operations: [
-						{
-							type: RenameOperation,
-							oldName: 'p',
-							newName: 'li',
-							position: new Position( root, [ 3, 3 ] )
-						}
-					]
-				} );
-			} );
-
-			it( 'split element is different than renamed element', () => {
-				const renameDelta = new RenameDelta();
-				renameDelta.addOperation( new RenameOperation(
-					new Position( root, [ 3, 3 ] ),
-					'p',
-					'li',
-					baseVersion
-				) );
-
-				const splitPosition = new Position( root, [ 3, 2, 1 ] );
-				const splitDelta = getSplitDelta( splitPosition, new Element( 'p' ), 9, baseVersion );
-
-				const transformed = transform( renameDelta, splitDelta, context );
-
-				baseVersion = splitDelta.length;
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: RenameDelta,
-					operations: [
-						{
-							type: RenameOperation,
-							oldName: 'p',
-							newName: 'li',
-							position: new Position( root, [ 3, 4 ] )
-						}
-					]
-				} );
-			} );
-
-			it( 'should not throw if clone operation is NoOperation and use default transformation in that case', () => {
-				const noOpSplitDelta = new SplitDelta();
-				noOpSplitDelta.addOperation( new NoOperation( 0 ) );
-				noOpSplitDelta.addOperation( new MoveOperation( new Position( root, [ 1, 2 ] ), 3, new Position( root, [ 2, 0 ] ), 1 ) );
-
-				const renameDelta = new RenameDelta();
-				renameDelta.addOperation( new RenameOperation(
-					new Position( root, [ 1 ] ),
-					'p',
-					'li',
-					baseVersion
-				) );
-
-				const transformed = transform( renameDelta, noOpSplitDelta, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: RenameDelta,
-					operations: [
-						{
-							type: RenameOperation,
-							position: new Position( root, [ 1 ] ),
-							oldName: 'p',
-							newName: 'li',
-							baseVersion: 2
-						}
-					]
-				} );
-			} );
-		} );
-
-		describe( 'RenameDelta', () => {
-			it( 'should be transformed to NoDelta if its operation is transformed to NoOperation', () => {
-				const renameDeltaA = new RenameDelta();
-				const renameDeltaB = new RenameDelta();
-
-				const op = new RenameOperation( new Position( root, [ 3 ] ), 'p', 'li', baseVersion );
-
-				renameDeltaA.addOperation( op );
-				renameDeltaB.addOperation( op.clone() );
-
-				const transformed = transform( renameDeltaA, renameDeltaB, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: Delta,
-					operations: [
-						{
-							type: NoOperation,
-							baseVersion: 1
-						}
-					]
-				} );
-			} );
-		} );
-	} );
-} );

+ 0 - 1091
packages/ckeditor5-engine/tests/model/delta/transform/splitdelta.js

@@ -1,1091 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import transformations from '../../../../src/model/delta/basic-transformations'; // eslint-disable-line no-unused-vars
-
-import deltaTransform from '../../../../src/model/delta/transform';
-const transform = deltaTransform.transform;
-
-import Element from '../../../../src/model/element';
-import Position from '../../../../src/model/position';
-import Range from '../../../../src/model/range';
-
-import Delta from '../../../../src/model/delta/delta';
-import SplitDelta from '../../../../src/model/delta/splitdelta';
-import AttributeDelta from '../../../../src/model/delta/attributedelta';
-import RenameDelta from '../../../../src/model/delta/renamedelta';
-
-import InsertOperation from '../../../../src/model/operation/insertoperation';
-import AttributeOperation from '../../../../src/model/operation/attributeoperation';
-import ReinsertOperation from '../../../../src/model/operation/reinsertoperation';
-import MoveOperation from '../../../../src/model/operation/moveoperation';
-import NoOperation from '../../../../src/model/operation/nooperation';
-import RenameOperation from '../../../../src/model/operation/renameoperation';
-
-import { getNodesAndText } from '../../../../tests/model/_utils/utils';
-
-import {
-	applyDelta,
-	expectDelta,
-	getFilledDocument,
-	getSplitDelta,
-	getWrapDelta,
-	getUnwrapDelta,
-	getRemoveDelta
-} from '../../../../tests/model/delta/transform/_utils/utils';
-
-describe( 'transform', () => {
-	let doc, root, gy, baseVersion, context;
-
-	beforeEach( () => {
-		doc = getFilledDocument();
-		root = doc.getRoot();
-		gy = doc.graveyard;
-		baseVersion = doc.version;
-		context = { isStrong: false };
-	} );
-
-	describe( 'SplitDelta by', () => {
-		let splitDelta, splitPosition;
-
-		beforeEach( () => {
-			splitPosition = new Position( root, [ 3, 3, 3, 3 ] );
-			splitDelta = getSplitDelta( splitPosition, new Element( 'p' ), 9, baseVersion );
-		} );
-
-		describe( 'SplitDelta', () => {
-			it( 'split in same parent and offset - isStrong = false', () => {
-				const splitDeltaB = getSplitDelta( splitPosition, new Element( 'p' ), 9, baseVersion );
-				const transformed = transform( splitDelta, splitDeltaB, context );
-
-				baseVersion = splitDeltaB.operations.length;
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: new Position( root, [ 3, 3, 5 ] ),
-							baseVersion
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 4, 0 ] ),
-							howMany: 9,
-							targetPosition: new Position( root, [ 3, 3, 5, 0 ] ),
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-
-				// Test if deltas do what they should after applying transformed delta.
-
-				applyDelta( splitDeltaB, doc );
-				applyDelta( transformed[ 0 ], doc );
-
-				const nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 0 ] ), 6 ) );
-
-				expect( nodesAndText ).to.equal( 'XXXXXabcdXPabcPPPPfoobarxyzP' );
-			} );
-
-			it( 'split in same parent and offset - isStrong = true', () => {
-				const splitDeltaB = getSplitDelta( splitPosition, new Element( 'p' ), 9, baseVersion );
-
-				context.isStrong = true;
-				const transformed = transform( splitDelta, splitDeltaB, context );
-
-				baseVersion = splitDeltaB.operations.length;
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: new Position( root, [ 3, 3, 4 ] ),
-							baseVersion
-						},
-						{
-							type: NoOperation,
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-
-				// Test if deltas do what they should after applying transformed delta.
-
-				applyDelta( splitDeltaB, doc );
-				applyDelta( transformed[ 0 ], doc );
-
-				const nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 0 ] ), 6 ) );
-
-				expect( nodesAndText ).to.equal( 'XXXXXabcdXPabcPPPPfoobarxyzP' );
-			} );
-
-			it( 'should not change context.insertBefore if it was set', () => {
-				// SplitDelta x SplitDelta transformation case sets `context.insertBefore` on its own if it is not set.
-				// It is to achieve better transformation results from UX point of view.
-				// However, if `context.insertBefore` was already set, it should not be changed. This might be important for undo.
-				const splitDeltaB = getSplitDelta( splitPosition, new Element( 'p' ), 9, baseVersion );
-				const transformed = transform( splitDelta, splitDeltaB, {
-					isStrong: false,
-					insertBefore: true,
-					undoMode: true // If `insertBefore` was set it means that delta `b` had to be undone.
-				} );
-
-				baseVersion = splitDeltaB.operations.length;
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							// If `context.insertBefore` would not be set, the offset would be 4. See an example above.
-							position: new Position( root, [ 3, 3, 5 ] ),
-							baseVersion
-						},
-						{
-							type: NoOperation,
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-			} );
-
-			it( 'split in same parent, incoming delta splits closer', () => {
-				const splitDeltaB = getSplitDelta( new Position( root, [ 3, 3, 3, 5 ] ), new Element( 'p' ), 7, baseVersion );
-				const transformed = transform( splitDelta, splitDeltaB, context );
-
-				baseVersion = splitDeltaB.operations.length;
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: new Position( root, [ 3, 3, 4 ] ),
-							baseVersion
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 3, 3 ] ),
-							howMany: 2,
-							targetPosition: new Position( root, [ 3, 3, 4, 0 ] ),
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-
-				// Test if deltas do what they should after applying transformed delta.
-
-				applyDelta( splitDeltaB, doc );
-				applyDelta( transformed[ 0 ], doc );
-
-				const nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 0 ] ), 6 ) );
-
-				// P element is correctly split, there are three P elements, letters in P elements are in correct order.
-				expect( nodesAndText ).to.equal( 'XXXXXabcdXPabcPPfoPPobarxyzP' );
-			} );
-
-			it( 'split in same parent, incoming delta splits closer, split deltas have reinsert operations', () => {
-				let reOp = new ReinsertOperation(
-					new Position( gy, [ 1 ] ),
-					1,
-					Position.createFromPosition( splitDelta.operations[ 0 ].position ),
-					splitDelta.operations[ 0 ].baseVersion
-				);
-				splitDelta.operations[ 0 ] = reOp;
-
-				const splitDeltaB = getSplitDelta( new Position( root, [ 3, 3, 3, 5 ] ), new Element( 'p' ), 7, baseVersion );
-				reOp = new ReinsertOperation(
-					new Position( gy, [ 0 ] ),
-					1,
-					Position.createFromPosition( splitDeltaB.operations[ 0 ].position ),
-					splitDeltaB.operations[ 0 ].baseVersion
-				);
-				splitDeltaB.operations[ 0 ] = reOp;
-
-				const transformed = transform( splitDelta, splitDeltaB, context );
-
-				baseVersion = splitDeltaB.operations.length;
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							type: ReinsertOperation,
-							sourcePosition: new Position( gy, [ 0 ] ),
-							howMany: 1,
-							targetPosition: new Position( root, [ 3, 3, 4 ] ),
-							baseVersion
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 3, 3 ] ),
-							howMany: 2,
-							targetPosition: new Position( root, [ 3, 3, 4, 0 ] ),
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-			} );
-
-			it( 'split in same parent, incoming delta splits further', () => {
-				const splitDeltaB = getSplitDelta( new Position( root, [ 3, 3, 3, 1 ] ), new Element( 'p' ), 11, baseVersion );
-				const transformed = transform( splitDelta, splitDeltaB, context );
-
-				baseVersion = splitDeltaB.operations.length;
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: new Position( root, [ 3, 3, 5 ] ),
-							baseVersion
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 4, 2 ] ),
-							howMany: 9,
-							targetPosition: new Position( root, [ 3, 3, 5, 0 ] ),
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-
-				// Test if deltas do what they should after applying transformed delta.
-
-				applyDelta( splitDeltaB, doc );
-				applyDelta( transformed[ 0 ], doc );
-
-				const nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 0 ] ), 6 ) );
-
-				// P element is correctly split, there are three P elements, letters in P elements are in correct order.
-				expect( nodesAndText ).to.equal( 'XXXXXabcdXPaPPbcPPfoobarxyzP' );
-			} );
-
-			it( 'split in same parent, incoming delta splits further, split deltas have reinsert operations', () => {
-				let reOp = new ReinsertOperation(
-					new Position( gy, [ 1 ] ),
-					1,
-					Position.createFromPosition( splitDelta.operations[ 0 ].position ),
-					splitDelta.operations[ 0 ].baseVersion
-				);
-				splitDelta.operations[ 0 ] = reOp;
-
-				const splitDeltaB = getSplitDelta( new Position( root, [ 3, 3, 3, 1 ] ), new Element( 'p' ), 11, baseVersion );
-				reOp = new ReinsertOperation(
-					new Position( gy, [ 0 ] ),
-					1,
-					Position.createFromPosition( splitDeltaB.operations[ 0 ].position ),
-					splitDeltaB.operations[ 0 ].baseVersion
-				);
-				splitDeltaB.operations[ 0 ] = reOp;
-
-				const transformed = transform( splitDelta, splitDeltaB, context );
-
-				baseVersion = splitDeltaB.operations.length;
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							type: ReinsertOperation,
-							sourcePosition: new Position( gy, [ 0 ] ),
-							howMany: 1,
-							targetPosition: new Position( root, [ 3, 3, 5 ] ),
-							baseVersion
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 4, 2 ] ),
-							howMany: 9,
-							targetPosition: new Position( root, [ 3, 3, 5, 0 ] ),
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-			} );
-
-			it( 'split in split parent', () => {
-				const splitDeltaB = getSplitDelta( new Position( root, [ 3, 3, 3 ] ), new Element( 'div' ), 1, baseVersion );
-				const transformed = transform( splitDelta, splitDeltaB, context );
-
-				baseVersion = splitDeltaB.operations.length;
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: new Position( root, [ 3, 4, 1 ] ),
-							baseVersion
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 4, 0, 3 ] ),
-							howMany: 9,
-							targetPosition: new Position( root, [ 3, 4, 1, 0 ] ),
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-
-				// Test if deltas do what they should after applying transformed delta.
-
-				applyDelta( splitDeltaB, doc );
-				applyDelta( transformed[ 0 ], doc );
-
-				const nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3 ] ), 2 ) );
-
-				// DIV and P elements are correctly split.
-				expect( nodesAndText ).to.equal( 'DIVXXXXXabcdXDIVDIVPabcPPfoobarxyzPDIV' );
-			} );
-
-			it( 'should use default algorithm and not throw if transformed split delta has NoOperation', () => {
-				splitDelta.operations[ 1 ] = new NoOperation( 1 );
-				const splitDeltaB = getSplitDelta( new Position( root, [ 3, 3, 3, 1 ] ), new Element( 'p' ), 11, baseVersion );
-
-				const transformed = transform( splitDelta, splitDeltaB, context );
-
-				baseVersion = splitDeltaB.operations.length;
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: new Position( root, [ 3, 3, 5 ] ),
-							baseVersion
-						},
-						{
-							type: NoOperation,
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-			} );
-
-			it( 'should use default algorithm and not throw if split delta to transform by has NoOperation', () => {
-				const splitDeltaB = getSplitDelta( new Position( root, [ 3, 3, 3, 1 ] ), new Element( 'p' ), 11, baseVersion );
-				splitDeltaB.operations[ 1 ] = new NoOperation( 1 );
-
-				const transformed = transform( splitDelta, splitDeltaB, context );
-
-				baseVersion = splitDeltaB.operations.length;
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: new Position( root, [ 3, 3, 5 ] ),
-							baseVersion
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 3, 3 ] ),
-							howMany: 9,
-							targetPosition: new Position( root, [ 3, 3, 5, 0 ] ),
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-			} );
-		} );
-
-		describe( 'UnwrapDelta', () => {
-			it( 'split position directly in unwrapped node', () => {
-				const unwrapDelta = getUnwrapDelta( new Position( root, [ 3, 3, 3 ] ), 12, baseVersion );
-				const transformed = transform( splitDelta, unwrapDelta, context );
-
-				baseVersion = unwrapDelta.operations.length;
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: Delta,
-					operations: [
-						{
-							type: NoOperation,
-							baseVersion
-						}
-					]
-				} );
-
-				// Test if deltas do what they should after applying transformed delta.
-				applyDelta( unwrapDelta, doc );
-				applyDelta( transformed[ 0 ], doc );
-
-				const nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3 ] ), 1 ) );
-
-				// UnwrapDelta is applied. SplitDelta is discarded.
-				expect( nodesAndText ).to.equal( 'DIVXXXXXabcdXabcfoobarxyzDIV' );
-			} );
-
-			it( 'split position indirectly in unwrapped node', () => {
-				const unwrapDelta = getUnwrapDelta( new Position( root, [ 3, 3 ] ), 4, baseVersion );
-
-				const transformed = transform( splitDelta, unwrapDelta, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				baseVersion = unwrapDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: new Position( root, [ 3, 7 ] ),
-							baseVersion
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 6, 3 ] ),
-							howMany: 9,
-							targetPosition: new Position( root, [ 3, 7, 0 ] ),
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-
-				// Test if deltas do what they should after applying transformed delta.
-				applyDelta( unwrapDelta, doc );
-				applyDelta( transformed[ 0 ], doc );
-
-				const nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3 ] ), 1 ) );
-
-				// UnwrapDelta and SplitDelta are correctly applied.
-				expect( nodesAndText ).to.equal( 'DIVXXXXXaXXXXXXabcdXPabcPPfoobarxyzPDIV' );
-			} );
-
-			it( 'should use default algorithm and not throw if split delta has NoOperation', () => {
-				splitDelta.operations[ 1 ] = new NoOperation( 1 );
-
-				const unwrapDelta = getUnwrapDelta( new Position( root, [ 3, 3 ] ), 4, baseVersion );
-
-				const transformed = transform( splitDelta, unwrapDelta, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				baseVersion = unwrapDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: new Position( root, [ 3, 7 ] ),
-							baseVersion
-						},
-						{
-							type: NoOperation,
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-			} );
-		} );
-
-		describe( 'WrapDelta', () => {
-			it( 'split position is between wrapped nodes', () => {
-				const wrapRange = new Range( new Position( root, [ 3, 3, 3, 1 ] ), new Position( root, [ 3, 3, 3, 5 ] ) );
-				const wrapElement = new Element( 'E' );
-				const wrapDelta = getWrapDelta( wrapRange, wrapElement, baseVersion );
-
-				const transformed = transform( splitDelta, wrapDelta, context );
-
-				baseVersion = wrapDelta.operations.length;
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: Delta,
-					operations: [
-						{
-							type: NoOperation,
-							baseVersion
-						}
-					]
-				} );
-
-				// Test if deltas do what they should after applying transformed delta.
-				applyDelta( wrapDelta, doc );
-				applyDelta( transformed[ 0 ], doc );
-
-				const nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 3 ] ), 1 ) );
-
-				// WrapDelta is applied. SplitDelta is discarded.
-				expect( nodesAndText ).to.equal( 'PaEbcfoEobarxyzP' );
-			} );
-
-			it( 'split position is before wrapped nodes', () => {
-				const wrapRange = new Range( new Position( root, [ 3, 3, 3, 5 ] ), new Position( root, [ 3, 3, 3, 7 ] ) );
-				const wrapElement = new Element( 'E' );
-				const wrapDelta = getWrapDelta( wrapRange, wrapElement, baseVersion );
-
-				const transformed = transform( splitDelta, wrapDelta, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				baseVersion = wrapDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: new Position( root, [ 3, 3, 4 ] ),
-							baseVersion
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 3, 3 ] ),
-							howMany: 8,
-							targetPosition: new Position( root, [ 3, 3, 4, 0 ] ),
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-
-				// Test if deltas do what they should after applying transformed delta.
-				applyDelta( wrapDelta, doc );
-				applyDelta( transformed[ 0 ], doc );
-
-				const nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 3 ] ), 2 ) );
-
-				// WrapDelta and SplitDelta are correctly applied.
-				expect( nodesAndText ).to.equal( 'PabcPPfoEobEarxyzP' );
-			} );
-
-			it( 'split position is inside wrapped node', () => {
-				const wrapRange = new Range( new Position( root, [ 3, 3, 2 ] ), new Position( root, [ 3, 3, 4 ] ) );
-				const wrapElement = new Element( 'E' );
-				const wrapDelta = getWrapDelta( wrapRange, wrapElement, baseVersion );
-
-				const transformed = transform( splitDelta, wrapDelta, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				baseVersion = wrapDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: new Position( root, [ 3, 3, 2, 2 ] ),
-							baseVersion
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 2, 1, 3 ] ),
-							howMany: 9,
-							targetPosition: new Position( root, [ 3, 3, 2, 2, 0 ] ),
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-
-				// Test if deltas do what they should after applying transformed delta.
-				applyDelta( wrapDelta, doc );
-				applyDelta( transformed[ 0 ], doc );
-
-				const nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 2 ] ), 1 ) );
-
-				// WrapDelta and SplitDelta are correctly applied.
-				expect( nodesAndText ).to.equal( 'EXabcdXPabcPPfoobarxyzPE' );
-			} );
-
-			it( 'should use default algorithm and not throw if split delta has NoOperation', () => {
-				splitDelta.operations[ 1 ] = new NoOperation( 1 );
-
-				const wrapRange = new Range( new Position( root, [ 3, 3, 2 ] ), new Position( root, [ 3, 3, 4 ] ) );
-				const wrapElement = new Element( 'E' );
-				const wrapDelta = getWrapDelta( wrapRange, wrapElement, baseVersion );
-
-				const transformed = transform( splitDelta, wrapDelta, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				baseVersion = wrapDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: new Position( root, [ 3, 3, 3 ] ),
-							baseVersion
-						},
-						{
-							type: NoOperation,
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-			} );
-		} );
-
-		describe( 'AttributeDelta', () => {
-			it( 'attribute changed on split element', () => {
-				const attributeDelta = new AttributeDelta();
-
-				attributeDelta.addOperation( new AttributeOperation(
-					Range.createFromParentsAndOffsets( root, 0, root, 2 ), 'key', 'oldValue', 'newValue', baseVersion
-				) );
-
-				attributeDelta.addOperation( new AttributeOperation(
-					Range.createFromPositionAndShift( new Position( root, [ 3, 3, 2 ] ), 3 ), 'key', null, 'newValue', baseVersion + 1
-				) );
-
-				const transformed = transform( splitDelta, attributeDelta, context );
-
-				baseVersion = attributeDelta.operations.length;
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: new Position( root, [ 3, 3, 4 ] ),
-							baseVersion
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 3, 3 ] ),
-							howMany: 9,
-							targetPosition: new Position( root, [ 3, 3, 4, 0 ] ),
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-
-				expect( transformed[ 0 ].operations[ 0 ].nodes.getNode( 0 ).getAttribute( 'key' ) ).to.equal( 'newValue' );
-			} );
-
-			it( 'attribute removed from split element', () => {
-				splitDelta.operations[ 0 ].nodes.getNode( 0 )._setAttribute( 'key', 'oldValue' );
-				const attributeDelta = new AttributeDelta();
-
-				attributeDelta.addOperation( new AttributeOperation(
-					Range.createFromParentsAndOffsets( root, 0, root, 2 ), 'key', 'otherValue', null, baseVersion
-				) );
-
-				attributeDelta.addOperation( new AttributeOperation(
-					Range.createFromPositionAndShift( new Position( root, [ 3, 3, 2 ] ), 3 ), 'key', 'oldValue', null, baseVersion + 1
-				) );
-
-				const transformed = transform( splitDelta, attributeDelta, context );
-
-				baseVersion = attributeDelta.operations.length;
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: new Position( root, [ 3, 3, 4 ] ),
-							baseVersion
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 3, 3 ] ),
-							howMany: 9,
-							targetPosition: new Position( root, [ 3, 3, 4, 0 ] ),
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-
-				expect( transformed[ 0 ].operations[ 0 ].nodes.getNode( 0 ).hasAttribute( 'key' ) ).to.be.false;
-			} );
-
-			it( 'attribute changed on split element that is reinserted from graveyard', () => {
-				splitDelta.operations[ 0 ] = new ReinsertOperation(
-					new Position( gy, [ 1 ] ),
-					1,
-					new Position( root, [ 3, 3, 4 ] ),
-					baseVersion
-				);
-
-				const attributeDelta = new AttributeDelta();
-
-				attributeDelta.addOperation( new AttributeOperation(
-					Range.createFromParentsAndOffsets( root, 0, root, 4 ), 'key', 'oldValue', 'newValue', baseVersion
-				) );
-
-				const transformed = transform( splitDelta, attributeDelta, context );
-
-				baseVersion = attributeDelta.operations.length;
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							type: ReinsertOperation,
-							sourcePosition: new Position( gy, [ 1 ] ),
-							howMany: 1,
-							targetPosition: new Position( root, [ 3, 3, 4 ] ),
-							baseVersion
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 3, 3 ] ),
-							howMany: 9,
-							targetPosition: new Position( root, [ 3, 3, 4, 0 ] ),
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-			} );
-
-			it( 'should use default algorithm and not throw if split delta has NoOperation', () => {
-				splitDelta.operations[ 1 ] = new NoOperation( 1 );
-
-				const attributeDelta = new AttributeDelta();
-				attributeDelta.addOperation( new AttributeOperation(
-					Range.createFromParentsAndOffsets( root, 0, root, 4 ), 'key', 'oldValue', 'newValue', baseVersion
-				) );
-
-				const transformed = transform( splitDelta, attributeDelta, context );
-
-				baseVersion = attributeDelta.operations.length;
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: new Position( root, [ 3, 3, 4 ] ),
-							baseVersion
-						},
-						{
-							type: NoOperation,
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-			} );
-		} );
-
-		describe( 'RenameDelta', () => {
-			it( 'renamed split element', () => {
-				const renameDelta = new RenameDelta();
-				renameDelta.addOperation( new RenameOperation(
-					new Position( root, [ 3, 3, 3 ] ), 'h2', 'li', baseVersion
-				) );
-
-				const transformed = transform( splitDelta, renameDelta, context );
-
-				baseVersion = renameDelta.operations.length;
-
-				expect( transformed.length ).to.equal( 2 );
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: new Position( root, [ 3, 3, 4 ] ),
-							baseVersion
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 3, 3 ] ),
-							howMany: 9,
-							targetPosition: new Position( root, [ 3, 3, 4, 0 ] ),
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-
-				expectDelta( transformed[ 1 ], {
-					type: RenameDelta,
-					operations: [
-						{
-							type: RenameOperation,
-							position: new Position( root, [ 3, 3, 4 ] ),
-							oldName: 'p', // `oldName` taken from SplitDelta.
-							newName: 'li',
-							baseVersion: baseVersion + 2
-						}
-					]
-				} );
-			} );
-
-			it( 'split element is different than renamed element', () => {
-				const renameDelta = new RenameDelta();
-				renameDelta.addOperation( new RenameOperation(
-					new Position( root, [ 4 ] ), 'p', 'li', baseVersion
-				) );
-
-				const transformed = transform( splitDelta, renameDelta, context );
-
-				baseVersion = renameDelta.operations.length;
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: new Position( root, [ 3, 3, 4 ] ),
-							baseVersion
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 3, 3 ] ),
-							howMany: 9,
-							targetPosition: new Position( root, [ 3, 3, 4, 0 ] ),
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-			} );
-
-			it( 'renamed split element that is reinserted from graveyard', () => {
-				splitDelta.operations[ 0 ] = new ReinsertOperation(
-					new Position( gy, [ 1 ] ),
-					1,
-					new Position( root, [ 3, 3, 4 ] ),
-					baseVersion
-				);
-
-				const renameDelta = new RenameDelta();
-				renameDelta.addOperation( new RenameOperation(
-					new Position( root, [ 3, 3, 3 ] ), 'p', 'li', baseVersion
-				) );
-
-				context.undoMode = true;
-				const transformed = transform( splitDelta, renameDelta, context );
-
-				baseVersion = renameDelta.operations.length;
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							type: ReinsertOperation,
-							sourcePosition: new Position( gy, [ 1 ] ),
-							howMany: 1,
-							targetPosition: new Position( root, [ 3, 3, 4 ] ),
-							baseVersion
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 3, 3 ] ),
-							howMany: 9,
-							targetPosition: new Position( root, [ 3, 3, 4, 0 ] ),
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-			} );
-
-			it( 'should not throw if clone operation is NoOperation and use default transformation in that case', () => {
-				const noOpSplitDelta = new SplitDelta();
-				noOpSplitDelta.addOperation( new NoOperation( 0 ) );
-				noOpSplitDelta.addOperation( new MoveOperation( new Position( root, [ 1, 2 ] ), 3, new Position( root, [ 2, 0 ] ), 1 ) );
-
-				const renameDelta = new RenameDelta();
-				renameDelta.addOperation( new RenameOperation(
-					new Position( root, [ 1 ] ),
-					'p',
-					'li',
-					baseVersion
-				) );
-
-				const transformed = transform( noOpSplitDelta, renameDelta, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							type: NoOperation,
-							baseVersion: 1
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 1, 2 ] ),
-							howMany: 3,
-							targetPosition: new Position( root, [ 2, 0 ] ),
-							baseVersion: 2
-						}
-					]
-				} );
-			} );
-		} );
-
-		describe( 'RemoveDelta', () => {
-			it( 'node inside the removed range was a node that has been split', () => {
-				splitPosition = new Position( root, [ 3, 3, 2, 2 ] );
-				splitDelta = getSplitDelta( splitPosition, new Element( 'x' ), 2, baseVersion );
-
-				const removePosition = new Position( root, [ 3, 3, 1 ] );
-				const removeDelta = getRemoveDelta( removePosition, 3, baseVersion );
-				const removeOperation = removeDelta.operations[ 0 ];
-
-				const transformed = transform( splitDelta, removeDelta, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				baseVersion = removeDelta.operations.length;
-
-				const newInsertPosition = removeOperation.targetPosition.getShiftedBy( 2 );
-				const newMoveSourcePosition = removeOperation.targetPosition.getShiftedBy( 1 );
-				newMoveSourcePosition.path.push( 2 );
-				const newMoveTargetPosition = Position.createAt( newInsertPosition );
-				newMoveTargetPosition.path.push( 0 );
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: newInsertPosition,
-							baseVersion
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: newMoveSourcePosition,
-							howMany: 2,
-							targetPosition: newMoveTargetPosition,
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-			} );
-
-			it( 'last node in the removed range was a node that has been split', () => {
-				const removePosition = new Position( root, [ 3, 3, 2 ] );
-				const removeDelta = getRemoveDelta( removePosition, 2, baseVersion );
-				const removeOperation = removeDelta.operations[ 0 ];
-
-				const transformed = transform( splitDelta, removeDelta, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				baseVersion = removeDelta.operations.length;
-
-				const newInsertPosition = removeOperation.targetPosition.getShiftedBy( 2 );
-				const newMoveSourcePosition = removeOperation.targetPosition.getShiftedBy( 1 );
-				newMoveSourcePosition.path.push( 3 );
-				const newMoveTargetPosition = Position.createAt( newInsertPosition );
-				newMoveTargetPosition.path.push( 0 );
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: newInsertPosition,
-							baseVersion
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: newMoveSourcePosition,
-							howMany: 9,
-							targetPosition: newMoveTargetPosition,
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-			} );
-
-			it( 'split node has been removed - undo context', () => {
-				const removePosition = new Position( root, [ 3, 3, 3 ] );
-				const removeDelta = getRemoveDelta( removePosition, 1, baseVersion );
-
-				context.undoMode = true;
-
-				const transformed = transform( splitDelta, removeDelta, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				baseVersion = removeDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: splitDelta.operations[ 0 ].position.getShiftedBy( -1 ),
-							baseVersion
-						},
-						{
-							type: ReinsertOperation,
-							sourcePosition: new Position( gy, [ 0, 3 ] ),
-							howMany: splitDelta.operations[ 1 ].howMany,
-							targetPosition: new Position( root, [ 3, 3, 3, 0 ] ),
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-			} );
-
-			it( 'should not throw if clone operation is NoOperation and use default transformation in that case', () => {
-				const noOpSplitDelta = new SplitDelta();
-				noOpSplitDelta.addOperation( new NoOperation( 0 ) );
-				noOpSplitDelta.addOperation( new MoveOperation( new Position( root, [ 1, 2 ] ), 3, new Position( root, [ 2, 0 ] ), 1 ) );
-
-				const removeDelta = getRemoveDelta( new Position( root, [ 0 ] ), 1, 0 );
-
-				const transformed = transform( noOpSplitDelta, removeDelta, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				expectDelta( transformed[ 0 ], {
-					type: SplitDelta,
-					operations: [
-						{
-							type: NoOperation,
-							baseVersion: 1
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 0, 2 ] ),
-							howMany: 3,
-							targetPosition: new Position( root, [ 1, 0 ] ),
-							baseVersion: 2
-						}
-					]
-				} );
-			} );
-		} );
-	} );
-} );

+ 0 - 569
packages/ckeditor5-engine/tests/model/delta/transform/transform.js

@@ -1,569 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import transformations from '../../../../src/model/delta/basic-transformations'; // eslint-disable-line no-unused-vars
-import deltaTransform from '../../../../src/model/delta/transform';
-const transformDeltaSets = deltaTransform.transformDeltaSets;
-
-import Model from '../../../../src/model/model';
-import Element from '../../../../src/model/element';
-import Text from '../../../../src/model/text';
-import Position from '../../../../src/model/position';
-
-import Delta from '../../../../src/model/delta/delta';
-import InsertDelta from '../../../../src/model/delta/insertdelta';
-import RemoveDelta from '../../../../src/model/delta/removedelta';
-import MoveDelta from '../../../../src/model/delta/movedelta';
-import SplitDelta from '../../../../src/model/delta/splitdelta';
-import UnwrapDelta from '../../../../src/model/delta/unwrapdelta';
-
-import NoOperation from '../../../../src/model/operation/nooperation';
-import MoveOperation from '../../../../src/model/operation/moveoperation';
-import RemoveOperation from '../../../../src/model/operation/removeoperation';
-import ReinsertOperation from '../../../../src/model/operation/reinsertoperation';
-import InsertOperation from '../../../../src/model/operation/insertoperation';
-
-import {
-	expectDelta,
-	getInsertDelta,
-	getSplitDelta,
-	getRemoveDelta,
-	getMoveDelta,
-	getMergeDelta,
-	getUnwrapDelta
-} from '../../../../tests/model/delta/transform/_utils/utils';
-
-describe( 'transform', () => {
-	let doc, root, baseVersion;
-
-	beforeEach( () => {
-		const model = new Model();
-
-		doc = model.document;
-		root = doc.createRoot();
-
-		root._appendChild( new Element( 'p', null, new Text( 'foobar' ) ) );
-
-		baseVersion = doc.version;
-	} );
-
-	describe( 'transformDeltaSets', () => {
-		it( 'should use deltaTransform.transform', () => {
-			sinon.spy( deltaTransform, 'transform' );
-
-			const insertDelta = getInsertDelta( new Position( root, [ 0, 4 ] ), new Text( 'xxx' ), baseVersion );
-			const removeDelta = getRemoveDelta( new Position( root, [ 0, 0 ] ), 2, baseVersion );
-
-			transformDeltaSets( [ insertDelta ], [ removeDelta ] );
-
-			expect( deltaTransform.transform.called ).to.be.true;
-
-			deltaTransform.transform.restore();
-		} );
-
-		it( 'should transform two arrays of deltas', () => {
-			const splitDelta = getSplitDelta( new Position( root, [ 0, 3 ] ), new Element( 'p' ), 3, baseVersion );
-			const insertDeltaX = getInsertDelta( new Position( root, [ 0, 3 ] ), new Text( 'xxx' ), baseVersion + 2 );
-
-			const removeDelta = getRemoveDelta( new Position( root, [ 0, 2 ] ), 2, baseVersion );
-			const insertDeltaY = getInsertDelta( new Position( root, [ 0, 4 ] ), new Text( 'yyy' ), baseVersion + 1 );
-
-			const { deltasA, deltasB } = transformDeltaSets( [ splitDelta, insertDeltaX ], [ removeDelta, insertDeltaY ] );
-
-			expect( deltasA.length ).to.equal( 3 );
-			expect( deltasB.length ).to.equal( 3 );
-
-			expectDelta( deltasA[ 0 ], {
-				type: SplitDelta,
-				operations: [
-					{
-						type: InsertOperation,
-						position: new Position( root, [ 1 ] ),
-						baseVersion: 2
-					},
-					{
-						type: MoveOperation,
-						sourcePosition: new Position( root, [ 0, 2 ] ),
-						howMany: 5,
-						targetPosition: new Position( root, [ 1, 0 ] ),
-						baseVersion: 3
-					}
-				]
-			} );
-
-			expectDelta( deltasA[ 1 ], {
-				type: InsertDelta,
-				operations: [
-					{
-						type: InsertOperation,
-						position: new Position( root, [ 0, 2 ] ),
-						baseVersion: 4
-					}
-				]
-			} );
-
-			expectDelta( deltasA[ 2 ], {
-				type: Delta,
-				operations: [
-					{
-						type: NoOperation,
-						baseVersion: 5
-					}
-				]
-			} );
-
-			expectDelta( deltasB[ 0 ], {
-				type: RemoveDelta,
-				operations: [
-					{
-						type: RemoveOperation,
-						sourcePosition: new Position( root, [ 0, 2 ] ),
-						howMany: 1,
-						baseVersion: 3
-					}
-				]
-			} );
-
-			expectDelta( deltasB[ 1 ], {
-				type: RemoveDelta,
-				operations: [
-					{
-						type: RemoveOperation,
-						sourcePosition: new Position( root, [ 1, 0 ] ),
-						howMany: 1,
-						baseVersion: 4
-					}
-				]
-			} );
-
-			expectDelta( deltasB[ 2 ], {
-				type: InsertDelta,
-				operations: [
-					{
-						type: InsertOperation,
-						position: new Position( root, [ 1, 2 ] ),
-						baseVersion: 5
-					}
-				]
-			} );
-		} );
-
-		it( 'should transform two arrays of deltas - reverse', () => {
-			const splitDelta = getSplitDelta( new Position( root, [ 0, 3 ] ), new Element( 'p' ), 3, baseVersion );
-			const insertDeltaX = getInsertDelta( new Position( root, [ 0, 3 ] ), new Text( 'xxx' ), baseVersion + 2 );
-
-			const removeDelta = getRemoveDelta( new Position( root, [ 0, 2 ] ), 2, baseVersion );
-			const insertDeltaY = getInsertDelta( new Position( root, [ 0, 4 ] ), new Text( 'yyy' ), baseVersion + 1 );
-
-			const { deltasA, deltasB } = transformDeltaSets( [ removeDelta, insertDeltaY ], [ splitDelta, insertDeltaX ] );
-
-			expect( deltasA.length ).to.equal( 3 );
-			expect( deltasB.length ).to.equal( 3 );
-
-			expectDelta( deltasA[ 0 ], {
-				type: RemoveDelta,
-				operations: [
-					{
-						type: RemoveOperation,
-						sourcePosition: new Position( root, [ 0, 2 ] ),
-						howMany: 1,
-						baseVersion: 3
-					}
-				]
-			} );
-
-			expectDelta( deltasA[ 1 ], {
-				type: RemoveDelta,
-				operations: [
-					{
-						type: RemoveOperation,
-						sourcePosition: new Position( root, [ 1, 0 ] ),
-						howMany: 1,
-						baseVersion: 4
-					}
-				]
-			} );
-
-			expectDelta( deltasA[ 2 ], {
-				type: InsertDelta,
-				operations: [
-					{
-						type: InsertOperation,
-						position: new Position( root, [ 1, 2 ] ),
-						baseVersion: 5
-					}
-				]
-			} );
-
-			expectDelta( deltasB[ 0 ], {
-				type: SplitDelta,
-				operations: [
-					{
-						type: InsertOperation,
-						position: new Position( root, [ 1 ] ),
-						baseVersion: 2
-					},
-					{
-						type: MoveOperation,
-						sourcePosition: new Position( root, [ 0, 2 ] ),
-						howMany: 5,
-						targetPosition: new Position( root, [ 1, 0 ] ),
-						baseVersion: 3
-					}
-				]
-			} );
-
-			expectDelta( deltasB[ 1 ], {
-				type: InsertDelta,
-				operations: [
-					{
-						type: InsertOperation,
-						position: new Position( root, [ 0, 2 ] ),
-						baseVersion: 4
-					}
-				]
-			} );
-
-			expectDelta( deltasB[ 2 ], {
-				type: Delta,
-				operations: [
-					{
-						type: NoOperation,
-						baseVersion: 5
-					}
-				]
-			} );
-		} );
-
-		it( 'should not modify original deltas or arrays', () => {
-			const insertDeltaA = getInsertDelta( new Position( root, [ 0, 0 ] ), new Text( 'x' ), baseVersion );
-			const insertDeltaB = getInsertDelta( new Position( root, [ 1, 0 ] ), new Text( 'y' ), baseVersion );
-
-			const originalDeltasA = [ insertDeltaA ];
-			const originalDeltasB = [ insertDeltaB ];
-
-			const { deltasA, deltasB } = transformDeltaSets( originalDeltasA, originalDeltasB );
-
-			expect( deltasA ).to.not.equal( originalDeltasA );
-			expect( deltasB ).to.not.equal( originalDeltasB );
-			expect( deltasA[ 0 ] ).to.not.equal( originalDeltasA[ 0 ] );
-			expect( deltasB[ 0 ] ).to.not.equal( originalDeltasB[ 0 ] );
-		} );
-
-		describe( 'context', () => {
-			it( 'first set is important', () => {
-				const insertDeltaA = getInsertDelta( new Position( root, [ 0, 4 ] ), new Text( 'xxx' ), baseVersion );
-				const insertDeltaB = getInsertDelta( new Position( root, [ 0, 4 ] ), new Text( 'yyy' ), baseVersion );
-
-				const { deltasA, deltasB } = transformDeltaSets( [ insertDeltaA ], [ insertDeltaB ] );
-
-				expectDelta( deltasA[ 0 ], {
-					type: InsertDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: new Position( root, [ 0, 4 ] ),
-							baseVersion: 1
-						}
-					]
-				} );
-
-				expectDelta( deltasB[ 0 ], {
-					type: InsertDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: new Position( root, [ 0, 7 ] ),
-							baseVersion: 1
-						}
-					]
-				} );
-			} );
-
-			it( 'remove delta is more important if additional context is not used', () => {
-				const moveDelta = getMoveDelta( new Position( root, [ 0, 4 ] ), 3, new Position( root, [ 1, 0 ] ), baseVersion );
-				const removeDelta = getRemoveDelta( new Position( root, [ 0, 4 ] ), 3, baseVersion );
-
-				const { deltasA, deltasB } = transformDeltaSets( [ moveDelta ], [ removeDelta ] );
-
-				expectDelta( deltasA[ 0 ], {
-					type: Delta,
-					operations: [
-						{
-							type: NoOperation,
-							baseVersion: 1
-						}
-					]
-				} );
-
-				expectDelta( deltasB[ 0 ], {
-					type: RemoveDelta,
-					operations: [
-						{
-							type: RemoveOperation,
-							sourcePosition: new Position( root, [ 1, 0 ] ),
-							howMany: 3,
-							targetPosition: new Position( doc.graveyard, [ 0 ] ),
-							baseVersion: 1
-						}
-					]
-				} );
-			} );
-
-			it( 'remove delta may be less important if additional context is used and the delta was undone', () => {
-				const moveDelta = getMoveDelta( new Position( root, [ 0, 4 ] ), 3, new Position( root, [ 1, 0 ] ), baseVersion );
-				const removeDelta = getRemoveDelta( new Position( root, [ 0, 4 ] ), 3, baseVersion );
-
-				// "Fake" delta undoing.
-				doc.history.setDeltaAsUndone( removeDelta, new Delta() );
-
-				const { deltasA, deltasB } = transformDeltaSets( [ moveDelta ], [ removeDelta ], doc );
-
-				expectDelta( deltasA[ 0 ], {
-					type: MoveDelta,
-					operations: [
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( doc.graveyard, [ 0 ] ),
-							howMany: 3,
-							targetPosition: new Position( root, [ 1, 0 ] ),
-							baseVersion: 1
-						}
-					]
-				} );
-
-				expectDelta( deltasB[ 0 ], {
-					type: Delta,
-					operations: [
-						{
-							type: NoOperation,
-							baseVersion: 1
-						}
-					]
-				} );
-			} );
-
-			// #1053.
-			it( 'remove operation importance should be set correctly also for deltas other than remove delta', () => {
-				const unwrapDelta = getUnwrapDelta( new Position( root, [ 0 ] ), 2, baseVersion );
-
-				const nodeCopy = new Element( 'p' );
-				const splitDelta = getSplitDelta( new Position( root, [ 0, 0, 1 ] ), nodeCopy, 1, baseVersion );
-				const mergeDelta = splitDelta.getReversed();
-
-				const { deltasA } = transformDeltaSets( [ unwrapDelta ], [ splitDelta, mergeDelta ], doc );
-
-				expect( deltasA.length ).to.equal( 2 );
-
-				expectDelta( deltasA[ 0 ], {
-					type: MoveDelta,
-					operations: [
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 0, 0 ] ),
-							howMany: 1,
-							targetPosition: new Position( root, [ 0 ] ),
-							baseVersion: baseVersion + 4
-						}
-					]
-				} );
-
-				expectDelta( deltasA[ 1 ], {
-					type: UnwrapDelta,
-					operations: [
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 1, 0 ] ),
-							howMany: 1,
-							targetPosition: new Position( root, [ 1 ] ),
-							baseVersion: baseVersion + 5
-						},
-						{
-							type: RemoveOperation,
-							sourcePosition: new Position( root, [ 2 ] ),
-							howMany: 1,
-							baseVersion: baseVersion + 6
-						}
-					]
-				} );
-			} );
-
-			it( 'should keep correct order of nodes if additional context is used (insert before)', () => {
-				// Assume document state: 'a^bcd'. User presses 'delete' key twice.
-				const removeDelta1 = getRemoveDelta( new Position( root, [ 1 ] ), 1, baseVersion ); // 'acd'.
-				const removeDelta2 = getRemoveDelta( new Position( root, [ 1 ] ), 1, baseVersion + 1 ); // 'ad'.
-				const reinsertDelta2 = removeDelta2.getReversed(); // 'acd'.
-
-				doc.history.addDelta( removeDelta1 );
-				doc.history.addDelta( removeDelta2 );
-				doc.history.addDelta( reinsertDelta2 );
-
-				doc.history.setDeltaAsUndone( removeDelta2, reinsertDelta2 );
-
-				// This should insert 'b' at offset 1.
-				// However this delta will be transformed by `reinsertDelta2` which also inserts a node at offset 1.
-				// This leads to a conflict and history needs to be check how deltas relate to each other.
-				// Compare this scenario to the scenario below.
-				const reinsertDelta1 = removeDelta1.getReversed();
-
-				const { deltasA } = transformDeltaSets( [ reinsertDelta1 ], [ removeDelta2, reinsertDelta2 ], doc );
-
-				expectDelta( deltasA[ 0 ], {
-					type: MoveDelta,
-					operations: [
-						{
-							type: ReinsertOperation,
-							targetPosition: new Position( root, [ 1 ] ),
-							howMany: 1,
-							baseVersion: 3
-						}
-					]
-				} );
-			} );
-
-			it( 'should keep correct order of nodes if additional context is used (insert after)', () => {
-				// Assume document state: 'abc^d'. User presses 'backspace' key twice.
-				const removeDelta1 = getRemoveDelta( new Position( root, [ 2 ] ), 1, baseVersion ); // 'abd'.
-				const removeDelta2 = getRemoveDelta( new Position( root, [ 1 ] ), 1, baseVersion + 1 ); // 'ad'.
-				const reinsertDelta2 = removeDelta2.getReversed(); // 'abd'.
-
-				doc.history.addDelta( removeDelta1 );
-				doc.history.addDelta( removeDelta2 );
-				doc.history.addDelta( reinsertDelta2 );
-
-				doc.history.setDeltaAsUndone( removeDelta2, reinsertDelta2 );
-
-				// This should insert 'c' at offset 2.
-				// However this delta will be first transformed by `removeDelta2`. After that transformation
-				// the delta will try to insert 'c' at offset 1. Then, it will be transformed by `reinsertDelta2`,
-				// which also tries to insert node at offset 1.
-				// This leads to a conflict and history needs to be check how deltas relate to each other.
-				// Compare this scenario to the scenario below.
-				// In both cases transformed delta was inserting something at the same position. However, in
-				// both cases the expected outcome is different.
-				const reinsertDelta1 = removeDelta1.getReversed();
-
-				const { deltasA } = transformDeltaSets( [ reinsertDelta1 ], [ removeDelta2, reinsertDelta2 ], doc );
-
-				expectDelta( deltasA[ 0 ], {
-					type: MoveDelta,
-					operations: [
-						{
-							type: ReinsertOperation,
-							targetPosition: new Position( root, [ 2 ] ),
-							howMany: 1,
-							baseVersion: 3
-						}
-					]
-				} );
-			} );
-
-			it( 'should not throw if delta transformed by undoing delta was not transformed by undone delta', () => {
-				// This scenario may happen when deltas are undone in different order than usual LIFO order.
-				const removeDelta1 = getRemoveDelta( new Position( root, [ 0 ] ), 1, baseVersion );
-				const removeDelta2 = getRemoveDelta( new Position( root, [ 2 ] ), 1, baseVersion + 1 );
-				const reinsertDelta1 = removeDelta1.getReversed();
-				reinsertDelta1.baseVersion = 2;
-
-				doc.history.addDelta( removeDelta1 );
-				doc.history.addDelta( removeDelta2 );
-				doc.history.addDelta( reinsertDelta1 );
-
-				doc.history.setDeltaAsUndone( removeDelta1, reinsertDelta1 );
-
-				const reinsertDelta2 = removeDelta2.getReversed();
-
-				const { deltasA } = transformDeltaSets( [ reinsertDelta2 ], [ reinsertDelta1 ], doc );
-
-				expectDelta( deltasA[ 0 ], {
-					type: MoveDelta,
-					operations: [
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 0 ] ),
-							targetPosition: new Position( root, [ 3 ] ),
-							howMany: 1,
-							baseVersion: 3
-						}
-					]
-				} );
-			} );
-
-			it( 'should use stickiness if additional context is not used', () => {
-				const moveDeltaA = getMoveDelta( new Position( root, [ 0, 0 ] ), 1, new Position( root, [ 1, 0 ] ), baseVersion );
-				const moveDeltaB = getMoveDelta( new Position( root, [ 1, 0 ] ), 3, new Position( root, [ 2, 2 ] ), baseVersion );
-				moveDeltaB.operations[ 0 ].isSticky = true;
-
-				const { deltasA } = transformDeltaSets( [ moveDeltaA ], [ moveDeltaB ], doc );
-
-				expectDelta( deltasA[ 0 ], {
-					type: MoveDelta,
-					operations: [
-						{
-							type: MoveOperation,
-							targetPosition: new Position( root, [ 2, 2 ] ),
-							howMany: 1,
-							baseVersion: 1
-						}
-					]
-				} );
-			} );
-
-			it( 'should discard stickiness if additional context is used and delta with sticky move operation has been undone', () => {
-				// Assume following data model:	<paragraph>ab</paragraph><paragraph>cd</paragraph>
-				// Then we remove 'b':			<paragraph>a</paragraph><paragraph>cd</paragraph>
-				const removeDelta = getRemoveDelta( new Position( root, [ 0, 1 ] ), 1, baseVersion );
-				// Then we merge paragraphs:	<paragraph>acd</paragraph>
-				// Keep in mind that merging has sticky move operation ('cd' is sticky-moved to the first paragraph).
-				const mergeDelta = getMergeDelta( new Position( root, [ 1 ] ), 2, 1, baseVersion + 1 );
-				// Then we reverse merging:		<paragraph>a</paragraph><paragraph>cd</paragraph>
-				// Keep in mind that now we have two deltas with sticky moves in a history.
-				const splitDelta = mergeDelta.getReversed();
-
-				doc.history.addDelta( removeDelta );
-				doc.history.addDelta( mergeDelta );
-				doc.history.addDelta( splitDelta );
-
-				doc.history.setDeltaAsUndone( mergeDelta, splitDelta );
-
-				// This delta should insert 'b' at position [ 0, 1 ].
-				// However, without context data, after transforming `reinsertDelta` by `splitDelta`,
-				// 'b' would "go away" together with 'cd'.
-				const reinsertDelta = removeDelta.getReversed();
-
-				const { deltasA } = transformDeltaSets( [ reinsertDelta ], [ mergeDelta, splitDelta ], doc );
-
-				expectDelta( deltasA[ 0 ], {
-					type: MoveDelta,
-					operations: [
-						{
-							type: ReinsertOperation,
-							targetPosition: new Position( root, [ 0, 1 ] ),
-							howMany: 1,
-							baseVersion: 5
-						}
-					]
-				} );
-			} );
-
-			it( 'should set `context.undoMode` in undo mode (when document is passed)', () => {
-				const deltaA = new Delta();
-				deltaA.addOperation( new NoOperation( 0 ) );
-				const deltaB = new Delta();
-				deltaB.addOperation( new NoOperation( 0 ) );
-
-				sinon.spy( deltaTransform, 'transform' );
-
-				transformDeltaSets( [ deltaA ], [ deltaB ], doc );
-
-				const contextAB = deltaTransform.transform.args[ 0 ][ 2 ];
-				const contextBA = deltaTransform.transform.args[ 1 ][ 2 ];
-
-				expect( contextAB.undoMode ).to.be.true;
-				expect( contextBA.undoMode ).to.be.true;
-			} );
-		} );
-	} );
-} );

+ 0 - 185
packages/ckeditor5-engine/tests/model/delta/transform/unwrapdelta.js

@@ -1,185 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import transformations from '../../../../src/model/delta/basic-transformations'; // eslint-disable-line no-unused-vars
-
-import deltaTransform from '../../../../src/model/delta/transform';
-const transform = deltaTransform.transform;
-
-import Element from '../../../../src/model/element';
-import Position from '../../../../src/model/position';
-import Range from '../../../../src/model/range';
-
-import MoveOperation from '../../../../src/model/operation/moveoperation';
-import RemoveOperation from '../../../../src/model/operation/removeoperation';
-import NoOperation from '../../../../src/model/operation/nooperation';
-
-import MergeDelta from '../../../../src/model/delta/mergedelta';
-import UnwrapDelta from '../../../../src/model/delta/unwrapdelta';
-
-import { getNodesAndText } from '../../../../tests/model/_utils/utils';
-
-import {
-	applyDelta,
-	expectDelta,
-	getFilledDocument,
-	getSplitDelta,
-	getUnwrapDelta
-} from '../../../../tests/model/delta/transform/_utils/utils';
-
-describe( 'transform', () => {
-	let doc, root, gy, baseVersion, context;
-
-	beforeEach( () => {
-		doc = getFilledDocument();
-		root = doc.getRoot();
-		gy = doc.graveyard;
-		baseVersion = doc.version;
-		context = { isStrong: false };
-	} );
-
-	describe( 'UnwrapDelta by', () => {
-		let unwrapDelta;
-
-		beforeEach( () => {
-			unwrapDelta = getUnwrapDelta( new Position( root, [ 3, 3, 3 ] ), 12, baseVersion );
-		} );
-
-		describe( 'SplitDelta', () => {
-			it( 'split position directly in unwrapped node', () => {
-				const splitPosition = new Position( root, [ 3, 3, 3, 3 ] );
-				const splitDelta = getSplitDelta( splitPosition, new Element( 'p' ), 9, baseVersion );
-
-				const transformed = transform( unwrapDelta, splitDelta, context );
-
-				expect( transformed.length ).to.equal( 2 );
-
-				baseVersion = splitDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: MergeDelta,
-					operations: [
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 4, 0 ] ),
-							howMany: 9,
-							targetPosition: new Position( root, [ 3, 3, 3, 3 ] ),
-							baseVersion
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 4 ] ),
-							howMany: 1,
-							targetPosition: new Position( gy, [ 0 ] ),
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-
-				expectDelta( transformed[ 1 ], {
-					type: UnwrapDelta,
-					operations: [
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 3, 0 ] ),
-							howMany: 12,
-							targetPosition: new Position( root, [ 3, 3, 3 ] ),
-							baseVersion: baseVersion + 2
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 15 ] ),
-							howMany: 1,
-							targetPosition: new Position( gy, [ 0 ] ),
-							baseVersion: baseVersion + 3
-						}
-					]
-				} );
-
-				// Test if deltas do what they should after applying transformed delta.
-				applyDelta( splitDelta, doc );
-				applyDelta( transformed[ 0 ], doc );
-				applyDelta( transformed[ 1 ], doc );
-
-				const nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3 ] ), 1 ) );
-
-				// UnwrapDelta is applied. SplitDelta is discarded.
-				expect( nodesAndText ).to.equal( 'DIVXXXXXabcdXabcfoobarxyzDIV' );
-			} );
-
-			it( 'split position before unwrapped node', () => {
-				const splitPosition = new Position( root, [ 3, 3, 3 ] );
-				const splitDelta = getSplitDelta( splitPosition, new Element( 'div' ), 1, baseVersion );
-
-				const transformed = transform( unwrapDelta, splitDelta, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				baseVersion = splitDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: UnwrapDelta,
-					operations: [
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 4, 0, 0 ] ),
-							howMany: 12,
-							targetPosition: new Position( root, [ 3, 4, 0 ] ),
-							baseVersion
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 4, 12 ] ),
-							howMany: 1,
-							targetPosition: new Position( gy, [ 0 ] ),
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-
-				// Test if deltas do what they should after applying transformed delta.
-				applyDelta( splitDelta, doc );
-				applyDelta( transformed[ 0 ], doc );
-
-				const nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3 ] ), 2 ) );
-
-				// UnwrapDelta and SplitDelta are applied.
-				expect( nodesAndText ).to.equal( 'DIVXXXXXabcdXDIVDIVabcfoobarxyzDIV' );
-			} );
-
-			it( 'should use default algorithm and not throw if split delta has NoOperation', () => {
-				const splitPosition = new Position( root, [ 3, 3, 2, 1 ] );
-				const splitDelta = getSplitDelta( splitPosition, new Element( 'div' ), 1, baseVersion );
-				splitDelta.operations[ 1 ] = new NoOperation( 1 );
-
-				const transformed = transform( unwrapDelta, splitDelta, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				baseVersion = splitDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: UnwrapDelta,
-					operations: [
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 4, 0 ] ),
-							howMany: 12,
-							targetPosition: new Position( root, [ 3, 3, 4 ] ),
-							baseVersion
-						},
-						{
-							type: RemoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 16 ] ),
-							howMany: 1,
-							targetPosition: new Position( gy, [ 0 ] ),
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-			} );
-		} );
-	} );
-} );

+ 0 - 132
packages/ckeditor5-engine/tests/model/delta/transform/weakinsertdelta.js

@@ -1,132 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import transformations from '../../../../src/model/delta/basic-transformations'; // eslint-disable-line no-unused-vars
-
-import deltaTransform from '../../../../src/model/delta/transform';
-const transform = deltaTransform.transform;
-
-import Text from '../../../../src/model/text';
-import Position from '../../../../src/model/position';
-import Range from '../../../../src/model/range';
-
-import WeakInsertDelta from '../../../../src/model/delta/weakinsertdelta';
-import AttributeDelta from '../../../../src/model/delta/attributedelta';
-
-import InsertOperation from '../../../../src/model/operation/insertoperation';
-import AttributeOperation from '../../../../src/model/operation/attributeoperation';
-
-import {
-	expectDelta,
-	getFilledDocument,
-	getAttributeDelta,
-	getWeakInsertDelta
-} from '../../../../tests/model/delta/transform/_utils/utils';
-
-describe( 'transform', () => {
-	let doc, root, baseVersion, context;
-
-	beforeEach( () => {
-		doc = getFilledDocument();
-		root = doc.getRoot();
-		baseVersion = doc.version;
-		context = { isStrong: false };
-	} );
-
-	describe( 'AttributeDelta by', () => {
-		let insertDelta;
-
-		beforeEach( () => {
-			const insertPosition = new Position( root, [ 3, 3, 0 ] );
-			insertDelta = getWeakInsertDelta(
-				insertPosition,
-				[
-					'a',
-					new Text( 'b', { key: 'new' } ),
-					new Text( 'c', { key: 'different' } ),
-					new Text( 'c', { key: 'different', key2: true } ),
-					'de'
-				],
-				baseVersion
-			);
-		} );
-
-		describe( 'WeakInsertDelta', () => {
-			it( 'weak insert inside attribute range should "fix" splitting the range', () => {
-				const attrRange = new Range( new Position( root, [ 3, 2 ] ), new Position( root, [ 3, 3, 3, 9 ] ) );
-				const attrDelta = getAttributeDelta( attrRange, 'key', 'old', 'new', baseVersion );
-
-				const transformed = transform( insertDelta, attrDelta, context );
-
-				expect( transformed.length ).to.equal( 2 );
-
-				baseVersion = attrDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: WeakInsertDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: new Position( root, [ 3, 3, 0 ] ),
-							baseVersion
-						}
-					]
-				} );
-
-				expectDelta( transformed[ 1 ], {
-					type: AttributeDelta,
-					operations: [
-						{
-							type: AttributeOperation,
-							range: new Range( new Position( root, [ 3, 3, 0 ] ), new Position( root, [ 3, 3, 1 ] ) ),
-							key: 'key',
-							oldValue: null,
-							newValue: 'new',
-							baseVersion: baseVersion + 1
-						},
-						{
-							type: AttributeOperation,
-							range: new Range( new Position( root, [ 3, 3, 2 ] ), new Position( root, [ 3, 3, 4 ] ) ),
-							key: 'key',
-							oldValue: 'different',
-							newValue: 'new',
-							baseVersion: baseVersion + 2
-						},
-						{
-							type: AttributeOperation,
-							range: new Range( new Position( root, [ 3, 3, 4 ] ), new Position( root, [ 3, 3, 6 ] ) ),
-							key: 'key',
-							oldValue: null,
-							newValue: 'new',
-							baseVersion: baseVersion + 3
-						}
-					]
-				} );
-			} );
-
-			it( 'should be normally transformed if weak insert is not in the attribute range', () => {
-				const attrRange = new Range( new Position( root, [ 5 ] ), new Position( root, [ 7 ] ) );
-				const attrDelta = getAttributeDelta( attrRange, 'key', 'old', 'new', baseVersion );
-
-				const transformed = transform( insertDelta, attrDelta, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				baseVersion = attrDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: WeakInsertDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: new Position( root, [ 3, 3, 0 ] ),
-							baseVersion
-						}
-					]
-				} );
-			} );
-		} );
-	} );
-} );

+ 0 - 226
packages/ckeditor5-engine/tests/model/delta/transform/wrapdelta.js

@@ -1,226 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import transformations from '../../../../src/model/delta/basic-transformations'; // eslint-disable-line no-unused-vars
-
-import deltaTransform from '../../../../src/model/delta/transform';
-const transform = deltaTransform.transform;
-
-import Element from '../../../../src/model/element';
-import Position from '../../../../src/model/position';
-import Range from '../../../../src/model/range';
-
-import MoveOperation from '../../../../src/model/operation/moveoperation';
-import InsertOperation from '../../../../src/model/operation/insertoperation';
-import NoOperation from '../../../../src/model/operation/nooperation';
-
-import MergeDelta from '../../../../src/model/delta/mergedelta';
-import WrapDelta from '../../../../src/model/delta/wrapdelta';
-
-import { getNodesAndText } from '../../../../tests/model/_utils/utils';
-
-import {
-	applyDelta,
-	expectDelta,
-	getFilledDocument,
-	getSplitDelta,
-	getWrapDelta
-} from '../../../../tests/model/delta/transform/_utils/utils';
-
-describe( 'transform', () => {
-	let doc, root, gy, baseVersion, context;
-
-	beforeEach( () => {
-		doc = getFilledDocument();
-		root = doc.getRoot();
-		gy = doc.graveyard;
-		baseVersion = doc.version;
-		context = { isStrong: false };
-	} );
-
-	describe( 'WrapDelta by', () => {
-		let wrapDelta;
-
-		beforeEach( () => {
-			const wrapRange = new Range( new Position( root, [ 3, 3, 3, 1 ] ), new Position( root, [ 3, 3, 3, 5 ] ) );
-			const wrapElement = new Element( 'E' );
-
-			wrapDelta = getWrapDelta( wrapRange, wrapElement, baseVersion );
-		} );
-
-		describe( 'SplitDelta', () => {
-			it( 'split position is between wrapped nodes', () => {
-				const splitPosition = new Position( root, [ 3, 3, 3, 3 ] );
-				const splitDelta = getSplitDelta( splitPosition, new Element( 'p' ), 9, baseVersion );
-
-				const transformed = transform( wrapDelta, splitDelta, context );
-
-				expect( transformed.length ).to.equal( 2 );
-
-				baseVersion = splitDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: MergeDelta,
-					operations: [
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 4, 0 ] ),
-							howMany: 9,
-							targetPosition: new Position( root, [ 3, 3, 3, 3 ] ),
-							baseVersion
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 4 ] ),
-							howMany: 1,
-							targetPosition: new Position( gy, [ 0 ] ),
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-
-				expectDelta( transformed[ 1 ], {
-					type: WrapDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: new Position( root, [ 3, 3, 3, 5 ] ),
-							baseVersion: baseVersion + 2
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 3, 1 ] ),
-							howMany: 4,
-							targetPosition: new Position( root, [ 3, 3, 3, 5, 0 ] ),
-							baseVersion: baseVersion + 3
-						}
-					]
-				} );
-
-				// Test if deltas do what they should after applying transformed delta.
-				applyDelta( splitDelta, doc );
-				applyDelta( transformed[ 0 ], doc );
-				applyDelta( transformed[ 1 ], doc );
-
-				const nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 3 ] ), 1 ) );
-
-				// WrapDelta is applied. SplitDelta is discarded.
-				expect( nodesAndText ).to.equal( 'PaEbcfoEobarxyzP' );
-			} );
-
-			it( 'split position is before wrapped nodes', () => {
-				const splitPosition = new Position( root, [ 3, 3, 3, 1 ] );
-				const splitDelta = getSplitDelta( splitPosition, new Element( 'p' ), 11, baseVersion );
-
-				const transformed = transform( wrapDelta, splitDelta, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				baseVersion = wrapDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: WrapDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: new Position( root, [ 3, 3, 4, 4 ] ),
-							baseVersion
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 4, 0 ] ),
-							howMany: 4,
-							targetPosition: new Position( root, [ 3, 3, 4, 4, 0 ] ),
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-
-				// Test if deltas do what they should after applying transformed delta.
-				applyDelta( splitDelta, doc );
-				applyDelta( transformed[ 0 ], doc );
-
-				const nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 3 ] ), 2 ) );
-
-				// WrapDelta and SplitDelta are correctly applied.
-				expect( nodesAndText ).to.equal( 'PaPPEbcfoEobarxyzP' );
-			} );
-
-			it( 'split position is inside wrapped node', () => {
-				// For this case, we need different WrapDelta so it is overwritten.
-				const wrapRange = new Range( new Position( root, [ 3, 3, 2 ] ), new Position( root, [ 3, 3, 4 ] ) );
-				const wrapElement = new Element( 'E' );
-
-				wrapDelta = getWrapDelta( wrapRange, wrapElement, baseVersion );
-
-				const splitPosition = new Position( root, [ 3, 3, 3, 3 ] );
-				const splitDelta = getSplitDelta( splitPosition, new Element( 'p' ), 9, baseVersion );
-
-				const transformed = transform( wrapDelta, splitDelta, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				baseVersion = wrapDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: WrapDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: new Position( root, [ 3, 3, 5 ] ),
-							baseVersion
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 2 ] ),
-							howMany: 3,
-							targetPosition: new Position( root, [ 3, 3, 5, 0 ] ),
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-
-				// Test if deltas do what they should after applying transformed delta.
-				applyDelta( splitDelta, doc );
-				applyDelta( transformed[ 0 ], doc );
-
-				const nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 2 ] ), 1 ) );
-
-				// WrapDelta and SplitDelta are correctly applied.
-				expect( nodesAndText ).to.equal( 'EXabcdXPabcPPfoobarxyzPE' );
-			} );
-
-			it( 'should use default algorithm and not throw if split delta has NoOperation', () => {
-				const splitPosition = new Position( root, [ 3, 3, 2, 1 ] );
-				const splitDelta = getSplitDelta( splitPosition, new Element( 'p' ), 11, baseVersion );
-				splitDelta.operations[ 1 ] = new NoOperation( 1 );
-
-				const transformed = transform( wrapDelta, splitDelta, context );
-
-				expect( transformed.length ).to.equal( 1 );
-
-				baseVersion = wrapDelta.operations.length;
-
-				expectDelta( transformed[ 0 ], {
-					type: WrapDelta,
-					operations: [
-						{
-							type: InsertOperation,
-							position: new Position( root, [ 3, 3, 4, 5 ] ),
-							baseVersion
-						},
-						{
-							type: MoveOperation,
-							sourcePosition: new Position( root, [ 3, 3, 4, 1 ] ),
-							howMany: 4,
-							targetPosition: new Position( root, [ 3, 3, 4, 5, 0 ] ),
-							baseVersion: baseVersion + 1
-						}
-					]
-				} );
-			} );
-		} );
-	} );
-} );

+ 0 - 87
packages/ckeditor5-engine/tests/model/delta/unwrapdelta.js

@@ -1,87 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import Model from '../../../src/model/model';
-import Position from '../../../src/model/position';
-
-import UnwrapDelta from '../../../src/model/delta/unwrapdelta';
-import WrapDelta from '../../../src/model/delta/wrapdelta';
-
-import MoveOperation from '../../../src/model/operation/moveoperation';
-import RemoveOperation from '../../../src/model/operation/removeoperation';
-import ReinsertOperation from '../../../src/model/operation/reinsertoperation';
-
-describe( 'UnwrapDelta', () => {
-	let unwrapDelta, doc, root;
-
-	beforeEach( () => {
-		const model = new Model();
-
-		doc = model.document;
-		root = doc.createRoot();
-		unwrapDelta = new UnwrapDelta();
-	} );
-
-	describe( 'constructor()', () => {
-		it( 'should create unwrap delta with no operations added', () => {
-			expect( unwrapDelta.operations.length ).to.equal( 0 );
-		} );
-	} );
-
-	describe( 'type', () => {
-		it( 'should be equal to unwrap', () => {
-			expect( unwrapDelta.type ).to.equal( 'unwrap' );
-		} );
-	} );
-
-	describe( 'position', () => {
-		it( 'should be null if there are no operations in delta', () => {
-			expect( unwrapDelta.position ).to.be.null;
-		} );
-
-		it( 'should be equal to the position before unwrapped node', () => {
-			unwrapDelta.operations.push( new MoveOperation( new Position( root, [ 1, 2, 0 ] ), 4, new Position( root, [ 1, 2 ] ) ) );
-			unwrapDelta.operations.push( new RemoveOperation( new Position( root, [ 1, 6 ] ), 1, new Position( doc.graveyard, [ 0 ] ) ) );
-
-			expect( unwrapDelta.position.root ).to.equal( root );
-			expect( unwrapDelta.position.path ).to.deep.equal( [ 1, 2 ] );
-		} );
-	} );
-
-	describe( 'getReversed', () => {
-		it( 'should return empty WrapDelta if there are no operations in delta', () => {
-			const reversed = unwrapDelta.getReversed();
-
-			expect( reversed ).to.be.instanceof( WrapDelta );
-			expect( reversed.operations.length ).to.equal( 0 );
-		} );
-
-		it( 'should return correct WrapDelta', () => {
-			unwrapDelta.operations.push( new MoveOperation( new Position( root, [ 1, 2, 0 ] ), 4, new Position( root, [ 1, 2 ] ) ) );
-			unwrapDelta.operations.push( new RemoveOperation( new Position( root, [ 1, 6 ] ), 1, new Position( doc.graveyard, [ 0 ] ) ) );
-
-			const reversed = unwrapDelta.getReversed();
-
-			expect( reversed ).to.be.instanceof( WrapDelta );
-			expect( reversed.operations.length ).to.equal( 2 );
-
-			// WrapDelta which is an effect of reversing UnwrapDelta has ReinsertOperation instead of InsertOperation.
-			// This is because we will "wrap" nodes into the element in which they were in the first place.
-			// That element has been removed so we reinsert it from the graveyard.
-			expect( reversed.operations[ 0 ] ).to.be.instanceof( ReinsertOperation );
-			expect( reversed.operations[ 0 ].howMany ).to.equal( 1 );
-			expect( reversed.operations[ 0 ].targetPosition.path ).to.deep.equal( [ 1, 6 ] );
-
-			expect( reversed.operations[ 1 ] ).to.be.instanceof( MoveOperation );
-			expect( reversed.operations[ 1 ].sourcePosition.path ).to.deep.equal( [ 1, 2 ] );
-			expect( reversed.operations[ 1 ].howMany ).to.equal( 4 );
-			expect( reversed.operations[ 1 ].targetPosition.path ).to.deep.equal( [ 1, 6, 0 ] );
-		} );
-	} );
-
-	it( 'should provide proper className', () => {
-		expect( UnwrapDelta.className ).to.equal( 'engine.model.delta.UnwrapDelta' );
-	} );
-} );

+ 0 - 12
packages/ckeditor5-engine/tests/model/delta/weakinsertdelta.js

@@ -1,12 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import WeakInsertDelta from '../../../src/model/delta/weakinsertdelta';
-
-describe( 'WeakInsertDelta', () => {
-	it( 'should provide proper className', () => {
-		expect( WeakInsertDelta.className ).to.equal( 'engine.model.delta.WeakInsertDelta' );
-	} );
-} );

+ 0 - 116
packages/ckeditor5-engine/tests/model/delta/wrapdelta.js

@@ -1,116 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import Model from '../../../src/model/model';
-import Position from '../../../src/model/position';
-import Element from '../../../src/model/element';
-
-import WrapDelta from '../../../src/model/delta/wrapdelta';
-import UnwrapDelta from '../../../src/model/delta/unwrapdelta';
-
-import InsertOperation from '../../../src/model/operation/insertoperation';
-import MoveOperation from '../../../src/model/operation/moveoperation';
-import RemoveOperation from '../../../src/model/operation/removeoperation';
-
-describe( 'WrapDelta', () => {
-	let wrapDelta, doc, root;
-
-	beforeEach( () => {
-		const model = new Model();
-
-		doc = model.document;
-		root = doc.createRoot();
-		wrapDelta = new WrapDelta();
-	} );
-
-	describe( 'constructor()', () => {
-		it( 'should create wrap delta with no operations added', () => {
-			expect( wrapDelta.operations.length ).to.equal( 0 );
-		} );
-	} );
-
-	describe( 'type', () => {
-		it( 'should be equal to wrap', () => {
-			expect( wrapDelta.type ).to.equal( 'wrap' );
-		} );
-	} );
-
-	describe( 'range', () => {
-		it( 'should be equal to null if there are no operations in delta', () => {
-			expect( wrapDelta.range ).to.be.null;
-		} );
-
-		it( 'should be equal to wrapped range', () => {
-			wrapDelta.operations.push( new InsertOperation( new Position( root, [ 1, 6 ] ), [], 1 ) );
-			wrapDelta.operations.push( new MoveOperation( new Position( root, [ 1, 1 ] ), 5, new Position( root, [ 1, 6, 0 ] ) ) );
-
-			expect( wrapDelta.range.start.isEqual( new Position( root, [ 1, 1 ] ) ) ).to.be.true;
-			expect( wrapDelta.range.end.isEqual( new Position( root, [ 1, 6 ] ) ) ).to.be.true;
-		} );
-	} );
-
-	describe( 'howMany', () => {
-		it( 'should be equal to 0 if there are no operations in delta', () => {
-			expect( wrapDelta.howMany ).to.equal( 0 );
-		} );
-
-		it( 'should be equal to the number of wrapped elements', () => {
-			const howMany = 5;
-
-			wrapDelta.operations.push( new InsertOperation( new Position( root, [ 1, 6 ] ), [], 1 ) );
-			wrapDelta.operations.push( new MoveOperation( new Position( root, [ 1, 1 ] ), howMany, new Position( root, [ 1, 6, 0 ] ) ) );
-
-			expect( wrapDelta.howMany ).to.equal( 5 );
-		} );
-	} );
-
-	describe( 'getReversed', () => {
-		it( 'should return empty UnwrapDelta if there are no operations in delta', () => {
-			const reversed = wrapDelta.getReversed();
-
-			expect( reversed ).to.be.instanceof( UnwrapDelta );
-			expect( reversed.operations.length ).to.equal( 0 );
-		} );
-
-		it( 'should return correct UnwrapDelta', () => {
-			wrapDelta.operations.push( new InsertOperation( new Position( root, [ 1, 6 ] ), new Element( 'p' ), 1 ) );
-			wrapDelta.operations.push( new MoveOperation( new Position( root, [ 1, 1 ] ), 5, new Position( root, [ 1, 6, 0 ] ) ) );
-
-			const reversed = wrapDelta.getReversed();
-
-			expect( reversed ).to.be.instanceof( UnwrapDelta );
-			expect( reversed.operations.length ).to.equal( 2 );
-
-			expect( reversed.operations[ 0 ] ).to.be.instanceof( MoveOperation );
-			expect( reversed.operations[ 0 ].sourcePosition.path ).to.deep.equal( [ 1, 1, 0 ] );
-			expect( reversed.operations[ 0 ].howMany ).to.equal( 5 );
-			expect( reversed.operations[ 0 ].targetPosition.path ).to.deep.equal( [ 1, 1 ] );
-
-			expect( reversed.operations[ 1 ] ).to.be.instanceof( RemoveOperation );
-			expect( reversed.operations[ 1 ].sourcePosition.path ).to.deep.equal( [ 1, 6 ] );
-			expect( reversed.operations[ 1 ].howMany ).to.equal( 1 );
-		} );
-	} );
-
-	describe( '_insertOperation', () => {
-		it( 'should be null if there are no operations in the delta', () => {
-			expect( wrapDelta._insertOperation ).to.be.null;
-		} );
-
-		it( 'should be equal to the first operation in the delta', () => {
-			const insertOperation = new InsertOperation( new Position( root, [ 1, 6 ] ), [], 1 );
-
-			wrapDelta.operations.push( insertOperation );
-			wrapDelta.operations.push( new MoveOperation( new Position( root, [ 1, 1 ] ), 5, new Position( root, [ 1, 6, 0 ] ) ) );
-
-			expect( wrapDelta._insertOperation ).to.equal( insertOperation );
-		} );
-	} );
-
-	it( 'should provide proper className', () => {
-		expect( WrapDelta.className ).to.equal( 'engine.model.delta.WrapDelta' );
-	} );
-} );
-

+ 17 - 27
packages/ckeditor5-engine/tests/model/differ.js

@@ -15,8 +15,6 @@ import MoveOperation from '../../src/model/operation/moveoperation';
 import RenameOperation from '../../src/model/operation/renameoperation';
 import AttributeOperation from '../../src/model/operation/attributeoperation';
 
-import { wrapInDelta } from '../../tests/model/_utils/utils';
-
 describe( 'Differ', () => {
 	let doc, differ, root, model;
 
@@ -169,16 +167,18 @@ describe( 'Differ', () => {
 		it( 'node in a element with changed attribute', () => {
 			const text = new Text( 'xyz', { bold: true } );
 			const position = new Position( root, [ 0, 3 ] );
-			const range = Range.createFromParentsAndOffsets( root, 0, root.getChild( 0 ), 0 );
+			const range = Range.createFromParentsAndOffsets( root, 0, root, 1 );
 
 			model.change( () => {
 				insert( text, position );
 				attribute( range, 'align', null, 'center' );
 
+				const diffRange = Range.createFromParentsAndOffsets( root, 0, root.getChild( 0 ), 0 );
+
 				// Compare to scenario above, this time there is only an attribute change on parent element,
 				// so there is also a diff for text.
 				expectChanges( [
-					{ type: 'attribute', range, attributeKey: 'align', attributeOldValue: null, attributeNewValue: 'center' },
+					{ type: 'attribute', range: diffRange, attributeKey: 'align', attributeOldValue: null, attributeNewValue: 'center' },
 					{ type: 'insert', name: '$text', length: 3, position },
 				] );
 			} );
@@ -697,19 +697,21 @@ describe( 'Differ', () => {
 		const attributeNewValue = 'foo';
 
 		it( 'on an element', () => {
-			const range = Range.createFromParentsAndOffsets( root, 0, root.getChild( 0 ), 0 );
+			const range = Range.createFromParentsAndOffsets( root, 0, root, 1 );
 
 			model.change( () => {
 				attribute( range, attributeKey, attributeOldValue, attributeNewValue );
 
+				const diffRange = Range.createFromParentsAndOffsets( root, 0, root.getChild( 0 ), 0 );
+
 				expectChanges( [
-					{ type: 'attribute', range, attributeKey, attributeOldValue, attributeNewValue }
+					{ type: 'attribute', range: diffRange, attributeKey, attributeOldValue, attributeNewValue }
 				] );
 			} );
 		} );
 
 		it( 'on an element - only one of many attributes changes', () => {
-			const range = Range.createFromParentsAndOffsets( root, 0, root.getChild( 0 ), 0 );
+			const range = Range.createFromParentsAndOffsets( root, 0, root, 1 );
 
 			model.change( () => {
 				// Set an attribute on an element. It won't change afterwards.
@@ -719,8 +721,10 @@ describe( 'Differ', () => {
 			model.change( () => {
 				attribute( range, attributeKey, attributeOldValue, attributeNewValue );
 
+				const diffRange = Range.createFromParentsAndOffsets( root, 0, root.getChild( 0 ), 0 );
+
 				expectChanges( [
-					{ type: 'attribute', range, attributeKey, attributeOldValue, attributeNewValue }
+					{ type: 'attribute', range: diffRange, attributeKey, attributeOldValue, attributeNewValue }
 				] );
 			} );
 		} );
@@ -953,26 +957,12 @@ describe( 'Differ', () => {
 						attributeOldValue,
 						attributeNewValue
 					},
-					{
-						type,
-						range: Range.createFromParentsAndOffsets( p1, 0, p1, 3 ),
-						attributeKey,
-						attributeOldValue,
-						attributeNewValue
-					},
 					{
 						type,
 						range: Range.createFromParentsAndOffsets( root, 1, p2, 0 ),
 						attributeKey,
 						attributeOldValue,
 						attributeNewValue
-					},
-					{
-						type,
-						range: Range.createFromParentsAndOffsets( p2, 0, p2, 3 ),
-						attributeKey,
-						attributeOldValue,
-						attributeNewValue
 					}
 				] );
 			} );
@@ -1622,32 +1612,32 @@ describe( 'Differ', () => {
 	function insert( item, position ) {
 		const operation = new InsertOperation( position, item, doc.version );
 
-		model.applyOperation( wrapInDelta( operation ) );
+		model.applyOperation( operation );
 	}
 
 	function remove( sourcePosition, howMany ) {
 		const targetPosition = Position.createAt( doc.graveyard, doc.graveyard.maxOffset );
 		const operation = new RemoveOperation( sourcePosition, howMany, targetPosition, doc.version );
 
-		model.applyOperation( wrapInDelta( operation ) );
+		model.applyOperation( operation );
 	}
 
 	function move( sourcePosition, howMany, targetPosition ) {
 		const operation = new MoveOperation( sourcePosition, howMany, targetPosition, doc.version );
 
-		model.applyOperation( wrapInDelta( operation ) );
+		model.applyOperation( operation );
 	}
 
 	function rename( element, newName ) {
 		const operation = new RenameOperation( Position.createBefore( element ), element.name, newName, doc.version );
 
-		model.applyOperation( wrapInDelta( operation ) );
+		model.applyOperation( operation );
 	}
 
 	function attribute( range, key, oldValue, newValue ) {
 		const operation = new AttributeOperation( range, key, oldValue, newValue, doc.version );
 
-		model.applyOperation( wrapInDelta( operation ) );
+		model.applyOperation( operation );
 	}
 
 	function expectChanges( expected, includeChangesInGraveyard = false ) {

+ 4 - 9
packages/ckeditor5-engine/tests/model/document.js

@@ -8,7 +8,6 @@ import Document from '../../src/model/document';
 import RootElement from '../../src/model/rootelement';
 import Text from '../../src/model/text';
 import Batch from '../../src/model/batch';
-import Delta from '../../src/model/delta/delta';
 import Range from '../../src/model/range';
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
@@ -37,7 +36,7 @@ describe( 'Document', () => {
 	} );
 
 	describe( 'model#applyOperation listener', () => {
-		let operation, data, delta, batch;
+		let operation, data, batch;
 
 		beforeEach( () => {
 			data = { data: 'x' };
@@ -50,19 +49,15 @@ describe( 'Document', () => {
 				_validate: () => {}
 			};
 
-			delta = new Delta();
-			delta.addOperation( operation );
-			delta.type = 'delta';
-
 			batch = new Batch();
-			batch.addDelta( delta );
+			batch.addOperation( operation );
 		} );
 
 		it( 'for document operation: should increase document version and execute operation', () => {
 			model.applyOperation( operation );
 
 			expect( doc.version ).to.equal( 1 );
-			expect( doc.history._deltas.length ).to.equal( 1 );
+			expect( doc.history._operations.length ).to.equal( 1 );
 			sinon.assert.calledOnce( operation._execute );
 		} );
 
@@ -72,7 +67,7 @@ describe( 'Document', () => {
 			model.applyOperation( operation );
 
 			expect( doc.version ).to.equal( 0 );
-			expect( doc.history._deltas.length ).to.equal( 0 );
+			expect( doc.history._operations.length ).to.equal( 0 );
 			sinon.assert.calledOnce( operation._execute );
 		} );
 

+ 40 - 78
packages/ckeditor5-engine/tests/model/documentselection.js

@@ -15,11 +15,10 @@ import InsertOperation from '../../src/model/operation/insertoperation';
 import MoveOperation from '../../src/model/operation/moveoperation';
 import RemoveOperation from '../../src/model/operation/removeoperation';
 import AttributeOperation from '../../src/model/operation/attributeoperation';
-import SplitDelta from '../../src/model/delta/splitdelta';
+import SplitOperation from '../../src/model/operation/splitoperation';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import count from '@ckeditor/ckeditor5-utils/src/count';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
-import { wrapInDelta } from '../../tests/model/_utils/utils';
 import { setData, getData } from '../../src/dev-utils/model';
 
 describe( 'DocumentSelection', () => {
@@ -613,7 +612,7 @@ describe( 'DocumentSelection', () => {
 
 				model.on( 'applyOperation', ( event, args ) => {
 					const operation = args[ 0 ];
-					const batch = operation.delta.batch;
+					const batch = operation.batch;
 
 					batchTypes.push( batch.type );
 				} );
@@ -638,7 +637,7 @@ describe( 'DocumentSelection', () => {
 
 				model.on( 'applyOperation', ( event, args ) => {
 					const operation = args[ 0 ];
-					const batch = operation.delta.batch;
+					const batch = operation.batch;
 
 					batchTypes.set( batch, batch.type );
 				} );
@@ -692,25 +691,6 @@ describe( 'DocumentSelection', () => {
 				expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.false;
 			} );
 
-			it( 'are removed only once in case of multi-op deltas', () => {
-				let batch;
-				const emptyP2 = new Element( 'p', null, 'x' );
-				root._appendChild( emptyP2 );
-
-				emptyP._setAttribute( fooStoreAttrKey, 'bar' );
-				emptyP2._setAttribute( fooStoreAttrKey, 'bar' );
-
-				model.change( writer => {
-					batch = writer.batch;
-					// <emptyP>{}<emptyP2>
-					writer.merge( Position.createAfter( emptyP ) );
-				} );
-
-				expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.false;
-				// Attribute delta is only one.
-				expect( Array.from( batch.deltas, delta => delta.type ) ).to.deep.equal( [ 'merge', 'attribute' ] );
-			} );
-
 			it( 'uses model change to clear attributes', () => {
 				selection._setTo( [ rangeInEmptyP ] );
 
@@ -745,7 +725,7 @@ describe( 'DocumentSelection', () => {
 				expect( emptyP.parent ).to.equal( root ); // Just to be sure we're checking the right element.
 			} );
 
-			// Rename and some other deltas don't specify range in doc#change event.
+			// Rename and some other operations don't specify range in doc#change event.
 			// So let's see if there's no crash or something.
 			it( 'are not removed on rename', () => {
 				model.change( writer => {
@@ -922,13 +902,13 @@ describe( 'DocumentSelection', () => {
 					expect( data.directChange ).to.be.false;
 				} );
 
-				model.applyOperation( wrapInDelta(
+				model.applyOperation(
 					new InsertOperation(
 						new Position( root, [ 0, 1 ] ),
 						'xyz',
 						doc.version
 					)
-				) );
+				);
 
 				const range = selection.getFirstRange();
 
@@ -942,13 +922,13 @@ describe( 'DocumentSelection', () => {
 					expect( data.directChange ).to.be.false;
 				} );
 
-				model.applyOperation( wrapInDelta(
+				model.applyOperation(
 					new InsertOperation(
 						new Position( root, [ 1, 0 ] ),
 						'xyz',
 						doc.version
 					)
-				) );
+				);
 
 				const range = selection.getFirstRange();
 
@@ -964,14 +944,14 @@ describe( 'DocumentSelection', () => {
 					expect( data.directChange ).to.be.false;
 				} );
 
-				model.applyOperation( wrapInDelta(
+				model.applyOperation(
 					new MoveOperation(
 						new Position( root, [ 0, 0 ] ),
 						2,
 						new Position( root, [ 2 ] ),
 						doc.version
 					)
-				) );
+				);
 
 				const range = selection.getFirstRange();
 
@@ -985,14 +965,14 @@ describe( 'DocumentSelection', () => {
 					expect( data.directChange ).to.be.false;
 				} );
 
-				model.applyOperation( wrapInDelta(
+				model.applyOperation(
 					new MoveOperation(
 						new Position( root, [ 2 ] ),
 						2,
 						new Position( root, [ 0, 0 ] ),
 						doc.version
 					)
-				) );
+				);
 
 				const range = selection.getFirstRange();
 
@@ -1006,14 +986,14 @@ describe( 'DocumentSelection', () => {
 					expect( data.directChange ).to.be.false;
 				} );
 
-				model.applyOperation( wrapInDelta(
+				model.applyOperation(
 					new MoveOperation(
 						new Position( root, [ 1, 0 ] ),
 						2,
 						new Position( root, [ 2 ] ),
 						doc.version
 					)
-				) );
+				);
 
 				const range = selection.getFirstRange();
 
@@ -1027,19 +1007,19 @@ describe( 'DocumentSelection', () => {
 					expect( data.directChange ).to.be.false;
 				} );
 
-				model.applyOperation( wrapInDelta(
+				model.applyOperation(
 					new MoveOperation(
 						new Position( root, [ 1, 3 ] ),
 						2,
 						new Position( root, [ 4 ] ),
 						doc.version
 					)
-				) );
+				);
 
 				const range = selection.getFirstRange();
 
 				expect( range.start.path ).to.deep.equal( [ 0, 2 ] );
-				expect( range.end.path ).to.deep.equal( [ 5 ] );
+				expect( range.end.path ).to.deep.equal( [ 1, 3 ] );
 				expect( spyRange.calledOnce ).to.be.true;
 			} );
 
@@ -1049,28 +1029,10 @@ describe( 'DocumentSelection', () => {
 				} );
 
 				const batch = new Batch();
-				const splitDelta = new SplitDelta();
+				const splitOperation = new SplitOperation( new Position( root, [ 1, 2 ] ), null, 0 );
 
-				const insertOperation = new InsertOperation(
-					new Position( root, [ 2 ] ),
-					new Element( 'p' ),
-					0
-				);
-
-				const moveOperation = new MoveOperation(
-					new Position( root, [ 1, 2 ] ),
-					4,
-					new Position( root, [ 2, 0 ] ),
-					1
-				);
-
-				batch.addDelta( splitDelta );
-
-				splitDelta.addOperation( insertOperation );
-				splitDelta.addOperation( moveOperation );
-
-				model.applyOperation( insertOperation );
-				model.applyOperation( moveOperation );
+				batch.addOperation( splitOperation );
+				model.applyOperation( splitOperation );
 
 				const range = selection.getFirstRange();
 
@@ -1090,7 +1052,7 @@ describe( 'DocumentSelection', () => {
 					expect( data.attributeKeys ).to.deep.equal( [ 'foo' ] );
 				} );
 
-				model.applyOperation( wrapInDelta(
+				model.applyOperation(
 					new AttributeOperation(
 						new Range( new Position( root, [ 0, 1 ] ), new Position( root, [ 0, 5 ] ) ),
 						'foo',
@@ -1098,7 +1060,7 @@ describe( 'DocumentSelection', () => {
 						'bar',
 						doc.version
 					)
-				) );
+				);
 
 				// Attributes are auto updated on document change.
 				model.change( () => {} );
@@ -1113,7 +1075,7 @@ describe( 'DocumentSelection', () => {
 				const spyAttribute = sinon.spy();
 				selection.on( 'change:attribute', spyAttribute );
 
-				model.applyOperation( wrapInDelta(
+				model.applyOperation(
 					new AttributeOperation(
 						new Range( new Position( root, [ 0, 1 ] ), new Position( root, [ 0, 5 ] ) ),
 						'foo',
@@ -1121,7 +1083,7 @@ describe( 'DocumentSelection', () => {
 						'bar',
 						doc.version
 					)
-				) );
+				);
 
 				expect( selection.getAttribute( 'foo' ) ).to.equal( 'xyz' );
 				expect( spyAttribute.called ).to.be.false;
@@ -1133,7 +1095,7 @@ describe( 'DocumentSelection', () => {
 				const spyAttribute = sinon.spy();
 				selection.on( 'change:attribute', spyAttribute );
 
-				model.applyOperation( wrapInDelta(
+				model.applyOperation(
 					new AttributeOperation(
 						new Range( new Position( root, [ 0, 1 ] ), new Position( root, [ 0, 5 ] ) ),
 						'foo',
@@ -1141,7 +1103,7 @@ describe( 'DocumentSelection', () => {
 						'xyz',
 						doc.version
 					)
-				) );
+				);
 
 				expect( selection.getAttribute( 'foo' ) ).to.equal( 'xyz' );
 				expect( spyAttribute.called ).to.be.false;
@@ -1154,7 +1116,7 @@ describe( 'DocumentSelection', () => {
 				const spyAttribute = sinon.spy();
 				selection.on( 'change:attribute', spyAttribute );
 
-				model.applyOperation( wrapInDelta(
+				model.applyOperation(
 					new AttributeOperation(
 						new Range( new Position( root, [ 0, 1 ] ), new Position( root, [ 0, 5 ] ) ),
 						'foo',
@@ -1162,7 +1124,7 @@ describe( 'DocumentSelection', () => {
 						'bar',
 						doc.version
 					)
-				) );
+				);
 
 				expect( selection.hasAttribute( 'foo' ) ).to.be.false;
 				expect( spyAttribute.called ).to.be.false;
@@ -1173,14 +1135,14 @@ describe( 'DocumentSelection', () => {
 			it( 'fix selection range if it ends up in graveyard #1', () => {
 				selection._setTo( new Position( root, [ 1, 3 ] ) );
 
-				model.applyOperation( wrapInDelta(
+				model.applyOperation(
 					new RemoveOperation(
 						new Position( root, [ 1, 2 ] ),
 						2,
 						new Position( doc.graveyard, [ 0 ] ),
 						doc.version
 					)
-				) );
+				);
 
 				expect( selection.getFirstPosition().path ).to.deep.equal( [ 1, 2 ] );
 			} );
@@ -1188,14 +1150,14 @@ describe( 'DocumentSelection', () => {
 			it( 'fix selection range if it ends up in graveyard #2', () => {
 				selection._setTo( [ new Range( new Position( root, [ 1, 2 ] ), new Position( root, [ 1, 4 ] ) ) ] );
 
-				model.applyOperation( wrapInDelta(
+				model.applyOperation(
 					new RemoveOperation(
 						new Position( root, [ 1, 2 ] ),
 						2,
 						new Position( doc.graveyard, [ 0 ] ),
 						doc.version
 					)
-				) );
+				);
 
 				expect( selection.getFirstPosition().path ).to.deep.equal( [ 1, 2 ] );
 			} );
@@ -1203,37 +1165,37 @@ describe( 'DocumentSelection', () => {
 			it( 'fix selection range if it ends up in graveyard #3', () => {
 				selection._setTo( [ new Range( new Position( root, [ 1, 1 ] ), new Position( root, [ 1, 2 ] ) ) ] );
 
-				model.applyOperation( wrapInDelta(
+				model.applyOperation(
 					new RemoveOperation(
 						new Position( root, [ 1 ] ),
 						2,
 						new Position( doc.graveyard, [ 0 ] ),
 						doc.version
 					)
-				) );
+				);
 
 				expect( selection.getFirstPosition().path ).to.deep.equal( [ 0, 6 ] );
 			} );
 
 			it( 'fix selection range if it ends up in graveyard #4 - whole content removed', () => {
-				model.applyOperation( wrapInDelta(
+				model.applyOperation(
 					new RemoveOperation(
 						new Position( root, [ 0 ] ),
 						3,
 						new Position( doc.graveyard, [ 0 ] ),
 						doc.version
 					)
-				) );
+				);
 
 				expect( selection.getFirstPosition().path ).to.deep.equal( [ 0 ] );
 
-				model.applyOperation( wrapInDelta(
+				model.applyOperation(
 					new InsertOperation(
 						new Position( root, [ 0 ] ),
 						new Element( 'p' ),
 						doc.version
 					)
-				) );
+				);
 
 				// Now it's clear that it's the default range.
 				expect( selection.getFirstPosition().path ).to.deep.equal( [ 0, 0 ] );
@@ -1256,13 +1218,13 @@ describe( 'DocumentSelection', () => {
 			spyRange = sinon.spy();
 			selection.on( 'change:range', spyRange );
 
-			model.applyOperation( wrapInDelta(
+			model.applyOperation(
 				new InsertOperation(
 					new Position( root, [ 0 ] ),
 					'xyz #1',
 					doc.version
 				)
-			) );
+			);
 
 			expect( spyRange.calledOnce ).to.be.true;
 		} );

+ 92 - 113
packages/ckeditor5-engine/tests/model/history.js

@@ -4,11 +4,8 @@
  */
 
 import History from '../../src/model/history';
-import Delta from '../../src/model/delta/delta';
 import Operation from '../../src/model/operation/operation';
 
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-
 describe( 'History', () => {
 	let history;
 
@@ -18,195 +15,177 @@ describe( 'History', () => {
 
 	describe( 'constructor()', () => {
 		it( 'should create an empty History instance', () => {
-			expect( Array.from( history.getDeltas() ).length ).to.equal( 0 );
+			expect( Array.from( history.getOperations() ).length ).to.equal( 0 );
 		} );
 	} );
 
-	describe( 'addDelta', () => {
-		it( 'should save delta in the history', () => {
-			const delta = new Delta();
-			delta.addOperation( new Operation( 0 ) );
+	describe( 'addOperation', () => {
+		it( 'should save operation in the history', () => {
+			const op = new Operation( 0 );
 
-			history.addDelta( delta );
+			history.addOperation( op );
 
-			const deltas = Array.from( history.getDeltas() );
-			expect( deltas.length ).to.equal( 1 );
-			expect( deltas[ 0 ] ).to.equal( delta );
+			const ops = Array.from( history.getOperations() );
+			expect( ops.length ).to.equal( 1 );
+			expect( ops[ 0 ] ).to.equal( op );
 		} );
 
-		it( 'should save each delta only once', () => {
-			const delta = new Delta();
-			delta.addOperation( new Operation( 0 ) );
+		it( 'should save each operation only once', () => {
+			const op = new Operation( 0 );
 
-			history.addDelta( delta );
-			history.addDelta( delta );
+			history.addOperation( op );
+			history.addOperation( op );
 
-			const deltas = Array.from( history.getDeltas() );
-			expect( deltas.length ).to.equal( 1 );
-			expect( deltas[ 0 ] ).to.equal( delta );
+			const ops = Array.from( history.getOperations() );
+			expect( ops.length ).to.equal( 1 );
+			expect( ops[ 0 ] ).to.equal( op );
 		} );
 
-		it( 'should save multiple deltas and keep their order', () => {
-			const deltas = getDeltaSet();
+		it( 'should save multiple operations and keep their order', () => {
+			const ops = getOperations();
 
-			for ( const delta of deltas ) {
-				history.addDelta( delta );
+			for ( const op of ops ) {
+				history.addOperation( op );
 			}
 
-			const historyDeltas = Array.from( history.getDeltas() );
-			expect( historyDeltas ).to.deep.equal( deltas );
+			const historyOperations = history.getOperations();
+			expect( historyOperations ).to.deep.equal( ops );
 		} );
+	} );
 
-		it( 'should skip deltas that does not have operations', () => {
-			const delta = new Delta();
+	describe( 'getOperation', () => {
+		it( 'should return operation with given base version', () => {
+			const op0 = new Operation( 0 );
+			const op1 = new Operation( 1 );
+			const op2 = new Operation( 2 );
 
-			history.addDelta( delta );
+			history.addOperation( op0 );
+			history.addOperation( op1 );
+			history.addOperation( op2 );
 
-			expect( Array.from( history.getDeltas() ).length ).to.equal( 0 );
+			const historyOperation = history.getOperation( 1 );
+			expect( historyOperation ).to.equal( op1 );
 		} );
-	} );
 
-	describe( 'getDelta', () => {
-		it( 'should return delta with given base version', () => {
-			const delta = getDelta( 0 );
-			history.addDelta( delta );
+		it( 'should return undefined if operation has not been found in history', () => {
+			const op0 = new Operation( 0 );
 
-			const historyDelta = history.getDelta( 0 );
-			expect( historyDelta ).to.equal( delta );
-		} );
+			history.addOperation( op0 );
 
-		it( 'should return null if delta has not been found in history', () => {
-			expect( history.getDelta( -1 ) ).to.be.null;
-			expect( history.getDelta( 2 ) ).to.be.null;
-			expect( history.getDelta( 20 ) ).to.be.null;
+			expect( history.getOperation( -1 ) ).to.be.undefined;
+			expect( history.getOperation( 10 ) ).to.be.undefined;
 		} );
 	} );
 
-	describe( 'getDeltas', () => {
-		let deltas;
+	describe( 'getOperations', () => {
+		let ops;
 
 		beforeEach( () => {
-			deltas = getDeltaSet();
+			ops = getOperations();
 
-			for ( const delta of deltas ) {
-				history.addDelta( delta );
+			for ( const op of ops ) {
+				history.addOperation( op );
 			}
 		} );
 
-		it( 'should return only history deltas from given base version', () => {
-			const historyDeltas = Array.from( history.getDeltas( 3 ) );
-			expect( historyDeltas ).to.deep.equal( deltas.slice( 1 ) );
-		} );
+		it( 'should return only operations from given base version', () => {
+			const historyOperations = Array.from( history.getOperations( 1 ) );
 
-		it( 'should return only history deltas to given base version', () => {
-			const historyDeltas = Array.from( history.getDeltas( 3, 6 ) );
-			expect( historyDeltas ).to.deep.equal( deltas.slice( 1, 2 ) );
+			expect( historyOperations ).to.deep.equal( ops.slice( 1 ) );
 		} );
 
-		it( 'should return empty (finished) iterator if given history point is too high or negative', () => {
-			expect( Array.from( history.getDeltas( 20 ) ).length ).to.equal( 0 );
-			expect( Array.from( history.getDeltas( -1 ) ).length ).to.equal( 0 );
+		it( 'should return only operations up to given base version', () => {
+			const historyOperations = Array.from( history.getOperations( 1, 2 ) );
+
+			expect( historyOperations ).to.deep.equal( ops.slice( 1, 2 ) );
 		} );
 
-		it( 'should throw if given history point is "inside" delta', () => {
-			expect( () => {
-				Array.from( history.getDeltas( 2 ) );
-			} ).to.throw( CKEditorError, /model-history-wrong-version/ );
+		it( 'should return empty array if no operations match', () => {
+			expect( Array.from( history.getOperations( 20 ) ).length ).to.equal( 0 );
+			expect( Array.from( history.getOperations( -1 ) ).length ).to.equal( 0 );
 		} );
 	} );
 
-	describe( 'isUndoingDelta', () => {
+	describe( 'isUndoingOperation', () => {
 		let undoing, undone;
 
 		beforeEach( () => {
-			undoing = new Delta();
-			undone = new Delta();
+			undone = new Operation( 0 );
+			undoing = new Operation( 1 );
 
-			history.addDelta( undone );
-			history.addDelta( undoing );
+			history.addOperation( undone );
+			history.addOperation( undoing );
 
-			history.setDeltaAsUndone( undone, undoing );
+			history.setOperationAsUndone( undone, undoing );
 		} );
 
-		it( 'should return true if delta is an undoing delta', () => {
-			expect( history.isUndoingDelta( undoing ) ).to.be.true;
+		it( 'should return true if operation is an undoing operation', () => {
+			expect( history.isUndoingOperation( undoing ) ).to.be.true;
 		} );
 
-		it( 'should return false if delta is not an undoing delta', () => {
-			const delta = new Delta();
+		it( 'should return false if operation is not an undoing operation', () => {
+			const operation = new Operation();
 
-			expect( history.isUndoingDelta( undone ) ).to.be.false;
-			expect( history.isUndoingDelta( delta ) ).to.be.false;
+			expect( history.isUndoingOperation( operation ) ).to.be.false;
 		} );
 	} );
 
-	describe( 'isUndoneDelta', () => {
+	describe( 'isUndoneOperation', () => {
 		let undoing, undone;
 
 		beforeEach( () => {
-			undoing = new Delta();
-			undone = new Delta();
+			undone = new Operation( 0 );
+			undoing = new Operation( 1 );
 
-			history.addDelta( undone );
-			history.addDelta( undoing );
+			history.addOperation( undone );
+			history.addOperation( undoing );
 
-			history.setDeltaAsUndone( undone, undoing );
+			history.setOperationAsUndone( undone, undoing );
 		} );
 
-		it( 'should return true if delta has been set as undone', () => {
-			expect( history.isUndoneDelta( undone ) ).to.be.true;
+		it( 'should return true if operation has been set as undone', () => {
+			expect( history.isUndoneOperation( undone ) ).to.be.true;
 		} );
 
-		it( 'should return false if delta has not been set as undone', () => {
-			const delta = new Delta();
+		it( 'should return false if operation is not an undone', () => {
+			const operation = new Operation();
 
-			expect( history.isUndoneDelta( undoing ) ).to.be.false;
-			expect( history.isUndoneDelta( delta ) ).to.be.false;
+			expect( history.isUndoneOperation( operation ) ).to.be.false;
 		} );
 	} );
 
-	describe( 'getUndoneDelta', () => {
+	describe( 'getUndoneOperation', () => {
 		let undoing, undone;
 
 		beforeEach( () => {
-			undoing = new Delta();
-			undone = new Delta();
+			undone = new Operation( 0 );
+			undoing = new Operation( 1 );
 
-			history.addDelta( undone );
-			history.addDelta( undoing );
+			history.addOperation( undone );
+			history.addOperation( undoing );
 
-			history.setDeltaAsUndone( undone, undoing );
+			history.setOperationAsUndone( undone, undoing );
 		} );
 
-		it( 'should return undone delta basing on undoing delta', () => {
-			expect( history.getUndoneDelta( undoing ) ).to.equal( undone );
+		it( 'should return undone operation basing on undoing operation', () => {
+			expect( history.getUndoneOperation( undoing ) ).to.equal( undone );
 		} );
 
-		it( 'should return undefined if given delta is not an undoing delta', () => {
-			const delta = new Delta();
+		it( 'should return undefined if given operation is not an undoing operation', () => {
+			const op = new Operation( 0 );
 
-			expect( history.getUndoneDelta( undone ) ).to.be.undefined;
-			expect( history.getUndoneDelta( delta ) ).to.be.undefined;
+			expect( history.getUndoneOperation( undone ) ).to.be.undefined;
+			expect( history.getUndoneOperation( op ) ).to.be.undefined;
 		} );
 	} );
 } );
 
-function getDeltaSet() {
-	const deltas = [];
-
-	deltas.push( getDelta( 0 ) );
-	deltas.push( getDelta( 3 ) );
-	deltas.push( getDelta( 6 ) );
-
-	return deltas;
-}
-
-function getDelta( baseVersion ) {
-	const delta = new Delta();
+function getOperations() {
+	const ops = [];
 
-	for ( let i = 0; i < 3; i++ ) {
-		delta.addOperation( new Operation( i + baseVersion ) );
-	}
+	ops.push( new Operation( 0 ) );
+	ops.push( new Operation( 1 ) );
+	ops.push( new Operation( 2 ) );
 
-	return delta;
+	return ops;
 }

+ 5 - 3
packages/ckeditor5-engine/tests/model/liveposition.js

@@ -116,7 +116,8 @@ describe( 'LivePosition', () => {
 				expect( spy.calledOnce ).to.be.true;
 			} );
 
-			it( 'is at the same position and live position is sticking to right side', () => {
+			it( 'is at the same position and live position is sticking to the next node', () => {
+				live.stickiness = 'toNext';
 				model.change( writer => {
 					writer.insertText( 'foo', new Position( root, [ 1, 1, 3 ] ) );
 				} );
@@ -150,6 +151,7 @@ describe( 'LivePosition', () => {
 			} );
 
 			it( 'is at the same position and live position is sticking to right side', () => {
+				live.stickiness = 'toNext';
 				model.change( writer => {
 					const sourcePosition = new Position( root, [ 1, 0, 1 ] );
 					const sourceRange = Range.createFromPositionAndShift( sourcePosition, 3 );
@@ -256,7 +258,7 @@ describe( 'LivePosition', () => {
 			} );
 
 			it( 'is at the same position and live position is sticking to left side', () => {
-				const newLive = new LivePosition( root, path, 'sticksToPrevious' );
+				const newLive = new LivePosition( root, path, 'toPrevious' );
 				spy = sinon.spy();
 				newLive.on( 'change', spy );
 
@@ -304,7 +306,7 @@ describe( 'LivePosition', () => {
 			} );
 
 			it( 'is at the same position and live position is sticking to left side', () => {
-				const newLive = new LivePosition( root, path, 'sticksToPrevious' );
+				const newLive = new LivePosition( root, path, 'toPrevious' );
 				spy = sinon.spy();
 				newLive.on( 'change', spy );
 

+ 12 - 13
packages/ckeditor5-engine/tests/model/liverange.js

@@ -100,11 +100,14 @@ describe( 'LiveRange', () => {
 		const sourcePosition = new Position( root, [ 2 ] );
 		const targetPosition = new Position( root, [ 0 ] );
 		const batch = new Batch();
+		let op = null;
 
 		model.enqueueChange( batch, writer => {
 			const sourceRange = Range.createFromPositionAndShift( sourcePosition, 1 );
 
 			writer.move( sourceRange, targetPosition );
+
+			op = batch.operations[ 0 ];
 		} );
 
 		expect( spy.calledOnce ).to.be.true;
@@ -112,11 +115,8 @@ describe( 'LiveRange', () => {
 		// First parameter available in event should be a range that is equal to the live range before the live range changed.
 		expect( spy.args[ 0 ][ 1 ].isEqual( copy ) ).to.be.true;
 
-		// Second parameter is an object with data about model changes that caused the live range to change.
-		expect( spy.args[ 0 ][ 2 ].type ).to.equal( 'move' );
-		expect( spy.args[ 0 ][ 2 ].batch ).to.equal( batch );
-		expect( spy.args[ 0 ][ 2 ].range.isEqual( Range.createFromPositionAndShift( targetPosition, 1 ) ) ).to.be.true;
-		expect( spy.args[ 0 ][ 2 ].sourcePosition.isEqual( sourcePosition ) ).to.be.true;
+		// Second parameter is the operation that changed the range.
+		expect( spy.args[ 0 ][ 2 ] ).to.equal( op );
 	} );
 
 	it( 'should fire change:content event with proper data when content inside the range has changed', () => {
@@ -128,24 +128,23 @@ describe( 'LiveRange', () => {
 		const sourcePosition = new Position( root, [ 0, 2, 0 ] );
 		const targetPosition = new Position( root, [ 0, 4, 0 ] );
 		const batch = new Batch();
+		let op = null;
 
 		model.enqueueChange( batch, writer => {
 			const sourceRange = Range.createFromPositionAndShift( sourcePosition, 2 );
 
 			writer.move( sourceRange, targetPosition );
+
+			op = batch.operations[ 0 ];
 		} );
 
 		expect( spy.calledOnce ).to.be.true;
 
 		// First parameter available in event should be a range that is equal to the live range before the live range changed.
-		// We compare to the `live` range, because boundaries should not have changed.
 		expect( spy.args[ 0 ][ 1 ].isEqual( live ) ).to.be.true;
 
-		// Second parameter is an object with data about model changes that caused the live range to change.
-		expect( spy.args[ 0 ][ 2 ].type ).to.equal( 'move' );
-		expect( spy.args[ 0 ][ 2 ].batch ).to.equal( batch );
-		expect( spy.args[ 0 ][ 2 ].range.isEqual( Range.createFromPositionAndShift( targetPosition, 2 ) ) ).to.be.true;
-		expect( spy.args[ 0 ][ 2 ].sourcePosition.isEqual( sourcePosition ) ).to.be.true;
+		// Second parameter is the operation that changed the range.
+		expect( spy.args[ 0 ][ 2 ] ).to.equal( op );
 	} );
 
 	describe( 'should get transformed and fire change:range if', () => {
@@ -311,7 +310,7 @@ describe( 'LiveRange', () => {
 				} );
 
 				expect( live.start.path ).to.deep.equal( [ 0, 1, 4 ] );
-				expect( live.end.path ).to.deep.equal( [ 0, 4, 2 ] );
+				expect( live.end.path ).to.deep.equal( [ 0, 2, 0 ] );
 				expect( spy.calledOnce ).to.be.true;
 			} );
 
@@ -353,7 +352,7 @@ describe( 'LiveRange', () => {
 				} );
 
 				expect( live.start.path ).to.deep.equal( [ 0, 1, 4 ] );
-				expect( live.end.path ).to.deep.equal( [ 0, 4, 1 ] ); // Included some nodes.
+				expect( live.end.path ).to.deep.equal( [ 0, 2, 1 ] );
 				expect( spy.calledOnce ).to.be.true;
 			} );
 

+ 3 - 44
packages/ckeditor5-engine/tests/model/model.js

@@ -5,9 +5,6 @@
 
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import Model from '../../src/model/model';
-import NoOperation from '../../src/model/operation/nooperation';
-import deltaTransform from '../../src/model/delta/transform';
-import Delta from '../../src/model/delta/delta';
 import ModelText from '../../src/model/text';
 import ModelRange from '../../src/model/range';
 import ModelSelection from '../../src/model/selection';
@@ -342,44 +339,6 @@ describe( 'Model', () => {
 		} );
 	} );
 
-	describe( 'transformDeltas()', () => {
-		it( 'should use deltaTransform.transformDeltaSets', () => {
-			sinon.spy( deltaTransform, 'transformDeltaSets' );
-
-			// Prepare some empty-ish deltas so the transformation won't throw an error.
-			const deltasA = [ new Delta() ];
-			deltasA[ 0 ].addOperation( new NoOperation( 0 ) );
-
-			const deltasB = [ new Delta() ];
-			deltasB[ 0 ].addOperation( new NoOperation( 0 ) );
-
-			model.transformDeltas( deltasA, deltasB );
-
-			expect( deltaTransform.transformDeltaSets.calledOnce ).to.be.true;
-			expect( deltaTransform.transformDeltaSets.calledWith( deltasA, deltasB, null ) ).to.be.true;
-
-			deltaTransform.transformDeltaSets.restore();
-		} );
-
-		it( 'should pass itself to transformDeltaSets if useContext was set to true', () => {
-			sinon.spy( deltaTransform, 'transformDeltaSets' );
-
-			// Prepare some empty-ish deltas so the transformation won't throw an error.
-			const deltasA = [ new Delta() ];
-			deltasA[ 0 ].addOperation( new NoOperation( 0 ) );
-
-			const deltasB = [ new Delta() ];
-			deltasB[ 0 ].addOperation( new NoOperation( 0 ) );
-
-			model.transformDeltas( deltasA, deltasB, true );
-
-			expect( deltaTransform.transformDeltaSets.calledOnce ).to.be.true;
-			expect( deltaTransform.transformDeltaSets.calledWith( deltasA, deltasB, model.document ) ).to.be.true;
-
-			deltaTransform.transformDeltaSets.restore();
-		} );
-	} );
-
 	describe( 'insertContent()', () => {
 		it( 'should be decorated', () => {
 			schema.extend( '$text', { allowIn: '$root' } ); // To surpress warnings.
@@ -419,7 +378,7 @@ describe( 'Model', () => {
 
 			model.change( writer => {
 				model.insertContent( new ModelText( 'abc' ), model.document.selection );
-				expect( writer.batch.deltas ).to.length( 1 );
+				expect( writer.batch.operations ).to.length( 1 );
 			} );
 		} );
 	} );
@@ -452,7 +411,7 @@ describe( 'Model', () => {
 
 			model.change( writer => {
 				model.deleteContent( model.document.selection );
-				expect( writer.batch.deltas ).to.length( 1 );
+				expect( writer.batch.operations ).to.length( 1 );
 			} );
 		} );
 	} );
@@ -513,7 +472,7 @@ describe( 'Model', () => {
 
 			model.change( writer => {
 				model.getSelectedContent( model.document.selection );
-				expect( writer.batch.deltas ).to.length( 1 );
+				expect( writer.batch.operations ).to.length( 1 );
 			} );
 		} );
 	} );

+ 21 - 62
packages/ckeditor5-engine/tests/model/operation/attributeoperation.js

@@ -4,14 +4,13 @@
  */
 
 import Model from '../../../src/model/model';
-import Element from '../../../src/model/element';
 import Text from '../../../src/model/text';
 import AttributeOperation from '../../../src/model/operation/attributeoperation';
 import Position from '../../../src/model/position';
 import Range from '../../../src/model/range';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import count from '@ckeditor/ckeditor5-utils/src/count';
-import { jsonParseStringify, wrapInDelta } from '../../../tests/model/_utils/utils';
+import { jsonParseStringify } from '../../../tests/model/_utils/utils';
 
 describe( 'AttributeOperation', () => {
 	let model, doc, root;
@@ -63,7 +62,7 @@ describe( 'AttributeOperation', () => {
 	it( 'should insert attribute to the set of nodes', () => {
 		root._insertChild( 0, new Text( 'bar' ) );
 
-		model.applyOperation( wrapInDelta(
+		model.applyOperation(
 			new AttributeOperation(
 				new Range( new Position( root, [ 0 ] ), new Position( root, [ 2 ] ) ),
 				'isNew',
@@ -71,7 +70,7 @@ describe( 'AttributeOperation', () => {
 				true,
 				doc.version
 			)
-		) );
+		);
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.maxOffset ).to.equal( 3 );
@@ -84,7 +83,7 @@ describe( 'AttributeOperation', () => {
 	it( 'should add attribute to the existing attributes', () => {
 		root._insertChild( 0, new Text( 'x', { foo: true, bar: true } ) );
 
-		model.applyOperation( wrapInDelta(
+		model.applyOperation(
 			new AttributeOperation(
 				new Range( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) ),
 				'isNew',
@@ -92,7 +91,7 @@ describe( 'AttributeOperation', () => {
 				true,
 				doc.version
 			)
-		) );
+		);
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.maxOffset ).to.equal( 1 );
@@ -105,7 +104,7 @@ describe( 'AttributeOperation', () => {
 	it( 'should change attribute to the set of nodes', () => {
 		root._insertChild( 0, new Text( 'bar', { isNew: false } ) );
 
-		model.applyOperation( wrapInDelta(
+		model.applyOperation(
 			new AttributeOperation(
 				new Range( new Position( root, [ 0 ] ), new Position( root, [ 2 ] ) ),
 				'isNew',
@@ -113,7 +112,7 @@ describe( 'AttributeOperation', () => {
 				true,
 				doc.version
 			)
-		) );
+		);
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.maxOffset ).to.equal( 3 );
@@ -126,7 +125,7 @@ describe( 'AttributeOperation', () => {
 	it( 'should change attribute in the middle of existing attributes', () => {
 		root._insertChild( 0, new Text( 'x', { foo: true, x: 1, bar: true } ) );
 
-		model.applyOperation( wrapInDelta(
+		model.applyOperation(
 			new AttributeOperation(
 				new Range( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) ),
 				'x',
@@ -134,7 +133,7 @@ describe( 'AttributeOperation', () => {
 				2,
 				doc.version
 			)
-		) );
+		);
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.maxOffset ).to.equal( 1 );
@@ -147,7 +146,7 @@ describe( 'AttributeOperation', () => {
 	it( 'should work correctly if old and new value are same', () => {
 		root._insertChild( 0, new Text( 'bar', { foo: 'bar' } ) );
 
-		model.applyOperation( wrapInDelta(
+		model.applyOperation(
 			new AttributeOperation(
 				new Range( new Position( root, [ 0 ] ), new Position( root, [ 2 ] ) ),
 				'foo',
@@ -155,7 +154,7 @@ describe( 'AttributeOperation', () => {
 				'bar',
 				doc.version
 			)
-		) );
+		);
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.childCount ).to.equal( 1 );
@@ -166,7 +165,7 @@ describe( 'AttributeOperation', () => {
 	it( 'should remove attribute', () => {
 		root._insertChild( 0, new Text( 'x', { foo: true, x: true, bar: true } ) );
 
-		model.applyOperation( wrapInDelta(
+		model.applyOperation(
 			new AttributeOperation(
 				new Range( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) ),
 				'x',
@@ -174,7 +173,7 @@ describe( 'AttributeOperation', () => {
 				null,
 				doc.version
 			)
-		) );
+		);
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.maxOffset ).to.equal( 1 );
@@ -275,54 +274,14 @@ describe( 'AttributeOperation', () => {
 
 		const reverse = operation.getReversed();
 
-		model.applyOperation( wrapInDelta( operation ) );
-		model.applyOperation( wrapInDelta( reverse ) );
+		model.applyOperation( operation );
+		model.applyOperation( reverse );
 
 		expect( doc.version ).to.equal( 2 );
 		expect( root.maxOffset ).to.equal( 3 );
 		expect( count( root.getChild( 0 ).getAttributes() ) ).to.equal( 0 );
 	} );
 
-	it( 'should not set attribute of element if change range starts in the middle of that element', () => {
-		const eleA = new Element( 'a', [], new Text( 'abc' ) );
-		const eleB = new Element( 'b', [], new Text( 'xyz' ) );
-
-		root._insertChild( 0, [ eleA, eleB ] );
-
-		model.applyOperation( wrapInDelta(
-			new AttributeOperation(
-				new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 1, 2 ] ) ),
-				'foo',
-				null,
-				true,
-				doc.version
-			)
-		) );
-
-		expect( root.getChild( 0 ).hasAttribute( 'foo' ) ).to.be.false;
-	} );
-
-	it( 'should not remove attribute of element if change range starts in the middle of that element', () => {
-		const fooAttr = { foo: true };
-
-		const eleA = new Element( 'a', fooAttr, new Text( 'abc' ) );
-		const eleB = new Element( 'b', fooAttr, new Text( 'xyz' ) );
-
-		root._insertChild( 0, [ eleA, eleB ] );
-
-		model.applyOperation( wrapInDelta(
-			new AttributeOperation(
-				new Range( new Position( root, [ 0, 3 ] ), new Position( root, [ 1, 0 ] ) ),
-				'foo',
-				true,
-				null,
-				doc.version
-			)
-		) );
-
-		expect( root.getChild( 0 ).hasAttribute( 'foo' ) ).to.be.true;
-	} );
-
 	it( 'should undo changing attribute by applying reverse operation', () => {
 		root._insertChild( 0, new Text( 'bar', { isNew: false } ) );
 
@@ -336,8 +295,8 @@ describe( 'AttributeOperation', () => {
 
 		const reverse = operation.getReversed();
 
-		model.applyOperation( wrapInDelta( operation ) );
-		model.applyOperation( wrapInDelta( reverse ) );
+		model.applyOperation( operation );
+		model.applyOperation( reverse );
 
 		expect( doc.version ).to.equal( 2 );
 		expect( root.maxOffset ).to.equal( 3 );
@@ -358,8 +317,8 @@ describe( 'AttributeOperation', () => {
 
 		const reverse = operation.getReversed();
 
-		model.applyOperation( wrapInDelta( operation ) );
-		model.applyOperation( wrapInDelta( reverse ) );
+		model.applyOperation( operation );
+		model.applyOperation( reverse );
 
 		expect( doc.version ).to.equal( 2 );
 		expect( root.maxOffset ).to.equal( 3 );
@@ -393,14 +352,14 @@ describe( 'AttributeOperation', () => {
 		root._insertChild( 0, new Text( 'abc', attrA ) );
 		root._insertChild( 1, new Text( 'xyz', attrB ) );
 
-		model.applyOperation( wrapInDelta(
+		model.applyOperation(
 			new AttributeOperation(
 				new Range( new Position( root, [ 1 ] ), new Position( root, [ 3 ] ) ),
 				'foo',
 				'a',
 				'b',
 				doc.version
-			) )
+			)
 		);
 
 		expect( root.getChild( 0 ).data ).to.equal( 'a' );

+ 2 - 2
packages/ckeditor5-engine/tests/model/operation/detachoperation.js

@@ -5,7 +5,7 @@
 
 import Model from '../../../src/model/model';
 import DetachOperation from '../../../src/model/operation/detachoperation';
-import { jsonParseStringify, wrapInDelta } from '../../../tests/model/_utils/utils';
+import { jsonParseStringify } from '../../../tests/model/_utils/utils';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import Position from '../../../src/model/position';
 import DocumentFragment from '../../../src/model/documentfragment';
@@ -30,7 +30,7 @@ describe( 'DetachOperation', () => {
 	it( 'should remove given element from parent', () => {
 		const op = new DetachOperation( Position.createBefore( element ), 1 );
 
-		model.applyOperation( wrapInDelta( op ) );
+		model.applyOperation( op );
 
 		expect( docFrag.childCount ).to.equal( 0 );
 	} );

+ 20 - 18
packages/ckeditor5-engine/tests/model/operation/insertoperation.js

@@ -11,7 +11,7 @@ import RemoveOperation from '../../../src/model/operation/removeoperation';
 import Position from '../../../src/model/position';
 import Text from '../../../src/model/text';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-import { jsonParseStringify, wrapInDelta } from '../../../tests/model/_utils/utils';
+import { jsonParseStringify } from '../../../tests/model/_utils/utils';
 
 describe( 'InsertOperation', () => {
 	let model, doc, root;
@@ -33,13 +33,13 @@ describe( 'InsertOperation', () => {
 	} );
 
 	it( 'should insert text node', () => {
-		model.applyOperation( wrapInDelta(
+		model.applyOperation(
 			new InsertOperation(
 				new Position( root, [ 0 ] ),
 				new Text( 'x' ),
 				doc.version
 			)
-		) );
+		);
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.maxOffset ).to.equal( 1 );
@@ -47,13 +47,13 @@ describe( 'InsertOperation', () => {
 	} );
 
 	it( 'should insert element', () => {
-		model.applyOperation( wrapInDelta(
+		model.applyOperation(
 			new InsertOperation(
 				new Position( root, [ 0 ] ),
 				new Element( 'p' ),
 				doc.version
 			)
-		) );
+		);
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.maxOffset ).to.equal( 1 );
@@ -61,13 +61,13 @@ describe( 'InsertOperation', () => {
 	} );
 
 	it( 'should insert set of nodes', () => {
-		model.applyOperation( wrapInDelta(
+		model.applyOperation(
 			new InsertOperation(
 				new Position( root, [ 0 ] ),
 				[ 'bar', new Element( 'p' ), 'foo' ],
 				doc.version
 			)
-		) );
+		);
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.maxOffset ).to.equal( 7 );
@@ -80,13 +80,13 @@ describe( 'InsertOperation', () => {
 	it( 'should insert between existing nodes', () => {
 		root._insertChild( 0, new Text( 'xy' ) );
 
-		model.applyOperation( wrapInDelta(
+		model.applyOperation(
 			new InsertOperation(
 				new Position( root, [ 1 ] ),
 				'bar',
 				doc.version
 			)
-		) );
+		);
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.maxOffset ).to.equal( 5 );
@@ -94,13 +94,13 @@ describe( 'InsertOperation', () => {
 	} );
 
 	it( 'should insert text', () => {
-		model.applyOperation( wrapInDelta(
+		model.applyOperation(
 			new InsertOperation(
 				new Position( root, [ 0 ] ),
 				[ 'foo', new Text( 'x' ), 'bar' ],
 				doc.version
 			)
-		) );
+		);
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.maxOffset ).to.equal( 7 );
@@ -132,11 +132,11 @@ describe( 'InsertOperation', () => {
 
 		const reverse = operation.getReversed();
 
-		model.applyOperation( wrapInDelta( operation ) );
+		model.applyOperation( operation );
 
 		expect( doc.version ).to.equal( 1 );
 
-		model.applyOperation( wrapInDelta( reverse ) );
+		model.applyOperation( reverse );
 
 		expect( doc.version ).to.equal( 2 );
 		expect( root.maxOffset ).to.equal( 0 );
@@ -151,11 +151,11 @@ describe( 'InsertOperation', () => {
 
 		const reverse = operation.getReversed();
 
-		model.applyOperation( wrapInDelta( operation ) );
+		model.applyOperation( operation );
 
 		expect( doc.version ).to.equal( 1 );
 
-		model.applyOperation( wrapInDelta( reverse ) );
+		model.applyOperation( reverse );
 
 		expect( doc.version ).to.equal( 2 );
 		expect( root.maxOffset ).to.equal( 0 );
@@ -192,11 +192,11 @@ describe( 'InsertOperation', () => {
 		const element = new Element( 'p', { key: 'value' } );
 
 		const op = new InsertOperation( new Position( root, [ 0 ] ), element, doc.version );
-		model.applyOperation( wrapInDelta( op ) );
+		model.applyOperation( op );
 
 		const text = new Text( 'text' );
 		const op2 = new InsertOperation( new Position( root, [ 0, 0 ] ), text, doc.version );
-		model.applyOperation( wrapInDelta( op2 ) );
+		model.applyOperation( op2 );
 
 		expect( op.nodes.getNode( 0 ) ).not.to.equal( element );
 		expect( op.nodes.getNode( 0 ).name ).to.equal( 'p' );
@@ -221,6 +221,7 @@ describe( 'InsertOperation', () => {
 		it( 'should create proper json object', () => {
 			const position = new Position( root, [ 0 ] );
 			const op = new InsertOperation( position, new Text( 'x' ), doc.version );
+			op.shouldReceiveAttributes = true;
 
 			const serialized = jsonParseStringify( op );
 
@@ -228,7 +229,8 @@ describe( 'InsertOperation', () => {
 				__className: 'engine.model.operation.InsertOperation',
 				baseVersion: 0,
 				nodes: jsonParseStringify( new NodeList( [ new Text( 'x' ) ] ) ),
-				position: jsonParseStringify( position )
+				position: jsonParseStringify( position ),
+				shouldReceiveAttributes: true
 			} );
 		} );
 	} );

+ 15 - 15
packages/ckeditor5-engine/tests/model/operation/markeroperation.js

@@ -7,7 +7,7 @@ import Model from '../../../src/model/model';
 import Text from '../../../src/model/text';
 import Range from '../../../src/model/range';
 import MarkerOperation from '../../../src/model/operation/markeroperation';
-import { jsonParseStringify, wrapInDelta } from '../../model/_utils/utils';
+import { jsonParseStringify } from '../../model/_utils/utils';
 
 function matchRange( range ) {
 	return sinon.match( rangeToMatch => rangeToMatch.isEqual( range ) );
@@ -32,9 +32,9 @@ describe( 'MarkerOperation', () => {
 	it( 'should add marker to document marker collection', () => {
 		sinon.spy( model.markers, '_set' );
 
-		model.applyOperation( wrapInDelta(
+		model.applyOperation(
 			new MarkerOperation( 'name', null, range, model.markers, doc.version, true )
-		) );
+		);
 
 		expect( doc.version ).to.equal( 1 );
 		expect( model.markers._set.calledWith( 'name', matchRange( range ) ) );
@@ -42,17 +42,17 @@ describe( 'MarkerOperation', () => {
 	} );
 
 	it( 'should update marker in document marker collection', () => {
-		model.applyOperation( wrapInDelta(
+		model.applyOperation(
 			new MarkerOperation( 'name', null, range, model.markers, doc.version, true )
-		) );
+		);
 
 		const range2 = Range.createFromParentsAndOffsets( root, 0, root, 3 );
 
 		sinon.spy( model.markers, '_set' );
 
-		model.applyOperation( wrapInDelta(
+		model.applyOperation(
 			new MarkerOperation( 'name', range, range2, model.markers, doc.version, true )
-		) );
+		);
 
 		expect( doc.version ).to.equal( 2 );
 		expect( model.markers._set.calledWith( 'name', matchRange( range2 ) ) );
@@ -60,15 +60,15 @@ describe( 'MarkerOperation', () => {
 	} );
 
 	it( 'should remove marker from document marker collection', () => {
-		model.applyOperation( wrapInDelta(
+		model.applyOperation(
 			new MarkerOperation( 'name', null, range, model.markers, doc.version, true )
-		) );
+		);
 
 		sinon.spy( model.markers, '_remove' );
 
-		model.applyOperation( wrapInDelta(
+		model.applyOperation(
 			new MarkerOperation( 'name', range, null, model.markers, doc.version, true )
-		) );
+		);
 
 		expect( doc.version ).to.equal( 2 );
 		expect( model.markers._remove.calledWith( 'name' ) );
@@ -78,9 +78,9 @@ describe( 'MarkerOperation', () => {
 	it( 'should not fire document markers remove event if removing non-existing range', () => {
 		sinon.spy( model.markers, 'fire' );
 
-		model.applyOperation( wrapInDelta(
+		model.applyOperation(
 			new MarkerOperation( 'name', null, null, model.markers, doc.version, true )
-		) );
+		);
 
 		expect( model.markers.fire.notCalled ).to.be.true;
 	} );
@@ -92,9 +92,9 @@ describe( 'MarkerOperation', () => {
 
 		sinon.spy( model.markers, 'fire' );
 
-		model.applyOperation( wrapInDelta(
+		model.applyOperation(
 			new MarkerOperation( 'name', range, range, model.markers, doc.version, false )
-		) );
+		);
 
 		expect( model.markers.fire.notCalled ).to.be.true;
 	} );

+ 11 - 35
packages/ckeditor5-engine/tests/model/operation/moveoperation.js

@@ -9,7 +9,7 @@ import Position from '../../../src/model/position';
 import Element from '../../../src/model/element';
 import Text from '../../../src/model/text';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-import { jsonParseStringify, wrapInDelta } from '../../../tests/model/_utils/utils';
+import { jsonParseStringify } from '../../../tests/model/_utils/utils';
 
 describe( 'MoveOperation', () => {
 	let model, doc, root;
@@ -31,31 +31,20 @@ describe( 'MoveOperation', () => {
 		expect( op.type ).to.equal( 'move' );
 	} );
 
-	it( 'should be sticky', () => {
-		const op = new MoveOperation(
-			new Position( root, [ 0, 0 ] ),
-			1,
-			new Position( root, [ 1, 0 ] ),
-			doc.version
-		);
-
-		expect( op.isSticky ).to.be.false;
-	} );
-
 	it( 'should move from one node to another', () => {
 		const p1 = new Element( 'p1', [], new Element( 'x' ) );
 		const p2 = new Element( 'p2' );
 
 		root._insertChild( 0, [ p1, p2 ] );
 
-		model.applyOperation( wrapInDelta(
+		model.applyOperation(
 			new MoveOperation(
 				new Position( root, [ 0, 0 ] ),
 				1,
 				new Position( root, [ 1, 0 ] ),
 				doc.version
 			)
-		) );
+		);
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.maxOffset ).to.equal( 2 );
@@ -69,14 +58,14 @@ describe( 'MoveOperation', () => {
 	it( 'should move position of children in one node backward', () => {
 		root._insertChild( 0, new Text( 'xbarx' ) );
 
-		model.applyOperation( wrapInDelta(
+		model.applyOperation(
 			new MoveOperation(
 				new Position( root, [ 2 ] ),
 				2,
 				new Position( root, [ 1 ] ),
 				doc.version
 			)
-		) );
+		);
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.maxOffset ).to.equal( 5 );
@@ -86,14 +75,14 @@ describe( 'MoveOperation', () => {
 	it( 'should move position of children in one node forward', () => {
 		root._insertChild( 0, new Text( 'xbarx' ) );
 
-		model.applyOperation( wrapInDelta(
+		model.applyOperation(
 			new MoveOperation(
 				new Position( root, [ 1 ] ),
 				2,
 				new Position( root, [ 4 ] ),
 				doc.version
 			)
-		) );
+		);
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.maxOffset ).to.equal( 5 );
@@ -133,7 +122,7 @@ describe( 'MoveOperation', () => {
 			doc.version
 		);
 
-		model.applyOperation( wrapInDelta( operation ) );
+		model.applyOperation( operation );
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.maxOffset ).to.equal( 2 );
@@ -141,7 +130,7 @@ describe( 'MoveOperation', () => {
 		expect( p2.maxOffset ).to.equal( 1 );
 		expect( p2.getChild( 0 ).name ).to.equal( 'x' );
 
-		model.applyOperation( wrapInDelta( operation.getReversed() ) );
+		model.applyOperation( operation.getReversed() );
 
 		expect( doc.version ).to.equal( 2 );
 		expect( root.maxOffset ).to.equal( 2 );
@@ -283,9 +272,8 @@ describe( 'MoveOperation', () => {
 				__className: 'engine.model.operation.MoveOperation',
 				baseVersion: 0,
 				howMany: 1,
-				isSticky: false,
-				sourcePosition: jsonParseStringify( sourcePosition ),
-				targetPosition: jsonParseStringify( targetPosition )
+				sourcePosition: jsonParseStringify( op.sourcePosition ),
+				targetPosition: jsonParseStringify( op.targetPosition )
 			} );
 		} );
 	} );
@@ -301,17 +289,5 @@ describe( 'MoveOperation', () => {
 
 			expect( deserialized ).to.deep.equal( op );
 		} );
-
-		it( 'should create proper MoveOperation from json object - sticky', () => {
-			const sourcePosition = new Position( root, [ 0, 0 ] );
-			const targetPosition = new Position( root, [ 1, 0 ] );
-			const op = new MoveOperation( sourcePosition, 1, targetPosition, doc.version );
-			op.isSticky = true;
-
-			const serialized = jsonParseStringify( op );
-			const deserialized = MoveOperation.fromJSON( serialized, doc );
-
-			expect( deserialized ).to.deep.equal( op );
-		} );
 	} );
 } );

+ 2 - 2
packages/ckeditor5-engine/tests/model/operation/nooperation.js

@@ -5,7 +5,7 @@
 
 import Model from '../../../src/model/model';
 import NoOperation from '../../../src/model/operation/nooperation';
-import { jsonParseStringify, wrapInDelta } from '../../../tests/model/_utils/utils';
+import { jsonParseStringify } from '../../../tests/model/_utils/utils';
 
 describe( 'NoOperation', () => {
 	let model, noop, doc;
@@ -17,7 +17,7 @@ describe( 'NoOperation', () => {
 	} );
 
 	it( 'should not throw an error when applied', () => {
-		expect( () => model.applyOperation( wrapInDelta( noop ) ) ).to.not.throw( Error );
+		expect( () => model.applyOperation( noop ) ).to.not.throw( Error );
 	} );
 
 	it( 'should create a NoOperation as a reverse', () => {

+ 15 - 14
packages/ckeditor5-engine/tests/model/operation/operation.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md.
  */
 
-import Delta from '../../../src/model/delta/delta';
+import Batch from '../../../src/model/batch';
 import Operation from '../../../src/model/operation/operation';
 import { jsonParseStringify } from '../../../tests/model/_utils/utils';
 
@@ -14,18 +14,6 @@ describe( 'Operation', () => {
 		expect( op.baseVersion ).to.equal( 4 );
 	} );
 
-	it( 'should be correctly transformed to JSON', () => {
-		const delta = new Delta();
-		const opInDelta = new Operation( 0 );
-		delta.addOperation( opInDelta );
-
-		const opOutsideDelta = new Operation( 0 );
-
-		const parsedOutside = jsonParseStringify( opOutsideDelta );
-
-		expect( parsedOutside.delta ).to.be.undefined;
-	} );
-
 	describe( 'isDocumentOperation', () => {
 		it( 'operation is a document operation if it has base version set', () => {
 			const op = new Operation( 0 );
@@ -41,8 +29,21 @@ describe( 'Operation', () => {
 	} );
 
 	describe( 'toJSON', () => {
-		it( 'should create proper json object', () => {
+		it( 'should create proper json object #1', () => {
+			const op = new Operation( 4 );
+
+			const serialized = jsonParseStringify( op );
+
+			expect( serialized ).to.deep.equal( {
+				__className: 'engine.model.operation.Operation',
+				baseVersion: 4
+			} );
+		} );
+
+		it( 'should create proper json object #1', () => {
 			const op = new Operation( 4 );
+			const batch = new Batch();
+			batch.addOperation( op );
 
 			const serialized = jsonParseStringify( op );
 

+ 3 - 8
packages/ckeditor5-engine/tests/model/operation/reinsertoperation.js

@@ -12,7 +12,7 @@ import DocumentFragment from '../../../src/model/documentfragment';
 import Element from '../../../src/model/element';
 import Text from '../../../src/model/text';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-import { jsonParseStringify, wrapInDelta } from '../../../tests/model/_utils/utils';
+import { jsonParseStringify } from '../../../tests/model/_utils/utils';
 
 describe( 'ReinsertOperation', () => {
 	let model, doc, root, graveyard, operation, graveyardPosition, rootPosition;
@@ -46,10 +46,6 @@ describe( 'ReinsertOperation', () => {
 		expect( operation.type ).to.equal( 'reinsert' );
 	} );
 
-	it( 'should not be sticky', () => {
-		expect( operation.isSticky ).to.be.false;
-	} );
-
 	it( 'should extend MoveOperation class', () => {
 		expect( operation ).to.be.instanceof( MoveOperation );
 	} );
@@ -89,13 +85,13 @@ describe( 'ReinsertOperation', () => {
 
 		graveyard._insertChild( 0, new Text( 'xx' ) );
 
-		model.applyOperation( wrapInDelta( operation ) );
+		model.applyOperation( operation );
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.maxOffset ).to.equal( 2 );
 		expect( graveyard.maxOffset ).to.equal( 0 );
 
-		model.applyOperation( wrapInDelta( reverse ) );
+		model.applyOperation( reverse );
 
 		expect( doc.version ).to.equal( 2 );
 		expect( root.maxOffset ).to.equal( 0 );
@@ -144,7 +140,6 @@ describe( 'ReinsertOperation', () => {
 				__className: 'engine.model.operation.ReinsertOperation',
 				baseVersion: 0,
 				howMany: 2,
-				isSticky: false,
 				sourcePosition: jsonParseStringify( operation.sourcePosition ),
 				targetPosition: jsonParseStringify( operation.targetPosition )
 			} );

+ 6 - 18
packages/ckeditor5-engine/tests/model/operation/removeoperation.js

@@ -12,7 +12,7 @@ import DocumentFragment from '../../../src/model/documentfragment';
 import Element from '../../../src/model/element';
 import Text from '../../../src/model/text';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-import { jsonParseStringify, wrapInDelta } from '../../../tests/model/_utils/utils';
+import { jsonParseStringify } from '../../../tests/model/_utils/utils';
 
 describe( 'RemoveOperation', () => {
 	let model, doc, root, graveyard;
@@ -35,17 +35,6 @@ describe( 'RemoveOperation', () => {
 		expect( op.type ).to.equal( 'remove' );
 	} );
 
-	it( 'should not be sticky', () => {
-		const op = new RemoveOperation(
-			new Position( root, [ 2 ] ),
-			2,
-			new Position( doc.graveyard, [ 0 ] ),
-			doc.version
-		);
-
-		expect( op.isSticky ).to.be.false;
-	} );
-
 	it( 'should extend MoveOperation class', () => {
 		const operation = new RemoveOperation(
 			new Position( root, [ 2 ] ),
@@ -60,14 +49,14 @@ describe( 'RemoveOperation', () => {
 	it( 'should be able to remove set of nodes and append them to graveyard root', () => {
 		root._insertChild( 0, new Text( 'fozbar' ) );
 
-		model.applyOperation( wrapInDelta(
+		model.applyOperation(
 			new RemoveOperation(
 				new Position( root, [ 2 ] ),
 				2,
 				new Position( doc.graveyard, [ 0 ] ),
 				doc.version
 			)
-		) );
+		);
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.maxOffset ).to.equal( 4 );
@@ -117,12 +106,12 @@ describe( 'RemoveOperation', () => {
 
 		root._insertChild( 0, new Text( 'bar' ) );
 
-		model.applyOperation( wrapInDelta( operation ) );
+		model.applyOperation( operation );
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.maxOffset ).to.equal( 0 );
 
-		model.applyOperation( wrapInDelta( reverse ) );
+		model.applyOperation( reverse );
 
 		expect( doc.version ).to.equal( 2 );
 		expect( root.maxOffset ).to.equal( 3 );
@@ -135,7 +124,7 @@ describe( 'RemoveOperation', () => {
 		const position = new Position( doc.graveyard, [ 2 ] );
 		const operation = new RemoveOperation( position, 1, new Position( doc.graveyard, [ 0 ] ), 0 );
 
-		model.applyOperation( wrapInDelta( operation ) );
+		model.applyOperation( operation );
 
 		expect( doc.graveyard.childCount ).to.equal( 3 );
 		expect( doc.graveyard.getChild( 0 ).name ).to.equal( 'z' );
@@ -178,7 +167,6 @@ describe( 'RemoveOperation', () => {
 				__className: 'engine.model.operation.RemoveOperation',
 				baseVersion: 0,
 				howMany: 2,
-				isSticky: false,
 				sourcePosition: jsonParseStringify( op.sourcePosition ),
 				targetPosition: jsonParseStringify( op.targetPosition )
 			} );

+ 4 - 4
packages/ckeditor5-engine/tests/model/operation/renameoperation.js

@@ -8,7 +8,7 @@ import Element from '../../../src/model/element';
 import RenameOperation from '../../../src/model/operation/renameoperation';
 import Position from '../../../src/model/position';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-import { jsonParseStringify, wrapInDelta } from '../../../tests/model/_utils/utils';
+import { jsonParseStringify } from '../../../tests/model/_utils/utils';
 
 describe( 'RenameOperation', () => {
 	const oldName = 'oldName';
@@ -36,7 +36,7 @@ describe( 'RenameOperation', () => {
 	it( 'should change name of given element', () => {
 		const op = new RenameOperation( position, oldName, newName, doc.version );
 
-		model.applyOperation( wrapInDelta( op ) );
+		model.applyOperation( op );
 
 		expect( element.name ).to.equal( newName );
 	} );
@@ -56,8 +56,8 @@ describe( 'RenameOperation', () => {
 		const op = new RenameOperation( position, oldName, newName, doc.version );
 		const reverse = op.getReversed();
 
-		model.applyOperation( wrapInDelta( op ) );
-		model.applyOperation( wrapInDelta( reverse ) );
+		model.applyOperation( op );
+		model.applyOperation( reverse );
 
 		expect( doc.version ).to.equal( 2 );
 		expect( element.name ).to.equal( oldName );

+ 13 - 13
packages/ckeditor5-engine/tests/model/operation/rootattributeoperation.js

@@ -8,7 +8,7 @@ import DocumentFragment from '../../../src/model/documentfragment';
 import Element from '../../../src/model/element';
 import RootAttributeOperation from '../../../src/model/operation/rootattributeoperation';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-import { jsonParseStringify, wrapInDelta } from '../../../tests/model/_utils/utils';
+import { jsonParseStringify } from '../../../tests/model/_utils/utils';
 
 describe( 'RootAttributeOperation', () => {
 	let model, doc, root;
@@ -58,7 +58,7 @@ describe( 'RootAttributeOperation', () => {
 	} );
 
 	it( 'should add attribute on the root element', () => {
-		model.applyOperation( wrapInDelta(
+		model.applyOperation(
 			new RootAttributeOperation(
 				root,
 				'isNew',
@@ -66,7 +66,7 @@ describe( 'RootAttributeOperation', () => {
 				true,
 				doc.version
 			)
-		) );
+		);
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.hasAttribute( 'isNew' ) ).to.be.true;
@@ -75,7 +75,7 @@ describe( 'RootAttributeOperation', () => {
 	it( 'should change attribute on the root element', () => {
 		root._setAttribute( 'isNew', false );
 
-		model.applyOperation( wrapInDelta(
+		model.applyOperation(
 			new RootAttributeOperation(
 				root,
 				'isNew',
@@ -83,7 +83,7 @@ describe( 'RootAttributeOperation', () => {
 				true,
 				doc.version
 			)
-		) );
+		);
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.getAttribute( 'isNew' ) ).to.be.true;
@@ -92,7 +92,7 @@ describe( 'RootAttributeOperation', () => {
 	it( 'should remove attribute from the root element', () => {
 		root._setAttribute( 'x', true );
 
-		model.applyOperation( wrapInDelta(
+		model.applyOperation(
 			new RootAttributeOperation(
 				root,
 				'x',
@@ -100,7 +100,7 @@ describe( 'RootAttributeOperation', () => {
 				null,
 				doc.version
 			)
-		) );
+		);
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.hasAttribute( 'x' ) ).to.be.false;
@@ -129,8 +129,8 @@ describe( 'RootAttributeOperation', () => {
 
 		const reverse = operation.getReversed();
 
-		model.applyOperation( wrapInDelta( operation ) );
-		model.applyOperation( wrapInDelta( reverse ) );
+		model.applyOperation( operation );
+		model.applyOperation( reverse );
 
 		expect( doc.version ).to.equal( 2 );
 		expect( root.hasAttribute( 'x' ) ).to.be.false;
@@ -149,8 +149,8 @@ describe( 'RootAttributeOperation', () => {
 
 		const reverse = operation.getReversed();
 
-		model.applyOperation( wrapInDelta( operation ) );
-		model.applyOperation( wrapInDelta( reverse ) );
+		model.applyOperation( operation );
+		model.applyOperation( reverse );
 
 		expect( doc.version ).to.equal( 2 );
 		expect( root.getAttribute( 'isNew' ) ).to.be.false;
@@ -169,8 +169,8 @@ describe( 'RootAttributeOperation', () => {
 
 		const reverse = operation.getReversed();
 
-		model.applyOperation( wrapInDelta( operation ) );
-		model.applyOperation( wrapInDelta( reverse ) );
+		model.applyOperation( operation );
+		model.applyOperation( reverse );
 
 		expect( doc.version ).to.equal( 2 );
 		expect( root.getAttribute( 'foo' ) ).to.be.true;

Некоторые файлы не были показаны из-за большого количества измененных файлов