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

Commit b7cc243

Browse files
author
Piotr Jasiun
authored
Merge pull request #1098 from ckeditor/t/1096
Fix: Splitting paragraph twice in the same position will now be undoable. Also fixed SplitDelta x SplitDelta transformation. Closes #1096. Closes #1097.
2 parents 7006279 + e04c27d commit b7cc243

File tree

4 files changed

+147
-21
lines changed

4 files changed

+147
-21
lines changed

src/model/delta/basic-transformations.js

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import Position from '../position';
1717
import NoOperation from '../operation/nooperation';
1818
import AttributeOperation from '../operation/attributeoperation';
1919
import InsertOperation from '../operation/insertoperation';
20+
import ReinsertOperation from '../operation/reinsertoperation';
2021

2122
import Delta from './delta';
2223
import AttributeDelta from './attributedelta';
@@ -96,10 +97,12 @@ addTransformationCase( AttributeDelta, SplitDelta, ( a, b, context ) => {
9697

9798
// Add special case for InsertDelta x MergeDelta transformation.
9899
addTransformationCase( InsertDelta, MergeDelta, ( a, b, context ) => {
100+
const undoMode = context.aWasUndone || context.bWasUndone;
101+
99102
// If insert is applied at the same position where merge happened, we reverse the merge (we treat it like it
100103
// didn't happen) and then apply the original insert operation. This is "mirrored" in MergeDelta x InsertDelta
101104
// transformation below, where we simply do not apply MergeDelta.
102-
if ( a.position.isEqual( b.position ) ) {
105+
if ( !undoMode && a.position.isEqual( b.position ) ) {
103106
return [
104107
b.getReversed(),
105108
a.clone()
@@ -155,9 +158,11 @@ addTransformationCase( MoveDelta, MergeDelta, ( a, b, context ) => {
155158

156159
// Add special case for MergeDelta x InsertDelta transformation.
157160
addTransformationCase( MergeDelta, InsertDelta, ( a, b, context ) => {
161+
const undoMode = context.aWasUndone || context.bWasUndone;
162+
158163
// If merge is applied at the same position where we inserted a range of nodes we cancel the merge as it's results
159164
// may be unexpected and very weird. Even if we do some "magic" we don't know what really are users' expectations.
160-
if ( a.position.isEqual( b.position ) ) {
165+
if ( !undoMode && a.position.isEqual( b.position ) ) {
161166
return [ noDelta() ];
162167
}
163168

@@ -182,8 +187,14 @@ addTransformationCase( MergeDelta, MoveDelta, ( a, b, context ) => {
182187
return defaultTransform( a, b, context );
183188
} );
184189

185-
// Add special case for SplitDelta x SplitDelta transformation.
186190
addTransformationCase( SplitDelta, SplitDelta, ( a, b, context ) => {
191+
const undoMode = context.aWasUndone || context.bWasUndone;
192+
193+
// Do not apply special transformation case if transformation is in undo mode.
194+
if ( undoMode ) {
195+
return defaultTransform( a, b, context );
196+
}
197+
187198
// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
188199
if ( !a.position || !b.position ) {
189200
return defaultTransform( a, b, context );
@@ -194,23 +205,48 @@ addTransformationCase( SplitDelta, SplitDelta, ( a, b, context ) => {
194205

195206
// The special case is for splits inside the same parent.
196207
if ( a.position.root == b.position.root && compareArrays( pathA, pathB ) == 'same' ) {
197-
const newContext = Object.assign( {}, context );
208+
a = a.clone();
209+
210+
if ( a.position.offset <= b.position.offset ) {
211+
// If both first operations are `ReinsertOperation`s, we might need to transform `a._cloneOperation`,
212+
// so it will take correct node from graveyard.
213+
if (
214+
a._cloneOperation instanceof ReinsertOperation && b._cloneOperation instanceof ReinsertOperation &&
215+
a._cloneOperation.sourcePosition.offset > b._cloneOperation.sourcePosition.offset
216+
) {
217+
a._cloneOperation.sourcePosition.offset--;
218+
}
198219

199-
// If `a` delta splits in further location, make sure that it will move some nodes by forcing it to be strong.
200-
// Similarly, if `a` splits closer, make sure that it is transformed accordingly.
201-
if ( a.position.offset != b.position.offset ) {
202-
// We need to ensure that incoming operation is strong / weak.
203-
newContext.isStrong = a.position.offset > b.position.offset;
204-
}
220+
// `a` splits closer or at same offset.
221+
// Change how many nodes are moved. Do not move nodes that were moved by delta `b`.
222+
const aRange = Range.createFromPositionAndShift( a.position, a._moveOperation.howMany );
223+
const bRange = Range.createFromPositionAndShift( b.position, b._moveOperation.howMany );
205224

206-
// Then, default transformation is almost good.
207-
// We need to change insert operations offsets, though.
208-
// We will use `context.insertBefore` for this (but only if it is not set!)
209-
if ( context.insertBefore === undefined ) {
210-
newContext.insertBefore = newContext.isStrong;
211-
}
225+
const diff = aRange.getDifference( bRange );
226+
227+
let newHowMany = 0;
228+
229+
for ( const range of diff ) {
230+
newHowMany += range.end.offset - range.start.offset;
231+
}
212232

213-
return defaultTransform( a, b, newContext );
233+
if ( newHowMany === 0 ) {
234+
a.operations.pop(); // Remove last operation (`MoveOperation`).
235+
a.addOperation( new NoOperation( a.operations[ 0 ].baseVersion + 1 ) ); // Add `NoOperation` instead.
236+
} else {
237+
a.operations[ 1 ].howMany = newHowMany;
238+
}
239+
240+
return [ a ];
241+
} else {
242+
// `a` splits further.
243+
// This is more complicated case, thankfully we can solve it using default transformation and setting proper context.
244+
const newContext = Object.assign( {}, context );
245+
newContext.isStrong = true;
246+
newContext.insertBefore = true;
247+
248+
return defaultTransform( a, b, newContext );
249+
}
214250
}
215251

216252
return defaultTransform( a, b, context );

tests/model/delta/transform/insertdelta.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,30 @@ describe( 'transform', () => {
132132
expect( nodesAndText ).to.equal( 'XXXXXabcdXAABBPabcfoobarxyzP' );
133133
} );
134134

135+
it( 'merge in same position as insert - undo mode', () => {
136+
// In undo mode, default transformation algorithm should be used.
137+
const mergeDelta = getMergeDelta( insertPosition, 4, 12, baseVersion );
138+
139+
context.bWasUndone = true;
140+
const transformed = transform( insertDelta, mergeDelta, context );
141+
142+
baseVersion = mergeDelta.operations.length;
143+
144+
expect( transformed.length ).to.equal( 1 );
145+
146+
expectDelta( transformed[ 0 ], {
147+
type: InsertDelta,
148+
operations: [
149+
{
150+
type: InsertOperation,
151+
position: Position.createFromPosition( insertPosition ),
152+
nodes: [ nodeA, nodeB ],
153+
baseVersion
154+
}
155+
]
156+
} );
157+
} );
158+
135159
it( 'merge the node that is parent of insert position (sticky move test)', () => {
136160
const mergeDelta = getMergeDelta( new Position( root, [ 3, 3 ] ), 1, 4, baseVersion );
137161
const transformed = transform( insertDelta, mergeDelta, context );

tests/model/delta/transform/mergedelta.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,38 @@ describe( 'transform', () => {
8888
expect( nodesAndText ).to.equal( 'XXXXXabcdXAABBPabcfoobarxyzP' );
8989
} );
9090

91+
it( 'insert at same position as merge - undo mode', () => {
92+
// In undo mode, default transformation algorithm should be used.
93+
const insertDelta = getInsertDelta( mergePosition, [ nodeA, nodeB ], baseVersion );
94+
95+
context.bWasUndone = true;
96+
const transformed = transform( mergeDelta, insertDelta, context );
97+
98+
baseVersion = insertDelta.operations.length;
99+
100+
expect( transformed.length ).to.equal( 1 );
101+
102+
expectDelta( transformed[ 0 ], {
103+
type: MergeDelta,
104+
operations: [
105+
{
106+
type: MoveOperation,
107+
sourcePosition: new Position( root, [ 3, 3, 5, 0 ] ),
108+
howMany: mergeDelta.operations[ 0 ].howMany,
109+
targetPosition: mergeDelta.operations[ 0 ].targetPosition,
110+
baseVersion
111+
},
112+
{
113+
type: RemoveOperation,
114+
sourcePosition: new Position( root, [ 3, 3, 5 ] ),
115+
howMany: 1,
116+
targetPosition: mergeDelta.operations[ 1 ].targetPosition,
117+
baseVersion: baseVersion + 1
118+
}
119+
]
120+
} );
121+
} );
122+
91123
it( 'insert inside merged node (sticky move test)', () => {
92124
const insertDelta = getInsertDelta( new Position( root, [ 3, 3, 3, 12 ] ), [ nodeA, nodeB ], baseVersion );
93125
const transformed = transform( mergeDelta, insertDelta, context );

tests/model/delta/transform/splitdelta.js

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ describe( 'transform', () => {
5656
} );
5757

5858
describe( 'SplitDelta', () => {
59-
it( 'split in same parent and offset', () => {
59+
it( 'split in same parent and offset - isStrong = false', () => {
6060
const splitDeltaB = getSplitDelta( splitPosition, new Element( 'p' ), 9, baseVersion );
6161
const transformed = transform( splitDelta, splitDeltaB, context );
6262

@@ -86,8 +86,41 @@ describe( 'transform', () => {
8686

8787
const nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 0 ] ), 6 ) );
8888

89-
// Incoming split delta is discarded. Only one new element is created after applying both split deltas.
90-
// There are no empty P elements.
89+
expect( nodesAndText ).to.equal( 'XXXXXabcdXPabcPPPPfoobarxyzP' );
90+
} );
91+
92+
it( 'split in same parent and offset - isStrong = true', () => {
93+
const splitDeltaB = getSplitDelta( splitPosition, new Element( 'p' ), 9, baseVersion );
94+
95+
context.isStrong = true;
96+
const transformed = transform( splitDelta, splitDeltaB, context );
97+
98+
baseVersion = splitDeltaB.operations.length;
99+
100+
expect( transformed.length ).to.equal( 1 );
101+
102+
expectDelta( transformed[ 0 ], {
103+
type: SplitDelta,
104+
operations: [
105+
{
106+
type: InsertOperation,
107+
position: new Position( root, [ 3, 3, 4 ] ),
108+
baseVersion
109+
},
110+
{
111+
type: NoOperation,
112+
baseVersion: baseVersion + 1
113+
}
114+
]
115+
} );
116+
117+
// Test if deltas do what they should after applying transformed delta.
118+
119+
applyDelta( splitDeltaB, doc );
120+
applyDelta( transformed[ 0 ], doc );
121+
122+
const nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 0 ] ), 6 ) );
123+
91124
expect( nodesAndText ).to.equal( 'XXXXXabcdXPabcPPPPfoobarxyzP' );
92125
} );
93126

@@ -98,7 +131,8 @@ describe( 'transform', () => {
98131
const splitDeltaB = getSplitDelta( splitPosition, new Element( 'p' ), 9, baseVersion );
99132
const transformed = transform( splitDelta, splitDeltaB, {
100133
isStrong: false,
101-
insertBefore: true
134+
insertBefore: true,
135+
bWasUndone: true // If `insertBefore` was set it means that delta `b` had to be undone.
102136
} );
103137

104138
baseVersion = splitDeltaB.operations.length;

0 commit comments

Comments
 (0)