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

Commit a6c6167

Browse files
authored
Merge pull request #1054 from ckeditor/t/1053
Fixed: Fixed a bug when additional list item has been created when undoing applying block quote to a list followed by splitting list item in that list. Closes #1053.
2 parents 4a0f151 + a32206e commit a6c6167

File tree

4 files changed

+188
-98
lines changed

4 files changed

+188
-98
lines changed

src/model/delta/transform.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -444,21 +444,21 @@ function _setForceNotSticky( context ) {
444444
// always treats `RemoveOperation` as a stronger one, no matter how `context.isStrong` is set. It is like this
445445
// to provide better results when transformations happen.
446446
//
447-
// This is however works fine only when additional context is not used.
447+
// This, however, works fine only when additional context is not used.
448448
//
449449
// When additional context is used, we need a better way to decide whether `RemoveOperation` is "dominating" (or in other
450450
// words, whether nodes removed by given operation should stay in graveyard if other operation wants to move them).
451451
//
452452
// The answer to this is easy: if `RemoveOperation` has been already undone, we are not forcing given nodes to stay
453453
// in graveyard. In such scenario, we set `context.forceWeakRemove` to `true`. However, if the `RemoveOperation` has
454-
// not been undone, we set `context.forceWeakRemove` to `false` because we want remove to be "dominating".
454+
// not been undone, we set `context.forceWeakRemove` to `false` because we want the operation to be "dominating".
455455
function _setForceWeakRemove( b, context ) {
456456
const history = context.document.history;
457457
const originalB = context.originalDelta.get( b );
458458

459-
// If `b` delta is a remove delta, that has not been undone yet, forceWeakRemove should be `false`.
460-
// It should be `true` in any other case, if additional context is used.
461-
context.forceWeakRemove = !( originalB instanceof RemoveDelta && !history.isUndoneDelta( originalB ) );
459+
// If `b` delta has not been undone yet, forceWeakRemove should be `false`.
460+
// It should be `true`, in any other case, if additional context is used.
461+
context.forceWeakRemove = history.isUndoneDelta( originalB );
462462
}
463463

464464
// Sets `context.wasAffected` which holds context information about how transformed deltas are related. `context.wasAffected`

src/model/operation/transform.js

Lines changed: 82 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -377,13 +377,17 @@ const ot = {
377377
const rangeA = Range.createFromPositionAndShift( a.sourcePosition, a.howMany );
378378
const rangeB = Range.createFromPositionAndShift( b.sourcePosition, b.howMany );
379379

380+
// Assign `context.isStrong` to a different variable, because the value may change during execution of
381+
// this algorithm and we do not want to override original `context.isStrong` that will be used in later transformations.
382+
let isStrong = context.isStrong;
383+
380384
// Whether range moved by operation `b` is includable in operation `a` move range.
381385
// For this, `a` operation has to be sticky (so `b` sticks to the range) and context has to allow stickiness.
382386
const includeB = a.isSticky && !context.forceNotSticky;
383387

384388
// Evaluate new target position for transformed operation.
385-
// Check whether there is a forced order of nodes or use `context.isStrong` flag for conflict resolving.
386-
const insertBefore = context.insertBefore === undefined ? !context.isStrong : context.insertBefore;
389+
// Check whether there is a forced order of nodes or use `isStrong` flag for conflict resolving.
390+
const insertBefore = context.insertBefore === undefined ? !isStrong : context.insertBefore;
387391

388392
// `a.targetPosition` could be affected by the `b` operation. We will transform it.
389393
const newTargetPosition = a.targetPosition._getTransformedByMove(
@@ -425,7 +429,7 @@ const ot = {
425429
rangeA.start = rangeA.start._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, !includeB );
426430
rangeA.end = rangeA.end._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, includeB );
427431

428-
return makeMoveOperationsFromRanges( a, [ rangeA ], newTargetPosition );
432+
return makeMoveOperationsFromRanges( [ rangeA ], newTargetPosition, a );
429433
}
430434

431435
//
@@ -441,7 +445,7 @@ const ot = {
441445
rangeA.start = rangeA.start._getCombined( b.sourcePosition, b.getMovedRangeStart() );
442446
rangeA.end = rangeA.end._getCombined( b.sourcePosition, b.getMovedRangeStart() );
443447

444-
return makeMoveOperationsFromRanges( a, [ rangeA ], newTargetPosition );
448+
return makeMoveOperationsFromRanges( [ rangeA ], newTargetPosition, a );
445449
}
446450
//
447451
// End of special case #2.
@@ -463,7 +467,7 @@ const ot = {
463467
rangeA.start = rangeA.start._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, !includeB );
464468
rangeA.end = rangeA.end._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, includeB );
465469

