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

Commit cb9d409

Browse files
authored
Merge pull request #1087 from ckeditor/t/1084
Fix: Fixed a bug when `SplitDelta` transformation might cause undo to throw an error in some cases. Closes #1084.
2 parents af34f31 + e348fa4 commit cb9d409

File tree

8 files changed

+395
-9
lines changed

8 files changed

+395
-9
lines changed

src/dev-utils/enableenginedebug.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,10 @@ function enableLoggingTools() {
414414

415415
SplitDelta.prototype.toString = function() {
416416
return getClassName( this ) + `( ${ this.baseVersion } ): ` +
417-
this.position.toString();
417+
( this.position ?
418+
this.position.toString() :
419+
`(clone to ${ this._cloneOperation.position || this._cloneOperation.targetPosition })`
420+
);
418421
};
419422

420423
UnwrapDelta.prototype.toString = function() {

src/model/delta/basic-transformations.js

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ addTransformationCase( AttributeDelta, WeakInsertDelta, ( a, b, context ) => {
5050

5151
// Add special case for AttributeDelta x SplitDelta transformation.
5252
addTransformationCase( AttributeDelta, SplitDelta, ( a, b, context ) => {
53+
// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
54+
if ( !b.position ) {
55+
return defaultTransform( a, b, context );
56+
}
57+
5358
const splitPosition = new Position( b.position.root, b.position.path.slice( 0, -1 ) );
5459

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

174179
// Add special case for SplitDelta x SplitDelta transformation.
175180
addTransformationCase( SplitDelta, SplitDelta, ( a, b, context ) => {
181+
// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
182+
if ( !a.position || !b.position ) {
183+
return defaultTransform( a, b, context );
184+
}
185+
176186
const pathA = a.position.getParentPath();
177187
const pathB = b.position.getParentPath();
178188

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

203213
// Add special case for SplitDelta x UnwrapDelta transformation.
204214
addTransformationCase( SplitDelta, UnwrapDelta, ( a, b, context ) => {
215+
// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
216+
if ( !a.position ) {
217+
return defaultTransform( a, b, context );
218+
}
219+
205220
// If incoming split delta tries to split a node that just got unwrapped, there is actually nothing to split,
206221
// so we discard that delta.
207222
if ( a.position.root == b.position.root && compareArrays( b.position.path, a.position.getParentPath() ) === 'same' ) {
@@ -213,6 +228,11 @@ addTransformationCase( SplitDelta, UnwrapDelta, ( a, b, context ) => {
213228

214229
// Add special case for SplitDelta x WrapDelta transformation.
215230
addTransformationCase( SplitDelta, WrapDelta, ( a, b, context ) => {
231+
// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
232+
if ( !a.position ) {
233+
return defaultTransform( a, b, context );
234+
}
235+
216236
// If split is applied at the position between wrapped nodes, we cancel the split as it's results may be unexpected and
217237
// very weird. Even if we do some "magic" we don't know what really are users' expectations.
218238

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

266286
// Add special case for SplitDelta x WrapDelta transformation.
267-
addTransformationCase( SplitDelta, AttributeDelta, ( a, b ) => {
287+
addTransformationCase( SplitDelta, AttributeDelta, ( a, b, context ) => {
288+
// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
289+
if ( !a.position ) {
290+
return defaultTransform( a, b, context );
291+
}
292+
268293
a = a.clone();
269294

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

291316
// Add special case for UnwrapDelta x SplitDelta transformation.
292317
addTransformationCase( UnwrapDelta, SplitDelta, ( a, b, context ) => {
318+
// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
319+
if ( !b.position ) {
320+
return defaultTransform( a, b, context );
321+
}
322+
293323
// If incoming unwrap delta tries to unwrap node that got split we should unwrap the original node and the split copy.
294324
// This can be achieved either by reverting split and applying unwrap to singular node, or creating additional unwrap delta.
295325
if ( a.position.root == b.position.root && compareArrays( a.position.path, b.position.getParentPath() ) === 'same' ) {
@@ -316,9 +346,13 @@ addTransformationCase( WeakInsertDelta, AttributeDelta, ( a, b ) => {
316346

317347
// Add special case for WrapDelta x SplitDelta transformation.
318348
addTransformationCase( WrapDelta, SplitDelta, ( a, b, context ) => {
349+
// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
350+
if ( !b.position ) {
351+
return defaultTransform( a, b, context );
352+
}
353+
319354
// If incoming wrap delta tries to wrap range that contains split position, we have to cancel the split and apply
320355
// the wrap. Since split was already applied, we have to revert it.
321-
322356
const sameRoot = a.range.start.root == b.position.root;
323357
const operateInSameParent = sameRoot && compareArrays( a.range.start.getParentPath(), b.position.getParentPath() ) === 'same';
324358
const splitInsideWrapRange = a.range.start.offset < b.position.offset && a.range.end.offset >= b.position.offset;
@@ -349,15 +383,22 @@ addTransformationCase( WrapDelta, SplitDelta, ( a, b, context ) => {
349383
// Add special case for RenameDelta x SplitDelta transformation.
350384
addTransformationCase( RenameDelta, SplitDelta, ( a, b, context ) => {
351385
const undoMode = context.aWasUndone || context.bWasUndone;
352-
const posBeforeSplitParent = new Position( b.position.root, b.position.path.slice( 0, -1 ) );
386+
387+
// The "clone operation" may be `InsertOperation`, `ReinsertOperation`, `MoveOperation` or `NoOperation`.
388+
// `MoveOperation` has `targetPosition` which we want to use. `NoOperation` has no `position` and we don't use special case then.
389+
let insertPosition = b._cloneOperation.position || b._cloneOperation.targetPosition;
390+
391+
if ( insertPosition ) {
392+
insertPosition = insertPosition.getShiftedBy( -1 );
393+
}
353394

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

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

362403
deltas.push( additionalRenameDelta );
363404
}
@@ -368,12 +409,19 @@ addTransformationCase( RenameDelta, SplitDelta, ( a, b, context ) => {
368409
// Add special case for SplitDelta x RenameDelta transformation.
369410
addTransformationCase( SplitDelta, RenameDelta, ( a, b, context ) => {
370411
const undoMode = context.aWasUndone || context.bWasUndone;
371-
const posBeforeSplitParent = new Position( a.position.root, a.position.path.slice( 0, -1 ) );
412+
413+
// The "clone operation" may be `InsertOperation`, `ReinsertOperation`, `MoveOperation` or `NoOperation`.
414+
// `MoveOperation` has `targetPosition` which we want to use. `NoOperation` has no `position` and we don't use special case then.
415+
let insertPosition = a._cloneOperation.position || a._cloneOperation.targetPosition;
416+
417+
if ( insertPosition ) {
418+
insertPosition = insertPosition.getShiftedBy( -1 );
419+
}
372420

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

378426
// `nodes` is a property that is available only if `SplitDelta` `a` has `InsertOperation`.
379427
// `SplitDelta` may have `ReinsertOperation` instead of `InsertOperation`.

tests/dev-utils/enableenginedebug.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,41 @@ describe( 'debug tools', () => {
454454
expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
455455
} );
456456

457+
it( 'SplitDelta - NoOperation as second operation', () => {
458+
const otherRoot = modelDoc.createRoot( 'main', 'otherRoot' );
459+
const splitEle = new ModelElement( 'paragraph', null, [ new ModelText( 'foo' ) ] );
460+
461+
otherRoot.appendChildren( [ splitEle ] );
462+
463+
const delta = new SplitDelta();
464+
const insert = new InsertOperation( ModelPosition.createAt( otherRoot, 1 ), [ new ModelElement( 'paragraph' ) ], 0 );
465+
const move = new NoOperation( 1 );
466+
467+
delta.addOperation( insert );
468+
delta.addOperation( move );
469+
470+
expect( delta.toString() ).to.equal( 'SplitDelta( 0 ): (clone to otherRoot [ 1 ])' );
471+
472+
delta.log();
473+
expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
474+
} );
475+
476+
it( 'SplitDelta - NoOperation as second operation, MoveOperation as first operation', () => {
477+
const otherRoot = modelDoc.createRoot( 'main', 'otherRoot' );
478+
479+
const delta = new SplitDelta();
480+
const insert = new MoveOperation( ModelPosition.createAt( modelRoot, 1 ), 1, ModelPosition.createAt( otherRoot, 1 ), 0 );
481+
const move = new NoOperation( 1 );
482+
483+
delta.addOperation( insert );
484+
delta.addOperation( move );
485+
486+
expect( delta.toString() ).to.equal( 'SplitDelta( 0 ): (clone to otherRoot [ 1 ])' );
487+
488+
delta.log();
489+
expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
490+
} );
491+
457492
it( 'UnwrapDelta', () => {
458493
const otherRoot = modelDoc.createRoot( 'main', 'otherRoot' );
459494
const unwrapEle = new ModelElement( 'paragraph', null, [ new ModelText( 'foo' ) ] );

tests/model/delta/transform/attributedelta.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,33 @@ describe( 'transform', () => {
256256
]
257257
} );
258258
} );
259+
260+
it( 'should use default algorithm and not throw if split delta has NoOperation', () => {
261+
const range = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2, 3 ] ) );
262+
const attrDelta = getAttributeDelta( range, 'foo', null, 'bar', 0 );
263+
const splitDelta = getSplitDelta( new Position( root, [ 0, 2 ] ), new Element( 'paragraph' ), 3, 0 );
264+
splitDelta.operations[ 1 ] = new NoOperation( 1 );
265+
266+
const transformed = transform( attrDelta, splitDelta, context );
267+
268+
baseVersion = splitDelta.operations.length;
269+
270+
expect( transformed.length ).to.equal( 1 );
271+
272+
expectDelta( transformed[ 0 ], {
273+
type: AttributeDelta,
274+
operations: [
275+
{
276+
type: AttributeOperation,
277+
range: new Range( new Position( root, [ 2 ] ), new Position( root, [ 3, 3 ] ) ),
278+
key: 'foo',
279+
oldValue: null,
280+
newValue: 'bar',
281+
baseVersion
282+
}
283+
]
284+
} );
285+
} );
259286
} );
260287

261288
describe( 'AttributeDelta', () => {

tests/model/delta/transform/renamedelta.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import Element from '../../../../src/model/element';
1111
import Position from '../../../../src/model/position';
1212

1313
import RenameDelta from '../../../../src/model/delta/renamedelta';
14+
import SplitDelta from '../../../../src/model/delta/splitdelta';
1415
import Delta from '../../../../src/model/delta/delta';
1516
import RenameOperation from '../../../../src/model/operation/renameoperation';
17+
import MoveOperation from '../../../../src/model/operation/moveoperation';
1618
import NoOperation from '../../../../src/model/operation/nooperation';
1719

1820
import {
@@ -106,6 +108,37 @@ describe( 'transform', () => {
106108
]
107109
} );
108110
} );
111+
112+
it( 'should not throw if clone operation is NoOperation and use default transformation in that case', () => {
113+
const noOpSplitDelta = new SplitDelta();
114+
noOpSplitDelta.addOperation( new NoOperation( 0 ) );
115+
noOpSplitDelta.addOperation( new MoveOperation( new Position( root, [ 1, 2 ] ), 3, new Position( root, [ 2, 0 ] ), 1 ) );
116+
117+
const renameDelta = new RenameDelta();
118+
renameDelta.addOperation( new RenameOperation(
119+
new Position( root, [ 1 ] ),
120+
'p',
121+
'li',
122+
baseVersion
123+
) );
124+
125+
const transformed = transform( renameDelta, noOpSplitDelta, context );
126+
127+
expect( transformed.length ).to.equal( 1 );
128+
129+
expectDelta( transformed[ 0 ], {
130+
type: RenameDelta,
131+
operations: [
132+
{
133+
type: RenameOperation,
134+
position: new Position( root, [ 1 ] ),
135+
oldName: 'p',
136+
newName: 'li',
137+
baseVersion: 2
138+
}
139+
]
140+
} );
141+
} );
109142
} );
110143

111144
describe( 'RenameDelta', () => {

0 commit comments

Comments
 (0)