Sfoglia il codice sorgente

Merge pull request #941 from ckeditor/t/940

Feature: When engine debugging is on, deltas that are results of transformation will keep their history of changes in `#history` property. Closes #940.
Szymon Cofalik 8 anni fa
parent
commit
72767420f7

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

@@ -9,6 +9,8 @@
 
 /* global console */
 
+import DeltaReplayer from './deltareplayer';
+
 import ModelPosition from '../model/position';
 import ModelRange from '../model/range';
 import ModelText from '../model/text';
@@ -33,6 +35,7 @@ import RenameDelta from '../model/delta/renamedelta';
 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 ModelDocument from '../model/document';
 import ModelDocumentFragment from '../model/documentfragment';
 import ModelRootElement from '../model/rootelement';
@@ -46,7 +49,7 @@ import ViewDocumentFragment from '../view/documentfragment';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Editor from '@ckeditor/ckeditor5-core/src/editor/editor';
 
-import DeltaReplayer from './deltareplayer';
+import clone from '@ckeditor/ckeditor5-utils/src/lib/lodash/clone';
 
 const treeDump = Symbol( '_treeDump' );
 
@@ -268,38 +271,38 @@ function enableLoggingTools() {
 	};
 
 	AttributeOperation.prototype.toString = function() {
-		return getClassName( this ) + ': ' +
+		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			`"${ this.key }": ${ JSON.stringify( this.oldValue ) } -> ${ JSON.stringify( this.newValue ) }, ${ this.range }`;
 	};
 
 	InsertOperation.prototype.toString = function() {
-		return getClassName( this ) + ': ' +
+		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			`[ ${ this.nodes.length } ] -> ${ this.position }`;
 	};
 
 	MarkerOperation.prototype.toString = function() {
-		return getClassName( this ) + ': ' +
+		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			`"${ this.name }": ${ this.oldRange } -> ${ this.newRange }`;
 	};
 
 	MoveOperation.prototype.toString = function() {
 		const range = ModelRange.createFromPositionAndShift( this.sourcePosition, this.howMany );
 
-		return getClassName( this ) + ': ' +
+		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			`${ range } -> ${ this.targetPosition }`;
 	};
 
 	NoOperation.prototype.toString = function() {
-		return 'NoOperation';
+		return `NoOperation( ${ this.baseVersion } )`;
 	};
 
 	RenameOperation.prototype.toString = function() {
-		return getClassName( this ) + ': ' +
+		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			`${ this.position }: "${ this.oldName }" -> "${ this.newName }"`;
 	};
 
 	RootAttributeOperation.prototype.toString = function() {
-		return getClassName( this ) + ': ' +
+		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			`"${ this.key }": ${ JSON.stringify( this.oldValue ) } -> ${ JSON.stringify( this.newValue ) }, ${ this.root.rootName }`;
 	};
 
@@ -317,27 +320,59 @@ function enableLoggingTools() {
 		}
 	};
 
+	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;
+
+	deltaTransform.transform = function( a, b, isAMoreImportantThanB ) {
+		const results = _deltaTransformTransform( a, b, isAMoreImportantThanB );
+
+		for ( let i = 0; i < results.length; i++ ) {
+			results[ i ]._saveHistory( {
+				before: a,
+				transformedBy: b,
+				wasImportant: isAMoreImportantThanB,
+				resultIndex: i,
+				resultsTotal: results.length
+			} );
+		}
+
+		return results;
+	};
+
 	AttributeDelta.prototype.toString = function() {
-		return getClassName( this ) + ': ' +
+		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			`"${ this.key }": -> ${ JSON.stringify( this.value ) }, ${ this.range }, ${ this.operations.length } ops`;
 	};
 
 	InsertDelta.prototype.toString = function() {
 		const op = this._insertOperation;
 
-		return getClassName( this ) + ': ' +
+		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			`[ ${ op.nodes.length } ] -> ${ op.position }`;
 	};
 
 	MarkerDelta.prototype.toString = function() {
 		const op = this.operations[ 0 ];
 
-		return getClassName( this ) + ': ' +
+		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			`"${ op.name }": ${ op.oldRange } -> ${ op.newRange }`;
 	};
 
 	MergeDelta.prototype.toString = function() {
-		return getClassName( this ) + ': ' +
+		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			this.position.toString();
 	};
 
@@ -350,38 +385,38 @@ function enableLoggingTools() {
 			opStrings.push( `${ range } -> ${ op.targetPosition }` );
 		}
 