466-
return makeMoveOperationsFromRanges( a, [ rangeA ], newTargetPosition );
470+
return makeMoveOperationsFromRanges( [ rangeA ], newTargetPosition, a );
467471
}
468472
//
469473
// End of special case #3.
@@ -473,39 +477,43 @@ const ot = {
473477
// Default case - ranges are on the same level or are not connected with each other.
474478
//
475479
// Modifier for default case.
476-
// Modifies `context.isStrong` in certain conditions.
480+
// Modifies `isStrong` flag in certain conditions.
477481
//
478482
// If only one of operations is a remove operation, we force remove operation to be the "stronger" one
479483
// to provide more expected results. This is done only if `context.forceWeakRemove` is set to `false`.
480484
// `context.forceWeakRemove` is set to `true` in certain conditions when transformation takes place during undo.
481485
if ( !context.forceWeakRemove ) {
482486
if ( a instanceof RemoveOperation && !( b instanceof RemoveOperation ) ) {
483-
context.isStrong = true;
487+
isStrong = true;
484488
} else if ( !( a instanceof RemoveOperation ) && b instanceof RemoveOperation ) {
485-
context.isStrong = false;
489+
isStrong = false;
486490
}
487491
}
488492

489493
// Handle operation's source ranges - check how `rangeA` is affected by `b` operation.
490494
// This will aggregate transformed ranges.
491-
let ranges = [];
495+
const ranges = [];
492496

493497
// Get the "difference part" of `a` operation source range.
494498
// This is an array with one or two ranges. Two ranges if `rangeB` is inside `rangeA`.
495499
const difference = rangeA.getDifference( rangeB );
496500

497501
for ( const range of difference ) {
498502
// Transform those ranges by `b` operation. For example if `b` moved range from before those ranges, fix those ranges.
499-
range.start = range.start._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, !includeB );
500-
range.end = range.end._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, includeB );
503+
range.start = range.start._getTransformedByDeletion( b.sourcePosition, b.howMany );
504+
range.end = range.end._getTransformedByDeletion( b.sourcePosition, b.howMany );
505+
506+
// If `b` operation targets into `rangeA` on the same level, spread `rangeA` into two ranges.
507+
const shouldSpread = compareArrays( range.start.getParentPath(), b.getMovedRangeStart().getParentPath() ) == 'same';
508+
const newRanges = range._getTransformedByInsertion( b.getMovedRangeStart(), b.howMany, shouldSpread, includeB );
501509

502-
ranges.push( range );
510+
ranges.push( ...newRanges );
503511
}
504512

505513
// Then, we have to manage the "common part" of both move ranges.
506514
const common = rangeA.getIntersection( rangeB );
507515

