Răsfoiți Sursa

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

Piotrek Koszuliński 8 ani în urmă
părinte
comite
93e3ccf3b4

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

@@ -15,8 +15,8 @@ import {
 	removeUIElement,
 	wrapItem,
 	unwrapItem,
-	convertTextsInsideMarker,
-	convertElementsInsideMarker
+	highlightText,
+	highlightElement
 } from './model-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' );
  *
- * 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',
  *			priority: 20
  *		} );
@@ -76,7 +76,7 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  * It is possible to provide various different parameters for
  * {@link module:engine/conversion/buildmodelconverter~ModelConverterBuilder#toElement},
  * {@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.
  *
  * 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:
 	 * * 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},
 	 * * 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:
 	 *
-	 *		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:
 	 *
-	 *		buildModelConverter.for( dispatcher ).fromMarker( 'search:blue' ).toVirtualSelection( data => {
+	 *		buildModelConverter.for( dispatcher ).fromMarker( 'search:blue' ).toHighlight( data => {
 	 *			const color = data.markerName.split( ':' )[ 1 ];
 	 *
 	 *			return { class: 'search-' + color };
 	 *		} );
 	 *
 	 * 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;
 
 		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(
-				'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.'
 			);
 		}
 
 		for ( const dispatcher of this._dispatchers ) {
 			// 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
- * can handle virtual selection separately by providing `setVirtualSelection` and `removeVirtualSelection` custom
+ * can handle displaying highlight separately by providing `addHighlight` and `removeHighlight` custom
  * properties.
  *
  * @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`
- * 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`
- * {@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 ViewRange from '../view/range';
 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
@@ -162,21 +162,21 @@ export function convertSelectionAttribute( elementCreator ) {
  *		modelDispatcher.on( 'selectionMarker:searchResult', convertSelectionMarker( { class: 'search' } ) );
  *
  * @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.
  */
-export function convertSelectionMarker( selectionDescriptor ) {
+export function convertSelectionMarker( highlightDescriptor ) {
 	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 ) {
 			return;
 		}
 
-		const viewElement = virtualSelectionDescriptorToAttributeElement( descriptor );
+		const viewElement = highlightDescriptorToAttributeElement( descriptor );
 		const consumableName = 'selectionMarker:' + data.markerName;
 
 		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
  * {@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}
  */
-export function convertTextsInsideMarker( selectionDescriptor ) {
+export function highlightText( highlightDescriptor ) {
 	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;
 
@@ -428,7 +428,7 @@ export function convertTextsInsideMarker( selectionDescriptor ) {
 			return;
 		}
 
-		const viewElement = virtualSelectionDescriptorToAttributeElement( descriptor );
+		const viewElement = highlightDescriptorToAttributeElement( descriptor );
 		const viewRange = conversionApi.mapper.toViewRange( data.range );
 
 		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
- * 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
- * {@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}.
- * 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.
  *
- * @param {module:engine/conversion/buildmodelconverter~VirtualSelectionDescriptor|Function} selectionDescriptor
+ * @param {module:engine/conversion/buildmodelconverter~HighlightDescriptor|Function} highlightDescriptor
  * @return {Function}
  */
-export function convertElementsInsideMarker( selectionDescriptor ) {
+export function highlightElement( highlightDescriptor ) {
 	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;
 
@@ -475,9 +475,9 @@ export function convertElementsInsideMarker( selectionDescriptor ) {
 
 		const viewElement = conversionApi.mapper.toViewElement( modelItem );
 		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.
 			consumable.consume( data.item, evt.name );
 
@@ -486,7 +486,7 @@ export function convertElementsInsideMarker( selectionDescriptor ) {
 				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
- * 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}
  */
-export function virtualSelectionDescriptorToAttributeElement( descriptor ) {
+export function highlightDescriptorToAttributeElement( descriptor ) {
 	const attributeElement = new ViewAttributeElement( 'span', descriptor.attributes );
 
 	if ( descriptor.class ) {

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

@@ -414,7 +414,10 @@ function enableLoggingTools() {
 
 	SplitDelta.prototype.toString = function() {
 		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() {

+ 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.
 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 deltas = defaultTransform( a, b, context );
@@ -173,6 +178,11 @@ addTransformationCase( MergeDelta, MoveDelta, ( a, b, context ) => {
 
 // Add special case for SplitDelta x SplitDelta transformation.
 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 pathB = b.position.getParentPath();
 
@@ -202,6 +212,11 @@ addTransformationCase( SplitDelta, SplitDelta, ( a, b, context ) => {
 
 // Add special case for SplitDelta x UnwrapDelta transformation.
 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,
 	// so we discard that delta.
 	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.
 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
 	// 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.
-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();
 
 	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.
 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.
 	// 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' ) {
@@ -316,9 +346,13 @@ addTransformationCase( WeakInsertDelta, AttributeDelta, ( a, b ) => {
 
 // Add special case for WrapDelta x SplitDelta transformation.
 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
 	// the wrap. Since split was already applied, we have to revert it.
-
 	const sameRoot = a.range.start.root == b.position.root;
 	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;
@@ -349,15 +383,22 @@ addTransformationCase( WrapDelta, SplitDelta, ( a, b, context ) => {
 // Add special case for RenameDelta x SplitDelta transformation.
 addTransformationCase( RenameDelta, SplitDelta, ( a, b, context ) => {
 	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 );
 
-	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
 		// the node created during splitting.
 		const additionalRenameDelta = a.clone();
-		additionalRenameDelta.operations[ 0 ].position = posBeforeSplitParent.getShiftedBy( 1 );
+		additionalRenameDelta.operations[ 0 ].position = insertPosition.getShiftedBy( 1 );
 
 		deltas.push( additionalRenameDelta );
 	}
@@ -368,12 +409,19 @@ addTransformationCase( RenameDelta, SplitDelta, ( a, b, context ) => {
 // Add special case for SplitDelta x RenameDelta transformation.
 addTransformationCase( SplitDelta, RenameDelta, ( a, b, context ) => {
 	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 ( !undoMode && b.operations[ 0 ].position.isEqual( posBeforeSplitParent ) ) {
+	if ( insertPosition && !undoMode && b.operations[ 0 ].position.isEqual( insertPosition ) ) {
 		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`.
 		// `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;
 
 		beforeEach( () => {
@@ -358,11 +358,11 @@ describe( 'Model converter builder', () => {
 			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,
-				attributes: { title: 'marker' }
+				attributes: { title: 'highlight title' }
 			} );
 
 			dispatcher.convertMarker( 'addMarker', 'search', ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 4 ) );
@@ -371,7 +371,7 @@ describe( 'Model converter builder', () => {
 				'<div>' +
 					'<p>' +
 						'fo' +
-						'<span class="virtual-selection" title="marker">ob</span>' +
+						'<span class="highlight" title="highlight title">ob</span>' +
 						'ar' +
 					'</p>' +
 				'</div>' );
@@ -385,11 +385,11 @@ describe( 'Model converter builder', () => {
 			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,
-				attributes: { title: 'marker' }
+				attributes: { title: 'highlight title' }
 			} ) );
 
 			dispatcher.convertMarker( 'addMarker', 'search', ModelRange.createFromParentsAndOffsets( modelElement, 2, modelElement, 4 ) );
@@ -398,7 +398,7 @@ describe( 'Model converter builder', () => {
 				'<div>' +
 					'<p>' +
 						'fo' +
-						'<span class="virtual-selection" title="marker">ob</span>' +
+						'<span class="highlight" title="highlight title">ob</span>' +
 						'ar' +
 					'</p>' +
 				'</div>' );
@@ -413,8 +413,8 @@ describe( 'Model converter builder', () => {
 		} );
 
 		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 ) );
@@ -429,11 +429,11 @@ describe( 'Model converter builder', () => {
 		} );
 
 		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'
 			} );
 
@@ -451,14 +451,14 @@ describe( 'Model converter builder', () => {
 
 		it( 'should throw if trying to convert from attribute', () => {
 			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', () => {
 			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,
 	insertText,
 	wrapItem,
-	convertTextsInsideMarker,
-	convertElementsInsideMarker
+	highlightText,
+	highlightElement
 } from '../../src/conversion/model-to-view-converters';
 
 import { stringify as stringifyView } from '../../src/dev-utils/view';
 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', () => {
 	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( '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.
 		dispatcher.on( 'selection', clearAttributes(), { priority: 'low' } );
@@ -80,7 +80,7 @@ describe( 'model-selection-to-view-converters', () => {
 			// Selection converters for selection attributes.
 			dispatcher.on( 'selectionAttribute:bold', convertSelectionAttribute( new ViewAttributeElement( 'strong' ) ) );
 			dispatcher.on( 'selectionAttribute:italic', convertSelectionAttribute( new ViewAttributeElement( 'em' ) ) );
-			dispatcher.on( 'selectionMarker:marker', convertSelectionMarker( virtualSelectionDescriptor ) );
+			dispatcher.on( 'selectionMarker:marker', convertSelectionMarker( highlightDescriptor ) );
 		} );
 
 		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>' );
 			} );
 
-			it( 'in marker - using virtual selection descriptor creator', () => {
+			it( 'in marker - using highlight descriptor creator', () => {
 				dispatcher.on( 'selectionMarker:marker2', convertSelectionMarker(
 					data => ( { 'class': data.markerName } )
 				) );

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

@@ -28,9 +28,9 @@ import {
 	unwrapItem,
 	remove,
 	removeUIElement,
-	convertTextsInsideMarker,
-	convertElementsInsideMarker,
-	virtualSelectionDescriptorToAttributeElement
+	highlightText,
+	highlightElement,
+	highlightDescriptorToAttributeElement
 } from '../../src/conversion/model-to-view-converters';
 
 import { createRangeOnElementOnly } from '../../tests/model/_utils/utils';
@@ -82,10 +82,10 @@ describe( 'model-to-view-converters', () => {
 		return result;
 	}
 
-	describe( 'convertTextsInsideMarker()', () => {
+	describe( 'highlightText()', () => {
 		let modelElement1, modelElement2, markerRange;
-		const virtualSelectionDescriptor = {
-			class: 'marker-class',
+		const highlightDescriptor = {
+			class: 'highlight-class',
 			priority: 7,
 			attributes: { title: 'title' }
 		};
@@ -105,8 +105,8 @@ describe( 'model-to-view-converters', () => {
 		} );
 
 		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 );
 
 			modelDoc.markers.set( 'marker', markerRange );
@@ -115,10 +115,10 @@ describe( 'model-to-view-converters', () => {
 			expect( viewToString( viewRoot ) ).to.equal(
 				'<div>' +
 					'<p>' +
-						'<span class="marker-class" title="title">foo</span>' +
+						'<span class="highlight-class" title="title">foo</span>' +
 					'</p>' +
 					'<p>' +
-						'<span class="marker-class" title="title">bar</span>' +
+						'<span class="highlight-class" title="title">bar</span>' +
 					'</p>' +
 				'</div>'
 			);
@@ -131,10 +131,10 @@ describe( 'model-to-view-converters', () => {
 		it( 'should not convert marker on elements already consumed', () => {
 			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 );
 
 			modelDoc.markers.set( 'marker', markerRange );
@@ -157,8 +157,8 @@ describe( 'model-to-view-converters', () => {
 		} );
 
 		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 );
 
@@ -171,10 +171,10 @@ describe( 'model-to-view-converters', () => {
 		} );
 	} );
 
-	describe( 'convertElementsInsideMarker()', () => {
+	describe( 'highlightElement()', () => {
 		let modelElement1, modelElement2, markerRange;
-		const virtualSelectionDescriptor = {
-			class: 'marker-class',
+		const highlightDescriptor = {
+			class: 'highlight-class',
 			priority: 7,
 			attributes: { title: 'title' }
 		};
@@ -193,9 +193,9 @@ describe( 'model-to-view-converters', () => {
 			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 => {
 				// Use special converter only for first paragraph.
 				if ( data.item == modelElement2 ) {
@@ -204,16 +204,16 @@ describe( 'model-to-view-converters', () => {
 
 				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;
@@ -225,7 +225,7 @@ describe( 'model-to-view-converters', () => {
 
 			expect( viewToString( viewRoot ) ).to.equal(
 				'<div>' +
-					'<p class="virtual-selection-own-class">' +
+					'<p class="highlight-own-class">' +
 						'foo' +
 					'</p>' +
 					'<p>' +
@@ -242,16 +242,16 @@ describe( 'model-to-view-converters', () => {
 		it( 'should not convert marker on elements already consumed', () => {
 			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( () => {
 				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;
 			} ), { priority: 'high' } );
@@ -277,8 +277,8 @@ describe( 'model-to-view-converters', () => {
 		} );
 
 		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 );
 
@@ -1192,14 +1192,14 @@ describe( 'model-to-view-converters', () => {
 		} );
 	} );
 
-	describe( 'virtualSelectionDescriptorToAttributeElement()', () => {
+	describe( 'highlightDescriptorToAttributeElement()', () => {
 		it( 'should return attribute element from descriptor object', () => {
 			const descriptor = {
 				class: 'foo-class',
 				attributes: { one: 1, two: 2 },
 				priority: 7,
 			};
-			const element = virtualSelectionDescriptorToAttributeElement( descriptor );
+			const element = highlightDescriptorToAttributeElement( descriptor );
 
 			expect( element.is( 'attributeElement' ) ).to.be.true;
 			expect( element.name ).to.equal( 'span' );
@@ -1216,7 +1216,7 @@ describe( 'model-to-view-converters', () => {
 				attributes: { one: 1, two: 2 },
 				priority: 7,
 			};
-			const element = virtualSelectionDescriptorToAttributeElement( descriptor );
+			const element = highlightDescriptorToAttributeElement( descriptor );
 
 			expect( element.is( 'attributeElement' ) ).to.be.true;
 			expect( element.name ).to.equal( 'span' );
@@ -1232,7 +1232,7 @@ describe( 'model-to-view-converters', () => {
 				class: 'foo-class',
 				attributes: { one: 1, two: 2 },
 			};
-			const element = virtualSelectionDescriptorToAttributeElement( descriptor );
+			const element = highlightDescriptorToAttributeElement( descriptor );
 
 			expect( element.is( 'attributeElement' ) ).to.be.true;
 			expect( element.name ).to.equal( 'span' );
@@ -1249,7 +1249,7 @@ describe( 'model-to-view-converters', () => {
 				class: 'foo-class',
 				priority: 7
 			};
-			const element = virtualSelectionDescriptorToAttributeElement( descriptor );
+			const element = highlightDescriptorToAttributeElement( descriptor );
 
 			expect( element.is( 'attributeElement' ) ).to.be.true;
 			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;
 			} );
 
+			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', () => {
 				const otherRoot = modelDoc.createRoot( 'main', 'otherRoot' );
 				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>
-	span.virtual-selection-yellow {
+	span.highlight-yellow {
 		background-color: yellow;
 	}
 
-	span.virtual-selection-blue {
+	span.highlight-blue {
 		background-color: blue;
 	}
 
-	span.virtual-selection-red {
+	span.highlight-red {
 		background-color: red;
 	}
 
@@ -19,17 +19,17 @@
 		line-height: 40px;
 	}
 
-	.ck-widget.fancy-widget.virtual-selection-yellow {
+	.ck-widget.fancy-widget.highlight-yellow {
 		background-color: yellow;
 		color: black;
 	}
 
-	.ck-widget.fancy-widget.virtual-selection-blue {
+	.ck-widget.fancy-widget.highlight-blue {
 		background-color: blue;
 		color: black;
 	}
 
-	.ck-widget.fancy-widget.virtual-selection-red {
+	.ck-widget.fancy-widget.highlight-red {
 		background-color: red;
 		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>
 </div>
 <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 />
-<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 />
-<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 />
-<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()
 			.for( editor.editing.modelToView )
 			.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 => {
 			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 )
 			.fromMarker( 'highlight' )
-			.toVirtualSelection( data => {
+			.toHighlight( data => {
 				const color = data.markerName.split( ':' )[ 1 ];
 
 				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', () => {

+ 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 RenameDelta from '../../../../src/model/delta/renamedelta';
+import SplitDelta from '../../../../src/model/delta/splitdelta';
 import Delta from '../../../../src/model/delta/delta';
 import RenameOperation from '../../../../src/model/operation/renameoperation';
+import MoveOperation from '../../../../src/model/operation/moveoperation';
 import NoOperation from '../../../../src/model/operation/nooperation';
 
 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', () => {

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

@@ -322,6 +322,61 @@ describe( 'transform', () => {
 				// DIV and P elements are correctly split.
 				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', () => {
@@ -389,6 +444,33 @@ describe( 'transform', () => {
 				// UnwrapDelta and SplitDelta are correctly applied.
 				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', () => {
@@ -500,6 +582,35 @@ describe( 'transform', () => {
 				// WrapDelta and SplitDelta are correctly applied.
 				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', () => {
@@ -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', () => {
@@ -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', () => {

+ 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 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 UnwrapDelta from '../../../../src/model/delta/unwrapdelta';
@@ -146,6 +148,38 @@ describe( 'transform', () => {
 				// UnwrapDelta and SplitDelta are applied.
 				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 InsertOperation from '../../../../src/model/operation/insertoperation';
+import NoOperation from '../../../../src/model/operation/nooperation';
 
 import MergeDelta from '../../../../src/model/delta/mergedelta';
 import WrapDelta from '../../../../src/model/delta/wrapdelta';
@@ -190,6 +191,36 @@ describe( 'transform', () => {
 				// WrapDelta and SplitDelta are correctly applied.
 				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
+						}
+					]
+				} );
+			} );
 		} );
 	} );
 } );