-		return getClassName( this ) + ': ' +
+		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			opStrings.join( '; ' );
 	};
 
 	RenameDelta.prototype.toString = function() {
 		const op = this.operations[ 0 ];
 
-		return getClassName( this ) + ': ' +
+		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			`${ op.position }: "${ op.oldName }" -> "${ op.newName }"`;
 	};
 
 	RootAttributeDelta.prototype.toString = function() {
 		const op = this.operations[ 0 ];
 
-		return getClassName( this ) + ': ' +
+		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			`"${ op.key }": ${ JSON.stringify( op.oldValue ) } -> ${ JSON.stringify( op.newValue ) }, ${ op.root.rootName }`;
 	};
 
 	SplitDelta.prototype.toString = function() {
-		return getClassName( this ) + ': ' +
+		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			this.position.toString();
 	};
 
 	UnwrapDelta.prototype.toString = function() {
-		return getClassName( this ) + ': ' +
+		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			this.position.toString();
 	};
 
 	WrapDelta.prototype.toString = function() {
 		const wrapElement = this._insertOperation.nodes.getNode( 0 );
 
-		return getClassName( this ) + ': ' +
+		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 			`${ this.range } -> ${ wrapElement }`;
 	};
 

+ 3 - 1
packages/ckeditor5-engine/src/model/delta/basic-transformations.js

@@ -7,7 +7,9 @@
  * @module engine/model/delta/basic-transformations
  */
 
-import { addTransformationCase, defaultTransform } from './transform';
+import deltaTransform from './transform';
+const addTransformationCase = deltaTransform.addTransformationCase;
+const defaultTransform = deltaTransform.defaultTransform;
 
 import Range from '../range';
 import Position from '../position';

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

@@ -51,6 +51,13 @@ export default class DeltaFactory {
 			delta.addOperation( OperationFactory.fromJSON( operation, doc ) );
 		}
 
+		// Rewrite all other properties.
+		for ( let prop in json ) {
+			if ( prop != '__className' && delta[ prop ] === undefined ) {
+				delta[ prop ] = json[ prop ];
+			}
+		}
+
 		return delta;
 	}
 

+ 226 - 222
packages/ckeditor5-engine/src/model/delta/transform.js

@@ -14,198 +14,256 @@ import arrayUtils from '@ckeditor/ckeditor5-utils/src/lib/lodash/array';
 
 const specialCases = new Map();
 
