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 #856 from ckeditor/t/841
Browse files Browse the repository at this point in the history
Fix: Fixed a bug where `LiveRange` position would be lost when using wrap and unwrap deltas. Closes #841.
  • Loading branch information
Reinmar authored Mar 7, 2017
2 parents 9aa4913 + 81bdf6d commit efe3987
Show file tree
Hide file tree
Showing 5 changed files with 510 additions and 45 deletions.
33 changes: 10 additions & 23 deletions src/model/delta/basic-transformations.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,33 +97,20 @@ addTransformationCase( InsertDelta, MergeDelta, ( a, b, isStrong ) => {
return defaultTransform( a, b, isStrong );
} );

// Add special case for MarkerDelta x SplitDelta
addTransformationCase( MarkerDelta, SplitDelta, ( a, b, isStrong ) => {
// If marked range is split, we need to fix it:
// ab[cdef]gh ==> ab[cd
// ef]gh
// To mimic what normally happens with LiveRange if you split it.

// Mote: MarkerDelta can't get split to two deltas, neither can MarkerOperation.
const transformedDelta = defaultTransform( a, b, isStrong )[ 0 ];
function transformMarkerDelta( a, b ) {
const transformedDelta = a.clone();
const transformedOp = transformedDelta.operations[ 0 ];

// Fix positions, if needed.
const markerOp = a.operations[ 0 ];

const source = b.position;
const target = b._moveOperation.targetPosition;

if ( markerOp.oldRange.containsPosition( b.position ) ) {
transformedOp.oldRange.end = markerOp.oldRange.end._getCombined( source, target );
}

if ( markerOp.newRange.containsPosition( b.position ) ) {
transformedOp.newRange.end = markerOp.newRange.end._getCombined( source, target );
}
transformedOp.oldRange = transformedOp.oldRange.getTransformedByDelta( b )[ 0 ];
transformedOp.newRange = transformedOp.newRange.getTransformedByDelta( b )[ 0 ];

return [ transformedDelta ];
} );
}

addTransformationCase( MarkerDelta, SplitDelta, transformMarkerDelta );
addTransformationCase( MarkerDelta, MergeDelta, transformMarkerDelta );
addTransformationCase( MarkerDelta, WrapDelta, transformMarkerDelta );
addTransformationCase( MarkerDelta, UnwrapDelta, transformMarkerDelta );

