Przeglądaj źródła

Added: introduce context in operations and delta transformations.

Szymon Cofalik 8 lat temu
rodzic
commit
7abb5fb325
26 zmienionych plików z 800 dodań i 622 usunięć
  1. 5 5
      packages/ckeditor5-engine/src/dev-utils/enableenginedebug.js
  2. 0 7
      packages/ckeditor5-engine/src/model/delta/attributedelta.js
  3. 37 35
      packages/ckeditor5-engine/src/model/delta/basic-transformations.js
  4. 0 12
      packages/ckeditor5-engine/src/model/delta/delta.js
  5. 0 7
      packages/ckeditor5-engine/src/model/delta/insertdelta.js
  6. 0 7
      packages/ckeditor5-engine/src/model/delta/movedelta.js
  7. 0 7
      packages/ckeditor5-engine/src/model/delta/splitdelta.js
  8. 252 47
      packages/ckeditor5-engine/src/model/delta/transform.js
  9. 0 7
      packages/ckeditor5-engine/src/model/delta/unwrapdelta.js
  10. 0 7
      packages/ckeditor5-engine/src/model/delta/wrapdelta.js
  11. 61 125
      packages/ckeditor5-engine/src/model/operation/transform.js
  12. 8 8
      packages/ckeditor5-engine/tests/dev-utils/enableenginedebug.js
  13. 6 5
      packages/ckeditor5-engine/tests/model/delta/transform/attributedelta.js
  14. 6 2
      packages/ckeditor5-engine/tests/model/delta/transform/delta.js
  15. 8 5
      packages/ckeditor5-engine/tests/model/delta/transform/insertdelta.js
  16. 9 8
      packages/ckeditor5-engine/tests/model/delta/transform/markerdelta.js
  17. 8 7
      packages/ckeditor5-engine/tests/model/delta/transform/mergedelta.js
  18. 4 3
      packages/ckeditor5-engine/tests/model/delta/transform/movedelta.js
  19. 5 4
      packages/ckeditor5-engine/tests/model/delta/transform/removedelta.js
  20. 4 3
      packages/ckeditor5-engine/tests/model/delta/transform/renamedelta.js
  21. 21 20
      packages/ckeditor5-engine/tests/model/delta/transform/splitdelta.js
  22. 272 132
      packages/ckeditor5-engine/tests/model/delta/transform/transform.js
  23. 4 3
      packages/ckeditor5-engine/tests/model/delta/transform/unwrapdelta.js
  24. 4 3
      packages/ckeditor5-engine/tests/model/delta/transform/weakinsertdelta.js
  25. 5 4
      packages/ckeditor5-engine/tests/model/delta/transform/wrapdelta.js
  26. 81 149
      packages/ckeditor5-engine/tests/model/operation/transform.js

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