-/**
- * 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~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 {Boolean} isAMoreImportantThanB Flag indicating whether the delta which will be transformed (`a`) should be treated
- * as more important when resolving conflicts. Note that this flag is used only if provided deltas have same
- * {@link module:engine/model/delta/delta~Delta._priority priority}. If deltas have different priorities, their importance is resolved
- * automatically and overwrites this flag.
- * @returns {Array.<module:engine/model/delta/delta~Delta>} Result of the transformation.
- */
-export default function transform( a, b, isAMoreImportantThanB ) {
-	const transformAlgorithm = getTransformationCase( a, b ) || defaultTransform;
-
-	const transformed = transformAlgorithm( a, b, isAMoreImportantThanB );
-	const baseVersion = arrayUtils.last( b.operations ).baseVersion;
+const deltaTransform = {
+	/**
+	 * 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~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 {Boolean} isAMoreImportantThanB Flag indicating whether the delta which will be transformed (`a`) should be treated
+	 * as more important when resolving conflicts. Note that this flag is used only if provided deltas have same
+	 * {@link module:engine/model/delta/delta~Delta._priority priority}. If deltas have different priorities, their importance is resolved
+	 * automatically and overwrites this flag.
+	 * @returns {Array.<module:engine/model/delta/delta~Delta>} Result of the transformation.
+	 */
+	transform: function( a, b, isAMoreImportantThanB ) {
+		const transformAlgorithm = deltaTransform.getTransformationCase( a, b ) || deltaTransform.defaultTransform;
+
+		const transformed = transformAlgorithm( a, b, isAMoreImportantThanB );
+		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 {Boolean} isAMoreImportantThanB Flag indicating whether the delta which will be transformed (`a`) should be treated
+	 * as more important when resolving conflicts. Note that this flag is used only if provided deltas have same
+	 * {@link module:engine/model/delta/delta~Delta._priority priority}. If deltas have different priorities, their importance is resolved
+	 * automatically and overwrites this flag.
+	 * @returns {Array.<module:engine/model/delta/delta~Delta>} Result of the transformation, that is an array with single delta instance.
+	 */
+	defaultTransform: function( a, b, isAMoreImportantThanB ) {
+		// First, resolve the flag real value.
+		isAMoreImportantThanB = getPriority( a.constructor, b.constructor, isAMoreImportantThanB );
+
+		// Create a new delta instance. Make sure that the new delta is of same type as transformed delta.
+		// We will transform operations in that delta but it doesn't mean the delta's "meaning" which is connected to
+		// the delta's type. Since the delta's type is heavily used in transformations and probably other parts
+		// of system it is important to keep proper delta type through all transformation process.
+		const transformed = new a.constructor();
+
+		// 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 ( let 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 ( let 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
+
+					// Using push.apply because operationTransform function is returning an array with one or multiple results.
+					Array.prototype.push.apply( newByOps, operationTransform( opB, op, !isAMoreImportantThanB ) );
+
+					// Then, we transform operation from delta A by operation from delta B.
+					const results = operationTransform( op, opB, isAMoreImportantThanB );
+
+					// We replace currently processed operation from `ops` array by the results of transformation.
+					// Note, that we process single operation but the operationTransform result might be an array, so we
+					// might splice-in more operations. We will process them further in next iterations. Right now we
+					// just save them in `ops` array and move `i` pointer by proper offset.
+					Array.prototype.splice.apply( ops, [ i, 1 ].concat( results ) );
+
+					i += results.length - 1;
+				}
 
-	return updateBaseVersion( baseVersion, transformed );
-}
+				// 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.
+			}
 
-// Updates base versions of operations inside deltas (which are the results of delta transformation).
-function updateBaseVersion( baseVersion, deltas ) {
-	for ( let delta of deltas ) {
-		for ( let op of delta.operations ) {
-			op.baseVersion = ++baseVersion;
-		}
-	}
+			// We got through all delta B operations and have a final transformed state of an operation from delta A.
 
-	return deltas;
-}
+			// As previously mentioned, we substitute operations from delta B by their transformed equivalents.
+			byOps = newByOps;
+			newByOps = [];
 
-/**
- * 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 {Boolean} isAMoreImportantThanB Flag indicating whether the delta which will be transformed (`a`) should be treated
- * as more important when resolving conflicts. Note that this flag is used only if provided deltas have same
- * {@link module:engine/model/delta/delta~Delta._priority priority}. If deltas have different priorities, their importance is resolved
- * automatically and overwrites this flag.
- * @returns {Array.<module:engine/model/delta/delta~Delta>} Result of the transformation, that is an array with single delta instance.
- */
-export function defaultTransform( a, b, isAMoreImportantThanB ) {
-	// First, resolve the flag real value.
-	isAMoreImportantThanB = getPriority( a.constructor, b.constructor, isAMoreImportantThanB );
-
-	// Create a new delta instance. Make sure that the new delta is of same type as transformed delta.
-	// We will transform operations in that delta but it doesn't mean the delta's "meaning" which is connected to
-	// the delta's type. Since the delta's type is heavily used in transformations and probably other parts
-	// of system it is important to keep proper delta type through all transformation process.
-	const transformed = new a.constructor();
-
-	// 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 ( let 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 ( let 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
-
-				// Using push.apply because operationTransform function is returning an array with one or multiple results.
-				Array.prototype.push.apply( newByOps, operationTransform( opB, op, !isAMoreImportantThanB ) );
-
-				// Then, we transform operation from delta A by operation from delta B.
-				const results = operationTransform( op, opB, isAMoreImportantThanB );
-
-				// We replace currently processed operation from `ops` array by the results of transformation.
-				// Note, that we process single operation but the operationTransform result might be an array, so we
-				// might splice-in more operations. We will process them further in next iterations. Right now we
-				// just save them in `ops` array and move `i` pointer by proper offset.
-				Array.prototype.splice.apply( ops, [ i, 1 ].concat( results ) );
-
-				i += results.length - 1;
+			// We add transformed operation from delta A to newly created delta.
+			// Remember that transformed operation from delta A may consist of multiple operations.
+			for ( let op of ops ) {
+				transformed.addOperation( op );
 			}
 
-			// 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.
+			// In next loop, we will take another operation from delta A and transform it through (transformed) operations
+			// from delta B...
 		}
 
-		// We got through all delta B operations and have a final transformed state of an operation from delta A.
+		return [ transformed ];
+	},
+
+	/**
+	 * Adds a special case callback for given delta classes.
+	 *
+	 * @external module:engine/model/delta/transform~transform
+	 * @function module:engine/model/delta/transform~transform.addTransformationCase
+	 * @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: function( A, B, resolver ) {
+		let casesA = specialCases.get( A );
+
+		if ( !casesA ) {
+			casesA = new Map();
+			specialCases.set( A, casesA );
+		}
 
-		// As previously mentioned, we substitute operations from delta B by their transformed equivalents.
-		byOps = newByOps;
-		newByOps = [];
+		casesA.set( B, resolver );
+	},
 
-		// We add transformed operation from delta A to newly created delta.
-		// Remember that transformed operation from delta A may consist of multiple operations.
-		for ( let op of ops ) {
-			transformed.addOperation( op );
-		}
+	/**
+	 * 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: function( a, b ) {
+		let casesA = specialCases.get( a.constructor );
 
-		// In next loop, we will take another operation from delta A and transform it through (transformed) operations
-		// from delta B...
-	}
+		// 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();
 
-	return [ transformed ];
-}
+			for ( let caseClass of cases ) {
+				if ( a instanceof caseClass && specialCases.get( caseClass ).get( b.constructor ) ) {
+					casesA = specialCases.get( caseClass );
 
-/**
- * Adds a special case callback for given delta classes.
- *
- * @external module:engine/model/delta/transform~transform
- * @function module:engine/model/delta/transform~transform.addTransformationCase
- * @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.
- */
-export function addTransformationCase( A, B, resolver ) {
-	let casesA = specialCases.get( A );
+					break;
+				}
+			}
+		}
 
