8
0
Просмотр исходного кода

Changes in OT connected with new position move algorithms (greedy and sticky).

Szymon Cofalik 10 лет назад
Родитель
Сommit
5a4afa20b3

+ 42 - 69
packages/ckeditor5-engine/src/treemodel/operation/transform.js

@@ -9,7 +9,6 @@ import InsertOperation from './insertoperation.js';
 import AttributeOperation from './attributeoperation.js';
 import MoveOperation from './moveoperation.js';
 import NoOperation from './nooperation.js';
-import Position from '../position.js';
 import Range from '../range.js';
 import isEqual from '../../lib/lodash/isEqual.js';
 import utils from '../../utils.js';
@@ -78,7 +77,7 @@ const ot = {
 			const transformed = a.clone();
 
 			// Transform insert position by the other operation parameters.
-			transformed.position = a.position.getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, !isStrong );
+			transformed.position = a.position.getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, !isStrong, true );
 
 			return [ transformed ];
 		}
@@ -88,7 +87,7 @@ const ot = {
 		// Transforms AttributeOperation `a` by InsertOperation `b`. Returns results as an array of operations.
 		InsertOperation( a, b ) {
 			// Transform this operation's range.
-			const ranges = a.range.getTransformedByInsertion( b.position, b.nodeList.length );
+			const ranges = a.range.getTransformedByInsertion( b.position, b.nodeList.length, true );
 
 			// Map transformed range(s) to operations and return them.
 			return ranges.reverse().map( ( range ) => {
@@ -101,19 +100,17 @@ const ot = {
 		AttributeOperation( a, b, isStrong ) {
 			if ( a.key === b.key ) {
 				// If operations attributes are in conflict, check if their ranges intersect and manage them properly.
-				let operations = [];
 
 				// First, we want to apply change to the part of a range that has not been changed by the other operation.
-				operations = operations.concat(
-					a.range.getDifference( b.range ).map( ( range ) => {
-						return new AttributeOperation( range, a.key, a.oldValue, a.newValue, a.baseVersion );
-					} )
-				);
+				let operations = a.range.getDifference( b.range ).map( ( range ) => {
+					return new AttributeOperation( range, a.key, a.oldValue, a.newValue, a.baseVersion );
+				} );
 
+				// Then we take care of the common part of ranges, but only if operations has different `newValue`.
 				if ( isStrong && !isEqual( a.newValue, b.newValue ) ) {
-					// If this operation is more important, we want also want to apply change to the part of the
+					// If this operation is more important, we also want to apply change to the part of the
 					// original range that has already been changed by the other operation. Since that range
-					// got changed we have to update oldAttr.
+					// got changed we also have to update `oldValue`.
 					const common = a.range.getIntersection( b.range );
 
 					if ( common !== null ) {
@@ -145,10 +142,9 @@ const ot = {
 			// This will aggregate transformed ranges.
 			let ranges = [];
 
-			// Difference is a part of changed range that is modified by AttributeOperation but are not affected
+			// Difference is a part of changed range that is modified by AttributeOperation but is not affected
 			// by MoveOperation. This can be zero, one or two ranges (if moved range is inside changed range).
-			// If two ranges were returned it means that rangeB was inside rangeA. We will cover rangeB later.
-			// Right now we will make a simplification and join difference ranges and transform them as one.
+			// Right now we will make a simplification and join difference ranges and transform them as one. We will cover rangeB later.
 			const difference = joinRanges( a.range.getDifference( rangeB ) );
 
 			// Common is a range of nodes that is affected by MoveOperation. So it got moved to other place.
@@ -167,7 +163,7 @@ const ot = {
 				// previously transformed target position.
 				// Note that we do not use Position.getTransformedByMove on range boundaries because we need to
 				// transform by insertion a range as a whole, since newTargetPosition might be inside that range.
-				ranges = difference.getTransformedByInsertion( newTargetPosition, b.howMany, false ).reverse();
+				ranges = difference.getTransformedByInsertion( newTargetPosition, b.howMany, true ).reverse();
 			}
 
 			if ( common !== null ) {
@@ -191,22 +187,18 @@ const ot = {
 		// Transforms MoveOperation `a` by InsertOperation `b`. Accepts a flag stating whether `a` is more important
 		// than `b` when it comes to resolving conflicts. Returns results as an array of operations.
 		InsertOperation( a, b, isStrong ) {
-			// Get target position from the state "after" nodes are inserted by InsertOperation.
-			const newTargetPosition = a.targetPosition.getTransformedByInsertion( b.position, b.nodeList.length, !isStrong );
-
-			// Create range from MoveOperation properties and transform it by insertion as well.
-			const rangeB = Range.createFromPositionAndShift( a.sourcePosition, a.howMany );
-			const ranges = rangeB.getTransformedByInsertion( b.position, b.nodeList.length, true );
+			// Create range from MoveOperation properties and transform it by insertion.
+			let range = Range.createFromPositionAndShift( a.sourcePosition, a.howMany );
+			range = range.getTransformedByInsertion( b.position, b.nodeList.length, false )[ 0 ];
 
-			// Map transformed range(s) to operations and return them.
-			return ranges.reverse().map( ( range ) => {
-				return new MoveOperation(
+			return [
+				new MoveOperation(
 					range.start,
 					range.end.offset - range.start.offset,
-					Position.createFromPosition( newTargetPosition ),
+					a.targetPosition.getTransformedByInsertion( b.position, b.nodeList.length, !isStrong ),
 					a.baseVersion
-				);
-			} );
+				)
+			];
 		},
 
 		AttributeOperation: doNotUpdate,
@@ -227,54 +219,25 @@ const ot = {
 			const rangeA = Range.createFromPositionAndShift( a.sourcePosition, a.howMany );
 			const rangeB = Range.createFromPositionAndShift( b.sourcePosition, b.howMany );
 
-			// Special case when transformed range contains both the other operation's whole range and target.
-			// In such case, operations are not really conflicting and we should leave transformed operation as it is.
-			// Without this we would have 3 or 4 operations and the transformation result would probably be not intuitive.
-			if ( rangeA.containsRange( rangeB ) && rangeA.containsPosition( b.targetPosition ) ) {
-				return [ a.clone() ];
-			}
-			// Mirror situation for the case above - now transformed range is wholly contained in the other
-			// operation's range and also targets to that range. Without this special treatment we would
-			// transform this operation into NoOperation, but this would not be compatible with the result
-			// generated by the special case above.
-			else if ( rangeB.containsRange( rangeA ) && rangeB.containsPosition( a.targetPosition ) ) {
-				return [
-					new MoveOperation(
-						a.sourcePosition._getCombined( b.sourcePosition, b.targetPosition ),
-						a.howMany,
-						a.targetPosition._getCombined( b.sourcePosition, b.targetPosition ),
-						a.baseVersion
-					)
-				];
-			}
-
-			// All the other non-special cases are treated by generic algorithm below.
-
-			const differenceSet = rangeA.getDifference( rangeB );
-			const common = rangeA.getIntersection( rangeB );
+			// Get target position from the state "after" nodes specified by other MoveOperation are "detached".
+			const moveTargetPosition = b.targetPosition.getTransformedByDeletion( b.sourcePosition, b.howMany );
 
 			// This will aggregate transformed ranges.
 			let ranges = [];
 
-			// Get target position from the state "after" nodes specified by other MoveOperation are "detached".
-			const moveTargetPosition = b.targetPosition.getTransformedByDeletion( b.sourcePosition, b.howMany );
+			// All the other non-special cases are treated by generic algorithm below.
+			let difference = joinRanges( rangeA.getDifference( rangeB ) );
 
-			// First, we take care of that part of the range that is only modified by transformed operation.
-			for ( let i = 0; i < differenceSet.length; i++ ) {
-				// MoveOperation removes nodes from their original position. We acknowledge this by proper transformation.
-				// Take the start and the end of the range and transform them by deletion of moved nodes.
-				differenceSet[ i ].start = differenceSet[ i ].start.getTransformedByDeletion( b.sourcePosition, b.howMany );
-				differenceSet[ i ].end = differenceSet[ i ].end.getTransformedByDeletion( b.sourcePosition, b.howMany );
+			if ( difference ) {
+				difference.start = difference.start.getTransformedByMove( b.sourcePosition, moveTargetPosition, b.howMany, true );
+				difference.end = difference.end.getTransformedByMove( b.sourcePosition, moveTargetPosition, b.howMany, false );
 
-				// MoveOperation pastes nodes into target position. We acknowledge this by proper transformation.
-				// Note that since we operate on transformed difference range, we should transform by
-				// previously transformed target position.
-				// Note that we do not use Position.getTransformedByMove on range boundaries because we need to
-				// transform by insertion a range as a whole, since newTargetPosition might be inside that range.
-				ranges = ranges.concat( differenceSet[ i ].getTransformedByInsertion( moveTargetPosition, b.howMany, true ) );
+				ranges.push( difference );
 			}
 
 			// Then, we have to manage the common part of both move ranges.
+			const common = rangeA.getIntersection( rangeB );
+
 			// If MoveOperations has common range it can be one of two:
 			// * on the same tree level - it means that we move the same nodes into different places
 			// * on deeper tree level - it means that we move nodes that are inside moved nodes
@@ -282,7 +245,17 @@ const ot = {
 			// So, we will handle common range if it is "deeper" or if transformed operation is more important.
 			let isDeeper = utils.compareArrays( b.sourcePosition.getParentPath(), a.sourcePosition.getParentPath() ) == 'PREFIX';
 
-			if ( common !== null && ( isDeeper || isStrong ) ) {
+			// If the `b` MoveOperation points inside the `a` MoveOperation range, the common part will be included in
+			// range(s) that (is) are results of processing `differenceSet`. If that's the case, we cannot include it again.
+			let bIsIncluded = rangeA.containsPosition( b.targetPosition );
+
+			// If the `b` MoveOperation range contains both whole `a` range and target position we do an exception and
+			// transform `a` operation. Normally, when same nodes are moved, we stick with stronger operation's target.
+			// Here it is a move inside larger range so there is no conflict because after all, all nodes from
+			// smaller range will be moved to larger range target. The effect of this transformation feels natural.
+			let aIsInside = rangeB.containsRange( rangeA ) && rangeB.containsPosition( a.targetPosition );
+
+			if ( common !== null && ( isDeeper || isStrong || aIsInside ) && !bIsIncluded ) {
 				// Here we do not need to worry that newTargetPosition is inside moved range, because that
 				// would mean that the MoveOperation targets into itself, and that is incorrect operation.
 				// Instead, we calculate the new position of that part of original range.
@@ -290,7 +263,7 @@ const ot = {
 				common.end = common.end._getCombined( b.sourcePosition, moveTargetPosition );
 
 				// We have to take care of proper range order.
-				// Note that both push, splice and unshift do the same if there are no ranges in the array.
+				// Note that push, splice and unshift do the same if there are no ranges in the modified array.
 				if ( rangeB.end.isAfter( rangeA.end ) ) {
 					ranges.push( common );
 				} else if ( rangeB.start.isBefore( rangeA.start ) ) {
@@ -307,7 +280,7 @@ const ot = {
 			}
 
 			// Target position also could be affected by the other MoveOperation. We will transform it.
-			let newTargetPosition = a.targetPosition.getTransformedByMove( b.sourcePosition, moveTargetPosition, b.howMany, !isStrong );
+			let newTargetPosition = a.targetPosition.getTransformedByMove( b.sourcePosition, moveTargetPosition, b.howMany, !isStrong, true );
 
 			// Map transformed range(s) to operations and return them.
 			return ranges.reverse().map( ( range ) => {

+ 86 - 99
packages/ckeditor5-engine/tests/treemodel/operation/transform.js

@@ -57,7 +57,7 @@ describe( 'transform', () => {
 			nodeC = new Node();
 			nodeD = new Node();
 
-			position = new Position( root, [ 0, 2, 1 ] );
+			position = new Position( root, [ 0, 2, 2 ] );
 
 			op = new InsertOperation( position, [ nodeA, nodeB ], baseVersion );
 
@@ -98,7 +98,7 @@ describe( 'transform', () => {
 
 			it( 'target at same offset and is important: increment offset', () => {
 				let transformBy = new InsertOperation(
-					new Position( root, [ 0, 2, 1 ] ),
+					new Position( root, [ 0, 2, 2 ] ),
 					[ nodeC, nodeD ],
 					baseVersion
 				);
@@ -112,7 +112,7 @@ describe( 'transform', () => {
 
 			it( 'target at same offset and is less important: no position update', () => {
 				let transformBy = new InsertOperation(
-					new Position( root, [ 0, 2, 1 ] ),
+					new Position( root, [ 0, 2, 2 ] ),
 					[ nodeC, nodeD ],
 					baseVersion
 				);
@@ -125,7 +125,7 @@ describe( 'transform', () => {
 
 			it( 'target at offset after: no position update', () => {
 				let transformBy = new InsertOperation(
-					new Position( root, [ 0, 2, 2 ] ),
+					new Position( root, [ 0, 2, 3 ] ),
 					[ nodeC, nodeD ],
 					baseVersion
 				);
@@ -196,7 +196,7 @@ describe( 'transform', () => {
 				expectOperation( transOp[ 0 ], expected );
 			} );
 
-			it( 'range offset is before insert position offset: decrement offset', () => {
+			it( 'range is before insert position offset: decrement offset', () => {
 				let transformBy = new MoveOperation(
 					new Position( root, [ 0, 2, 0 ] ),
 					1,
@@ -213,7 +213,7 @@ describe( 'transform', () => {
 
 			it( 'range offset is after insert position offset: no position update', () => {
 				let transformBy = new MoveOperation(
-					new Position( root, [ 0, 2, 4 ] ),
+					new Position( root, [ 0, 2, 5 ] ),
 					1,
 					new Position( root, [ 1, 1 ] ),
 					baseVersion
@@ -244,7 +244,7 @@ describe( 'transform', () => {
 				let transformBy = new MoveOperation(
 					new Position( root, [ 1, 1 ] ),
 					2,
-					new Position( root, [ 0, 2, 4 ] ),
+					new Position( root, [ 0, 2, 5 ] ),
 					baseVersion
 				);
 
@@ -258,7 +258,7 @@ describe( 'transform', () => {
 				let transformBy = new MoveOperation(
 					new Position( root, [ 1, 1 ] ),
 					2,
-					new Position( root, [ 0, 2, 1 ] ),
+					new Position( root, [ 0, 2, 2 ] ),
 					baseVersion
 				);
 
@@ -273,7 +273,7 @@ describe( 'transform', () => {
 				let transformBy = new MoveOperation(
 					new Position( root, [ 1, 1 ] ),
 					2,
-					new Position( root, [ 0, 2, 1 ] ),
+					new Position( root, [ 0, 2, 2 ] ),
 					baseVersion
 				);
 
@@ -286,13 +286,13 @@ describe( 'transform', () => {
 			it( 'range is before node from insert position path: decrement index on path', () => {
 				let transformBy = new MoveOperation(
 					new Position( root, [ 0, 0 ] ),
-					2,
+					1,
 					new Position( root, [ 1, 0 ] ),
 					baseVersion
 				);
 
 				let transOp = transform( op, transformBy );
-				expected.position.path[ 1 ] -= 2;
+				expected.position.path[ 1 ] -= 1;
 
 				expect( transOp.length ).to.equal( 1 );
 				expectOperation( transOp[ 0 ], expected );
@@ -350,22 +350,22 @@ describe( 'transform', () => {
 				);
 
 				let transOp = transform( op, transformBy );
-				expected.position.path = [ 1, 2, 1 ];
+				expected.position.path = [ 1, 2, 2 ];
 
 				expect( transOp.length ).to.equal( 1 );
 				expectOperation( transOp[ 0 ], expected );
 			} );
 
-			it( 'range contains insert position (on same level): set position offset to range start', () => {
+			it( 'range contains insert position (on same level): update position', () => {
 				let transformBy = new MoveOperation(
 					new Position( root, [ 0, 2, 0 ] ),
-					3,
+					4,
 					new Position( root, [ 1, 0 ] ),
 					baseVersion
 				);
 
 				let transOp = transform( op, transformBy );
-				expected.position.offset = 0;
+				expected.position.path = [ 1, 2 ];
 
 				expect( transOp.length ).to.equal( 1 );
 				expectOperation( transOp[ 0 ], expected );
@@ -1335,7 +1335,7 @@ describe( 'transform', () => {
 
 			it( 'target inside node from move range: no operation update', () => {
 				let transformBy = new InsertOperation(
-					new Position( root, [ 2, 2, 3, 1 ] ),
+					new Position( root, [ 2, 2, 4, 1 ] ),
 					[ nodeA, nodeB ],
 					baseVersion
 				);
@@ -1486,7 +1486,7 @@ describe( 'transform', () => {
 				expectOperation( transOp[ 0 ], expected );
 			} );
 
-			it( 'target offset same as move target offset and is important: increment target offset', () => {
+			it( 'target offset same as move target offset: increment target offset', () => {
 				let transformBy = new InsertOperation(
 					new Position( root, [ 3, 3 ] ),
 					[ nodeA, nodeB ],
@@ -1501,7 +1501,7 @@ describe( 'transform', () => {
 				expectOperation( transOp[ 0 ], expected );
 			} );
 
-			it( 'target inside move range: split into two operations', () => {
+			it( 'target inside move range: expand range', () => {
 				let transformBy = new InsertOperation(
 					new Position( root, [ 2, 2, 5 ] ),
 					[ nodeA, nodeB ],
@@ -1510,18 +1510,11 @@ describe( 'transform', () => {
 
 				let transOp = transform( op, transformBy );
 
-				expect( transOp.length ).to.equal( 2 );
+				expect( transOp.length ).to.equal( 1 );
 
-				expected.sourcePosition.path = [ 2, 2, 7 ];
-				expected.howMany = 1;
+				expected.howMany = 4;
 
 				expectOperation( transOp[ 0 ], expected );
-
-				expected.sourcePosition.path = [ 2, 2, 4 ];
-				expected.howMany = 1;
-				expected.baseVersion++;
-
-				expectOperation( transOp[ 1 ], expected );
 			} );
 
 			it( 'target at offset same as range end boundary: expand range', () => {
@@ -1665,14 +1658,14 @@ describe( 'transform', () => {
 			it( 'range before node from transforming range start path: decrement index on range start path', () => {
 				let transformBy = new MoveOperation(
 					new Position( root, [ 2, 0 ] ),
-					2,
+					1,
 					new Position( root, [ 4, 1, 0 ] ),
 					baseVersion
 				);
 
 				let transOp = transform( op, transformBy );
 
-				expected.sourcePosition.path[ 1 ] -= 2;
+				expected.sourcePosition.path[ 1 ] -= 1;
 
 				expect( transOp.length ).to.equal( 1 );
 				expectOperation( transOp[ 0 ], expected );
@@ -1812,7 +1805,7 @@ describe( 'transform', () => {
 				expectOperation( transOp[ 0 ], expected );
 			} );
 
-			it( 'target inside transforming move range: split into two operations', () => {
+			it( 'target inside transforming move range: expand move range', () => {
 				let transformBy = new MoveOperation(
 					new Position( root, [ 4, 1, 0 ] ),
 					2,
@@ -1822,17 +1815,11 @@ describe( 'transform', () => {
 
 				let transOp = transform( op, transformBy );
 
-				expect( transOp.length ).to.equal( 2 );
+				expect( transOp.length ).to.equal( 1 );
 
-				expected.howMany = 1;
-				expected.sourcePosition.offset = 7;
+				expected.howMany = 4;
 
 				expectOperation( transOp[ 0 ], expected );
-
-				expected.sourcePosition.offset = 4;
-				expected.baseVersion++;
-
-				expectOperation( transOp[ 1 ], expected );
 			} );
 
 			it( 'target inside a node from transforming range: no operation update', () => {
@@ -2088,7 +2075,7 @@ describe( 'transform', () => {
 				expectOperation( transOp[ 1 ], expected );
 			} );
 
-			it( 'range intersects on left side, target inside transforming range and is important: split into two operations', () => {
+			it( 'range intersects on left side, target inside transforming range and is important: expand move range', () => {
 				op.howMany = 4;
 
 				let transformBy = new MoveOperation(
@@ -2100,21 +2087,15 @@ describe( 'transform', () => {
 
 				let transOp = transform( op, transformBy );
 
-				expect( transOp.length ).to.equal( 2 );
-
-				expected.sourcePosition.path = [ 2, 2, 6 ];
-				expected.howMany = 2;
-
-				expectOperation( transOp[ 0 ], expected );
+				expect( transOp.length ).to.equal( 1 );
 
 				expected.sourcePosition.path = [ 2, 2, 3 ];
-				expected.howMany = 1;
-				expected.baseVersion++;
+				expected.howMany = 5;
 
-				expectOperation( transOp[ 1 ], expected );
+				expectOperation( transOp[ 0 ], expected );
 			} );
 
-			it( 'range intersects on left side, target inside transforming range and is less important: split into three operations', () => {
+			it( 'range intersects on left side, target inside transforming range and is less important: expand move range', () => {
 				op.howMany = 4;
 
 				let transformBy = new MoveOperation(
@@ -2126,85 +2107,103 @@ describe( 'transform', () => {
 
 				let transOp = transform( op, transformBy, true );
 
-				expect( transOp.length ).to.equal( 3 );
+				expect( transOp.length ).to.equal( 1 );
 
-				expected.sourcePosition.path = [ 2, 2, 6 ];
-				expected.howMany = 2;
+				expected.sourcePosition.path = [ 2, 2, 3 ];
+				expected.howMany = 5;
 
 				expectOperation( transOp[ 0 ], expected );
+			} );
 
-				expected.sourcePosition.path = [ 2, 2, 3 ];
-				expected.howMany = 1;
-				expected.baseVersion++;
+			it( 'range intersects on right side of transforming range and is important: shrink range', () => {
+				let transformBy = new MoveOperation(
+					new Position( root, [ 2, 2, 5 ] ),
+					2,
+					new Position( root, [ 4, 1, 0 ] ),
+					baseVersion
+				);
 
-				expectOperation( transOp[ 1 ], expected );
+				let transOp = transform( op, transformBy );
 
-				expected.sourcePosition.path = [ 2, 2, 5 ];
 				expected.howMany = 1;
-				expected.baseVersion++;
 
-				expectOperation( transOp[ 2 ], expected );
+				expect( transOp.length ).to.equal( 1 );
+				expectOperation( transOp[ 0 ], expected );
 			} );
 
-			it( 'range intersects on right side, target inside transforming range and is important: split into two operations', () => {
-				op.howMany = 4;
-
+			it( 'range intersects on right side of transforming range and is less important: split into two operations', () => {
 				let transformBy = new MoveOperation(
-					new Position( root, [ 2, 2, 7 ] ),
-					2,
 					new Position( root, [ 2, 2, 5 ] ),
+					2,
+					new Position( root, [ 4, 1, 0 ] ),
 					baseVersion
 				);
 
-				let transOp = transform( op, transformBy );
+				let transOp = transform( op, transformBy, true );
 
 				expect( transOp.length ).to.equal( 2 );
 
-				expected.sourcePosition.path = [ 2, 2, 7 ];
-				expected.howMany = 2;
+				expected.sourcePosition.path = [ 4, 1, 0 ];
+				expected.howMany = 1;
 
 				expectOperation( transOp[ 0 ], expected );
 
-				expected.sourcePosition.path = [ 2, 2, 4 ];
-				expected.howMany = 1;
+				expected.sourcePosition.path = op.sourcePosition.path;
 				expected.baseVersion++;
 
 				expectOperation( transOp[ 1 ], expected );
 			} );
 
-			it( 'range intersects on right side, target inside transforming range and is less important: split into three operations', () => {
-				op.howMany = 4;
+			it( 'range intersects, target inside transforming range and is important: split into two operations', () => {
+				op.targetPosition.path = [ 2, 2, 7 ];
 
 				let transformBy = new MoveOperation(
-					new Position( root, [ 2, 2, 7 ] ),
-					2,
 					new Position( root, [ 2, 2, 5 ] ),
+					4,
+					new Position( root, [ 4, 1, 0 ] ),
 					baseVersion
 				);
 
 				let transOp = transform( op, transformBy, true );
 
-				expect( transOp.length ).to.equal( 3 );
+				expect( transOp.length ).to.equal( 2 );
 
-				expected.sourcePosition.path = [ 2, 2, 5 ];
+				expected.sourcePosition.path = [ 4, 1, 0 ];
+				expected.targetPosition.path = [ 4, 1, 2 ];
 				expected.howMany = 1;
 
 				expectOperation( transOp[ 0 ], expected );
 
-				expected.sourcePosition.path = [ 2, 2, 7 ];
-				expected.howMany = 2;
+				expected.sourcePosition.path = [ 2, 2, 4 ];
+				expected.targetPosition.path = [ 4, 1, 2 ];
+				expected.howMany = 1;
 				expected.baseVersion++;
 
 				expectOperation( transOp[ 1 ], expected );
+			} );
+
+			it( 'range intersects, target inside transforming range and is less important: shrink range', () => {
+				op.targetPosition.path = [ 2, 2, 7 ];
+
+				let transformBy = new MoveOperation(
+					new Position( root, [ 2, 2, 5 ] ),
+					4,
+					new Position( root, [ 4, 1, 0 ] ),
+					baseVersion
+				);
+
+				let transOp = transform( op, transformBy );
+
+				expect( transOp.length ).to.equal( 1 );
 
 				expected.sourcePosition.path = [ 2, 2, 4 ];
+				expected.targetPosition.path = [ 4, 1, 2 ];
 				expected.howMany = 1;
-				expected.baseVersion++;
 
-				expectOperation( transOp[ 2 ], expected );
+				expectOperation( transOp[ 0 ], expected );
 			} );
 
-			it( 'range inside transforming range and is important: split into two operations', () => {
+			it( 'range inside transforming range and is important: shrink range', () => {
 				op.howMany = 4;
 
 				let transformBy = new MoveOperation(
@@ -2216,20 +2215,14 @@ describe( 'transform', () => {
 
 				let transOp = transform( op, transformBy );
 
-				expect( transOp.length ).to.equal( 2 );
+				expect( transOp.length ).to.equal( 1 );
 
-				expected.sourcePosition.path = [ 2, 2, 5 ];
-				expected.howMany = 1;
+				expected.howMany = 2;
 
 				expectOperation( transOp[ 0 ], expected );
-
-				expected.sourcePosition.path = [ 2, 2, 4 ];
-				expected.baseVersion++;
-
-				expectOperation( transOp[ 1 ], expected );
 			} );
 
-			it( 'range inside transforming range and is less important: split into three operations', () => {
+			it( 'range inside transforming range and is less important: split into two operations', () => {
 				op.howMany = 4;
 
 				let transformBy = new MoveOperation(
@@ -2241,24 +2234,18 @@ describe( 'transform', () => {
 
 				let transOp = transform( op, transformBy, true );
 
-				expect( transOp.length ).to.equal( 3 );
-
-				expected.sourcePosition.path = [ 2, 2, 5 ];
-				expected.howMany = 1;
-
-				expectOperation( transOp[ 0 ], expected );
+				expect( transOp.length ).to.equal( 2 );
 
 				expected.sourcePosition.path = [ 4, 1, 0 ];
 				expected.howMany = 2;
-				expected.baseVersion++;
 
-				expectOperation( transOp[ 1 ], expected );
+				expectOperation( transOp[ 0 ], expected );
 
 				expected.sourcePosition.path = [ 2, 2, 4 ];
-				expected.howMany = 1;
+				expected.howMany = 2;
 				expected.baseVersion++;
 
-				expectOperation( transOp[ 2 ], expected );
+				expectOperation( transOp[ 1 ], expected );
 			} );
 
 			it( 'range and target inside transforming range and is important: no operation update', () => {