@@ -334,15 +334,15 @@ function enableLoggingTools() {
 
 	const _deltaTransformTransform = deltaTransform.transform;
 
-	deltaTransform.transform = function( a, b, isAMoreImportantThanB ) {
+	deltaTransform.transform = function( a, b, context ) {
 		let results;
 
 		try {
-			results = _deltaTransformTransform( a, b, isAMoreImportantThanB );
+			results = _deltaTransformTransform( a, b, context );
 		} catch ( e ) {
 			logger.error( 'Error during delta transformation!' );
-			logger.error( a.toString() + ( isAMoreImportantThanB ? ' (important)' : '' ) );
-			logger.error( b.toString() + ( isAMoreImportantThanB ? '' : ' (important)' ) );
+			logger.error( a.toString() + ( context.isStrong ? ' (important)' : '' ) );
+			logger.error( b.toString() + ( context.isStrong ? '' : ' (important)' ) );
 
 			throw e;
 		}
@@ -351,7 +351,7 @@ function enableLoggingTools() {
 			results[ i ]._saveHistory( {
 				before: a,
 				transformedBy: b,
-				wasImportant: !!isAMoreImportantThanB,
+				wasImportant: !!context.isStrong,
 				resultIndex: i,
 				resultsTotal: results.length
 			} );

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

@@ -104,13 +104,6 @@ export default class AttributeDelta extends Delta {
 	static get className() {
 		return 'engine.model.delta.AttributeDelta';
 	}
-
-	/**
-	 * @inheritDoc
-	 */
-	static get _priority() {
-		return 20;
-	}
 }
 
 /**

+ 37 - 35
packages/ckeditor5-engine/src/model/delta/basic-transformations.js

@@ -37,10 +37,10 @@ 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, isStrong ) => {
+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, isStrong );
+	const deltas = defaultTransform( a, b, context );
 
 	if ( a.range.containsPosition( b.position ) ) {
 		deltas.push( _getComplementaryAttrDelta( b, a ) );
@@ -50,10 +50,10 @@ addTransformationCase( AttributeDelta, WeakInsertDelta, ( a, b, isStrong ) => {
 } );
 
 // Add special case for AttributeDelta x SplitDelta transformation.
-addTransformationCase( AttributeDelta, SplitDelta, ( a, b, isStrong ) => {
+addTransformationCase( AttributeDelta, SplitDelta, ( a, b, context ) => {
 	const splitPosition = new Position( b.position.root, b.position.path.slice( 0, -1 ) );
 
-	const deltas = defaultTransform( a, b, isStrong );
+	const deltas = defaultTransform( a, b, context );
 
 	for ( const operation of a.operations ) {
 		// If a node that has been split has it's attribute updated, we should also update attribute of
@@ -85,7 +85,7 @@ addTransformationCase( AttributeDelta, SplitDelta, ( a, b, isStrong ) => {
 } );
 
 // Add special case for InsertDelta x MergeDelta transformation.
-addTransformationCase( InsertDelta, MergeDelta, ( a, b, isStrong ) => {
+addTransformationCase( InsertDelta, MergeDelta, ( a, b, context ) => {
 	// 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.
@@ -96,7 +96,7 @@ addTransformationCase( InsertDelta, MergeDelta, ( a, b, isStrong ) => {
 		];
 	}
 
-	return defaultTransform( a, b, isStrong );
+	return defaultTransform( a, b, context );
 } );
 
 function transformMarkerDelta( a, b ) {
@@ -122,7 +122,7 @@ addTransformationCase( MarkerDelta, MoveDelta, transformMarkerDelta );
 addTransformationCase( MarkerDelta, RenameDelta, transformMarkerDelta );
 
 // Add special case for MoveDelta x MergeDelta transformation.
-addTransformationCase( MoveDelta, MergeDelta, ( a, b, isStrong ) => {
+addTransformationCase( MoveDelta, MergeDelta, ( 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.
@@ -140,22 +140,22 @@ addTransformationCase( MoveDelta, MergeDelta, ( a, b, isStrong ) => {
 		];
 	}
 
-	return defaultTransform( a, b, isStrong );
+	return defaultTransform( a, b, context );
 } );
 
 // Add special case for MergeDelta x InsertDelta transformation.
-addTransformationCase( MergeDelta, InsertDelta, ( a, b, isStrong ) => {
+addTransformationCase( MergeDelta, InsertDelta, ( a, b, context ) => {
 	// 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 ( a.position.isEqual( b.position ) ) {
 		return [ noDelta() ];
 	}
 
-	return defaultTransform( a, b, isStrong );
+	return defaultTransform( a, b, context );
 } );
 
 // Add special case for MergeDelta x MoveDelta transformation.
-addTransformationCase( MergeDelta, MoveDelta, ( a, b, isStrong ) => {
+addTransformationCase( MergeDelta, MoveDelta, ( 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.
 
@@ -169,16 +169,16 @@ addTransformationCase( MergeDelta, MoveDelta, ( a, b, isStrong ) => {
 		return [ noDelta() ];
 	}
 
-	return defaultTransform( a, b, isStrong );
+	return defaultTransform( a, b, context );
 } );
 
 // Add special case for SplitDelta x SplitDelta transformation.
-addTransformationCase( SplitDelta, SplitDelta, ( a, b, isStrong ) => {
+addTransformationCase( SplitDelta, SplitDelta, ( a, b, context ) => {
 	const pathA = a.position.getParentPath();
 	const pathB = b.position.getParentPath();
 
 	// The special case is for splits inside the same parent.
-	if ( compareArrays( pathA, pathB ) == 'same' ) {
+	if ( a.position.root == b.position.root && compareArrays( pathA, pathB ) == 'same' ) {
 		if ( a.position.offset == b.position.offset ) {
 			// We are applying split at the position where split already happened. Additional split is not needed.
 			return [ noDelta() ];
@@ -225,31 +225,32 @@ addTransformationCase( SplitDelta, SplitDelta, ( a, b, isStrong ) => {
 		}
 	}
 
-	return defaultTransform( a, b, isStrong );
+	return defaultTransform( a, b, context );
 } );
 
 // Add special case for SplitDelta x UnwrapDelta transformation.
-addTransformationCase( SplitDelta, UnwrapDelta, ( a, b, isStrong ) => {
+addTransformationCase( SplitDelta, UnwrapDelta, ( 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 ( compareArrays( b.position.path, a.position.getParentPath() ) === 'same' ) {
+	if ( a.position.root == b.position.root && compareArrays( b.position.path, a.position.getParentPath() ) === 'same' ) {
 		return [ noDelta() ];
 	}
 
-	return defaultTransform( a, b, isStrong );
+	return defaultTransform( a, b, context );
 } );
 
 // Add special case for SplitDelta x WrapDelta transformation.
-addTransformationCase( SplitDelta, WrapDelta, ( a, b, isStrong ) => {
+addTransformationCase( SplitDelta, WrapDelta, ( 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 operateInSameParent = compareArrays( a.position.getParentPath(), b.range.start.getParentPath() ) === 'same';
+	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 ( compareArrays( a.position.getParentPath(), b.range.end.getShiftedBy( -1 ).path ) === 'same' ) {
+	} 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.
@@ -287,7 +288,7 @@ addTransformationCase( SplitDelta, WrapDelta, ( a, b, isStrong ) => {
 		return [ delta ];
 	}
 
-	return defaultTransform( a, b, isStrong );
+	return defaultTransform( a, b, context );
 } );
 
 // Add special case for SplitDelta x WrapDelta transformation.
@@ -316,17 +317,17 @@ addTransformationCase( SplitDelta, AttributeDelta, ( a, b ) => {
 } );
 
 // Add special case for UnwrapDelta x SplitDelta transformation.
-addTransformationCase( UnwrapDelta, SplitDelta, ( a, b, isStrong ) => {
+addTransformationCase( UnwrapDelta, SplitDelta, ( 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 ( compareArrays( a.position.path, b.position.getParentPath() ) === 'same' ) {
+	if ( a.position.root == b.position.root && compareArrays( a.position.path, b.position.getParentPath() ) === 'same' ) {
 		return [
 			b.getReversed(),
 			a.clone()
 		];
 	}
 
-	return defaultTransform( a, b, isStrong );
+	return defaultTransform( a, b, context );
 } );
 
 // Add special case for WeakInsertDelta x AttributeDelta transformation.
@@ -342,11 +343,12 @@ addTransformationCase( WeakInsertDelta, AttributeDelta, ( a, b ) => {
 } );
 
 // Add special case for WrapDelta x SplitDelta transformation.
-addTransformationCase( WrapDelta, SplitDelta, ( a, b, isStrong ) => {
+addTransformationCase( WrapDelta, SplitDelta, ( 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 operateInSameParent = compareArrays( a.range.start.getParentPath(), b.position.getParentPath() ) === 'same';
+	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 ) {
@@ -354,7 +356,7 @@ addTransformationCase( WrapDelta, SplitDelta, ( a, b, isStrong ) => {
 			b.getReversed(),
 			a.clone()
 		];
-	} else if ( compareArrays( b.position.getParentPath(), a.range.end.getShiftedBy( -1 ).path ) === 'same' ) {
+	} 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.
@@ -369,14 +371,14 @@ addTransformationCase( WrapDelta, SplitDelta, ( a, b, isStrong ) => {
 		return [ delta ];
 	}
 
-	return defaultTransform( a, b, isStrong );
+	return defaultTransform( a, b, context );
 } );
 
 // Add special case for RenameDelta x SplitDelta transformation.
-addTransformationCase( RenameDelta, SplitDelta, ( a, b, isStrong ) => {
+addTransformationCase( RenameDelta, SplitDelta, ( a, b, context ) => {
 	const splitPosition = new Position( b.position.root, b.position.path.slice( 0, -1 ) );
 
-	const deltas = defaultTransform( a, b, isStrong );
+	const deltas = defaultTransform( a, b, context );
 
 	if ( a.operations[ 0 ].position.isEqual( splitPosition ) ) {
 		// If a node that has been split has it's name changed, we should also change name of
@@ -408,8 +410,8 @@ addTransformationCase( SplitDelta, RenameDelta, ( a, b ) => {
 } );
 
 // Add special case for RemoveDelta x SplitDelta transformation.
-addTransformationCase( RemoveDelta, SplitDelta, ( a, b, isStrong ) => {
-	const deltas = defaultTransform( a, b, isStrong );
+addTransformationCase( RemoveDelta, SplitDelta, ( a, b, context ) => {
+	const deltas = defaultTransform( a, b, context );
 	const insertPosition = b._cloneOperation.position;
 
 	// In case if `defaultTransform` returned more than one delta.
@@ -427,7 +429,7 @@ addTransformationCase( RemoveDelta, SplitDelta, ( a, b, isStrong ) => {
 } );
 
 // Add special case for SplitDelta x RemoveDelta transformation.
-addTransformationCase( SplitDelta, RemoveDelta, ( a, b, isStrong ) => {
+addTransformationCase( SplitDelta, RemoveDelta, ( a, b, context ) => {
 	b = b.clone();
 
 	const insertPosition = a._cloneOperation.position;
@@ -440,7 +442,7 @@ addTransformationCase( SplitDelta, RemoveDelta, ( a, b, isStrong ) => {
 		}
 	}
 
-	return defaultTransform( a, b, isStrong );
+	return defaultTransform( a, b, context );
 } );
 
 // Helper function for `AttributeDelta` class transformations.

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

@@ -162,18 +162,6 @@ export default class Delta {
 	static get className() {
 		return 'engine.model.delta.Delta';
 	}
-
-	/**
-	 * Delta priority. Used in {@link module:engine/model/delta/transform~transform delta transformations}. Delta with the higher
-	 * priority will be treated as more important when resolving transformation conflicts. If deltas have same
-	 * priority, other factors will be used to determine which delta is more important.
-	 *
-	 * @private
-	 * @type {Number}
-	 */
-	static get _priority() {
-		return 0;
-	}
 }
 
 DeltaFactory.register( Delta );

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

@@ -75,13 +75,6 @@ export default class InsertDelta extends Delta {
 	static get className() {
 		return 'engine.model.delta.InsertDelta';
 	}
-
-	/**
-	 * @inheritDoc
-	 */
-	static get _priority() {
-		return 20;
-	}
 }
 
 /**

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

@@ -83,13 +83,6 @@ export default class MoveDelta extends Delta {
 	static get className() {
 		return 'engine.model.delta.MoveDelta';
 	}
-
-	/**
-	 * @inheritDoc
-	 */
-	static get _priority() {
-		return 20;
-	}
 }
 
 function addMoveOperation( batch, delta, sourcePosition, howMany, targetPosition ) {

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

@@ -82,13 +82,6 @@ export default class SplitDelta extends Delta {
 	static get className() {
 		return 'engine.model.delta.SplitDelta';
 	}
-
-	/**
-	 * @inheritDoc
-	 */
-	static get _priority() {
-		return 5;
-	}
 }
 
 /**

+ 252 - 47
packages/ckeditor5-engine/src/model/delta/transform.js

@@ -4,13 +4,17 @@
  */
 
 /**
+ * @protected
  * @module engine/model/delta/transform
  */
 
 import Delta from './delta';
+import MoveDelta from './movedelta';
 import operationTransform from '../operation/transform';
 import NoOperation from '../operation/nooperation';
+import MoveOperation from '../operation/moveoperation';
 import arrayUtils from '@ckeditor/ckeditor5-utils/src/lib/lodash/array';
+import compareArrays from '@ckeditor/ckeditor5-utils/src/comparearrays';
 
 const specialCases = new Map();
 
@@ -37,18 +41,24 @@ const transform = {
 	 *
 	 * @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.
+	 * @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, isAMoreImportantThanB ) {
+	transform( a, b, context ) {
+		if ( context.useAdditionalContext ) {
+			_setContext( a, b, context );
+		}
+
 		const transformAlgorithm = transform.getTransformationCase( a, b ) || transform.defaultTransform;
 
-		const transformed = transformAlgorithm( a, b, isAMoreImportantThanB );
+		// 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;
 
+		if ( context.useAdditionalContext ) {
+			_updateContext( a, transformed, context );
+		}
+
 		return updateBaseVersion( baseVersion, transformed );
 	},
 
@@ -60,16 +70,10 @@ const transform = {
 	 *
 	 * @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.
+	 * @param {module:engine/model/delta/transform~transformationContext} context Transformation context object.
 	 * @returns {Array.<module:engine/model/delta/delta~Delta>} Result of the transformation, that is an array with single delta instance.
 	 */
-	defaultTransform( a, b, isAMoreImportantThanB ) {
-		// First, resolve the flag real value.
-		isAMoreImportantThanB = getPriority( a.constructor, b.constructor, isAMoreImportantThanB );
-
+	defaultTransform( a, b, context ) {
 		// 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
@@ -110,19 +114,28 @@ const transform = {
 					// 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 );
+					// 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 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.
+					// 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.
@@ -148,7 +161,21 @@ const transform = {
 			// from delta B...
 		}
 
-		return [ transformed ];
+		// If `MoveDelta` or `RemoveDelta` operation got split, instead of returning one delta with multiple operations,
+		// return multiple deltas with single operation each.
+		if ( transformed instanceof MoveDelta && transformed.operations.length > 1 ) {
+			const result = [];
+
+			for ( const operation of transformed.operations ) {
+				const delta = new a.constructor();
+				delta.addOperation( operation );
+				result.push( delta );
+			}
+
+			return result;
+		} else {
+			return [ transformed ];
+		}
 	},
 
 	/**
@@ -200,22 +227,48 @@ const transform = {
 	},
 
 	/**
-	 * Transforms two sets of deltas by themselves. Returns both transformed sets. Does not modify passed parameters.
+	 * Transforms two sets of deltas by themselves. Returns both transformed sets.
 	 *
-	 * @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.
+	 * @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 "context 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.
+	 * @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, isAMoreImportantThanB ) {
+	transformDeltaSets( deltasA, deltasB, document = null ) {
 		const transformedDeltasA = Array.from( deltasA );
 		const transformedDeltasB = Array.from( deltasB );
 
+		const useAdditionalContext = document !== null;
+
+		const contextAB = {
+			isStrong: true,
+			useAdditionalContext
+		};
+
+		const contextBA = {
+			isStrong: false,
+			useAdditionalContext
+		};
+
+		if ( useAdditionalContext ) {
+			const additionalContext = {
+				forceWeakRemove: true,
+				document
+			};
+
+			// We need two different instances for `wasAffected` property.
+			Object.assign( contextAB, { wasAffected: new Map() }, additionalContext );
+			Object.assign( contextBA, { wasAffected: new Map() }, additionalContext );
+		}
+
 		for ( let i = 0; i < transformedDeltasA.length; i++ ) {
 			const deltaA = [ transformedDeltasA[ i ] ];
 
@@ -224,8 +277,8 @@ const transform = {
 
 				for ( let k = 0; k < deltaA.length; k++ ) {
 					for ( let l = 0; l < deltaB.length; l++ ) {
-						const resultAB = transform.transform( deltaA[ k ], deltaB[ l ], isAMoreImportantThanB );
-						const resultBA = transform.transform( deltaB[ l ], deltaA[ k ], !isAMoreImportantThanB );
+						const resultAB = transform.transform( deltaA[ k ], deltaB[ l ], contextAB );
+						const resultBA = transform.transform( deltaB[ l ], deltaA[ k ], contextBA );
 
 						deltaA.splice( k, 1, ...resultAB );
 						k += resultAB.length - 1;
@@ -269,18 +322,6 @@ function updateBaseVersion( baseVersion, deltas ) {
 	return deltas;
 }
 
-// Checks priorities of passed constructors and decides which one is more important.
-// If both priorities are same, value passed in `isAMoreImportantThanB` parameter is used.
-function getPriority( A, B, isAMoreImportantThanB ) {
-	if ( A._priority > B._priority ) {
-		return true;
-	} else if ( A._priority < B._priority ) {
-		return false;
-	} else {
-		return isAMoreImportantThanB;
-	}
-}
-
 // Returns number of operations in given array of deltas.
 function getOpsCount( deltas ) {
 	return deltas.reduce( ( current, delta ) => {
@@ -302,3 +343,167 @@ function padWithNoOps( deltas, howMany ) {
 
 	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 );
+	_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).
+//
+// Keep in mind that this problem only affects `MoveOperation` (and operations that derive from it).
+function _setInsertBeforeContext( a, b, context ) {
+	// If `b` is a delta that undoes other delta...
+	if ( context.document.history.isUndoingDelta( b ) ) {
+		// Get the undone delta...
+		const undoneDelta = context.document.history.getUndoneDelta( b );
+		// 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.
+//
+// Keep in mind that this problem only affects `MoveOperation` (and operations that derive from it).
+function _setForceNotSticky( b, context ) {
+	// If `b` delta is undoing or undone delta, stickiness should not be taken into consideration.
+	if ( context.document.history.isUndoingDelta( b ) || context.document.history.isUndoneDelta( b ) ) {
+		context.forceNotSticky = true;
+	}
+}
+
+// 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() );
+	}
+
+	let wasAffected = false;
+
+	// 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( b, 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;
+
+	const wasAffected = context.wasAffected.get( oldDelta );
+
+	context.wasAffected.delete( oldDelta );
+
+	for ( const delta of newDeltas ) {
+		context.wasAffected.set( delta, new Map( wasAffected ) );
+	}
+}
+
+/**
+ * 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 - 7
packages/ckeditor5-engine/src/model/delta/unwrapdelta.js

@@ -61,13 +61,6 @@ export default class UnwrapDelta extends Delta {
 	static get className() {
 		return 'engine.model.delta.UnwrapDelta';
 	}
-
-	/**
-	 * @inheritDoc
-	 */
-	static get _priority() {
-		return 10;
-	}
 }
 
 /**

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

@@ -88,13 +88,6 @@ export default class WrapDelta extends Delta {
 	static get className() {
 		return 'engine.model.delta.WrapDelta';
 	}
-
-	/**
-	 * @inheritDoc
-	 */
-	static get _priority() {
-		return 10;
-	}
 }
 
 /**

+ 61 - 125
packages/ckeditor5-engine/src/model/operation/transform.js

@@ -52,12 +52,11 @@ import compareArrays from '@ckeditor/ckeditor5-utils/src/comparearrays';
  * `a` by `b` and also `b` by `a`. In both transformations the same operation has to be the important one. If we assume
  * that first or the second passed operation is always more important we won't be able to solve this case.
  *
- * @external module:engine/model/model.operation
+ * @protected
  * @function module:engine/model/operation/transform~transform
  * @param {module:engine/model/operation/operation~Operation} a Operation that will be transformed.
  * @param {module:engine/model/operation/operation~Operation} b Operation to transform by.
- * @param {Boolean} isAMoreImportantThanB Flag indicating whether the operation which will be transformed (`a`) should be treated
- * as more important when resolving conflicts.
+ * @param {module:engine/model/delta/transform~transformationContext} [context] Transformation context.
  * @returns {Array.<module:engine/model/operation/operation~Operation>} Result of the transformation.
  */
 
@@ -67,12 +66,12 @@ const ot = {
 	InsertOperation: {
 		// Transforms InsertOperation `a` by InsertOperation `b`. Accepts a flag stating whether `a` is more important
 		// than `b` when it comes to resolving conflicts. Returns results as an array of operations.
-		InsertOperation( a, b, isStrong ) {
+		InsertOperation( a, b, context ) {
 			// Transformed operations are always new instances, not references to the original operations.
 			const transformed = a.clone();
 
 			// Transform insert position by the other operation position.
-			transformed.position = transformed.position._getTransformedByInsertion( b.position, b.nodes.maxOffset, !isStrong );
+			transformed.position = transformed.position._getTransformedByInsertion( b.position, b.nodes.maxOffset, !context.isStrong );
 
 			return [ transformed ];
 		},
@@ -87,11 +86,17 @@ const ot = {
 
 		// Transforms InsertOperation `a` by MoveOperation `b`. Accepts a flag stating whether `a` is more important
 		// than `b` when it comes to resolving conflicts. Returns results as an array of operations.
-		MoveOperation( a, b, isStrong ) {
+		MoveOperation( a, b, context ) {
 			const transformed = a.clone();
 
 			// Transform insert position by the other operation parameters.
-			transformed.position = a.position._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, !isStrong, b.isSticky );
+			transformed.position = a.position._getTransformedByMove(
+				b.sourcePosition,
+				b.targetPosition,
+				b.howMany,
+				!context.isStrong,
+				b.isSticky && !context.forceNotSticky
+			);
 
 			return [ transformed ];
 		}
@@ -111,7 +116,7 @@ const ot = {
 
 		// Transforms AttributeOperation `a` by AttributeOperation `b`. Accepts a flag stating whether `a` is more important
 		// than `b` when it comes to resolving conflicts. Returns results as an array of operations.
-		AttributeOperation( a, b, isStrong ) {
+		AttributeOperation( a, b, context ) {
 			if ( a.key === b.key ) {
 				// If operations attributes are in conflict, check if their ranges intersect and manage them properly.
 
@@ -127,7 +132,7 @@ const ot = {
 					// If this operation is more important, we also want to apply change to the part of the
 					// original range that has already been changed by the other operation. Since that range
 					// got changed we also have to update `oldValue`.
-					if ( isStrong ) {
+					if ( context.isStrong ) {
 						operations.push( new AttributeOperation( common, b.key, b.newValue, a.newValue, a.baseVersion ) );
 					} else if ( operations.length === 0 ) {
 						operations.push( new NoOperation( 0 ) );
@@ -155,18 +160,6 @@ const ot = {
 			// This will aggregate transformed ranges.
 			let ranges = [];
 
-			// Special case when MoveOperation is in fact a RemoveOperation. RemoveOperation not only moves nodes but also
-			// creates a "holder" element for them in graveyard. If there was a RemoveOperation pointing to an offset
-			// before this AttributeOperation, we have to increment AttributeOperation's offset.
-			if ( b instanceof RemoveOperation && b._needsHolderElement &&
-				a.range.root == b.targetPosition.root && a.range.start.path[ 0 ] >= b._holderElementOffset
-			) {
-				// Do not change original operation!
-				a = a.clone();
-				a.range.start.path[ 0 ]++;
-				a.range.end.path[ 0 ]++;
-			}
-
 			// Difference is a part of changed range that is modified by AttributeOperation but is not affected
 			// by MoveOperation. This can be zero, one or two ranges (if moved range is inside changed range).
 			// Right now we will make a simplification and join difference ranges and transform them as one. We will cover rangeB later.
@@ -215,9 +208,9 @@ const ot = {
 
 		// Transforms RootAttributeOperation `a` by RootAttributeOperation `b`. Accepts a flag stating whether `a` is more important
 		// than `b` when it comes to resolving conflicts. Returns results as an array of operations.
-		RootAttributeOperation( a, b, isStrong ) {
+		RootAttributeOperation( a, b, context ) {
 			if ( a.root === b.root && a.key === b.key ) {
-				if ( ( a.newValue !== b.newValue && !isStrong ) || a.newValue === b.newValue ) {
+				if ( ( a.newValue !== b.newValue && !context.isStrong ) || a.newValue === b.newValue ) {
 					return [ new NoOperation( a.baseVersion ) ];
 				}
 			}
@@ -250,12 +243,12 @@ const ot = {
 
 		// Transforms RenameOperation `a` by RenameOperation `b`. Accepts a flag stating whether `a` is more important
 		// than `b` when it comes to resolving conflicts. Returns results as an array of operations.
-		RenameOperation( a, b, isStrong ) {
+		RenameOperation( a, b, context ) {
 			// Clone the operation, we don't want to alter the original operation.
 			const clone = a.clone();
 
 			if ( a.position.isEqual( b.position ) ) {
-				if ( isStrong ) {
+				if ( context.isStrong ) {
 					clone.oldName = b.newName;
 				} else {
 					return [ new NoOperation( a.baseVersion ) ];
@@ -303,12 +296,12 @@ const ot = {
 
 		// Transforms MarkerOperation `a` by MarkerOperation `b`. Accepts a flag stating whether `a` is more important
 		// than `b` when it comes to resolving conflicts. Returns results as an array of operations.
-		MarkerOperation( a, b, isStrong ) {
+		MarkerOperation( a, b, context ) {
 			// Clone the operation, we don't want to alter the original operation.
 			const clone = a.clone();
 
 			if ( a.name == b.name ) {
-				if ( isStrong ) {
+				if ( context.isStrong ) {
 					clone.oldRange = b.newRange;
 				} else {
 					return [ new NoOperation( a.baseVersion ) ];
@@ -340,27 +333,20 @@ const ot = {
 	MoveOperation: {
 		// Transforms MoveOperation `a` by InsertOperation `b`. Accepts a flag stating whether `a` is more important
 		// than `b` when it comes to resolving conflicts. Returns results as an array of operations.
-		InsertOperation( a, b, isStrong ) {
+		InsertOperation( a, b, context ) {
 			// Create range from MoveOperation properties and transform it by insertion.
 			let range = Range.createFromPositionAndShift( a.sourcePosition, a.howMany );
-			range = range._getTransformedByInsertion( b.position, b.nodes.maxOffset, false, a.isSticky )[ 0 ];
+			range = range._getTransformedByInsertion( b.position, b.nodes.maxOffset, false, a.isSticky && !context.forceNotSticky )[ 0 ];
 
 			const result = new a.constructor(
 				range.start,
 				range.end.offset - range.start.offset,
-				a instanceof RemoveOperation ?
-					a.baseVersion :
-					a.targetPosition._getTransformedByInsertion( b.position, b.nodes.maxOffset, !isStrong ),
-				a instanceof RemoveOperation ? undefined : a.baseVersion
+				a.targetPosition._getTransformedByInsertion( b.position, b.nodes.maxOffset, !context.isStrong ),
+				a.baseVersion
 			);
 
 			result.isSticky = a.isSticky;
 
-			if ( a instanceof RemoveOperation ) {
-				result._needsHolderElement = a._needsHolderElement;
-				result._holderElementOffset = a._holderElementOffset;
-			}
-
 			return [ result ];
 		},
 
@@ -374,7 +360,7 @@ const ot = {
 
 		// Transforms MoveOperation `a` by MoveOperation `b`. Accepts a flag stating whether `a` is more important
 		// than `b` when it comes to resolving conflicts. Returns results as an array of operations.
-		MoveOperation( a, b, isStrong ) {
+		MoveOperation( a, b, context ) {
 			// Special case when both move operations' target positions are inside nodes that are
 			// being moved by the other move operation. So in other words, we move ranges into inside of each other.
 			// This case can't be solved reasonably (on the other hand, it should not happen often).
@@ -384,39 +370,14 @@ const ot = {
 				return [ b.getReversed() ];
 			}
 
-			// Special case when operation transformed by is RemoveOperation. RemoveOperation not only moves nodes but also
-			// (usually) creates a "holder" element for them in graveyard. This has to be taken into consideration when
-			// transforming operations that operate in graveyard root.
-			if ( b instanceof RemoveOperation && b._needsHolderElement ) {
-				a = a.clone();
-
-				const aSourceOffset = a.sourcePosition.path[ 0 ];
-				const aTargetOffset = a.targetPosition.path[ 0 ];
-
-				// We can't use Position#_getTransformedByInsertion because it works a bit differently and we would get wrong results.
-				// That's because `sourcePosition`/`targetPosition` is, for example, `[ 1, 0 ]`, while `insertionPosition` is
-				// `[ 1 ]`. In this case, `sourcePosition`/`targetPosition` would always be transformed but in fact, only
-				// "holderElement" offsets should be looked at (so `[ 1 ]` in case of `sourcePosition`/`targetPosition`, not `[ 1, 0 ]` ).
-				// This is why we are doing it "by hand".
-				if ( a.sourcePosition.root == b.targetPosition.root ) {
-					if ( aSourceOffset > b._holderElementOffset || aSourceOffset == b._holderElementOffset ) {
-						a.sourcePosition.path[ 0 ]++;
-					}
-				}
-
-				if ( a.targetPosition.root == b.targetPosition.root ) {
-					if ( aTargetOffset > b._holderElementOffset || ( aTargetOffset == b._holderElementOffset && isStrong ) ) {
-						a.targetPosition.path[ 0 ]++;
-					}
-				}
-			}
-
 			// If only one of operations is a remove operation, we force remove operation to be the "stronger" one
 			// to provide more expected results.
-			if ( a instanceof RemoveOperation && !( b instanceof RemoveOperation ) ) {
-				isStrong = true;
-			} else if ( !( a instanceof RemoveOperation ) && b instanceof RemoveOperation ) {
-				isStrong = false;
+			if ( !context.forceWeakRemove || b.isPermanent ) {
+				if ( a instanceof RemoveOperation && !( b instanceof RemoveOperation ) ) {
+					context.isStrong = true;
+				} else if ( !( a instanceof RemoveOperation ) && b instanceof RemoveOperation ) {
+					context.isStrong = false;
+				}
 			}
 
 			// Create ranges from MoveOperations properties.
@@ -426,29 +387,21 @@ const ot = {
 			// This will aggregate transformed ranges.
 			const ranges = [];
 
-			// All the other non-special cases are treated by generic algorithm below.
+			// First, get the "difference part" of `a` operation source range.
 			const difference = joinRanges( rangeA.getDifference( rangeB ) );
 
 			if ( difference ) {
-				difference.start = difference.start._getTransformedByMove(
-					b.sourcePosition,
-					b.targetPosition,
-					b.howMany,
-					!a.isSticky,
-					false
-				);
-				difference.end = difference.end._getTransformedByMove(
-					b.sourcePosition,
-					b.targetPosition,
-					b.howMany,
-					a.isSticky,
-					false
-				);
+				// `a` operation might be sticky. If it is, we might need to update its source range to include
+				// nodes moved-in by `b` operation. Sometimes however, stickiness should not be applied (`context.forceNotSticky`).
+				const sticky = a.isSticky && !context.forceNotSticky;
+
+				difference.start = difference.start._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, !sticky );
+				difference.end = difference.end._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, sticky );
 
 				ranges.push( difference );
 			}
 
-			// Then, we have to manage the common part of both move ranges.
+			// Then, we have to manage the "common part" of both move ranges.
 			const common = rangeA.getIntersection( rangeB );
 
 			// If MoveOperations has common range it can be one of two:
@@ -472,22 +425,25 @@ const ot = {
 			// smaller range will be moved to larger range target. The effect of this transformation feels natural.
 			// Also if we wouldn't do that, we would get different results on both sides of transformation (i.e. in
 			// collaborative editing).
-			const aIsInside = rangeB.containsRange( rangeA ) &&
+			const aInside =
+				common && rangeA.isEqual( common ) && !rangeA.isEqual( rangeB ) &&
 				(
 					rangeB.containsPosition( a.targetPosition ) ||
 					rangeB.start.isEqual( a.targetPosition ) ||
 					rangeB.end.isEqual( a.targetPosition )
 				);
 
-			if ( common !== null && ( aCompB === 'extension' || ( aCompB === 'same' && isStrong ) || aIsInside ) && !bTargetsToA ) {
-				// Here we do not need to worry that newTargetPosition is inside moved range, because that
-				// would mean that the MoveOperation targets into itself, and that is incorrect operation.
-				// Instead, we calculate the new position of that part of original range.
+			if ( common !== null && ( aCompB === 'extension' || ( aCompB === 'same' && context.isStrong ) || aInside ) && !bTargetsToA ) {
+				// Calculate the new position of that part of original range.
 				common.start = common.start._getCombined( b.sourcePosition, b.getMovedRangeStart() );
 				common.end = common.end._getCombined( b.sourcePosition, b.getMovedRangeStart() );
 
-				// We have to take care of proper range order.
-				if ( difference && rangeA.start.isBefore( rangeB.start ) ) {
+				// We have to take care of proper range order. "Farther" ranges should be processed first, so they
+				// won't mess up ranges in following operations. So, for example, if we have to move range
+				// [ 4 ] - [ 6 ] and then [ 0 ] - [ 2 ], we should do it in this order, because if we would
+				// move [ 0 ] - [ 2 ] range first, it would invalidate moving [ 4 ] - [ 6 ] range (it would
+				// have to be updated to [ 2 ] - [ 4 ] range).
+				if ( difference && difference.start.isBefore( common.start ) ) {
 					ranges.push( common );
 				} else {
 					ranges.unshift( common );
@@ -495,61 +451,41 @@ const ot = {
 			}
 
 			if ( ranges.length === 0 ) {
-				// At this point we transformed this operation's source ranges it means that nothing should be changed.
-				// But since we need to return an instance of Operation we return an array with NoOperation.
-
-				if ( a instanceof RemoveOperation ) {
-					// If `a` operation was RemoveOperation, we cannot convert it to NoOperation.
-					// This is because RemoveOperation creates a holder in graveyard.
-					// Even if we "remove nothing" we need a RemoveOperation to create holder element
-					// so that the tree structure is synchronised between clients.
-					// Note that this can happen only if both operations are remove operations, because in
-					// other case RemoveOperation would be forced to be stronger and there would be a common range to move.
-					a = a.clone();
-					a.howMany = 0;
-					a.sourcePosition = b.targetPosition;
-
-					return [ a ];
-				} else {
-					return [ new NoOperation( a.baseVersion ) ];
-				}
+				// If there are no "source ranges", nothing should be changed.
+				return [ new NoOperation( a.baseVersion ) ];
 			}
 
+			// Check whether there is a forced order of nodes or to use `isStrong` flag for conflict resolving.
+			const insertBefore = context.insertBefore === undefined ? !context.isStrong : context.insertBefore;
+
 			// Target position also could be affected by the other MoveOperation. We will transform it.
 			const newTargetPosition = a.targetPosition._getTransformedByMove(
 				b.sourcePosition,
 				b.targetPosition,
 				b.howMany,
-				!isStrong,
-				b.isSticky || aIsInside
+				insertBefore,
+				( b.isSticky && !context.forceNotSticky ) || aInside
 			);
 
 			// Map transformed range(s) to operations and return them.
-			return ranges.reverse().map( ( range, i ) => {
+			return ranges.reverse().map( range => {
 				// We want to keep correct operation class.
 				const result = new a.constructor(
 					range.start,
 					range.end.offset - range.start.offset,
-					a instanceof RemoveOperation ? a.baseVersion : newTargetPosition,
-					a instanceof RemoveOperation ? undefined : a.baseVersion
+					newTargetPosition,
+					a.baseVersion
 				);
 
 				result.isSticky = a.isSticky;
 
-				if ( a instanceof RemoveOperation ) {
-					// Transformed `RemoveOperation` needs graveyard holder only when the original operation needed it.
-					// If `RemoveOperation` got split into two or more operations, only first operation needs graveyard holder.
-					result._needsHolderElement = a._needsHolderElement && i === 0;
-					result._holderElementOffset = a._holderElementOffset;
-				}
-
 				return result;
 			} );
 		}
 	}
 };
 
-function transform( a, b, isStrong ) {
+function transform( a, b, context = { isStrong: false } ) {
 	let group, algorithm;
 
 	if ( a instanceof InsertOperation ) {
@@ -586,7 +522,7 @@ function transform( a, b, isStrong ) {
 		}
 	}
 
-	const transformed = algorithm( a, b, isStrong );
+	const transformed = algorithm( a, b, context );
 
 	return updateBaseVersions( a.baseVersion, transformed );
 }

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

@@ -861,7 +861,7 @@ describe( 'debug tools', () => {
 			const opC = new MoveOperation( ModelPosition.createAt( root, 4 ), 2, ModelPosition.createAt( root, 0 ), 1 );
 			deltaC.addOperation( opC );
 
-			let result = deltaTransform.transform( deltaA, deltaB, false );
+			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 );
@@ -877,7 +877,7 @@ describe( 'debug tools', () => {
 			const firstResultWithoutHistory = result[ 0 ].clone();
 			delete firstResultWithoutHistory.history;
 
-			result = deltaTransform.transform( result[ 0 ], deltaC, true );
+			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 );
 
@@ -911,15 +911,15 @@ describe( 'debug tools', () => {
 			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 ];
+			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 ),
-				history[ 0 ].wasImportant
+				{ isStrong: history[ 0 ].wasImportant, document, wasAffected: new Map() }
 			);
 
 			recreated = recreated[ history[ 0 ].resultIndex ];
@@ -927,7 +927,7 @@ describe( 'debug tools', () => {
 			recreated = deltaTransform.transform(
 				recreated,
 				DeltaFactory.fromJSON( JSON.parse( history[ 1 ].transformedBy ), document ),
-				history[ 1 ].wasImportant
+				{ isStrong: history[ 1 ].wasImportant, document, wasAffected: new Map() }
 			);
 
 			recreated = recreated[ history[ 1 ].resultIndex ];
@@ -952,7 +952,7 @@ describe( 'debug tools', () => {
 					throw new Error();
 				};
 
-				expect( () => deltaTransform.transform( deltaA, deltaB, true ) ).to.throw( 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;
 			} );
@@ -970,7 +970,7 @@ describe( 'debug tools', () => {
 					throw new Error();
 				};
 
-				expect( () => deltaTransform.transform( deltaA, deltaB, false ) ).to.throw( 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;
 			} );

+ 6 - 5
packages/ckeditor5-engine/tests/model/delta/transform/attributedelta.js

@@ -25,12 +25,13 @@ import {
 } from '../../../../tests/model/delta/transform/_utils/utils';
 
 describe( 'transform', () => {
-	let doc, root, baseVersion;
+	let doc, root, baseVersion, context;
 
 	beforeEach( () => {
 		doc = getFilledDocument();
 		root = doc.getRoot();
 		baseVersion = doc.version;
+		context = { isStrong: false };
 	} );
 
 	describe( 'AttributeDelta by', () => {
@@ -51,7 +52,7 @@ describe( 'transform', () => {
 					baseVersion
 				);
 
-				const transformed = transform( attrDelta, insertDelta );
+				const transformed = transform( attrDelta, insertDelta, context );
 
 				expect( transformed.length ).to.equal( 2 );
 
@@ -117,7 +118,7 @@ describe( 'transform', () => {
 				const insertPosition = new Position( root, [ 5 ] );
 				const insertDelta = getWeakInsertDelta( insertPosition, 'abc', baseVersion );
 
-				const transformed = transform( attrDelta, insertDelta );
+				const transformed = transform( attrDelta, insertDelta, context );
 
 				expect( transformed.length ).to.equal( 1 );
 
@@ -151,7 +152,7 @@ describe( 'transform', () => {
 				const splitPosition = new Position( root, [ 3, 3, 3, 3 ] );
 				const splitDelta = getSplitDelta( splitPosition, new Element( 'p' ), 9, baseVersion );
 
-				const transformed = transform( attrDelta, splitDelta );
+				const transformed = transform( attrDelta, splitDelta, context );
 
 				expect( transformed.length ).to.equal( 2 );
 
@@ -219,7 +220,7 @@ describe( 'transform', () => {
 				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 );
+				const transformed = transform( attrDelta, splitDelta, context );
 
 				expect( transformed.length ).to.equal( 2 );
 

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

@@ -42,11 +42,15 @@ describe( '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 );
-		const deltaBbyA = transform( deltaB, deltaA );
+		const deltaAbyB = transform( deltaA, deltaB, context );
+		const deltaBbyA = transform( deltaB, deltaA, context );
 
 		expect( deltaAbyB.length ).to.equal( 1 );
 

+ 8 - 5
packages/ckeditor5-engine/tests/model/delta/transform/insertdelta.js

@@ -30,13 +30,16 @@ import {
 } from '../../../../tests/model/delta/transform/_utils/utils';
 
 describe( 'transform', () => {
-	let doc, root, gy, baseVersion;
+	let doc, root, gy, baseVersion, context;
 
 	beforeEach( () => {
 		doc = getFilledDocument();
 		root = doc.getRoot();
 		gy = doc.graveyard;
 		baseVersion = doc.version;
+		context = {
+			isStrong: false
+		};
 	} );
 
 	describe( 'InsertDelta by', () => {
@@ -55,7 +58,7 @@ describe( 'transform', () => {
 				const insertPositionB = new Position( root, [ 3, 1 ] );
 				const insertDeltaB = getInsertDelta( insertPositionB, [ new Element( 'c' ), new Element( 'd' ) ], baseVersion );
 
-				const transformed = transform( insertDelta, insertDeltaB );
+				const transformed = transform( insertDelta, insertDeltaB, context );
 
 				expect( transformed.length ).to.equal( 1 );
 
@@ -76,7 +79,7 @@ describe( 'transform', () => {
 		describe( 'MergeDelta', () => {
 			it( 'merge in same position as insert', () => {
 				const mergeDelta = getMergeDelta( insertPosition, 4, 12, baseVersion );
-				const transformed = transform( insertDelta, mergeDelta );
+				const transformed = transform( insertDelta, mergeDelta, context );
 
 				baseVersion = mergeDelta.operations.length;
 
@@ -131,7 +134,7 @@ describe( 'transform', () => {
 
 			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 );
+				const transformed = transform( insertDelta, mergeDelta, context );
 
 				baseVersion = mergeDelta.operations.length;
 
@@ -164,7 +167,7 @@ describe( 'transform', () => {
 
 			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 );
+				const transformed = transform( insertDelta, mergeDelta, context );
 
 				baseVersion = mergeDelta.operations.length;
 

+ 9 - 8
packages/ckeditor5-engine/tests/model/delta/transform/markerdelta.js

@@ -26,12 +26,13 @@ import {
 } from '../../../model/delta/transform/_utils/utils';
 
 describe( 'transform', () => {
-	let doc, root, baseVersion;
+	let doc, root, baseVersion, context;
 
 	beforeEach( () => {
 		doc = getFilledDocument();
 		root = doc.getRoot();
 		baseVersion = doc.version;
+		context = { isStrong: false };
 	} );
 
 	describe( 'MarkerDelta by', () => {
@@ -47,7 +48,7 @@ describe( 'transform', () => {
 
 			it( 'split inside oldRange', () => {
 				const splitDelta = getSplitDelta( new Position( root, [ 3, 1 ] ), new Element( 'div' ), 3, baseVersion );
-				const transformed = transform( markerDelta, splitDelta );
+				const transformed = transform( markerDelta, splitDelta, context );
 
 				baseVersion = splitDelta.operations.length;
 
@@ -72,7 +73,7 @@ describe( 'transform', () => {
 
 			it( 'split inside newRange', () => {
 				const splitDelta = getSplitDelta( new Position( root, [ 3, 3, 3, 4 ] ), new Element( 'p' ), 8, baseVersion );
-				const transformed = transform( markerDelta, splitDelta );
+				const transformed = transform( markerDelta, splitDelta, context );
 
 				baseVersion = splitDelta.operations.length;
 
@@ -107,7 +108,7 @@ describe( 'transform', () => {
 				// 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 );
+				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,
@@ -143,7 +144,7 @@ describe( 'transform', () => {
 				const wrapRange = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) );
 				const wrapDelta = getWrapDelta( wrapRange, wrapElement, baseVersion );
 
-				const transformed = transform( markerDelta, wrapDelta );
+				const transformed = transform( markerDelta, wrapDelta, context );
 
 				// It is expected, that ranges in MarkerDelta got correctly transformed:
 				// `oldRange` end is in wrapped element,
@@ -178,7 +179,7 @@ describe( 'transform', () => {
 				const unwrapPosition = new Position( root, [ 1, 0 ] );
 				const unwrapDelta = getUnwrapDelta( unwrapPosition, 4, baseVersion );
 
-				const transformed = transform( markerDelta, unwrapDelta );
+				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 ] ) );
@@ -209,7 +210,7 @@ describe( 'transform', () => {
 				const unwrapPosition = new Position( root, [ 1 ] );
 				const unwrapDelta = getUnwrapDelta( unwrapPosition, 4, baseVersion );
 
-				const transformed = transform( markerDelta, unwrapDelta );
+				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 ] ) );
@@ -239,7 +240,7 @@ describe( 'transform', () => {
 		const wrapRange = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) );
 		const wrapDelta = getWrapDelta( wrapRange, wrapElement, baseVersion );
 
-		const transformed = transform( markerDelta, wrapDelta );
+		const transformed = transform( markerDelta, wrapDelta, context );
 
 		expect( transformed.length ).to.equal( 1 );
 		expect( transformed[ 0 ].operations.length ).to.equal( 1 );

+ 8 - 7
packages/ckeditor5-engine/tests/model/delta/transform/mergedelta.js

@@ -31,13 +31,14 @@ import {
 } from '../../../../tests/model/delta/transform/_utils/utils';
 
 describe( 'transform', () => {
-	let doc, root, gy, baseVersion;
+	let doc, root, gy, baseVersion, context;
 
 	beforeEach( () => {
 		doc = getFilledDocument();
 		root = doc.getRoot();
 		gy = doc.graveyard;
 		baseVersion = doc.version;
+		context = { isStrong: false };
 	} );
 
 	describe( 'MergeDelta by', () => {
@@ -58,7 +59,7 @@ describe( 'transform', () => {
 
 			it( 'insert at same position as merge', () => {
 				const insertDelta = getInsertDelta( mergePosition, [ nodeA, nodeB ], baseVersion );
-				const transformed = transform( mergeDelta, insertDelta );
+				const transformed = transform( mergeDelta, insertDelta, context );
 
 				// Expected: MergeDelta gets ignored and is not applied.
 
@@ -89,7 +90,7 @@ describe( 'transform', () => {
 
 			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 );
+				const transformed = transform( mergeDelta, insertDelta, context );
 
 				baseVersion = insertDelta.operations.length;
 
@@ -132,7 +133,7 @@ describe( 'transform', () => {
 		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 );
+				const transformed = transform( mergeDelta, moveDelta, context );
 
 				baseVersion = moveDelta.operations.length;
 
@@ -160,7 +161,7 @@ describe( 'transform', () => {
 
 			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 );
+				const transformed = transform( mergeDelta, moveDelta, context );
 
 				expect( transformed.length ).to.equal( 1 );
 
@@ -199,7 +200,7 @@ describe( 'transform', () => {
 		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 );
+				const transformed = transform( mergeDelta, mergeDeltaB, context );
 
 				expect( transformed.length ).to.equal( 1 );
 
@@ -237,7 +238,7 @@ describe( 'transform', () => {
 				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 );
+				const transformed = transform( mergeDelta, mergeDeltaB, context );
 
 				expect( transformed.length ).to.equal( 1 );
 

+ 4 - 3
packages/ckeditor5-engine/tests/model/delta/transform/movedelta.js

@@ -27,13 +27,14 @@ import {
 } from '../../../../tests/model/delta/transform/_utils/utils';
 
 describe( 'transform', () => {
-	let doc, root, gy, baseVersion;
+	let doc, root, gy, baseVersion, context;
 
 	beforeEach( () => {
 		doc = getFilledDocument();
 		root = doc.getRoot();
 		gy = doc.graveyard;
 		baseVersion = doc.version;
+		context = { isStrong: false };
 	} );
 
 	describe( 'MoveDelta by', () => {
@@ -52,7 +53,7 @@ describe( 'transform', () => {
 				const mergePosition = new Position( root, [ 3, 3, 3 ] );
 				const mergeDelta = getMergeDelta( mergePosition, 4, 12, baseVersion );
 
-				const transformed = transform( moveDelta, mergeDelta );
+				const transformed = transform( moveDelta, mergeDelta, context );
 
 				expect( transformed.length ).to.equal( 2 );
 
@@ -112,7 +113,7 @@ describe( 'transform', () => {
 				const mergePosition = new Position( root, [ 3, 3 ] );
 				const mergeDelta = getMergeDelta( mergePosition, 1, 4, baseVersion );
 
-				const transformed = transform( moveDelta, mergeDelta );
+				const transformed = transform( moveDelta, mergeDelta, context );
 
 				expect( transformed.length ).to.equal( 1 );
 

+ 5 - 4
packages/ckeditor5-engine/tests/model/delta/transform/removedelta.js

@@ -30,13 +30,14 @@ import {
 } from '../../../../tests/model/delta/transform/_utils/utils';
 
 describe( 'transform', () => {
-	let doc, root, gy, baseVersion;
+	let doc, root, gy, baseVersion, context;
 
 	beforeEach( () => {
 		doc = getFilledDocument();
 		root = doc.getRoot();
 		gy = doc.graveyard;
 		baseVersion = doc.version;
+		context = { isStrong: false };
 	} );
 
 	describe( 'RemoveDelta by', () => {
@@ -49,7 +50,7 @@ describe( 'transform', () => {
 				const mergePosition = new Position( root, [ 3, 3, 3 ] );
 				const mergeDelta = getMergeDelta( mergePosition, 4, 12, baseVersion );
 
-				const transformed = transform( removeDelta, mergeDelta );
+				const transformed = transform( removeDelta, mergeDelta, context );
 
 				expect( transformed.length ).to.equal( 2 );
 
@@ -108,7 +109,7 @@ describe( 'transform', () => {
 				const nodeCopy = new Element( 'x' );
 				const splitDelta = getSplitDelta( splitPosition, nodeCopy, 2, baseVersion );
 
-				const transformed = transform( removeDelta, splitDelta );
+				const transformed = transform( removeDelta, splitDelta, context );
 
 				expect( transformed.length ).to.equal( 1 );
 
@@ -135,7 +136,7 @@ describe( 'transform', () => {
 				const nodeCopy = new Element( 'div' );
 				const splitDelta = getSplitDelta( splitPosition, nodeCopy, 2, baseVersion );
 
-				const transformed = transform( removeDelta, splitDelta );
+				const transformed = transform( removeDelta, splitDelta, context );
 
 				expect( transformed.length ).to.equal( 1 );
 

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

@@ -20,12 +20,13 @@ import {
 } from '../../../../tests/model/delta/transform/_utils/utils';
 
 describe( 'transform', () => {
-	let doc, root, baseVersion;
+	let doc, root, baseVersion, context;
 
 	beforeEach( () => {
 		doc = getFilledDocument();
 		root = doc.getRoot();
 		baseVersion = doc.version;
+		context = { isStrong: false };
 	} );
 
 	describe( 'RenameDelta by', () => {
@@ -42,7 +43,7 @@ describe( 'transform', () => {
 				const splitPosition = new Position( root, [ 3, 3, 3 ] );
 				const splitDelta = getSplitDelta( splitPosition, new Element( 'p' ), 9, baseVersion );
 
-				const transformed = transform( renameDelta, splitDelta );
+				const transformed = transform( renameDelta, splitDelta, context );
 
 				baseVersion = splitDelta.length;
 
@@ -85,7 +86,7 @@ describe( 'transform', () => {
 				const splitPosition = new Position( root, [ 3, 2, 1 ] );
 				const splitDelta = getSplitDelta( splitPosition, new Element( 'p' ), 9, baseVersion );
 
-				const transformed = transform( renameDelta, splitDelta );
+				const transformed = transform( renameDelta, splitDelta, context );
 
 				baseVersion = splitDelta.length;
 

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

@@ -37,13 +37,14 @@ import {
 } from '../../../../tests/model/delta/transform/_utils/utils';
 
 describe( 'transform', () => {
-	let doc, root, gy, baseVersion;
+	let doc, root, gy, baseVersion, context;
 
 	beforeEach( () => {
 		doc = getFilledDocument();
 		root = doc.getRoot();
 		gy = doc.graveyard;
 		baseVersion = doc.version;
+		context = { isStrong: false };
 	} );
 
 	describe( 'SplitDelta by', () => {
@@ -57,7 +58,7 @@ describe( 'transform', () => {
 		describe( 'SplitDelta', () => {
 			it( 'split in same parent and offset', () => {
 				const splitDeltaB = getSplitDelta( splitPosition, new Element( 'p' ), 9, baseVersion );
-				const transformed = transform( splitDelta, splitDeltaB );
+				const transformed = transform( splitDelta, splitDeltaB, context );
 
 				baseVersion = splitDeltaB.operations.length;
 
@@ -87,7 +88,7 @@ describe( 'transform', () => {
 
 			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 );
+				const transformed = transform( splitDelta, splitDeltaB, context );
 
 				baseVersion = splitDeltaB.operations.length;
 
@@ -140,7 +141,7 @@ describe( 'transform', () => {
 				);
 				splitDeltaB.operations[ 0 ] = reOp;
 
-				const transformed = transform( splitDelta, splitDeltaB );
+				const transformed = transform( splitDelta, splitDeltaB, context );
 
 				baseVersion = splitDeltaB.operations.length;
 
@@ -169,7 +170,7 @@ describe( 'transform', () => {
 
 			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 );
+				const transformed = transform( splitDelta, splitDeltaB, context );
 
 				baseVersion = splitDeltaB.operations.length;
 
@@ -222,7 +223,7 @@ describe( 'transform', () => {
 				);
 				splitDeltaB.operations[ 0 ] = reOp;
 
-				const transformed = transform( splitDelta, splitDeltaB );
+				const transformed = transform( splitDelta, splitDeltaB, context );
 
 				baseVersion = splitDeltaB.operations.length;
 
@@ -251,7 +252,7 @@ describe( 'transform', () => {
 
 			it( 'split in split parent', () => {
 				const splitDeltaB = getSplitDelta( new Position( root, [ 3, 3, 3 ] ), new Element( 'div' ), 1, baseVersion );
-				const transformed = transform( splitDelta, splitDeltaB );
+				const transformed = transform( splitDelta, splitDeltaB, context );
 
 				baseVersion = splitDeltaB.operations.length;
 
@@ -290,7 +291,7 @@ describe( 'transform', () => {
 		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 );
+				const transformed = transform( splitDelta, unwrapDelta, context );
 
 				baseVersion = unwrapDelta.operations.length;
 
@@ -319,7 +320,7 @@ describe( 'transform', () => {
 			it( 'split position indirectly in unwrapped node', () => {
 				const unwrapDelta = getUnwrapDelta( new Position( root, [ 3, 3 ] ), 4, baseVersion );
 
-				const transformed = transform( splitDelta, unwrapDelta );
+				const transformed = transform( splitDelta, unwrapDelta, context );
 
 				expect( transformed.length ).to.equal( 1 );
 
@@ -360,7 +361,7 @@ describe( 'transform', () => {
 				const wrapElement = new Element( 'E' );
 				const wrapDelta = getWrapDelta( wrapRange, wrapElement, baseVersion );
 
-				const transformed = transform( splitDelta, wrapDelta );
+				const transformed = transform( splitDelta, wrapDelta, context );
 
 				baseVersion = wrapDelta.operations.length;
 
@@ -391,7 +392,7 @@ describe( 'transform', () => {
 				const wrapElement = new Element( 'E' );
 				const wrapDelta = getWrapDelta( wrapRange, wrapElement, baseVersion );
 
-				const transformed = transform( splitDelta, wrapDelta );
+				const transformed = transform( splitDelta, wrapDelta, context );
 
 				expect( transformed.length ).to.equal( 1 );
 
@@ -430,7 +431,7 @@ describe( 'transform', () => {
 				const wrapElement = new Element( 'E' );
 				const wrapDelta = getWrapDelta( wrapRange, wrapElement, baseVersion );
 
-				const transformed = transform( splitDelta, wrapDelta );
+				const transformed = transform( splitDelta, wrapDelta, context );
 
 				expect( transformed.length ).to.equal( 1 );
 
@@ -477,7 +478,7 @@ describe( 'transform', () => {
 					Range.createFromPositionAndShift( new Position( root, [ 3, 3, 2 ] ), 3 ), 'key', null, 'newValue', baseVersion + 1
 				) );
 
-				const transformed = transform( splitDelta, attributeDelta );
+				const transformed = transform( splitDelta, attributeDelta, context );
 
 				baseVersion = attributeDelta.operations.length;
 
@@ -516,7 +517,7 @@ describe( 'transform', () => {
 					Range.createFromPositionAndShift( new Position( root, [ 3, 3, 2 ] ), 3 ), 'key', 'oldValue', null, baseVersion + 1
 				) );
 
-				const transformed = transform( splitDelta, attributeDelta );
+				const transformed = transform( splitDelta, attributeDelta, context );
 
 				baseVersion = attributeDelta.operations.length;
 
@@ -557,7 +558,7 @@ describe( 'transform', () => {
 					Range.createFromParentsAndOffsets( root, 0, root, 4 ), 'key', 'oldValue', 'newValue', baseVersion
 				) );
 
-				const transformed = transform( splitDelta, attributeDelta );
+				const transformed = transform( splitDelta, attributeDelta, context );
 
 				baseVersion = attributeDelta.operations.length;
 				expect( transformed.length ).to.equal( 1 );
@@ -591,7 +592,7 @@ describe( 'transform', () => {
 					new Position( root, [ 3, 3, 3 ] ), 'p', 'li', baseVersion
 				) );
 
-				const transformed = transform( splitDelta, renameDelta );
+				const transformed = transform( splitDelta, renameDelta, context );
 
 				baseVersion = renameDelta.operations.length;
 
@@ -624,7 +625,7 @@ describe( 'transform', () => {
 					new Position( root, [ 4 ] ), 'p', 'li', baseVersion
 				) );
 
-				const transformed = transform( splitDelta, renameDelta );
+				const transformed = transform( splitDelta, renameDelta, context );
 
 				baseVersion = renameDelta.operations.length;
 
@@ -662,7 +663,7 @@ describe( 'transform', () => {
 					new Position( root, [ 3, 3, 3 ] ), 'p', 'li', baseVersion
 				) );
 
-				const transformed = transform( splitDelta, renameDelta );
+				const transformed = transform( splitDelta, renameDelta, context );
 
 				baseVersion = renameDelta.operations.length;
 
@@ -699,7 +700,7 @@ describe( 'transform', () => {
 				const removeDelta = getRemoveDelta( removePosition, 3, baseVersion );
 				const removeOperation = removeDelta.operations[ 0 ];
 
-				const transformed = transform( splitDelta, removeDelta );
+				const transformed = transform( splitDelta, removeDelta, context );
 
 				expect( transformed.length ).to.equal( 1 );
 
@@ -735,7 +736,7 @@ describe( 'transform', () => {
 				const removeDelta = getRemoveDelta( removePosition, 2, baseVersion );
 				const removeOperation = removeDelta.operations[ 0 ];
 
-				const transformed = transform( splitDelta, removeDelta );
+				const transformed = transform( splitDelta, removeDelta, context );
 
 				expect( transformed.length ).to.equal( 1 );
 

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

@@ -15,18 +15,22 @@ 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 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
+	getRemoveDelta,
+	getMoveDelta,
+	getMergeDelta
 } from '../../../../tests/model/delta/transform/_utils/utils';
 
 describe( 'transform', () => {
@@ -55,72 +59,6 @@ describe( 'transform', () => {
 			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 );
-
-			const { deltasA, deltasB } = transformDeltaSets( [ insertDelta ], [ removeDelta ] );
-
-			expect( deltasA.length ).to.equal( 1 );
-			expect( deltasB.length ).to.equal( 1 );
-
-			expectDelta( deltasA[ 0 ], {
-				type: InsertDelta,
-				operations: [
-					{
-						type: InsertOperation,
-						position: new Position( root, [ 0, 2 ] ),
-						baseVersion: 1
-					}
-				]
-			} );
-
-			expectDelta( deltasB[ 0 ], {
-				type: RemoveDelta,
-				operations: [
-					{
-						type: RemoveOperation,
-						sourcePosition: new Position( root, [ 0, 0 ] ),
-						howMany: 2,
-						baseVersion: 1
-					}
-				]
-			} );
-		} );
-
-		it( 'should transform two deltas - reverse', () => {
-			const insertDelta = getInsertDelta( new Position( root, [ 0, 4 ] ), new Text( 'xxx' ), baseVersion );
-			const removeDelta = getRemoveDelta( new Position( root, [ 0, 0 ] ), 2, baseVersion );
-
-			const { deltasA, deltasB } = transformDeltaSets( [ removeDelta ], [ insertDelta ] );
-
-			expect( deltasA.length ).to.equal( 1 );
-			expect( deltasB.length ).to.equal( 1 );
-
-			expectDelta( deltasA[ 0 ], {
-				type: RemoveDelta,
-				operations: [
-					{
-						type: RemoveOperation,
-						sourcePosition: new Position( root, [ 0, 0 ] ),
-						howMany: 2,
-						baseVersion: 1
-					}
-				]
-			} );
-
-			expectDelta( deltasB[ 0 ], {
-				type: InsertDelta,
-				operations: [
-					{
-						type: InsertOperation,
-						position: new Position( root, [ 0, 2 ] ),
-						baseVersion: 1
-					}
-				]
-			} );
-		} );
-
 		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 );
@@ -128,10 +66,10 @@ describe( 'transform', () => {
 			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 ], true );
+			const { deltasA, deltasB } = transformDeltaSets( [ splitDelta, insertDeltaX ], [ removeDelta, insertDeltaY ] );
 
 			expect( deltasA.length ).to.equal( 3 );
-			expect( deltasB.length ).to.equal( 2 );
+			expect( deltasB.length ).to.equal( 3 );
 
 			expectDelta( deltasA[ 0 ], {
 				type: SplitDelta,
@@ -180,7 +118,13 @@ describe( 'transform', () => {
 						sourcePosition: new Position( root, [ 1, 0 ] ),
 						howMany: 1,
 						baseVersion: 3
-					},
+					}
+				]
+			} );
+
+			expectDelta( deltasB[ 1 ], {
+				type: RemoveDelta,
+				operations: [
 					{
 						type: RemoveOperation,
 						sourcePosition: new Position( root, [ 0, 2 ] ),
@@ -190,7 +134,7 @@ describe( 'transform', () => {
 				]
 			} );
 
-			expectDelta( deltasB[ 1 ], {
+			expectDelta( deltasB[ 2 ], {
 				type: InsertDelta,
 				operations: [
 					{
@@ -209,9 +153,9 @@ describe( 'transform', () => {
 			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 ], true );
+			const { deltasA, deltasB } = transformDeltaSets( [ removeDelta, insertDeltaY ], [ splitDelta, insertDeltaX ] );
 
-			expect( deltasA.length ).to.equal( 2 );
+			expect( deltasA.length ).to.equal( 3 );
 			expect( deltasB.length ).to.equal( 3 );
 
 			expectDelta( deltasA[ 0 ], {
@@ -222,7 +166,13 @@ describe( 'transform', () => {
 						sourcePosition: new Position( root, [ 1, 0 ] ),
 						howMany: 1,
 						baseVersion: 3
-					},
+					}
+				]
+			} );
+
+			expectDelta( deltasA[ 1 ], {
+				type: RemoveDelta,
+				operations: [
 					{
 						type: RemoveOperation,
 						sourcePosition: new Position( root, [ 0, 2 ] ),
@@ -232,7 +182,7 @@ describe( 'transform', () => {
 				]
 			} );
 
-			expectDelta( deltasA[ 1 ], {
+			expectDelta( deltasA[ 2 ], {
 				type: InsertDelta,
 				operations: [
 					{
@@ -283,77 +233,267 @@ describe( 'transform', () => {
 			} );
 		} );
 
-		it( 'importance flag - 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 );
+		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 { deltasA, deltasB } = transformDeltaSets( [ insertDeltaA ], [ insertDeltaB ], true );
+			const originalDeltasA = [ insertDeltaA ];
+			const originalDeltasB = [ insertDeltaB ];
 
-			expectDelta( deltasA[ 0 ], {
-				type: InsertDelta,
-				operations: [
-					{
-						type: InsertOperation,
-						position: new Position( root, [ 0, 4 ] ),
-						baseVersion: 1
-					}
-				]
-			} );
+			const { deltasA, deltasB } = transformDeltaSets( originalDeltasA, originalDeltasB );
 
-			expectDelta( deltasB[ 0 ], {
-				type: InsertDelta,
-				operations: [
-					{
-						type: InsertOperation,
-						position: new Position( root, [ 0, 7 ] ),
-						baseVersion: 1
-					}
-				]
-			} );
+			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 ] );
 		} );
 
-		it( 'importance flag - second 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 );
+		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
+						}
+					]
+				} );
+			} );
 
-			const { deltasA, deltasB } = transformDeltaSets( [ insertDeltaA ], [ insertDeltaB ], false );
+			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: MoveDelta,
+					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
+						}
+					]
+				} );
+			} );
 
-			expectDelta( deltasA[ 0 ], {
-				type: InsertDelta,
-				operations: [
-					{
-						type: InsertOperation,
-						position: new Position( root, [ 0, 7 ] ),
-						baseVersion: 1
-					}
-				]
+			it( 'remove delta may be less important if additional context is 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 ], 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: RemoveDelta,
+					operations: [
+						{
+							type: NoOperation,
+							baseVersion: 1
+						}
+					]
+				} );
 			} );
 
-			expectDelta( deltasB[ 0 ], {
-				type: InsertDelta,
-				operations: [
-					{
-						type: InsertOperation,
-						position: new Position( root, [ 0, 4 ] ),
-						baseVersion: 1
-					}
-				]
+			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 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 );
+			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
+						}
+					]
+				} );
+			} );
 
-			const originalDeltasA = [ insertDeltaA ];
-			const originalDeltasB = [ insertDeltaB ];
+			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: ReinsertOperation,
+							targetPosition: new Position( root, [ 3 ] ),
+							howMany: 1,
+							baseVersion: 3
+						}
+					]
+				} );
+			} );
 
-			const { deltasA, deltasB } = transformDeltaSets( originalDeltasA, originalDeltasB, false );
+			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
+						}
+					]
+				} );
+			} );
 
-			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 ] );
+			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
+						}
+					]
+				} );
+			} );
 		} );
 	} );
 } );

+ 4 - 3
packages/ckeditor5-engine/tests/model/delta/transform/unwrapdelta.js

@@ -28,13 +28,14 @@ import {
 } from '../../../../tests/model/delta/transform/_utils/utils';
 
 describe( 'transform', () => {
-	let doc, root, gy, baseVersion;
+	let doc, root, gy, baseVersion, context;
 
 	beforeEach( () => {
 		doc = getFilledDocument();
 		root = doc.getRoot();
 		gy = doc.graveyard;
 		baseVersion = doc.version;
+		context = { isStrong: false };
 	} );
 
 	describe( 'UnwrapDelta by', () => {
@@ -49,7 +50,7 @@ describe( 'transform', () => {
 				const splitPosition = new Position( root, [ 3, 3, 3, 3 ] );
 				const splitDelta = getSplitDelta( splitPosition, new Element( 'p' ), 9, baseVersion );
 
-				const transformed = transform( unwrapDelta, splitDelta );
+				const transformed = transform( unwrapDelta, splitDelta, context );
 
 				expect( transformed.length ).to.equal( 2 );
 
@@ -110,7 +111,7 @@ describe( 'transform', () => {
 				const splitPosition = new Position( root, [ 3, 3, 3 ] );
 				const splitDelta = getSplitDelta( splitPosition, new Element( 'div' ), 1, baseVersion );
 
-				const transformed = transform( unwrapDelta, splitDelta );
+				const transformed = transform( unwrapDelta, splitDelta, context );
 
 				expect( transformed.length ).to.equal( 1 );
 

+ 4 - 3
packages/ckeditor5-engine/tests/model/delta/transform/weakinsertdelta.js

@@ -26,12 +26,13 @@ import {
 } from '../../../../tests/model/delta/transform/_utils/utils';
 
 describe( 'transform', () => {
-	let doc, root, baseVersion;
+	let doc, root, baseVersion, context;
 
 	beforeEach( () => {
 		doc = getFilledDocument();
 		root = doc.getRoot();
 		baseVersion = doc.version;
+		context = { isStrong: false };
 	} );
 
 	describe( 'AttributeDelta by', () => {
@@ -57,7 +58,7 @@ describe( 'transform', () => {
 				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 );
+				const transformed = transform( insertDelta, attrDelta, context );
 
 				expect( transformed.length ).to.equal( 2 );
 
@@ -109,7 +110,7 @@ describe( 'transform', () => {
 				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 );
+				const transformed = transform( insertDelta, attrDelta, context );
 
 				expect( transformed.length ).to.equal( 1 );
 

+ 5 - 4
packages/ckeditor5-engine/tests/model/delta/transform/wrapdelta.js

@@ -29,13 +29,14 @@ import {
 } from '../../../../tests/model/delta/transform/_utils/utils';
 
 describe( 'transform', () => {
-	let doc, root, gy, baseVersion;
+	let doc, root, gy, baseVersion, context;
 
 	beforeEach( () => {
 		doc = getFilledDocument();
 		root = doc.getRoot();
 		gy = doc.graveyard;
 		baseVersion = doc.version;
+		context = { isStrong: false };
 	} );
 
 	describe( 'WrapDelta by', () => {
@@ -53,7 +54,7 @@ describe( 'transform', () => {
 				const splitPosition = new Position( root, [ 3, 3, 3, 3 ] );
 				const splitDelta = getSplitDelta( splitPosition, new Element( 'p' ), 9, baseVersion );
 
-				const transformed = transform( wrapDelta, splitDelta );
+				const transformed = transform( wrapDelta, splitDelta, context );
 
 				expect( transformed.length ).to.equal( 2 );
 
@@ -112,7 +113,7 @@ describe( 'transform', () => {
 				const splitPosition = new Position( root, [ 3, 3, 3, 1 ] );
 				const splitDelta = getSplitDelta( splitPosition, new Element( 'p' ), 11, baseVersion );
 
-				const transformed = transform( wrapDelta, splitDelta );
+				const transformed = transform( wrapDelta, splitDelta, context );
 
 				expect( transformed.length ).to.equal( 1 );
 
@@ -156,7 +157,7 @@ describe( 'transform', () => {
 				const splitPosition = new Position( root, [ 3, 3, 3, 3 ] );
 				const splitDelta = getSplitDelta( splitPosition, new Element( 'p' ), 9, baseVersion );
 
-				const transformed = transform( wrapDelta, splitDelta );
+				const transformed = transform( wrapDelta, splitDelta, context );
 
 				expect( transformed.length ).to.equal( 1 );
 

+ 81 - 149
packages/ckeditor5-engine/tests/model/operation/transform.js

@@ -18,7 +18,6 @@ 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';
-import ReinsertOperation from '../../../src/model/operation/reinsertoperation';
 import NoOperation from '../../../src/model/operation/nooperation';
 
 describe( 'transform', () => {
@@ -122,7 +121,7 @@ describe( 'transform', () => {
 					baseVersion
 				);
 
-				const transOp = transform( transformBy, true );
+				const transOp = transform( transformBy, { isStrong: true } );
 
 				expect( transOp.length ).to.equal( 1 );
 				expectOperation( transOp[ 0 ], expected );
@@ -299,7 +298,7 @@ describe( 'transform', () => {
 					baseVersion
 				);
 
-				const transOp = transform( op, transformBy, true );
+				const transOp = transform( op, transformBy, { isStrong: true } );
 
 				expect( transOp.length ).to.equal( 1 );
 				expectOperation( transOp[ 0 ], expected );
@@ -610,7 +609,7 @@ describe( 'transform', () => {
 							baseVersion
 						);
 
-						const transOp = transform( op, transformBy, true );
+						const transOp = transform( op, transformBy, { isStrong: true } );
 
 						expected.oldValue = null;
 
@@ -626,7 +625,7 @@ describe( 'transform', () => {
 							baseVersion
 						);
 
-						const transOp = transform( op, transformBy, true );
+						const transOp = transform( op, transformBy, { isStrong: true } );
 
 						expect( transOp.length ).to.equal( 1 );
 						expectOperation( transOp[ 0 ], expected );
@@ -641,7 +640,7 @@ describe( 'transform', () => {
 							baseVersion
 						);
 
-						const transOp = transform( op, transformBy, true );
+						const transOp = transform( op, transformBy, { isStrong: true } );
 
 						expected.oldValue = null;
 
@@ -664,7 +663,7 @@ describe( 'transform', () => {
 							baseVersion
 						);
 
-						const transOp = transform( op, transformBy, true );
+						const transOp = transform( op, transformBy, { isStrong: true } );
 
 						expect( transOp.length ).to.equal( 2 );
 
@@ -690,7 +689,7 @@ describe( 'transform', () => {
 							baseVersion
 						);
 
-						const transOp = transform( op, transformBy, true );
+						const transOp = transform( op, transformBy, { isStrong: true } );
 
 						expect( transOp.length ).to.equal( 2 );
 
@@ -715,7 +714,7 @@ describe( 'transform', () => {
 							baseVersion
 						);
 
-						const transOp = transform( op, transformBy, true );
+						const transOp = transform( op, transformBy, { isStrong: true } );
 
 						expect( transOp.length ).to.equal( 3 );
 
@@ -1410,33 +1409,35 @@ describe( 'transform', () => {
 				expected.range = new Range( start, end );
 			} );
 
-			it( 'remove operation inserted holder element before attribute operation range: increment path', () => {
+			it( 'remove operation inserted elements before attribute operation range: increment path', () => {
 				const transformBy = new RemoveOperation(
 					new Position( root, [ 0 ] ),
 					2,
+					new Position( doc.graveyard, [ 0 ] ),
 					baseVersion
 				);
 
-				transformBy.targetPosition.path = [ 0, 0 ];
+				transformBy.targetPosition.path = [ 0 ];
 
 				const transOp = transform( op, transformBy );
 
 				expect( transOp.length ).to.equal( 1 );
 
-				expected.range.start.path = [ 3, 0 ];
-				expected.range.end.path = [ 3, 4 ];
+				expected.range.start.path = [ 4, 0 ];
+				expected.range.end.path = [ 4, 4 ];
 
 				expectOperation( transOp[ 0 ], expected );
 			} );
 
-			it( 'remove operation inserted holder element after attribute operation range: do nothing', () => {
+			it( 'remove operation inserted elements after attribute operation range: do nothing', () => {
 				const transformBy = new RemoveOperation(
 					new Position( root, [ 0 ] ),
 					2,
+					new Position( doc.graveyard, [ 0 ] ),
 					baseVersion
 				);
 
-				transformBy.targetPosition.path = [ 4, 0 ];
+				transformBy.targetPosition.path = [ 4 ];
 
 				const transOp = transform( op, transformBy );
 
@@ -1573,7 +1574,7 @@ describe( 'transform', () => {
 					baseVersion
 				);
 
-				const transOp = transform( op, transformBy, true );
+				const transOp = transform( op, transformBy, { isStrong: true } );
 
 				expect( transOp.length ).to.equal( 1 );
 				expectOperation( transOp[ 0 ], expected );
@@ -1814,7 +1815,7 @@ describe( 'transform', () => {
 					baseVersion
 				);
 
-				const transOp = transform( op, transformBy, true );
+				const transOp = transform( op, transformBy, { isStrong: true } );
 
 				expect( transOp.length ).to.equal( 1 );
 				expectOperation( transOp[ 0 ], expected );
@@ -1985,7 +1986,7 @@ describe( 'transform', () => {
 					baseVersion
 				);
 
-				const transOp = transform( op, transformBy, true );
+				const transOp = transform( op, transformBy, { isStrong: true } );
 
 				expect( transOp.length ).to.equal( 1 );
 				expectOperation( transOp[ 0 ], expected );
@@ -2347,7 +2348,7 @@ describe( 'transform', () => {
 					baseVersion
 				);
 
-				const transOp = transform( op, transformBy, true );
+				const transOp = transform( op, transformBy, { isStrong: true } );
 
 				expected.sourcePosition.path = [ 4, 1, 0 ];
 
@@ -2380,7 +2381,7 @@ describe( 'transform', () => {
 					baseVersion
 				);
 
-				const transOp = transform( op, transformBy, true );
+				const transOp = transform( op, transformBy, { isStrong: true } );
 
 				expected.sourcePosition.path = [ 4, 1, 1 ];
 
@@ -2418,7 +2419,7 @@ describe( 'transform', () => {
 					baseVersion
 				);
 
-				const transOp = transform( op, transformBy, true );
+				const transOp = transform( op, transformBy, { isStrong: true } );
 
 				expect( transOp.length ).to.equal( 1 );
 
@@ -2456,7 +2457,7 @@ describe( 'transform', () => {
 					baseVersion
 				);
 
-				const transOp = transform( op, transformBy, true );
+				const transOp = transform( op, transformBy, { isStrong: true } );
 
 				expect( transOp.length ).to.equal( 2 );
 
@@ -2495,7 +2496,7 @@ describe( 'transform', () => {
 					baseVersion
 				);
 
-				const transOp = transform( op, transformBy, true );
+				const transOp = transform( op, transformBy, { isStrong: true } );
 
 				expect( transOp.length ).to.equal( 2 );
 
@@ -2540,7 +2541,7 @@ describe( 'transform', () => {
 					baseVersion
 				);
 
-				const transOp = transform( op, transformBy, true );
+				const transOp = transform( op, transformBy, { isStrong: true } );
 
 				expect( transOp.length ).to.equal( 1 );
 
@@ -2574,7 +2575,7 @@ describe( 'transform', () => {
 					baseVersion
 				);
 
-				const transOp = transform( op, transformBy, true );
+				const transOp = transform( op, transformBy, { isStrong: true } );
 
 				expect( transOp.length ).to.equal( 2 );
 
@@ -2599,7 +2600,7 @@ describe( 'transform', () => {
 					baseVersion
 				);
 
-				const transOp = transform( op, transformBy, true );
+				const transOp = transform( op, transformBy, { isStrong: true } );
 
 				expect( transOp.length ).to.equal( 2 );
 
@@ -2667,7 +2668,7 @@ describe( 'transform', () => {
 					baseVersion
 				);
 
-				const transOp = transform( op, transformBy, true );
+				const transOp = transform( op, transformBy, { isStrong: true } );
 
 				expect( transOp.length ).to.equal( 2 );
 
@@ -2712,7 +2713,7 @@ describe( 'transform', () => {
 					baseVersion
 				);
 
-				const transOp = transform( op, transformBy, true );
+				const transOp = transform( op, transformBy, { isStrong: true } );
 
 				expect( transOp.length ).to.equal( 1 );
 
@@ -2722,6 +2723,53 @@ describe( 'transform', () => {
 			} );
 		} );
 
+		describe( 'by RemoveOperation', () => {
+			let transformBy;
+
+			beforeEach( () => {
+				transformBy = new RemoveOperation(
+					Position.createFromPosition( op.sourcePosition ),
+					op.howMany,
+					new Position( doc.graveyard, [ 0 ] ),
+					baseVersion
+				);
+			} );
+
+			it( 'should skip context.isStrong and be less important than RemoveOperation', () => {
+				const transOp = transform( op, transformBy, { isStrong: true } );
+
+				expect( transOp.length ).to.equal( 1 );
+
+				expectOperation( transOp[ 0 ], {
+					type: NoOperation,
+					baseVersion: baseVersion + 1
+				} );
+			} );
+
+			it( 'should use context.isStrong if forceWeakRemove flag is used', () => {
+				const transOp = transform( op, transformBy, { isStrong: true, forceWeakRemove: true } );
+
+				expect( transOp.length ).to.equal( 1 );
+
+				expected.sourcePosition = transformBy.targetPosition;
+
+				expectOperation( transOp[ 0 ], expected );
+			} );
+
+			it( 'should skip context.isStrong and be less important if RemoveOperation is permanent', () => {
+				transformBy.isPermanent = true;
+
+				const transOp = transform( op, transformBy, { isStrong: true, forceWeakRemove: true } );
+
+				expect( transOp.length ).to.equal( 1 );
+
+				expectOperation( transOp[ 0 ], {
+					type: NoOperation,
+					baseVersion: baseVersion + 1
+				} );
+			} );
+		} );
+
 		describe( 'by NoOperation', () => {
 			it( 'no operation update', () => {
 				const transformBy = new NoOperation( baseVersion );
@@ -2758,48 +2806,9 @@ describe( 'transform', () => {
 	} );
 
 	describe( 'RemoveOperation', () => {
-		describe( 'by InsertOperation', () => {
-			it( 'should not need new graveyard holder if original operation did not needed it either', () => {
-				const op = new RemoveOperation( new Position( root, [ 1 ] ), 1, baseVersion );
-				op._needsHolderElement = false;
-
-				const transformBy = new InsertOperation( new Position( root, [ 0 ] ), [ new Node() ], baseVersion );
-
-				const transOp = transform( op, transformBy )[ 0 ];
-
-				expect( transOp._needsHolderElement ).to.be.false;
-			} );
-		} );
-
 		describe( 'by MoveOperation', () => {
-			it( 'should create not more than one RemoveOperation that needs new graveyard holder', () => {
-				const op = new RemoveOperation( new Position( root, [ 1 ] ), 4, baseVersion );
-				const transformBy = new MoveOperation( new Position( root, [ 0 ] ), 2, new Position( root, [ 8 ] ), baseVersion );
-
-				const transOp = transform( op, transformBy );
-
-				expect( transOp.length ).to.equal( 2 );
-
-				expect( transOp[ 0 ]._needsHolderElement ).to.be.true;
-				expect( transOp[ 1 ]._needsHolderElement ).to.be.false;
-			} );
-
-			it( 'should not need new graveyard holder if original operation did not needed it either', () => {
-				const op = new RemoveOperation( new Position( root, [ 1 ] ), 4, baseVersion );
-				op._needsHolderElement = false;
-
-				const transformBy = new MoveOperation( new Position( root, [ 0 ] ), 2, new Position( root, [ 8 ] ), baseVersion );
-
-				const transOp = transform( op, transformBy );
-
-				expect( transOp.length ).to.equal( 2 );
-
-				expect( transOp[ 0 ]._needsHolderElement ).to.be.false;
-				expect( transOp[ 1 ]._needsHolderElement ).to.be.false;
-			} );
-
 			it( 'should force removing content even if was less important', () => {
-				const op = new RemoveOperation( new Position( root, [ 8 ] ), 2, baseVersion );
+				const op = new RemoveOperation( new Position( root, [ 8 ] ), 2, new Position( doc.graveyard, [ 0 ] ), baseVersion );
 
 				const targetPosition = Position.createFromPosition( op.targetPosition );
 
@@ -2807,7 +2816,7 @@ describe( 'transform', () => {
 
 				const sourcePosition = Position.createFromPosition( transformBy.targetPosition );
 
-				const transOp = transform( op, transformBy, false );
+				const transOp = transform( op, transformBy );
 
 				expect( transOp.length ).to.equal( 1 );
 
@@ -2820,83 +2829,6 @@ describe( 'transform', () => {
 				} );
 			} );
 		} );
-
-		describe( 'by RemoveOperation', () => {
-			it( 'removes same nodes and transformed is weak: change howMany to 0', () => {
-				const position = new Position( root, [ 2, 1 ] );
-				const op = new RemoveOperation( position, 3, baseVersion );
-				const transformBy = new RemoveOperation( position, 3, baseVersion );
-
-				const transOp = transform( op, transformBy );
-
-				expect( transOp.length ).to.equal( 1 );
-				expectOperation( transOp[ 0 ], {
-					type: RemoveOperation,
-					howMany: 0,
-					sourcePosition: new Position( doc.graveyard, [ 0, 0 ] ),
-					targetPosition: new Position( doc.graveyard, [ 0, 0 ] ),
-					baseVersion: baseVersion + 1
-				} );
-			} );
-
-			it( 'removes same nodes and transformed is strong: change source position', () => {
-				const position = new Position( root, [ 2, 1 ] );
-				const op = new RemoveOperation( position, 3, baseVersion );
-				const transformBy = new RemoveOperation( position, 3, baseVersion );
-
-				const transOp = transform( op, transformBy, true );
-
-				expect( transOp.length ).to.equal( 1 );
-				expectOperation( transOp[ 0 ], {
-					type: RemoveOperation,
-					howMany: 3,
-					sourcePosition: new Position( doc.graveyard, [ 0, 0 ] ),
-					targetPosition: new Position( doc.graveyard, [ 1, 0 ] ),
-					baseVersion: baseVersion + 1
-				} );
-			} );
-		} );
-	} );
-
-	describe( 'ReinsertOperation', () => {
-		describe( 'by RemoveOperation', () => {
-			it( 'should update sourcePosition if remove operation inserted holder offset before reinsert operation holder', () => {
-				const position = new Position( root, [ 2, 1 ] );
-				const transformBy = new RemoveOperation( position, 3, baseVersion );
-				const op = new ReinsertOperation( new Position( doc.graveyard, [ 0, 0 ] ), 3, position, baseVersion );
-
-				const transOp = transform( op, transformBy );
-
-				expect( transOp.length ).to.equal( 1 );
-
-				expectOperation( transOp[ 0 ], {
-					type: ReinsertOperation,
-					howMany: 3,
-					sourcePosition: new Position( doc.graveyard, [ 1, 0 ] ),
-					targetPosition: position,
-					baseVersion: baseVersion + 1
-				} );
-			} );
-
-			it( 'should not update sourcePosition if remove operation inserted holder offset after reinsert operation source', () => {
-				const position = new Position( root, [ 2, 1 ] );
-				const transformBy = new RemoveOperation( position, 3, baseVersion );
-				transformBy.targetPosition = new Position( doc.graveyard, [ 3, 0 ] );
-				const op = new ReinsertOperation( new Position( doc.graveyard, [ 2, 0 ] ), 3, position, baseVersion );
-
-				const transOp = transform( op, transformBy );
-
-				expect( transOp.length ).to.equal( 1 );
-
-				expectOperation( transOp[ 0 ], {
-					type: ReinsertOperation,
-					howMany: 3,
-					sourcePosition: new Position( doc.graveyard, [ 2, 0 ] ),
-					targetPosition: position,
-					baseVersion: baseVersion + 1
-				} );
-			} );
-		} );
 	} );
 
 	describe( 'NoOperation', () => {
@@ -3158,7 +3090,7 @@ describe( 'transform', () => {
 					baseVersion
 				);
 
-				const transOp = transform( op, transformBy, false );
+				const transOp = transform( op, transformBy );
 
 				expect( transOp.length ).to.equal( 1 );
 				expectOperation( transOp[ 0 ], {
@@ -3175,7 +3107,7 @@ describe( 'transform', () => {
 					baseVersion
 				);
 
-				const transOp = transform( op, transformBy, true );
+				const transOp = transform( op, transformBy, { isStrong: true } );
 
 				expect( transOp.length ).to.equal( 1 );
 
@@ -3470,7 +3402,7 @@ describe( 'transform', () => {
 				const anotherRange = Range.createFromParentsAndOffsets( root, 2, root, 2 );
 				const transformBy = new MarkerOperation( 'name', oldRange, anotherRange, doc.markers, baseVersion );
 
-				const transOp = transform( op, transformBy, true );
+				const transOp = transform( op, transformBy, { isStrong: true } );
 
 				expected.oldRange = anotherRange;