From 71af35a62838d03e75515335cea1ac7f0bd4865b Mon Sep 17 00:00:00 2001 From: Szymon Cofalik Date: Tue, 19 Mar 2019 14:42:17 +0100 Subject: [PATCH] Fix: Editor crashed during undo in some pasting+remove scenarios. --- src/model/operation/transform.js | 58 ++++++++++++++++++++----- tests/model/operation/transform/undo.js | 53 ++++++++++++++++++---- 2 files changed, 91 insertions(+), 20 deletions(-) diff --git a/src/model/operation/transform.js b/src/model/operation/transform.js index 76f76f621..68313f648 100644 --- a/src/model/operation/transform.js +++ b/src/model/operation/transform.js @@ -508,6 +508,10 @@ class ContextFactory { this._setRelation( opA, opB, 'mergeTargetNotMoved' ); } + if ( opA.sourcePosition.isEqual( opB.targetPosition ) ) { + this._setRelation( opA, opB, 'mergeSourceNotMoved' ); + } + if ( opA.sourcePosition.isEqual( opB.sourcePosition ) ) { this._setRelation( opA, opB, 'mergeSameElement' ); } @@ -1434,19 +1438,34 @@ setTransformation( MergeOperation, SplitOperation, ( a, b, context ) => { // Case 2: // - // Merge source is at the same position as split position. This sometimes happen during undo. This merge operation - // might have been earlier transformed by a merge operation which both merged the same element. See case in - // `MergeOperation` x `MergeOperation` transformation. In that case, if the merge operation has been undone, the special - // case is not applied. - // - // In this scenario the merge operation is now transformed by the split which has undone the previous merge operation. - // So now we are fixing situation which was skipped in `MergeOperation` x `MergeOperation` case. + // Merge source is at the same position as split position. This sometimes happen, mostly during undo. + // The decision here is mostly to choose whether merge source position should stay where it is (so it will be at the end of the + // split element) or should be move to the beginning of the new element. // - if ( a.sourcePosition.isEqual( b.splitPosition ) && ( context.abRelation == 'mergeSameElement' || a.sourcePosition.offset > 0 ) ) { - a.sourcePosition = b.moveTargetPosition.clone(); - a.targetPosition = a.targetPosition._getTransformedBySplitOperation( b ); + if ( a.sourcePosition.isEqual( b.splitPosition ) ) { + // Use context to check if `SplitOperation` is not undoing a merge operation, that didn't change the `a` operation. + // This scenario happens the undone merge operation moved nodes at the source position of `a` operation. + // In that case `a` operation source position should stay where it is. + if ( context.abRelation == 'mergeSourceNotMoved' ) { + a.howMany = 0; + a.targetPosition = a.targetPosition._getTransformedBySplitOperation( b ); - return [ a ]; + return [ a ]; + } + + // This merge operation might have been earlier transformed by a merge operation which both merged the same element. + // See that case in `MergeOperation` x `MergeOperation` transformation. In that scenario, if the merge operation has been undone, + // the special case is not applied. + // + // Now, the merge operation is transformed by the split which has undone that previous merge operation. + // So now we are fixing situation which was skipped in `MergeOperation` x `MergeOperation` case. + // + if ( context.abRelation == 'mergeSameElement' || a.sourcePosition.offset > 0 ) { + a.sourcePosition = b.moveTargetPosition.clone(); + a.targetPosition = a.targetPosition._getTransformedBySplitOperation( b ); + + return [ a ]; + } } // The default case. @@ -2165,6 +2184,9 @@ setTransformation( SplitOperation, SplitOperation, ( a, b, context ) => { // // So we cancel split operation only if it was really identical. // + // Also, there is additional case, where split operations aren't identical and should not be cancelled, however the + // default transformation is incorrect too. + // if ( a.splitPosition.isEqual( b.splitPosition ) ) { if ( !a.graveyardPosition && !b.graveyardPosition ) { return [ new NoOperation( 0 ) ]; @@ -2173,6 +2195,20 @@ setTransformation( SplitOperation, SplitOperation, ( a, b, context ) => { if ( a.graveyardPosition && b.graveyardPosition && a.graveyardPosition.isEqual( b.graveyardPosition ) ) { return [ new NoOperation( 0 ) ]; } + + // Use context to know that the `a.splitPosition` should stay where it is. + // This happens during undo when first a merge operation moved nodes to `a.splitPosition` and now `b` operation undoes that merge. + if ( context.abRelation == 'splitBefore' ) { + // Since split is at the same position, there are no nodes left to split. + a.howMany = 0; + + // Note: there was `if ( a.graveyardPosition )` here but it was uncovered in tests and I couldn't find any scenarios for now. + // That would have to be a `SplitOperation` that didn't come from undo but is transformed by operations that were undone. + // It could happen if `context` is enabled in collaboration. + a.graveyardPosition = a.graveyardPosition._getTransformedBySplitOperation( b ); + + return [ a ]; + } } // Case 2: diff --git a/tests/model/operation/transform/undo.js b/tests/model/operation/transform/undo.js index 30cc761f9..a5965c92a 100644 --- a/tests/model/operation/transform/undo.js +++ b/tests/model/operation/transform/undo.js @@ -616,15 +616,13 @@ describe( 'transform', () => { // https://github.com/ckeditor/ckeditor5/issues/1540 it( 'paste, select all, paste, undo, undo, redo, redo, redo', () => { - const model = john.editor.model; - john.setData( '[]' ); - model.insertContent( getPastedContent() ); + pasteContent(); john.setSelection( [ 0, 0 ], [ 1, 3 ] ); - model.insertContent( getPastedContent() ); + pasteContent(); expectClients( 'FooBar' ); @@ -644,11 +642,48 @@ describe( 'transform', () => { expectClients( 'FooBar' ); - function getPastedContent() { - return new DocumentFragment( [ - new Element( 'heading1', null, new Text( 'Foo' ) ), - new Element( 'paragraph', null, new Text( 'Bar' ) ) - ] ); + function pasteContent() { + john.editor.model.insertContent( + new DocumentFragment( [ + new Element( 'heading1', null, new Text( 'Foo' ) ), + new Element( 'paragraph', null, new Text( 'Bar' ) ) + ] ) + ); } } ); + + // Happens in track changes. Emulated here. + // https://github.com/ckeditor/ckeditor5-engine/issues/1701 + it( 'paste, remove, undo, undo, redo, redo', () => { + john.setData( 'Ab[]cdWxyz' ); + + john.editor.model.insertContent( + new DocumentFragment( [ + new Element( 'paragraph', null, new Text( 'Foo' ) ), + new Element( 'paragraph', null, new Text( 'Bar' ) ) + ] ) + ); + + john.setSelection( [ 1, 3 ], [ 2, 2 ] ); + + john._processExecute( 'delete' ); + + expectClients( 'AbFooBaryz' ); + + john.undo(); + + expectClients( 'AbFooBarcdWxyz' ); + + john.undo(); + + expectClients( 'AbcdWxyz' ); + + john.redo(); + + expectClients( 'AbFooBarcdWxyz' ); + + john.redo(); + + expectClients( 'AbFooBaryz' ); + } ); } );