-	if ( !casesA ) {
-		casesA = new Map();
-		specialCases.set( A, casesA );
-	}
+		if ( casesA ) {
+			return casesA.get( b.constructor );
+		}
 
-	casesA.set( B, resolver );
-}
+		return undefined;
+	},
+
+	/**
+	 * Transforms two sets of deltas by themselves. Returns both transformed sets. Does not modify passed parameters.
+	 *
+	 * @param {Array.<module:engine/model/delta/delta~Delta>} deltasA Array with first set of deltas to transform.
+	 * @param {Array.<module:engine/model/delta/delta~Delta>} deltasB Array with second set of deltas to transform.
+	 * @param {Boolean} isAMoreImportantThanB Flag indicating whether the deltas from `deltasA` set should be treated as more
+	 * important when resolving conflicts.
+	 * @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: function( deltasA, deltasB, isAMoreImportantThanB ) {
+		let transformedDeltasA = Array.from( deltasA );
+		let transformedDeltasB = Array.from( deltasB );
+
+		for ( let i = 0; i < transformedDeltasA.length; i++ ) {
+			let deltaA = [ transformedDeltasA[ i ] ];
+
+			for ( let j = 0; j < transformedDeltasB.length; j++ ) {
+				let deltaB = [ transformedDeltasB[ j ] ];
+
+				for ( let k = 0; k < deltaA.length; k++ ) {
+					for ( let l = 0; l < deltaB.length; l++ ) {
+						let resultAB = deltaTransform.transform( deltaA[ k ], deltaB[ l ], isAMoreImportantThanB );
+						let resultBA = deltaTransform.transform( deltaB[ l ], deltaA[ k ], !isAMoreImportantThanB );
+
+						deltaA.splice( k, 1, ...resultAB );
+						k += resultAB.length - 1;
+
+						deltaB.splice( l, 1, ...resultBA );
+						l += resultBA.length - 1;
+					}
+				}
 
-/**
- * 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.
- */
-export function getTransformationCase( a, b ) {
-	let casesA = specialCases.get( a.constructor );
+				transformedDeltasB.splice( j, 1, ...deltaB );
+				j += deltaB.length - 1;
+			}
 
-	// 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();
+			transformedDeltasA.splice( i, 1, ...deltaA );
+			i += deltaA.length - 1;
+		}
 
-		for ( let caseClass of cases ) {
-			if ( a instanceof caseClass && specialCases.get( caseClass ).get( b.constructor ) ) {
-				casesA = specialCases.get( caseClass );
+		const opsDiffA = getOpsCount( transformedDeltasA ) - getOpsCount( deltasA );
+		const opsDiffB = getOpsCount( transformedDeltasB ) - getOpsCount( deltasB );
 
-				break;
-			}
+		if ( opsDiffB < opsDiffA ) {
+			padWithNoOps( transformedDeltasB, opsDiffA - opsDiffB );
+		} else if ( opsDiffA < opsDiffB ) {
+			padWithNoOps( transformedDeltasA, opsDiffB - opsDiffA );
 		}
+
+		return { deltasA: transformedDeltasA, deltasB: transformedDeltasB };
 	}
+};
 
-	if ( casesA ) {
-		return casesA.get( b.constructor );
+export default deltaTransform;
+
+// Updates base versions of operations inside deltas (which are the results of delta transformation).
+function updateBaseVersion( baseVersion, deltas ) {
+	for ( let delta of deltas ) {
+		for ( let op of delta.operations ) {
+			op.baseVersion = ++baseVersion;
+		}
 	}
 
-	return undefined;
+	return deltas;
 }
 
 // Checks priorities of passed constructors and decides which one is more important.
@@ -220,60 +278,6 @@ function getPriority( A, B, isAMoreImportantThanB ) {
 	}
 }
 
