Jelajahi Sumber

Merge branch 'master' into t/ckeditor5-typing/101

Piotrek Koszuliński 8 tahun lalu
induk
melakukan
93e3ccf3b4

+ 37 - 38
packages/ckeditor5-engine/src/conversion/buildmodelconverter.js

@@ -15,8 +15,8 @@ import {
 	removeUIElement,
 	removeUIElement,
 	wrapItem,
 	wrapItem,
 	unwrapItem,
 	unwrapItem,
-	convertTextsInsideMarker,
-	convertElementsInsideMarker
+	highlightText,
+	highlightElement
 } from './model-to-view-converters';
 } from './model-to-view-converters';
 
 
 import { convertSelectionAttribute, convertSelectionMarker } from './model-selection-to-view-converters';
 import { convertSelectionAttribute, convertSelectionMarker } from './model-selection-to-view-converters';
@@ -58,11 +58,11 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  *
  *
  *		buildModelConverter().for( dispatcher ).fromAttribute( 'bold' ).toElement( 'strong' );
  *		buildModelConverter().for( dispatcher ).fromAttribute( 'bold' ).toElement( 'strong' );
  *
  *
- * 4. Model marker to virtual selection converter. This is a converter that converts model markers to virtual
- * selection described by {@link module:engine/conversion/buildmodelconverter~VirtualSelectionDescriptor} object passed to
- * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toVirtualSelection} method.
+ * 4. Model marker to view highlight converter. This is a converter that converts model markers to view highlight
+ * described by {@link module:engine/conversion/buildmodelconverter~HighlightDescriptor} object passed to
+ * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toHighlight} method.
  *
  *
- *		buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toVirtualSelection( {
+ *		buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toHighlight( {
  *			class: 'search',
  *			class: 'search',
  *			priority: 20
  *			priority: 20
  *		} );
  *		} );
@@ -76,7 +76,7 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  * It is possible to provide various different parameters for
  * It is possible to provide various different parameters for
  * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toElement},
  * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toElement},
  * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toAttribute} and
  * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toAttribute} and
- * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toVirtualSelection} methods.
+ * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toHighlight} methods.
  * See their descriptions to learn more.
  * See their descriptions to learn more.
  *
  *
  * It is also possible to {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#withPriority change default priority}
  * It is also possible to {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#withPriority change default priority}
@@ -274,66 +274,65 @@ class ModelConverterBuilder {
 	}
 	}
 
 
 	/**
 	/**
-	 * Registers that marker should be converted to virtual selection. Markers, basically,
-	 * are {@link module:engine/model/liverange~LiveRange} instances, that are named. Virtual selection is
+	 * Registers that marker should be converted to view highlight. Markers, basically,
+	 * are {@link module:engine/model/liverange~LiveRange} instances, that are named. View highlight is
 	 * a representation of the model marker in the view:
 	 * a representation of the model marker in the view:
 	 * * each {@link module:engine/view/text~Text view text node} in the marker's range will be wrapped with `span`
 	 * * each {@link module:engine/view/text~Text view text node} in the marker's range will be wrapped with `span`
 	 * {@link module:engine/view/attributeelement~AttributeElement},
 	 * {@link module:engine/view/attributeelement~AttributeElement},
 	 * * each {@link module:engine/view/containerelement~ContainerElement container view element} in the marker's
 	 * * each {@link module:engine/view/containerelement~ContainerElement container view element} in the marker's
-	 * range can handle the virtual selection individually by providing `setVirtualSelection` and `removeVirtualSelection`
+	 * range can handle highlighting individually by providing `addHighlight` and `removeHighlight`
 	 * custom properties:
 	 * custom properties:
 	 *
 	 *
-	 *		viewElement.setCustomProperty( 'setVirtualSelection', ( element, descriptor ) => {} );
-	 *		viewElement.setCustomProperty( 'removeVirtualSelection', ( element, descriptor ) => {} );
+	 *		viewElement.setCustomProperty( 'addHighlight', ( element, descriptor ) => {} );
+	 *		viewElement.setCustomProperty( 'removeHighlight', ( element, descriptor ) => {} );
 	 *
 	 *
-	 * {@link module:engine/conversion/buildmodelconverter~VirtualSelectionDescriptor Descriptor} will be used to create
-	 * spans over text nodes and also will be provided to `setVirtualSelection` and `removeVirtualSelection` methods
-	 * each time virtual selection should be set or removed from view elements.
-	 * NOTE: When `setVirtualSelection` and `removeVirtualSelection` custom properties are present, converter assumes
-	 * that element itself is taking care of presenting virtual selection on its child nodes, so it won't convert virtual
-	 * selection on them.
+	 * {@link module:engine/conversion/buildmodelconverter~HighlightDescriptor} will be used to create
+	 * spans over text nodes and also will be provided to `addHighlight` and `removeHighlight` methods
+	 * each time highlight should be set or removed from view elements.
+	 * NOTE: When `addHighlight` and `removeHighlight` custom properties are present, converter assumes
+	 * that element itself is taking care of presenting highlight on its child nodes, so it won't convert them.
 	 *
 	 *
-	 * Virtual selection descriptor can be provided as plain object:
+	 * Highlight descriptor can be provided as plain object:
 	 *
 	 *
-	 *		buildModelConverter.for( dispatcher ).fromMarker( 'search' ).toVirtualSelection( { class: 'search-mark' } );
+	 *		buildModelConverter.for( dispatcher ).fromMarker( 'search' ).toHighlight( { class: 'search-highlight' } );
  	 *
  	 *
 	 * Also, descriptor creator function can be provided:
 	 * Also, descriptor creator function can be provided:
 	 *
 	 *
-	 *		buildModelConverter.for( dispatcher ).fromMarker( 'search:blue' ).toVirtualSelection( data => {
+	 *		buildModelConverter.for( dispatcher ).fromMarker( 'search:blue' ).toHighlight( data => {
 	 *			const color = data.markerName.split( ':' )[ 1 ];
 	 *			const color = data.markerName.split( ':' )[ 1 ];
 	 *
 	 *
 	 *			return { class: 'search-' + color };
 	 *			return { class: 'search-' + color };
 	 *		} );
 	 *		} );
 	 *
 	 *
 	 * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
 	 * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
-	 * `build-model-converter-non-marker-to-virtual-selection` when trying to convert not from marker.
+	 * `build-model-converter-non-marker-to-highlight` when trying to convert not from marker.
 	 *
 	 *
-	 * @param {function|module:engine/conversion/buildmodelconverter~VirtualSelectionDescriptor} selectionDescriptor
+	 * @param {function|module:engine/conversion/buildmodelconverter~HighlightDescriptor} highlightDescriptor
 	 */
 	 */
-	toVirtualSelection( selectionDescriptor ) {
+	toHighlight( highlightDescriptor ) {
 		const priority = this._from.priority === null ? 'normal' : this._from.priority;
 		const priority = this._from.priority === null ? 'normal' : this._from.priority;
 
 
 		if ( this._from.type != 'marker' ) {
 		if ( this._from.type != 'marker' ) {
 			/**
 			/**
-			 * To virtual selection conversion is supported only for model markers.
+			 * To highlight conversion is supported only for model markers.
 			 *
 			 *
-			 * @error build-model-converter-non-marker-to-virtual-selection
+			 * @error build-model-converter-non-marker-to-highlight
 			 */
 			 */
 			throw new CKEditorError(
 			throw new CKEditorError(
-				'build-model-converter-non-marker-to-virtual-selection: Conversion to virtual selection is supported ' +
+				'build-model-converter-non-marker-to-highlight: Conversion to highlight is supported ' +
 				'only from model markers.'
 				'only from model markers.'
 			);
 			);
 		}
 		}
 
 
 		for ( const dispatcher of this._dispatchers ) {
 		for ( const dispatcher of this._dispatchers ) {
 			// Separate converters for converting texts and elements inside marker's range.
 			// Separate converters for converting texts and elements inside marker's range.
-			dispatcher.on( 'addMarker:' + this._from.name, convertTextsInsideMarker( selectionDescriptor ), { priority } );
-			dispatcher.on( 'addMarker:' + this._from.name, convertElementsInsideMarker( selectionDescriptor ), { priority } );
+			dispatcher.on( 'addMarker:' + this._from.name, highlightText( highlightDescriptor ), { priority } );
+			dispatcher.on( 'addMarker:' + this._from.name, highlightElement( highlightDescriptor ), { priority } );
 
 
-			dispatcher.on( 'removeMarker:' + this._from.name, convertTextsInsideMarker( selectionDescriptor ), { priority } );
-			dispatcher.on( 'removeMarker:' + this._from.name, convertElementsInsideMarker( selectionDescriptor ), { priority } );
+			dispatcher.on( 'removeMarker:' + this._from.name, highlightText( highlightDescriptor ), { priority } );
+			dispatcher.on( 'removeMarker:' + this._from.name, highlightElement( highlightDescriptor ), { priority } );
 
 
-			dispatcher.on( 'selectionMarker:' + this._from.name, convertSelectionMarker( selectionDescriptor ), { priority } );
+			dispatcher.on( 'selectionMarker:' + this._from.name, convertSelectionMarker( highlightDescriptor ), { priority } );
 		}
 		}
 	}
 	}
 
 
@@ -434,16 +433,16 @@ export default function buildModelConverter() {
  */
  */
 
 
 /**
 /**
- * @typedef VirtualSelectionDescriptor
- * Object describing how virtual selection should be created in the view. Each text node in virtual selection
+ * @typedef HighlightDescriptor
+ * Object describing how content highlight should be created in the view. Each text node contained in highlight
  * will be wrapped with `span` element with CSS class, attributes and priority described by this object. Each element
  * will be wrapped with `span` element with CSS class, attributes and priority described by this object. Each element
- * can handle virtual selection separately by providing `setVirtualSelection` and `removeVirtualSelection` custom
+ * can handle displaying highlight separately by providing `addHighlight` and `removeHighlight` custom
  * properties.
  * properties.
  *
  *
  * @property {String} class CSS class that will be added to `span`
  * @property {String} class CSS class that will be added to `span`
- * {@link module:engine/view/attributeelement~AttributeElement} wrapping each text node in the virtual selection.
+ * {@link module:engine/view/attributeelement~AttributeElement} wrapping each text node in the highlighted content.
  * @property {Number} [priority] {@link module:engine/view/attributeelement~AttributeElement#priority} of the `span`
  * @property {Number} [priority] {@link module:engine/view/attributeelement~AttributeElement#priority} of the `span`
- * wrapping each text node in the virtual selection. If not provided, default 10 priority will be used.
+ * wrapping each text node in the highlighted content. If not provided, default 10 priority will be used.
  * @property {Object} [attributes] Attributes that will be added to `span`
  * @property {Object} [attributes] Attributes that will be added to `span`
- * {@link module:engine/view/attributeelement~AttributeElement} wrapping each text node it the virtual selection.
+ * {@link module:engine/view/attributeelement~AttributeElement} wrapping each text node it the highlighted content.
  */
  */

+ 8 - 8
packages/ckeditor5-engine/src/conversion/model-selection-to-view-converters.js

@@ -6,7 +6,7 @@
 import ViewElement from '../view/element';
 import ViewElement from '../view/element';
 import ViewRange from '../view/range';
 import ViewRange from '../view/range';
 import viewWriter from '../view/writer';
 import viewWriter from '../view/writer';
-import { virtualSelectionDescriptorToAttributeElement } from './model-to-view-converters';
+import { highlightDescriptorToAttributeElement } from './model-to-view-converters';
 
 
 /**
 /**
  * Contains {@link module:engine/model/selection~Selection model selection} to
  * Contains {@link module:engine/model/selection~Selection model selection} to
@@ -162,21 +162,21 @@ export function convertSelectionAttribute( elementCreator ) {
  *		modelDispatcher.on( 'selectionMarker:searchResult', convertSelectionMarker( { class: 'search' } ) );
  *		modelDispatcher.on( 'selectionMarker:searchResult', convertSelectionMarker( { class: 'search' } ) );
  *
  *
  * @see module:engine/conversion/model-selection-to-view-converters~convertSelectionAttribute
  * @see module:engine/conversion/model-selection-to-view-converters~convertSelectionAttribute
- * @param {module:engine/conversion/buildmodelconverter~VirtualSelectionDescriptor|Function} selectionDescriptor Virtual
- * selection descriptor object or function returning a descriptor object.
+ * @param {module:engine/conversion/buildmodelconverter~HighlightDescriptor|Function} highlightDescriptor Highlight
+ * descriptor object or function returning a descriptor object.
  * @returns {Function} Selection converter.
  * @returns {Function} Selection converter.
  */
  */
-export function convertSelectionMarker( selectionDescriptor ) {
+export function convertSelectionMarker( highlightDescriptor ) {
 	return ( evt, data, consumable, conversionApi ) => {
 	return ( evt, data, consumable, conversionApi ) => {
-		const descriptor = typeof selectionDescriptor == 'function' ?
-			selectionDescriptor( data, consumable, conversionApi ) :
-			selectionDescriptor;
+		const descriptor = typeof highlightDescriptor == 'function' ?
+			highlightDescriptor( data, consumable, conversionApi ) :
+			highlightDescriptor;
 
 
 		if ( !descriptor ) {
 		if ( !descriptor ) {
 			return;
 			return;
 		}
 		}
 
 
-		const viewElement = virtualSelectionDescriptorToAttributeElement( descriptor );
+		const viewElement = highlightDescriptorToAttributeElement( descriptor );
 		const consumableName = 'selectionMarker:' + data.markerName;
 		const consumableName = 'selectionMarker:' + data.markerName;
 
 
 		wrapCollapsedSelectionPosition( data.selection, conversionApi.viewSelection, viewElement, consumable, consumableName );
 		wrapCollapsedSelectionPosition( data.selection, conversionApi.viewSelection, viewElement, consumable, consumableName );

+ 24 - 24
packages/ckeditor5-engine/src/conversion/model-to-view-converters.js

@@ -407,16 +407,16 @@ export function remove() {
 /**
 /**
  * Function factory, creates converter that converts all texts inside marker's range. Converter wraps each text with
  * Function factory, creates converter that converts all texts inside marker's range. Converter wraps each text with
  * {@link module:engine/view/attributeelement~AttributeElement} created from provided descriptor.
  * {@link module:engine/view/attributeelement~AttributeElement} created from provided descriptor.
- * See {link module:engine/conversion/model-to-view-converters~virtualSelectionDescriptorToAttributeElement}.
+ * See {link module:engine/conversion/model-to-view-converters~highlightDescriptorToAttributeElement}.
  *
  *
- * @param {module:engine/conversion/buildmodelconverter~VirtualSelectionDescriptor|Function} selectionDescriptor
+ * @param {module:engine/conversion/buildmodelconverter~HighlightDescriptor|Function} highlightDescriptor
  * @return {Function}
  * @return {Function}
  */
  */
-export function convertTextsInsideMarker( selectionDescriptor ) {
+export function highlightText( highlightDescriptor ) {
 	return ( evt, data, consumable, conversionApi ) => {
 	return ( evt, data, consumable, conversionApi ) => {
-		const descriptor = typeof selectionDescriptor == 'function' ?
-			selectionDescriptor( data, consumable, conversionApi ) :
-			selectionDescriptor;
+		const descriptor = typeof highlightDescriptor == 'function' ?
+			highlightDescriptor( data, consumable, conversionApi ) :
+			highlightDescriptor;
 
 
 		const modelItem = data.item;
 		const modelItem = data.item;
 
 
@@ -428,7 +428,7 @@ export function convertTextsInsideMarker( selectionDescriptor ) {
 			return;
 			return;
 		}
 		}
 
 
-		const viewElement = virtualSelectionDescriptorToAttributeElement( descriptor );
+		const viewElement = highlightDescriptorToAttributeElement( descriptor );
 		const viewRange = conversionApi.mapper.toViewRange( data.range );
 		const viewRange = conversionApi.mapper.toViewRange( data.range );
 
 
 		if ( evt.name.split( ':' )[ 0 ] == 'addMarker' ) {
 		if ( evt.name.split( ':' )[ 0 ] == 'addMarker' ) {
@@ -441,23 +441,23 @@ export function convertTextsInsideMarker( selectionDescriptor ) {
 
 
 /**
 /**
  * Function factory, creates converter that converts all elements inside marker's range. Converter checks if element has
  * Function factory, creates converter that converts all elements inside marker's range. Converter checks if element has
- * functions stored under `setVirtualSelection` and `removeVirtualSelection` custom properties and calls them passing
- * {@link module:engine/conversion/buildmodelconverter~VirtualSelectionDescriptor}. In such case converter will consume
- * all element's children, assuming that they were handled by element itself. If selection descriptor will not provide
+ * functions stored under `addHighlight` and `removeHighlight` custom properties and calls them passing
+ * {@link module:engine/conversion/buildmodelconverter~HighlightDescriptor}. In such case converter will consume
+ * all element's children, assuming that they were handled by element itself. If highlight descriptor will not provide
  * priority, priority 10 will be used as default, to be compliant with
  * priority, priority 10 will be used as default, to be compliant with
- * {@link module:engine/conversion/model-to-view-converters~convertTextsInsideMarker} method which uses default priority of
+ * {@link module:engine/conversion/model-to-view-converters~highlightText} method which uses default priority of
  * {@link module:engine/view/attributeelement~AttributeElement}.
  * {@link module:engine/view/attributeelement~AttributeElement}.
- * When `setVirtualSelection` and `removeVirtualSelection` custom properties are not present, element is not converted
+ * When `addHighlight` and `removeHighlight` custom properties are not present, element is not converted
  * in any special way. This means that converters will proceed to convert element's child nodes.
  * in any special way. This means that converters will proceed to convert element's child nodes.
  *
  *
- * @param {module:engine/conversion/buildmodelconverter~VirtualSelectionDescriptor|Function} selectionDescriptor
+ * @param {module:engine/conversion/buildmodelconverter~HighlightDescriptor|Function} highlightDescriptor
  * @return {Function}
  * @return {Function}
  */
  */
-export function convertElementsInsideMarker( selectionDescriptor ) {
+export function highlightElement( highlightDescriptor ) {
 	return ( evt, data, consumable, conversionApi ) => {
 	return ( evt, data, consumable, conversionApi ) => {
-		const descriptor = typeof selectionDescriptor == 'function' ?
-			selectionDescriptor( data, consumable, conversionApi ) :
-			selectionDescriptor;
+		const descriptor = typeof highlightDescriptor == 'function' ?
+			highlightDescriptor( data, consumable, conversionApi ) :
+			highlightDescriptor;
 
 
 		const modelItem = data.item;
 		const modelItem = data.item;
 
 
@@ -475,9 +475,9 @@ export function convertElementsInsideMarker( selectionDescriptor ) {
 
 
 		const viewElement = conversionApi.mapper.toViewElement( modelItem );
 		const viewElement = conversionApi.mapper.toViewElement( modelItem );
 		const addMarker = evt.name.split( ':' )[ 0 ] == 'addMarker';
 		const addMarker = evt.name.split( ':' )[ 0 ] == 'addMarker';
-		const selectionHandlingMethod = addMarker ? 'setVirtualSelection' : 'removeVirtualSelection';
+		const highlightHandlingMethod = addMarker ? 'addHighlight' : 'removeHighlight';
 
 
-		if ( viewElement && viewElement.getCustomProperty( selectionHandlingMethod ) ) {
+		if ( viewElement && viewElement.getCustomProperty( highlightHandlingMethod ) ) {
 			// Consume element itself.
 			// Consume element itself.
 			consumable.consume( data.item, evt.name );
 			consumable.consume( data.item, evt.name );
 
 
@@ -486,7 +486,7 @@ export function convertElementsInsideMarker( selectionDescriptor ) {
 				consumable.consume( value.item, evt.name );
 				consumable.consume( value.item, evt.name );
 			}
 			}
 
 
-			viewElement.getCustomProperty( selectionHandlingMethod )( viewElement, descriptor );
+			viewElement.getCustomProperty( highlightHandlingMethod )( viewElement, descriptor );
 		}
 		}
 	};
 	};
 }
 }
@@ -559,13 +559,13 @@ export function eventNameToConsumableType( evtName ) {
 
 
 /**
 /**
  * Creates `span` {@link module:engine/view/attributeelement~AttributeElement view attribute element} from information
  * Creates `span` {@link module:engine/view/attributeelement~AttributeElement view attribute element} from information
- * provided by {@link module:engine/conversion/buildmodelconverter~VirtualSelectionDescriptor} object. If priority
- * is not provided in selection descriptor - default priority will be used.
+ * provided by {@link module:engine/conversion/buildmodelconverter~HighlightDescriptor} object. If priority
+ * is not provided in descriptor - default priority will be used.
  *
  *
- * @param {module:engine/conversion/buildmodelconverter~VirtualSelectionDescriptor } descriptor
+ * @param {module:engine/conversion/buildmodelconverter~HighlightDescriptor } descriptor
  * @return {module:engine/view/attributeelement~AttributeElement}
  * @return {module:engine/view/attributeelement~AttributeElement}
  */
  */
-export function virtualSelectionDescriptorToAttributeElement( descriptor ) {
+export function highlightDescriptorToAttributeElement( descriptor ) {
 	const attributeElement = new ViewAttributeElement( 'span', descriptor.attributes );
 	const attributeElement = new ViewAttributeElement( 'span', descriptor.attributes );
 
 
 	if ( descriptor.class ) {
 	if ( descriptor.class ) {

+ 4 - 1
packages/ckeditor5-engine/src/dev-utils/enableenginedebug.js

@@ -414,7 +414,10 @@ function enableLoggingTools() {
 
 
 	SplitDelta.prototype.toString = function() {
 	SplitDelta.prototype.toString = function() {
 		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
 		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
-			this.position.toString();
+			( this.position ?
+				this.position.toString() :
+				`(clone to ${ this._cloneOperation.position || this._cloneOperation.targetPosition })`
+			);
 	};
 	};
 
 
 	UnwrapDelta.prototype.toString = function() {
 	UnwrapDelta.prototype.toString = function() {

+ 56 - 8
packages/ckeditor5-engine/src/model/delta/basic-transformations.js

@@ -50,6 +50,11 @@ addTransformationCase( AttributeDelta, WeakInsertDelta, ( a, b, context ) => {
 
 
 // Add special case for AttributeDelta x SplitDelta transformation.
 // Add special case for AttributeDelta x SplitDelta transformation.
 addTransformationCase( AttributeDelta, SplitDelta, ( a, b, context ) => {
 addTransformationCase( AttributeDelta, SplitDelta, ( a, b, context ) => {
+	// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
+	if ( !b.position ) {
+		return defaultTransform( a, b, context );
+	}
+
 	const splitPosition = new Position( b.position.root, b.position.path.slice( 0, -1 ) );
 	const splitPosition = new Position( b.position.root, b.position.path.slice( 0, -1 ) );
 
 
 	const deltas = defaultTransform( a, b, context );
 	const deltas = defaultTransform( a, b, context );
@@ -173,6 +178,11 @@ addTransformationCase( MergeDelta, MoveDelta, ( a, b, context ) => {
 
 
 // Add special case for SplitDelta x SplitDelta transformation.
 // Add special case for SplitDelta x SplitDelta transformation.
 addTransformationCase( SplitDelta, SplitDelta, ( a, b, context ) => {
 addTransformationCase( SplitDelta, SplitDelta, ( a, b, context ) => {
+	// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
+	if ( !a.position || !b.position ) {
+		return defaultTransform( a, b, context );
+	}
+
 	const pathA = a.position.getParentPath();
 	const pathA = a.position.getParentPath();
 	const pathB = b.position.getParentPath();
 	const pathB = b.position.getParentPath();
 
 
@@ -202,6 +212,11 @@ addTransformationCase( SplitDelta, SplitDelta, ( a, b, context ) => {
 
 
 // Add special case for SplitDelta x UnwrapDelta transformation.
 // Add special case for SplitDelta x UnwrapDelta transformation.
 addTransformationCase( SplitDelta, UnwrapDelta, ( a, b, context ) => {
 addTransformationCase( SplitDelta, UnwrapDelta, ( a, b, context ) => {
+	// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
+	if ( !a.position ) {
+		return defaultTransform( a, b, context );
+	}
+
 	// If incoming split delta tries to split a node that just got unwrapped, there is actually nothing to split,
 	// If incoming split delta tries to split a node that just got unwrapped, there is actually nothing to split,
 	// so we discard that delta.
 	// so we discard that delta.
 	if ( a.position.root == b.position.root && compareArrays( b.position.path, a.position.getParentPath() ) === 'same' ) {
 	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 ) => {
 
 
 // Add special case for SplitDelta x WrapDelta transformation.
 // Add special case for SplitDelta x WrapDelta transformation.
 addTransformationCase( SplitDelta, WrapDelta, ( a, b, context ) => {
 addTransformationCase( SplitDelta, WrapDelta, ( a, b, context ) => {
+	// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
+	if ( !a.position ) {
+		return defaultTransform( a, b, context );
+	}
+
 	// If split is applied at the position between wrapped nodes, we cancel the split as it's results may be unexpected and
 	// If split is applied at the position between wrapped nodes, we cancel the split as it's results may be unexpected and
 	// very weird. Even if we do some "magic" we don't know what really are users' expectations.
 	// very weird. Even if we do some "magic" we don't know what really are users' expectations.
 
 
@@ -264,7 +284,12 @@ addTransformationCase( SplitDelta, WrapDelta, ( a, b, context ) => {
 } );
 } );
 
 
 // Add special case for SplitDelta x WrapDelta transformation.
 // Add special case for SplitDelta x WrapDelta transformation.
-addTransformationCase( SplitDelta, AttributeDelta, ( a, b ) => {
+addTransformationCase( SplitDelta, AttributeDelta, ( a, b, context ) => {
+	// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
+	if ( !a.position ) {
+		return defaultTransform( a, b, context );
+	}
+
 	a = a.clone();
 	a = a.clone();
 
 
 	const splitPosition = new Position( a.position.root, a.position.path.slice( 0, -1 ) );
 	const splitPosition = new Position( a.position.root, a.position.path.slice( 0, -1 ) );
@@ -290,6 +315,11 @@ addTransformationCase( SplitDelta, AttributeDelta, ( a, b ) => {
 
 
 // Add special case for UnwrapDelta x SplitDelta transformation.
 // Add special case for UnwrapDelta x SplitDelta transformation.
 addTransformationCase( UnwrapDelta, SplitDelta, ( a, b, context ) => {
 addTransformationCase( UnwrapDelta, SplitDelta, ( a, b, context ) => {
+	// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
+	if ( !b.position ) {
+		return defaultTransform( a, b, context );
+	}
+
 	// If incoming unwrap delta tries to unwrap node that got split we should unwrap the original node and the split copy.
 	// If incoming unwrap delta tries to unwrap node that got split we should unwrap the original node and the split copy.
 	// This can be achieved either by reverting split and applying unwrap to singular node, or creating additional unwrap delta.
 	// This can be achieved either by reverting split and applying unwrap to singular node, or creating additional unwrap delta.
 	if ( a.position.root == b.position.root && compareArrays( a.position.path, b.position.getParentPath() ) === 'same' ) {
 	if ( a.position.root == b.position.root && compareArrays( a.position.path, b.position.getParentPath() ) === 'same' ) {
@@ -316,9 +346,13 @@ addTransformationCase( WeakInsertDelta, AttributeDelta, ( a, b ) => {
 
 
 // Add special case for WrapDelta x SplitDelta transformation.
 // Add special case for WrapDelta x SplitDelta transformation.
 addTransformationCase( WrapDelta, SplitDelta, ( a, b, context ) => {
 addTransformationCase( WrapDelta, SplitDelta, ( a, b, context ) => {
+	// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
+	if ( !b.position ) {
+		return defaultTransform( a, b, context );
+	}
+
 	// If incoming wrap delta tries to wrap range that contains split position, we have to cancel the split and apply
 	// If incoming wrap delta tries to wrap range that contains split position, we have to cancel the split and apply
 	// the wrap. Since split was already applied, we have to revert it.
 	// the wrap. Since split was already applied, we have to revert it.
-
 	const sameRoot = a.range.start.root == b.position.root;
 	const sameRoot = a.range.start.root == b.position.root;
 	const operateInSameParent = sameRoot && compareArrays( a.range.start.getParentPath(), b.position.getParentPath() ) === 'same';
 	const operateInSameParent = sameRoot && compareArrays( a.range.start.getParentPath(), b.position.getParentPath() ) === 'same';
 	const splitInsideWrapRange = a.range.start.offset < b.position.offset && a.range.end.offset >= b.position.offset;
 	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 ) => {
 // Add special case for RenameDelta x SplitDelta transformation.
 // Add special case for RenameDelta x SplitDelta transformation.
 addTransformationCase( RenameDelta, SplitDelta, ( a, b, context ) => {
 addTransformationCase( RenameDelta, SplitDelta, ( a, b, context ) => {
 	const undoMode = context.aWasUndone || context.bWasUndone;
 	const undoMode = context.aWasUndone || context.bWasUndone;
-	const posBeforeSplitParent = new Position( b.position.root, b.position.path.slice( 0, -1 ) );
+
+	// The "clone operation" may be `InsertOperation`, `ReinsertOperation`, `MoveOperation` or `NoOperation`.
+	// `MoveOperation` has `targetPosition` which we want to use. `NoOperation` has no `position` and we don't use special case then.
+	let insertPosition = b._cloneOperation.position || b._cloneOperation.targetPosition;
+
+	if ( insertPosition ) {
+		insertPosition = insertPosition.getShiftedBy( -1 );
+	}
 
 
 	const deltas = defaultTransform( a, b, context );
 	const deltas = defaultTransform( a, b, context );
 
 
-	if ( !undoMode && a.operations[ 0 ].position.isEqual( posBeforeSplitParent ) ) {
+	if ( insertPosition && !undoMode && a.operations[ 0 ].position.isEqual( insertPosition ) ) {
 		// If a node that has been split has it's name changed, we should also change name of
 		// If a node that has been split has it's name changed, we should also change name of
 		// the node created during splitting.
 		// the node created during splitting.
 		const additionalRenameDelta = a.clone();
 		const additionalRenameDelta = a.clone();
-		additionalRenameDelta.operations[ 0 ].position = posBeforeSplitParent.getShiftedBy( 1 );
+		additionalRenameDelta.operations[ 0 ].position = insertPosition.getShiftedBy( 1 );
 
 
 		deltas.push( additionalRenameDelta );
 		deltas.push( additionalRenameDelta );
 	}
 	}
@@ -368,12 +409,19 @@ addTransformationCase( RenameDelta, SplitDelta, ( a, b, context ) => {
 // Add special case for SplitDelta x RenameDelta transformation.
 // Add special case for SplitDelta x RenameDelta transformation.
 addTransformationCase( SplitDelta, RenameDelta, ( a, b, context ) => {
 addTransformationCase( SplitDelta, RenameDelta, ( a, b, context ) => {
 	const undoMode = context.aWasUndone || context.bWasUndone;
 	const undoMode = context.aWasUndone || context.bWasUndone;
-	const posBeforeSplitParent = new Position( a.position.root, a.position.path.slice( 0, -1 ) );
+
+	// The "clone operation" may be `InsertOperation`, `ReinsertOperation`, `MoveOperation` or `NoOperation`.
+	// `MoveOperation` has `targetPosition` which we want to use. `NoOperation` has no `position` and we don't use special case then.
+	let insertPosition = a._cloneOperation.position || a._cloneOperation.targetPosition;
+
+	if ( insertPosition ) {
+		insertPosition = insertPosition.getShiftedBy( -1 );
+	}
 
 
 	// If element to split had it's name changed, we have to reflect this by creating additional rename operation.
 	// If element to split had it's name changed, we have to reflect this by creating additional rename operation.
-	if ( !undoMode && b.operations[ 0 ].position.isEqual( posBeforeSplitParent ) ) {
+	if ( insertPosition && !undoMode && b.operations[ 0 ].position.isEqual( insertPosition ) ) {
 		const additionalRenameDelta = b.clone();
 		const additionalRenameDelta = b.clone();
-		additionalRenameDelta.operations[ 0 ].position = posBeforeSplitParent.getShiftedBy( 1 );
+		additionalRenameDelta.operations[ 0 ].position = insertPosition.getShiftedBy( 1 );
 
 
 		// `nodes` is a property that is available only if `SplitDelta` `a` has `InsertOperation`.
 		// `nodes` is a property that is available only if `SplitDelta` `a` has `InsertOperation`.
 		// `SplitDelta` may have `ReinsertOperation` instead of `InsertOperation`.
 		// `SplitDelta` may have `ReinsertOperation` instead of `InsertOperation`.

+ 20 - 20
packages/ckeditor5-engine/tests/conversion/buildmodelconverter.js

@@ -343,7 +343,7 @@ describe( 'Model converter builder', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'model marker to virtual selection converter', () => {
+	describe( 'model marker to highlight converter', () => {
 		let modelText, modelElement;
 		let modelText, modelElement;
 
 
 		beforeEach( () => {
 		beforeEach( () => {
@@ -358,11 +358,11 @@ describe( 'Model converter builder', () => {
 			mapper.bindElements( modelElement, viewElement );
 			mapper.bindElements( modelElement, viewElement );
 		} );
 		} );
 
 
-		it( 'using passed virtual selection descriptor object', () => {
-			buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toVirtualSelection( {
-				class: 'virtual-selection',
+		it( 'using passed highlight descriptor object', () => {
+			buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toHighlight( {
+				class: 'highlight',
 				priority: 3,
 				priority: 3,
-				attributes: { title: 'marker' }
+				attributes: { title: 'highlight title' }
 			} );
 			} );
 
 
 			dispatcher.convertMarker( 'addMarker', 'search', ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 4 ) );
 			dispatcher.convertMarker( 'addMarker', 'search', ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 4 ) );
@@ -371,7 +371,7 @@ describe( 'Model converter builder', () => {
 				'<div>' +
 				'<div>' +
 					'<p>' +
 					'<p>' +
 						'fo' +
 						'fo' +
-						'<span class="virtual-selection" title="marker">ob</span>' +
+						'<span class="highlight" title="highlight title">ob</span>' +
 						'ar' +
 						'ar' +
 					'</p>' +
 					'</p>' +
 				'</div>' );
 				'</div>' );
@@ -385,11 +385,11 @@ describe( 'Model converter builder', () => {
 			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
 			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
 		} );
 		} );
 
 
-		it( 'using passed virtual selection descriptor object creator', () => {
-			buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toVirtualSelection( () => ( {
-				class: 'virtual-selection',
+		it( 'using passed highlight descriptor object creator', () => {
+			buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toHighlight( () => ( {
+				class: 'highlight',
 				priority: 12,
 				priority: 12,
-				attributes: { title: 'marker' }
+				attributes: { title: 'highlight title' }
 			} ) );
 			} ) );
 
 
 			dispatcher.convertMarker( 'addMarker', 'search', ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 4 ) );
 			dispatcher.convertMarker( 'addMarker', 'search', ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 4 ) );
@@ -398,7 +398,7 @@ describe( 'Model converter builder', () => {
 				'<div>' +
 				'<div>' +
 					'<p>' +
 					'<p>' +
 						'fo' +
 						'fo' +
-						'<span class="virtual-selection" title="marker">ob</span>' +
+						'<span class="highlight" title="highlight title">ob</span>' +
 						'ar' +
 						'ar' +
 					'</p>' +
 					'</p>' +
 				'</div>' );
 				'</div>' );
@@ -413,8 +413,8 @@ describe( 'Model converter builder', () => {
 		} );
 		} );
 
 
 		it( 'should do nothing when marker range is collapsed', () => {
 		it( 'should do nothing when marker range is collapsed', () => {
-			buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toVirtualSelection( {
-				class: 'virtual-selection'
+			buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toHighlight( {
+				class: 'highlight'
 			} );
 			} );
 
 
 			dispatcher.convertMarker( 'addMarker', 'search', ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 2 ) );
 			dispatcher.convertMarker( 'addMarker', 'search', ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 2 ) );
@@ -429,11 +429,11 @@ describe( 'Model converter builder', () => {
 		} );
 		} );
 
 
 		it( 'should create converters with provided priority', () => {
 		it( 'should create converters with provided priority', () => {
-			buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toVirtualSelection( {
-				class: 'virtual-selection'
+			buildModelConverter().for( dispatcher ).fromMarker( 'search' ).toHighlight( {
+				class: 'highlight'
 			} );
 			} );
 
 
-			buildModelConverter().for( dispatcher ).fromMarker( 'search' ).withPriority( 'high' ).toVirtualSelection( {
+			buildModelConverter().for( dispatcher ).fromMarker( 'search' ).withPriority( 'high' ).toHighlight( {
 				class: 'override'
 				class: 'override'
 			} );
 			} );
 
 
@@ -451,14 +451,14 @@ describe( 'Model converter builder', () => {
 
 
 		it( 'should throw if trying to convert from attribute', () => {
 		it( 'should throw if trying to convert from attribute', () => {
 			expect( () => {
 			expect( () => {
-				buildModelConverter().for( dispatcher ).fromAttribute( 'bold' ).toVirtualSelection( { class: 'foo' } );
-			} ).to.throw( CKEditorError, /^build-model-converter-non-marker-to-virtual-selection/ );
+				buildModelConverter().for( dispatcher ).fromAttribute( 'bold' ).toHighlight( { class: 'foo' } );
+			} ).to.throw( CKEditorError, /^build-model-converter-non-marker-to-highlight/ );
 		} );
 		} );
 
 
 		it( 'should throw if trying to convert from element', () => {
 		it( 'should throw if trying to convert from element', () => {
 			expect( () => {
 			expect( () => {
-				buildModelConverter().for( dispatcher ).fromElement( 'paragraph' ).toVirtualSelection( { class: 'foo' } );
-			} ).to.throw( CKEditorError, /^build-model-converter-non-marker-to-virtual-selection/ );
+				buildModelConverter().for( dispatcher ).fromElement( 'paragraph' ).toHighlight( { class: 'foo' } );
+			} ).to.throw( CKEditorError, /^build-model-converter-non-marker-to-highlight/ );
 		} );
 		} );
 	} );
 	} );
 
 

+ 9 - 9
packages/ckeditor5-engine/tests/conversion/model-selection-to-view-converters.js

@@ -29,14 +29,14 @@ import {
 	insertElement,
 	insertElement,
 	insertText,
 	insertText,
 	wrapItem,
 	wrapItem,
-	convertTextsInsideMarker,
-	convertElementsInsideMarker
+	highlightText,
+	highlightElement
 } from '../../src/conversion/model-to-view-converters';
 } from '../../src/conversion/model-to-view-converters';
 
 
 import { stringify as stringifyView } from '../../src/dev-utils/view';
 import { stringify as stringifyView } from '../../src/dev-utils/view';
 import { setData as setModelData } from '../../src/dev-utils/model';
 import { setData as setModelData } from '../../src/dev-utils/model';
 
 
-const virtualSelectionDescriptor = { class: 'marker', priority: 1 };
+const highlightDescriptor = { class: 'marker', priority: 1 };
 
 
 describe( 'model-selection-to-view-converters', () => {
 describe( 'model-selection-to-view-converters', () => {
 	let dispatcher, mapper, modelDoc, modelRoot, modelSelection, viewDoc, viewRoot, viewSelection;
 	let dispatcher, mapper, modelDoc, modelRoot, modelSelection, viewDoc, viewRoot, viewSelection;
@@ -60,10 +60,10 @@ describe( 'model-selection-to-view-converters', () => {
 		dispatcher.on( 'insert:$text', insertText() );
 		dispatcher.on( 'insert:$text', insertText() );
 		dispatcher.on( 'addAttribute:bold', wrapItem( new ViewAttributeElement( 'strong' ) ) );
 		dispatcher.on( 'addAttribute:bold', wrapItem( new ViewAttributeElement( 'strong' ) ) );
 
 
-		dispatcher.on( 'addMarker:marker', convertTextsInsideMarker( virtualSelectionDescriptor ) );
-		dispatcher.on( 'addMarker:marker', convertElementsInsideMarker( virtualSelectionDescriptor ) );
-		dispatcher.on( 'removeMarker:marker', convertTextsInsideMarker( virtualSelectionDescriptor ) );
-		dispatcher.on( 'removeMarker:marker', convertElementsInsideMarker( virtualSelectionDescriptor ) );
+		dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) );
+		dispatcher.on( 'addMarker:marker', highlightElement( highlightDescriptor ) );
+		dispatcher.on( 'removeMarker:marker', highlightText( highlightDescriptor ) );
+		dispatcher.on( 'removeMarker:marker', highlightElement( highlightDescriptor ) );
 
 
 		// Default selection converters.
 		// Default selection converters.
 		dispatcher.on( 'selection', clearAttributes(), { priority: 'low' } );
 		dispatcher.on( 'selection', clearAttributes(), { priority: 'low' } );
@@ -80,7 +80,7 @@ describe( 'model-selection-to-view-converters', () => {
 			// Selection converters for selection attributes.
 			// Selection converters for selection attributes.
 			dispatcher.on( 'selectionAttribute:bold', convertSelectionAttribute( new ViewAttributeElement( 'strong' ) ) );
 			dispatcher.on( 'selectionAttribute:bold', convertSelectionAttribute( new ViewAttributeElement( 'strong' ) ) );
 			dispatcher.on( 'selectionAttribute:italic', convertSelectionAttribute( new ViewAttributeElement( 'em' ) ) );
 			dispatcher.on( 'selectionAttribute:italic', convertSelectionAttribute( new ViewAttributeElement( 'em' ) ) );
-			dispatcher.on( 'selectionMarker:marker', convertSelectionMarker( virtualSelectionDescriptor ) );
+			dispatcher.on( 'selectionMarker:marker', convertSelectionMarker( highlightDescriptor ) );
 		} );
 		} );
 
 
 		describe( 'range selection', () => {
 		describe( 'range selection', () => {
@@ -268,7 +268,7 @@ describe( 'model-selection-to-view-converters', () => {
 					.to.equal( '<div>f<span class="marker">o<strong>o</strong>[]<strong>b</strong>a</span>r</div>' );
 					.to.equal( '<div>f<span class="marker">o<strong>o</strong>[]<strong>b</strong>a</span>r</div>' );
 			} );
 			} );
 
 
-			it( 'in marker - using virtual selection descriptor creator', () => {
+			it( 'in marker - using highlight descriptor creator', () => {
 				dispatcher.on( 'selectionMarker:marker2', convertSelectionMarker(
 				dispatcher.on( 'selectionMarker:marker2', convertSelectionMarker(
 					data => ( { 'class': data.markerName } )
 					data => ( { 'class': data.markerName } )
 				) );
 				) );

+ 42 - 42
packages/ckeditor5-engine/tests/conversion/model-to-view-converters.js

@@ -28,9 +28,9 @@ import {
 	unwrapItem,
 	unwrapItem,
 	remove,
 	remove,
 	removeUIElement,
 	removeUIElement,
-	convertTextsInsideMarker,
-	convertElementsInsideMarker,
-	virtualSelectionDescriptorToAttributeElement
+	highlightText,
+	highlightElement,
+	highlightDescriptorToAttributeElement
 } from '../../src/conversion/model-to-view-converters';
 } from '../../src/conversion/model-to-view-converters';
 
 
 import { createRangeOnElementOnly } from '../../tests/model/_utils/utils';
 import { createRangeOnElementOnly } from '../../tests/model/_utils/utils';
@@ -82,10 +82,10 @@ describe( 'model-to-view-converters', () => {
 		return result;
 		return result;
 	}
 	}
 
 
-	describe( 'convertTextsInsideMarker()', () => {
+	describe( 'highlightText()', () => {
 		let modelElement1, modelElement2, markerRange;
 		let modelElement1, modelElement2, markerRange;
-		const virtualSelectionDescriptor = {
-			class: 'marker-class',
+		const highlightDescriptor = {
+			class: 'highlight-class',
 			priority: 7,
 			priority: 7,
 			attributes: { title: 'title' }
 			attributes: { title: 'title' }
 		};
 		};
@@ -105,8 +105,8 @@ describe( 'model-to-view-converters', () => {
 		} );
 		} );
 
 
 		it( 'should wrap and unwrap text nodes', () => {
 		it( 'should wrap and unwrap text nodes', () => {
-			dispatcher.on( 'addMarker:marker', convertTextsInsideMarker( virtualSelectionDescriptor ) );
-			dispatcher.on( 'removeMarker:marker', convertTextsInsideMarker( virtualSelectionDescriptor ) );
+			dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) );
+			dispatcher.on( 'removeMarker:marker', highlightText( highlightDescriptor ) );
 			dispatcher.convertInsertion( markerRange );
 			dispatcher.convertInsertion( markerRange );
 
 
 			modelDoc.markers.set( 'marker', markerRange );
 			modelDoc.markers.set( 'marker', markerRange );
@@ -115,10 +115,10 @@ describe( 'model-to-view-converters', () => {
 			expect( viewToString( viewRoot ) ).to.equal(
 			expect( viewToString( viewRoot ) ).to.equal(
 				'<div>' +
 				'<div>' +
 					'<p>' +
 					'<p>' +
-						'<span class="marker-class" title="title">foo</span>' +
+						'<span class="highlight-class" title="title">foo</span>' +
 					'</p>' +
 					'</p>' +
 					'<p>' +
 					'<p>' +
-						'<span class="marker-class" title="title">bar</span>' +
+						'<span class="highlight-class" title="title">bar</span>' +
 					'</p>' +
 					'</p>' +
 				'</div>'
 				'</div>'
 			);
 			);
@@ -131,10 +131,10 @@ describe( 'model-to-view-converters', () => {
 		it( 'should not convert marker on elements already consumed', () => {
 		it( 'should not convert marker on elements already consumed', () => {
 			const newDescriptor = { class: 'override-class' };
 			const newDescriptor = { class: 'override-class' };
 
 
-			dispatcher.on( 'addMarker:marker', convertTextsInsideMarker( virtualSelectionDescriptor ) );
-			dispatcher.on( 'addMarker:marker', convertTextsInsideMarker( newDescriptor ), { priority: 'high' } );
-			dispatcher.on( 'removeMarker:marker', convertTextsInsideMarker( virtualSelectionDescriptor ) );
-			dispatcher.on( 'removeMarker:marker', convertTextsInsideMarker( newDescriptor ), { priority: 'high' } );
+			dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) );
+			dispatcher.on( 'addMarker:marker', highlightText( newDescriptor ), { priority: 'high' } );
+			dispatcher.on( 'removeMarker:marker', highlightText( highlightDescriptor ) );
+			dispatcher.on( 'removeMarker:marker', highlightText( newDescriptor ), { priority: 'high' } );
 			dispatcher.convertInsertion( markerRange );
 			dispatcher.convertInsertion( markerRange );
 
 
 			modelDoc.markers.set( 'marker', markerRange );
 			modelDoc.markers.set( 'marker', markerRange );
@@ -157,8 +157,8 @@ describe( 'model-to-view-converters', () => {
 		} );
 		} );
 
 
 		it( 'should do nothing if descriptor is not provided', () => {
 		it( 'should do nothing if descriptor is not provided', () => {
-			dispatcher.on( 'addMarker:marker', convertTextsInsideMarker( () => null ) );
-			dispatcher.on( 'removeMarker:marker', convertTextsInsideMarker( () => null ) );
+			dispatcher.on( 'addMarker:marker', highlightText( () => null ) );
+			dispatcher.on( 'removeMarker:marker', highlightText( () => null ) );
 
 
 			dispatcher.convertInsertion( markerRange );
 			dispatcher.convertInsertion( markerRange );
 
 
@@ -171,10 +171,10 @@ describe( 'model-to-view-converters', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'convertElementsInsideMarker()', () => {
+	describe( 'highlightElement()', () => {
 		let modelElement1, modelElement2, markerRange;
 		let modelElement1, modelElement2, markerRange;
-		const virtualSelectionDescriptor = {
-			class: 'marker-class',
+		const highlightDescriptor = {
+			class: 'highlight-class',
 			priority: 7,
 			priority: 7,
 			attributes: { title: 'title' }
 			attributes: { title: 'title' }
 		};
 		};
@@ -193,9 +193,9 @@ describe( 'model-to-view-converters', () => {
 			markerRange = ModelRange.createIn( modelRoot );
 			markerRange = ModelRange.createIn( modelRoot );
 		} );
 		} );
 
 
-		it( 'should use setVirtualSelection and removeVirtualSelection on elements and not convert children nodes', () => {
-			dispatcher.on( 'addMarker:marker', convertElementsInsideMarker( virtualSelectionDescriptor ) );
-			dispatcher.on( 'removeMarker:marker', convertElementsInsideMarker( virtualSelectionDescriptor ) );
+		it( 'should use addHighlight and removeHighlight on elements and not convert children nodes', () => {
+			dispatcher.on( 'addMarker:marker', highlightElement( highlightDescriptor ) );
+			dispatcher.on( 'removeMarker:marker', highlightElement( highlightDescriptor ) );
 			dispatcher.on( 'insert:paragraph', insertElement( data => {
 			dispatcher.on( 'insert:paragraph', insertElement( data => {
 				// Use special converter only for first paragraph.
 				// Use special converter only for first paragraph.
 				if ( data.item == modelElement2 ) {
 				if ( data.item == modelElement2 ) {
@@ -204,16 +204,16 @@ describe( 'model-to-view-converters', () => {
 
 
 				const viewContainer = new ViewContainerElement( 'p' );
 				const viewContainer = new ViewContainerElement( 'p' );
 
 
-				viewContainer.setCustomProperty( 'setVirtualSelection', ( element, descriptor ) => {
-					element.addClass( 'virtual-selection-own-class' );
+				viewContainer.setCustomProperty( 'addHighlight', ( element, descriptor ) => {
+					element.addClass( 'highlight-own-class' );
 
 
-					expect( descriptor ).to.equal( virtualSelectionDescriptor );
+					expect( descriptor ).to.equal( highlightDescriptor );
 				} );
 				} );
 
 
-				viewContainer.setCustomProperty( 'removeVirtualSelection', ( element, descriptor ) => {
-					element.removeClass( 'virtual-selection-own-class' );
+				viewContainer.setCustomProperty( 'removeHighlight', ( element, descriptor ) => {
+					element.removeClass( 'highlight-own-class' );
 
 
-					expect( descriptor ).to.equal( virtualSelectionDescriptor );
+					expect( descriptor ).to.equal( highlightDescriptor );
 				} );
 				} );
 
 
 				return viewContainer;
 				return viewContainer;
@@ -225,7 +225,7 @@ describe( 'model-to-view-converters', () => {
 
 
 			expect( viewToString( viewRoot ) ).to.equal(
 			expect( viewToString( viewRoot ) ).to.equal(
 				'<div>' +
 				'<div>' +
-					'<p class="virtual-selection-own-class">' +
+					'<p class="highlight-own-class">' +
 						'foo' +
 						'foo' +
 					'</p>' +
 					'</p>' +
 					'<p>' +
 					'<p>' +
@@ -242,16 +242,16 @@ describe( 'model-to-view-converters', () => {
 		it( 'should not convert marker on elements already consumed', () => {
 		it( 'should not convert marker on elements already consumed', () => {
 			const newDescriptor = { class: 'override-class' };
 			const newDescriptor = { class: 'override-class' };
 
 
-			dispatcher.on( 'addMarker:marker', convertElementsInsideMarker( virtualSelectionDescriptor ) );
-			dispatcher.on( 'removeMarker:marker', convertElementsInsideMarker( virtualSelectionDescriptor ) );
+			dispatcher.on( 'addMarker:marker', highlightElement( highlightDescriptor ) );
+			dispatcher.on( 'removeMarker:marker', highlightElement( highlightDescriptor ) );
 
 
-			dispatcher.on( 'addMarker:marker', convertElementsInsideMarker( newDescriptor ), { priority: 'high' } );
-			dispatcher.on( 'removeMarker:marker', convertElementsInsideMarker( newDescriptor ), { priority: 'high' } );
+			dispatcher.on( 'addMarker:marker', highlightElement( newDescriptor ), { priority: 'high' } );
+			dispatcher.on( 'removeMarker:marker', highlightElement( newDescriptor ), { priority: 'high' } );
 
 
 			dispatcher.on( 'insert:paragraph', insertElement( () => {
 			dispatcher.on( 'insert:paragraph', insertElement( () => {
 				const element = new ViewContainerElement( 'p' );
 				const element = new ViewContainerElement( 'p' );
-				element.setCustomProperty( 'setVirtualSelection', ( element, data ) => element.addClass( data.class ) );
-				element.setCustomProperty( 'removeVirtualSelection', ( element, data ) => element.removeClass( data.class ) );
+				element.setCustomProperty( 'addHighlight', ( element, data ) => element.addClass( data.class ) );
+				element.setCustomProperty( 'removeHighlight', ( element, data ) => element.removeClass( data.class ) );
 
 
 				return element;
 				return element;
 			} ), { priority: 'high' } );
 			} ), { priority: 'high' } );
@@ -277,8 +277,8 @@ describe( 'model-to-view-converters', () => {
 		} );
 		} );
 
 
 		it( 'should do nothing if descriptor is not provided', () => {
 		it( 'should do nothing if descriptor is not provided', () => {
-			dispatcher.on( 'addMarker:marker', convertElementsInsideMarker( () => null ) );
-			dispatcher.on( 'removeMarker:marker', convertElementsInsideMarker( () => null ) );
+			dispatcher.on( 'addMarker:marker', highlightElement( () => null ) );
+			dispatcher.on( 'removeMarker:marker', highlightElement( () => null ) );
 
 
 			dispatcher.convertInsertion( markerRange );
 			dispatcher.convertInsertion( markerRange );
 
 
@@ -1192,14 +1192,14 @@ describe( 'model-to-view-converters', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( 'virtualSelectionDescriptorToAttributeElement()', () => {
+	describe( 'highlightDescriptorToAttributeElement()', () => {
 		it( 'should return attribute element from descriptor object', () => {
 		it( 'should return attribute element from descriptor object', () => {
 			const descriptor = {
 			const descriptor = {
 				class: 'foo-class',
 				class: 'foo-class',
 				attributes: { one: 1, two: 2 },
 				attributes: { one: 1, two: 2 },
 				priority: 7,
 				priority: 7,
 			};
 			};
-			const element = virtualSelectionDescriptorToAttributeElement( descriptor );
+			const element = highlightDescriptorToAttributeElement( descriptor );
 
 
 			expect( element.is( 'attributeElement' ) ).to.be.true;
 			expect( element.is( 'attributeElement' ) ).to.be.true;
 			expect( element.name ).to.equal( 'span' );
 			expect( element.name ).to.equal( 'span' );
@@ -1216,7 +1216,7 @@ describe( 'model-to-view-converters', () => {
 				attributes: { one: 1, two: 2 },
 				attributes: { one: 1, two: 2 },
 				priority: 7,
 				priority: 7,
 			};
 			};
-			const element = virtualSelectionDescriptorToAttributeElement( descriptor );
+			const element = highlightDescriptorToAttributeElement( descriptor );
 
 
 			expect( element.is( 'attributeElement' ) ).to.be.true;
 			expect( element.is( 'attributeElement' ) ).to.be.true;
 			expect( element.name ).to.equal( 'span' );
 			expect( element.name ).to.equal( 'span' );
@@ -1232,7 +1232,7 @@ describe( 'model-to-view-converters', () => {
 				class: 'foo-class',
 				class: 'foo-class',
 				attributes: { one: 1, two: 2 },
 				attributes: { one: 1, two: 2 },
 			};
 			};
-			const element = virtualSelectionDescriptorToAttributeElement( descriptor );
+			const element = highlightDescriptorToAttributeElement( descriptor );
 
 
 			expect( element.is( 'attributeElement' ) ).to.be.true;
 			expect( element.is( 'attributeElement' ) ).to.be.true;
 			expect( element.name ).to.equal( 'span' );
 			expect( element.name ).to.equal( 'span' );
@@ -1249,7 +1249,7 @@ describe( 'model-to-view-converters', () => {
 				class: 'foo-class',
 				class: 'foo-class',
 				priority: 7
 				priority: 7
 			};
 			};
-			const element = virtualSelectionDescriptorToAttributeElement( descriptor );
+			const element = highlightDescriptorToAttributeElement( descriptor );
 
 
 			expect( element.is( 'attributeElement' ) ).to.be.true;
 			expect( element.is( 'attributeElement' ) ).to.be.true;
 			expect( element.name ).to.equal( 'span' );
 			expect( element.name ).to.equal( 'span' );

+ 35 - 0
packages/ckeditor5-engine/tests/dev-utils/enableenginedebug.js

@@ -454,6 +454,41 @@ describe( 'debug tools', () => {
 				expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
 				expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
 			} );
 			} );
 
 
+			it( 'SplitDelta - NoOperation as second operation', () => {
+				const otherRoot = modelDoc.createRoot( 'main', 'otherRoot' );
+				const splitEle = new ModelElement( 'paragraph', null, [ new ModelText( 'foo' ) ] );
+
+				otherRoot.appendChildren( [ splitEle ] );
+
+				const delta = new SplitDelta();
+				const insert = new InsertOperation( ModelPosition.createAt( otherRoot, 1 ), [ new ModelElement( 'paragraph' ) ], 0 );
+				const move = new NoOperation( 1 );
+
+				delta.addOperation( insert );
+				delta.addOperation( move );
+
+				expect( delta.toString() ).to.equal( 'SplitDelta( 0 ): (clone to otherRoot [ 1 ])' );
+
+				delta.log();
+				expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
+			} );
+
+			it( 'SplitDelta - NoOperation as second operation, MoveOperation as first operation', () => {
+				const otherRoot = modelDoc.createRoot( 'main', 'otherRoot' );
+
+				const delta = new SplitDelta();
+				const insert = new MoveOperation( ModelPosition.createAt( modelRoot, 1 ), 1, ModelPosition.createAt( otherRoot, 1 ), 0 );
+				const move = new NoOperation( 1 );
+
+				delta.addOperation( insert );
+				delta.addOperation( move );
+
+				expect( delta.toString() ).to.equal( 'SplitDelta( 0 ): (clone to otherRoot [ 1 ])' );
+
+				delta.log();
+				expect( log.calledWithExactly( delta.toString() ) ).to.be.true;
+			} );
+
 			it( 'UnwrapDelta', () => {
 			it( 'UnwrapDelta', () => {
 				const otherRoot = modelDoc.createRoot( 'main', 'otherRoot' );
 				const otherRoot = modelDoc.createRoot( 'main', 'otherRoot' );
 				const unwrapEle = new ModelElement( 'paragraph', null, [ new ModelText( 'foo' ) ] );
 				const unwrapEle = new ModelElement( 'paragraph', null, [ new ModelText( 'foo' ) ] );

+ 10 - 10
packages/ckeditor5-engine/tests/manual/virtualselection.html → packages/ckeditor5-engine/tests/manual/highlight.html

@@ -1,13 +1,13 @@
 <style>
 <style>
-	span.virtual-selection-yellow {
+	span.highlight-yellow {
 		background-color: yellow;
 		background-color: yellow;
 	}
 	}
 
 
-	span.virtual-selection-blue {
+	span.highlight-blue {
 		background-color: blue;
 		background-color: blue;
 	}
 	}
 
 
-	span.virtual-selection-red {
+	span.highlight-red {
 		background-color: red;
 		background-color: red;
 	}
 	}
 
 
@@ -19,17 +19,17 @@
 		line-height: 40px;
 		line-height: 40px;
 	}
 	}
 
 
-	.ck-widget.fancy-widget.virtual-selection-yellow {
+	.ck-widget.fancy-widget.highlight-yellow {
 		background-color: yellow;
 		background-color: yellow;
 		color: black;
 		color: black;
 	}
 	}
 
 
-	.ck-widget.fancy-widget.virtual-selection-blue {
+	.ck-widget.fancy-widget.highlight-blue {
 		background-color: blue;
 		background-color: blue;
 		color: black;
 		color: black;
 	}
 	}
 
 
-	.ck-widget.fancy-widget.virtual-selection-red {
+	.ck-widget.fancy-widget.highlight-red {
 		background-color: red;
 		background-color: red;
 		color: black;
 		color: black;
 	}
 	}
@@ -45,10 +45,10 @@
 	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas at diam quis justo imperdiet posuere non eu tortor. Mauris vel magna eu sem hendrerit varius. Ut imperdiet velit ut ante interdum convallis. Vestibulum vitae lacinia mi. </p>
 	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas at diam quis justo imperdiet posuere non eu tortor. Mauris vel magna eu sem hendrerit varius. Ut imperdiet velit ut ante interdum convallis. Vestibulum vitae lacinia mi. </p>
 </div>
 </div>
 <h2>Markers</h2>
 <h2>Markers</h2>
-<button id="add-marker-yellow">Set yellow marker</button><button id="remove-marker-yellow">Remove yellow marker</button>
+<button id="add-marker-yellow">Set yellow highlight</button><button id="remove-marker-yellow">Remove yellow highlight</button>
 <br />
 <br />
-<button id="add-marker-red">Set red marker</button><button id="remove-marker-red">Remove red marker</button>
+<button id="add-marker-red">Set red highlight</button><button id="remove-marker-red">Remove red highlight</button>
 <br />
 <br />
-<button id="add-marker-blue">Set blue marker</button><button id="remove-marker-blue">Remove blue marker</button>
+<button id="add-marker-blue">Set blue highlight</button><button id="remove-marker-blue">Remove blue highlight</button>
 <br />
 <br />
-<button id="remove-markers">Remove all markers</button>
+<button id="remove-markers">Remove all highlights</button>

+ 1 - 1
packages/ckeditor5-engine/tests/manual/virtualselection.js → packages/ckeditor5-engine/tests/manual/highlight.js

@@ -68,7 +68,7 @@ ClassicEditor.create( global.document.querySelector( '#editor' ), {
 		buildModelConverter()
 		buildModelConverter()
 			.for( editor.editing.modelToView )
 			.for( editor.editing.modelToView )
 			.fromMarker( 'marker' )
 			.fromMarker( 'marker' )
-			.toVirtualSelection( data => ( { class: 'virtual-selection-' + data.markerName.split( ':' )[ 1 ] } ) );
+			.toHighlight( data => ( { class: 'highlight-' + data.markerName.split( ':' )[ 1 ] } ) );
 
 
 		document.getElementById( 'add-marker-yellow' ).addEventListener( 'mousedown', evt => {
 		document.getElementById( 'add-marker-yellow' ).addEventListener( 'mousedown', evt => {
 			addMarker( editor, 'yellow' );
 			addMarker( editor, 'yellow' );

+ 14 - 0
packages/ckeditor5-engine/tests/manual/highlight.md

@@ -0,0 +1,14 @@
+### Highlight manual test
+
+1. Create selection that starts before the last word of the first paragraph and ends after first
+word of the second paragraph and press `Set yellow highlight`.
+1. Yellow highlight should be visible on text and widget.
+1. Select whole contents of the editor and press `Set red highlight`.
+1. Yellow highlight should be visible as before, red highlight should be visible on ranges where yellow highlight is not
+applied.
+1. Click `Remove yellow highlight`.
+1. Red highlight should be visible on the whole content, including the widget.
+1. Play with highlights, add and remove them, check if they are applied correctly.
+
+NOTE: Yellow highlight should be visible over red and blue one. Red highlight should be visible over blue one. 
+

+ 1 - 1
packages/ckeditor5-engine/tests/manual/markers.js

@@ -34,7 +34,7 @@ ClassicEditor
 
 
 		buildModelConverter().for( editor.editing.modelToView )
 		buildModelConverter().for( editor.editing.modelToView )
 			.fromMarker( 'highlight' )
 			.fromMarker( 'highlight' )
-			.toVirtualSelection( data => {
+			.toHighlight( data => {
 				const color = data.markerName.split( ':' )[ 1 ];
 				const color = data.markerName.split( ':' )[ 1 ];
 
 
 				return {
 				return {

+ 0 - 14
packages/ckeditor5-engine/tests/manual/virtualselection.md

@@ -1,14 +0,0 @@
-### Virtual selection manual test
-
-1. Create selection that starts before the last word of the first paragraph and ends after first
-word of the second paragraph and press `Set yellow marker`.
-1. Yellow marker should be visible on text and widget.
-1. Select whole contents of the editor and press `Set red marker`.
-1. Yellow marker should be visible as before, red marker should be visible on ranges where yellow marker is not
-applied.
-1. Click `Remove yellow marker`.
-1. Red marker should be visible on the whole content, including the widget.
-1. Play with markers, add and remove them, check if they are applied correctly.
-
-NOTE: Yellow marker should be visible over red and blue one. Red marker should be visible over blue one. 
-

+ 27 - 0
packages/ckeditor5-engine/tests/model/delta/transform/attributedelta.js

@@ -256,6 +256,33 @@ describe( 'transform', () => {
 					]
 					]
 				} );
 				} );
 			} );
 			} );
+
+			it( 'should use default algorithm and not throw if split delta has NoOperation', () => {
+				const range = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2, 3 ] ) );
+				const attrDelta = getAttributeDelta( range, 'foo', null, 'bar', 0 );
+				const splitDelta = getSplitDelta( new Position( root, [ 0, 2 ] ), new Element( 'paragraph' ), 3, 0 );
+				splitDelta.operations[ 1 ] = new NoOperation( 1 );
+
+				const transformed = transform( attrDelta, splitDelta, context );
+
+				baseVersion = splitDelta.operations.length;
+
+				expect( transformed.length ).to.equal( 1 );
+
+				expectDelta( transformed[ 0 ], {
+					type: AttributeDelta,
+					operations: [
+						{
+							type: AttributeOperation,
+							range: new Range( new Position( root, [ 2 ] ), new Position( root, [ 3, 3 ] ) ),
+							key: 'foo',
+							oldValue: null,
+							newValue: 'bar',
+							baseVersion
+						}
+					]
+				} );
+			} );
 		} );
 		} );
 
 
 		describe( 'AttributeDelta', () => {
 		describe( 'AttributeDelta', () => {

+ 33 - 0
packages/ckeditor5-engine/tests/model/delta/transform/renamedelta.js

@@ -11,8 +11,10 @@ import Element from '../../../../src/model/element';
 import Position from '../../../../src/model/position';
 import Position from '../../../../src/model/position';
 
 
 import RenameDelta from '../../../../src/model/delta/renamedelta';
 import RenameDelta from '../../../../src/model/delta/renamedelta';
+import SplitDelta from '../../../../src/model/delta/splitdelta';
 import Delta from '../../../../src/model/delta/delta';
 import Delta from '../../../../src/model/delta/delta';
 import RenameOperation from '../../../../src/model/operation/renameoperation';
 import RenameOperation from '../../../../src/model/operation/renameoperation';
+import MoveOperation from '../../../../src/model/operation/moveoperation';
 import NoOperation from '../../../../src/model/operation/nooperation';
 import NoOperation from '../../../../src/model/operation/nooperation';
 
 
 import {
 import {
@@ -106,6 +108,37 @@ describe( 'transform', () => {
 					]
 					]
 				} );
 				} );
 			} );
 			} );
+
+			it( 'should not throw if clone operation is NoOperation and use default transformation in that case', () => {
+				const noOpSplitDelta = new SplitDelta();
+				noOpSplitDelta.addOperation( new NoOperation( 0 ) );
+				noOpSplitDelta.addOperation( new MoveOperation( new Position( root, [ 1, 2 ] ), 3, new Position( root, [ 2, 0 ] ), 1 ) );
+
+				const renameDelta = new RenameDelta();
+				renameDelta.addOperation( new RenameOperation(
+					new Position( root, [ 1 ] ),
+					'p',
+					'li',
+					baseVersion
+				) );
+
+				const transformed = transform( renameDelta, noOpSplitDelta, context );
+
+				expect( transformed.length ).to.equal( 1 );
+
+				expectDelta( transformed[ 0 ], {
+					type: RenameDelta,
+					operations: [
+						{
+							type: RenameOperation,
+							position: new Position( root, [ 1 ] ),
+							oldName: 'p',
+							newName: 'li',
+							baseVersion: 2
+						}
+					]
+				} );
+			} );
 		} );
 		} );
 
 
 		describe( 'RenameDelta', () => {
 		describe( 'RenameDelta', () => {

+ 175 - 0
packages/ckeditor5-engine/tests/model/delta/transform/splitdelta.js

@@ -322,6 +322,61 @@ describe( 'transform', () => {
 				// DIV and P elements are correctly split.
 				// DIV and P elements are correctly split.
 				expect( nodesAndText ).to.equal( 'DIVXXXXXabcdXDIVDIVPabcPPfoobarxyzPDIV' );
 				expect( nodesAndText ).to.equal( 'DIVXXXXXabcdXDIVDIVPabcPPfoobarxyzPDIV' );
 			} );
 			} );
+
+			it( 'should use default algorithm and not throw if transformed split delta has NoOperation', () => {
+				splitDelta.operations[ 1 ] = new NoOperation( 1 );
+				const splitDeltaB = getSplitDelta( new Position( root, [ 3, 3, 3, 1 ] ), new Element( 'p' ), 11, baseVersion );
+
+				const transformed = transform( splitDelta, splitDeltaB, context );
+
+				baseVersion = splitDeltaB.operations.length;
+
+				expect( transformed.length ).to.equal( 1 );
+
+				expectDelta( transformed[ 0 ], {
+					type: SplitDelta,
+					operations: [
+						{
+							type: InsertOperation,
+							position: new Position( root, [ 3, 3, 5 ] ),
+							baseVersion
+						},
+						{
+							type: NoOperation,
+							baseVersion: baseVersion + 1
+						}
+					]
+				} );
+			} );
+
+			it( 'should use default algorithm and not throw if split delta to transform by has NoOperation', () => {
+				const splitDeltaB = getSplitDelta( new Position( root, [ 3, 3, 3, 1 ] ), new Element( 'p' ), 11, baseVersion );
+				splitDeltaB.operations[ 1 ] = new NoOperation( 1 );
+
+				const transformed = transform( splitDelta, splitDeltaB, context );
+
+				baseVersion = splitDeltaB.operations.length;
+
+				expect( transformed.length ).to.equal( 1 );
+
+				expectDelta( transformed[ 0 ], {
+					type: SplitDelta,
+					operations: [
+						{
+							type: InsertOperation,
+							position: new Position( root, [ 3, 3, 5 ] ),
+							baseVersion
+						},
+						{
+							type: MoveOperation,
+							sourcePosition: new Position( root, [ 3, 3, 3, 3 ] ),
+							howMany: 9,
+							targetPosition: new Position( root, [ 3, 3, 5, 0 ] ),
+							baseVersion: baseVersion + 1
+						}
+					]
+				} );
+			} );
 		} );
 		} );
 
 
 		describe( 'UnwrapDelta', () => {
 		describe( 'UnwrapDelta', () => {
@@ -389,6 +444,33 @@ describe( 'transform', () => {
 				// UnwrapDelta and SplitDelta are correctly applied.
 				// UnwrapDelta and SplitDelta are correctly applied.
 				expect( nodesAndText ).to.equal( 'DIVXXXXXaXXXXXXabcdXPabcPPfoobarxyzPDIV' );
 				expect( nodesAndText ).to.equal( 'DIVXXXXXaXXXXXXabcdXPabcPPfoobarxyzPDIV' );
 			} );
 			} );
+
+			it( 'should use default algorithm and not throw if split delta has NoOperation', () => {
+				splitDelta.operations[ 1 ] = new NoOperation( 1 );
+
+				const unwrapDelta = getUnwrapDelta( new Position( root, [ 3, 3 ] ), 4, baseVersion );
+
+				const transformed = transform( splitDelta, unwrapDelta, context );
+
+				expect( transformed.length ).to.equal( 1 );
+
+				baseVersion = unwrapDelta.operations.length;
+
+				expectDelta( transformed[ 0 ], {
+					type: SplitDelta,
+					operations: [
+						{
+							type: InsertOperation,
+							position: new Position( root, [ 3, 7 ] ),
+							baseVersion
+						},
+						{
+							type: NoOperation,
+							baseVersion: baseVersion + 1
+						}
+					]
+				} );
+			} );
 		} );
 		} );
 
 
 		describe( 'WrapDelta', () => {
 		describe( 'WrapDelta', () => {
@@ -500,6 +582,35 @@ describe( 'transform', () => {
 				// WrapDelta and SplitDelta are correctly applied.
 				// WrapDelta and SplitDelta are correctly applied.
 				expect( nodesAndText ).to.equal( 'EXabcdXPabcPPfoobarxyzPE' );
 				expect( nodesAndText ).to.equal( 'EXabcdXPabcPPfoobarxyzPE' );
 			} );
 			} );
+
+			it( 'should use default algorithm and not throw if split delta has NoOperation', () => {
+				splitDelta.operations[ 1 ] = new NoOperation( 1 );
+
+				const wrapRange = new Range( new Position( root, [ 3, 3, 2 ] ), new Position( root, [ 3, 3, 4 ] ) );
+				const wrapElement = new Element( 'E' );
+				const wrapDelta = getWrapDelta( wrapRange, wrapElement, baseVersion );
+
+				const transformed = transform( splitDelta, wrapDelta, context );
+
+				expect( transformed.length ).to.equal( 1 );
+
+				baseVersion = wrapDelta.operations.length;
+
+				expectDelta( transformed[ 0 ], {
+					type: SplitDelta,
+					operations: [
+						{
+							type: InsertOperation,
+							position: new Position( root, [ 3, 3, 3 ] ),
+							baseVersion
+						},
+						{
+							type: NoOperation,
+							baseVersion: baseVersion + 1
+						}
+					]
+				} );
+			} );
 		} );
 		} );
 
 
 		describe( 'AttributeDelta', () => {
 		describe( 'AttributeDelta', () => {
@@ -619,6 +730,35 @@ describe( 'transform', () => {
 					]
 					]
 				} );
 				} );
 			} );
 			} );
+
+			it( 'should use default algorithm and not throw if split delta has NoOperation', () => {
+				splitDelta.operations[ 1 ] = new NoOperation( 1 );
+
+				const attributeDelta = new AttributeDelta();
+				attributeDelta.addOperation( new AttributeOperation(
+					Range.createFromParentsAndOffsets( root, 0, root, 4 ), 'key', 'oldValue', 'newValue', baseVersion
+				) );
+
+				const transformed = transform( splitDelta, attributeDelta, context );
+
+				baseVersion = attributeDelta.operations.length;
+				expect( transformed.length ).to.equal( 1 );
+
+				expectDelta( transformed[ 0 ], {
+					type: SplitDelta,
+					operations: [
+						{
+							type: InsertOperation,
+							position: new Position( root, [ 3, 3, 4 ] ),
+							baseVersion
+						},
+						{
+							type: NoOperation,
+							baseVersion: baseVersion + 1
+						}
+					]
+				} );
+			} );
 		} );
 		} );
 
 
 		describe( 'RenameDelta', () => {
 		describe( 'RenameDelta', () => {
@@ -737,6 +877,41 @@ describe( 'transform', () => {
 					]
 					]
 				} );
 				} );
 			} );
 			} );
+
+			it( 'should not throw if clone operation is NoOperation and use default transformation in that case', () => {
+				const noOpSplitDelta = new SplitDelta();
+				noOpSplitDelta.addOperation( new NoOperation( 0 ) );
+				noOpSplitDelta.addOperation( new MoveOperation( new Position( root, [ 1, 2 ] ), 3, new Position( root, [ 2, 0 ] ), 1 ) );
+
+				const renameDelta = new RenameDelta();
+				renameDelta.addOperation( new RenameOperation(
+					new Position( root, [ 1 ] ),
+					'p',
+					'li',
+					baseVersion
+				) );
+
+				const transformed = transform( noOpSplitDelta, renameDelta, context );
+
+				expect( transformed.length ).to.equal( 1 );
+
+				expectDelta( transformed[ 0 ], {
+					type: SplitDelta,
+					operations: [
+						{
+							type: NoOperation,
+							baseVersion: 1
+						},
+						{
+							type: MoveOperation,
+							sourcePosition: new Position( root, [ 1, 2 ] ),
+							howMany: 3,
+							targetPosition: new Position( root, [ 2, 0 ] ),
+							baseVersion: 2
+						}
+					]
+				} );
+			} );
 		} );
 		} );
 
 
 		describe( 'RemoveDelta', () => {
 		describe( 'RemoveDelta', () => {

+ 34 - 0
packages/ckeditor5-engine/tests/model/delta/transform/unwrapdelta.js

@@ -13,6 +13,8 @@ import Position from '../../../../src/model/position';
 import Range from '../../../../src/model/range';
 import Range from '../../../../src/model/range';
 
 
 import MoveOperation from '../../../../src/model/operation/moveoperation';
 import MoveOperation from '../../../../src/model/operation/moveoperation';
+import RemoveOperation from '../../../../src/model/operation/removeoperation';
+import NoOperation from '../../../../src/model/operation/nooperation';
 
 
 import MergeDelta from '../../../../src/model/delta/mergedelta';
 import MergeDelta from '../../../../src/model/delta/mergedelta';
 import UnwrapDelta from '../../../../src/model/delta/unwrapdelta';
 import UnwrapDelta from '../../../../src/model/delta/unwrapdelta';
@@ -146,6 +148,38 @@ describe( 'transform', () => {
 				// UnwrapDelta and SplitDelta are applied.
 				// UnwrapDelta and SplitDelta are applied.
 				expect( nodesAndText ).to.equal( 'DIVXXXXXabcdXDIVDIVabcfoobarxyzDIV' );
 				expect( nodesAndText ).to.equal( 'DIVXXXXXabcdXDIVDIVabcfoobarxyzDIV' );
 			} );
 			} );
+
+			it( 'should use default algorithm and not throw if split delta has NoOperation', () => {
+				const splitPosition = new Position( root, [ 3, 3, 2, 1 ] );
+				const splitDelta = getSplitDelta( splitPosition, new Element( 'div' ), 1, baseVersion );
+				splitDelta.operations[ 1 ] = new NoOperation( 1 );
+
+				const transformed = transform( unwrapDelta, splitDelta, context );
+
+				expect( transformed.length ).to.equal( 1 );
+
+				baseVersion = splitDelta.operations.length;
+
+				expectDelta( transformed[ 0 ], {
+					type: UnwrapDelta,
+					operations: [
+						{
+							type: MoveOperation,
+							sourcePosition: new Position( root, [ 3, 3, 4, 0 ] ),
+							howMany: 12,
+							targetPosition: new Position( root, [ 3, 3, 4 ] ),
+							baseVersion
+						},
+						{
+							type: RemoveOperation,
+							sourcePosition: new Position( root, [ 3, 3, 16 ] ),
+							howMany: 1,
+							targetPosition: new Position( gy, [ 0 ] ),
+							baseVersion: baseVersion + 1
+						}
+					]
+				} );
+			} );
 		} );
 		} );
 	} );
 	} );
 } );
 } );

+ 31 - 0
packages/ckeditor5-engine/tests/model/delta/transform/wrapdelta.js

@@ -14,6 +14,7 @@ import Range from '../../../../src/model/range';
 
 
 import MoveOperation from '../../../../src/model/operation/moveoperation';
 import MoveOperation from '../../../../src/model/operation/moveoperation';
 import InsertOperation from '../../../../src/model/operation/insertoperation';
 import InsertOperation from '../../../../src/model/operation/insertoperation';
+import NoOperation from '../../../../src/model/operation/nooperation';
 
 
 import MergeDelta from '../../../../src/model/delta/mergedelta';
 import MergeDelta from '../../../../src/model/delta/mergedelta';
 import WrapDelta from '../../../../src/model/delta/wrapdelta';
 import WrapDelta from '../../../../src/model/delta/wrapdelta';
@@ -190,6 +191,36 @@ describe( 'transform', () => {
 				// WrapDelta and SplitDelta are correctly applied.
 				// WrapDelta and SplitDelta are correctly applied.
 				expect( nodesAndText ).to.equal( 'EXabcdXPabcPPfoobarxyzPE' );
 				expect( nodesAndText ).to.equal( 'EXabcdXPabcPPfoobarxyzPE' );
 			} );
 			} );
+
+			it( 'should use default algorithm and not throw if split delta has NoOperation', () => {
+				const splitPosition = new Position( root, [ 3, 3, 2, 1 ] );
+				const splitDelta = getSplitDelta( splitPosition, new Element( 'p' ), 11, baseVersion );
+				splitDelta.operations[ 1 ] = new NoOperation( 1 );
+
+				const transformed = transform( wrapDelta, splitDelta, context );
+
+				expect( transformed.length ).to.equal( 1 );
+
+				baseVersion = wrapDelta.operations.length;
+
+				expectDelta( transformed[ 0 ], {
+					type: WrapDelta,
+					operations: [
+						{
+							type: InsertOperation,
+							position: new Position( root, [ 3, 3, 4, 5 ] ),
+							baseVersion
+						},
+						{
+							type: MoveOperation,
+							sourcePosition: new Position( root, [ 3, 3, 4, 1 ] ),
+							howMany: 4,
+							targetPosition: new Position( root, [ 3, 3, 4, 5, 0 ] ),
+							baseVersion: baseVersion + 1
+						}
+					]
+				} );
+			} );
 		} );
 		} );
 	} );
 	} );
 } );
 } );