// Add special case for MoveDelta x MergeDelta transformation.
addTransformationCase( MoveDelta, MergeDelta, ( a, b, isStrong ) => {
Expand Down
61 changes: 49 additions & 12 deletions src/model/range.js
Original file line number Diff line number Diff line change
Expand Up @@ -447,20 +447,58 @@ export default class Range {
* @returns {Array.<module:engine/model/range~Range>}
*/
_getTransformedByDocumentChange( type, deltaType, targetPosition, howMany, sourcePosition ) {
// IMPORTANT! Every special case added here has to be reflected in MarkerDelta transformations!
// Check /src/model/delta/basic-transformations.js.
if ( type == 'insert' ) {
return this._getTransformedByInsertion( targetPosition, howMany, false, false );
} else {
const ranges = this._getTransformedByMove( sourcePosition, targetPosition, howMany );

if ( deltaType == 'split' && this.containsPosition( sourcePosition ) ) {
// Special case for splitting element inside range.
// <p>f[ooba]r</p> -> <p>f[oo</p><p>ba]r</p>
ranges[ 0 ].end = ranges[ 1 ].end;
ranges.pop();
} else if ( deltaType == 'merge' && type == 'move' && this.isCollapsed && ranges[ 0 ].start.isEqual( sourcePosition ) ) {
// Special case when collapsed range is in merged element.
// <p>foo</p><p>[]bar{}</p> -> <p>foo[]bar{}</p>
ranges[ 0 ] = new Range( targetPosition.getShiftedBy( this.start.offset ) );
// Don't ask. Just debug.
// Like this: https://github.com/ckeditor/ckeditor5-engine/issues/841#issuecomment-282706488.
//
// In following cases, in examples, the last step is the fix step.
// When there are multiple ranges in an example, ranges[] array indices are represented as follows:
// * [] is ranges[ 0 ],
// * {} is ranges[ 1 ],
// * () is ranges[ 2 ].
if ( type == 'move' ) {
const sourceRange = Range.createFromPositionAndShift( sourcePosition, howMany );

if ( deltaType == 'split' && this.containsPosition( sourcePosition ) ) {
// Range contains a position where an element is split.
// <p>f[ooba]r</p> -> <p>f[ooba]r</p><p></p> -> <p>f[oo]</p><p>{ba}r</p> -> <p>f[oo</p><p>ba]r</p>
return [ new Range( ranges[ 0 ].start, ranges[ 1 ].end ) ];
} else if ( deltaType == 'merge' && this.isCollapsed && ranges[ 0 ].start.isEqual( sourcePosition ) ) {
// Collapsed range is in merged element.
// Without fix, the range would end up in the graveyard, together with removed element.
// <p>foo</p><p>[]bar</p> -> <p>foobar</p><p>[]</p> -> <p>foobar</p> -> <p>foo[]bar</p>
return [ new Range( targetPosition.getShiftedBy( this.start.offset ) ) ];
} else if ( deltaType == 'wrap' ) {
// Range intersects (at the start) with wrapped element (<p>ab</p>).
// <p>a[b</p><p>c]d</p> -> <p>a[b</p><w></w><p>c]d</p> -> [<w>]<p>a(b</p>){</w><p>c}d</p> -> <w><p>a[b</p></w><p>c]d</p>
if ( sourceRange.containsPosition( this.start ) && this.containsPosition( sourceRange.end ) ) {
return [ new Range( ranges[ 2 ].start, ranges[ 1 ].end ) ];
}
// Range intersects (at the end) with wrapped element (<p>cd</p>).
// <p>a[b</p><p>c]d</p> -> <p>a[b</p><p>c]d</p><w></w> -> <p>a[b</p>]<w>{<p>c}d</p></w> -> <p>a[b</p><w><p>c]d</p></w>
else if ( sourceRange.containsPosition( this.end ) && this.containsPosition( sourceRange.start ) ) {
return [ new Range( ranges[ 0 ].start, ranges[ 1 ].end ) ];
}
} else if ( deltaType == 'unwrap' ) {
// Range intersects (at the beginning) with unwrapped element (<w></w>).
// <w><p>a[b</p></w><p>c]d</p> -> <p>a{b</p>}<w>[</w><p>c]d</p> -> <p>a[b</p><w></w><p>c]d</p>
// <w></w> is removed in next operation, but the remove does not mess up ranges.
if ( sourceRange.containsPosition( this.start ) && this.containsPosition( sourceRange.end ) ) {
return [ new Range( ranges[ 1 ].start, ranges[ 0 ].end ) ];
}
// Range intersects (at the end) with unwrapped element (<w></w>).
// <p>a[b</p><w><p>c]d</p></w> -> <p>a[b</p>](<p>c)d</p>{<w>}</w> -> <p>a[b</p><p>c]d</p><w></w>
// <w></w> is removed in next operation, but the remove does not mess up ranges.
else if ( sourceRange.containsPosition( this.end ) && this.containsPosition( sourceRange.start ) ) {
return [ new Range( ranges[ 0 ].start, ranges[ 2 ].end ) ];
}
}
}

return ranges;
Expand Down Expand Up @@ -652,11 +690,10 @@ export default class Range {
* Combines all ranges from the passed array into a one range. At least one range has to be passed.
* Passed ranges must not have common parts.
*
* The first range from the array is a reference range. If other ranges starts or ends on the exactly same position where
* The first range from the array is a reference range. If other ranges start or end on the exactly same position where
* the reference range, they get combined into one range.
*
* [ ][] [ ][ ][ ref range ][ ][] [ ] // Passed ranges, shown sorted. "Ref range" was the first range in original array.
* [ returned range ] [ ] // The combined range.
* [ ][] [ ][ ][ ][ ][] [ ] // Passed ranges, shown sorted
* [ ] // The result of the function if the first range was a reference range.
* [ ] // The result of the function if the third-to-seventh range was a reference range.
* [ ] // The result of the function if the last range was a reference range.
Expand Down
153 changes: 145 additions & 8 deletions tests/model/delta/transform/markerdelta.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ import {
expectDelta,
getFilledDocument,
getMarkerDelta,
getSplitDelta
getSplitDelta,
getMergeDelta,
getWrapDelta,
getUnwrapDelta
} from '../../../model/delta/transform/_utils/utils';

describe( 'transform', () => {
Expand All @@ -33,16 +36,16 @@ describe( 'transform', () => {
} );

describe( 'MarkerDelta by', () => {
let markerDelta;
describe( 'SplitDelta', () => {
let markerDelta;

beforeEach( () => {
const oldRange = new Range( new Position( root, [ 3, 0 ] ), new Position( root, [ 3, 3 ] ) );
const newRange = new Range( new Position( root, [ 3, 3, 3, 2 ] ), new Position( root, [ 3, 3, 3, 6 ] ) );
beforeEach( () => {
const oldRange = new Range( new Position( root, [ 3, 0 ] ), new Position( root, [ 3, 3 ] ) );
const newRange = new Range( new Position( root, [ 3, 3, 3, 2 ] ), new Position( root, [ 3, 3, 3, 6 ] ) );

markerDelta = getMarkerDelta( 'name', oldRange, newRange, baseVersion );
} );
markerDelta = getMarkerDelta( 'name', oldRange, newRange, baseVersion );
} );

describe( 'SplitDelta', () => {
it( 'split inside oldRange', () => {
let splitDelta = getSplitDelta( new Position( root, [ 3, 1 ] ), new Element( 'div' ), 3, baseVersion );
let transformed = transform( markerDelta, splitDelta );
Expand Down Expand Up @@ -93,5 +96,139 @@ describe( 'transform', () => {
} );
} );
} );

describe( 'MergeDelta', () => {
it( 'collapsed marker in merged element', () => {
// MarkerDelta with collapsed range, which changes from the beginning of merged element to the end.
const oldRange = new Range( new Position( root, [ 3, 3, 3, 0 ] ) );
const newRange = new Range( new Position( root, [ 3, 3, 3, 12 ] ) );

const markerDelta = getMarkerDelta( 'name', oldRange, newRange, baseVersion );

// MergeDelta merges the element in which is collapsed marker range with the previous element.
const mergeDelta = getMergeDelta( new Position( root, [ 3, 3, 3 ] ), 4, 12, baseVersion );

const transformed = transform( markerDelta, mergeDelta );

// It is expected, that ranges in MarkerDelta got correctly transformed:
// from start of merged element to the place where merged nodes where moved in the previous element,
// from end of merged element to the end of previous element.
const expectedOldRange = new Range( new Position( root, [ 3, 3, 2, 4 ] ), new Position( root, [ 3, 3, 2, 4 ] ) );
const expectedNewRange = new Range( new Position( root, [ 3, 3, 2, 16 ] ), new Position( root, [ 3, 3, 2, 16 ] ) );

expectDelta( transformed[ 0 ], {
type: MarkerDelta,
operations: [
{
type: MarkerOperation,
name: 'name',
oldRange: expectedOldRange,
newRange: expectedNewRange,
baseVersion: baseVersion + 2
}
]
} );
} );
} );

describe( 'WrapDelta', () => {
it( 'ranges intersecting with wrapped range', () => {
// MarkerDelta with ranges that intersects with wrapped range.
const oldRange = new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 1, 2 ] ) );
const newRange = new Range( new Position( root, [ 1, 2 ] ), new Position( root, [ 2, 2 ] ) );

const markerDelta = getMarkerDelta( 'name', oldRange, newRange, baseVersion );

// Wrap delta wraps element on position ( root [ 1 ] ), which intersects with both `oldRange` and `newRange`.
const wrapElement = new Element( 'w' );
const wrapRange = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) );
const wrapDelta = getWrapDelta( wrapRange, wrapElement, baseVersion );

const transformed = transform( markerDelta, wrapDelta );

// It is expected, that ranges in MarkerDelta got correctly transformed:
// `oldRange` end is in wrapped element,
// `newRange` start is in wrapped element.
const expectedOldRange = new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 1, 0, 2 ] ) );
const expectedNewRange = new Range( new Position( root, [ 1, 0, 2 ] ), new Position( root, [ 2, 2 ] ) );

expectDelta( transformed[ 0 ], {
type: MarkerDelta,
operations: [
{
type: MarkerOperation,
name: 'name',
oldRange: expectedOldRange,
newRange: expectedNewRange,
baseVersion: baseVersion + 2
}
]
} );
} );
} );

describe( 'UnwrapDelta', () => {
it( 'ranges intersecting with unwrapped element', () => {
// MarkerDelta with ranges that intersects with unwrapped element.
const oldRange = new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 1, 0, 2 ] ) );
const newRange = new Range( new Position( root, [ 1, 0, 2 ] ), new Position( root, [ 2, 2 ] ) );

const markerDelta = getMarkerDelta( 'name', oldRange, newRange, baseVersion );

// Unwrap delta unwraps element on position ( root [ 1, 0 ] ), which intersects with both `oldRange` and `newRange`.
const unwrapPosition = new Position( root, [ 1, 0 ] );
const unwrapDelta = getUnwrapDelta( unwrapPosition, 4, baseVersion );

const transformed = transform( markerDelta, unwrapDelta );

// It is expected, that ranges in MarkerDelta got correctly transformed.
const expectedOldRange = new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 1, 2 ] ) );
const expectedNewRange = new Range( new Position( root, [ 1, 2 ] ), new Position( root, [ 2, 2 ] ) );

expectDelta( transformed[ 0 ], {
type: MarkerDelta,
operations: [
{
type: MarkerOperation,
name: 'name',
oldRange: expectedOldRange,
newRange: expectedNewRange,
baseVersion: baseVersion + 2
}
]
} );
} );

it( 'ranges intersecting with unwrapped element #2', () => {
// MarkerDelta with ranges that intersects with unwrapped element.
const oldRange = new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 1, 2 ] ) );
const newRange = new Range( new Position( root, [ 1, 2 ] ), new Position( root, [ 2, 2 ] ) );

const markerDelta = getMarkerDelta( 'name', oldRange, newRange, baseVersion );

// Unwrap delta unwraps element on position ( root [ 1 ] ), which intersects with both `oldRange` and `newRange`.
const unwrapPosition = new Position( root, [ 1 ] );
const unwrapDelta = getUnwrapDelta( unwrapPosition, 4, baseVersion );

const transformed = transform( markerDelta, unwrapDelta );

// It is expected, that ranges in MarkerDelta got correctly transformed.
const expectedOldRange = new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 3 ] ) );
const expectedNewRange = new Range( new Position( root, [ 3 ] ), new Position( root, [ 5, 2 ] ) );

expectDelta( transformed[ 0 ], {
type: MarkerDelta,
operations: [
{
type: MarkerOperation,
name: 'name',
oldRange: expectedOldRange,
newRange: expectedNewRange,
baseVersion: baseVersion + 2
}
]
} );
} );
} );
} );
} );
Loading

0 comments on commit efe3987

Please sign in to comment.