浏览代码

Added: Introduced `SplitOperation#insertionPosition` as a settable property. Renamed `SplitOperation#position` to `#splitPosition`.

Szymon Cofalik 7 年之前
父节点
当前提交
fe735a2084

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

@@ -357,8 +357,8 @@ function enableLoggingTools() {
 	} );
 
 	sandbox.mock( SplitOperation.prototype, 'toString', function() {
-		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
-			`${ this.position } ( ${ this.howMany } )${ this.graveyardPosition ? ', ' + this.graveyardPosition : '' }`;
+		return getClassName( this ) + `( ${ this.baseVersion } ): ${ this.splitPosition } ` +
+			`( ${ this.howMany } ) -> ${ this.insertionPosition }${ this.graveyardPosition ? ' with ' + this.graveyardPosition : '' }`;
 	} );
 
 	sandbox.mock( WrapOperation.prototype, 'toString', function() {

+ 5 - 7
packages/ckeditor5-engine/src/model/differ.js

@@ -181,23 +181,21 @@ export default class Differ {
 				break;
 			}
 			case 'split': {
-				const splitElement = operation.position.parent;
+				const splitElement = operation.splitPosition.parent;
 
 				// Mark that children of the split element were removed.
 				if ( !this._isInInsertedElement( splitElement ) ) {
-					this._markRemove( splitElement, operation.position.offset, operation.howMany );
+					this._markRemove( splitElement, operation.splitPosition.offset, operation.howMany );
 				}
 
 				// Mark that the new element (split copy) was inserted.
-				if ( !this._isInInsertedElement( splitElement.parent ) ) {
-					this._markInsert( splitElement.parent, operation.insertionPosition.offset, 1 );
+				if ( !this._isInInsertedElement( operation.insertionPosition.parent ) ) {
+					this._markInsert( operation.insertionPosition.parent, operation.insertionPosition.offset, 1 );
 				}
 
 				// If the split took the element from the graveyard, mark that the element from the graveyard was removed.
 				if ( operation.graveyardPosition ) {
-					const graveyardParent = operation.graveyardPosition.parent;
-
-					this._markRemove( graveyardParent, operation.graveyardPosition.offset, 1 );
+					this._markRemove( operation.graveyardPosition.parent, operation.graveyardPosition.offset, 1 );
 				}
 
 				break;

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

@@ -170,7 +170,6 @@ function transform( operation ) {
 function doesOperationChangeRangeContent( range, operation ) {
 	switch ( operation.type ) {
 		case 'insert':
-		case 'split':
 		case 'wrap':
 		case 'unwrap':
 			return range.containsPosition( operation.position );
@@ -181,6 +180,8 @@ function doesOperationChangeRangeContent( range, operation ) {
 			return range.containsPosition( operation.sourcePosition ) ||
 				range.start.isEqual( operation.sourcePosition ) ||
 				range.containsPosition( operation.targetPosition );
+		case 'split':
+			return range.containsPosition( operation.splitPosition ) || range.containsPosition( operation.insertionPosition );
 	}
 
 	return false;

+ 6 - 1
packages/ckeditor5-engine/src/model/operation/mergeoperation.js

@@ -119,7 +119,12 @@ export default class MergeOperation extends Operation {
 	 * @returns {module:engine/model/operation/splitoperation~SplitOperation}
 	 */
 	getReversed() {
-		return new SplitOperation( this.targetPosition, this.howMany, this.graveyardPosition, this.baseVersion + 1 );
+		const targetPosition = this.targetPosition._getTransformedByMergeOperation( this );
+
+		const path = this.sourcePosition.path.slice( 0, -1 );
+		const insertionPosition = new Position( this.sourcePosition.root, path )._getTransformedByMergeOperation( this );
+
+		return new SplitOperation( targetPosition, this.howMany, insertionPosition, this.graveyardPosition, this.baseVersion + 1 );
 	}
 
 	/**

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

@@ -17,7 +17,7 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 /**
  * Operation to split {@link module:engine/model/element~Element an element} at given
- * {@link module:engine/model/operation/splitoperation~SplitOperation#position position} into two elements,
+ * {@link module:engine/model/operation/splitoperation~SplitOperation#splitPosition split position} into two elements,
  * both containing a part of the element's original content.
  *
  * @extends module:engine/model/operation/operation~Operation
@@ -26,25 +26,27 @@ export default class SplitOperation extends Operation {
 	/**
 	 * Creates a split operation.
 	 *
-	 * @param {module:engine/model/position~Position} position Position at which an element should be split.
+	 * @param {module:engine/model/position~Position} splitPosition Position at which an element should be split.
 	 * @param {Number} howMany Total offset size of elements that are in the split element after `position`.
+	 * @param {module:engine/model/position~Position} insertionPosition Position at which the clone of split element
+	 * (or element from graveyard) will be inserted.
 	 * @param {module:engine/model/position~Position|null} graveyardPosition Position in the graveyard root before the element which
 	 * should be used as a parent of the nodes after `position`. If it is not set, a copy of the the `position` parent will be used.
 	 * @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
 	 * can be applied or `null` if the operation operates on detached (non-document) tree.
 	 */
-	constructor( position, howMany, graveyardPosition, baseVersion ) {
+	constructor( splitPosition, howMany, insertionPosition, graveyardPosition, baseVersion ) {
 		super( baseVersion );
 
 		/**
 		 * Position at which an element should be split.
 		 *
-		 * @member {module:engine/model/position~Position} module:engine/model/operation/splitoperation~SplitOperation#position
+		 * @member {module:engine/model/position~Position} module:engine/model/operation/splitoperation~SplitOperation#splitPosition
 		 */
-		this.position = Position.createFromPosition( position );
+		this.splitPosition = Position.createFromPosition( splitPosition );
 		// Keep position sticking to the next node. This way any new content added at the place where the element is split
 		// will be left in the original element.
-		this.position.stickiness = 'toNext';
+		this.splitPosition.stickiness = 'toNext';
 
 		/**
 		 * Total offset size of elements that are in the split element after `position`.
@@ -54,6 +56,14 @@ export default class SplitOperation extends Operation {
 		this.howMany = howMany;
 
 		/**
+		 * Position at which the clone of split element (or element from graveyard) will be inserted.
+		 *
+		 * @member {module:engine/model/position~Position} module:engine/model/operation/splitoperation~SplitOperation#insertionPosition
+		 */
+		this.insertionPosition = Position.createFromPosition( insertionPosition );
+		this.insertionPosition.stickiness = 'toNone';
+
+		/**
 		 * Position in the graveyard root before the element which should be used as a parent of the nodes after `position`.
 		 * If it is not set, a copy of the the `position` parent will be used.
 		 *
@@ -76,27 +86,6 @@ export default class SplitOperation extends Operation {
 	}
 
 	/**
-	 * Position after the split element.
-	 *
-	 * This is a position at which the clone of split element (or element from graveyard) will be inserted.
-	 *
-	 * @readonly
-	 * @type {module:engine/model/position~Position}
-	 */
-	get insertionPosition() {
-		const path = this.position.path.slice( 0, -1 );
-		path[ path.length - 1 ]++;
-
-		let pos = new Position( this.position.root, path );
-
-		if ( this.graveyardPosition && this.graveyardPosition.root == pos.root ) {
-			pos = pos._getTransformedByDeletion( this.graveyardPosition, 1 );
-		}
-
-		return pos;
-	}
-
-	/**
 	 * Position inside the new clone of a split element.
 	 *
 	 * This is a position where nodes that are after the split position will be moved to.
@@ -105,30 +94,23 @@ export default class SplitOperation extends Operation {
 	 * @type {module:engine/model/position~Position}
 	 */
 	get moveTargetPosition() {
-		const path = this.position.path.slice( 0, -1 );
-		path[ path.length - 1 ]++;
+		const path = this.insertionPosition.path.slice();
 		path.push( 0 );
 
-		let pos = new Position( this.position.root, path );
-
-		if ( this.graveyardPosition && this.graveyardPosition.root == pos.root ) {
-			pos = pos._getTransformedByDeletion( this.graveyardPosition, 1 );
-		}
-
-		return pos;
+		return new Position( this.insertionPosition.root, path );
 	}
 
 	/**
 	 * Artificial range that contains all the nodes from the split element that will be moved to the new element.
-	 * The range starts at {@link ~#position} and ends in the same parent, at `POSITIVE_INFINITY` offset.
+	 * The range starts at {@link ~#splitPosition} and ends in the same parent, at `POSITIVE_INFINITY` offset.
 	 *
 	 * @readonly
 	 * @type {module:engine/model/range~Range}
 	 */
 	get movedRange() {
-		const end = this.position.getShiftedBy( Number.POSITIVE_INFINITY );
+		const end = this.splitPosition.getShiftedBy( Number.POSITIVE_INFINITY );
 
-		return new Range( this.position, end );
+		return new Range( this.splitPosition, end );
 	}
 
 	/**
@@ -137,7 +119,7 @@ export default class SplitOperation extends Operation {
 	 * @returns {module:engine/model/operation/splitoperation~SplitOperation} Clone of this operation.
 	 */
 	clone() {
-		return new this.constructor( this.position, this.howMany, this.graveyardPosition, this.baseVersion );
+		return new this.constructor( this.splitPosition, this.howMany, this.insertionPosition, this.graveyardPosition, this.baseVersion );
 	}
 
 	/**
@@ -146,18 +128,18 @@ export default class SplitOperation extends Operation {
 	 * @returns {module:engine/model/operation/mergeoperation~MergeOperation}
 	 */
 	getReversed() {
-		const graveyard = this.position.root.document.graveyard;
+		const graveyard = this.splitPosition.root.document.graveyard;
 		const graveyardPosition = new Position( graveyard, [ 0 ] );
 
-		return new MergeOperation( this.moveTargetPosition, this.howMany, this.position, graveyardPosition, this.baseVersion + 1 );
+		return new MergeOperation( this.moveTargetPosition, this.howMany, this.splitPosition, graveyardPosition, this.baseVersion + 1 );
 	}
 
 	/**
 	 * @inheritDoc
 	 */
 	_validate() {
-		const element = this.position.parent;
-		const offset = this.position.offset;
+		const element = this.splitPosition.parent;
+		const offset = this.splitPosition.offset;
 
 		// Validate whether split operation has correct parameters.
 		if ( !element || element.maxOffset < offset ) {
@@ -174,7 +156,7 @@ export default class SplitOperation extends Operation {
 			 * @error split-operation-split-in-root
 			 */
 			throw new CKEditorError( 'split-operation-split-in-root: Cannot split root element.' );
-		} else if ( this.howMany != element.maxOffset - this.position.offset ) {
+		} else if ( this.howMany != element.maxOffset - this.splitPosition.offset ) {
 			/**
 			 * Split operation specifies wrong number of nodes to move.
 			 *
@@ -195,7 +177,7 @@ export default class SplitOperation extends Operation {
 	 * @inheritDoc
 	 */
 	_execute() {
-		const splitElement = this.position.parent;
+		const splitElement = this.splitPosition.parent;
 
 		if ( this.graveyardPosition ) {
 			_move( Range.createFromPositionAndShift( this.graveyardPosition, 1 ), this.insertionPosition );
@@ -205,7 +187,9 @@ export default class SplitOperation extends Operation {
 			_insert( this.insertionPosition, newElement );
 		}
 
-		const sourceRange = Range.createFromParentsAndOffsets( splitElement, this.position.offset, splitElement, splitElement.maxOffset );
+		const sourceRange = Range.createFromParentsAndOffsets(
+			splitElement, this.splitPosition.offset, splitElement, splitElement.maxOffset
+		);
 
 		_move( sourceRange, this.moveTargetPosition );
 	}
@@ -216,7 +200,8 @@ export default class SplitOperation extends Operation {
 	toJSON() {
 		const json = super.toJSON();
 
-		json.position = this.position.toJSON();
+		json.splitPosition = this.splitPosition.toJSON();
+		json.insertionPosition = this.insertionPosition.toJSON();
 
 		if ( this.graveyardPosition ) {
 			json.graveyardPosition = this.graveyardPosition.toJSON();
@@ -233,6 +218,20 @@ export default class SplitOperation extends Operation {
 	}
 
 	/**
+	 * Helper function that returns a default insertion position basing on given `splitPosition`. The default insertion
+	 * position is after the split element.
+	 *
+	 * @param {module:engine/model/position~Position} splitPosition
+	 * @returns {module:engine/model/position~Position}
+	 */
+	static getInsertionPosition( splitPosition ) {
+		const path = splitPosition.path.slice( 0, -1 );
+		path[ path.length - 1 ]++;
+
+		return new Position( splitPosition.root, path );
+	}
+
+	/**
 	 * Creates `SplitOperation` object from deserilized object, i.e. from parsed JSON string.
 	 *
 	 * @param {Object} json Deserialized JSON object.
@@ -240,9 +239,10 @@ export default class SplitOperation extends Operation {
 	 * @returns {module:engine/model/operation/splitoperation~SplitOperation}
 	 */
 	static fromJSON( json, document ) {
-		const position = Position.fromJSON( json.position, document );
+		const splitPosition = Position.fromJSON( json.splitPosition, document );
+		const insertionPosition = Position.fromJSON( json.insertionPosition, document );
 		const graveyardPosition = json.graveyardPosition ? Position.fromJSON( json.graveyardPosition, document ) : null;
 
-		return new this( position, json.howMany, graveyardPosition, json.baseVersion );
+		return new this( splitPosition, json.howMany, insertionPosition, graveyardPosition, json.baseVersion );
 	}
 }

+ 62 - 42
packages/ckeditor5-engine/src/model/operation/transform.js

@@ -483,7 +483,7 @@ class ContextFactory {
 			case SplitOperation: {
 				switch ( opB.constructor ) {
 					case MergeOperation: {
-						if ( opA.position.isBefore( opB.sourcePosition ) ) {
+						if ( opA.splitPosition.isBefore( opB.sourcePosition ) ) {
 							this._setRelation( opA, opB, 'splitBefore' );
 						}
 
@@ -491,7 +491,7 @@ class ContextFactory {
 					}
 
 					case MoveOperation: {
-						if ( opA.position.isEqual( opB.sourcePosition ) || opA.position.isBefore( opB.sourcePosition ) ) {
+						if ( opA.splitPosition.isEqual( opB.sourcePosition ) || opA.splitPosition.isBefore( opB.sourcePosition ) ) {
 							this._setRelation( opA, opB, 'splitBefore' );
 						}
 
@@ -941,15 +941,15 @@ setTransformation( AttributeOperation, SplitOperation, ( a, b ) => {
 	// Make two separate ranges containing all nodes to change:
 	// <p>Fo[zb]</p><p>[a]r</p>
 	//
-	if ( a.range.start.hasSameParentAs( b.position ) && a.range.containsPosition( b.position ) ) {
+	if ( a.range.start.hasSameParentAs( b.splitPosition ) && a.range.containsPosition( b.splitPosition ) ) {
 		const secondPart = a.clone();
 
 		secondPart.range = new Range(
 			Position.createFromPosition( b.moveTargetPosition ),
-			a.range.end._getCombined( b.position, b.moveTargetPosition )
+			a.range.end._getCombined( b.splitPosition, b.moveTargetPosition )
 		);
 
-		a.range.end = Position.createFromPosition( b.position );
+		a.range.end = Position.createFromPosition( b.splitPosition );
 		a.range.end.stickiness = 'toPrevious';
 
 		return [ a, secondPart ];
@@ -1432,7 +1432,7 @@ setTransformation( MergeOperation, SplitOperation, ( a, b, context ) => {
 	// that target position has not been affected by the operation which is undone by this split then this split should
 	// not move the target position either.
 	//
-	if ( a.targetPosition.isEqual( b.position ) ) {
+	if ( a.targetPosition.isEqual( b.splitPosition ) ) {
 		const mergeInside = b.howMany != 0;
 		const mergeSplittingElement = b.graveyardPosition && a.deletionPosition.isEqual( b.graveyardPosition );
 
@@ -1468,8 +1468,8 @@ setTransformation( MergeOperation, SplitOperation, ( a, b, context ) => {
 
 	// The default case.
 	//
-	if ( a.sourcePosition.hasSameParentAs( b.position ) ) {
-		a.howMany = b.position.offset;
+	if ( a.sourcePosition.hasSameParentAs( b.splitPosition ) ) {
+		a.howMany = b.splitPosition.offset;
 	}
 
 	a.sourcePosition = a.sourcePosition._getTransformedBySplitOperation( b );
@@ -1658,7 +1658,7 @@ setTransformation( MoveOperation, MoveOperation, ( a, b, context ) => {
 	//
 	// Special case #2.
 	//
-	// Check if `b` operation targets inside `rangeA`. Use stickiness if possible.
+	// Check if `b` operation targets inside `rangeA`.
 	const bTargetsToA = rangeA.containsPosition( b.targetPosition );
 
 	// If `b` targets to `rangeA` and `rangeA` contains `rangeB`, `b` operation has no influence on `a` operation.
@@ -1748,7 +1748,7 @@ setTransformation( MoveOperation, MoveOperation, ( a, b, context ) => {
 	// Then, we have to manage the "common part" of both move ranges.
 	const common = rangeA.getIntersection( rangeB );
 
-	if ( common !== null && aIsStrong && !bTargetsToA ) {
+	if ( common !== null && aIsStrong ) {
 		// 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() );
@@ -1787,7 +1787,14 @@ setTransformation( MoveOperation, MoveOperation, ( a, b, context ) => {
 } );
 
 setTransformation( MoveOperation, SplitOperation, ( a, b, context ) => {
-	let newTargetPosition = a.targetPosition._getTransformedBySplitOperation( b );
+	let newTargetPosition = Position.createFromPosition( a.targetPosition );
+
+	// Do not transform if target position is same as split insertion position and this split comes from undo.
+	// This should be done on relations but it is too much work for now as it would require relations working in collaboration.
+	// We need to make a decision how we will resolve such conflict and this is less harmful way.
+	if ( !a.targetPosition.isEqual( b.insertionPosition ) || !b.graveyardPosition ) {
+		newTargetPosition = a.targetPosition._getTransformedBySplitOperation( b );
+	}
 
 	// Case 1:
 	//
@@ -1823,12 +1830,12 @@ setTransformation( MoveOperation, SplitOperation, ( a, b, context ) => {
 	// After move:
 	// <p>F</p><p>r</p><p>Xyzozba</p>
 	//
-	if ( moveRange.start.hasSameParentAs( b.position ) && moveRange.containsPosition( b.position ) ) {
-		let rightRange = new Range( b.position, moveRange.end );
+	if ( moveRange.start.hasSameParentAs( b.splitPosition ) && moveRange.containsPosition( b.splitPosition ) ) {
+		let rightRange = new Range( b.splitPosition, moveRange.end );
 		rightRange = rightRange._getTransformedBySplitOperation( b );
 
 		const ranges = [
-			new Range( moveRange.start, b.position ),
+			new Range( moveRange.start, b.splitPosition ),
 			rightRange
 		];
 
@@ -1840,7 +1847,7 @@ setTransformation( MoveOperation, SplitOperation, ( a, b, context ) => {
 	// Move operation targets at the split position. We need to decide if the nodes should be inserted
 	// at the end of the split element or at the beginning of the new element.
 	//
-	if ( a.targetPosition.isEqual( b.position ) && context.abRelation == 'insertAtSource' ) {
+	if ( a.targetPosition.isEqual( b.splitPosition ) && context.abRelation == 'insertAtSource' ) {
 		newTargetPosition = b.moveTargetPosition;
 	}
 
@@ -2064,7 +2071,7 @@ setTransformation( RenameOperation, SplitOperation, ( a, b ) => {
 	// <listItem>Foo</listItem><listItem>bar</listItem>
 	//
 	const renamePath = a.position.path;
-	const splitPath = b.position.getParentPath();
+	const splitPath = b.splitPosition.getParentPath();
 
 	if ( compareArrays( renamePath, splitPath ) == 'same' && !b.graveyardPosition ) {
 		const extraRename = new RenameOperation( a.position.getShiftedBy( 1 ), a.oldName, a.newName, 0 );
@@ -2120,11 +2127,12 @@ setTransformation( RootAttributeOperation, RootAttributeOperation, ( a, b, conte
 setTransformation( SplitOperation, InsertOperation, ( a, b ) => {
 	// The default case.
 	//
-	if ( a.position.hasSameParentAs( b.position ) && a.position.offset < b.position.offset ) {
+	if ( a.splitPosition.hasSameParentAs( b.position ) && a.splitPosition.offset < b.position.offset ) {
 		a.howMany += b.howMany;
 	}
 
-	a.position = a.position._getTransformedByInsertOperation( b );
+	a.splitPosition = a.splitPosition._getTransformedByInsertOperation( b );
+	a.insertionPosition = SplitOperation.getInsertionPosition( a.splitPosition );
 
 	return [ a ];
 } );
@@ -2182,13 +2190,17 @@ setTransformation( SplitOperation, MergeOperation, ( a, b, context ) => {
 	//
 	// Additionally, the special case applies only if the merge wasn't already undone.
 	//
-	if ( !a.graveyardPosition && !context.bWasUndone && a.position.hasSameParentAs( b.sourcePosition ) ) {
+	if ( !a.graveyardPosition && !context.bWasUndone && a.splitPosition.hasSameParentAs( b.sourcePosition ) ) {
 		const splitPath = b.graveyardPosition.path.slice();
 		splitPath.push( 0 );
 
-		const additionalSplit = new SplitOperation( new Position( b.graveyardPosition.root, splitPath ), 0, null, 0 );
+		const splitPosition = new Position( b.graveyardPosition.root, splitPath );
+		const insertionPosition = SplitOperation.getInsertionPosition( new Position( b.graveyardPosition.root, splitPath ) );
+
+		const additionalSplit = new SplitOperation( splitPosition, 0, insertionPosition, null, 0 );
 
-		a.position = a.position._getTransformedByMergeOperation( b );
+		a.splitPosition = a.splitPosition._getTransformedByMergeOperation( b );
+		a.insertionPosition = SplitOperation.getInsertionPosition( a.splitPosition );
 		a.graveyardPosition = Position.createFromPosition( additionalSplit.insertionPosition );
 		a.graveyardPosition.stickiness = 'toNext';
 
@@ -2197,15 +2209,16 @@ setTransformation( SplitOperation, MergeOperation, ( a, b, context ) => {
 
 	// The default case.
 	//
-	if ( a.position.hasSameParentAs( b.deletionPosition ) && !a.position.isAfter( b.deletionPosition ) ) {
+	if ( a.splitPosition.hasSameParentAs( b.deletionPosition ) && !a.splitPosition.isAfter( b.deletionPosition ) ) {
 		a.howMany--;
 	}
 
-	if ( a.position.hasSameParentAs( b.targetPosition ) ) {
+	if ( a.splitPosition.hasSameParentAs( b.targetPosition ) ) {
 		a.howMany += b.howMany;
 	}
 
-	a.position = a.position._getTransformedByMergeOperation( b );
+	a.splitPosition = a.splitPosition._getTransformedByMergeOperation( b );
+	a.insertionPosition = SplitOperation.getInsertionPosition( a.splitPosition );
 
 	if ( a.graveyardPosition ) {
 		a.graveyardPosition = a.graveyardPosition._getTransformedByMergeOperation( b );
@@ -2238,15 +2251,16 @@ setTransformation( SplitOperation, MoveOperation, ( a, b, context ) => {
 	//
 	const rangeToMove = Range.createFromPositionAndShift( b.sourcePosition, b.howMany );
 
-	if ( a.position.hasSameParentAs( b.sourcePosition ) && rangeToMove.containsPosition( a.position ) ) {
-		const howManyRemoved = b.howMany - ( a.position.offset - b.sourcePosition.offset );
+	if ( a.splitPosition.hasSameParentAs( b.sourcePosition ) && rangeToMove.containsPosition( a.splitPosition ) ) {
+		const howManyRemoved = b.howMany - ( a.splitPosition.offset - b.sourcePosition.offset );
 		a.howMany -= howManyRemoved;
 
-		if ( a.position.hasSameParentAs( b.targetPosition ) && a.position.offset < b.targetPosition.offset ) {
+		if ( a.splitPosition.hasSameParentAs( b.targetPosition ) && a.splitPosition.offset < b.targetPosition.offset ) {
 			a.howMany += b.howMany;
 		}
 
-		a.position = Position.createFromPosition( b.sourcePosition );
+		a.splitPosition = Position.createFromPosition( b.sourcePosition );
+		a.insertionPosition = SplitOperation.getInsertionPosition( a.splitPosition );
 
 		return [ a ];
 	}
@@ -2258,27 +2272,34 @@ setTransformation( SplitOperation, MoveOperation, ( a, b, context ) => {
 	// This is a scenario described in `MoveOperation` x `SplitOperation` transformation but from the
 	// "split operation point of view".
 	//
-	if ( a.position.isEqual( b.targetPosition ) && ( context.baRelation == 'insertAtSource' || context.abRelation == 'splitBefore' ) ) {
+	if ( a.splitPosition.isEqual( b.targetPosition ) && ( context.baRelation == 'insertAtSource' || context.abRelation == 'splitBefore' ) ) {
 		a.howMany += b.howMany;
-		a.position = a.position._getTransformedByDeletion( b.sourcePosition, b.howMany );
+		a.splitPosition = a.splitPosition._getTransformedByDeletion( b.sourcePosition, b.howMany );
+		a.insertionPosition = SplitOperation.getInsertionPosition( a.splitPosition );
 
 		return [ a ];
 	}
 
 	// The default case.
 	//
-	if ( a.position.hasSameParentAs( b.sourcePosition ) && a.position.offset <= b.sourcePosition.offset ) {
+	if ( a.splitPosition.hasSameParentAs( b.sourcePosition ) && a.splitPosition.offset <= b.sourcePosition.offset ) {
 		a.howMany -= b.howMany;
 	}
 
-	if ( a.position.hasSameParentAs( b.targetPosition ) && a.position.offset < b.targetPosition.offset ) {
+	if ( a.splitPosition.hasSameParentAs( b.targetPosition ) && a.splitPosition.offset < b.targetPosition.offset ) {
 		a.howMany += b.howMany;
 	}
 
 	// Change position stickiness to force a correct transformation.
-	a.position.stickiness = 'toNone';
-	a.position = a.position._getTransformedByMoveOperation( b );
-	a.position.stickiness = 'toNext';
+	a.splitPosition.stickiness = 'toNone';
+	a.splitPosition = a.splitPosition._getTransformedByMoveOperation( b );
+	a.splitPosition.stickiness = 'toNext';
+
+	if ( a.graveyardPosition ) {
+		a.insertionPosition = a.insertionPosition._getTransformedByMoveOperation( b );
+	} else {
+		a.insertionPosition = SplitOperation.getInsertionPosition( a.splitPosition );
+	}
 
 	return [ a ];
 } );
@@ -2297,7 +2318,7 @@ setTransformation( SplitOperation, SplitOperation, ( a, b, context ) => {
 	//
 	// So we cancel split operation only if it was really identical.
 	//
-	if ( a.position.isEqual( b.position ) ) {
+	if ( a.splitPosition.isEqual( b.splitPosition ) ) {
 		if ( !a.graveyardPosition && !b.graveyardPosition ) {
 			return [ new NoOperation( 0 ) ];
 		}
@@ -2316,7 +2337,7 @@ setTransformation( SplitOperation, SplitOperation, ( a, b, context ) => {
 	// Position where operation `b` inserted a new node after split is the same as the operation `a` split position.
 	// As in similar cases, there is ambiguity if the split should be before the new node (created by `b`) or after.
 	//
-	if ( a.position.isEqual( b.insertionPosition ) && context.abRelation == 'splitBefore' ) {
+	if ( a.splitPosition.isEqual( b.insertionPosition ) && context.abRelation == 'splitBefore' ) {
 		a.howMany++;
 
 		return [ a ];
@@ -2326,7 +2347,7 @@ setTransformation( SplitOperation, SplitOperation, ( a, b, context ) => {
 	//
 	// This is a mirror to the case 2. above.
 	//
-	if ( b.position.isEqual( a.insertionPosition ) && context.baRelation == 'splitBefore' ) {
+	if ( b.splitPosition.isEqual( a.insertionPosition ) && context.baRelation == 'splitBefore' ) {
 		const newPositionPath = b.insertionPosition.path.slice();
 		newPositionPath.push( 0 );
 
@@ -2338,13 +2359,12 @@ setTransformation( SplitOperation, SplitOperation, ( a, b, context ) => {
 
 	// The default case.
 	//
-	if ( a.position.hasSameParentAs( b.position ) && a.position.offset < b.position.offset ) {
+	if ( a.splitPosition.hasSameParentAs( b.splitPosition ) && a.splitPosition.offset < b.splitPosition.offset ) {
 		a.howMany -= b.howMany;
 	}
 
-	a.position = a.position._getTransformedBySplitOperation( b );
-
-	return [ a ];
+	a.splitPosition = a.splitPosition._getTransformedBySplitOperation( b );
+	a.insertionPosition = SplitOperation.getInsertionPosition( a.splitPosition );
 } );
 
 setTransformation( SplitOperation, WrapOperation, ( a, b ) => {

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

@@ -587,7 +587,7 @@ export default class Position {
 			( movedRange.start.isEqual( this ) && this.stickiness == 'toNext' );
 
 		if ( isContained ) {
-			return this._getCombined( operation.position, operation.moveTargetPosition );
+			return this._getCombined( operation.splitPosition, operation.moveTargetPosition );
 		} else {
 			if ( operation.graveyardPosition ) {
 				return this._getTransformedByMove( operation.graveyardPosition, operation.insertionPosition, 1 );

+ 2 - 3
packages/ckeditor5-engine/src/model/writer.js

@@ -16,8 +16,6 @@ import RenameOperation from './operation/renameoperation';
 import RootAttributeOperation from './operation/rootattributeoperation';
 import SplitOperation from './operation/splitoperation';
 import MergeOperation from './operation/mergeoperation';
-import WrapOperation from './operation/wrapoperation';
-import UnwrapOperation from './operation/unwrapoperation';
 
 import DocumentFragment from './documentfragment';
 import Text from './text';
@@ -653,7 +651,8 @@ export default class Writer {
 		do {
 			const version = splitElement.root.document ? splitElement.root.document.version : null;
 			const howMany = splitElement.maxOffset - position.offset;
-			const split = new SplitOperation( position, howMany, null, version );
+			const insertionPosition = SplitOperation.getInsertionPosition( position );
+			const split = new SplitOperation( position, howMany, insertionPosition, null, version );
 
 			this.batch.addOperation( split );
 			this.model.applyOperation( split );

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

@@ -360,15 +360,13 @@ describe( 'debug tools', () => {
 			} );
 
 			it( 'SplitOperation without graveyard position', () => {
-				const op = new SplitOperation(
-					new ModelPosition( modelRoot, [ 1, 4 ] ),
-					6,
-					null,
-					0
-				);
+				const position = new ModelPosition( modelRoot, [ 1, 4 ] );
+				const insertionPosition = SplitOperation.getInsertionPosition( position );
+
+				const op = new SplitOperation( position, 6, insertionPosition, null, 0 );
 
 				expect( op.toString() ).to.equal(
-					'SplitOperation( 0 ): main [ 1, 4 ] ( 6 )'
+					'SplitOperation( 0 ): main [ 1, 4 ] ( 6 ) -> main [ 2 ]'
 				);
 
 				op.log();
@@ -376,15 +374,13 @@ describe( 'debug tools', () => {
 			} );
 
 			it( 'SplitOperation with graveyard position', () => {
-				const op = new SplitOperation(
-					new ModelPosition( modelRoot, [ 1, 4 ] ),
-					6,
-					new ModelPosition( modelDoc.graveyard, [ 0 ] ),
-					0
-				);
+				const position = new ModelPosition( modelRoot, [ 1, 4 ] );
+				const insertionPosition = SplitOperation.getInsertionPosition( position );
+
+				const op = new SplitOperation( position, 6, insertionPosition, new ModelPosition( modelDoc.graveyard, [ 0 ] ), 0 );
 
 				expect( op.toString() ).to.equal(
-					'SplitOperation( 0 ): main [ 1, 4 ] ( 6 ), $graveyard [ 0 ]'
+					'SplitOperation( 0 ): main [ 1, 4 ] ( 6 ) -> main [ 2 ] with $graveyard [ 0 ]'
 				);
 
 				op.log();

+ 6 - 7
packages/ckeditor5-engine/tests/model/differ.js

@@ -1309,12 +1309,10 @@ describe( 'Differ', () => {
 			} );
 
 			model.change( () => {
-				const operation = new SplitOperation(
-					new Position( root, [ 0, 3 ] ),
-					3,
-					new Position( doc.graveyard, [ 0 ] ),
-					doc.version
-				);
+				const position = new Position( root, [ 0, 3 ] );
+				const insertionPosition = SplitOperation.getInsertionPosition( position );
+
+				const operation = new SplitOperation( position, 3, insertionPosition, new Position( doc.graveyard, [ 0 ] ), doc.version );
 
 				model.applyOperation( operation );
 
@@ -1881,7 +1879,8 @@ describe( 'Differ', () => {
 
 	function split( position ) {
 		const howMany = position.parent.maxOffset - position.offset;
-		const operation = new SplitOperation( position, howMany, null, doc.version );
+		const insertionPosition = SplitOperation.getInsertionPosition( position );
+		const operation = new SplitOperation( position, howMany, insertionPosition, null, doc.version );
 
 		model.applyOperation( operation );
 	}

+ 3 - 1
packages/ckeditor5-engine/tests/model/documentselection.js

@@ -1028,7 +1028,9 @@ describe( 'DocumentSelection', () => {
 				} );
 
 				const batch = new Batch();
-				const splitOperation = new SplitOperation( new Position( root, [ 1, 2 ] ), 4, null, 0 );
+				const position = new Position( root, [ 1, 2 ] );
+				const insertionPosition = SplitOperation.getInsertionPosition( position );
+				const splitOperation = new SplitOperation( position, 4, insertionPosition, null, 0 );
 
 				batch.addOperation( splitOperation );
 				model.applyOperation( splitOperation );

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

@@ -74,7 +74,8 @@ describe( 'MergeOperation', () => {
 		expect( reverse ).to.be.an.instanceof( SplitOperation );
 		expect( reverse.baseVersion ).to.equal( 1 );
 		expect( reverse.howMany ).to.equal( 2 );
-		expect( reverse.position.isEqual( targetPosition ) ).to.be.true;
+		expect( reverse.splitPosition.isEqual( targetPosition ) ).to.be.true;
+		expect( reverse.insertionPosition.isEqual( new Position( root, [ 1 ] ) ) ).to.be.true;
 		expect( reverse.graveyardPosition.isEqual( gyPos ) ).to.be.true;
 	} );
 

+ 59 - 22
packages/ckeditor5-engine/tests/model/operation/splitoperation.js

@@ -23,25 +23,37 @@ describe( 'SplitOperation', () => {
 	} );
 
 	it( 'should have proper type', () => {
-		const split = new SplitOperation( new Position( root, [ 1, 3 ] ), 2, null, 1 );
+		const position = new Position( root, [ 1, 3 ] );
+		const insertionPosition = SplitOperation.getInsertionPosition( position );
+
+		const split = new SplitOperation( position, 2, insertionPosition, null, 1 );
 
 		expect( split.type ).to.equal( 'split' );
 	} );
 
 	it( 'should have proper insertionPosition', () => {
-		const split = new SplitOperation( new Position( root, [ 1, 3 ] ), 2, null, 1 );
+		const position = new Position( root, [ 1, 3 ] );
+		const insertionPosition = SplitOperation.getInsertionPosition( position );
+
+		const split = new SplitOperation( position, 2, insertionPosition, null, 1 );
 
 		expect( split.insertionPosition.path ).to.deep.equal( [ 2 ] );
 	} );
 
 	it( 'should have proper moveTargetPosition', () => {
-		const split = new SplitOperation( new Position( root, [ 1, 3 ] ), 2, null, 1 );
+		const position = new Position( root, [ 1, 3 ] );
+		const insertionPosition = SplitOperation.getInsertionPosition( position );
+
+		const split = new SplitOperation( position, 2, insertionPosition, null, 1 );
 
 		expect( split.moveTargetPosition.path ).to.deep.equal( [ 2, 0 ] );
 	} );
 
 	it( 'should have proper movedRange', () => {
-		const split = new SplitOperation( new Position( root, [ 1, 3 ] ), 2, null, 1 );
+		const position = new Position( root, [ 1, 3 ] );
+		const insertionPosition = SplitOperation.getInsertionPosition( position );
+
+		const split = new SplitOperation( position, 2, insertionPosition, null, 1 );
 
 		expect( split.movedRange.start.path ).to.deep.equal( [ 1, 3 ] );
 		expect( split.movedRange.end.path ).to.deep.equal( [ 1, Number.POSITIVE_INFINITY ] );
@@ -52,7 +64,10 @@ describe( 'SplitOperation', () => {
 
 		root._insertChild( 0, [ p1 ] );
 
-		model.applyOperation( new SplitOperation( new Position( root, [ 0, 3 ] ), 3, null, doc.version ) );
+		const position = new Position( root, [ 0, 3 ] );
+		const insertionPosition = SplitOperation.getInsertionPosition( position );
+
+		model.applyOperation( new SplitOperation( position, 3, insertionPosition, null, doc.version ) );
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.maxOffset ).to.equal( 2 );
@@ -73,7 +88,10 @@ describe( 'SplitOperation', () => {
 		root._insertChild( 0, [ p1 ] );
 		gy._insertChild( 0, [ p2 ] );
 
-		model.applyOperation( new SplitOperation( new Position( root, [ 0, 3 ] ), 3, gyPos, doc.version ) );
+		const position = new Position( root, [ 0, 3 ] );
+		const insertionPosition = SplitOperation.getInsertionPosition( position );
+
+		model.applyOperation( new SplitOperation( position, 3, insertionPosition, gyPos, doc.version ) );
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.maxOffset ).to.equal( 2 );
@@ -90,7 +108,10 @@ describe( 'SplitOperation', () => {
 	} );
 
 	it( 'should create a proper MergeOperation as a reverse', () => {
-		const operation = new SplitOperation( new Position( root, [ 1, 3 ] ), 3, null, doc.version );
+		const position = new Position( root, [ 1, 3 ] );
+		const insertionPosition = SplitOperation.getInsertionPosition( position );
+
+		const operation = new SplitOperation( position, 3, insertionPosition, null, doc.version );
 		const reverse = operation.getReversed();
 
 		expect( reverse ).to.be.an.instanceof( MergeOperation );
@@ -106,7 +127,10 @@ describe( 'SplitOperation', () => {
 
 		root._insertChild( 0, [ p1 ] );
 
-		const operation = new SplitOperation( new Position( root, [ 0, 3 ] ), 3, null, doc.version );
+		const position = new Position( root, [ 0, 3 ] );
+		const insertionPosition = SplitOperation.getInsertionPosition( position );
+
+		const operation = new SplitOperation( position, 3, insertionPosition, null, doc.version );
 
 		model.applyOperation( operation );
 		model.applyOperation( operation.getReversed() );
@@ -123,7 +147,10 @@ describe( 'SplitOperation', () => {
 
 			root._insertChild( 0, [ p1 ] );
 
-			const operation = new SplitOperation( new Position( root, [ 0, 8 ] ), 3, null, doc.version );
+			const position = new Position( root, [ 0, 8 ] );
+			const insertionPosition = SplitOperation.getInsertionPosition( position );
+
+			const operation = new SplitOperation( position, 3, insertionPosition, null, doc.version );
 
 			expect( () => operation._validate() ).to.throw( CKEditorError, /split-operation-position-invalid/ );
 		} );
@@ -133,7 +160,7 @@ describe( 'SplitOperation', () => {
 
 			root._insertChild( 0, [ p1 ] );
 
-			const operation = new SplitOperation( new Position( root, [ 1 ] ), 3, null, doc.version );
+			const operation = new SplitOperation( new Position( root, [ 1 ] ), 3, new Position( root, [ 2 ] ), null, doc.version );
 
 			expect( () => operation._validate() ).to.throw( CKEditorError, /split-operation-split-in-root/ );
 		} );
@@ -143,7 +170,7 @@ describe( 'SplitOperation', () => {
 
 			root._insertChild( 0, [ p1 ] );
 
-			const operation = new SplitOperation( new Position( root, [ 0, 2 ] ), 6, null, doc.version );
+			const operation = new SplitOperation( new Position( root, [ 0, 2 ] ), 6, new Position( root, [ 1 ] ), null, doc.version );
 
 			expect( () => operation._validate() ).to.throw( CKEditorError, /split-operation-how-many-invalid/ );
 		} );
@@ -153,7 +180,7 @@ describe( 'SplitOperation', () => {
 
 			root._insertChild( 0, [ p1 ] );
 
-			const operation = new SplitOperation( new Position( root, [ 0, 2 ] ), 4, gyPos, doc.version );
+			const operation = new SplitOperation( new Position( root, [ 0, 2 ] ), 4, new Position( root, [ 1 ] ), gyPos, doc.version );
 
 			expect( () => operation._validate() ).to.throw( CKEditorError, /split-operation-graveyard-position-invalid/ );
 		} );
@@ -161,10 +188,11 @@ describe( 'SplitOperation', () => {
 
 	it( 'should create SplitOperation with the same parameters when cloned #1', () => {
 		const position = new Position( root, [ 1, 2 ] );
+		const insertionPosition = SplitOperation.getInsertionPosition( position );
 		const howMany = 4;
 		const baseVersion = doc.version;
 
-		const op = new SplitOperation( position, howMany, null, baseVersion );
+		const op = new SplitOperation( position, howMany, insertionPosition, null, baseVersion );
 
 		const clone = op.clone();
 
@@ -172,18 +200,20 @@ describe( 'SplitOperation', () => {
 		expect( clone ).not.to.be.equal( op );
 
 		expect( clone ).to.be.instanceof( SplitOperation );
-		expect( clone.position.isEqual( position ) ).to.be.true;
+		expect( clone.splitPosition.isEqual( position ) ).to.be.true;
 		expect( clone.howMany ).to.equal( howMany );
+		expect( clone.insertionPosition.isEqual( insertionPosition ) );
 		expect( clone.graveyardPosition ).to.be.null;
 		expect( clone.baseVersion ).to.equal( baseVersion );
 	} );
 
 	it( 'should create SplitOperation with the same parameters when cloned #2', () => {
 		const position = new Position( root, [ 1, 2 ] );
+		const insertionPosition = SplitOperation.getInsertionPosition( position );
 		const howMany = 4;
 		const baseVersion = doc.version;
 
-		const op = new SplitOperation( position, howMany, gyPos, baseVersion );
+		const op = new SplitOperation( position, howMany, insertionPosition, gyPos, baseVersion );
 
 		const clone = op.clone();
 
@@ -191,8 +221,9 @@ describe( 'SplitOperation', () => {
 		expect( clone ).not.to.be.equal( op );
 
 		expect( clone ).to.be.instanceof( SplitOperation );
-		expect( clone.position.isEqual( position ) ).to.be.true;
+		expect( clone.splitPosition.isEqual( position ) ).to.be.true;
 		expect( clone.howMany ).to.equal( howMany );
+		expect( clone.insertionPosition.isEqual( insertionPosition ) );
 		expect( clone.graveyardPosition.isEqual( gyPos ) ).to.be.true;
 		expect( clone.baseVersion ).to.equal( baseVersion );
 	} );
@@ -200,7 +231,8 @@ describe( 'SplitOperation', () => {
 	describe( 'toJSON', () => {
 		it( 'should create proper json object #1', () => {
 			const position = new Position( root, [ 0, 3 ] );
-			const op = new SplitOperation( position, 2, null, doc.version );
+			const insertionPosition = SplitOperation.getInsertionPosition( position );
+			const op = new SplitOperation( position, 2, insertionPosition, null, doc.version );
 
 			const serialized = op.toJSON();
 
@@ -208,14 +240,16 @@ describe( 'SplitOperation', () => {
 				__className: 'SplitOperation',
 				baseVersion: 0,
 				howMany: 2,
-				position: op.position.toJSON(),
+				splitPosition: op.splitPosition.toJSON(),
+				insertionPosition: op.insertionPosition.toJSON(),
 				graveyardPosition: null
 			} );
 		} );
 
 		it( 'should create proper json object #2', () => {
 			const position = new Position( root, [ 0, 3 ] );
-			const op = new SplitOperation( position, 2, gyPos, doc.version );
+			const insertionPosition = SplitOperation.getInsertionPosition( position );
+			const op = new SplitOperation( position, 2, insertionPosition, gyPos, doc.version );
 
 			const serialized = op.toJSON();
 
@@ -223,7 +257,8 @@ describe( 'SplitOperation', () => {
 				__className: 'SplitOperation',
 				baseVersion: 0,
 				howMany: 2,
-				position: op.position.toJSON(),
+				splitPosition: op.splitPosition.toJSON(),
+				insertionPosition: op.insertionPosition.toJSON(),
 				graveyardPosition: op.graveyardPosition.toJSON()
 			} );
 		} );
@@ -232,7 +267,8 @@ describe( 'SplitOperation', () => {
 	describe( 'fromJSON', () => {
 		it( 'should create proper SplitOperation from json object #1', () => {
 			const position = new Position( root, [ 0, 3 ] );
-			const op = new SplitOperation( position, 2, null, doc.version );
+			const insertionPosition = SplitOperation.getInsertionPosition( position );
+			const op = new SplitOperation( position, 2, insertionPosition, null, doc.version );
 
 			const serialized = op.toJSON();
 
@@ -243,7 +279,8 @@ describe( 'SplitOperation', () => {
 
 		it( 'should create proper SplitOperation from json object #2', () => {
 			const position = new Position( root, [ 0, 3 ] );
-			const op = new SplitOperation( position, 2, gyPos, doc.version );
+			const insertionPosition = SplitOperation.getInsertionPosition( position );
+			const op = new SplitOperation( position, 2, insertionPosition, gyPos, doc.version );
 
 			const serialized = op.toJSON();
 

+ 24 - 6
packages/ckeditor5-engine/tests/model/position.js

@@ -702,35 +702,50 @@ describe( 'Position', () => {
 
 		describe( 'by SplitOperation', () => {
 			it( 'transformed position is at the split position', () => {
-				const op = new SplitOperation( new Position( root, [ 3, 2 ] ), 3, null, 1 );
+				const position = new Position( root, [ 3, 2 ] );
+				const insertionPosition = SplitOperation.getInsertionPosition( position );
+
+				const op = new SplitOperation( position, 3, insertionPosition, null, 1 );
 				const transformed = pos.getTransformedByOperation( op );
 
 				expect( transformed.path ).to.deep.equal( [ 3, 2 ] );
 			} );
 
 			it( 'transformed position is after the split position', () => {
-				const op = new SplitOperation( new Position( root, [ 3, 1 ] ), 3, null, 1 );
+				const position = new Position( root, [ 3, 1 ] );
+				const insertionPosition = SplitOperation.getInsertionPosition( position );
+
+				const op = new SplitOperation( position, 3, insertionPosition, null, 1 );
 				const transformed = pos.getTransformedByOperation( op );
 
 				expect( transformed.path ).to.deep.equal( [ 4, 1 ] );
 			} );
 
 			it( 'transformed position is before the split position', () => {
-				const op = new SplitOperation( new Position( root, [ 3, 3 ] ), 3, null, 1 );
+				const position = new Position( root, [ 3, 3 ] );
+				const insertionPosition = SplitOperation.getInsertionPosition( position );
+
+				const op = new SplitOperation( position, 3, insertionPosition, null, 1 );
 				const transformed = pos.getTransformedByOperation( op );
 
 				expect( transformed.path ).to.deep.equal( [ 3, 2 ] );
 			} );
 
 			it( 'transformed position is after the split element', () => {
-				const op = new SplitOperation( new Position( root, [ 3, 1, 5 ] ), 3, null, 1 );
+				const position = new Position( root, [ 3, 1, 5 ] );
+				const insertionPosition = SplitOperation.getInsertionPosition( position );
+
+				const op = new SplitOperation( position, 3, insertionPosition, null, 1 );
 				const transformed = pos.getTransformedByOperation( op );
 
 				expect( transformed.path ).to.deep.equal( [ 3, 3 ] );
 			} );
 
 			it( 'transformed position is before the split element', () => {
-				const op = new SplitOperation( new Position( root, [ 3, 3, 5 ] ), 3, null, 1 );
+				const position = new Position( root, [ 3, 3, 5 ] );
+				const insertionPosition = SplitOperation.getInsertionPosition( position );
+
+				const op = new SplitOperation( position, 3, insertionPosition, null, 1 );
 				const transformed = pos.getTransformedByOperation( op );
 
 				expect( transformed.path ).to.deep.equal( [ 3, 2 ] );
@@ -739,7 +754,10 @@ describe( 'Position', () => {
 			it( 'transformed position is in graveyard and split position uses graveyard element', () => {
 				pos = new Position( doc.graveyard, [ 1 ] );
 
-				const op = new SplitOperation( new Position( root, [ 3, 2 ] ), 3, new Position( doc.graveyard, [ 0 ] ), 1 );
+				const position = new Position( root, [ 3, 2 ] );
+				const insertionPosition = SplitOperation.getInsertionPosition( position );
+
+				const op = new SplitOperation( position, 3, insertionPosition, new Position( doc.graveyard, [ 0 ] ), 1 );
 				const transformed = pos.getTransformedByOperation( op );
 
 				expect( transformed.path ).to.deep.equal( [ 0 ] );

+ 20 - 5
packages/ckeditor5-engine/tests/model/range.js

@@ -989,7 +989,10 @@ describe( 'Range', () => {
 			it( 'split inside range', () => {
 				range = new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 0, 4 ] ) );
 
-				const op = new SplitOperation( new Position( root, [ 0, 3 ] ), 6, gyPos, 1 );
+				const position = new Position( root, [ 0, 3 ] );
+				const insertionPosition = SplitOperation.getInsertionPosition( position );
+
+				const op = new SplitOperation( position, 6, insertionPosition, gyPos, 1 );
 				const transformed = range.getTransformedByOperation( op );
 
 				expect( transformed.length ).to.equal( 1 );
@@ -1000,7 +1003,10 @@ describe( 'Range', () => {
 			it( 'split at the beginning of multi-element range', () => {
 				range = new Range( new Position( root, [ 0, 4 ] ), new Position( root, [ 1, 2 ] ) );
 
-				const op = new SplitOperation( new Position( root, [ 0, 4 ] ), 5, gyPos, 1 );
+				const position = new Position( root, [ 0, 4 ] );
+				const insertionPosition = SplitOperation.getInsertionPosition( position );
+
+				const op = new SplitOperation( position, 5, insertionPosition, gyPos, 1 );
 				const transformed = range.getTransformedByOperation( op );
 
 				expect( transformed.length ).to.equal( 1 );
@@ -1011,7 +1017,10 @@ describe( 'Range', () => {
 			it( 'split inside range which starts at the beginning of split element', () => {
 				range = new Range( new Position( root, [ 0, 0 ] ), new Position( root, [ 0, 4 ] ) );
 
-				const op = new SplitOperation( new Position( root, [ 0, 3 ] ), 6, gyPos, 1 );
+				const position = new Position( root, [ 0, 3 ] );
+				const insertionPosition = SplitOperation.getInsertionPosition( position );
+
+				const op = new SplitOperation( position, 6, insertionPosition, gyPos, 1 );
 				const transformed = range.getTransformedByOperation( op );
 
 				expect( transformed.length ).to.equal( 1 );
@@ -1022,7 +1031,10 @@ describe( 'Range', () => {
 			it( 'split inside range which end is at the end of split element', () => {
 				range = new Range( new Position( root, [ 0, 3 ] ), new Position( root, [ 0, 6 ] ) );
 
-				const op = new SplitOperation( new Position( root, [ 0, 4 ] ), 5, gyPos, 1 );
+				const position = new Position( root, [ 0, 4 ] );
+				const insertionPosition = SplitOperation.getInsertionPosition( position );
+
+				const op = new SplitOperation( position, 5, insertionPosition, gyPos, 1 );
 				const transformed = range.getTransformedByOperation( op );
 
 				expect( transformed.length ).to.equal( 1 );
@@ -1033,7 +1045,10 @@ describe( 'Range', () => {
 			it( 'split element which has collapsed range at the end', () => {
 				range = new Range( new Position( root, [ 0, 6 ] ), new Position( root, [ 0, 6 ] ) );
 
-				const op = new SplitOperation( new Position( root, [ 0, 3 ] ), 6, gyPos, 1 );
+				const position = new Position( root, [ 0, 3 ] );
+				const insertionPosition = SplitOperation.getInsertionPosition( position );
+
+				const op = new SplitOperation( position, 6, insertionPosition, gyPos, 1 );
 				const transformed = range.getTransformedByOperation( op );
 
 				expect( transformed.length ).to.equal( 1 );