508-
if ( common !== null && context.isStrong && !bTargetsToA ) {
516+
if ( common !== null && isStrong && !bTargetsToA ) {
509517
// Calculate the new position of that part of original range.
510518
common.start = common.start._getCombined( b.sourcePosition, b.getMovedRangeStart() );
511519
common.end = common.end._getCombined( b.sourcePosition, b.getMovedRangeStart() );
@@ -521,7 +529,7 @@ const ot = {
521529
}
522530
// If there is one difference range, we need to check whether common part was before it or after it.
523531
else if ( ranges.length == 1 ) {
524-
if ( rangeB.start.isBefore( rangeA.start ) ) {
532+
if ( rangeB.start.isBefore( rangeA.start ) || rangeB.start.isEqual( rangeA.start ) ) {
525533
ranges.unshift( common );
526534
} else {
527535
ranges.push( common );
@@ -536,34 +544,11 @@ const ot = {
536544

537545
if ( ranges.length === 0 ) {
538546
// If there are no "source ranges", nothing should be changed.
539-
// Note that this can happen only if `context.isStrong == false` and `rangeA.isEqual( rangeB )`.
547+
// Note that this can happen only if `isStrong == false` and `rangeA.isEqual( rangeB )`.
540548
return [ new NoOperation( a.baseVersion ) ];
541549
}
542550

543-
// At this moment we have some ranges and a target position, to which those ranges should be moved.
544-
// Order in `ranges` array is the go-to order of after transformation. We can reverse the `ranges`
545-
// array so the last range will be moved first. This will ensure that each transformed `MoveOperation`
546-
// can have same `targetPosition` and the order of ranges will be kept.
547-
ranges = ranges.reverse();
548-
549-
// We are almost done. We have `ranges` and `targetPosition` to make operations from.
550-
// Unfortunately, those operations may affect each other. Precisely, first operation after move
551-
// may affect source range of second and third operation. Same with second operation affecting third.
552-
// We need to fix those source ranges once again, before converting `ranges` to operations.
553-
// Keep in mind that `targetPosition` is already set in stone thanks to the trick above.
554-
for ( let i = 1; i < ranges.length; i++ ) {
555-
for ( let j = 0; j < i; j++ ) {
556-
const howMany = ranges[ j ].end.offset - ranges[ j ].start.offset;
557-
558-
// Thankfully, all ranges in `ranges` array are:
559-
// * non-intersecting (these are part of original `a` operation source range), and
560-
// * `newTargetPosition` does not target into them (opposite would mean that `a` operation targets "inside itself").
561-
// This means that the transformation will be "clean" and always return one result.
562-
ranges[ i ] = ranges[ i ]._getTransformedByMove( ranges[ j ].start, newTargetPosition, howMany )[ 0 ];
563-
}
564-
}
565-
566-
return makeMoveOperationsFromRanges( a, ranges, newTargetPosition );
551+
return makeMoveOperationsFromRanges( ranges, newTargetPosition, a );
567552
}
568553
}
569554
};
@@ -646,29 +631,66 @@ function joinRanges( ranges ) {
646631
}
647632
}
648633

649-
// Map transformed range(s) to operations and return them.
650-
function makeMoveOperationsFromRanges( a, ranges, targetPosition ) {
651-
return ranges.map( range => {
652-
// We want to keep correct operation class.
653-
let OperationClass;
654-
655-
if ( targetPosition.root.rootName == '$graveyard' ) {
656-
OperationClass = RemoveOperation;
657-
} else if ( range.start.root.rootName == '$graveyard' ) {
658-
OperationClass = ReinsertOperation;
659-
} else {
660-
OperationClass = MoveOperation;
634+
// Helper function for `MoveOperation` x `MoveOperation` transformation.
635+
// Convert given ranges and target position to move operations and return them.
636+
// Ranges and target position will be transformed on-the-fly when generating operations.
637+
// Given `ranges` should be in the order of how they were in the original transformed operation.
638+
// Given `targetPosition` is the target position of the first range from `ranges`.
639+
function makeMoveOperationsFromRanges( ranges, targetPosition, a ) {
640+
// At this moment we have some ranges and a target position, to which those ranges should be moved.
641+
// Order in `ranges` array is the go-to order of after transformation.
642+
//
643+
// We are almost done. We have `ranges` and `targetPosition` to make operations from.
644+
// Unfortunately, those operations may affect each other. Precisely, first operation after move
645+
// may affect source range and target position of second and third operation. Same with second
646+
// operation affecting third.
647+
//
648+
// We need to fix those source ranges and target positions once again, before converting `ranges` to operations.
649+
const operations = [];
650+
651+
// Keep in mind that nothing will be transformed if there is just one range in `ranges`.
652+
for ( let i = 0; i < ranges.length; i++ ) {
653+
// Create new operation out of a range and target position.
654+
const op = makeMoveOperation( ranges[ i ], targetPosition, a.isSticky );
655+
656+
operations.push( op );
657+
658+
// Transform other ranges by the generated operation.
659+
for ( let j = i + 1; j < ranges.length; j++ ) {
660+
// All ranges in `ranges` array should be:
661+
// * non-intersecting (these are part of original operation source range), and
662+
// * `targetPosition` does not target into them (opposite would mean that transformed operation targets "inside itself").
663+
//
664+
// This means that the transformation will be "clean" and always return one result.
665+
ranges[ j ] = ranges[ j ]._getTransformedByMove( op.sourcePosition, op.targetPosition, op.howMany )[ 0 ];
661666
}
662667

663-
const result = new OperationClass(
664-
range.start,
665-
range.end.offset - range.start.offset,
666-
targetPosition,
667-
0 // Is corrected anyway later.
668-
);
668+
targetPosition = targetPosition._getTransformedByMove( op.sourcePosition, op.targetPosition, op.howMany, true, false );
669+
}
670+
671+
return operations;
672+
}
673+
674+
function makeMoveOperation( range, targetPosition, isSticky ) {
675+
// We want to keep correct operation class.
676+
let OperationClass;
677+
678+
if ( targetPosition.root.rootName == '$graveyard' ) {
679+
OperationClass = RemoveOperation;
680+
} else if ( range.start.root.rootName == '$graveyard' ) {
681+
OperationClass = ReinsertOperation;
682+
} else {
683+
OperationClass = MoveOperation;
684+
}
685+
686+
const result = new OperationClass(
687+
range.start,
688+
range.end.offset - range.start.offset,
689+
targetPosition,
690+
0 // Is corrected anyway later.
691+
);
669692

670-
result.isSticky = a.isSticky;
693+
result.isSticky = isSticky;
671694

672-
return result;
673-
} );
695+
return result;
674696
}

tests/model/delta/transform/transform.js

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import InsertDelta from '../../../../src/model/delta/insertdelta';
1717
import RemoveDelta from '../../../../src/model/delta/removedelta';
1818
import MoveDelta from '../../../../src/model/delta/movedelta';
1919
import SplitDelta from '../../../../src/model/delta/splitdelta';
20+
import UnwrapDelta from '../../../../src/model/delta/unwrapdelta';
2021

2122
import NoOperation from '../../../../src/model/operation/nooperation';
2223
import MoveOperation from '../../../../src/model/operation/moveoperation';
@@ -30,7 +31,8 @@ import {
3031
getSplitDelta,
3132
getRemoveDelta,
3233
getMoveDelta,
33-
getMergeDelta
34+
getMergeDelta,
35+
getUnwrapDelta
3436
} from '../../../../tests/model/delta/transform/_utils/utils';
3537

3638
describe( 'transform', () => {
@@ -115,7 +117,7 @@ describe( 'transform', () => {
115117
operations: [
116118
{
117119
type: RemoveOperation,
118-
sourcePosition: new Position( root, [ 1, 0 ] ),
120+
sourcePosition: new Position( root, [ 0, 2 ] ),
119121
howMany: 1,
120122
baseVersion: 3
121123
}
@@ -127,7 +129,7 @@ describe( 'transform', () => {
127129
operations: [
128130
{
129131
type: RemoveOperation,
130-
sourcePosition: new Position( root, [ 0, 2 ] ),
132+
sourcePosition: new Position( root, [ 1, 0 ] ),
131133
howMany: 1,
132134
baseVersion: 4
133135
}
@@ -163,7 +165,7 @@ describe( 'transform', () => {
163165
operations: [
164166
{
165167
type: RemoveOperation,
166-
sourcePosition: new Position( root, [ 1, 0 ] ),
168+
sourcePosition: new Position( root, [ 0, 2 ] ),
167169
howMany: 1,
168170
baseVersion: 3
169171
}
@@ -175,7 +177,7 @@ describe( 'transform', () => {
175177
operations: [
176178
{
177179
type: RemoveOperation,
178-
sourcePosition: new Position( root, [ 0, 2 ] ),
180+
sourcePosition: new Position( root, [ 1, 0 ] ),
179181
howMany: 1,
180182
baseVersion: 4
181183
}
@@ -341,6 +343,51 @@ describe( 'transform', () => {
341343
} );
342344
} );
343345

346+
// #1053.
347+
it( 'remove operation importance should be set correctly also for deltas other than remove delta', () => {
348+
const unwrapDelta = getUnwrapDelta( new Position( root, [ 0 ] ), 2, baseVersion );
349+
350+
const nodeCopy = new Element( 'p' );
351+
const splitDelta = getSplitDelta( new Position( root, [ 0, 0, 1 ] ), nodeCopy, 1, baseVersion );
352+
const mergeDelta = splitDelta.getReversed();
353+
354+
const { deltasA } = transformDeltaSets( [ unwrapDelta ], [ splitDelta, mergeDelta ], doc );
355+
356+
expect( deltasA.length ).to.equal( 2 );
357+
358+
expectDelta( deltasA[ 0 ], {
359+
type: MoveDelta,
360+
operations: [
361+
{
362+
type: MoveOperation,
363+
sourcePosition: new Position( root, [ 0, 0 ] ),
364+
howMany: 1,
365+
targetPosition: new Position( root, [ 0 ] ),
366+
baseVersion: baseVersion + 4
367+
}
368+
]
369+
} );
370+
371+
expectDelta( deltasA[ 1 ], {
372+
type: UnwrapDelta,
373+
operations: [
374+
{
375+
type: MoveOperation,
376+
sourcePosition: new Position( root, [ 1, 0 ] ),
377+
howMany: 1,
378+
targetPosition: new Position( root, [ 1 ] ),
379+
baseVersion: baseVersion + 5
380+
},
381+
{
382+
type: RemoveOperation,
383+
sourcePosition: new Position( root, [ 2 ] ),
384+
howMany: 1,
385+
baseVersion: baseVersion + 6
386+
}
387+
]
388+
} );
389+
} );
390+
344391
it( 'should keep correct order of nodes if additional context is used (insert before)', () => {
345392
// Assume document state: 'a^bcd'. User presses 'delete' key twice.
346393
const removeDelta1 = getRemoveDelta( new Position( root, [ 1 ] ), 1, baseVersion ); // 'acd'.

0 commit comments

Comments
 (0)