Procházet zdrojové kódy

Introduced transformations for Deltas

Szymon Cofalik před 10 roky
rodič
revize
0f5e8812c8

+ 11 - 0
packages/ckeditor5-engine/src/treemodel/delta/delta.js

@@ -49,4 +49,15 @@ export default class Delta {
 
 		return operation;
 	}
+
+	clone() {
+		let delta = new this.constructor();
+		delta.batch = this.batch;
+
+		for ( let op of this.operations ) {
+			delta.addOperation( op.clone() );
+		}
+
+		return delta;
+	}
 }

+ 12 - 4
packages/ckeditor5-engine/src/treemodel/delta/insertdelta.js

@@ -16,7 +16,15 @@ import InsertOperation from '../operation/insertoperation.js';
  *
  * @memberOf core.treeModel.delta
  */
-export default class InsertDelta extends Delta {}
+export default class InsertDelta extends Delta {
+	get insertOperation() {
+		return this.operations[ 0 ] || null;
+	}
+
+	get position() {
+		return this.insertOperation ? this.insertOperation.position : null;
+	}
+}
 
 /**
  * Inserts a node or nodes at the given position.
@@ -29,10 +37,10 @@ export default class InsertDelta extends Delta {}
  */
 register( 'insert', function( position, nodes ) {
 	const delta = new InsertDelta();
+	const insert = new InsertOperation( position, nodes, this.doc.version );
 
-	const operation = new InsertOperation( position, nodes, this.doc.version );
-	this.doc.applyOperation( operation );
-	delta.addOperation( operation );
+	delta.addOperation( insert );
+	this.doc.applyOperation( insert );
 
 	this.addDelta( delta );
 

+ 30 - 1
packages/ckeditor5-engine/src/treemodel/delta/mergedelta.js

@@ -6,6 +6,7 @@
 'use strict';
 
 import Delta from './delta.js';
+import SplitDelta from './splitdelta.js';
 import { register } from '../batch-base.js';
 import Position from '../position.js';
 import Element from '../element.js';
@@ -20,7 +21,35 @@ import CKEditorError from '../../ckeditorerror.js';
  *
  * @memberOf core.treeModel.delta
  */
-export default class MergeDelta extends Delta {}
+export default class MergeDelta extends Delta {
+	get moveOperation() {
+		return this.operations[ 0 ] || null;
+	}
+
+	get removeOperation() {
+		return this.operations[ 1 ] || null;
+	}
+
+	get position() {
+		return this.removeOperation ? this.removeOperation.sourcePosition : null;
+	}
+
+	getReversed() {
+		if ( this.moveOperation ) {
+			let delta = new SplitDelta();
+
+			for ( let op of this.operations ) {
+				delta.addOperation( op.getReversed() );
+			}
+
+			delta.operations.reverse();
+
+			return delta;
+		}
+
+		return null;
+	}
+}
 
 /**
  * Merges two siblings at the given position.

+ 17 - 3
packages/ckeditor5-engine/src/treemodel/delta/splitdelta.js

@@ -20,7 +20,19 @@ import CKEditorError from '../../ckeditorerror.js';
  *
  * @memberOf core.treeModel.delta
  */
-export default class SplitDelta extends Delta {}
+export default class SplitDelta extends Delta {
+	get cloneOperation() {
+		return this.operations[ 0 ] || null;
+	}
+
+	get moveOperation() {
+		return this.operations[ 1 ] || null;
+	}
+
+	get position() {
+		return this.moveOperation ? this.moveOperation.sourcePosition : null;
+	}
+}
 
 /**
  * Splits a node at the given position.
@@ -34,6 +46,7 @@ export default class SplitDelta extends Delta {}
  */
 register( 'split', function( position ) {
 	const delta = new SplitDelta();
+
 	const splitElement = position.parent;
 
 	if ( !splitElement.parent ) {
@@ -46,10 +59,11 @@ register( 'split', function( position ) {
 	}
 
 	const copy = new Element( splitElement.name, splitElement._attrs );
+
 	const insert = new InsertOperation( Position.createAfter( splitElement ), copy, this.doc.version );
 
-	this.doc.applyOperation( insert );
 	delta.addOperation( insert );
+	this.doc.applyOperation( insert );
 
 	const move = new MoveOperation(
 		position,
@@ -58,8 +72,8 @@ register( 'split', function( position ) {
 		this.doc.version
 	);
 
-	this.doc.applyOperation( move );
 	delta.addOperation( move );
+	this.doc.applyOperation( move );
 
 	this.addDelta( delta );
 

+ 170 - 0
packages/ckeditor5-engine/src/treemodel/delta/transform.js

@@ -0,0 +1,170 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import OT from '../operation/transform.js';
+
+import AttributeDelta from './attributedelta.js';
+import InsertDelta from './insertdelta.js';
+import MergeDelta from './mergedelta.js';
+import MoveDelta from './movedelta.js';
+import RemoveDelta from './removedelta.js';
+import SplitDelta from './splitdelta.js';
+import Delta from './delta.js';
+
+import arrayUtils from '../../lib/lodash/array.js';
+import utils from '../../utils.js';
+
+const baseDeltas = [ AttributeDelta, InsertDelta, MoveDelta, RemoveDelta ];
+const specialCases = new Map();
+
+export default function transform( a, b, isStrong ) {
+	let transformAlgorithm = defaultTransform;
+
+	let casesA = specialCases.get( a.constructor );
+
+	if ( casesA ) {
+		let caseB = casesA.get( b.constructor );
+		transformAlgorithm = caseB || transformAlgorithm;
+	}
+
+	let transformed = transformAlgorithm( a, b, isStrong );
+	let baseVersion = arrayUtils.last( b.operations ).baseVersion;
+
+	return updateBaseVersion( baseVersion, transformed );
+}
+
+export function defaultTransform( a, b, isStrong ) {
+	isStrong = getPriority( a, b, isStrong );
+
+	const transformed = new a.constructor();
+
+	let byOps = b.operations;
+	let newByOps = [];
+
+	for ( let opA of a.operations ) {
+		let ops = [ opA ];
+
+		for ( let opB of byOps ) {
+			for ( let i = 0; i < ops.length; i++ ) {
+				let op = ops[ i ];
+
+				// Using push.apply because OT function is returning an array with one or multiple results.
+				Array.prototype.push.apply( newByOps, OT( opB, op, !isStrong ) );
+
+				// Using splice.apply for the same reason.
+				let results = OT( op, opB, isStrong );
+				Array.prototype.splice.apply( ops, [ i, 1 ].concat( results ) );
+
+				i += results.length - 1;
+			}
+		}
+
+		byOps = newByOps;
+		newByOps = [];
+
+		for ( let op of ops ) {
+			transformed.addOperation( op );
+		}
+	}
+
+	return [ transformed ];
+}
+
+export function addSpecialCase( a, b, resolver ) {
+	let casesA = specialCases.get( a );
+
+	if ( !casesA ) {
+		casesA = new Map();
+		specialCases.set( a, casesA );
+	}
+
+	casesA.set( b, resolver );
+}
+
+addSpecialCase( InsertDelta, MergeDelta, ( a, b, isStrong ) => {
+	// If insert is applied at the same position where merge happened, we reverse the merge (we treat it like it
+	// didn't happen) and then apply the original insert operation. This is "mirrored" in MergeDelta x InsertDelta
+	// transformation below, where we simply do not apply MergeDelta.
+	if ( a.position.isEqual( b.position ) ) {
+		return [
+			b.getReversed(),
+			a.clone()
+		];
+	}
+
+	return defaultTransform( a, b, isStrong );
+} );
+
+addSpecialCase( MergeDelta, InsertDelta, ( a, b, isStrong ) => {
+	// If merge is applied at the same position where we inserted a range of nodes we cancel the merge as it's results
+	// may be unexpected and very weird. Even if we do some "magic" we don't know what really are users' expectations.
+	if ( a.position.isEqual( b.position ) ) {
+		// This is "no-op" delta, it has no type and no operations, it basically does nothing.
+		// It is used when we don't want to apply changes but still we need to return a delta.
+		return [ new Delta() ];
+	}
+
+	return defaultTransform( a, b, isStrong );
+} );
+
+addSpecialCase( SplitDelta, SplitDelta, ( a, b, isStrong ) => {
+	let pathA = a.position.getParentPath();
+	let pathB = b.position.getParentPath();
+
+	// The special case is for splits inside the same parent.
+	if ( utils.compareArrays( pathA, pathB ) == 'SAME' ) {
+		if ( a.position.offset == b.position.offset ) {
+			// We are applying split at the position where split already happened. Additional split is not needed.
+			return [ new Delta() ];
+		} else if ( a.position.offset < b.position.offset ) {
+			// Incoming split delta splits at closer offset. So we simply have to once again split the same node,
+			// but since it was already split (at further offset) there are less child nodes in the split node.
+			// This means that we have to update `howMany` parameter of `MoveOperation` for that delta.
+
+			let delta = a.clone();
+			delta.moveOperation.howMany = b.position.offset - a.position.offset;
+
+			return [ delta ];
+		} else if ( a.position.offset > b.position.offset ) {
+			// Incoming split delta splits at further offset. We have to simulate that we are not splitting the
+			// original split node but the node after it, which got created by the other split delta.
+			// To do so, we increment offsets so it looks like the split delta was created in the next node.
+
+			let delta = a.clone();
+
+			delta.cloneOperation.position.offset++;
+			delta.moveOperation.sourcePosition.path[ delta.moveOperation.sourcePosition.path.length - 2 ]++;
+			delta.moveOperation.targetPosition.path[ delta.moveOperation.targetPosition.path.length - 2 ]++;
+			delta.moveOperation.sourcePosition.offset = a.position.offset - b.position.offset;
+
+			return [ delta ];
+		}
+	}
+
+	return defaultTransform( a, b, isStrong );
+} );
+
+function getPriority( a, b, isStrong ) {
+	let aIsBase = baseDeltas.indexOf( a ) > -1;
+	let bIsBase = baseDeltas.indexOf( b ) > -1;
+
+	if ( aIsBase != bIsBase ) {
+		return aIsBase;
+	} else {
+		return isStrong;
+	}
+}
+
+function updateBaseVersion( baseVersion, deltas ) {
+	for ( let delta of deltas ) {
+		for ( let op of delta.operations ) {
+			op.baseVersion = ++baseVersion;
+		}
+	}
+
+	return deltas;
+}

+ 458 - 0
packages/ckeditor5-engine/tests/treemodel/delta/transform.js

@@ -0,0 +1,458 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: treemodel, operation */
+
+'use strict';
+
+import transform from '/ckeditor5/core/treemodel/delta/transform.js';
+
+import Document from '/ckeditor5/core/treemodel/document.js';
+import Element from '/ckeditor5/core/treemodel/element.js';
+import Position from '/ckeditor5/core/treemodel/position.js';
+import Range from '/ckeditor5/core/treemodel/range.js';
+
+import Delta from '/ckeditor5/core/treemodel/delta/delta.js';
+import InsertDelta from '/ckeditor5/core/treemodel/delta/insertdelta.js';
+import MergeDelta from '/ckeditor5/core/treemodel/delta/mergedelta.js';
+import SplitDelta from '/ckeditor5/core/treemodel/delta/splitdelta.js';
+
+import InsertOperation from '/ckeditor5/core/treemodel/operation/insertoperation.js';
+import MoveOperation from '/ckeditor5/core/treemodel/operation/moveoperation.js';
+import RemoveOperation from '/ckeditor5/core/treemodel/operation/removeoperation.js';
+import ReinsertOperation from '/ckeditor5/core/treemodel/operation/reinsertoperation.js';
+
+import treeModelTestUtils from '/tests/core/treemodel/_utils/utils.js';
+const getNodesAndText = treeModelTestUtils.getNodesAndText;
+
+function getInsertDelta( position, nodes, version ) {
+	let delta = new InsertDelta();
+	delta.addOperation( new InsertOperation( position, nodes, version ) );
+
+	return delta;
+}
+
+function getMergeDelta( position, howManyInPrev, howManyInNext, version ) {
+	let delta = new MergeDelta();
+
+	let sourcePosition = Position.createFromPosition( position );
+	sourcePosition.path.push( 0 );
+
+	let targetPosition = Position.createFromPosition( position );
+	targetPosition.offset--;
+	targetPosition.path.push( howManyInPrev );
+
+	delta.addOperation( new MoveOperation( sourcePosition, howManyInNext, targetPosition, version ) );
+	delta.addOperation( new RemoveOperation( position, 1, version + 1 ) );
+
+	return delta;
+}
+
+function getSplitDelta( position, nodeCopy, howManyMove, version ) {
+	let delta = new SplitDelta();
+
+	let insertPosition = Position.createFromPosition( position );
+	insertPosition.path = insertPosition.getParentPath();
+	insertPosition.offset++;
+
+	let targetPosition = Position.createFromPosition( insertPosition );
+	targetPosition.path.push( 0 );
+
+	delta.addOperation( new InsertOperation( insertPosition, [ nodeCopy ], version ) );
+	delta.addOperation( new MoveOperation( position, howManyMove, targetPosition, version + 1 ) );
+
+	return delta;
+}
+
+function expectDelta( delta, expected ) {
+	expect( delta ).to.be.instanceof( expected.type );
+	expect( delta.operations.length ).to.equal( expected.operations.length );
+
+	for ( let i = 0; i < delta.operations.length; i++ ) {
+		expectOperation( delta.operations[ i ], expected.operations[ i ] );
+	}
+}
+
+function expectOperation( op, params ) {
+	for ( let i in params ) {
+		if ( params.hasOwnProperty( i ) ) {
+			if ( i == 'type' ) {
+				expect( op ).to.be.instanceof( params[ i ] );
+			}
+			else if ( i == 'nodes' ) {
+				expect( op.nodeList._nodes ).to.deep.equal( params[ i ] );
+			} else if ( params[ i ] instanceof Position || params[ i ] instanceof Range ) {
+				expect( op[ i ].isEqual( params[ i ] ) ).to.be.true;
+			} else {
+				expect( op[ i ] ).to.equal( params[ i ] );
+			}
+		}
+	}
+}
+
+function applyDelta( delta, document ) {
+	for ( let op of delta.operations ) {
+		document.applyOperation( op );
+	}
+}
+
+describe( 'transform', () => {
+	let doc, root, gy, baseVersion;
+
+	beforeEach( () => {
+		doc = new Document();
+		root = doc.createRoot( 'root' );
+
+		root.insertChildren( 0, [
+			new Element( 'x' ),
+			new Element( 'x' ),
+			new Element( 'x', [], 'a' ),
+			new Element( 'div', [], [
+				new Element( 'x' ),
+				new Element( 'x' ),
+				new Element( 'x', [], 'a' ),
+				new Element( 'div', [], [
+					new Element( 'x' ),
+					new Element( 'x' ),
+					new Element( 'x', [], 'abcd' ),
+					new Element( 'p', [], 'abcfoobarxyz' )
+				] )
+			] )
+		] );
+
+		gy = doc.graveyard;
+		baseVersion = doc.version;
+	} );
+
+	describe( 'InsertDelta by', () => {
+		let insertDelta, insertPosition, nodeA, nodeB;
+
+		beforeEach( () => {
+			nodeA = new Element( 'a' );
+			nodeB = new Element( 'b' );
+
+			insertPosition = new Position( root, [ 3, 3, 3 ] );
+			insertDelta = getInsertDelta( insertPosition, [ nodeA, nodeB ], baseVersion );
+		} );
+
+		describe( 'MergeDelta', () => {
+			it( 'merge in same position as insert', () => {
+				let mergeDelta = getMergeDelta( insertPosition, 4, 12, baseVersion );
+				let transformed = transform( insertDelta, mergeDelta );
+
+				baseVersion = mergeDelta.operations.length;
+
+				// Expected: MergeOperation gets reversed (by special case of SplitDelta that has ReinsertOperation
+				// instead of InsertOperation. Then InsertDelta is applied as is.
+
+				expect( transformed.length ).to.equal( 2 );
+
+				expectDelta( transformed[ 0 ], {
+					type: SplitDelta,
+					operations: [
+						{
+							type: ReinsertOperation,
+							sourcePosition: new Position( gy, [ 0 ] ),
+							howMany: 1,
+							targetPosition: new Position( root, [ 3, 3, 3 ] ),
+							baseVersion: baseVersion
+						},
+						{
+							type: MoveOperation,
+							sourcePosition: new Position( root, [ 3, 3, 2, 4 ] ),
+							howMany: 12,
+							targetPosition: new Position( root, [ 3, 3, 3, 0 ] ),
+							baseVersion: baseVersion + 1
+						}
+					]
+				} );
+
+				expectDelta( transformed[ 1 ], {
+					type: InsertDelta,
+					operations: [
+						{
+							type: InsertOperation,
+							position: Position.createFromPosition( insertPosition ),
+							nodes: [ nodeA, nodeB ],
+							baseVersion: baseVersion + 2
+						}
+					]
+				} );
+
+				// Test if deltas do what they should after applying transformed delta.
+
+				applyDelta( mergeDelta, doc );
+				applyDelta( transformed[ 0 ], doc );
+				applyDelta( transformed[ 1 ], doc );
+
+				let nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 0 ] ), 6 ) );
+
+				// Merge between X with "abcd" and P with "abcfoobarxyz" should be reversed and AB should be inserted between X and P.
+				expect( nodesAndText ).to.equal( 'XXXXXabcdXAABBPabcfoobarxyzP' );
+			} );
+
+			it( 'merge the node that is parent of insert position (sticky move test)', () => {
+				let mergeDelta = getMergeDelta( new Position( root, [ 3, 3 ] ), 1, 4, baseVersion );
+				let transformed = transform( insertDelta, mergeDelta );
+
+				baseVersion = mergeDelta.operations.length;
+
+				// Expected: InsertOperation in InsertDelta has it's path updated.
+
+				expect( transformed.length ).to.equal( 1 );
+
+				expectDelta( transformed[ 0 ], {
+					type: InsertDelta,
+					operations: [
+						{
+							type: InsertOperation,
+							position: new Position( root, [ 3, 2, 4 ] ),
+							nodes: [ nodeA, nodeB ],
+							baseVersion: baseVersion
+						}
+					]
+				} );
+
+				// Test if deltas do what they should after applying transformed delta.
+
+				applyDelta( mergeDelta, doc );
+				applyDelta( transformed[ 0 ], doc );
+
+				let nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 2, 0 ] ), 7 ) );
+
+				// Merge between X with "a" and DIV should be applied. AB should be inserted in new, correct position.
+				expect( nodesAndText ).to.equal( 'aXXXXXabcdXAABBPabcfoobarxyzP' );
+			} );
+
+			it( 'merge at affected position but resolved by default OT', () => {
+				let mergeDelta = getMergeDelta( new Position( root, [ 3 ] ), 1, 4, baseVersion );
+				let transformed = transform( insertDelta, mergeDelta );
+
+				baseVersion = mergeDelta.operations.length;
+
+				// Expected: InsertOperation in InsertDelta has it's path updated.
+
+				expect( transformed.length ).to.equal( 1 );
+
+				expectDelta( transformed[ 0 ], {
+					type: InsertDelta,
+					operations: [
+						{
+							type: InsertOperation,
+							position: new Position( root, [ 2, 4, 3 ] ),
+							nodes: [ nodeA, nodeB ],
+							baseVersion: baseVersion
+						}
+					]
+				} );
+
+				// Test if deltas do what they should after applying transformed delta.
+
+				applyDelta( mergeDelta, doc );
+				applyDelta( transformed[ 0 ], doc );
+
+				let nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 2, 0 ] ), 5 ) );
+
+				// Merge between X with "a" and DIV should be applied. AB should be inserted in new, correct position.
+				expect( nodesAndText ).to.equal( 'aXXXXXaXDIVXXXXXabcdXAABBPabcfoobarxyzPDIV' );
+			} );
+		} );
+	} );
+
+	describe( 'MergeDelta by', () => {
+		let mergeDelta, mergePosition;
+
+		beforeEach( () => {
+			mergePosition = new Position( root, [ 3, 3, 3 ] );
+			mergeDelta = getMergeDelta( mergePosition, 4, 12, baseVersion );
+		} );
+
+		describe( 'InsertDelta', () => {
+			let nodeA, nodeB;
+
+			beforeEach( () => {
+				nodeA = new Element( 'a' );
+				nodeB = new Element( 'b' );
+			} );
+
+			it( 'insert at same position as merge', () => {
+				let insertDelta = getInsertDelta( mergePosition, [ nodeA, nodeB ], baseVersion );
+				let transformed = transform( mergeDelta, insertDelta );
+
+				// Expected: MergeDelta gets ignored and is not applied.
+
+				expect( transformed.length ).to.equal( 1 );
+
+				expectDelta( transformed[ 0 ], {
+					type: Delta,
+					operations: []
+				} );
+
+				// Test if deltas do what they should after applying transformed delta.
+
+				applyDelta( insertDelta, doc );
+				applyDelta( transformed[ 0 ], doc );
+
+				let nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 0 ] ), 6 ) );
+
+				// InsertDelta is applied. Merge between X and P is discarded.
+				expect( nodesAndText ).to.equal( 'XXXXXabcdXAABBPabcfoobarxyzP' );
+			} );
+
+			it( 'insert inside merged node (sticky move test)', () => {
+				let insertDelta = getInsertDelta( new Position( root, [ 3, 3, 3, 12 ] ), [ nodeA, nodeB ], baseVersion );
+				let transformed = transform( mergeDelta, insertDelta );
+
+				baseVersion = insertDelta.operations.length;
+
+				expect( transformed.length ).to.equal( 1 );
+
+				// Expected: MoveOperation in MergeDelta has it's "range" expanded.
+
+				expectDelta( transformed[ 0 ], {
+					type: MergeDelta,
+					operations: [
+						{
+							type: MoveOperation,
+							sourcePosition: new Position( root, [ 3, 3, 3, 0 ] ),
+							howMany: 14,
+							targetPosition: new Position( root, [ 3, 3, 2, 4 ] ),
+							baseVersion: baseVersion
+						},
+						{
+							// This is `MoveOperation` instead of `RemoveOperation` because during OT,
+							// `RemoveOperation` may get converted to `MoveOperation`. Still, this expectation is
+							// correct because `RemoveOperation` is deriving from `MoveOperation`. So we can expect
+							// that something that was `RemoveOperation` is a `MoveOperation`.
+							type: MoveOperation,
+							sourcePosition: new Position( root, [ 3, 3, 3 ] ),
+							howMany: 1,
+							targetPosition: new Position( gy, [ 0 ] ),
+							baseVersion: baseVersion + 1
+						}
+					]
+				} );
+
+				// Test if deltas do what they should after applying transformed delta.
+
+				applyDelta( insertDelta, doc );
+				applyDelta( transformed[ 0 ], doc );
+
+				let nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 0 ] ), 3 ) );
+
+				// InsertDelta is applied. Merge between X and P is discarded.
+				expect( nodesAndText ).to.equal( 'XXXXXabcdabcfoobarxyzAABBX' );
+			} );
+		} );
+	} );
+
+	describe( 'SplitDelta by', () => {
+		let splitDeltaA, splitPosition;
+
+		beforeEach( () => {
+			splitPosition = new Position( root, [ 3, 3, 3, 3 ] );
+			splitDeltaA = getSplitDelta( splitPosition, new Element( 'p' ), 9, baseVersion );
+		} );
+
+		describe( 'SplitDelta', () => {
+			it( 'split in same parent and offset', () => {
+				let splitDeltaB = getSplitDelta( splitPosition, new Element( 'p' ), 9, baseVersion );
+				let transformed = transform( splitDeltaA, splitDeltaB );
+
+				expect( transformed.length ).to.equal( 1 );
+
+				expectDelta( transformed[ 0 ], {
+					type: Delta,
+					operations: []
+				} );
+
+				// Test if deltas do what they should after applying transformed delta.
+
+				applyDelta( splitDeltaB, doc );
+				applyDelta( transformed[ 0 ], doc );
+
+				let nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 0 ] ), 5 ) );
+
+				// Incoming split delta is discarded. Only one new element is created after applying both split deltas.
+				// There are no empty P elements.
+				expect( nodesAndText ).to.equal( 'XXXXXabcdXPabcPPfoobarxyzP' );
+			} );
+
+			it( 'split in same parent, incoming delta splits closer', () => {
+				let splitDeltaB = getSplitDelta( new Position( root, [ 3, 3, 3, 5 ] ), new Element( 'p' ), 7, baseVersion );
+				let transformed = transform( splitDeltaA, splitDeltaB );
+
+				baseVersion = splitDeltaB.operations.length;
+
+				expect( transformed.length ).to.equal( 1 );
+
+				expectDelta( transformed[ 0 ], {
+					type: SplitDelta,
+					operations: [
+						{
+							type: InsertOperation,
+							position: new Position( root, [ 3, 3, 4 ] ),
+							baseVersion: baseVersion
+						},
+						{
+							type: MoveOperation,
+							sourcePosition: new Position( root, [ 3, 3, 3, 3 ] ),
+							howMany: 2,
+							targetPosition: new Position( root, [ 3, 3, 4, 0 ] ),
+							baseVersion: baseVersion + 1
+						}
+					]
+				} );
+
+				// Test if deltas do what they should after applying transformed delta.
+
+				applyDelta( splitDeltaB, doc );
+				applyDelta( transformed[ 0 ], doc );
+
+				let nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 0 ] ), 6 ) );
+
+				// P element is correctly split, there are three P elements, letters in P elements are in correct order.
+				expect( nodesAndText ).to.equal( 'XXXXXabcdXPabcPPfoPPobarxyzP' );
+			} );
+
+			it( 'split in same parent, incoming delta splits further', () => {
+				let splitDeltaB = getSplitDelta( new Position( root, [ 3, 3, 3, 1 ] ), new Element( 'p' ), 11, baseVersion );
+				let transformed = transform( splitDeltaA, splitDeltaB );
+
+				baseVersion = splitDeltaB.operations.length;
+
+				expect( transformed.length ).to.equal( 1 );
+
+				expectDelta( transformed[ 0 ], {
+					type: SplitDelta,
+					operations: [
+						{
+							type: InsertOperation,
+							position: new Position( root, [ 3, 3, 5 ] ),
+							baseVersion: baseVersion
+						},
+						{
+							type: MoveOperation,
+							sourcePosition: new Position( root, [ 3, 3, 4, 2 ] ),
+							howMany: 9,
+							targetPosition: new Position( root, [ 3, 3, 5, 0 ] ),
+							baseVersion: baseVersion + 1
+						}
+					]
+				} );
+
+				// Test if deltas do what they should after applying transformed delta.
+
+				applyDelta( splitDeltaB, doc );
+				applyDelta( transformed[ 0 ], doc );
+
+				let nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 0 ] ), 6 ) );
+
+				// P element is correctly split, there are three P elements, letters in P elements are in correct order.
+				expect( nodesAndText ).to.equal( 'XXXXXabcdXPaPPbcPPfoobarxyzP' );
+			} );
+		} );
+	} );
+} );