-/**
- * Transforms two sets of deltas by themselves. Returns both transformed sets. Does not modify passed parameters.
- *
- * @param {Array.<module:engine/model/delta/delta~Delta>} deltasA Array with first set of deltas to transform.
- * @param {Array.<module:engine/model/delta/delta~Delta>} deltasB Array with second set of deltas to transform.
- * @param {Boolean} isAMoreImportantThanB Flag indicating whether the deltas from `deltasA` set should be treated as more
- * important when resolving conflicts.
- * @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.
- */
-export function transformDeltaSets( deltasA, deltasB, isAMoreImportantThanB ) {
-	let transformedDeltasA = Array.from( deltasA );
-	let transformedDeltasB = Array.from( deltasB );
-
-	for ( let i = 0; i < transformedDeltasA.length; i++ ) {
-		let deltaA = [ transformedDeltasA[ i ] ];
-
-		for ( let j = 0; j < transformedDeltasB.length; j++ ) {
-			let deltaB = [ transformedDeltasB[ j ] ];
-
-			for ( let k = 0; k < deltaA.length; k++ ) {
-				for ( let l = 0; l < deltaB.length; l++ ) {
-					let resultAB = transform( deltaA[ k ], deltaB[ l ], isAMoreImportantThanB );
-					let resultBA = transform( deltaB[ l ], deltaA[ k ], !isAMoreImportantThanB );
-
-					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 };
-}
-
 // Returns number of operations in given array of deltas.
 function getOpsCount( deltas ) {
 	return deltas.reduce( ( current, delta ) => {

+ 196 - 27
packages/ckeditor5-engine/tests/dev-utils/enableenginedebug.js

@@ -20,6 +20,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 { RootAttributeDelta } from '../../src/model/delta/attributedelta';
@@ -31,6 +32,7 @@ import RenameDelta from '../../src/model/delta/renamedelta';
 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 ModelDocument from '../../src/model/document';
 import ModelDocumentFragment from '../../src/model/documentfragment';
 
@@ -201,7 +203,7 @@ describe( 'debug tools', () => {
 			it( 'AttributeOperation', () => {
 				const op = new AttributeOperation( ModelRange.createIn( modelRoot ), 'key', null, { foo: 'bar' }, 0 );
 
-				expect( op.toString() ).to.equal( 'AttributeOperation: "key": null -> {"foo":"bar"}, main [ 0 ] - [ 6 ]' );
+				expect( op.toString() ).to.equal( 'AttributeOperation( 0 ): "key": null -> {"foo":"bar"}, main [ 0 ] - [ 6 ]' );
 
 				op.log();
 				expect( log.calledWithExactly( op.toString() ) ).to.be.true;
@@ -210,7 +212,7 @@ describe( 'debug tools', () => {
 			it( 'InsertOperation', () => {
 				const op = new InsertOperation( ModelPosition.createAt( modelRoot, 3 ), [ new ModelText( 'abc' ) ], 0 );
 
-				expect( op.toString() ).to.equal( 'InsertOperation: [ 1 ] -> main [ 3 ]' );
+				expect( op.toString() ).to.equal( 'InsertOperation( 0 ): [ 1 ] -> main [ 3 ]' );
 
 				op.log();
 				expect( log.calledWithExactly( op.toString() ) ).to.be.true;
@@ -219,7 +221,7 @@ describe( 'debug tools', () => {
 			it( 'MarkerOperation', () => {
 				const op = new MarkerOperation( 'marker', null, ModelRange.createIn( modelRoot ), modelDoc.markers, 0 );
 
-				expect( op.toString() ).to.equal( 'MarkerOperation: "marker": null -> main [ 0 ] - [ 6 ]' );
+				expect( op.toString() ).to.equal( 'MarkerOperation( 0 ): "marker": null -> main [ 0 ] - [ 6 ]' );
 
 				op.log();
 				expect( log.calledWithExactly( op.toString() ) ).to.be.true;
@@ -228,7 +230,7 @@ describe( 'debug tools', () => {
 			it( 'MoveOperation', () => {
 				const op = new MoveOperation( ModelPosition.createAt( modelRoot, 1 ), 2, ModelPosition.createAt( modelRoot, 6 ), 0 );
 
-				expect( op.toString() ).to.equal( 'MoveOperation: main [ 1 ] - [ 3 ] -> main [ 6 ]' );
+				expect( op.toString() ).to.equal( 'MoveOperation( 0 ): main [ 1 ] - [ 3 ] -> main [ 6 ]' );
 
 				op.log();
 				expect( log.calledWithExactly( op.toString() ) ).to.be.true;
@@ -237,16 +239,16 @@ describe( 'debug tools', () => {
 			it( 'NoOperation', () => {
 				const op = new NoOperation( 0 );
 
-				expect( op.toString() ).to.equal( 'NoOperation' );
+				expect( op.toString() ).to.equal( 'NoOperation( 0 )' );
 
 				op.log();
-				expect( log.calledWithExactly( 'NoOperation' ) ).to.be.true;
+				expect( log.calledWithExactly( op.toString() ) ).to.be.true;
 			} );
 
 			it( 'RenameOperation', () => {
 				const op = new RenameOperation( ModelPosition.createAt( modelRoot, 1 ), 'old', 'new', 0 );
 
-				expect( op.toString() ).to.equal( 'RenameOperation: main [ 1 ]: "old" -> "new"' );
+				expect( op.toString() ).to.equal( 'RenameOperation( 0 ): main [ 1 ]: "old" -> "new"' );
 
 				op.log();
 				expect( log.calledWithExactly( op.toString() ) ).to.be.true;
@@ -255,7 +257,7 @@ describe( 'debug tools', () => {
 			it( 'RootAttributeOperation', () => {
 				const op = new RootAttributeOperation( modelRoot, 'key', 'old', null, 0 );
 
-				expect( op.toString() ).to.equal( 'RootAttributeOperation: "key": "old" -> null, main' );
+				expect( op.toString() ).to.equal( 'RootAttributeOperation( 0 ): "key": "old" -> null, main' );
 
 				op.log();
 				expect( log.calledWithExactly( op.toString() ) ).to.be.true;
@@ -283,7 +285,7 @@ describe( 'debug tools', () => {
 
 				delta.addOperation( op );
 
-				expect( delta.toString() ).to.equal( 'AttributeDelta: "key": -> {"foo":"bar"}, main [ 0 ] - [ 6 ], 1 ops' );
+				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;
@@ -295,7 +297,7 @@ describe( 'debug tools', () => {
 
 				delta.addOperation( op );
 
-				expect( delta.toString() ).to.equal( 'InsertDelta: [ 1 ] -> main [ 3 ]' );
+				expect( delta.toString() ).to.equal( 'InsertDelta( 0 ): [ 1 ] -> main [ 3 ]' );
 
 				delta.log();
 				expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
@@ -309,7 +311,7 @@ describe( 'debug tools', () => {
 
 				delta.addOperation( op );
 
-				expect( delta.toString() ).to.equal( 'MarkerDelta: "marker": null -> main [ 0 ] - [ 6 ]' );
+				expect( delta.toString() ).to.equal( 'MarkerDelta( 0 ): "marker": null -> main [ 0 ] - [ 6 ]' );
 
 				delta.log();
 				expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
@@ -329,7 +331,7 @@ describe( 'debug tools', () => {
 				delta.addOperation( move );
 				delta.addOperation( remove );
 
-				expect( delta.toString() ).to.equal( 'MergeDelta: otherRoot [ 1 ]' );
+				expect( delta.toString() ).to.equal( 'MergeDelta( 0 ): otherRoot [ 1 ]' );
 
 				delta.log();
 				expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
@@ -343,7 +345,7 @@ describe( 'debug tools', () => {
 				delta.addOperation( move1 );
 				delta.addOperation( move2 );
 
-				expect( delta.toString() ).to.equal( 'MoveDelta: main [ 0 ] - [ 1 ] -> main [ 3 ]; main [ 1 ] - [ 2 ] -> main [ 6 ]' );
+				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;
@@ -355,7 +357,7 @@ describe( 'debug tools', () => {
 
 				delta.addOperation( op );
 
-				expect( delta.toString() ).to.equal( 'RenameDelta: main [ 1 ]: "old" -> "new"' );
+				expect( delta.toString() ).to.equal( 'RenameDelta( 0 ): main [ 1 ]: "old" -> "new"' );
 
 				delta.log();
 				expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
@@ -367,7 +369,7 @@ describe( 'debug tools', () => {
 
 				delta.addOperation( op );
 
-				expect( delta.toString() ).to.equal( 'RootAttributeDelta: "key": "old" -> null, main' );
+				expect( delta.toString() ).to.equal( 'RootAttributeDelta( 0 ): "key": "old" -> null, main' );
 
 				delta.log();
 				expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
@@ -386,7 +388,7 @@ describe( 'debug tools', () => {
 				delta.addOperation( insert );
 				delta.addOperation( move );
 
-				expect( delta.toString() ).to.equal( 'SplitDelta: otherRoot [ 0, 1 ]' );
+				expect( delta.toString() ).to.equal( 'SplitDelta( 0 ): otherRoot [ 0, 1 ]' );
 
 				delta.log();
 				expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
@@ -399,13 +401,13 @@ describe( 'debug tools', () => {
 				otherRoot.appendChildren( [ unwrapEle ] );
 
 				const delta = new UnwrapDelta();
-				const move = new MoveOperation( ModelPosition.createAt( unwrapEle, 0 ), 3, ModelPosition.createAt( otherRoot, 0 ), 1 );
-				const remove = new RemoveOperation( ModelPosition.createAt( otherRoot, 3 ), 1, 0 );
+				const move = new MoveOperation( ModelPosition.createAt( unwrapEle, 0 ), 3, ModelPosition.createAt( otherRoot, 0 ), 0 );
+				const remove = new RemoveOperation( ModelPosition.createAt( otherRoot, 3 ), 1, 1 );
 
 				delta.addOperation( move );
 				delta.addOperation( remove );
 
-				expect( delta.toString() ).to.equal( 'UnwrapDelta: otherRoot [ 0 ]' );
+				expect( delta.toString() ).to.equal( 'UnwrapDelta( 0 ): otherRoot [ 0 ]' );
 
 				delta.log();
 				expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
@@ -420,7 +422,7 @@ describe( 'debug tools', () => {
 				delta.addOperation( insert );
 				delta.addOperation( move );
 
-				expect( delta.toString() ).to.equal( 'WrapDelta: main [ 0 ] - [ 6 ] -> <paragraph>' );
+				expect( delta.toString() ).to.equal( 'WrapDelta( 0 ): main [ 0 ] - [ 6 ] -> <paragraph>' );
 
 				delta.log();
 				expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
@@ -434,7 +436,7 @@ describe( 'debug tools', () => {
 
 			modelDoc.applyOperation( op );
 
-			expect( log.calledWithExactly( 'Applying InsertOperation: [ 1 ] -> main [ 0 ]' ) ).to.be.true;
+			expect( log.calledWithExactly( 'Applying InsertOperation( 0 ): [ 1 ] -> main [ 0 ]' ) ).to.be.true;
 		} );
 	} );
 
@@ -764,15 +766,182 @@ describe( 'debug tools', () => {
 		} );
 	} );
 
+	describe( 'should provide means for saving delta history transformation', () => {
+		let document, root, otherRoot;
+
+		beforeEach( () => {
+			document = new ModelDocument();
+			root = document.createRoot();
+			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();
+			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, false );
+
+			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, true );
+			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: 1
+			} );
+		} );
+
+		it( 'recreate delta using history', () => {
+			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 );
+
+			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, false );
+			original = deltaTransform.transform( original[ 0 ], deltaC, true )[ 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 ),
+				history[ 0 ].wasImportant
+			);
+
+			recreated = recreated[ history[ 0 ].resultIndex ];
+
+			recreated = deltaTransform.transform(
+				recreated,
+				DeltaFactory.fromJSON( JSON.parse( history[ 1 ].transformedBy ), document ),
+				history[ 1 ].wasImportant
+			);
+
+			recreated = recreated[ history[ 1 ].resultIndex ];
+
+			delete original.history;
+			delete recreated.history;
+
+			expect( JSON.stringify( recreated ) ).to.equal( JSON.stringify( original ) );
+		} );
+	} );
+
 	function expectLog( expectedLogMsg ) {
 		expect( log.calledWithExactly( expectedLogMsg ) ).to.be.true;
 		log.reset();
 	}
-} );
 
-function wrapInDelta( op ) {
-	const delta = new Delta();
-	delta.addOperation( op );
+	function wrapInDelta( op ) {
+		const delta = new Delta();
+		delta.addOperation( op );
 
-	return op;
-}
+		return op;
+	}
+} );

+ 2 - 1
packages/ckeditor5-engine/tests/model/delta/transform/attributedelta.js

@@ -6,7 +6,8 @@
 import transformations from '../../../../src/model/delta/basic-transformations';
 /*jshint unused: false*/
 
-import transform from '../../../../src/model/delta/transform';
+import deltaTransform from '../../../../src/model/delta/transform';
+const transform = deltaTransform.transform;
 
 import Element from '../../../../src/model/element';
 import Text from '../../../../src/model/text';

+ 2 - 1
packages/ckeditor5-engine/tests/model/delta/transform/delta.js

@@ -6,7 +6,8 @@
 import transformations from '../../../../src/model/delta/basic-transformations';
 /*jshint unused: false*/
 
-import transform from '../../../../src/model/delta/transform';
+import deltaTransform from '../../../../src/model/delta/transform';
+const transform = deltaTransform.transform;
 
 import Position from '../../../../src/model/position';
 import MoveOperation from '../../../../src/model/operation/moveoperation';

+ 2 - 1
packages/ckeditor5-engine/tests/model/delta/transform/insertdelta.js

@@ -6,7 +6,8 @@
 import transformations from '../../../../src/model/delta/basic-transformations';
 /*jshint unused: false*/
 
-import transform from '../../../../src/model/delta/transform';
+import deltaTransform from '../../../../src/model/delta/transform';
+const transform = deltaTransform.transform;
 
 import Element from '../../../../src/model/element';
 import Position from '../../../../src/model/position';

+ 2 - 1
packages/ckeditor5-engine/tests/model/delta/transform/markerdelta.js

@@ -6,7 +6,8 @@
 import transformations from '../../../../src/model/delta/basic-transformations';
 /*jshint unused: false*/
 
-import transform from '../../../../src/model/delta/transform';
+import deltaTransform from '../../../../src/model/delta/transform';
+const transform = deltaTransform.transform;
 
 import Element from '../../../../src/model/element';
 import Position from '../../../../src/model/position';

+ 2 - 1
packages/ckeditor5-engine/tests/model/delta/transform/mergedelta.js

@@ -6,7 +6,8 @@
 import transformations from '../../../../src/model/delta/basic-transformations';
 /*jshint unused: false*/
 
-import transform from '../../../../src/model/delta/transform';
+import deltaTransform from '../../../../src/model/delta/transform';
+const transform = deltaTransform.transform;
 
 import Element from '../../../../src/model/element';
 import Position from '../../../../src/model/position';

+ 2 - 1
packages/ckeditor5-engine/tests/model/delta/transform/movedelta.js

@@ -6,7 +6,8 @@
 import transformations from '../../../../src/model/delta/basic-transformations';
 /*jshint unused: false*/
 
-import transform from '../../../../src/model/delta/transform';
+import deltaTransform from '../../../../src/model/delta/transform';
+const transform = deltaTransform.transform;
 
 import Position from '../../../../src/model/position';
 import Range from '../../../../src/model/range';

+ 2 - 1
packages/ckeditor5-engine/tests/model/delta/transform/removedelta.js

@@ -6,7 +6,8 @@
 import transformations from '../../../../src/model/delta/basic-transformations';
 /*jshint unused: false*/
 
-import transform from '../../../../src/model/delta/transform';
+import deltaTransform from '../../../../src/model/delta/transform';
+const transform = deltaTransform.transform;
 
 import Element from '../../../../src/model/element';
 import Position from '../../../../src/model/position';

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

@@ -6,11 +6,11 @@
 import transformations from '../../../../src/model/delta/basic-transformations';
 /*jshint unused: false*/
 
-import transform from '../../../../src/model/delta/transform';
+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 RenameDelta from '../../../../src/model/delta/renamedelta';
 import RenameOperation from '../../../../src/model/operation/renameoperation';

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

@@ -6,7 +6,8 @@
 import transformations from '../../../../src/model/delta/basic-transformations';
 /*jshint unused: false*/
 
-import transform from '../../../../src/model/delta/transform';
+import deltaTransform from '../../../../src/model/delta/transform';
+const transform = deltaTransform.transform;
 
 import Element from '../../../../src/model/element';
 import Position from '../../../../src/model/position';
@@ -22,7 +23,6 @@ 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 RemoveOperation from '../../../../src/model/operation/removeoperation';
 import NoOperation from '../../../../src/model/operation/nooperation';
 import RenameOperation from '../../../../src/model/operation/renameoperation';
 

+ 15 - 1
packages/ckeditor5-engine/tests/model/delta/transform/transform.js

@@ -6,7 +6,8 @@
 import transformations from '../../../../src/model/delta/basic-transformations';
 /*jshint unused: false*/
 
-import { transformDeltaSets } from '../../../../src/model/delta/transform';
+import deltaTransform from '../../../../src/model/delta/transform';
+const transformDeltaSets = deltaTransform.transformDeltaSets;
 
 import Document from '../../../../src/model/document';
 import Element from '../../../../src/model/element';
@@ -44,6 +45,19 @@ describe( 'transform', () => {
 	} );
 
 	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 deltas', () => {
 			const insertDelta = getInsertDelta( new Position( root, [ 0, 4 ] ), new Text( 'xxx' ), baseVersion );
 			const removeDelta = getRemoveDelta( new Position( root, [ 0, 0 ] ), 2, baseVersion );

+ 2 - 1
packages/ckeditor5-engine/tests/model/delta/transform/unwrapdelta.js

@@ -6,7 +6,8 @@
 import transformations from '../../../../src/model/delta/basic-transformations';
 /*jshint unused: false*/
 
-import transform from '../../../../src/model/delta/transform';
+import deltaTransform from '../../../../src/model/delta/transform';
+const transform = deltaTransform.transform;
 
 import Element from '../../../../src/model/element';
 import Position from '../../../../src/model/position';

+ 2 - 1
packages/ckeditor5-engine/tests/model/delta/transform/weakinsertdelta.js

@@ -6,7 +6,8 @@
 import transformations from '../../../../src/model/delta/basic-transformations';
 /*jshint unused: false*/
 
-import transform from '../../../../src/model/delta/transform';
+import deltaTransform from '../../../../src/model/delta/transform';
+const transform = deltaTransform.transform;
 
 import Text from '../../../../src/model/text';
 import Position from '../../../../src/model/position';

+ 2 - 1
packages/ckeditor5-engine/tests/model/delta/transform/wrapdelta.js

@@ -6,7 +6,8 @@
 import transformations from '../../../../src/model/delta/basic-transformations';
 /*jshint unused: false*/
 
-import transform from '../../../../src/model/delta/transform';
+import deltaTransform from '../../../../src/model/delta/transform';
+const transform = deltaTransform.transform;
 
 import Element from '../../../../src/model/element';
 import Position from '../../../../src/model/position';