Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Commit

Permalink
Merge pull request #1087 from ckeditor/t/1084
Browse files Browse the repository at this point in the history
Fix: Fixed a bug when `SplitDelta` transformation might cause undo to throw an error in some cases. Closes #1084.
  • Loading branch information
Reinmar authored Aug 21, 2017
2 parents af34f31 + e348fa4 commit cb9d409
Show file tree
Hide file tree
Showing 8 changed files with 395 additions and 9 deletions.
5 changes: 4 additions & 1 deletion src/dev-utils/enableenginedebug.js
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,10 @@ function enableLoggingTools() {

SplitDelta.prototype.toString = function() {
return getClassName( this ) + `( ${ this.baseVersion } ): ` +
this.position.toString();
( this.position ?
this.position.toString() :
`(clone to ${ this._cloneOperation.position || this._cloneOperation.targetPosition })`
);
};

UnwrapDelta.prototype.toString = function() {
Expand Down
64 changes: 56 additions & 8 deletions src/model/delta/basic-transformations.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ addTransformationCase( AttributeDelta, WeakInsertDelta, ( a, b, context ) => {

// Add special case for AttributeDelta x SplitDelta transformation.
addTransformationCase( AttributeDelta, SplitDelta, ( a, b, context ) => {
// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
if ( !b.position ) {
return defaultTransform( a, b, context );
}

const splitPosition = new Position( b.position.root, b.position.path.slice( 0, -1 ) );

const deltas = defaultTransform( a, b, context );
Expand Down Expand Up @@ -173,6 +178,11 @@ addTransformationCase( MergeDelta, MoveDelta, ( a, b, context ) => {

// Add special case for SplitDelta x SplitDelta transformation.
addTransformationCase( SplitDelta, SplitDelta, ( a, b, context ) => {
// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
if ( !a.position || !b.position ) {
return defaultTransform( a, b, context );
}

const pathA = a.position.getParentPath();
const pathB = b.position.getParentPath();

Expand Down Expand Up @@ -202,6 +212,11 @@ addTransformationCase( SplitDelta, SplitDelta, ( a, b, context ) => {

// Add special case for SplitDelta x UnwrapDelta transformation.
addTransformationCase( SplitDelta, UnwrapDelta, ( a, b, context ) => {
// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
if ( !a.position ) {
return defaultTransform( a, b, context );
}

// If incoming split delta tries to split a node that just got unwrapped, there is actually nothing to split,
// so we discard that delta.
if ( a.position.root == b.position.root && compareArrays( b.position.path, a.position.getParentPath() ) === 'same' ) {
Expand All @@ -213,6 +228,11 @@ addTransformationCase( SplitDelta, UnwrapDelta, ( a, b, context ) => {

// Add special case for SplitDelta x WrapDelta transformation.
addTransformationCase( SplitDelta, WrapDelta, ( a, b, context ) => {
// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
if ( !a.position ) {
return defaultTransform( a, b, context );
}

// If split is applied at the position between wrapped nodes, we cancel the split as it's results may be unexpected and
// very weird. Even if we do some "magic" we don't know what really are users' expectations.

Expand Down Expand Up @@ -264,7 +284,12 @@ addTransformationCase( SplitDelta, WrapDelta, ( a, b, context ) => {
} );

// Add special case for SplitDelta x WrapDelta transformation.
addTransformationCase( SplitDelta, AttributeDelta, ( a, b ) => {
addTransformationCase( SplitDelta, AttributeDelta, ( a, b, context ) => {
// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
if ( !a.position ) {
return defaultTransform( a, b, context );
}

a = a.clone();

const splitPosition = new Position( a.position.root, a.position.path.slice( 0, -1 ) );
Expand All @@ -290,6 +315,11 @@ addTransformationCase( SplitDelta, AttributeDelta, ( a, b ) => {

// Add special case for UnwrapDelta x SplitDelta transformation.
addTransformationCase( UnwrapDelta, SplitDelta, ( a, b, context ) => {
// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
if ( !b.position ) {
return defaultTransform( a, b, context );
}

// If incoming unwrap delta tries to unwrap node that got split we should unwrap the original node and the split copy.
// This can be achieved either by reverting split and applying unwrap to singular node, or creating additional unwrap delta.
if ( a.position.root == b.position.root && compareArrays( a.position.path, b.position.getParentPath() ) === 'same' ) {
Expand All @@ -316,9 +346,13 @@ addTransformationCase( WeakInsertDelta, AttributeDelta, ( a, b ) => {

// Add special case for WrapDelta x SplitDelta transformation.
addTransformationCase( WrapDelta, SplitDelta, ( a, b, context ) => {
// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
if ( !b.position ) {
return defaultTransform( a, b, context );
}

// If incoming wrap delta tries to wrap range that contains split position, we have to cancel the split and apply
// the wrap. Since split was already applied, we have to revert it.

const sameRoot = a.range.start.root == b.position.root;
const operateInSameParent = sameRoot && compareArrays( a.range.start.getParentPath(), b.position.getParentPath() ) === 'same';
const splitInsideWrapRange = a.range.start.offset < b.position.offset && a.range.end.offset >= b.position.offset;
Expand Down Expand Up @@ -349,15 +383,22 @@ addTransformationCase( WrapDelta, SplitDelta, ( a, b, context ) => {
// Add special case for RenameDelta x SplitDelta transformation.
addTransformationCase( RenameDelta, SplitDelta, ( a, b, context ) => {
const undoMode = context.aWasUndone || context.bWasUndone;
const posBeforeSplitParent = new Position( b.position.root, b.position.path.slice( 0, -1 ) );

// The "clone operation" may be `InsertOperation`, `ReinsertOperation`, `MoveOperation` or `NoOperation`.
// `MoveOperation` has `targetPosition` which we want to use. `NoOperation` has no `position` and we don't use special case then.
let insertPosition = b._cloneOperation.position || b._cloneOperation.targetPosition;

if ( insertPosition ) {
insertPosition = insertPosition.getShiftedBy( -1 );
}

const deltas = defaultTransform( a, b, context );

if ( !undoMode && a.operations[ 0 ].position.isEqual( posBeforeSplitParent ) ) {
if ( insertPosition && !undoMode && a.operations[ 0 ].position.isEqual( insertPosition ) ) {
// If a node that has been split has it's name changed, we should also change name of
// the node created during splitting.
const additionalRenameDelta = a.clone();
additionalRenameDelta.operations[ 0 ].position = posBeforeSplitParent.getShiftedBy( 1 );
additionalRenameDelta.operations[ 0 ].position = insertPosition.getShiftedBy( 1 );

deltas.push( additionalRenameDelta );
}
Expand All @@ -368,12 +409,19 @@ addTransformationCase( RenameDelta, SplitDelta, ( a, b, context ) => {
// Add special case for SplitDelta x RenameDelta transformation.
addTransformationCase( SplitDelta, RenameDelta, ( a, b, context ) => {
const undoMode = context.aWasUndone || context.bWasUndone;
const posBeforeSplitParent = new Position( a.position.root, a.position.path.slice( 0, -1 ) );

// The "clone operation" may be `InsertOperation`, `ReinsertOperation`, `MoveOperation` or `NoOperation`.
// `MoveOperation` has `targetPosition` which we want to use. `NoOperation` has no `position` and we don't use special case then.
let insertPosition = a._cloneOperation.position || a._cloneOperation.targetPosition;

if ( insertPosition ) {
insertPosition = insertPosition.getShiftedBy( -1 );
}

// If element to split had it's name changed, we have to reflect this by creating additional rename operation.
if ( !undoMode && b.operations[ 0 ].position.isEqual( posBeforeSplitParent ) ) {
if ( insertPosition && !undoMode && b.operations[ 0 ].position.isEqual( insertPosition ) ) {
const additionalRenameDelta = b.clone();
additionalRenameDelta.operations[ 0 ].position = posBeforeSplitParent.getShiftedBy( 1 );
additionalRenameDelta.operations[ 0 ].position = insertPosition.getShiftedBy( 1 );

// `nodes` is a property that is available only if `SplitDelta` `a` has `InsertOperation`.
// `SplitDelta` may have `ReinsertOperation` instead of `InsertOperation`.
Expand Down
35 changes: 35 additions & 0 deletions tests/dev-utils/enableenginedebug.js
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,41 @@ describe( 'debug tools', () => {
expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
} );

it( 'SplitDelta - NoOperation as second operation', () => {
const otherRoot = modelDoc.createRoot( 'main', 'otherRoot' );
const splitEle = new ModelElement( 'paragraph', null, [ new ModelText( 'foo' ) ] );

otherRoot.appendChildren( [ splitEle ] );

const delta = new SplitDelta();
const insert = new InsertOperation( ModelPosition.createAt( otherRoot, 1 ), [ new ModelElement( 'paragraph' ) ], 0 );
const move = new NoOperation( 1 );

delta.addOperation( insert );
delta.addOperation( move );

expect( delta.toString() ).to.equal( 'SplitDelta( 0 ): (clone to otherRoot [ 1 ])' );

delta.log();
expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
} );

it( 'SplitDelta - NoOperation as second operation, MoveOperation as first operation', () => {
const otherRoot = modelDoc.createRoot( 'main', 'otherRoot' );

const delta = new SplitDelta();
const insert = new MoveOperation( ModelPosition.createAt( modelRoot, 1 ), 1, ModelPosition.createAt( otherRoot, 1 ), 0 );
const move = new NoOperation( 1 );

delta.addOperation( insert );
delta.addOperation( move );

expect( delta.toString() ).to.equal( 'SplitDelta( 0 ): (clone to otherRoot [ 1 ])' );

delta.log();
expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
} );

it( 'UnwrapDelta', () => {
const otherRoot = modelDoc.createRoot( 'main', 'otherRoot' );
const unwrapEle = new ModelElement( 'paragraph', null, [ new ModelText( 'foo' ) ] );
Expand Down
27 changes: 27 additions & 0 deletions tests/model/delta/transform/attributedelta.js
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,33 @@ describe( 'transform', () => {
]
} );
} );

it( 'should use default algorithm and not throw if split delta has NoOperation', () => {
const range = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2, 3 ] ) );
const attrDelta = getAttributeDelta( range, 'foo', null, 'bar', 0 );
const splitDelta = getSplitDelta( new Position( root, [ 0, 2 ] ), new Element( 'paragraph' ), 3, 0 );
splitDelta.operations[ 1 ] = new NoOperation( 1 );

const transformed = transform( attrDelta, splitDelta, context );

baseVersion = splitDelta.operations.length;

expect( transformed.length ).to.equal( 1 );

expectDelta( transformed[ 0 ], {
type: AttributeDelta,
operations: [
{
type: AttributeOperation,
range: new Range( new Position( root, [ 2 ] ), new Position( root, [ 3, 3 ] ) ),
key: 'foo',
oldValue: null,
newValue: 'bar',
baseVersion
}
]
} );
} );
} );

describe( 'AttributeDelta', () => {
Expand Down
33 changes: 33 additions & 0 deletions tests/model/delta/transform/renamedelta.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import Element from '../../../../src/model/element';
import Position from '../../../../src/model/position';

import RenameDelta from '../../../../src/model/delta/renamedelta';
import SplitDelta from '../../../../src/model/delta/splitdelta';
import Delta from '../../../../src/model/delta/delta';
import RenameOperation from '../../../../src/model/operation/renameoperation';
import MoveOperation from '../../../../src/model/operation/moveoperation';
import NoOperation from '../../../../src/model/operation/nooperation';

import {
Expand Down Expand Up @@ -106,6 +108,37 @@ describe( 'transform', () => {
]
} );
} );

it( 'should not throw if clone operation is NoOperation and use default transformation in that case', () => {
const noOpSplitDelta = new SplitDelta();
noOpSplitDelta.addOperation( new NoOperation( 0 ) );
noOpSplitDelta.addOperation( new MoveOperation( new Position( root, [ 1, 2 ] ), 3, new Position( root, [ 2, 0 ] ), 1 ) );

const renameDelta = new RenameDelta();
renameDelta.addOperation( new RenameOperation(
new Position( root, [ 1 ] ),
'p',
'li',
baseVersion
) );

const transformed = transform( renameDelta, noOpSplitDelta, context );

expect( transformed.length ).to.equal( 1 );

expectDelta( transformed[ 0 ], {
type: RenameDelta,
operations: [
{
type: RenameOperation,
position: new Position( root, [ 1 ] ),
oldName: 'p',
newName: 'li',
baseVersion: 2
}
]
} );
} );
} );

describe( 'RenameDelta', () => {
Expand Down
Loading

0 comments on commit cb9d409

Please sign in to comment.