Kaynağa Gözat

Merge branch 't/1468' into t/ot-followups

# Conflicts:
#	tests/model/operation/transform.js
Szymon Cofalik 7 yıl önce
ebeveyn
işleme
36111d303b

+ 291 - 43
packages/ckeditor5-engine/src/model/operation/transform.js

@@ -48,7 +48,7 @@ function updateBaseVersions( operations, baseVersion ) {
 	return operations;
 }
 
-function transform( a, b, context = { aIsStrong: false } ) {
+function transform( a, b, context = {} ) {
 	const transformationFunction = getTransformation( a, b );
 
 	return transformationFunction( a.clone(), b, context );
@@ -89,17 +89,28 @@ function transformSets( operationsA, operationsB, options ) {
 					const opA = opsA[ k ];
 					const opB = opsB[ l ];
 
-					const newOpA = transform( opA, opB, {
+					if ( options.useContext ) {
+						updateRelations( context, opA, opB );
+					}
+
+					const contextAB = {
 						aIsStrong: true,
 						aWasUndone: context.wasUndone( opA ),
-						bWasUndone: context.wasUndone( opB )
-					} );
+						bWasUndone: context.wasUndone( opB ),
+						abRelation: context.getRelation( opA, opB ),
+						baRelation: context.getRelation( opB, opA )
+					};
 
-					const newOpB = transform( opB, opA, {
+					const contextBA = {
 						aIsStrong: false,
 						aWasUndone: context.wasUndone( opB ),
 						bWasUndone: context.wasUndone( opA ),
-					} );
+						abRelation: context.getRelation( opB, opA ),
+						baRelation: context.getRelation( opA, opB )
+					};
+
+					const newOpA = transform( opA, opB, contextAB );
+					const newOpB = transform( opB, opA, contextBA );
 
 					if ( options.useContext ) {
 						updateOriginalOperation( context, opA, newOpA );
@@ -156,6 +167,7 @@ function initializeContext( opsA, opsB, options ) {
 	}
 
 	context.document = options.document;
+	context.relations = new Map();
 
 	context.wasUndone = function( op ) {
 		if ( !options.useContext ) {
@@ -167,9 +179,152 @@ function initializeContext( opsA, opsB, options ) {
 		return this.document.history.isUndoneOperation( originalOp );
 	};
 
+	context.getRelation = function( opA, opB ) {
+		if ( !options.useContext ) {
+			return null;
+		}
+
+		const origB = this.originalOperations.get( opB );
+		const undoneB = this.document.history.getUndoneOperation( origB );
+
+		if ( !undoneB ) {
+			return null;
+		}
+
+		const origA = this.originalOperations.get( opA );
+		const relationsA = this.relations.get( origA );
+
+		if ( relationsA ) {
+			return relationsA.get( undoneB ) || null;
+		}
+
+		return null;
+	};
+
 	return context;
 }
 
+function updateRelations( context, opA, opB ) {
+	switch ( opA.constructor ) {
+		case MoveOperation: {
+			switch ( opB.constructor ) {
+				case MergeOperation: {
+					if ( opA.targetPosition.isEqual( opB.sourcePosition ) || opB.movedRange.containsPosition( opA.targetPosition ) ) {
+						setRelation( context, opA, opB, 'insertAtSource' );
+						setRelation( context, opB, opA, 'splitBefore' );
+					}
+
+					break;
+				}
+
+				case MoveOperation: {
+					if ( opA.targetPosition.isEqual( opB.sourcePosition ) || opA.targetPosition.isBefore( opB.sourcePosition ) ) {
+						setRelation( context, opA, opB, 'insertBefore' );
+						setRelation( context, opB, opA, 'insertAfter' );
+					} else {
+						setRelation( context, opA, opB, 'insertAfter' );
+						setRelation( context, opB, opA, 'insertBefore' );
+					}
+
+					break;
+				}
+
+				case UnwrapOperation: {
+					const isInside = opA.targetPosition.hasSameParentAs( opB.targetPosition );
+
+					if ( isInside ) {
+						setRelation( context, opA, opB, 'insertInside' );
+					}
+
+					break;
+				}
+			}
+
+			break;
+		}
+
+		case SplitOperation: {
+			switch ( opB.constructor ) {
+				case MergeOperation: {
+					if ( opA.position.isBefore( opB.sourcePosition ) ) {
+						setRelation( context, opA, opB, 'splitBefore' );
+						setRelation( context, opB, opA, 'splitAfter' );
+					}
+
+					break;
+				}
+
+				case MoveOperation: {
+					if ( opA.position.isEqual( opB.sourcePosition ) || opA.position.isBefore( opB.sourcePosition ) ) {
+						setRelation( context, opA, opB, 'splitBefore' );
+						setRelation( context, opB, opA, 'insertAtSource' );
+					}
+
+					break;
+				}
+
+				case UnwrapOperation: {
+					const isInside = opA.position.hasSameParentAs( opB.position );
+
+					if ( isInside ) {
+						setRelation( context, opA, opB, 'splitInside' );
+					}
+
+					break;
+				}
+			}
+
+			break;
+		}
+
+		case InsertOperation: {
+			switch ( opB.constructor ) {
+				case MergeOperation: {
+					if ( opA.position.isEqual( opB.sourcePosition ) || opB.movedRange.containsPosition( opA.position ) ) {
+						setRelation( context, opA, opB, 'insertAtSource' );
+					}
+
+					break;
+				}
+
+				case MoveOperation: {
+					if ( opA.position.isEqual( opB.sourcePosition ) || opA.position.isBefore( opB.sourcePosition ) ) {
+						setRelation( context, opA, opB, 'insertBefore' );
+					}
+
+					break;
+				}
+
+				case UnwrapOperation: {
+					const isInside = opA.position.hasSameParentAs( opB.position );
+
+					if ( isInside ) {
+						setRelation( context, opA, opB, 'insertInside' );
+					}
+
+					break;
+				}
+			}
+
+			break;
+		}
+	}
+}
+
+function setRelation( context, opA, opB, relation ) {
+	const origA = context.originalOperations.get( opA );
+	const origB = context.originalOperations.get( opB );
+
+	let relationsA = context.relations.get( origA );
+
+	if ( !relationsA ) {
+		relationsA = new Map();
+		context.relations.set( origA, relationsA );
+	}
+
+	relationsA.set( origB, relation );
+}
+
 function updateOriginalOperation( context, oldOp, newOps ) {
 	const originalOp = context.originalOperations.get( oldOp );
 
@@ -325,8 +480,12 @@ setTransformation( AttributeOperation, MergeOperation, ( a, b ) => {
 
 	// Case 1:	Attribute change on the merged element. In this case, the merged element was moved to graveyard.
 	//			An additional attribute operation that will change the (re)moved element needs to be generated.
+	//			Do it only, if there is more than one element in attribute range. If there is only one element,
+	//			it will be handled by the default algorithm.
 	//
-	if ( a.range.start.hasSameParentAs( b.deletionPosition ) ) {
+	const howMany = a.range.end.offset - a.range.start.offset;
+
+	if ( howMany > 1 && a.range.start.hasSameParentAs( b.deletionPosition ) ) {
 		if ( a.range.containsPosition( b.deletionPosition ) || a.range.start.isEqual( b.deletionPosition ) ) {
 			ranges.push( Range.createFromPositionAndShift( b.graveyardPosition, 1 ) );
 		}
@@ -521,13 +680,23 @@ setTransformation( InsertOperation, InsertOperation, ( a, b, context ) => {
 	return [ a ];
 } );
 
-setTransformation( InsertOperation, MoveOperation, ( a, b ) => {
+setTransformation( InsertOperation, MoveOperation, ( a, b, context ) => {
+	if ( a.position.isEqual( b.targetPosition ) && context.abRelation == 'insertBefore' ) {
+		return [ a ];
+	}
+
 	a.position = a.position._getTransformedByMoveOperation( b );
 
 	return [ a ];
 } );
 
-setTransformation( InsertOperation, SplitOperation, ( a, b ) => {
+setTransformation( InsertOperation, SplitOperation, ( a, b, context ) => {
+	if ( a.position.isEqual( b.position ) && context.abRelation == 'insertAtSource' ) {
+		a.position = b.moveTargetPosition;
+
+		return [ a ];
+	}
+
 	a.position = a.position._getTransformedBySplitOperation( b );
 
 	return [ a ];
@@ -539,7 +708,13 @@ setTransformation( InsertOperation, MergeOperation, ( a, b ) => {
 	return [ a ];
 } );
 
-setTransformation( InsertOperation, WrapOperation, ( a, b ) => {
+setTransformation( InsertOperation, WrapOperation, ( a, b, context ) => {
+	if ( a.position.isEqual( b.position ) && context.abRelation == 'insertInside' ) {
+		a.position = b.targetPosition;
+
+		return [ a ];
+	}
+
 	a.position = a.position._getTransformedByWrapOperation( b );
 
 	return [ a ];
@@ -696,7 +871,7 @@ setTransformation( MergeOperation, MoveOperation, ( a, b, context ) => {
 	a.sourcePosition = a.sourcePosition._getTransformedByMoveOperation( b );
 	a.targetPosition = a.targetPosition._getTransformedByMoveOperation( b );
 
-	if ( !a.graveyardPosition.isEqual( b.targetPosition ) || !context.aIsStrong ) {
+	if ( !a.graveyardPosition.isEqual( b.targetPosition ) ) {
 		a.graveyardPosition = a.graveyardPosition._getTransformedByMoveOperation( b );
 	}
 
@@ -704,8 +879,6 @@ setTransformation( MergeOperation, MoveOperation, ( a, b, context ) => {
 } );
 
 setTransformation( MergeOperation, SplitOperation, ( a, b ) => {
-	a.sourcePosition = a.sourcePosition._getTransformedBySplitOperation( b );
-
 	if ( b.graveyardPosition ) {
 		a.graveyardPosition = a.graveyardPosition._getTransformedByDeletion( b.graveyardPosition, 1 );
 	}
@@ -724,13 +897,18 @@ setTransformation( MergeOperation, SplitOperation, ( a, b ) => {
 	//			This means that `targetPosition` needs to be transformed. This is the default case though.
 	//			For example, if the split would be after `F`, `targetPosition` should also be transformed.
 	//
-	//			There is one exception though - when the split operation is a result of undo. In those cases, it is needed
-	//			to keep `targetPosition` intact, so the nodes are returned to the correct element.
+	//			There is an exception though. It is when merge operation targets into inside of an element.
+	//			Such merge operation can be a result of merge x merge transformation, when merges are identical.
+	//			Such merge operation's source position is in graveyard and we will use that to recognize it
+	//			(although a more precise method would be more correct).
 	//
-	if ( a.targetPosition.isEqual( b.position ) && b.graveyardPosition ) {
+	if ( a.targetPosition.isEqual( b.position ) && a.sourcePosition.root.rootName == '$graveyard' ) {
+		a.sourcePosition = a.sourcePosition._getTransformedBySplitOperation( b );
+
 		return [ a ];
 	}
 
+	a.sourcePosition = a.sourcePosition._getTransformedBySplitOperation( b );
 	a.targetPosition = a.targetPosition._getTransformedBySplitOperation( b );
 
 	return [ a ];
@@ -794,14 +972,14 @@ setTransformation( MergeOperation, UnwrapOperation, ( a, b, context ) => {
 
 // -----------------------
 
-setTransformation( MoveOperation, InsertOperation, ( a, b ) => {
+setTransformation( MoveOperation, InsertOperation, ( a, b, context ) => {
 	const moveRange = Range.createFromPositionAndShift( a.sourcePosition, a.howMany );
 	const transformed = moveRange._getTransformedByInsertOperation( b, false )[ 0 ];
 
 	a.sourcePosition = transformed.start;
 	a.howMany = transformed.end.offset - transformed.start.offset;
 
-	if ( !a.targetPosition.isEqual( b.position ) ) {
+	if ( !a.targetPosition.isEqual( b.position ) || context.abRelation == 'insertBefore' ) {
 		a.targetPosition = a.targetPosition._getTransformedByInsertOperation( b );
 	}
 
@@ -820,12 +998,27 @@ setTransformation( MoveOperation, MoveOperation, ( a, b, context ) => {
 	// this algorithm and we do not want to override original `context.aIsStrong` that will be used in later transformations.
 	let aIsStrong = context.aIsStrong;
 
+	if ( context.abRelation == 'insertBefore' ) {
+		aIsStrong = true;
+	} else if ( context.abRelation == 'insertAfter' ) {
+		aIsStrong = false;
+	}
+
 	// `a.targetPosition` could be affected by the `b` operation. We will transform it.
-	const newTargetPosition = a.targetPosition._getTransformedByMove(
-		b.sourcePosition,
-		b.targetPosition,
-		b.howMany
-	);
+	let newTargetPosition;
+
+	if ( a.targetPosition.isEqual( b.targetPosition ) && aIsStrong ) {
+		newTargetPosition = a.targetPosition._getTransformedByDeletion(
+			b.sourcePosition,
+			b.howMany
+		);
+	} else {
+		newTargetPosition = a.targetPosition._getTransformedByMove(
+			b.sourcePosition,
+			b.targetPosition,
+			b.howMany
+		);
+	}
 
 	//
 	// Special case #1 + mirror.
@@ -973,7 +1166,7 @@ setTransformation( MoveOperation, MoveOperation, ( a, b, context ) => {
 	return makeMoveOperationsFromRanges( ranges, newTargetPosition );
 } );
 
-setTransformation( MoveOperation, SplitOperation, ( a, b ) => {
+setTransformation( MoveOperation, SplitOperation, ( a, b, context ) => {
 	const newTargetPosition = a.targetPosition._getTransformedBySplitOperation( b );
 
 	// Case 1:	Last element in the moved range got split.
@@ -1008,8 +1201,8 @@ setTransformation( MoveOperation, SplitOperation, ( a, b ) => {
 		rightRange = rightRange._getTransformedBySplitOperation( b );
 
 		const ranges = [
-			rightRange,
-			Range.createFromPositionAndShift( moveRange.start, b.position.offset - moveRange.start.offset )
+			new Range( moveRange.start, b.position ),
+			rightRange
 		];
 
 		return makeMoveOperationsFromRanges( ranges, newTargetPosition );
@@ -1023,6 +1216,10 @@ setTransformation( MoveOperation, SplitOperation, ( a, b ) => {
 	a.howMany = transformed.end.offset - transformed.start.offset;
 	a.targetPosition = newTargetPosition;
 
+	if ( a.targetPosition.isEqual( b.position ) && context.abRelation == 'insertAtSource' ) {
+		a.targetPosition = b.targetPosition;
+	}
+
 	return [ a ];
 } );
 
@@ -1061,7 +1258,7 @@ setTransformation( MoveOperation, MergeOperation, ( a, b, context ) => {
 	return [ a ];
 } );
 
-setTransformation( MoveOperation, WrapOperation, ( a, b ) => {
+setTransformation( MoveOperation, WrapOperation, ( a, b, context ) => {
 	const moveRange = Range.createFromPositionAndShift( a.sourcePosition, a.howMany );
 	const newTargetPosition = a.targetPosition._getTransformedByWrapOperation( b );
 
@@ -1113,6 +1310,10 @@ setTransformation( MoveOperation, WrapOperation, ( a, b ) => {
 	a.howMany = transformed.end.offset - transformed.start.offset;
 	a.targetPosition = newTargetPosition;
 
+	if ( a.targetPosition.isEqual( b.position ) && context.abRelation == 'insertInside' ) {
+		a.targetPosition = b.targetPosition;
+	}
+
 	return [ a ];
 } );
 
@@ -1221,7 +1422,11 @@ setTransformation( RootAttributeOperation, RootAttributeOperation, ( a, b, conte
 
 // -----------------------
 
-setTransformation( SplitOperation, InsertOperation, ( a, b ) => {
+setTransformation( SplitOperation, InsertOperation, ( a, b, context ) => {
+	if ( a.position.isEqual( b.position ) && context.baRelation == 'insertAtSource' ) {
+		return [ a ];
+	}
+
 	a.position = a.position._getTransformedByInsertOperation( b );
 
 	return [ a ];
@@ -1237,7 +1442,7 @@ setTransformation( SplitOperation, MergeOperation, ( a, b ) => {
 	return [ a ];
 } );
 
-setTransformation( SplitOperation, MoveOperation, ( a, b ) => {
+setTransformation( SplitOperation, MoveOperation, ( a, b, context ) => {
 	if ( a.graveyardPosition ) {
 		a.graveyardPosition = a.graveyardPosition._getTransformedByMoveOperation( b );
 	}
@@ -1265,6 +1470,12 @@ setTransformation( SplitOperation, MoveOperation, ( a, b ) => {
 		return [ a ];
 	}
 
+	if ( a.position.isEqual( b.targetPosition ) && context.abRelation == 'splitBefore' ) {
+		a.position = a.position._getTransformedByDeletion( b.sourcePosition, b.howMany );
+
+		return [ a ];
+	}
+
 	// The default case.
 	//
 	a.position = a.position._getTransformedByMoveOperation( b );
@@ -1272,7 +1483,7 @@ setTransformation( SplitOperation, MoveOperation, ( a, b ) => {
 	return [ a ];
 } );
 
-setTransformation( SplitOperation, SplitOperation, ( a, b ) => {
+setTransformation( SplitOperation, SplitOperation, ( a, b, context ) => {
 	if ( a.position.isEqual( b.position ) ) {
 		if ( !a.graveyardPosition && !b.graveyardPosition ) {
 			return getNoOp();
@@ -1281,6 +1492,8 @@ setTransformation( SplitOperation, SplitOperation, ( a, b ) => {
 		if ( a.graveyardPosition && b.graveyardPosition && a.graveyardPosition.isEqual( b.graveyardPosition ) ) {
 			return getNoOp();
 		}
+	} else if ( a.position.isEqual( b.insertionPosition ) && context.abRelation == 'splitBefore' ) {
+		return [ a ];
 	} else {
 		a.position = a.position._getTransformedBySplitOperation( b );
 	}
@@ -1292,7 +1505,7 @@ setTransformation( SplitOperation, SplitOperation, ( a, b ) => {
 	return [ a ];
 } );
 
-setTransformation( SplitOperation, WrapOperation, ( a, b ) => {
+setTransformation( SplitOperation, WrapOperation, ( a, b, context ) => {
 	// Case 1:	If split position has been wrapped, reverse the wrapping so that split can be applied as intended.
 	//			This is an edge case scenario where it is difficult to find a correct solution.
 	//			Since it will be a rare (or only theoretical) scenario, the algorithm will perform the easy solution.
@@ -1310,7 +1523,11 @@ setTransformation( SplitOperation, WrapOperation, ( a, b ) => {
 		return [ reversed, a ];
 	}
 
-	a.position = a.position._getTransformedByWrapOperation( b );
+	if ( a.position.isEqual( b.position ) && context.abRelation == 'splitInside' ) {
+		a.position = b.targetPosition;
+	} else {
+		a.position = a.position._getTransformedByWrapOperation( b );
+	}
 
 	if ( a.graveyardPosition && b.graveyardPosition ) {
 		a.graveyardPosition = a.graveyardPosition._getTransformedByDeletion( b.graveyardPosition, 1 );
@@ -1319,8 +1536,17 @@ setTransformation( SplitOperation, WrapOperation, ( a, b ) => {
 	return [ a ];
 } );
 
-setTransformation( SplitOperation, UnwrapOperation, ( a, b ) => {
-	a.position = a.position._getTransformedByUnwrapOperation( b );
+setTransformation( SplitOperation, UnwrapOperation, ( a, b, context ) => {
+	const splitInside = a.position.hasSameParentAs( b.position );
+
+	if ( splitInside && !context.bWasUndone ) {
+		const path = b.graveyardPosition.path.slice();
+		path.push( 0 );
+
+		a.position = new Position( b.graveyardPosition.root, path );
+	} else {
+		a.position = a.position._getTransformedByUnwrapOperation( b );
+	}
 
 	if ( a.graveyardPosition ) {
 		a.graveyardPosition = a.graveyardPosition._getTransformedByInsertion( b.graveyardPosition, 1 );
@@ -1331,7 +1557,13 @@ setTransformation( SplitOperation, UnwrapOperation, ( a, b ) => {
 
 // -----------------------
 
-setTransformation( WrapOperation, InsertOperation, ( a, b ) => {
+setTransformation( WrapOperation, InsertOperation, ( a, b, context ) => {
+	if ( a.position.isEqual( b.position ) && context.baRelation == 'insertInside' ) {
+		a.howMany += b.howMany;
+
+		return [ a ];
+	}
+
 	const transformed = a.wrappedRange._getTransformedByInsertOperation( b, false )[ 0 ];
 
 	a.position = transformed.start;
@@ -1353,11 +1585,18 @@ setTransformation( WrapOperation, MergeOperation, ( a, b ) => {
 	return [ a ];
 } );
 
-setTransformation( WrapOperation, MoveOperation, ( a, b ) => {
+setTransformation( WrapOperation, MoveOperation, ( a, b, context ) => {
 	if ( a.graveyardPosition ) {
 		a.graveyardPosition = a.graveyardPosition._getTransformedByMoveOperation( b );
 	}
 
+	if ( a.position.isEqual( b.targetPosition ) && context.baRelation == 'insertInside' ) {
+		a.position._getTransformedByDeletion( b.sourcePosition, b.howMany );
+		a.howMany += b.howMany;
+
+		return [ a ];
+	}
+
 	const ranges = breakRangeByMoveOperation( a.wrappedRange, b, false );
 
 	if ( ranges.length == 0 ) {
@@ -1374,10 +1613,14 @@ setTransformation( WrapOperation, MoveOperation, ( a, b ) => {
 	} );
 } );
 
-setTransformation( WrapOperation, SplitOperation, ( a, b ) => {
+setTransformation( WrapOperation, SplitOperation, ( a, b, context ) => {
 	// Case 1:	If range to wrap got split by split operation cancel the wrapping.
+	//			Do that only if this is not undo mode. If `b` operation was earlier transformed by unwrap operation
+	//			and the split position was inside the unwrapped range, then proceed without special case.
 	//
-	if ( a.position.hasSameParentAs( b.position ) && a.wrappedRange.containsPosition( b.position ) ) {
+	const isInside = a.position.hasSameParentAs( b.position ) && a.wrappedRange.containsPosition( b.position );
+
+	if ( isInside && context.baRelation !== 'splitInside' ) {
 		// We cannot just return no-op in this case, because in the mirror case scenario the wrap is reversed, which
 		// might introduce a new node in the graveyard (if the wrap didn't have `graveyardPosition`, then the wrap
 		// created a new element which was put to the graveyard when the wrap was reversed).
@@ -1387,7 +1630,7 @@ setTransformation( WrapOperation, SplitOperation, ( a, b ) => {
 			const graveyard = a.position.root.document.graveyard;
 			const graveyardPosition = new Position( graveyard, [ 0 ] );
 
-			return new InsertOperation( graveyardPosition, a.element, 0 );
+			return [ new InsertOperation( graveyardPosition, a.element, 0 ) ];
 		} else {
 			return getNoOp();
 		}
@@ -1608,7 +1851,7 @@ setTransformation( UnwrapOperation, MergeOperation, ( a, b, context ) => {
 	return [ a ];
 } );
 
-setTransformation( UnwrapOperation, MoveOperation, ( a, b, context ) => {
+setTransformation( UnwrapOperation, MoveOperation, ( a, b ) => {
 	// Case 1:	Move operation moves nodes from the unwrapped element.
 	//			This does not have any impact on `UnwrapOperation#position`, but `#howMany` has to be changed.
 	//
@@ -1626,7 +1869,7 @@ setTransformation( UnwrapOperation, MoveOperation, ( a, b, context ) => {
 
 	a.position = a.position._getTransformedByMoveOperation( b );
 
-	if ( !a.graveyardPosition.isEqual( b.targetPosition ) || !context.aIsStrong ) {
+	if ( !a.graveyardPosition.isEqual( b.targetPosition ) ) {
 		a.graveyardPosition = a.graveyardPosition._getTransformedByMoveOperation( b );
 	}
 
@@ -1677,6 +1920,10 @@ setTransformation( UnwrapOperation, WrapOperation, ( a, b ) => {
 		a.howMany = a.howMany - b.howMany + 1;
 	}
 
+	if ( b.graveyardPosition && compareArrays( a.position.getParentPath(), b.graveyardPosition.path ) == 'same' ) {
+		a.howMany = b.howMany;
+	}
+
 	// The default case.
 	//
 	a.position = a.position._getTransformedByWrapOperation( b );
@@ -1695,6 +1942,8 @@ setTransformation( UnwrapOperation, UnwrapOperation, ( a, b, context ) => {
 		a.position = new Position( b.graveyardPosition.root, path );
 		a.howMany = 0;
 		a.graveyardPosition = Position.createFromPosition( b.graveyardPosition );
+
+		return [ a ];
 	}
 
 	a.position = a.position._getTransformedByUnwrapOperation( b );
@@ -1745,8 +1994,7 @@ function makeMoveOperationsFromRanges( ranges, targetPosition ) {
 			ranges[ j ] = ranges[ j ]._getTransformedByMove( op.sourcePosition, op.targetPosition, op.howMany )[ 0 ];
 		}
 
-		// targetPosition.stickiness = 'toPrevious';
-		targetPosition = targetPosition._getTransformedByMove( op.sourcePosition, op.targetPosition, op.howMany, true );
+		targetPosition = targetPosition._getTransformedByMove( op.sourcePosition, op.targetPosition, op.howMany );
 	}
 
 	return operations;

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

@@ -763,6 +763,7 @@ export default class Position {
 
 		// The first part of a path to combined position is a path to the place where nodes were moved.
 		const combined = Position.createFromPosition( target );
+		combined.stickiness = this.stickiness;
 
 		// Then we have to update the rest of the path.
 

+ 24 - 18
packages/ckeditor5-engine/tests/model/operation/transform.js

@@ -52,6 +52,12 @@ describe( 'transform', () => {
 		}
 	}
 
+	const contextIsStrong = {
+		aIsStrong: true,
+		wasUndone: () => false,
+		getRelation: () => false
+	};
+
 	describe( 'InsertOperation', () => {
 		let nodeC, nodeD, position;
 
@@ -118,7 +124,7 @@ describe( 'transform', () => {
 					0
 				);
 
-				const transOp = transform.transform( op, transformBy, { aIsStrong: true } );
+				const transOp = transform.transform( op, transformBy, contextIsStrong );
 
 				expect( transOp.length ).to.equal( 1 );
 				expectOperation( transOp[ 0 ], expected );
@@ -309,7 +315,7 @@ describe( 'transform', () => {
 					0
 				);
 
-				const transOp = transform.transform( op, transformBy, { aIsStrong: true } );
+				const transOp = transform.transform( op, transformBy, contextIsStrong );
 				expected.position.offset += 2;
 
 				expect( transOp.length ).to.equal( 1 );
@@ -861,7 +867,7 @@ describe( 'transform', () => {
 					0
 				);
 
-				const transOp = transform.transform( op, transformBy, { aIsStrong: true } );
+				const transOp = transform.transform( op, transformBy, contextIsStrong );
 
 				expected.oldValue = 'xyz';
 
@@ -1101,7 +1107,7 @@ describe( 'transform', () => {
 					0
 				);
 
-				const transOp = transform.transform( op, transformBy, { aIsStrong: true } );
+				const transOp = transform.transform( op, transformBy, contextIsStrong );
 
 				expect( transOp.length ).to.equal( 1 );
 				expectOperation( transOp[ 0 ], expected );
@@ -1240,7 +1246,7 @@ describe( 'transform', () => {
 					0
 				);
 
-				const transOp = transform.transform( op, transformBy, { aIsStrong: true } );
+				const transOp = transform.transform( op, transformBy, contextIsStrong );
 
 				expect( transOp.length ).to.equal( 1 );
 				expectOperation( transOp[ 0 ], expected );
@@ -1566,7 +1572,7 @@ describe( 'transform', () => {
 					0
 				);
 
-				const transOp = transform.transform( op, transformBy, { aIsStrong: true } );
+				const transOp = transform.transform( op, transformBy, contextIsStrong );
 
 				expected.sourcePosition.path = [ 4, 1, 0 ];
 
@@ -1598,7 +1604,7 @@ describe( 'transform', () => {
 					0
 				);
 
-				const transOp = transform.transform( op, transformBy, { aIsStrong: true } );
+				const transOp = transform.transform( op, transformBy, contextIsStrong );
 
 				expected.sourcePosition.path = [ 4, 1, 1 ];
 
@@ -1636,7 +1642,7 @@ describe( 'transform', () => {
 					0
 				);
 
-				const transOp = transform.transform( op, transformBy, { aIsStrong: true } );
+				const transOp = transform.transform( op, transformBy, contextIsStrong );
 
 				expect( transOp.length ).to.equal( 1 );
 
@@ -1674,7 +1680,7 @@ describe( 'transform', () => {
 					0
 				);
 
-				const transOp = transform.transform( op, transformBy, { aIsStrong: true } );
+				const transOp = transform.transform( op, transformBy, contextIsStrong );
 
 				expect( transOp.length ).to.equal( 2 );
 
@@ -1713,7 +1719,7 @@ describe( 'transform', () => {
 					0
 				);
 
-				const transOp = transform.transform( op, transformBy, { aIsStrong: true } );
+				const transOp = transform.transform( op, transformBy, contextIsStrong );
 
 				expect( transOp.length ).to.equal( 2 );
 
@@ -1764,7 +1770,7 @@ describe( 'transform', () => {
 					0
 				);
 
-				const transOp = transform.transform( op, transformBy, { aIsStrong: true } );
+				const transOp = transform.transform( op, transformBy, contextIsStrong );
 
 				expect( transOp.length ).to.equal( 2 );
 
@@ -1804,7 +1810,7 @@ describe( 'transform', () => {
 					0
 				);
 
-				const transOp = transform.transform( op, transformBy, { aIsStrong: true } );
+				const transOp = transform.transform( op, transformBy, contextIsStrong );
 
 				expect( transOp.length ).to.equal( 2 );
 
@@ -1829,7 +1835,7 @@ describe( 'transform', () => {
 					0
 				);
 
-				const transOp = transform.transform( op, transformBy, { aIsStrong: true } );
+				const transOp = transform.transform( op, transformBy, contextIsStrong );
 
 				expect( transOp.length ).to.equal( 2 );
 
@@ -1900,7 +1906,7 @@ describe( 'transform', () => {
 					0
 				);
 
-				const transOp = transform.transform( op, transformBy, { aIsStrong: true } );
+				const transOp = transform.transform( op, transformBy, contextIsStrong );
 
 				expect( transOp.length ).to.equal( 3 );
 
@@ -1951,7 +1957,7 @@ describe( 'transform', () => {
 					0
 				);
 
-				const transOp = transform.transform( op, transformBy, { aIsStrong: true } );
+				const transOp = transform.transform( op, transformBy, contextIsStrong );
 
 				expect( transOp.length ).to.equal( 1 );
 
@@ -1974,7 +1980,7 @@ describe( 'transform', () => {
 			} );
 
 			it( 'should skip context.aIsStrong and be less important than MoveOperation', () => {
-				const transOp = transform.transform( op, transformBy, { aIsStrong: true } );
+				const transOp = transform.transform( op, transformBy, contextIsStrong );
 
 				expect( transOp.length ).to.equal( 1 );
 
@@ -2317,7 +2323,7 @@ describe( 'transform', () => {
 					0
 				);
 
-				const transOp = transform.transform( op, transformBy, { aIsStrong: true } );
+				const transOp = transform.transform( op, transformBy, contextIsStrong );
 
 				expect( transOp.length ).to.equal( 1 );
 
@@ -2610,7 +2616,7 @@ describe( 'transform', () => {
 				const anotherRange = Range.createFromParentsAndOffsets( root, 2, root, 2 );
 				const transformBy = new MarkerOperation( 'name', oldRange, anotherRange, model.markers, 0 );
 
-				const transOp = transform.transform( op, transformBy, { aIsStrong: true } );
+				const transOp = transform.transform( op, transformBy, contextIsStrong );
 
 				expected.oldRange = anotherRange;
 

+ 1 - 1
packages/ckeditor5-engine/tests/model/operation/transform/attribute.js

@@ -795,7 +795,7 @@ describe( 'transform', () => {
 				expectClients( '<paragraph>FooB<$text bold="true">ar</$text></paragraph>' );
 			} );
 
-			it.skip( 'element into paragraph #2, then undo', () => {
+			it( 'element into paragraph #2, then undo', () => {
 				john.setData( '<paragraph>Foo</paragraph>[<paragraph>Bar</paragraph>]' );
 				kate.setData( '<paragraph>Foo</paragraph>[]<paragraph>Bar</paragraph>' );
 

+ 1 - 3
packages/ckeditor5-engine/tests/model/operation/transform/insert.js

@@ -811,7 +811,7 @@ describe( 'transform', () => {
 				expectClients( '<paragraph>Foo</paragraph><paragraph>BarAbc</paragraph>' );
 			} );
 
-			it.skip( 'element, then add marker, split and undo with type #2', () => {
+			it( 'element, then add marker, split and undo with type #2', () => {
 				john.setData( '<paragraph>Foo</paragraph>[]' );
 				kate.setData( '[<paragraph>Foo</paragraph>]' );
 
@@ -845,8 +845,6 @@ describe( 'transform', () => {
 
 				syncClients();
 
-				// Actual content:
-				// <paragraph></paragraph><paragraph>Ba</paragraph><paragraph></paragraph>
 				expectClients( '<paragraph>Foo</paragraph><paragraph>BarAbc</paragraph>' );
 			} );
 		} );

+ 1 - 3
packages/ckeditor5-engine/tests/model/operation/transform/marker.js

@@ -183,7 +183,6 @@ describe( 'transform', () => {
 				kate.remove();
 
 				syncClients();
-
 				expectClients( '<paragraph></paragraph>' );
 
 				john.undo();
@@ -191,8 +190,7 @@ describe( 'transform', () => {
 
 				syncClients();
 
-				// Actual result for Kate:
-				// <paragraph><m1:start></m1:start>Fo<m1:end></m1:end><m2:start></m2:start>o<m2:end></m2:end> Bar</paragraph>
+				// Wrong markers transforming.
 				expectClients(
 					'<paragraph>' +
 						'<m1:start></m1:start>Foo<m1:end></m1:end><m2:start></m2:start> Bar<m2:end></m2:end>' +

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

@@ -335,7 +335,7 @@ describe( 'transform', () => {
 				);
 			} );
 
-			it.skip( 'text in other user\'s selection', () => {
+			it( 'text in other user\'s selection', () => {
 				john.setData( '<paragraph>[Foo] Bar</paragraph>' );
 				kate.setData( '<paragraph>F[]oo Bar</paragraph>' );
 
@@ -366,7 +366,7 @@ describe( 'transform', () => {
 				);
 			} );
 
-			it.skip( 'inside moved text', () => {
+			it( 'inside moved text', () => {
 				john.setData( '<paragraph>F[oo Ba]r</paragraph><paragraph>Abc</paragraph>' );
 				kate.setData( '<paragraph>Foo[] Bar</paragraph><paragraph>Abc</paragraph>' );
 
@@ -375,10 +375,6 @@ describe( 'transform', () => {
 
 				syncClients();
 
-				// Actual result for Kate:
-				// <paragraph>F</paragraph><paragraph>r</paragraph><paragraph> BaooAbc</paragraph>
-				// Move operations have wrong targets when they target at same place.
-
 				expectClients(
 					'<paragraph>F</paragraph>' +
 					'<paragraph>r</paragraph>' +

+ 2 - 10
packages/ckeditor5-engine/tests/model/operation/transform/remove.js

@@ -218,7 +218,7 @@ describe( 'transform', () => {
 				);
 			} );
 
-			it.skip( 'element while removing, then undo', () => {
+			it( 'element while removing, then undo', () => {
 				john.setData( '<paragraph>Foo</paragraph><blockQuote>[<paragraph>Bar</paragraph>]</blockQuote>' );
 				kate.setData( '<paragraph>Foo</paragraph><blockQuote>[]<paragraph>Bar</paragraph></blockQuote>' );
 
@@ -226,15 +226,11 @@ describe( 'transform', () => {
 				kate.unwrap();
 
 				syncClients();
-
 				expectClients( '<paragraph>Foo</paragraph>' );
 
 				john.undo();
 
 				syncClients();
-
-				// Actual result:
-				// <paragraph>Foo</paragraph><blockQuote></blockQuote>
 				expectClients(
 					'<paragraph>Foo</paragraph>' +
 					'<paragraph>Bar</paragraph>'
@@ -443,7 +439,7 @@ describe( 'transform', () => {
 				);
 			} );
 
-			it.skip( 'element into paragraph, then undo', () => {
+			it( 'element into paragraph, then undo', () => {
 				john.setData( '<paragraph>F[oo]</paragraph><paragraph>Bar</paragraph>' );
 				kate.setData( '<paragraph>Foo</paragraph>[]<paragraph>Bar</paragraph>' );
 
@@ -451,15 +447,11 @@ describe( 'transform', () => {
 				kate.merge();
 
 				syncClients();
-
 				expectClients( '<paragraph>FBar</paragraph>' );
 
 				john.undo();
 
 				syncClients();
-
-				// Actual result:
-				// <paragraph>F<paragraph></paragraph>oBar</paragraph>
 				expectClients( '<paragraph>FooBar</paragraph>' );
 			} );
 

+ 12 - 15
packages/ckeditor5-engine/tests/model/operation/transform/split.js

@@ -90,7 +90,7 @@ describe( 'transform', () => {
 				);
 			} );
 
-			it.skip( 'intersecting wrap', () => {
+			it( 'intersecting wrap', () => {
 				john.setData( '<paragraph>Fo[]o</paragraph>' );
 				kate.setData( '<paragraph>F[oo]</paragraph>' );
 
@@ -98,10 +98,9 @@ describe( 'transform', () => {
 				kate.wrap( 'div' );
 
 				syncClients();
-
 				expectClients(
-					'<paragraph>F</paragraph>' +
-					'<paragraph><div>oo</div></paragraph>'
+					'<paragraph>Fo</paragraph>' +
+					'<paragraph>o</paragraph>'
 				);
 			} );
 		} );
@@ -122,7 +121,7 @@ describe( 'transform', () => {
 				);
 			} );
 
-			it.skip( 'element in same position', () => {
+			it( 'element in same position', () => {
 				john.setData( '<blockQuote><paragraph>[]Foo</paragraph></blockQuote>' );
 				kate.setData( '<blockQuote><paragraph>[]Foo</paragraph></blockQuote>' );
 
@@ -130,7 +129,6 @@ describe( 'transform', () => {
 				kate.unwrap();
 
 				syncClients();
-
 				expectClients( '<blockQuote>Foo</blockQuote>' );
 			} );
 
@@ -285,7 +283,7 @@ describe( 'transform', () => {
 				expectClients( '<paragraph>FooBar</paragraph>' );
 			} );
 
-			it.skip( 'element into paragraph #3', () => {
+			it( 'element into paragraph #3', () => {
 				john.setData( '<paragraph>Foo[]</paragraph><paragraph>Bar</paragraph>' );
 				kate.setData( '<paragraph>Foo</paragraph>[]<paragraph>Bar</paragraph>' );
 
@@ -299,13 +297,10 @@ describe( 'transform', () => {
 				kate.undo();
 
 				syncClients();
-				// Actual content for Kate:
-				// <paragraph>FooBar</paragraph><paragraph></paragraph>
-				// There is a problem in Merge x Split transform in undo case.
 				expectClients( '<paragraph>Foo</paragraph><paragraph>Bar</paragraph>' );
 			} );
 
-			it.skip( 'element into paragraph #4', () => {
+			it( 'element into paragraph #4', () => {
 				john.setData( '<paragraph>Foo</paragraph><paragraph>B[]ar</paragraph>' );
 				kate.setData( '<paragraph>Foo</paragraph>[]<paragraph>Bar</paragraph>' );
 
@@ -313,14 +308,16 @@ describe( 'transform', () => {
 				kate.merge();
 
 				syncClients();
+				expectClients(
+					'<paragraph>FooB</paragraph>' +
+					'<paragraph>ar</paragraph>'
+				);
 
 				john.undo();
 				kate.undo();
 
-				expectClients(
-					'<paragraph>Fo</paragraph>' +
-					'<paragraph>oBar</paragraph>'
-				);
+				syncClients();
+				expectClients( '<paragraph>Foo</paragraph><paragraph>Bar</paragraph>' );
 			} );
 		} );
 

+ 1 - 4
packages/ckeditor5-engine/tests/model/operation/transform/unwrap.js

@@ -50,7 +50,7 @@ describe( 'transform', () => {
 				expectClients( '<paragraph>Foo</paragraph>' );
 			} );
 
-			it.skip( 'the same element, then undo', () => {
+			it( 'the same element, then undo', () => {
 				john.setData( '<blockQuote>[]<paragraph>Foo</paragraph></blockQuote>' );
 				kate.setData( '<blockQuote>[]<paragraph>Foo</paragraph></blockQuote>' );
 
@@ -63,9 +63,6 @@ describe( 'transform', () => {
 				john.undo();
 
 				syncClients();
-				// Actual content for Kate:
-				// <paragraph><paragraph>Foo</paragraph></paragraph>
-				// Kate has a different order of nodes in graveyard after syncing.
 				expectClients( '<blockQuote><paragraph>Foo</paragraph></blockQuote>' );
 			} );
 		} );

+ 30 - 12
packages/ckeditor5-engine/tests/model/operation/transform/utils.js

@@ -253,16 +253,34 @@ function bufferOperations( operations, client ) {
 }
 
 export function syncClients() {
-	for ( const client of clients ) {
-		for ( const item of bufferedOperations ) {
-			const remoteOperations = item.operations.map( op => OperationFactory.fromJSON( JSON.parse( op ), client.document ) );
-			const remoteClient = item.client;
+	const clientsOperations = {};
+
+	// For each client, flatten all buffered operations into one set.
+	for ( const item of bufferedOperations ) {
+		const name = item.client.name;
+
+		if ( !clientsOperations[ name ] ) {
+			clientsOperations[ name ] = [];
+		}
+
+		clientsOperations[ name ].push( ...item.operations );
+	}
 
-			if ( remoteClient == client ) {
+	for ( const localClient of clients ) {
+		for ( const remoteClient of clients ) {
+			if ( remoteClient == localClient ) {
 				continue;
 			}
 
-			const clientOperations = Array.from( client.document.history.getOperations( client.syncedVersion ) );
+			const remoteOperationsJson = clientsOperations[ remoteClient.name ];
+
+			if ( !remoteOperationsJson ) {
+				continue;
+			}
+
+			const remoteOperations = remoteOperationsJson.map( op => OperationFactory.fromJSON( JSON.parse( op ), localClient.document ) );
+
+			const localOperations = Array.from( localClient.document.history.getOperations( localClient.syncedVersion ) );
 
 			let remoteOperationsTransformed = null;
 
@@ -271,21 +289,21 @@ export function syncClients() {
 				padWithNoOps: true
 			};
 
-			if ( client.orderNumber < remoteClient.orderNumber ) {
-				remoteOperationsTransformed = transform.transformSets( clientOperations, remoteOperations, options ).operationsB;
+			if ( localClient.orderNumber < remoteClient.orderNumber ) {
+				remoteOperationsTransformed = transform.transformSets( localOperations, remoteOperations, options ).operationsB;
 			} else {
-				remoteOperationsTransformed = transform.transformSets( remoteOperations, clientOperations, options ).operationsA;
+				remoteOperationsTransformed = transform.transformSets( remoteOperations, localOperations, options ).operationsA;
 			}
 
-			client.editor.model.enqueueChange( 'transparent', writer => {
+			localClient.editor.model.enqueueChange( 'transparent', writer => {
 				for ( const operation of remoteOperationsTransformed ) {
 					writer.batch.addOperation( operation );
-					client.editor.model.applyOperation( operation );
+					localClient.editor.model.applyOperation( operation );
 				}
 			} );
 		}
 
-		client.syncedVersion = client.document.version;
+		localClient.syncedVersion = localClient.document.version;
 	}
 
 	bufferedOperations.clear();

+ 36 - 16
packages/ckeditor5-engine/tests/model/operation/transform/wrap.js

@@ -63,19 +63,24 @@ describe( 'transform', () => {
 				);
 			} );
 
-			it.skip( 'intersecting wrap #2', () => {
-				john.setData( '<paragraph>[Foo]</paragraph>' );
-				kate.setData( '<paragraph>F[o]o</paragraph>' );
+			it( 'intersecting wrap #2', () => {
+				john.setData( '[<paragraph>Foo</paragraph><paragraph>Bar</paragraph><paragraph>Abc</paragraph>]' );
+				kate.setData( '<paragraph>Foo</paragraph>[<paragraph>Bar</paragraph>]<paragraph>Abc</paragraph>' );
 
-				john.wrap( 'div' );
+				john.wrap( 'blockQuote' );
 				kate.wrap( 'div' );
 
 				syncClients();
-
-				expectClients( '<paragraph><div>Foo</div></pragraph>' );
+				expectClients(
+					'<blockQuote>' +
+						'<paragraph>Foo</paragraph>' +
+						'<paragraph>Bar</paragraph>' +
+						'<paragraph>Abc</paragraph>' +
+					'</blockQuote>'
+				);
 			} );
 
-			it.skip( 'intersecting wrap, then undo #1', () => {
+			it( 'intersecting wrap, then undo #1', () => {
 				john.setData( '[<paragraph>Foo</paragraph><paragraph>Bar</paragraph>]<paragraph>Abc</paragraph>' );
 				kate.setData( '<paragraph>Foo</paragraph>[<paragraph>Bar</paragraph><paragraph>Abc</paragraph>]' );
 
@@ -83,12 +88,20 @@ describe( 'transform', () => {
 				kate.wrap( 'div' );
 
 				syncClients();
+				expectClients(
+					'<blockQuote>' +
+						'<paragraph>Foo</paragraph>' +
+						'<paragraph>Bar</paragraph>' +
+					'</blockQuote>' +
+					'<div>' +
+						'<paragraph>Abc</paragraph>' +
+					'</div>'
+				);
 
 				john.undo();
 				kate.undo();
 
 				syncClients();
-
 				expectClients(
 					'<paragraph>Foo</paragraph>' +
 					'<paragraph>Bar</paragraph>' +
@@ -96,21 +109,30 @@ describe( 'transform', () => {
 				);
 			} );
 
-			it.skip( 'intersecting wrap, then undo #2', () => {
-				john.setData( '<paragraph>[Foo]</paragraph>' );
-				kate.setData( '<paragraph>F[o]o</paragraph>' );
+			it( 'intersecting wrap, then undo #2', () => {
+				john.setData( '[<paragraph>Foo</paragraph><paragraph>Bar</paragraph>]<paragraph>Abc</paragraph>' );
+				kate.setData( '<paragraph>Foo</paragraph>[<paragraph>Bar</paragraph><paragraph>Abc</paragraph>]' );
 
-				john.wrap( 'div' );
+				john.wrap( 'blockQuote' );
 				kate.wrap( 'div' );
 
 				syncClients();
+				expectClients(
+					'<blockQuote>' +
+						'<paragraph>Foo</paragraph>' +
+						'<paragraph>Bar</paragraph>' +
+					'</blockQuote>' +
+					'<div>' +
+						'<paragraph>Abc</paragraph>' +
+					'</div>'
+				);
 
 				john.undo();
 				kate.undo();
 
 				syncClients();
 
-				expectClients( '<paragraph><div>Foo</div></pragraph>' );
+				expectClients( '<paragraph>Foo</paragraph><paragraph>Bar</paragraph><paragraph>Abc</paragraph>' );
 			} );
 
 			it( 'element and text', () => {
@@ -227,14 +249,12 @@ describe( 'transform', () => {
 				kate.merge();
 
 				syncClients();
-
-				expectClients( '<blockQuote><paragraph>FooBar</paragraph></blockQuote>' );
+				expectClients( '<paragraph>FooBar</paragraph><blockQuote></blockQuote>' );
 
 				john.undo();
 				kate.undo();
 
 				syncClients();
-
 				expectClients( '<paragraph>Foo</paragraph><paragraph>Bar</paragraph>' );
 			} );
 		} );