소스 검색

Merge branch 'master' into t/ckeditor5/645

Aleksander Nowodzinski 8 년 전
부모
커밋
38d3076471
47개의 변경된 파일2795개의 추가작업 그리고 2128개의 파일을 삭제
  1. 2 2
      packages/ckeditor5-engine/src/controller/datacontroller.js
  2. 6 6
      packages/ckeditor5-engine/src/conversion/model-selection-to-view-converters.js
  3. 4 3
      packages/ckeditor5-engine/src/conversion/model-to-view-converters.js
  4. 3 3
      packages/ckeditor5-engine/src/conversion/view-selection-to-model-converters.js
  5. 17 16
      packages/ckeditor5-engine/src/dev-utils/model.js
  6. 14 15
      packages/ckeditor5-engine/src/dev-utils/view.js
  7. 2 94
      packages/ckeditor5-engine/src/model/document.js
  8. 472 312
      packages/ckeditor5-engine/src/model/documentselection.js
  9. 145 0
      packages/ckeditor5-engine/src/model/schema.js
  10. 137 215
      packages/ckeditor5-engine/src/model/selection.js
  11. 14 3
      packages/ckeditor5-engine/src/model/utils/deletecontent.js
  12. 9 3
      packages/ckeditor5-engine/src/model/utils/insertcontent.js
  13. 8 1
      packages/ckeditor5-engine/src/model/utils/modifyselection.js
  14. 155 0
      packages/ckeditor5-engine/src/model/writer.js
  15. 5 4
      packages/ckeditor5-engine/src/view/domconverter.js
  16. 3 3
      packages/ckeditor5-engine/src/view/observer/fakeselectionobserver.js
  17. 2 2
      packages/ckeditor5-engine/src/view/observer/mutationobserver.js
  18. 123 143
      packages/ckeditor5-engine/src/view/selection.js
  19. 1 1
      packages/ckeditor5-engine/src/view/writer.js
  20. 22 19
      packages/ckeditor5-engine/tests/controller/editingcontroller.js
  21. 7 8
      packages/ckeditor5-engine/tests/conversion/buildmodelconverter.js
  22. 57 47
      packages/ckeditor5-engine/tests/conversion/model-selection-to-view-converters.js
  23. 31 20
      packages/ckeditor5-engine/tests/conversion/modelconversiondispatcher.js
  24. 19 17
      packages/ckeditor5-engine/tests/conversion/view-selection-to-model-converters.js
  25. 56 33
      packages/ckeditor5-engine/tests/dev-utils/model.js
  26. 10 19
      packages/ckeditor5-engine/tests/dev-utils/view.js
  27. 2 233
      packages/ckeditor5-engine/tests/model/document.js
  28. 138 193
      packages/ckeditor5-engine/tests/model/documentselection.js
  29. 402 8
      packages/ckeditor5-engine/tests/model/schema.js
  30. 280 290
      packages/ckeditor5-engine/tests/model/selection.js
  31. 45 3
      packages/ckeditor5-engine/tests/model/utils/deletecontent.js
  32. 18 0
      packages/ckeditor5-engine/tests/model/utils/insertcontent.js
  33. 2 2
      packages/ckeditor5-engine/tests/model/utils/modifyselection.js
  34. 196 1
      packages/ckeditor5-engine/tests/model/writer.js
  35. 3 3
      packages/ckeditor5-engine/tests/view/document/document.js
  36. 1 2
      packages/ckeditor5-engine/tests/view/document/jumpoverinlinefiller.js
  37. 13 13
      packages/ckeditor5-engine/tests/view/document/jumpoveruielement.js
  38. 3 4
      packages/ckeditor5-engine/tests/view/domconverter/binding.js
  39. 2 4
      packages/ckeditor5-engine/tests/view/domconverter/dom-to-view.js
  40. 6 6
      packages/ckeditor5-engine/tests/view/editableelement.js
  41. 1 1
      packages/ckeditor5-engine/tests/view/manual/fakeselection.js
  42. 2 2
      packages/ckeditor5-engine/tests/view/observer/focusobserver.js
  43. 1 1
      packages/ckeditor5-engine/tests/view/observer/mutationobserver.js
  44. 6 6
      packages/ckeditor5-engine/tests/view/observer/selectionobserver.js
  45. 3 3
      packages/ckeditor5-engine/tests/view/placeholder.js
  46. 25 40
      packages/ckeditor5-engine/tests/view/renderer.js
  47. 322 324
      packages/ckeditor5-engine/tests/view/selection.js

+ 2 - 2
packages/ckeditor5-engine/src/controller/datacontroller.js

@@ -194,8 +194,8 @@ export default class DataController {
 		this.model.enqueueChange( 'transparent', writer => {
 			// Clearing selection is a workaround for ticket #569 (LiveRange loses position after removing data from document).
 			// After fixing it this code should be removed.
-			this.model.document.selection.removeAllRanges();
-			this.model.document.selection.clearAttributes();
+			writer.setSelection( null );
+			writer.removeSelectionAttribute( this.model.document.selection.getAttributeKeys() );
 
 			writer.remove( ModelRange.createIn( modelRoot ) );
 			writer.insert( this.parse( data ), modelRoot );

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

@@ -3,7 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-import ViewRange from '../view/range';
 import viewWriter from '../view/writer';
 
 /**
@@ -35,12 +34,14 @@ export function convertRangeSelection() {
 			return;
 		}
 
-		conversionApi.viewSelection.removeAllRanges();
+		const viewRanges = [];
 
 		for ( const range of selection.getRanges() ) {
 			const viewRange = conversionApi.mapper.toViewRange( range );
-			conversionApi.viewSelection.addRange( viewRange, selection.isBackward );
+			viewRanges.push( viewRange );
 		}
+
+		conversionApi.viewSelection.setTo( viewRanges, selection.isBackward );
 	};
 }
 
@@ -82,8 +83,7 @@ export function convertCollapsedSelection() {
 		const viewPosition = conversionApi.mapper.toViewPosition( modelPosition );
 		const brokenPosition = viewWriter.breakAttributes( viewPosition );
 
-		conversionApi.viewSelection.removeAllRanges();
-		conversionApi.viewSelection.addRange( new ViewRange( brokenPosition, brokenPosition ) );
+		conversionApi.viewSelection.setTo( brokenPosition );
 	};
 }
 
@@ -122,7 +122,7 @@ export function clearAttributes() {
 				}
 			}
 		}
-		conversionApi.viewSelection.removeAllRanges();
+		conversionApi.viewSelection.setTo( null );
 	};
 }
 

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

@@ -12,6 +12,7 @@ import ViewAttributeElement from '../view/attributeelement';
 import ViewText from '../view/text';
 import ViewRange from '../view/range';
 import viewWriter from '../view/writer';
+import DocumentSelection from '../model/documentselection';
 
 /**
  * Contains model to view converters for
@@ -329,7 +330,7 @@ export function wrap( elementCreator ) {
 			return;
 		}
 
-		if ( data.item instanceof ModelSelection ) {
+		if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) {
 			// Selection attribute conversion.
 			viewWriter.wrap( conversionApi.viewSelection.getFirstRange(), newViewElement, conversionApi.viewSelection );
 		} else {
@@ -369,7 +370,7 @@ export function highlightText( highlightDescriptor ) {
 			return;
 		}
 
-		if ( !( data.item instanceof ModelSelection ) && !data.item.is( 'textProxy' ) ) {
+		if ( !( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) && !data.item.is( 'textProxy' ) ) {
 			return;
 		}
 
@@ -385,7 +386,7 @@ export function highlightText( highlightDescriptor ) {
 
 		const viewElement = createViewElementFromHighlightDescriptor( descriptor );
 
-		if ( data.item instanceof ModelSelection ) {
+		if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) {
 			viewWriter.wrap( conversionApi.viewSelection.getFirstRange(), viewElement, conversionApi.viewSelection );
 		} else {
 			const viewRange = conversionApi.mapper.toViewRange( data.range );

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

@@ -37,11 +37,11 @@ export function convertSelectionChange( model, mapper ) {
 			ranges.push( mapper.toModelRange( viewRange ) );
 		}
 
-		modelSelection.setRanges( ranges, viewSelection.isBackward );
+		modelSelection.setTo( ranges, viewSelection.isBackward );
 
 		if ( !modelSelection.isEqual( model.document.selection ) ) {
-			model.change( () => {
-				model.document.selection.setTo( modelSelection );
+			model.change( writer => {
+				writer.setSelection( modelSelection );
 			} );
 		}
 	};

+ 17 - 16
packages/ckeditor5-engine/src/dev-utils/model.js

@@ -19,6 +19,7 @@ import ModelPosition from '../model/position';
 import ModelConversionDispatcher from '../conversion/modelconversiondispatcher';
 import ModelSelection from '../model/selection';
 import ModelDocumentFragment from '../model/documentfragment';
+import DocumentSelection from '../model/documentselection';
 
 import ViewConversionDispatcher from '../conversion/viewconversiondispatcher';
 import ViewSelection from '../view/selection';
@@ -34,6 +35,7 @@ import {
 } from '../conversion/model-selection-to-view-converters';
 import { insertText, insertElement, wrap } from '../conversion/model-to-view-converters';
 import isPlainObject from '@ckeditor/ckeditor5-utils/src/lib/lodash/isPlainObject';
+import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
 
 /**
  * Writes the contents of the {@link module:engine/model/document~Document Document} to an HTML-like string.
@@ -46,7 +48,7 @@ import isPlainObject from '@ckeditor/ckeditor5-utils/src/lib/lodash/isPlainObjec
  * @param {Object} [options]
  * @param {Boolean} [options.withoutSelection=false] Whether to write the selection. When set to `true` selection will
  * be not included in returned string.
- * @param {Boolean} [options.rootName='main'] Name of the root from which data should be stringified. If not provided
+ * @param {String} [options.rootName='main'] Name of the root from which data should be stringified. If not provided
  * default `main` name will be used.
  * @returns {String} The stringified data.
  */
@@ -114,8 +116,8 @@ export function setData( model, data, options = {} ) {
 		writer.insert( modelDocumentFragment, modelRoot );
 
 		// Clean up previous document selection.
-		model.document.selection.clearAttributes();
-		model.document.selection.removeAllRanges();
+		writer.setSelection( null );
+		writer.removeSelectionAttribute( model.document.selection.getAttributeKeys() );
 
 		// Update document selection if specified.
 		if ( selection ) {
@@ -128,10 +130,10 @@ export function setData( model, data, options = {} ) {
 				ranges.push( new ModelRange( start, end ) );
 			}
 
-			model.document.selection.setRanges( ranges, selection.isBackward );
+			writer.setSelection( ranges, selection.isBackward );
 
 			if ( options.selectionAttributes ) {
-				model.document.selection.setAttributesTo( selection.getAttributes() );
+				writer.setSelectionAttribute( selection.getAttributes() );
 			}
 		}
 	} );
@@ -180,12 +182,12 @@ export function stringify( node, selectionOrPositionOrRange = null ) {
 	// Get selection from passed selection or position or range if at least one is specified.
 	if ( selectionOrPositionOrRange instanceof ModelSelection ) {
 		selection = selectionOrPositionOrRange;
+	} else if ( selectionOrPositionOrRange instanceof DocumentSelection ) {
+		selection = selectionOrPositionOrRange;
 	} else if ( selectionOrPositionOrRange instanceof ModelRange ) {
-		selection = new ModelSelection();
-		selection.addRange( selectionOrPositionOrRange );
+		selection = new ModelSelection( selectionOrPositionOrRange );
 	} else if ( selectionOrPositionOrRange instanceof ModelPosition ) {
-		selection = new ModelSelection();
-		selection.addRange( new ModelRange( selectionOrPositionOrRange, selectionOrPositionOrRange ) );
+		selection = new ModelSelection( selectionOrPositionOrRange );
 	}
 
 	// Setup model to view converter.
@@ -198,7 +200,7 @@ export function stringify( node, selectionOrPositionOrRange = null ) {
 
 	modelToView.on( 'insert:$text', insertText() );
 	modelToView.on( 'attribute', wrap( ( value, data ) => {
-		if ( data.item instanceof ModelSelection || data.item.is( 'textProxy' ) ) {
+		if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection || data.item.is( 'textProxy' ) ) {
 			return new ViewAttributeElement( 'model-text-with-attributes', { [ data.attributeKey ]: stringifyAttributeValue( value ) } );
 		}
 	} ) );
@@ -216,7 +218,7 @@ export function stringify( node, selectionOrPositionOrRange = null ) {
 
 	// Convert model selection to view selection.
 	if ( selection ) {
-		modelToView.convertSelection( selection, [] );
+		modelToView.convertSelection( selection );
 	}
 
 	// Parse view to data string.
@@ -290,16 +292,15 @@ export function parse( data, schema, options = {} ) {
 
 		// Convert ranges.
 		for ( const viewRange of viewSelection.getRanges() ) {
-			ranges.push( ( mapper.toModelRange( viewRange ) ) );
+			ranges.push( mapper.toModelRange( viewRange ) );
 		}
 
 		// Create new selection.
-		selection = new ModelSelection();
-		selection.setRanges( ranges, viewSelection.isBackward );
+		selection = new ModelSelection( ranges, viewSelection.isBackward );
 
 		// Set attributes to selection if specified.
-		if ( options.selectionAttributes ) {
-			selection.setAttributesTo( options.selectionAttributes );
+		for ( const [ key, value ] of toMap( options.selectionAttributes || [] ) ) {
+			selection.setAttribute( key, value );
 		}
 	}
 

+ 14 - 15
packages/ckeditor5-engine/src/dev-utils/view.js

@@ -125,8 +125,9 @@ setData._parse = parse;
  *		const text = new Text( 'foobar' );
  *		const b = new Element( 'b', null, text );
  *		const p = new Element( 'p', null, b );
- *		const selection = new Selection();
- *		selection.addRange( Range.createFromParentsAndOffsets( p, 0, p, 1 ) );
+ *		const selection = new Selection(
+ *			Range.createFromParentsAndOffsets( p, 0, p, 1 )
+ *		);
  *
  *		stringify( p, selection ); // '<p>[<b>foobar</b>]</p>'
  *
@@ -135,8 +136,7 @@ setData._parse = parse;
  *		const text = new Text( 'foobar' );
  *		const b = new Element( 'b', null, text );
  *		const p = new Element( 'p', null, b );
- *		const selection = new Selection();
- *		selection.addRange( Range.createFromParentsAndOffsets( text, 1, text, 5 ) );
+ *		const selection = new Selection( Range.createFromParentsAndOffsets( text, 1, text, 5 ) );
  *
  *		stringify( p, selection ); // '<p><b>f{ooba}r</b></p>'
  *
@@ -147,9 +147,10 @@ setData._parse = parse;
  * Multiple ranges are supported:
  *
  *		const text = new Text( 'foobar' );
- *		const selection = new Selection();
- *		selection.addRange( Range.createFromParentsAndOffsets( text, 0, text, 1 ) );
- *		selection.addRange( Range.createFromParentsAndOffsets( text, 3, text, 5 ) );
+ *		const selection = new Selection( [
+ *			Range.createFromParentsAndOffsets( text, 0, text, 1 ) ),
+ *			Range.createFromParentsAndOffsets( text, 3, text, 5 ) )
+ *		] );
  *
  *		stringify( text, selection ); // '{f}oo{ba}r'
  *
@@ -209,12 +210,11 @@ setData._parse = parse;
 export function stringify( node, selectionOrPositionOrRange = null, options = {} ) {
 	let selection;
 
-	if ( selectionOrPositionOrRange instanceof Position ) {
-		selection = new Selection();
-		selection.addRange( new Range( selectionOrPositionOrRange, selectionOrPositionOrRange ) );
-	} else if ( selectionOrPositionOrRange instanceof Range ) {
-		selection = new Selection();
-		selection.addRange( selectionOrPositionOrRange );
+	if (
+		selectionOrPositionOrRange instanceof Position ||
+		selectionOrPositionOrRange instanceof Range
+	) {
+		selection = new Selection( selectionOrPositionOrRange );
 	} else {
 		selection = selectionOrPositionOrRange;
 	}
@@ -332,8 +332,7 @@ export function parse( data, options = {} ) {
 
 	// When ranges are present - return object containing view, and selection.
 	if ( ranges.length ) {
-		const selection = new Selection();
-		selection.setRanges( ranges, !!options.lastRangeBackward );
+		const selection = new Selection( ranges, !!options.lastRangeBackward );
 
 		return {
 			view,

+ 2 - 94
packages/ckeditor5-engine/src/model/document.js

@@ -13,7 +13,6 @@ import Position from './position';
 import RootElement from './rootelement';
 import History from './history';
 import DocumentSelection from './documentselection';
-import TreeWalker from './treewalker';
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import clone from '@ckeditor/ckeditor5-utils/src/lib/lodash/clone';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
@@ -248,59 +247,6 @@ export default class Document {
 		return Array.from( this.roots, root => root.rootName ).filter( name => name != graveyardName );
 	}
 
-	/**
-	 * Basing on given `position`, finds and returns a {@link module:engine/model/range~Range Range} instance that is
-	 * nearest to that `position` and is a correct range for selection.
-	 *
-	 * Correct selection range might be collapsed - when it's located in position where text node can be placed.
-	 * Non-collapsed range is returned when selection can be placed around element marked as "object" in
-	 * {@link module:engine/model/schema~Schema schema}.
-	 *
-	 * Direction of searching for nearest correct selection range can be specified as:
-	 * * `both` - searching will be performed in both ways,
-	 * * `forward` - searching will be performed only forward,
-	 * * `backward` - searching will be performed only backward.
-	 *
-	 * When valid selection range cannot be found, `null` is returned.
-	 *
-	 * @param {module:engine/model/position~Position} position Reference position where new selection range should be looked for.
-	 * @param {'both'|'forward'|'backward'} [direction='both'] Search direction.
-	 * @returns {module:engine/model/range~Range|null} Nearest selection range or `null` if one cannot be found.
-	 */
-	getNearestSelectionRange( position, direction = 'both' ) {
-		const schema = this.model.schema;
-
-		// Return collapsed range if provided position is valid.
-		if ( schema.checkChild( position, '$text' ) ) {
-			return new Range( position );
-		}
-
-		let backwardWalker, forwardWalker;
-
-		if ( direction == 'both' || direction == 'backward' ) {
-			backwardWalker = new TreeWalker( { startPosition: position, direction: 'backward' } );
-		}
-
-		if ( direction == 'both' || direction == 'forward' ) {
-			forwardWalker = new TreeWalker( { startPosition: position } );
-		}
-
-		for ( const data of combineWalkers( backwardWalker, forwardWalker ) ) {
-			const type = ( data.walker == backwardWalker ? 'elementEnd' : 'elementStart' );
-			const value = data.value;
-
-			if ( value.type == type && schema.isObject( value.item ) ) {
-				return Range.createOn( value.item );
-			}
-
-			if ( schema.checkChild( value.nextPosition, '$text' ) ) {
-				return new Range( value.nextPosition );
-			}
-		}
-
-		return null;
-	}
-
 	/**
 	 * Used to register a post-fixer callback. Post-fixers mechanism guarantees that the features that listen to
 	 * {@link module:engine/model/model~Model#event:_change model's change event} will operate on a correct model state.
@@ -380,10 +326,11 @@ export default class Document {
 	 */
 	_getDefaultRange() {
 		const defaultRoot = this._getDefaultRoot();
+		const schema = this.model.schema;
 
 		// Find the first position where the selection can be put.
 		const position = new Position( defaultRoot, [ 0 ] );
-		const nearestRange = this.getNearestSelectionRange( position );
+		const nearestRange = schema.getNearestSelectionRange( position );
 
 		// If valid selection range is not found - return range collapsed at the beginning of the root.
 		return nearestRange || new Range( position );
@@ -446,42 +393,3 @@ function validateTextNodePosition( rangeBoundary ) {
 
 	return true;
 }
-
-// Generator function returning values from provided walkers, switching between them at each iteration. If only one walker
-// is provided it will return data only from that walker.
-//
-// @param {module:engine/module/treewalker~TreeWalker} [backward] Walker iterating in backward direction.
-// @param {module:engine/module/treewalker~TreeWalker} [forward] Walker iterating in forward direction.
-// @returns {Iterable.<Object>} Object returned at each iteration contains `value` and `walker` (informing which walker returned
-// given value) fields.
-function* combineWalkers( backward, forward ) {
-	let done = false;
-
-	while ( !done ) {
-		done = true;
-
-		if ( backward ) {
-			const step = backward.next();
-
-			if ( !step.done ) {
-				done = false;
-				yield {
-					walker: backward,
-					value: step.value
-				};
-			}
-		}
-
-		if ( forward ) {
-			const step = forward.next();
-
-			if ( !step.done ) {
-				done = false;
-				yield {
-					walker: forward,
-					value: step.value
-				};
-			}
-		}
-	}
-}

+ 472 - 312
packages/ckeditor5-engine/src/model/documentselection.js

@@ -7,8 +7,11 @@
  * @module engine/model/documentselection
  */
 
+import mix from '@ckeditor/ckeditor5-utils/src/mix';
+import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
+
+import Selection from './selection';
 import Position from './position';
-import Range from './range';
 import LiveRange from './liverange';
 import Text from './text';
 import TextProxy from './textproxy';
@@ -16,19 +19,20 @@ import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import log from '@ckeditor/ckeditor5-utils/src/log';
 
-import Selection from './selection';
-
-const storePrefix = 'selection:';
-
 const attrOpTypes = new Set(
 	[ 'addAttribute', 'removeAttribute', 'changeAttribute', 'addRootAttribute', 'removeRootAttribute', 'changeRootAttribute' ]
 );
 
+const storePrefix = 'selection:';
+
 /**
  * `DocumentSelection` is a special selection which is used as the
  * {@link module:engine/model/document~Document#selection document's selection}.
  * There can be only one instance of `DocumentSelection` per document.
  *
+ * All selection modifiers should be used from the {@link module:engine/model/writer~Writer} instance
+ * inside the {@link module:engine/model/model~Model#change} block, as it provides a secure way to modify model.
+ *
  * `DocumentSelection` is automatically updated upon changes in the {@link module:engine/model/document~Document document}
  * to always contain valid ranges. Its attributes are inherited from the text unless set explicitly.
  *
@@ -44,44 +48,411 @@ const attrOpTypes = new Set(
  * that are inside {@link module:engine/model/documentfragment~DocumentFragment document fragment}.
  * If you need to represent a selection in document fragment,
  * use {@link module:engine/model/selection~Selection Selection class} instead.
- *
- * @extends module:engine/model/selection~Selection
  */
-export default class DocumentSelection extends Selection {
+export default class DocumentSelection {
 	/**
 	 * Creates an empty live selection for given {@link module:engine/model/document~Document}.
 	 *
 	 * @param {module:engine/model/document~Document} doc Document which owns this selection.
 	 */
 	constructor( doc ) {
-		super();
-
 		/**
-		 * Document which owns this selection.
+		 * Selection used internally by that class (`DocumentSelection` is a proxy to that selection).
 		 *
 		 * @protected
-		 * @member {module:engine/model/model~Model}
 		 */
+		this._selection = new LiveSelection( doc );
+
+		this._selection.delegate( 'change:range' ).to( this );
+		this._selection.delegate( 'change:attribute' ).to( this );
+	}
+
+	/**
+	 * Returns whether the selection is collapsed. Selection is collapsed when there is exactly one range which is
+	 * collapsed.
+	 *
+	 * @readonly
+	 * @type {Boolean}
+	 */
+	get isCollapsed() {
+		return this._selection.isCollapsed;
+	}
+
+	/**
+	 * Selection anchor. Anchor may be described as a position where the most recent part of the selection starts.
+	 * Together with {@link #focus} they define the direction of selection, which is important
+	 * when expanding/shrinking selection. Anchor is always {@link module:engine/model/range~Range#start start} or
+	 * {@link module:engine/model/range~Range#end end} position of the most recently added range.
+	 *
+	 * Is set to `null` if there are no ranges in selection.
+	 *
+	 * @see #focus
+	 * @readonly
+	 * @type {module:engine/model/position~Position|null}
+	 */
+	get anchor() {
+		return this._selection.anchor;
+	}
+
+	/**
+	 * Selection focus. Focus is a position where the selection ends.
+	 *
+	 * Is set to `null` if there are no ranges in selection.
+	 *
+	 * @see #anchor
+	 * @readonly
+	 * @type {module:engine/model/position~Position|null}
+	 */
+	get focus() {
+		return this._selection.focus;
+	}
+
+	/**
+	 * Returns number of ranges in selection.
+	 *
+	 * @readonly
+	 * @type {Number}
+	 */
+	get rangeCount() {
+		return this._selection.rangeCount;
+	}
+
+	/**
+	 * Describes whether `Documentselection` has own range(s) set, or if it is defaulted to
+	 * {@link module:engine/model/document~Document#_getDefaultRange document's default range}.
+	 *
+	 * @readonly
+	 * @type {Boolean}
+	 */
+	get hasOwnRange() {
+		return this._selection.hasOwnRange;
+	}
+
+	/**
+	 * Specifies whether the {@link #focus}
+	 * precedes {@link #anchor}.
+	 *
+	 * @readonly
+	 * @type {Boolean}
+	 */
+	get isBackward() {
+		return this._selection.isBackward;
+	}
+
+	/**
+	 * Used for the compatibility with the {@link module:engine/model/selection~Selection#isEqual} method.
+	 *
+	 * @protected
+	 */
+	get _ranges() {
+		return this._selection._ranges;
+	}
+
+	/**
+	 * Returns an iterable that iterates over copies of selection ranges.
+	 *
+	 * @returns {Iterable.<module:engine/model/range~Range>}
+	 */
+	getRanges() {
+		return this._selection.getRanges();
+	}
+
+	/**
+	 * Returns the first position in the selection.
+	 * First position is the position that {@link module:engine/model/position~Position#isBefore is before}
+	 * any other position in the selection.
+	 *
+	 * Returns `null` if there are no ranges in selection.
+	 *
+	 * @returns {module:engine/model/position~Position|null}
+	 */
+	getFirstPosition() {
+		return this._selection.getFirstPosition();
+	}
+
+	/**
+	 * Returns the last position in the selection.
+	 * Last position is the position that {@link module:engine/model/position~Position#isAfter is after}
+	 * any other position in the selection.
+	 *
+	 * Returns `null` if there are no ranges in selection.
+	 *
+	 * @returns {module:engine/model/position~Position|null}
+	 */
+	getLastPosition() {
+		return this._selection.getLastPosition();
+	}
+
+	/**
+	 * Returns a copy of the first range in the selection.
+	 * First range is the one which {@link module:engine/model/range~Range#start start} position
+	 * {@link module:engine/model/position~Position#isBefore is before} start position of all other ranges
+	 * (not to confuse with the first range added to the selection).
+	 *
+	 * Returns `null` if there are no ranges in selection.
+	 *
+	 * @returns {module:engine/model/range~Range|null}
+	 */
+	getFirstRange() {
+		return this._selection.getFirstRange();
+	}
+
+	/**
+	 * Returns a copy of the last range in the selection.
+	 * Last range is the one which {@link module:engine/model/range~Range#end end} position
+	 * {@link module:engine/model/position~Position#isAfter is after} end position of all other ranges (not to confuse with the range most
+	 * recently added to the selection).
+	 *
+	 * Returns `null` if there are no ranges in selection.
+	 *
+	 * @returns {module:engine/model/range~Range|null}
+	 */
+	getLastRange() {
+		return this._selection.getLastRange();
+	}
+
+	/**
+	 * Gets elements of type "block" touched by the selection.
+	 *
+	 * This method's result can be used for example to apply block styling to all blocks covered by this selection.
+	 *
+	 * **Note:** `getSelectedBlocks()` always returns the deepest block.
+	 *
+	 * In this case the function will return exactly all 3 paragraphs:
+	 *
+	 *		<paragraph>[a</paragraph>
+	 *		<quote>
+	 *			<paragraph>b</paragraph>
+	 *		</quote>
+	 *		<paragraph>c]d</paragraph>
+	 *
+	 * In this case the paragraph will also be returned, despite the collapsed selection:
+	 *
+	 *		<paragraph>[]a</paragraph>
+	 *
+	 * **Special case**: If a selection ends at the beginning of a block, that block is not returned as from user perspective
+	 * this block wasn't selected. See [#984](https://github.com/ckeditor/ckeditor5-engine/issues/984) for more details.
+	 *
+	 *		<paragraph>[a</paragraph>
+	 *		<paragraph>b</paragraph>
+	 *		<paragraph>]c</paragraph> // this block will not be returned
+	 *
+	 * @returns {Iterator.<module:engine/model/element~Element>}
+	 */
+	getSelectedBlocks() {
+		return this._selection.getSelectedBlocks();
+	}
+
+	/**
+	 * Returns the selected element. {@link module:engine/model/element~Element Element} is considered as selected if there is only
+	 * one range in the selection, and that range contains exactly one element.
+	 * Returns `null` if there is no selected element.
+	 *
+	 * @returns {module:engine/model/element~Element|null}
+	 */
+	getSelectedElement() {
+		return this._selection.getSelectedElement();
+	}
+
+	/**
+	 * Checks whether the selection contains the entire content of the given element. This means that selection must start
+	 * at a position {@link module:engine/model/position~Position#isTouching touching} the element's start and ends at position
+	 * touching the element's end.
+	 *
+	 * By default, this method will check whether the entire content of the selection's current root is selected.
+	 * Useful to check if e.g. the user has just pressed <kbd>Ctrl</kbd> + <kbd>A</kbd>.
+	 *
+	 * @param {module:engine/model/element~Element} [element=this.anchor.root]
+	 * @returns {Boolean}
+	 */
+	containsEntireContent( element ) {
+		return this._selection.containsEntireContent( element );
+	}
+
+	/**
+	 * Unbinds all events previously bound by document selection.
+	 */
+	destroy() {
+		this._selection.destroy();
+	}
+
+	/**
+	 * Returns iterable that iterates over this selection's attribute keys.
+	 *
+	 * @returns {Iterable.<String>}
+	 */
+	getAttributeKeys() {
+		return this._selection.getAttributeKeys();
+	}
+
+	/**
+	 * Returns iterable that iterates over this selection's attributes.
+	 *
+	 * Attributes are returned as arrays containing two items. First one is attribute key and second is attribute value.
+	 * This format is accepted by native `Map` object and also can be passed in `Node` constructor.
+	 *
+	 * @returns {Iterable.<*>}
+	 */
+	getAttributes() {
+		return this._selection.getAttributes();
+	}
+
+	/**
+	 * Gets an attribute value for given key or `undefined` if that attribute is not set on the selection.
+	 *
+	 * @param {String} key Key of attribute to look for.
+	 * @returns {*} Attribute value or `undefined`.
+	 */
+	getAttribute( key ) {
+		return this._selection.getAttribute( key );
+	}
+
+	/**
+	 * Checks if the selection has an attribute for given key.
+	 *
+	 * @param {String} key Key of attribute to check.
+	 * @returns {Boolean} `true` if attribute with given key is set on selection, `false` otherwise.
+	 */
+	hasAttribute( key ) {
+		return this._selection.hasAttribute( key );
+	}
+
+	/**
+	 * Moves {@link module:engine/model/documentselection~DocumentSelection#focus} to the specified location.
+	 * Should be used only within the {@link module:engine/model/writer~Writer#setSelectionFocus} method.
+	 *
+	 * The location can be specified in the same form as {@link module:engine/model/position~Position.createAt} parameters.
+	 *
+	 * @see module:engine/model/writer~Writer#setSelectionFocus
+	 * @protected
+	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
+	 * first parameter is a {@link module:engine/model/item~Item model item}.
+	 */
+	_setFocus( itemOrPosition, offset ) {
+		this._selection.setFocus( itemOrPosition, offset );
+	}
+
+	/**
+	 * Sets this selection's ranges and direction to the specified location based on the given
+	 * {@link module:engine/model/selection~Selection selection}, {@link module:engine/model/position~Position position},
+	 * {@link module:engine/model/element~Element element}, {@link module:engine/model/position~Position position},
+	 * {@link module:engine/model/range~Range range}, an iterable of {@link module:engine/model/range~Range ranges} or null.
+	 * Should be used only within the {@link module:engine/model/writer~Writer#setSelection} method.
+	 *
+	 * @see module:engine/model/writer~Writer#setSelection
+	 * @protected
+	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
+	 * module:engine/model/position~Position|module:engine/model/element~Element|
+	 * Iterable.<module:engine/model/range~Range>|module:engine/model/range~Range|null} selectable
+	 * @param {Boolean|Number|'before'|'end'|'after'} [backwardSelectionOrOffset]
+	 */
+	_setTo( selectable, backwardSelectionOrOffset ) {
+		this._selection.setTo( selectable, backwardSelectionOrOffset );
+	}
+
+	/**
+	 * Sets attribute on the selection. If attribute with the same key already is set, it's value is overwritten.
+	 * Should be used only within the {@link module:engine/model/writer~Writer#setSelectionAttribute} method.
+	 *
+	 * @see module:engine/model/writer~Writer#setSelectionAttribute
+	 * @protected
+	 * @param {String} key Key of the attribute to set.
+	 * @param {*} value Attribute value.
+	 */
+	_setAttribute( key, value ) {
+		this._selection.setAttribute( key, value );
+	}
+
+	/**
+	 * Removes an attribute with given key from the selection.
+	 * If the given attribute was set on the selection, fires the {@link module:engine/model/selection~Selection#event:change}
+	 * event with removed attribute key.
+	 * Should be used only within the {@link module:engine/model/writer~Writer#removeSelectionAttribute} method.
+	 *
+	 * @see module:engine/model/writer~Writer#removeSelectionAttribute
+	 * @protected
+	 * @param {String} key Key of the attribute to remove.
+	 */
+	_removeAttribute( key ) {
+		this._selection.removeAttribute( key );
+	}
+
+	/**
+	 * Returns an iterable that iterates through all selection attributes stored in current selection's parent.
+	 *
+	 * @protected
+	 * @returns {Iterable.<*>}
+	 */
+	_getStoredAttributes() {
+		return this._selection._getStoredAttributes();
+	}
+
+	/**
+	 * Generates and returns an attribute key for selection attributes store, basing on original attribute key.
+	 *
+	 * @protected
+	 * @param {String} key Attribute key to convert.
+	 * @returns {String} Converted attribute key, applicable for selection store.
+	 */
+	static _getStoreAttributeKey( key ) {
+		return storePrefix + key;
+	}
+
+	/**
+	 * Checks whether the given attribute key is an attribute stored on an element.
+	 *
+	 * @protected
+	 * @param {String} key
+	 * @returns {Boolean}
+	 */
+	static _isStoreAttributeKey( key ) {
+		return key.startsWith( storePrefix );
+	}
+}
+
+mix( DocumentSelection, EmitterMixin );
+
+// `LiveSelection` is used internally by {@link module:engine/model/documentselection~DocumentSelection} and shouldn't be used directly.
+//
+// LiveSelection` is automatically updated upon changes in the {@link module:engine/model/document~Document document}
+// to always contain valid ranges. Its attributes are inherited from the text unless set explicitly.
+//
+// Differences between {@link module:engine/model/selection~Selection} and `LiveSelection` are:
+// * there is always a range in `LiveSelection` - even if no ranges were added there is a "default range"
+// present in the selection,
+// * ranges added to this selection updates automatically when the document changes,
+// * attributes of `LiveSelection` are updated automatically according to selection ranges.
+//
+// @extends module:engine/model/selection~Selection
+//
+
+class LiveSelection extends Selection {
+	// Creates an empty live selection for given {@link module:engine/model/document~Document}.
+	// @param {module:engine/model/document~Document} doc Document which owns this selection.
+	constructor( doc ) {
+		super();
+
+		// Document which owns this selection.
+		//
+		// @protected
+		// @member {module:engine/model/model~Model}
 		this._model = doc.model;
 
-		/**
-		 * Document which owns this selection.
-		 *
-		 * @protected
-		 * @member {module:engine/model/document~Document}
-		 */
+		// Document which owns this selection.
+		//
+		// @protected
+		// @member {module:engine/model/document~Document}
 		this._document = doc;
 
-		/**
-		 * Keeps mapping of attribute name to priority with which the attribute got modified (added/changed/removed)
-		 * last time. Possible values of priority are: `'low'` and `'normal'`.
-		 *
-		 * Priorities are used by internal `DocumentSelection` mechanisms. All attributes set using `DocumentSelection`
-		 * attributes API are set with `'normal'` priority.
-		 *
-		 * @private
-		 * @member {Map} module:engine/model/documentselection~DocumentSelection#_attributePriority
-		 */
+		// Keeps mapping of attribute name to priority with which the attribute got modified (added/changed/removed)
+		// last time. Possible values of priority are: `'low'` and `'normal'`.
+		//
+		// Priorities are used by internal `LiveSelection` mechanisms. All attributes set using `LiveSelection`
+		// attributes API are set with `'normal'` priority.
+		//
+		// @private
+		// @member {Map} module:engine/model/liveselection~LiveSelection#_attributePriority
 		this._attributePriority = new Map();
 
 		// Add events that will ensure selection correctness.
@@ -128,50 +499,34 @@ export default class DocumentSelection extends Selection {
 		}, { priority: 'low' } );
 	}
 
-	/**
-	 * @inheritDoc
-	 */
 	get isCollapsed() {
 		const length = this._ranges.length;
 
 		return length === 0 ? this._document._getDefaultRange().isCollapsed : super.isCollapsed;
 	}
 
-	/**
-	 * @inheritDoc
-	 */
 	get anchor() {
 		return super.anchor || this._document._getDefaultRange().start;
 	}
 
-	/**
-	 * @inheritDoc
-	 */
 	get focus() {
 		return super.focus || this._document._getDefaultRange().end;
 	}
 
-	/**
-	 * @inheritDoc
-	 */
 	get rangeCount() {
 		return this._ranges.length ? this._ranges.length : 1;
 	}
 
-	/**
-	 * Describes whether `DocumentSelection` has own range(s) set, or if it is defaulted to
-	 * {@link module:engine/model/document~Document#_getDefaultRange document's default range}.
-	 *
-	 * @readonly
-	 * @type {Boolean}
-	 */
+	// Describes whether `LiveSelection` has own range(s) set, or if it is defaulted to
+	// {@link module:engine/model/document~Document#_getDefaultRange document's default range}.
+	//
+	// @readonly
+	// @type {Boolean}
 	get hasOwnRange() {
 		return this._ranges.length > 0;
 	}
 
-	/**
-	 * Unbinds all events previously bound by document selection.
-	 */
+	// Unbinds all events previously bound by live selection.
 	destroy() {
 		for ( let i = 0; i < this._ranges.length; i++ ) {
 			this._ranges[ i ].detach();
@@ -180,9 +535,6 @@ export default class DocumentSelection extends Selection {
 		this.stopListening();
 	}
 
-	/**
-	 * @inheritDoc
-	 */
 	* getRanges() {
 		if ( this._ranges.length ) {
 			yield* super.getRanges();
@@ -191,53 +543,25 @@ export default class DocumentSelection extends Selection {
 		}
 	}
 
-	/**
-	 * @inheritDoc
-	 */
 	getFirstRange() {
 		return super.getFirstRange() || this._document._getDefaultRange();
 	}
 
-	/**
-	 * @inheritDoc
-	 */
 	getLastRange() {
 		return super.getLastRange() || this._document._getDefaultRange();
 	}
 
-	/**
-	 * @inheritDoc
-	 */
-	addRange( range, isBackward = false ) {
-		super.addRange( range, isBackward );
-		this.refreshAttributes();
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	removeAllRanges() {
-		super.removeAllRanges();
-		this.refreshAttributes();
+	setTo( selectable, backwardSelectionOrOffset ) {
+		super.setTo( selectable, backwardSelectionOrOffset );
+		this._refreshAttributes();
 	}
 
-	/**
-	 * @inheritDoc
-	 */
-	setRanges( newRanges, isLastBackward = false ) {
-		super.setRanges( newRanges, isLastBackward );
-		this.refreshAttributes();
+	setFocus( itemOrPosition, offset ) {
+		super.setFocus( itemOrPosition, offset );
+		this._refreshAttributes();
 	}
 
-	/**
-	 * @inheritDoc
-	 */
 	setAttribute( key, value ) {
-		// Store attribute in parent element if the selection is collapsed in an empty node.
-		if ( this.isCollapsed && this.anchor.parent.isEmpty ) {
-			this._storeAttribute( key, value );
-		}
-
 		if ( this._setAttribute( key, value ) ) {
 			// Fire event with exact data.
 			const attributeKeys = [ key ];
@@ -245,15 +569,7 @@ export default class DocumentSelection extends Selection {
 		}
 	}
 
-	/**
-	 * @inheritDoc
-	 */
 	removeAttribute( key ) {
-		// Remove stored attribute from parent element if the selection is collapsed in an empty node.
-		if ( this.isCollapsed && this.anchor.parent.isEmpty ) {
-			this._removeStoredAttribute( key );
-		}
-
 		if ( this._removeAttribute( key ) ) {
 			// Fire event with exact data.
 			const attributeKeys = [ key ];
@@ -261,67 +577,15 @@ export default class DocumentSelection extends Selection {
 		}
 	}
 
-	/**
-	 * @inheritDoc
-	 */
-	setAttributesTo( attrs ) {
-		attrs = toMap( attrs );
-
-		if ( this.isCollapsed && this.anchor.parent.isEmpty ) {
-			this._setStoredAttributesTo( attrs );
-		}
-
-		const changed = this._setAttributesTo( attrs );
-
-		if ( changed.size > 0 ) {
-			// Fire event with exact data (fire only if anything changed).
-			const attributeKeys = Array.from( changed );
-			this.fire( 'change:attribute', { attributeKeys, directChange: true } );
-		}
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	clearAttributes() {
-		this.setAttributesTo( [] );
-	}
-
-	/**
-	 * Removes all attributes from the selection and sets attributes according to the surrounding nodes.
-	 */
-	refreshAttributes() {
+	// Removes all attributes from the selection and sets attributes according to the surrounding nodes.
+	_refreshAttributes() {
 		this._updateAttributes( true );
 	}
 
-	/**
-	 * This method is not available in `DocumentSelection`. There can be only one
-	 * `DocumentSelection` per document instance, so creating new `DocumentSelection`s this way
-	 * would be unsafe.
-	 */
-	static createFromSelection() {
-		/**
-		 * Cannot create a new `DocumentSelection` instance.
-		 *
-		 * `DocumentSelection#createFromSelection()` is not available. There can be only one
-		 * `DocumentSelection` per document instance, so creating new `DocumentSelection`s this way
-		 * would be unsafe.
-		 *
-		 * @error documentselection-cannot-create
-		 */
-		throw new CKEditorError( 'documentselection-cannot-create: Cannot create a new DocumentSelection instance.' );
-	}
-
-	/**
-	 * @inheritDoc
-	 */
 	_popRange() {
 		this._ranges.pop().detach();
 	}
 
-	/**
-	 * @inheritDoc
-	 */
 	_pushRange( range ) {
 		const liveRange = this._prepareRange( range );
 
@@ -331,23 +595,14 @@ export default class DocumentSelection extends Selection {
 		}
 	}
 
-	/**
-	 * Prepares given range to be added to selection. Checks if it is correct,
-	 * converts it to {@link module:engine/model/liverange~LiveRange LiveRange}
-	 * and sets listeners listening to the range's change event.
-	 *
-	 * @private
-	 * @param {module:engine/model/range~Range} range
-	 */
+	// Prepares given range to be added to selection. Checks if it is correct,
+	// converts it to {@link module:engine/model/liverange~LiveRange LiveRange}
+	// and sets listeners listening to the range's change event.
+	//
+	// @private
+	// @param {module:engine/model/range~Range} range
 	_prepareRange( range ) {
-		if ( !( range instanceof Range ) ) {
-			/**
-			 * Trying to add an object that is not an instance of Range.
-			 *
-			 * @error model-selection-added-not-range
-			 */
-			throw new CKEditorError( 'model-selection-added-not-range: Trying to add an object that is not an instance of Range.' );
-		}
+		this._checkRange( range );
 
 		if ( range.root == this._document.graveyard ) {
 			/**
@@ -360,8 +615,6 @@ export default class DocumentSelection extends Selection {
 			return;
 		}
 
-		this._checkRange( range );
-
 		const liveRange = LiveRange.createFromRange( range );
 
 		liveRange.on( 'change:range', ( evt, oldRange, data ) => {
@@ -377,13 +630,11 @@ export default class DocumentSelection extends Selection {
 		return liveRange;
 	}
 
-	/**
-	 * Updates this selection attributes according to its ranges and the {@link module:engine/model/document~Document model document}.
-	 *
-	 * @protected
-	 * @param {Boolean} clearAll
-	 * @fires change:attribute
-	 */
+	// Updates this selection attributes according to its ranges and the {@link module:engine/model/document~Document model document}.
+	//
+	// @protected
+	// @param {Boolean} clearAll
+	// @fires change:attribute
 	_updateAttributes( clearAll ) {
 		const newAttributes = toMap( this._getSurroundingAttributes() );
 		const oldAttributes = toMap( this.getAttributes() );
@@ -402,7 +653,7 @@ export default class DocumentSelection extends Selection {
 			}
 		}
 
-		this._setAttributesTo( newAttributes, false );
+		this._setAttributesTo( newAttributes );
 
 		// Let's evaluate which attributes really changed.
 		const changed = [];
@@ -428,39 +679,15 @@ export default class DocumentSelection extends Selection {
 		}
 	}
 
-	/**
-	 * Generates and returns an attribute key for selection attributes store, basing on original attribute key.
-	 *
-	 * @protected
-	 * @param {String} key Attribute key to convert.
-	 * @returns {String} Converted attribute key, applicable for selection store.
-	 */
-	static _getStoreAttributeKey( key ) {
-		return storePrefix + key;
-	}
-
-	/**
-	 * Checks whether the given attribute key is an attribute stored on an element.
-	 *
-	 * @protected
-	 * @param {String} key
-	 * @returns {Boolean}
-	 */
-	static _isStoreAttributeKey( key ) {
-		return key.startsWith( storePrefix );
-	}
-
-	/**
-	 * Internal method for setting `DocumentSelection` attribute. Supports attribute priorities (through `directChange`
-	 * parameter).
-	 *
-	 * @private
-	 * @param {String} key Attribute key.
-	 * @param {*} value Attribute value.
-	 * @param {Boolean} [directChange=true] `true` if the change is caused by `Selection` API, `false` if change
-	 * is caused by `Batch` API.
-	 * @returns {Boolean} Whether value has changed.
-	 */
+	// Internal method for setting `LiveSelection` attribute. Supports attribute priorities (through `directChange`
+	// parameter).
+	//
+	// @private
+	// @param {String} key Attribute key.
+	// @param {*} value Attribute value.
+	// @param {Boolean} [directChange=true] `true` if the change is caused by `Selection` API, `false` if change
+	// is caused by `Batch` API.
+	// @returns {Boolean} Whether value has changed.
 	_setAttribute( key, value, directChange = true ) {
 		const priority = directChange ? 'normal' : 'low';
 
@@ -484,17 +711,15 @@ export default class DocumentSelection extends Selection {
 		return true;
 	}
 
-	/**
-	 * Internal method for removing `DocumentSelection` attribute. Supports attribute priorities (through `directChange`
-	 * parameter).
-	 *
-	 * @private
-	 * @param {String} key Attribute key.
-	 * @param {Boolean} [directChange=true] `true` if the change is caused by `Selection` API, `false` if change
-	 * is caused by `Batch` API.
-	 * @returns {Boolean} Whether attribute was removed. May not be true if such attributes didn't exist or the
-	 * existing attribute had higher priority.
-	 */
+	// Internal method for removing `LiveSelection` attribute. Supports attribute priorities (through `directChange`
+	// parameter).
+	//
+	// @private
+	// @param {String} key Attribute key.
+	// @param {Boolean} [directChange=true] `true` if the change is caused by `Selection` API, `false` if change
+	// is caused by `Batch` API.
+	// @returns {Boolean} Whether attribute was removed. May not be true if such attributes didn't exist or the
+	// existing attribute had higher priority.
 	_removeAttribute( key, directChange = true ) {
 		const priority = directChange ? 'normal' : 'low';
 
@@ -516,17 +741,13 @@ export default class DocumentSelection extends Selection {
 		return true;
 	}
 
-	/**
-	 * Internal method for setting multiple `DocumentSelection` attributes. Supports attribute priorities (through
-	 * `directChange` parameter).
-	 *
-	 * @private
-	 * @param {Map} attrs Iterable object containing attributes to be set.
-	 * @param {Boolean} [directChange=true] `true` if the change is caused by `Selection` API, `false` if change
-	 * is caused by `Batch` API.
-	 * @returns {Set.<String>} Changed attribute keys.
-	 */
-	_setAttributesTo( attrs, directChange = true ) {
+	// Internal method for setting multiple `LiveSelection` attributes. Supports attribute priorities (through
+	// `directChange` parameter).
+	//
+	// @private
+	// @param {Map.<String,*>} attrs Iterable object containing attributes to be set.
+	// @returns {Set.<String>} Changed attribute keys.
+	_setAttributesTo( attrs ) {
 		const changed = new Set();
 
 		for ( const [ oldKey, oldValue ] of this.getAttributes() ) {
@@ -535,15 +756,13 @@ export default class DocumentSelection extends Selection {
 				continue;
 			}
 
-			// Attribute still might not get removed because of priorities.
-			if ( this._removeAttribute( oldKey, directChange ) ) {
-				changed.add( oldKey );
-			}
+			// All rest attributes will be removed so changed attributes won't change .
+			this._removeAttribute( oldKey, false );
 		}
 
 		for ( const [ key, value ] of attrs ) {
 			// Attribute may not be set because of attributes or because same key/value is already added.
-			const gotAdded = this._setAttribute( key, value, directChange );
+			const gotAdded = this._setAttribute( key, value, false );
 
 			if ( gotAdded ) {
 				changed.add( key );
@@ -553,12 +772,10 @@ export default class DocumentSelection extends Selection {
 		return changed;
 	}
 
-	/**
-	 * Returns an iterator that iterates through all selection attributes stored in current selection's parent.
-	 *
-	 * @private
-	 * @returns {Iterable.<*>}
-	 */
+	// Returns an iterable that iterates through all selection attributes stored in current selection's parent.
+	//
+	// @protected
+	// @returns {Iterable.<*>}
 	* _getStoredAttributes() {
 		const selectionParent = this.getFirstPosition().parent;
 
@@ -573,67 +790,12 @@ export default class DocumentSelection extends Selection {
 		}
 	}
 
-	/**
-	 * Removes attribute with given key from attributes stored in current selection's parent node.
-	 *
-	 * @private
-	 * @param {String} key Key of attribute to remove.
-	 */
-	_removeStoredAttribute( key ) {
-		const storeKey = DocumentSelection._getStoreAttributeKey( key );
-
-		this._model.change( writer => {
-			writer.removeAttribute( storeKey, this.anchor.parent );
-		} );
-	}
-
-	/**
-	 * Stores given attribute key and value in current selection's parent node.
-	 *
-	 * @private
-	 * @param {String} key Key of attribute to set.
-	 * @param {*} value Attribute value.
-	 */
-	_storeAttribute( key, value ) {
-		const storeKey = DocumentSelection._getStoreAttributeKey( key );
-
-		this._model.change( writer => {
-			writer.setAttribute( storeKey, value, this.anchor.parent );
-		} );
-	}
-
-	/**
-	 * Sets selection attributes stored in current selection's parent node to given set of attributes.
-	 *
-	 * @private
-	 * @param {Iterable} attrs Iterable object containing attributes to be set.
-	 */
-	_setStoredAttributesTo( attrs ) {
-		const selectionParent = this.anchor.parent;
-
-		this._model.change( writer => {
-			for ( const [ oldKey ] of this._getStoredAttributes() ) {
-				const storeKey = DocumentSelection._getStoreAttributeKey( oldKey );
-
-				writer.removeAttribute( storeKey, selectionParent );
-			}
-
-			for ( const [ key, value ] of attrs ) {
-				const storeKey = DocumentSelection._getStoreAttributeKey( key );
-
-				writer.setAttribute( storeKey, value, selectionParent );
-			}
-		} );
-	}
-
-	/**
-	 * Checks model text nodes that are closest to the selection's first position and returns attributes of first
-	 * found element. If there are no text nodes in selection's first position parent, it returns selection
-	 * attributes stored in that parent.
-	 *
-	 * @private
-	 * @returns {Iterable.<*>} Collection of attributes.
-	 */
+	// Checks model text nodes that are closest to the selection's first position and returns attributes of first
+	// found element. If there are no text nodes in selection's first position parent, it returns selection
+	// attributes stored in that parent.
+	//
+	// @private
+	// @returns {Iterable.<*>} Collection of attributes.
 	_getSurroundingAttributes() {
 		const position = this.getFirstPosition();
 		const schema = this._model.schema;
@@ -700,20 +862,18 @@ export default class DocumentSelection extends Selection {
 		return attrs;
 	}
 
-	/**
-	 * Fixes a selection range after it ends up in graveyard root.
-	 *
-	 * @private
-	 * @param {module:engine/model/liverange~LiveRange} liveRange The range from selection, that ended up in the graveyard root.
-	 * @param {module:engine/model/position~Position} removedRangeStart Start position of a range which was removed.
-	 */
+	// Fixes a selection range after it ends up in graveyard root.
+	//
+	// @private
+	// @param {module:engine/model/liverange~LiveRange} liveRange The range from selection, that ended up in the graveyard root.
+	// @param {module:engine/model/position~Position} removedRangeStart Start position of a range which was removed.
 	_fixGraveyardSelection( liveRange, removedRangeStart ) {
 		// The start of the removed range is the closest position to the `liveRange` - the original selection range.
 		// This is a good candidate for a fixed selection range.
 		const positionCandidate = Position.createFromPosition( removedRangeStart );
 
 		// Find a range that is a correct selection range and is closest to the start of removed range.
-		const selectionRange = this._document.getNearestSelectionRange( positionCandidate );
+		const selectionRange = this._model.schema.getNearestSelectionRange( positionCandidate );
 
 		// Remove the old selection range before preparing and adding new selection range. This order is important,
 		// because new range, in some cases, may intersect with old range (it depends on `getNearestSelectionRange()` result).
@@ -736,12 +896,12 @@ export default class DocumentSelection extends Selection {
 	}
 }
 
-// Helper function for {@link module:engine/model/documentselection~DocumentSelection#_updateAttributes}.
+// Helper function for {@link module:engine/model/liveselection~LiveSelection#_updateAttributes}.
 //
 // It takes model item, checks whether it is a text node (or text proxy) and, if so, returns it's attributes. If not, returns `null`.
 //
 // @param {module:engine/model/item~Item|null}  node
-// @returns {Boolean|Iterable}
+// @returns {Boolean}
 function getAttrsIfCharacter( node ) {
 	if ( node instanceof TextProxy || node instanceof Text ) {
 		return node.getAttributes();

+ 145 - 0
packages/ckeditor5-engine/src/model/schema.js

@@ -12,6 +12,9 @@ import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 
 import Range from './range';
+import Position from './position';
+import Element from './element';
+import TreeWalker from './treewalker';
 
 /**
  * The model's schema. It defines allowed and disallowed structures of nodes as well as nodes' attributes.
@@ -410,6 +413,57 @@ export default class Schema {
 		return def.allowAttributes.includes( attributeName );
 	}
 
+	/**
+	 * Checks whether the given element (`elementToMerge`) can be merged with the specified base element (`positionOrBaseElement`).
+	 *
+	 * In other words – whether `elementToMerge`'s children {@link #checkChild are allowed} in the `positionOrBaseElement`.
+	 *
+	 * This check ensures that elements merged with {@link module:engine/model/writer~Writer#merge `Writer#merge()`}
+	 * will be valid.
+	 *
+	 * Instead of elements, you can pass the instance of {@link module:engine/model/position~Position} class as the `positionOrBaseElement`.
+	 * It means that the elements before and after the position will be checked whether they can be merged.
+	 *
+	 * @param {module:engine/model/position~Position|module:engine/model/element~Element} positionOrBaseElement The position or base
+	 * element to which the `elementToMerge` will be merged.
+	 * @param {module:engine/model/element~Element} elementToMerge The element to merge. Required if `positionOrBaseElement` is a element.
+	 * @returns {Boolean}
+	 */
+	checkMerge( positionOrBaseElement, elementToMerge = null ) {
+		if ( positionOrBaseElement instanceof Position ) {
+			const nodeBefore = positionOrBaseElement.nodeBefore;
+			const nodeAfter = positionOrBaseElement.nodeAfter;
+
+			if ( !( nodeBefore instanceof Element ) ) {
+				/**
+				 * The node before the merge position must be an element.
+				 *
+				 * @error schema-check-merge-no-element-before
+				 */
+				throw new CKEditorError( 'schema-check-merge-no-element-before: The node before the merge position must be an element.' );
+			}
+
+			if ( !( nodeAfter instanceof Element ) ) {
+				/**
+				 * The node after the merge position must be an element.
+				 *
+				 * @error schema-check-merge-no-element-after
+				 */
+				throw new CKEditorError( 'schema-check-merge-no-element-after: The node after the merge position must be an element.' );
+			}
+
+			return this.checkMerge( nodeBefore, nodeAfter );
+		}
+
+		for ( const child of elementToMerge.getChildren() ) {
+			if ( !this.checkChild( positionOrBaseElement, child ) ) {
+				return false;
+			}
+		}
+
+		return true;
+	}
+
 	/**
 	 * Allows registering a callback to the {@link #checkChild} method calls.
 	 *
@@ -614,6 +668,58 @@ export default class Schema {
 		return validRanges;
 	}
 
+	/**
+	 * Basing on given `position`, finds and returns a {@link module:engine/model/range~Range Range} instance that is
+	 * nearest to that `position` and is a correct range for selection.
+	 *
+	 * Correct selection range might be collapsed - when it's located in position where text node can be placed.
+	 * Non-collapsed range is returned when selection can be placed around element marked as "object" in
+	 * {@link module:engine/model/schema~Schema schema}.
+	 *
+	 * Direction of searching for nearest correct selection range can be specified as:
+	 * * `both` - searching will be performed in both ways,
+	 * * `forward` - searching will be performed only forward,
+	 * * `backward` - searching will be performed only backward.
+	 *
+	 * When valid selection range cannot be found, `null` is returned.
+	 *
+	 * @param {module:engine/model/position~Position} position Reference position where new selection range should be looked for.
+	 * @param {'both'|'forward'|'backward'} [direction='both'] Search direction.
+	 * @returns {module:engine/model/range~Range|null} Nearest selection range or `null` if one cannot be found.
+	 */
+
+	getNearestSelectionRange( position, direction = 'both' ) {
+		// Return collapsed range if provided position is valid.
+		if ( this.checkChild( position, '$text' ) ) {
+			return new Range( position );
+		}
+
+		let backwardWalker, forwardWalker;
+
+		if ( direction == 'both' || direction == 'backward' ) {
+			backwardWalker = new TreeWalker( { startPosition: position, direction: 'backward' } );
+		}
+
+		if ( direction == 'both' || direction == 'forward' ) {
+			forwardWalker = new TreeWalker( { startPosition: position } );
+		}
+
+		for ( const data of combineWalkers( backwardWalker, forwardWalker ) ) {
+			const type = ( data.walker == backwardWalker ? 'elementEnd' : 'elementStart' );
+			const value = data.value;
+
+			if ( value.type == type && this.isObject( value.item ) ) {
+				return Range.createOn( value.item );
+			}
+
+			if ( this.checkChild( value.nextPosition, '$text' ) ) {
+				return new Range( value.nextPosition );
+			}
+		}
+
+		return null;
+	}
+
 	/**
 	 * Removes attributes disallowed by the schema.
 	 *
@@ -1331,3 +1437,42 @@ function mapContextItem( ctxItem ) {
 		};
 	}
 }
+
+// Generator function returning values from provided walkers, switching between them at each iteration. If only one walker
+// is provided it will return data only from that walker.
+//
+// @param {module:engine/module/treewalker~TreeWalker} [backward] Walker iterating in backward direction.
+// @param {module:engine/module/treewalker~TreeWalker} [forward] Walker iterating in forward direction.
+// @returns {Iterable.<Object>} Object returned at each iteration contains `value` and `walker` (informing which walker returned
+// given value) fields.
+function* combineWalkers( backward, forward ) {
+	let done = false;
+
+	while ( !done ) {
+		done = true;
+
+		if ( backward ) {
+			const step = backward.next();
+
+			if ( !step.done ) {
+				done = false;
+				yield {
+					walker: backward,
+					value: step.value
+				};
+			}
+		}
+
+		if ( forward ) {
+			const step = forward.next();
+
+			if ( !step.done ) {
+				done = false;
+				yield {
+					walker: forward,
+					value: step.value
+				};
+			}
+		}
+	}
+}

+ 137 - 215
packages/ckeditor5-engine/src/model/selection.js

@@ -13,29 +13,63 @@ import Range from './range';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
-import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
-import mapsEqual from '@ckeditor/ckeditor5-utils/src/mapsequal';
 import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
 
 /**
  * `Selection` is a group of {@link module:engine/model/range~Range ranges} which has a direction specified by
  * {@link module:engine/model/selection~Selection#anchor anchor} and {@link module:engine/model/selection~Selection#focus focus}.
  * Additionally, `Selection` may have it's own attributes.
+ *
+ * @mixes {module:utils/emittermixin~EmitterMixin}
  */
 export default class Selection {
 	/**
-	 * Creates new selection instance.
+	 * Creates new selection instance on the given
+	 * {@link module:engine/model/selection~Selection selection}, {@link module:engine/model/position~Position position},
+	 * {@link module:engine/model/element~Element element}, {@link module:engine/model/position~Position position},
+	 * {@link module:engine/model/range~Range range}, an iterable of {@link module:engine/model/range~Range ranges}
+	 * or creates an empty selection if no arguments passed.
+	 *
+	 * 		// Creates empty selection without ranges.
+	 *		const selection = new Selection();
+	 *
+	 *		// Creates selection at the given range.
+	 *		const range = new Range( start, end );
+	 *		const selection = new Selection( range, isBackwardSelection );
+	 *
+	 *		// Creates selection at the given ranges
+	 * 		const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ];
+	 *		const selection = new Selection( ranges, isBackwardSelection );
+	 *
+	 *		// Creates selection from the other selection.
+	 *		// Note: It doesn't copies selection attributes.
+	 *		const otherSelection = new Selection();
+	 *		const selection = new Selection( otherSelection );
 	 *
-	 * @param {Iterable.<module:engine/view/range~Range>} [ranges] An optional iterable object of ranges to set.
-	 * @param {Boolean} [isLastBackward] An optional flag describing if last added range was selected forward - from start to end
-	 * (`false`) or backward - from end to start (`true`). Defaults to `false`.
+	 * 		// Creates selection from the given document selection.
+	 *		// Note: It doesn't copies selection attributes.
+	 *		const documentSelection = new DocumentSelection( doc );
+	 *		const selection = new Selection( documentSelection );
+	 *
+	 * 		// Creates selection at the given position.
+	 *		const position = new Position( root, path );
+	 *		const selection = new Selection( position );
+	 *
+	 * 		// Creates selection at the start position of given element.
+	 *		const paragraph = writer.createElement( 'paragraph' );
+	 *		const selection = new Selection( paragraph, offset );
+	 *
+	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
+	 * module:engine/model/position~Position|module:engine/model/element~Element|
+	 * Iterable.<module:engine/model/range~Range>|module:engine/model/range~Range} [selectable]
+	 * @param {Boolean|Number|'before'|'end'|'after'} [backwardSelectionOrOffset]
 	 */
-	constructor( ranges, isLastBackward ) {
+	constructor( selectable, backwardSelectionOrOffset ) {
 		/**
 		 * Specifies whether the last added range was added as a backward or forward range.
 		 *
 		 * @private
-		 * @member {Boolean}
+		 * @type {Boolean}
 		 */
 		this._lastRangeBackward = false;
 
@@ -43,7 +77,7 @@ export default class Selection {
 		 * Stores selection ranges.
 		 *
 		 * @protected
-		 * @member {Array.<module:engine/model/range~Range>}
+		 * @type {Array.<module:engine/model/range~Range>}
 		 */
 		this._ranges = [];
 
@@ -51,12 +85,12 @@ export default class Selection {
 		 * List of attributes set on current selection.
 		 *
 		 * @protected
-		 * @member {Map} module:engine/model/selection~Selection#_attrs
+		 * @type {Map.<String,*>}
 		 */
 		this._attrs = new Map();
 
-		if ( ranges ) {
-			this.setRanges( ranges, isLastBackward );
+		if ( selectable ) {
+			this.setTo( selectable, backwardSelectionOrOffset );
 		}
 	}
 
@@ -121,6 +155,7 @@ export default class Selection {
 	/**
 	 * Returns number of ranges in selection.
 	 *
+	 * @readonly
 	 * @type {Number}
 	 */
 	get rangeCount() {
@@ -131,6 +166,7 @@ export default class Selection {
 	 * Specifies whether the {@link #focus}
 	 * precedes {@link #anchor}.
 	 *
+	 * @readonly
 	 * @type {Boolean}
 	 */
 	get isBackward() {
@@ -174,7 +210,7 @@ export default class Selection {
 	}
 
 	/**
-	 * Returns an iterator that iterates over copies of selection ranges.
+	 * Returns an iterable that iterates over copies of selection ranges.
 	 *
 	 * @returns {Iterable.<module:engine/model/range~Range>}
 	 */
@@ -259,52 +295,85 @@ export default class Selection {
 	}
 
 	/**
-	 * Adds a range to this selection. Added range is copied. This means that passed range is not saved in `Selection`
-	 * instance and operating on it will not change `Selection` state.
-	 *
-	 * Accepts a flag describing in which way the selection is made - passed range might be selected from
-	 * {@link module:engine/model/range~Range#start start} to {@link module:engine/model/range~Range#end end}
-	 * or from {@link module:engine/model/range~Range#end end}
-	 * to {@link module:engine/model/range~Range#start start}.
-	 * The flag is used to set {@link #anchor} and
-	 * {@link #focus} properties.
-	 *
-	 * @fires change:range
-	 * @param {module:engine/model/range~Range} range Range to add.
-	 * @param {Boolean} [isBackward=false] Flag describing if added range was selected forward - from start to end (`false`)
-	 * or backward - from end to start (`true`).
-	 */
-	addRange( range, isBackward = false ) {
-		this._pushRange( range );
-		this._lastRangeBackward = !!isBackward;
-
-		this.fire( 'change:range', { directChange: true } );
-	}
-
-	/**
-	 * Removes all ranges that were added to the selection.
-	 *
-	 * @fires change:range
-	 */
-	removeAllRanges() {
-		if ( this._ranges.length > 0 ) {
-			this._removeAllRanges();
-			this.fire( 'change:range', { directChange: true } );
+	 * Sets this selection's ranges and direction to the specified location based on the given
+	 * {@link module:engine/model/selection~Selection selection}, {@link module:engine/model/position~Position position},
+	 * {@link module:engine/model/element~Element element}, {@link module:engine/model/position~Position position},
+	 * {@link module:engine/model/range~Range range}, an iterable of {@link module:engine/model/range~Range ranges} or null.
+	 *
+	 *		// Sets ranges from the given range.
+	 *		const range = new Range( start, end );
+	 *		selection.setTo( range, isBackwardSelection );
+	 *
+	 *		// Sets ranges from the iterable of ranges.
+	 * 		const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ];
+	 *		selection.setTo( ranges, isBackwardSelection );
+	 *
+	 *		// Sets ranges from the other selection.
+	 *		// Note: It doesn't copies selection attributes.
+	 *		const otherSelection = new Selection();
+	 *		selection.setTo( otherSelection );
+	 *
+	 * 		// Sets ranges from the given document selection's ranges.
+	 *		// Note: It doesn't copies selection attributes.
+	 *		const documentSelection = new DocumentSelection( doc );
+	 *		selection.setTo( documentSelection );
+	 *
+	 * 		// Sets range at the given position.
+	 *		const position = new Position( root, path );
+	 *		selection.setTo( position );
+	 *
+	 * 		// Sets range at the position of given element and optional offset.
+	 *		const paragraph = writer.createElement( 'paragraph' );
+	 *		selection.setTo( paragraph, offset );
+	 *
+	 * 		// Removes all ranges.
+	 *		selection.setTo( null );
+	 *
+	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
+	 * module:engine/model/position~Position|module:engine/model/element~Element|
+	 * Iterable.<module:engine/model/range~Range>|module:engine/model/range~Range|null} selectable
+	 * @param {Boolean|Number|'before'|'end'|'after'} [backwardSelectionOrOffset]
+	 */
+	setTo( selectable, backwardSelectionOrOffset ) {
+		if ( selectable === null ) {
+			this._setRanges( [] );
+		} else if ( selectable instanceof Selection ) {
+			this._setRanges( selectable.getRanges(), selectable.isBackward );
+		} else if ( selectable && selectable._selection instanceof Selection ) {
+			// We assume that the selectable is a DocumentSelection.
+			// It can't be imported here, because it would lead to circular imports.
+			this._setRanges( selectable.getRanges(), selectable.isBackward );
+		} else if ( selectable instanceof Range ) {
+			this._setRanges( [ selectable ], backwardSelectionOrOffset );
+		} else if ( selectable instanceof Position ) {
+			this._setRanges( [ new Range( selectable ) ] );
+		} else if ( selectable instanceof Element ) {
+			this._setRanges( [ Range.createCollapsedAt( selectable, backwardSelectionOrOffset ) ] );
+		} else if ( isIterable( selectable ) ) {
+			// We assume that the selectable is an iterable of ranges.
+			this._setRanges( selectable, backwardSelectionOrOffset );
+		} else {
+			/**
+			 * Cannot set selection to given place.
+			 *
+			 * @error model-selection-setTo-not-selectable
+			 */
+			throw new CKEditorError( 'model-selection-setTo-not-selectable: Cannot set selection to given place.' );
 		}
 	}
 
 	/**
 	 * Replaces all ranges that were added to the selection with given array of ranges. Last range of the array
 	 * is treated like the last added range and is used to set {@link module:engine/model/selection~Selection#anchor} and
-	 * {@link module:engine/model/selection~Selection#focus}. Accepts a flag describing in which direction the selection is made
-	 * (see {@link module:engine/model/selection~Selection#addRange}).
+	 * {@link module:engine/model/selection~Selection#focus}. Accepts a flag describing in which direction the selection is made.
 	 *
+	 * @protected
 	 * @fires change:range
 	 * @param {Iterable.<module:engine/model/range~Range>} newRanges Ranges to set.
 	 * @param {Boolean} [isLastBackward=false] Flag describing if last added range was selected forward - from start to end (`false`)
 	 * or backward - from end to start (`true`).
 	 */
-	setRanges( newRanges, isLastBackward = false ) {
+	_setRanges( newRanges, isLastBackward = false ) {
 		newRanges = Array.from( newRanges );
 
 		// Check whether there is any range in new ranges set that is different than all already added ranges.
@@ -334,93 +403,6 @@ export default class Selection {
 		this.fire( 'change:range', { directChange: true } );
 	}
 
-	/**
-	 * Sets this selection's ranges and direction to the specified location based on the given
-	 * {@link module:engine/model/selection~Selection selection}, {@link module:engine/model/position~Position position},
-	 * {@link module:engine/model/range~Range range} or an iterable of {@link module:engine/model/range~Range ranges}.
-	 *
-	 * @param {module:engine/model/selection~Selection|module:engine/model/position~Position|
-	 * Iterable.<module:engine/model/range~Range>|module:engine/model/range~Range} selectable
-	 */
-	setTo( selectable ) {
-		if ( selectable instanceof Selection ) {
-			this.setRanges( selectable.getRanges(), selectable.isBackward );
-		} else if ( selectable instanceof Range ) {
-			this.setRanges( [ selectable ] );
-		} else if ( isIterable( selectable ) ) {
-			// We assume that the selectable is an iterable of ranges.
-			this.setRanges( selectable );
-		} else {
-			// We assume that the selectable is a position.
-			this.setRanges( [ new Range( selectable ) ] );
-		}
-	}
-
-	/**
-	 * Sets this selection in the provided element.
-	 *
-	 * @param {module:engine/model/element~Element} element
-	 */
-	setIn( element ) {
-		this.setRanges( [ Range.createIn( element ) ] );
-	}
-
-	/**
-	 * Sets this selection on the provided item.
-	 *
-	 * @param {module:engine/model/item~Item} item
-	 */
-	setOn( item ) {
-		this.setRanges( [ Range.createOn( item ) ] );
-	}
-
-	/**
-	 * Sets collapsed selection at the specified location.
-	 *
-	 * The location can be specified in the same form as {@link module:engine/model/position~Position.createAt} parameters.
-	 *
-	 * @fires change:range
-	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
-	 * first parameter is a {@link module:engine/model/item~Item model item}.
-	 */
-	setCollapsedAt( itemOrPosition, offset ) {
-		const pos = Position.createAt( itemOrPosition, offset );
-		const range = new Range( pos, pos );
-
-		this.setRanges( [ range ] );
-	}
-
-	/**
-	 * Collapses selection to the selection's {@link module:engine/model/selection~Selection#getFirstPosition first position}.
-	 * All ranges, besides the collapsed one, will be removed. Nothing will change if there are no ranges stored
-	 * inside selection.
-	 *
-	 * @fires change
-	 */
-	collapseToStart() {
-		const startPosition = this.getFirstPosition();
-
-		if ( startPosition !== null ) {
-			this.setRanges( [ new Range( startPosition, startPosition ) ] );
-		}
-	}
-
-	/**
-	 * Collapses selection to the selection's {@link module:engine/model/selection~Selection#getLastPosition last position}.
-	 * All ranges, besides the collapsed one, will be removed. Nothing will change if there are no ranges stored
-	 * inside selection.
-	 *
-	 * @fires change
-	 */
-	collapseToEnd() {
-		const endPosition = this.getLastPosition();
-
-		if ( endPosition !== null ) {
-			this.setRanges( [ new Range( endPosition, endPosition ) ] );
-		}
-	}
-
 	/**
 	 * Moves {@link module:engine/model/selection~Selection#focus} to the specified location.
 	 *
@@ -431,15 +413,15 @@ export default class Selection {
 	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
 	 * first parameter is a {@link module:engine/model/item~Item model item}.
 	 */
-	moveFocusTo( itemOrPosition, offset ) {
+	setFocus( itemOrPosition, offset ) {
 		if ( this.anchor === null ) {
 			/**
 			 * Cannot set selection focus if there are no ranges in selection.
 			 *
-			 * @error model-selection-moveFocusTo-no-ranges
+			 * @error model-selection-setFocus-no-ranges
 			 */
 			throw new CKEditorError(
-				'model-selection-moveFocusTo-no-ranges: Cannot set selection focus if there are no ranges in selection.'
+				'model-selection-setFocus-no-ranges: Cannot set selection focus if there are no ranges in selection.'
 			);
 		}
 
@@ -456,10 +438,14 @@ export default class Selection {
 		}
 
 		if ( newFocus.compareWith( anchor ) == 'before' ) {
-			this.addRange( new Range( newFocus, anchor ), true );
+			this._pushRange( new Range( newFocus, anchor ) );
+			this._lastRangeBackward = true;
 		} else {
-			this.addRange( new Range( anchor, newFocus ) );
+			this._pushRange( new Range( anchor, newFocus ) );
+			this._lastRangeBackward = false;
 		}
+
+		this.fire( 'change:range', { directChange: true } );
 	}
 
 	/**
@@ -473,7 +459,7 @@ export default class Selection {
 	}
 
 	/**
-	 * Returns iterator that iterates over this selection's attributes.
+	 * Returns iterable that iterates over this selection's attributes.
 	 *
 	 * Attributes are returned as arrays containing two items. First one is attribute key and second is attribute value.
 	 * This format is accepted by native `Map` object and also can be passed in `Node` constructor.
@@ -485,7 +471,7 @@ export default class Selection {
 	}
 
 	/**
-	 * Returns iterator that iterates over this selection's attribute keys.
+	 * Returns iterable that iterates over this selection's attribute keys.
 	 *
 	 * @returns {Iterable.<String>}
 	 */
@@ -503,23 +489,6 @@ export default class Selection {
 		return this._attrs.has( key );
 	}
 
-	/**
-	 * Removes all attributes from the selection.
-	 *
-	 * If there were any attributes in selection, fires the {@link #event:change} event with
-	 * removed attributes' keys.
-	 *
-	 * @fires change:attribute
-	 */
-	clearAttributes() {
-		if ( this._attrs.size > 0 ) {
-			const attributeKeys = Array.from( this._attrs.keys() );
-			this._attrs.clear();
-
-			this.fire( 'change:attribute', { attributeKeys, directChange: true } );
-		}
-	}
-
 	/**
 	 * Removes an attribute with given key from the selection.
 	 *
@@ -555,35 +524,6 @@ export default class Selection {
 		}
 	}
 
-	/**
-	 * Removes all attributes from the selection and sets given attributes.
-	 *
-	 * If given set of attributes is different than set of attributes already added to selection, fires
-	 * {@link #event:change change event} with keys of attributes that changed.
-	 *
-	 * @fires event:change:attribute
-	 * @param {Iterable|Object} attrs Iterable object containing attributes to be set.
-	 */
-	setAttributesTo( attrs ) {
-		attrs = toMap( attrs );
-
-		if ( !mapsEqual( attrs, this._attrs ) ) {
-			// Create a set from keys of old and new attributes.
-			const changed = new Set( Array.from( attrs.keys() ).concat( Array.from( this._attrs.keys() ) ) );
-
-			for ( const [ key, value ] of attrs ) {
-				// If the attribute remains unchanged, remove it from changed set.
-				if ( this._attrs.get( key ) === value ) {
-					changed.delete( key );
-				}
-			}
-
-			this._attrs = attrs;
-
-			this.fire( 'change:attribute', { attributeKeys: Array.from( changed ), directChange: true } );
-		}
-	}
-
 	/**
 	 * Returns the selected element. {@link module:engine/model/element~Element Element} is considered as selected if there is only
 	 * one range in the selection, and that range contains exactly one element.
@@ -675,20 +615,6 @@ export default class Selection {
 			limitEndPosition.isTouching( this.getLastPosition() );
 	}
 
-	/**
-	 * Creates and returns an instance of `Selection` that is a clone of given selection, meaning that it has same
-	 * ranges and same direction as this selection.
-	 *
-	 * @params {module:engine/model/selection~Selection} otherSelection Selection to be cloned.
-	 * @returns {module:engine/model/selection~Selection} `Selection` instance that is a clone of given selection.
-	 */
-	static createFromSelection( otherSelection ) {
-		const selection = new this();
-		selection.setTo( otherSelection );
-
-		return selection;
-	}
-
 	/**
 	 * Adds given range to internal {@link #_ranges ranges array}. Throws an error
 	 * if given range is intersecting with any range that is already stored in this selection.
@@ -697,10 +623,6 @@ export default class Selection {
 	 * @param {module:engine/model/range~Range} range Range to add.
 	 */
 	_pushRange( range ) {
-		if ( !( range instanceof Range ) ) {
-			throw new CKEditorError( 'model-selection-added-not-range: Trying to add an object that is not an instance of Range.' );
-		}
-
 		this._checkRange( range );
 		this._ranges.push( Range.createFromRange( range ) );
 	}
@@ -729,20 +651,11 @@ export default class Selection {
 		}
 	}
 
-	/**
-	 * Removes most recently added range from the selection.
-	 *
-	 * @protected
-	 */
-	_popRange() {
-		this._ranges.pop();
-	}
-
 	/**
 	 * Deletes ranges from internal range array. Uses {@link #_popRange _popRange} to
 	 * ensure proper ranges removal.
 	 *
-	 * @private
+	 * @protected
 	 */
 	_removeAllRanges() {
 		while ( this._ranges.length > 0 ) {
@@ -750,6 +663,15 @@ export default class Selection {
 		}
 	}
 
+	/**
+	 * Removes most recently added range from the selection.
+	 *
+	 * @protected
+	 */
+	_popRange() {
+		this._ranges.pop();
+	}
+
 	/**
 	 * @event change
 	 */

+ 14 - 3
packages/ckeditor5-engine/src/model/utils/deletecontent.js

@@ -10,13 +10,15 @@
 import LivePosition from '../liveposition';
 import Position from '../position';
 import Range from '../range';
+import DocumentSelection from '../documentselection';
 
 /**
  * Deletes content of the selection and merge siblings. The resulting selection is always collapsed.
  *
  * @param {module:engine/model/model~Model} model The model in context of which the insertion
  * should be performed.
- * @param {module:engine/model/selection~Selection} selection Selection of which the content should be deleted.
+ * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
+ * Selection of which the content should be deleted.
  * @param {module:engine/model/batch~Batch} batch Batch to which the deltas will be added.
  * @param {Object} [options]
  * @param {Boolean} [options.leaveUnmerged=false] Whether to merge elements after removing the content of the selection.
@@ -82,7 +84,11 @@ export default function deleteContent( model, selection, options = {} ) {
 			schema.removeDisallowedAttributes( startPos.parent.getChildren(), writer );
 		}
 
-		selection.setCollapsedAt( startPos );
+		if ( selection instanceof DocumentSelection ) {
+			writer.setSelection( startPos );
+		} else {
+			selection.setTo( startPos );
+		}
 
 		// 4. Autoparagraphing.
 		// Check if a text is allowed in the new container. If not, try to create a new paragraph (if it's allowed here).
@@ -186,7 +192,12 @@ function insertParagraph( writer, position, selection ) {
 	const paragraph = writer.createElement( 'paragraph' );
 
 	writer.insert( paragraph, position );
-	selection.setCollapsedAt( paragraph );
+
+	if ( selection instanceof DocumentSelection ) {
+		writer.setSelection( paragraph );
+	} else {
+		selection.setTo( paragraph );
+	}
 }
 
 function replaceEntireContentWithParagraph( writer, selection ) {

+ 9 - 3
packages/ckeditor5-engine/src/model/utils/insertcontent.js

@@ -12,6 +12,7 @@ import LivePosition from '../liveposition';
 import Element from '../element';
 import Range from '../range';
 import log from '@ckeditor/ckeditor5-utils/src/log';
+import DocumentSelection from '../documentselection';
 
 /**
  * Inserts content into the editor (specified selection) as one would expect the paste
@@ -25,7 +26,8 @@ import log from '@ckeditor/ckeditor5-utils/src/log';
  * @param {module:engine/model/model~Model} model The model in context of which the insertion
  * should be performed.
  * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert.
- * @param {module:engine/model/selection~Selection} selection Selection into which the content should be inserted.
+ * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
+ * Selection into which the content should be inserted.
  */
 export default function insertContent( model, content, selection ) {
 	model.change( writer => {
@@ -54,7 +56,11 @@ export default function insertContent( model, content, selection ) {
 
 		/* istanbul ignore else */
 		if ( newRange ) {
-			selection.setRanges( [ newRange ] );
+			if ( selection instanceof DocumentSelection ) {
+				writer.setSelection( newRange );
+			} else {
+				selection.setTo( newRange );
+			}
 		} else {
 			// We are not testing else because it's a safe check for unpredictable edge cases:
 			// an insertion without proper range to select.
@@ -155,7 +161,7 @@ class Insertion {
 			return Range.createOn( this.nodeToSelect );
 		}
 
-		return this.model.document.getNearestSelectionRange( this.position );
+		return this.model.schema.getNearestSelectionRange( this.position );
 	}
 
 	/**

+ 8 - 1
packages/ckeditor5-engine/src/model/utils/modifyselection.js

@@ -11,6 +11,7 @@ import Position from '../position';
 import TreeWalker from '../treewalker';
 import Range from '../range';
 import { isInsideSurrogatePair, isInsideCombinedSymbol } from '@ckeditor/ckeditor5-utils/src/unicode';
+import DocumentSelection from '../documentselection';
 
 /**
  * Modifies the selection. Currently, the supported modifications are:
@@ -64,7 +65,13 @@ export default function modifySelection( model, selection, options = {} ) {
 		const position = tryExtendingTo( data, next.value );
 
 		if ( position ) {
-			selection.moveFocusTo( position );
+			if ( selection instanceof DocumentSelection ) {
+				model.change( writer => {
+					writer.setSelectionFocus( position );
+				} );
+			} else {
+				selection.setFocus( position );
+			}
 
 			return;
 		}

+ 155 - 0
packages/ckeditor5-engine/src/model/writer.js

@@ -35,6 +35,7 @@ import Element from './element';
 import RootElement from './rootelement';
 import Position from './position';
 import Range from './range.js';
+import DocumentSelection from './documentselection';
 
 import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
 
@@ -790,8 +791,162 @@ export default class Writer {
 	}
 
 	/**
+	 * Sets this selection's ranges and direction to the specified location based on the given
+	 * {@link module:engine/model/selection~Selection selection}, {@link module:engine/model/position~Position position},
+	 * {@link module:engine/model/element~Element element}, {@link module:engine/model/position~Position position},
+	 * {@link module:engine/model/range~Range range}, an iterable of {@link module:engine/model/range~Range ranges} or null.
+	 *
+	 * Uses internally {@link module:engine/model/documentselection~DocumentSelection#_setTo}.
+	 *
+	 *		// Sets ranges from the given range.
+	 *		const range = new Range( start, end );
+	 *		writer.setSelection( range, isBackwardSelection );
+	 *
+	 *		// Sets ranges from the iterable of ranges.
+	 * 		const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ];
+	 *		writer.setSelection( range, isBackwardSelection );
+	 *
+	 *		// Sets ranges from the other selection.
+	 *		const otherSelection = new Selection();
+	 *		writer.setSelection( otherSelection );
+	 *
+	 * 		// Sets ranges from the given document selection's ranges.
+	 *		const documentSelection = new DocumentSelection( doc );
+	 *		writer.setSelection( documentSelection );
+	 *
+	 * 		// Sets range at the given position.
+	 *		const position = new Position( root, path );
+	 *		writer.setSelection( position );
+	 *
+	 * 		// Sets range at the given element.
+	 *		const paragraph = writer.createElement( 'paragraph' );
+	 *		writer.setSelection( paragraph, offset );
+	 *
+	 * 		// Removes all ranges.
+	 *		writer.setSelection( null );
+	 *
 	 * Throws `writer-incorrect-use` error when the writer is used outside the `change()` block.
 	 *
+	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
+	 * module:engine/model/position~Position|module:engine/model/element~Element|
+	 * Iterable.<module:engine/model/range~Range>|module:engine/model/range~Range|null} selectable
+	 * @param {Boolean|Number|'before'|'end'|'after'} [backwardSelectionOrOffset]
+	 */
+	setSelection( selectable, backwardSelectionOrOffset ) {
+		this._assertWriterUsedCorrectly();
+
+		this.model.document.selection._setTo( selectable, backwardSelectionOrOffset );
+	}
+
+	/**
+	 * Moves {@link module:engine/model/documentselection~DocumentSelection#focus} to the specified location.
+	 *
+	 * The location can be specified in the same form as {@link module:engine/model/position~Position.createAt} parameters.
+	 *
+	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
+	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * first parameter is a {@link module:engine/model/item~Item model item}.
+	 */
+	setSelectionFocus( itemOrPosition, offset ) {
+		this._assertWriterUsedCorrectly();
+
+		this.model.document.selection._setFocus( itemOrPosition, offset );
+	}
+
+	/**
+	 * Sets attribute(s) on the selection. If attribute with the same key already is set, it's value is overwritten.
+	 *
+	 * Using key and value pair:
+	 *
+	 * 	writer.setSelectionAttribute( 'italic', true );
+	 *
+	 * Using key-value object:
+	 *
+	 * 	writer.setSelectionAttribute( { italic: true, bold: false } );
+	 *
+	 * Using iterable object:
+	 *
+	 * 	writer.setSelectionAttribute( new Map( [ [ 'italic', true ] ] ) );
+	 *
+	 * @param {String|Object|Iterable.<*>} keyOrObjectOrIterable Key of the attribute to set
+	 * or object / iterable of key - value attribute pairs.
+	 * @param {*} [value] Attribute value.
+	 */
+	setSelectionAttribute( keyOrObjectOrIterable, value ) {
+		this._assertWriterUsedCorrectly();
+
+		if ( typeof keyOrObjectOrIterable === 'string' ) {
+			this._setSelectionAttribute( keyOrObjectOrIterable, value );
+		} else {
+			for ( const [ key, value ] of toMap( keyOrObjectOrIterable ) ) {
+				this._setSelectionAttribute( key, value );
+			}
+		}
+	}
+
+	/**
+	 * Removes attribute(s) with given key(s) from the selection.
+	 *
+	 * Using key
+	 *
+	 * 	writer.removeSelectionAttribute( 'italic' );
+	 *
+	 * Using iterable of keys
+	 *
+	 * 	writer.removeSelectionAttribute( [ 'italic', 'bold' ] );
+	 *
+	 * @param {String|Iterable.<String>} keyOrIterableOfKeys Key of the attribute to remove or an iterable of attribute keys to remove.
+	 */
+	removeSelectionAttribute( keyOrIterableOfKeys ) {
+		this._assertWriterUsedCorrectly();
+
+		if ( typeof keyOrIterableOfKeys === 'string' ) {
+			this._removeSelectionAttribute( keyOrIterableOfKeys );
+		} else {
+			for ( const key of keyOrIterableOfKeys ) {
+				this._removeSelectionAttribute( key );
+			}
+		}
+	}
+
+	/**
+	 * @private
+	 * @param {String} key Key of the attribute to remove.
+	 * @param {*} value Attribute value.
+	 */
+	_setSelectionAttribute( key, value ) {
+		const selection = this.model.document.selection;
+
+		// Store attribute in parent element if the selection is collapsed in an empty node.
+		if ( selection.isCollapsed && selection.anchor.parent.isEmpty ) {
+			const storeKey = DocumentSelection._getStoreAttributeKey( key );
+
+			this.setAttribute( storeKey, value, selection.anchor.parent );
+		}
+
+		selection._setAttribute( key, value );
+	}
+
+	/**
+	 * @private
+	 * @param {String} key Key of the attribute to remove.
+	 */
+	_removeSelectionAttribute( key ) {
+		const selection = this.model.document.selection;
+
+		// Remove stored attribute from parent element if the selection is collapsed in an empty node.
+		if ( selection.isCollapsed && selection.anchor.parent.isEmpty ) {
+			const storeKey = DocumentSelection._getStoreAttributeKey( key );
+
+			this.removeAttribute( storeKey, selection.anchor.parent );
+		}
+
+		selection._removeAttribute( key );
+	}
+
+	/**
+	 * Throws `writer-detached-writer-tries-to-modify-model` error when the writer is used outside of the `change()` block.
+	 *
 	 * @private
 	 */
 	_assertWriterUsedCorrectly() {

+ 5 - 4
packages/ckeditor5-engine/src/view/domconverter.js

@@ -108,7 +108,7 @@ export default class DomConverter {
 	 * @param {module:engine/view/selection~Selection} viewSelection
 	 */
 	bindFakeSelection( domElement, viewSelection ) {
-		this._fakeSelectionMapping.set( domElement, ViewSelection.createFromSelection( viewSelection ) );
+		this._fakeSelectionMapping.set( domElement, new ViewSelection( viewSelection ) );
 	}
 
 	/**
@@ -471,20 +471,21 @@ export default class DomConverter {
 			}
 		}
 
-		const viewSelection = new ViewSelection();
 		const isBackward = this.isDomSelectionBackward( domSelection );
 
+		const viewRanges = [];
+
 		for ( let i = 0; i < domSelection.rangeCount; i++ ) {
 			// DOM Range have correct start and end, no matter what is the DOM Selection direction. So we don't have to fix anything.
 			const domRange = domSelection.getRangeAt( i );
 			const viewRange = this.domRangeToView( domRange );
 
 			if ( viewRange ) {
-				viewSelection.addRange( viewRange, isBackward );
+				viewRanges.push( viewRange );
 			}
 		}
 
-		return viewSelection;
+		return new ViewSelection( viewRanges, isBackward );
 	}
 
 	/**

+ 3 - 3
packages/ckeditor5-engine/src/view/observer/fakeselectionobserver.js

@@ -82,17 +82,17 @@ export default class FakeSelectionObserver extends Observer {
 	 */
 	_handleSelectionMove( keyCode ) {
 		const selection = this.document.selection;
-		const newSelection = ViewSelection.createFromSelection( selection );
+		const newSelection = new ViewSelection( selection );
 		newSelection.setFake( false );
 
 		// Left or up arrow pressed - move selection to start.
 		if ( keyCode == keyCodes.arrowleft || keyCode == keyCodes.arrowup ) {
-			newSelection.collapseToStart();
+			newSelection.setTo( newSelection.getFirstPosition() );
 		}
 
 		// Right or down arrow pressed - move selection to end.
 		if ( keyCode == keyCodes.arrowright || keyCode == keyCodes.arrowdown ) {
-			newSelection.collapseToEnd();
+			newSelection.setTo( newSelection.getLastPosition() );
 		}
 
 		const data = {

+ 2 - 2
packages/ckeditor5-engine/src/view/observer/mutationobserver.js

@@ -240,8 +240,8 @@ export default class MutationObserver extends Observer {
 			// Anchor and focus has to be properly mapped to view.
 			if ( viewSelectionAnchor && viewSelectionFocus ) {
 				viewSelection = new ViewSelection();
-				viewSelection.setCollapsedAt( viewSelectionAnchor );
-				viewSelection.moveFocusTo( viewSelectionFocus );
+				viewSelection.setTo( viewSelectionAnchor );
+				viewSelection.setFocus( viewSelectionFocus );
 			}
 		}
 

+ 123 - 143
packages/ckeditor5-engine/src/view/selection.js

@@ -13,16 +13,16 @@ import Position from './position';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import Element from './element';
+import Text from './text';
 import count from '@ckeditor/ckeditor5-utils/src/count';
 import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
 
 /**
  * Class representing selection in tree view.
  *
- * Selection can consist of {@link module:engine/view/range~Range ranges} that can be added using
- * {@link module:engine/view/selection~Selection#addRange addRange}
- * and {@link module:engine/view/selection~Selection#setRanges setRanges} methods.
- * Both methods create copies of provided ranges and store those copies internally. Further modifications to passed
+ * Selection can consist of {@link module:engine/view/range~Range ranges} that can be set using
+ * {@link module:engine/view/selection~Selection#setTo} method.
+ * That method create copies of provided ranges and store those copies internally. Further modifications to passed
  * ranges will not change selection's state.
  * Selection's ranges can be obtained via {@link module:engine/view/selection~Selection#getRanges getRanges},
  * {@link module:engine/view/selection~Selection#getFirstRange getFirstRange}
@@ -36,11 +36,34 @@ export default class Selection {
 	/**
 	 * Creates new selection instance.
 	 *
-	 * @param {Iterable.<module:engine/view/range~Range>} [ranges] An optional array of ranges to set.
-	 * @param {Boolean} [isLastBackward] An optional flag describing if last added range was selected forward - from start to end
-	 * (`false`) or backward - from end to start (`true`). Defaults to `false`.
+	 * 		// Creates empty selection without ranges.
+	 *		const selection = new Selection();
+	 *
+	 *		// Creates selection at the given range.
+	 *		const range = new Range( start, end );
+	 *		const selection = new Selection( range, isBackwardSelection );
+	 *
+	 *		// Creates selection at the given ranges
+	 * 		const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ];
+	 *		const selection = new Selection( ranges, isBackwardSelection );
+	 *
+	 *		// Creates selection from the other selection.
+	 *		const otherSelection = new Selection();
+	 *		const selection = new Selection( otherSelection );
+	 *
+	 * 		// Creates selection at the given position.
+	 *		const position = new Position( root, path );
+	 *		const selection = new Selection( position );
+	 *
+	 * 		// Creates selection at the start position of given element.
+	 *		const paragraph = writer.createElement( 'paragraph' );
+	 *		const selection = new Selection( paragraph, offset );
+	 *
+	 * @param {module:engine/view/selection~Selection|module:engine/view/position~Position|
+	 * Iterable.<module:engine/view/range~Range>|module:engine/view/range~Range|module:engine/view/item~Item} [selectable]
+	 * @param {Boolean|Number|'before'|'end'|'after'} [backwardSelectionOrOffset]
 	 */
-	constructor( ranges, isLastBackward ) {
+	constructor( selectable, backwardSelectionOrOffset ) {
 		/**
 		 * Stores all ranges that are selected.
 		 *
@@ -73,8 +96,8 @@ export default class Selection {
 		 */
 		this._fakeSelectionLabel = '';
 
-		if ( ranges ) {
-			this.setRanges( ranges, isLastBackward );
+		if ( selectable ) {
+			this.setTo( selectable, backwardSelectionOrOffset );
 		}
 	}
 
@@ -196,33 +219,7 @@ export default class Selection {
 	}
 
 	/**
-	 * Adds a range to the selection. Added range is copied. This means that passed range is not saved in the
-	 * selection instance and you can safely operate on it.
-	 *
-	 * Accepts a flag describing in which way the selection is made - passed range might be selected from
-	 * {@link module:engine/view/range~Range#start start} to {@link module:engine/view/range~Range#end end}
-	 * or from {@link module:engine/view/range~Range#end end} to {@link module:engine/view/range~Range#start start}.
-	 * The flag is used to set {@link #anchor anchor} and {@link #focus focus} properties.
-	 *
-	 * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-selection-range-intersects` if added range intersects
-	 * with ranges already stored in Selection instance.
-	 *
-	 * @fires change
-	 * @param {module:engine/view/range~Range} range
-	 * @param {Boolean} isBackward
-	 */
-	addRange( range, isBackward ) {
-		if ( !( range instanceof Range ) ) {
-			throw new CKEditorError( 'view-selection-invalid-range: Invalid Range.' );
-		}
-
-		this._pushRange( range );
-		this._lastRangeBackward = !!isBackward;
-		this.fire( 'change' );
-	}
-
-	/**
-	 * Returns an iterator that contains copies of all ranges added to the selection.
+	 * Returns an iterable that contains copies of all ranges added to the selection.
 	 *
 	 * @returns {Iterable.<module:engine/view/range~Range>}
 	 */
@@ -397,125 +394,94 @@ export default class Selection {
 	 *
 	 * @fires change
 	 */
-	removeAllRanges() {
+	_removeAllRanges() {
 		if ( this._ranges.length ) {
 			this._ranges = [];
 			this.fire( 'change' );
 		}
 	}
 
-	/**
-	 * Replaces all ranges that were added to the selection with given array of ranges. Last range of the array
-	 * is treated like the last added range and is used to set {@link #anchor anchor} and {@link #focus focus}.
-	 * Accepts a flag describing in which way the selection is made (see {@link #addRange addRange}).
-	 *
-	 * @fires change
-	 * @param {Iterable.<module:engine/view/range~Range>} newRanges Iterable object of ranges to set.
-	 * @param {Boolean} [isLastBackward] Flag describing if last added range was selected forward - from start to end
-	 * (`false`) or backward - from end to start (`true`). Defaults to `false`.
-	 */
-	setRanges( newRanges, isLastBackward ) {
-		this._ranges = [];
-
-		for ( const range of newRanges ) {
-			if ( !( range instanceof Range ) ) {
-				throw new CKEditorError( 'view-selection-invalid-range: Invalid Range.' );
-			}
-
-			this._pushRange( range );
-		}
-
-		this._lastRangeBackward = !!isLastBackward;
-		this.fire( 'change' );
-	}
-
 	/**
 	 * Sets this selection's ranges and direction to the specified location based on the given
 	 * {@link module:engine/view/selection~Selection selection}, {@link module:engine/view/position~Position position},
-	 * {@link module:engine/view/range~Range range} or an iterable of {@link module:engine/view/range~Range ranges}.
+	 * {@link module:engine/view/item~Item item}, {@link module:engine/view/range~Range range},
+	 * an iterable of {@link module:engine/view/range~Range ranges} or null.
+	 *
+	 *		// Sets ranges from the given range.
+	 *		const range = new Range( start, end );
+	 *		selection.setTo( range, isBackwardSelection );
 	 *
+	 *		// Sets ranges from the iterable of ranges.
+	 * 		const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ];
+	 *		selection.setTo( range, isBackwardSelection );
+	 *
+	 *		// Sets ranges from the other selection.
+	 *		const otherSelection = new Selection();
+	 *		selection.setTo( otherSelection );
+	 *
+	 * 		// Sets collapsed range at the given position.
+	 *		const position = new Position( root, path );
+	 *		selection.setTo( position );
+	 *
+	 * 		// Sets collapsed range on the given item.
+	 *		const paragraph = writer.createElement( 'paragraph' );
+	 *		selection.setTo( paragraph, offset );
+	 *
+	 * 		// Removes all ranges.
+	 *		selection.setTo( null );
+
 	 * @param {module:engine/view/selection~Selection|module:engine/view/position~Position|
-	 * Iterable.<module:engine/view/range~Range>|module:engine/view/range~Range} selectable
+	 * Iterable.<module:engine/view/range~Range>|module:engine/view/range~Range|module:engine/view/item~Item|null} selectable
+	 * @param {Boolean|Number|'before'|'end'|'after'} [backwardSelectionOrOffset]
 	 */
-	setTo( selectable ) {
-		if ( selectable instanceof Selection ) {
-			this._isFake = selectable._isFake;
-			this._fakeSelectionLabel = selectable._fakeSelectionLabel;
-			this.setRanges( selectable.getRanges(), selectable.isBackward );
+	setTo( selectable, backwardSelectionOrOffset ) {
+		if ( selectable === null ) {
+			this._removeAllRanges();
+		} else if ( selectable instanceof Selection ) {
+			this._isFake = selectable.isFake;
+			this._fakeSelectionLabel = selectable.fakeSelectionLabel;
+			this._setRanges( selectable.getRanges(), selectable.isBackward );
 		} else if ( selectable instanceof Range ) {
-			this.setRanges( [ selectable ] );
+			this._setRanges( [ selectable ], backwardSelectionOrOffset );
+		} else if ( selectable instanceof Position ) {
+			this._setRanges( [ new Range( selectable ) ] );
+		} else if ( selectable instanceof Text ) {
+			this._setRanges( [ Range.createCollapsedAt( selectable, backwardSelectionOrOffset ) ] );
+		} else if ( selectable instanceof Element ) {
+			this._setRanges( [ Range.createCollapsedAt( selectable, backwardSelectionOrOffset ) ] );
 		} else if ( isIterable( selectable ) ) {
 			// We assume that the selectable is an iterable of ranges.
-			this.setRanges( selectable );
+			this._setRanges( selectable, backwardSelectionOrOffset );
 		} else {
-			// We assume that the selectable is a position.
-			this.setRanges( [ new Range( selectable ) ] );
+			/**
+			 * Cannot set selection to given place.
+			 *
+			 * @error view-selection-setTo-not-selectable
+			 */
+			throw new CKEditorError( 'view-selection-setTo-not-selectable: Cannot set selection to given place.' );
 		}
 	}
 
 	/**
-	 * Sets this selection in the provided element.
-	 *
-	 * @param {module:engine/view/element~Element} element
-	 */
-	setIn( element ) {
-		this.setRanges( [ Range.createIn( element ) ] );
-	}
-
-	/**
-	 * Sets this selection on the provided item.
-	 *
-	 * @param {module:engine/view/item~Item} item
-	 */
-	setOn( item ) {
-		this.setRanges( [ Range.createOn( item ) ] );
-	}
-
-	/**
-	 * Sets collapsed selection at the specified location.
-	 *
-	 * The location can be specified in the same form as {@link module:engine/view/position~Position.createAt} parameters.
-	 *
-	 * @fires change
-	 * @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
-	 * first parameter is a {@link module:engine/view/item~Item view item}.
-	 */
-	setCollapsedAt( itemOrPosition, offset ) {
-		const pos = Position.createAt( itemOrPosition, offset );
-		const range = new Range( pos, pos );
-
-		this.setRanges( [ range ] );
-	}
-
-	/**
-	 * Collapses selection to the selection's {@link #getFirstPosition first position}.
-	 * All ranges, besides the collapsed one, will be removed. Nothing will change if there are no ranges stored
-	 * inside selection.
+	 * Replaces all ranges that were added to the selection with given array of ranges. Last range of the array
+	 * is treated like the last added range and is used to set {@link #anchor anchor} and {@link #focus focus}.
+	 * Accepts a flag describing in which way the selection is made.
 	 *
+	 * @protected
 	 * @fires change
+	 * @param {Iterable.<module:engine/view/range~Range>} newRanges Iterable object of ranges to set.
+	 * @param {Boolean} [isLastBackward] Flag describing if last added range was selected forward - from start to end
+	 * (`false`) or backward - from end to start (`true`). Defaults to `false`.
 	 */
-	collapseToStart() {
-		const startPosition = this.getFirstPosition();
+	_setRanges( newRanges, isLastBackward ) {
+		this._ranges = [];
 
-		if ( startPosition !== null ) {
-			this.setRanges( [ new Range( startPosition, startPosition ) ] );
+		for ( const range of newRanges ) {
+			this._addRange( range );
 		}
-	}
-
-	/**
-	 * Collapses selection to the selection's {@link #getLastPosition last position}.
-	 * All ranges, besides the collapsed one, will be removed. Nothing will change if there are no ranges stored
-	 * inside selection.
-	 *
-	 * @fires change
-	 */
-	collapseToEnd() {
-		const endPosition = this.getLastPosition();
 
-		if ( endPosition !== null ) {
-			this.setRanges( [ new Range( endPosition, endPosition ) ] );
-		}
+		this._lastRangeBackward = !!isLastBackward;
+		this.fire( 'change' );
 	}
 
 	/**
@@ -528,15 +494,15 @@ export default class Selection {
 	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
 	 * first parameter is a {@link module:engine/view/item~Item view item}.
 	 */
-	moveFocusTo( itemOrPosition, offset ) {
+	setFocus( itemOrPosition, offset ) {
 		if ( this.anchor === null ) {
 			/**
 			 * Cannot set selection focus if there are no ranges in selection.
 			 *
-			 * @error view-selection-moveFocusTo-no-ranges
+			 * @error view-selection-setFocus-no-ranges
 			 */
 			throw new CKEditorError(
-				'view-selection-moveFocusTo-no-ranges: Cannot set selection focus if there are no ranges in selection.'
+				'view-selection-setFocus-no-ranges: Cannot set selection focus if there are no ranges in selection.'
 			);
 		}
 
@@ -551,10 +517,12 @@ export default class Selection {
 		this._ranges.pop();
 
 		if ( newFocus.compareWith( anchor ) == 'before' ) {
-			this.addRange( new Range( newFocus, anchor ), true );
+			this._addRange( new Range( newFocus, anchor ), true );
 		} else {
-			this.addRange( new Range( anchor, newFocus ) );
+			this._addRange( new Range( anchor, newFocus ) );
 		}
+
+		this.fire( 'change' );
 	}
 
 	/**
@@ -577,17 +545,29 @@ export default class Selection {
 	}
 
 	/**
-	 * Creates and returns an instance of `Selection` that is a clone of given selection, meaning that it has same
-	 * ranges and same direction as this selection.
+	 * Adds a range to the selection. Added range is copied. This means that passed range is not saved in the
+	 * selection instance and you can safely operate on it.
 	 *
-	 * @params {module:engine/view/selection~Selection} otherSelection Selection to be cloned.
-	 * @returns {module:engine/view/selection~Selection} `Selection` instance that is a clone of given selection.
+	 * Accepts a flag describing in which way the selection is made - passed range might be selected from
+	 * {@link module:engine/view/range~Range#start start} to {@link module:engine/view/range~Range#end end}
+	 * or from {@link module:engine/view/range~Range#end end} to {@link module:engine/view/range~Range#start start}.
+	 * The flag is used to set {@link #anchor anchor} and {@link #focus focus} properties.
+	 *
+	 * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-selection-range-intersects` if added range intersects
+	 * with ranges already stored in Selection instance.
+	 *
+	 * @private
+	 * @fires change
+	 * @param {module:engine/view/range~Range} range
+	 * @param {Boolean} [isBackward]
 	 */
-	static createFromSelection( otherSelection ) {
-		const selection = new Selection();
-		selection.setTo( otherSelection );
+	_addRange( range, isBackward = false ) {
+		if ( !( range instanceof Range ) ) {
+			throw new CKEditorError( 'view-selection-invalid-range: Invalid Range.' );
+		}
 
-		return selection;
+		this._pushRange( range );
+		this._lastRangeBackward = !!isBackward;
 	}
 
 	/**

+ 1 - 1
packages/ckeditor5-engine/src/view/writer.js

@@ -496,7 +496,7 @@ export function wrap( range, attribute, viewSelection = null ) {
 
 		// If wrapping position is equal to view selection, move view selection inside wrapping attribute element.
 		if ( viewSelection && viewSelection.isCollapsed && viewSelection.getFirstPosition().isEqual( range.start ) ) {
-			viewSelection.setRanges( [ new Range( position ) ] );
+			viewSelection.setTo( position );
 		}
 
 		return new Range( position );

+ 22 - 19
packages/ckeditor5-engine/tests/controller/editingcontroller.js

@@ -94,7 +94,10 @@ describe( 'EditingController', () => {
 			buildModelConverter().for( editing.modelToView ).fromMarker( 'marker' ).toHighlight( {} );
 
 			// Note: The below code is highly overcomplicated due to #455.
-			model.document.selection.removeAllRanges();
+			model.change( writer => {
+				writer.setSelection( null );
+			} );
+
 			modelRoot.removeChildren( 0, modelRoot.childCount );
 
 			viewRoot.removeChildren( 0, viewRoot.childCount );
@@ -109,7 +112,7 @@ describe( 'EditingController', () => {
 			model.change( writer => {
 				writer.insert( modelData, model.document.getRoot() );
 
-				model.document.selection.addRange( ModelRange.createFromParentsAndOffsets(
+				writer.setSelection( ModelRange.createFromParentsAndOffsets(
 					modelRoot.getChild( 0 ), 1, modelRoot.getChild( 0 ), 1 )
 				);
 			} );
@@ -131,9 +134,9 @@ describe( 'EditingController', () => {
 			model.change( writer => {
 				writer.split( model.document.selection.getFirstPosition() );
 
-				model.document.selection.setRanges( [
+				writer.setSelection(
 					ModelRange.createFromParentsAndOffsets(	modelRoot.getChild( 1 ), 0, modelRoot.getChild( 1 ), 0 )
-				] );
+				);
 			} );
 
 			expect( getViewData( editing.view ) ).to.equal( '<p>f</p><p>{}oo</p><p></p><p>bar</p>' );
@@ -155,9 +158,9 @@ describe( 'EditingController', () => {
 					ModelRange.createFromPositionAndShift( model.document.selection.getFirstPosition(), 1 )
 				);
 
-				model.document.selection.setRanges( [
+				writer.setSelection(
 					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 1, modelRoot.getChild( 0 ), 1 )
-				] );
+				);
 			} );
 
 			expect( getViewData( editing.view ) ).to.equal( '<p>f{}o</p><p></p><p>bar</p>' );
@@ -189,38 +192,38 @@ describe( 'EditingController', () => {
 		} );
 
 		it( 'should convert collapsed selection', () => {
-			model.change( () => {
-				model.document.selection.setRanges( [
+			model.change( writer => {
+				writer.setSelection(
 					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 2 ), 1, modelRoot.getChild( 2 ), 1 )
-				] );
+				);
 			} );
 
 			expect( getViewData( editing.view ) ).to.equal( '<p>foo</p><p></p><p>b{}ar</p>' );
 		} );
 
 		it( 'should convert not collapsed selection', () => {
-			model.change( () => {
-				model.document.selection.setRanges( [
+			model.change( writer => {
+				writer.setSelection(
 					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 2 ), 1, modelRoot.getChild( 2 ), 2 )
-				] );
+				);
 			} );
 
 			expect( getViewData( editing.view ) ).to.equal( '<p>foo</p><p></p><p>b{a}r</p>' );
 		} );
 
 		it( 'should clear previous selection', () => {
-			model.change( () => {
-				model.document.selection.setRanges( [
+			model.change( writer => {
+				writer.setSelection(
 					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 2 ), 1, modelRoot.getChild( 2 ), 1 )
-				] );
+				);
 			} );
 
 			expect( getViewData( editing.view ) ).to.equal( '<p>foo</p><p></p><p>b{}ar</p>' );
 
-			model.change( () => {
-				model.document.selection.setRanges( [
+			model.change( writer => {
+				writer.setSelection(
 					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 2 ), 2, modelRoot.getChild( 2 ), 2 )
-				] );
+				);
 			} );
 
 			expect( getViewData( editing.view ) ).to.equal( '<p>foo</p><p></p><p>ba{}r</p>' );
@@ -369,7 +372,7 @@ describe( 'EditingController', () => {
 				writer.insert( modelData, modelRoot );
 				p1 = modelRoot.getChild( 0 );
 
-				model.document.selection.addRange( ModelRange.createFromParentsAndOffsets( p1, 0, p1, 0 ) );
+				writer.setSelection( ModelRange.createFromParentsAndOffsets( p1, 0, p1, 0 ) );
 			} );
 
 			mcd = editing.modelToView;

+ 7 - 8
packages/ckeditor5-engine/tests/conversion/buildmodelconverter.js

@@ -184,8 +184,7 @@ describe( 'Model converter builder', () => {
 
 				// Set collapsed selection after "f".
 				const position = new ModelPosition( modelRoot, [ 1 ] );
-				modelDoc.selection.setRanges( [ new ModelRange( position, position ) ] );
-				modelDoc.selection._updateAttributes();
+				writer.setSelection( new ModelRange( position, position ) );
 			} );
 
 			// Check if view structure is ok.
@@ -199,8 +198,8 @@ describe( 'Model converter builder', () => {
 			expect( ranges[ 0 ].start.offset ).to.equal( 1 );
 
 			// Change selection attribute, convert it.
-			model.change( () => {
-				modelDoc.selection.setAttribute( 'italic', 'i' );
+			model.change( writer => {
+				writer.setSelectionAttribute( 'italic', 'i' );
 			} );
 
 			// Check if view structure has changed.
@@ -214,8 +213,8 @@ describe( 'Model converter builder', () => {
 			expect( ranges[ 0 ].start.offset ).to.equal( 0 );
 
 			// Some more tests checking how selection attributes changes are converted:
-			model.change( () => {
-				modelDoc.selection.removeAttribute( 'italic' );
+			model.change( writer => {
+				writer.removeSelectionAttribute( 'italic' );
 			} );
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div><em>f</em><em>oo</em></div>' );
@@ -223,8 +222,8 @@ describe( 'Model converter builder', () => {
 			expect( ranges[ 0 ].start.parent.name ).to.equal( 'div' );
 			expect( ranges[ 0 ].start.offset ).to.equal( 1 );
 
-			model.change( () => {
-				modelDoc.selection.setAttribute( 'italic', 'em' );
+			model.change( writer => {
+				writer.setSelectionAttribute( 'italic', 'em' );
 			} );
 
 			expect( viewToString( viewRoot ) ).to.equal( '<div><em>foo</em></div>' );

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

@@ -37,13 +37,13 @@ import { stringify as stringifyView } from '../../src/dev-utils/view';
 import { setData as setModelData } from '../../src/dev-utils/model';
 
 describe( 'model-selection-to-view-converters', () => {
-	let dispatcher, mapper, model, modelDoc, modelRoot, modelSelection, viewDoc, viewRoot, viewSelection, highlightDescriptor;
+	let dispatcher, mapper, model, modelDoc, modelRoot, docSelection, viewDoc, viewRoot, viewSelection, highlightDescriptor;
 
 	beforeEach( () => {
 		model = new Model();
 		modelDoc = model.document;
 		modelRoot = modelDoc.createRoot();
-		modelSelection = modelDoc.selection;
+		docSelection = modelDoc.selection;
 
 		model.schema.extend( '$text', { allowIn: '$root' } );
 
@@ -196,10 +196,9 @@ describe( 'model-selection-to-view-converters', () => {
 				setModelData( model, 'fo<$text bold="true">ob</$text>ar' );
 				const marker = model.markers.set( 'marker', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 5 ) );
 
-				modelSelection.setRanges( [ new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) ] );
-
-				// Update selection attributes according to model.
-				modelSelection.refreshAttributes();
+				model.change( writer => {
+					writer.setSelection( new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) );
+				} );
 
 				// Remove view children manually (without firing additional conversion).
 				viewRoot.removeChildren( 0, viewRoot.childCount );
@@ -208,8 +207,8 @@ describe( 'model-selection-to-view-converters', () => {
 				dispatcher.convertInsert( ModelRange.createIn( modelRoot ) );
 				dispatcher.convertMarkerAdd( marker.name, marker.getRange() );
 
-				const markers = Array.from( model.markers.getMarkersAtPosition( modelSelection.getFirstPosition() ) );
-				dispatcher.convertSelection( modelSelection, markers );
+				const markers = Array.from( model.markers.getMarkersAtPosition( docSelection.getFirstPosition() ) );
+				dispatcher.convertSelection( docSelection, markers );
 
 				// Stringify view and check if it is same as expected.
 				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ).to.equal(
@@ -221,12 +220,10 @@ describe( 'model-selection-to-view-converters', () => {
 				setModelData( model, 'fo<$text bold="true">ob</$text>ar' );
 				const marker = model.markers.set( 'marker', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 5 ) );
 
-				modelSelection.setRanges( [ new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) ] );
-
-				// Update selection attributes according to model.
-				modelSelection.refreshAttributes();
-
-				modelSelection.removeAttribute( 'bold' );
+				model.change( writer => {
+					writer.setSelection( new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) );
+					writer.removeSelectionAttribute( 'bold' );
+				} );
 
 				// Remove view children manually (without firing additional conversion).
 				viewRoot.removeChildren( 0, viewRoot.childCount );
@@ -235,8 +232,8 @@ describe( 'model-selection-to-view-converters', () => {
 				dispatcher.convertInsert( ModelRange.createIn( modelRoot ) );
 				dispatcher.convertMarkerAdd( marker.name, marker.getRange() );
 
-				const markers = Array.from( model.markers.getMarkersAtPosition( modelSelection.getFirstPosition() ) );
-				dispatcher.convertSelection( modelSelection, markers );
+				const markers = Array.from( model.markers.getMarkersAtPosition( docSelection.getFirstPosition() ) );
+				dispatcher.convertSelection( docSelection, markers );
 
 				// Stringify view and check if it is same as expected.
 				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
@@ -251,7 +248,9 @@ describe( 'model-selection-to-view-converters', () => {
 				setModelData( model, 'foobar' );
 				const marker = model.markers.set( 'marker2', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 5 ) );
 
-				modelSelection.setRanges( [ new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) ] );
+				model.change( writer => {
+					writer.setSelection( new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) );
+				} );
 
 				// Remove view children manually (without firing additional conversion).
 				viewRoot.removeChildren( 0, viewRoot.childCount );
@@ -260,8 +259,8 @@ describe( 'model-selection-to-view-converters', () => {
 				dispatcher.convertInsert( ModelRange.createIn( modelRoot ) );
 				dispatcher.convertMarkerAdd( marker.name, marker.getRange() );
 
-				const markers = Array.from( model.markers.getMarkersAtPosition( modelSelection.getFirstPosition() ) );
-				dispatcher.convertSelection( modelSelection, markers );
+				const markers = Array.from( model.markers.getMarkersAtPosition( docSelection.getFirstPosition() ) );
+				dispatcher.convertSelection( docSelection, markers );
 
 				// Stringify view and check if it is same as expected.
 				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
@@ -274,7 +273,9 @@ describe( 'model-selection-to-view-converters', () => {
 				setModelData( model, 'foobar' );
 				const marker = model.markers.set( 'marker3', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 5 ) );
 
-				modelSelection.setRanges( [ new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) ] );
+				model.change( writer => {
+					writer.setSelection( new ModelRange( ModelPosition.createAt( modelRoot, 3 ) ) );
+				} );
 
 				// Remove view children manually (without firing additional conversion).
 				viewRoot.removeChildren( 0, viewRoot.childCount );
@@ -283,8 +284,8 @@ describe( 'model-selection-to-view-converters', () => {
 				dispatcher.convertInsert( ModelRange.createIn( modelRoot ) );
 				dispatcher.convertMarkerAdd( marker.name, marker.getRange() );
 
-				const markers = Array.from( model.markers.getMarkersAtPosition( modelSelection.getFirstPosition() ) );
-				dispatcher.convertSelection( modelSelection, markers );
+				const markers = Array.from( model.markers.getMarkersAtPosition( docSelection.getFirstPosition() ) );
+				dispatcher.convertSelection( docSelection, markers );
 
 				// Stringify view and check if it is same as expected.
 				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
@@ -301,11 +302,13 @@ describe( 'model-selection-to-view-converters', () => {
 					new ViewUIElement( 'span' )
 				] );
 
-				modelSelection.setRanges( [ new ModelRange( new ModelPosition( modelRoot, [ 0 ] ) ) ] );
-				modelSelection.setAttribute( 'bold', true );
+				model.change( writer => {
+					writer.setSelection( new ModelRange( new ModelPosition( modelRoot, [ 0 ] ) ) );
+					writer.setSelectionAttribute( 'bold', true );
+				} );
 
 				// Convert model to view.
-				dispatcher.convertSelection( modelSelection, [] );
+				dispatcher.convertSelection( docSelection, [] );
 
 				// Stringify view and check if it is same as expected.
 				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
@@ -316,8 +319,10 @@ describe( 'model-selection-to-view-converters', () => {
 			it( 'selection with attribute before ui element - has non-ui children #1', () => {
 				setModelData( model, 'x' );
 
-				modelSelection.setRanges( [ new ModelRange( new ModelPosition( modelRoot, [ 1 ] ) ) ] );
-				modelSelection.setAttribute( 'bold', true );
+				model.change( writer => {
+					writer.setSelection( new ModelRange( new ModelPosition( modelRoot, [ 1 ] ) ) );
+					writer.setSelectionAttribute( 'bold', true );
+				} );
 
 				// Convert model to view.
 				dispatcher.convertInsert( ModelRange.createIn( modelRoot ) );
@@ -326,7 +331,7 @@ describe( 'model-selection-to-view-converters', () => {
 				const uiElement = new ViewUIElement( 'span' );
 				viewRoot.insertChildren( 1, uiElement );
 
-				dispatcher.convertSelection( modelSelection, [] );
+				dispatcher.convertSelection( docSelection, [] );
 
 				// Stringify view and check if it is same as expected.
 				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
@@ -337,8 +342,10 @@ describe( 'model-selection-to-view-converters', () => {
 			it( 'selection with attribute before ui element - has non-ui children #2', () => {
 				setModelData( model, '<$text bold="true">x</$text>y' );
 
-				modelSelection.setRanges( [ new ModelRange( new ModelPosition( modelRoot, [ 1 ] ) ) ] );
-				modelSelection.setAttribute( 'bold', true );
+				model.change( writer => {
+					writer.setSelection( new ModelRange( new ModelPosition( modelRoot, [ 1 ] ) ) );
+					writer.setSelectionAttribute( 'bold', true );
+				} );
 
 				// Convert model to view.
 				dispatcher.convertInsert( ModelRange.createIn( modelRoot ) );
@@ -347,7 +354,7 @@ describe( 'model-selection-to-view-converters', () => {
 				const uiElement = new ViewUIElement( 'span' );
 				viewRoot.insertChildren( 1, uiElement );
 
-				dispatcher.convertSelection( modelSelection, [] );
+				dispatcher.convertSelection( docSelection, [] );
 
 				// Stringify view and check if it is same as expected.
 				expect( stringifyView( viewRoot, viewSelection, { showType: false } ) )
@@ -422,7 +429,9 @@ describe( 'model-selection-to-view-converters', () => {
 				);
 
 				const modelRange = ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 1 );
-				modelDoc.selection.setRanges( [ modelRange ] );
+				model.change( writer => {
+					writer.setSelection( modelRange );
+				} );
 
 				dispatcher.convertSelection( modelDoc.selection, [] );
 
@@ -444,7 +453,9 @@ describe( 'model-selection-to-view-converters', () => {
 				mergeAttributes( viewSelection.getFirstPosition() );
 
 				const modelRange = ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 1 );
-				modelDoc.selection.setRanges( [ modelRange ] );
+				model.change( writer => {
+					writer.setSelection( modelRange );
+				} );
 
 				dispatcher.convertSelection( modelDoc.selection, [] );
 
@@ -460,7 +471,7 @@ describe( 'model-selection-to-view-converters', () => {
 				dispatcher.on( 'selection', clearFakeSelection() );
 				viewSelection.setFake( true );
 
-				dispatcher.convertSelection( modelSelection, [] );
+				dispatcher.convertSelection( docSelection, [] );
 
 				expect( viewSelection.isFake ).to.be.false;
 			} );
@@ -549,28 +560,27 @@ describe( 'model-selection-to-view-converters', () => {
 		const endPos = new ModelPosition( modelRoot, endPath );
 
 		const isBackward = selectionPaths[ 2 ] === 'backward';
-		modelSelection.setRanges( [ new ModelRange( startPos, endPos ) ], isBackward );
+		model.change( writer => {
+			writer.setSelection( new ModelRange( startPos, endPos ), isBackward );
 
-		// Update selection attributes according to model.
-		modelSelection.refreshAttributes();
+			// And add or remove passed attributes.
+			for ( const key in selectionAttributes ) {
+				const value = selectionAttributes[ key ];
 
-		// And add or remove passed attributes.
-		for ( const key in selectionAttributes ) {
-			const value = selectionAttributes[ key ];
-
-			if ( value ) {
-				modelSelection.setAttribute( key, value );
-			} else {
-				modelSelection.removeAttribute( key );
+				if ( value ) {
+					writer.setSelectionAttribute( key, value );
+				} else {
+					writer.removeSelectionAttribute( key );
+				}
 			}
-		}
+		} );
 
 		// Remove view children manually (without firing additional conversion).
 		viewRoot.removeChildren( 0, viewRoot.childCount );
 
 		// Convert model to view.
 		dispatcher.convertInsert( ModelRange.createIn( modelRoot ) );
-		dispatcher.convertSelection( modelSelection, [] );
+		dispatcher.convertSelection( docSelection, [] );
 
 		// Stringify view and check if it is same as expected.
 		expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ).to.equal( '<div>' + expectedView + '</div>' );

+ 31 - 20
packages/ckeditor5-engine/tests/conversion/modelconversiondispatcher.js

@@ -239,10 +239,12 @@ describe( 'ModelConversionDispatcher', () => {
 			dispatcher.off( 'selection' );
 
 			root.appendChildren( new ModelText( 'foobar' ) );
-			doc.selection.setRanges( [
-				new ModelRange( new ModelPosition( root, [ 1 ] ), new ModelPosition( root, [ 3 ] ) ),
-				new ModelRange( new ModelPosition( root, [ 4 ] ), new ModelPosition( root, [ 5 ] ) )
-			] );
+			model.change( writer => {
+				writer.setSelection( [
+					new ModelRange( new ModelPosition( root, [ 1 ] ), new ModelPosition( root, [ 3 ] ) ),
+					new ModelRange( new ModelPosition( root, [ 4 ] ), new ModelPosition( root, [ 5 ] ) )
+				] );
+			} );
 		} );
 
 		it( 'should fire selection event', () => {
@@ -286,9 +288,11 @@ describe( 'ModelConversionDispatcher', () => {
 		} );
 
 		it( 'should fire attributes events for collapsed selection', () => {
-			doc.selection.setRanges( [
-				new ModelRange( new ModelPosition( root, [ 2 ] ), new ModelPosition( root, [ 2 ] ) )
-			] );
+			model.change( writer => {
+				writer.setSelection(
+					new ModelRange( new ModelPosition( root, [ 2 ] ), new ModelPosition( root, [ 2 ] ) )
+				);
+			} );
 
 			model.change( writer => {
 				writer.setAttribute( 'bold', true, ModelRange.createIn( root ) );
@@ -302,9 +306,11 @@ describe( 'ModelConversionDispatcher', () => {
 		} );
 
 		it( 'should not fire attributes events if attribute has been consumed', () => {
-			doc.selection.setRanges( [
-				new ModelRange( new ModelPosition( root, [ 2 ] ), new ModelPosition( root, [ 2 ] ) )
-			] );
+			model.change( writer => {
+				writer.setSelection(
+					new ModelRange( new ModelPosition( root, [ 2 ] ), new ModelPosition( root, [ 2 ] ) )
+				);
+			} );
 
 			model.change( writer => {
 				writer.setAttribute( 'bold', true, ModelRange.createIn( root ) );
@@ -323,9 +329,11 @@ describe( 'ModelConversionDispatcher', () => {
 		} );
 
 		it( 'should fire events for markers for collapsed selection', () => {
-			doc.selection.setRanges( [
-				new ModelRange( new ModelPosition( root, [ 1 ] ), new ModelPosition( root, [ 1 ] ) )
-			] );
+			model.change( writer => {
+				writer.setSelection(
+					new ModelRange( new ModelPosition( root, [ 1 ] ), new ModelPosition( root, [ 1 ] ) )
+				);
+			} );
 
 			model.markers.set( 'name', ModelRange.createFromParentsAndOffsets( root, 0, root, 2 ) );
 
@@ -362,8 +370,8 @@ describe( 'ModelConversionDispatcher', () => {
 			const viewFigure = new ViewContainerElement( 'figure', null, viewCaption );
 
 			// Create custom highlight handler mock.
-			viewFigure.setCustomProperty( 'addHighlight', () => {} );
-			viewFigure.setCustomProperty( 'removeHighlight', () => {} );
+			viewFigure.setCustomProperty( 'addHighlight', () => { } );
+			viewFigure.setCustomProperty( 'removeHighlight', () => { } );
 
 			// Create mapper mock.
 			dispatcher.conversionApi.mapper = {
@@ -377,8 +385,9 @@ describe( 'ModelConversionDispatcher', () => {
 			};
 
 			model.markers.set( 'name', ModelRange.createFromParentsAndOffsets( root, 0, root, 1 ) );
-			doc.selection.setRanges( [ ModelRange.createFromParentsAndOffsets( caption, 1, caption, 1 ) ] );
-
+			model.change( writer => {
+				writer.setSelection( ModelRange.createFromParentsAndOffsets( caption, 1, caption, 1 ) );
+			} );
 			sinon.spy( dispatcher, 'fire' );
 
 			const markers = Array.from( model.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) );
@@ -389,9 +398,11 @@ describe( 'ModelConversionDispatcher', () => {
 		} );
 
 		it( 'should not fire events if information about marker has been consumed', () => {
-			doc.selection.setRanges( [
-				new ModelRange( new ModelPosition( root, [ 1 ] ), new ModelPosition( root, [ 1 ] ) )
-			] );
+			model.change( writer => {
+				writer.setSelection(
+					new ModelRange( new ModelPosition( root, [ 1 ] ), new ModelPosition( root, [ 1 ] ) )
+				);
+			} );
 
 			model.markers.set( 'foo', ModelRange.createFromParentsAndOffsets( root, 0, root, 2 ) );
 			model.markers.set( 'bar', ModelRange.createFromParentsAndOffsets( root, 0, root, 2 ) );

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

@@ -45,7 +45,7 @@ describe( 'convertSelectionChange', () => {
 
 	it( 'should convert collapsed selection', () => {
 		const viewSelection = new ViewSelection();
-		viewSelection.addRange( ViewRange.createFromParentsAndOffsets(
+		viewSelection.setTo( ViewRange.createFromParentsAndOffsets(
 			viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 1 ) );
 
 		convertSelection( null, { newSelection: viewSelection } );
@@ -61,10 +61,9 @@ describe( 'convertSelectionChange', () => {
 		// Re-bind elements that were just re-set.
 		mapper.bindElements( modelRoot.getChild( 0 ), viewRoot.getChild( 0 ) );
 
-		const viewSelection = new ViewSelection();
-		viewSelection.addRange(
+		const viewSelection = new ViewSelection( [
 			ViewRange.createFromParentsAndOffsets( viewRoot.getChild( 0 ).getChild( 0 ), 2, viewRoot.getChild( 0 ).getChild( 0 ), 6 )
-		);
+		] );
 
 		convertSelection( null, { newSelection: viewSelection } );
 
@@ -72,11 +71,12 @@ describe( 'convertSelectionChange', () => {
 	} );
 
 	it( 'should convert multi ranges selection', () => {
-		const viewSelection = new ViewSelection();
-		viewSelection.addRange( ViewRange.createFromParentsAndOffsets(
-			viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 2 ) );
-		viewSelection.addRange( ViewRange.createFromParentsAndOffsets(
-			viewRoot.getChild( 1 ).getChild( 0 ), 1, viewRoot.getChild( 1 ).getChild( 0 ), 2 ) );
+		const viewSelection = new ViewSelection( [
+			ViewRange.createFromParentsAndOffsets(
+				viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 2 ),
+			ViewRange.createFromParentsAndOffsets(
+				viewRoot.getChild( 1 ).getChild( 0 ), 1, viewRoot.getChild( 1 ).getChild( 0 ), 2 )
+		] );
 
 		convertSelection( null, { newSelection: viewSelection } );
 
@@ -98,11 +98,12 @@ describe( 'convertSelectionChange', () => {
 	} );
 
 	it( 'should convert reverse selection', () => {
-		const viewSelection = new ViewSelection();
-		viewSelection.addRange( ViewRange.createFromParentsAndOffsets(
-			viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 2 ), true );
-		viewSelection.addRange( ViewRange.createFromParentsAndOffsets(
-			viewRoot.getChild( 1 ).getChild( 0 ), 1, viewRoot.getChild( 1 ).getChild( 0 ), 2 ), true );
+		const viewSelection = new ViewSelection( [
+			ViewRange.createFromParentsAndOffsets(
+				viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 2 ),
+			ViewRange.createFromParentsAndOffsets(
+				viewRoot.getChild( 1 ).getChild( 0 ), 1, viewRoot.getChild( 1 ).getChild( 0 ), 2 )
+		], true );
 
 		convertSelection( null, { newSelection: viewSelection } );
 
@@ -111,9 +112,10 @@ describe( 'convertSelectionChange', () => {
 	} );
 
 	it( 'should not enqueue changes if selection has not changed', () => {
-		const viewSelection = new ViewSelection();
-		viewSelection.addRange( ViewRange.createFromParentsAndOffsets(
-			viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 1 ) );
+		const viewSelection = new ViewSelection( [
+			ViewRange.createFromParentsAndOffsets(
+				viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 1 )
+		] );
 
 		convertSelection( null, { newSelection: viewSelection } );
 

+ 56 - 33
packages/ckeditor5-engine/tests/dev-utils/model.js

@@ -21,7 +21,10 @@ describe( 'model test utils', () => {
 		root = document.createRoot();
 		selection = document.selection;
 		sandbox = sinon.sandbox.create();
-		selection.removeAllRanges();
+
+		model.change( writer => {
+			writer.setSelection( null );
+		} );
 
 		model.schema.register( 'a', {
 			allowWhere: '$text',
@@ -64,8 +67,9 @@ describe( 'model test utils', () => {
 		it( 'should use stringify method with selection', () => {
 			const stringifySpy = sandbox.spy( getData, '_stringify' );
 			root.appendChildren( new Element( 'b', null, new Text( 'btext' ) ) );
-			document.selection.addRange( Range.createFromParentsAndOffsets( root, 0, root, 1 ) );
-
+			model.change( writer => {
+				writer.setSelection( Range.createFromParentsAndOffsets( root, 0, root, 1 ) );
+			} );
 			expect( getData( model ) ).to.equal( '[<b>btext</b>]' );
 			sinon.assert.calledOnce( stringifySpy );
 			sinon.assert.calledWithExactly( stringifySpy, root, document.selection );
@@ -278,7 +282,9 @@ describe( 'model test utils', () => {
 
 			it( 'writes selection in an empty root', () => {
 				const root = document.createRoot( '$root', 'empty' );
-				selection.setCollapsedAt( root );
+				model.change( writer => {
+					writer.setSelection( root );
+				} );
 
 				expect( stringify( root, selection ) ).to.equal(
 					'[]'
@@ -286,7 +292,9 @@ describe( 'model test utils', () => {
 			} );
 
 			it( 'writes selection collapsed in an element', () => {
-				selection.setCollapsedAt( root );
+				model.change( writer => {
+					writer.setSelection( root );
+				} );
 
 				expect( stringify( root, selection ) ).to.equal(
 					'[]<a></a>foo<$text bold="true">bar</$text><b></b>'
@@ -294,7 +302,9 @@ describe( 'model test utils', () => {
 			} );
 
 			it( 'writes selection collapsed in a text', () => {
-				selection.setCollapsedAt( root, 3 );
+				model.change( writer => {
+					writer.setSelection( root, 3 );
+				} );
 
 				expect( stringify( root, selection ) ).to.equal(
 					'<a></a>fo[]o<$text bold="true">bar</$text><b></b>'
@@ -302,7 +312,9 @@ describe( 'model test utils', () => {
 			} );
 
 			it( 'writes selection collapsed at the text left boundary', () => {
-				selection.setCollapsedAt( elA, 'after' );
+				model.change( writer => {
+					writer.setSelection( elA, 'after' );
+				} );
 
 				expect( stringify( root, selection ) ).to.equal(
 					'<a></a>[]foo<$text bold="true">bar</$text><b></b>'
@@ -310,7 +322,9 @@ describe( 'model test utils', () => {
 			} );
 
 			it( 'writes selection collapsed at the text right boundary', () => {
-				selection.setCollapsedAt( elB, 'before' );
+				model.change( writer => {
+					writer.setSelection( elB, 'before' );
+				} );
 
 				expect( stringify( root, selection ) ).to.equal(
 					'<a></a>foo<$text bold="true">bar[]</$text><b></b>'
@@ -318,10 +332,12 @@ describe( 'model test utils', () => {
 			} );
 
 			it( 'writes selection collapsed at the end of the root', () => {
-				selection.setCollapsedAt( root, 'end' );
+				model.change( writer => {
+					writer.setSelection( root, 'end' );
 
-				// Needed due to https://github.com/ckeditor/ckeditor5-engine/issues/320.
-				selection.clearAttributes();
+					// Needed due to https://github.com/ckeditor/ckeditor5-engine/issues/320.
+					writer.removeSelectionAttribute( model.document.selection.getAttributeKeys() );
+				} );
 
 				expect( stringify( root, selection ) ).to.equal(
 					'<a></a>foo<$text bold="true">bar</$text><b></b>[]'
@@ -329,7 +345,9 @@ describe( 'model test utils', () => {
 			} );
 
 			it( 'writes selection collapsed selection in a text with attributes', () => {
-				selection.setCollapsedAt( root, 5 );
+				model.change( writer => {
+					writer.setSelection( root, 5 );
+				} );
 
 				expect( stringify( root, selection ) ).to.equal(
 					'<a></a>foo<$text bold="true">b[]ar</$text><b></b>'
@@ -337,9 +355,9 @@ describe( 'model test utils', () => {
 			} );
 
 			it( 'writes flat selection containing couple of nodes', () => {
-				selection.addRange(
-					Range.createFromParentsAndOffsets( root, 0, root, 4 )
-				);
+				model.change( writer => {
+					writer.setSelection( Range.createFromParentsAndOffsets( root, 0, root, 4 ) );
+				} );
 
 				expect( stringify( root, selection ) ).to.equal(
 					'[<a></a>foo]<$text bold="true">bar</$text><b></b>'
@@ -347,9 +365,9 @@ describe( 'model test utils', () => {
 			} );
 
 			it( 'writes flat selection within text', () => {
-				selection.addRange(
-					Range.createFromParentsAndOffsets( root, 2, root, 3 )
-				);
+				model.change( writer => {
+					writer.setSelection( Range.createFromParentsAndOffsets( root, 2, root, 3 ) );
+				} );
 
 				expect( stringify( root, selection ) ).to.equal(
 					'<a></a>f[o]o<$text bold="true">bar</$text><b></b>'
@@ -357,9 +375,9 @@ describe( 'model test utils', () => {
 			} );
 
 			it( 'writes multi-level selection', () => {
-				selection.addRange(
-					Range.createFromParentsAndOffsets( elA, 0, elB, 0 )
-				);
+				model.change( writer => {
+					writer.setSelection( Range.createFromParentsAndOffsets( elA, 0, elB, 0 ) );
+				} );
 
 				expect( stringify( root, selection ) ).to.equal(
 					'<a>[</a>foo<$text bold="true">bar</$text><b>]</b>'
@@ -367,10 +385,9 @@ describe( 'model test utils', () => {
 			} );
 
 			it( 'writes selection when is backward', () => {
-				selection.addRange(
-					Range.createFromParentsAndOffsets( elA, 0, elB, 0 ),
-					true
-				);
+				model.change( writer => {
+					writer.setSelection( Range.createFromParentsAndOffsets( elA, 0, elB, 0 ), true );
+				} );
 
 				expect( stringify( root, selection ) ).to.equal(
 					'<a>[</a>foo<$text bold="true">bar</$text><b>]</b>'
@@ -381,7 +398,9 @@ describe( 'model test utils', () => {
 				const root = document.createRoot( '$root', 'empty' );
 
 				root.appendChildren( new Text( 'நிலைக்கு' ) );
-				selection.addRange( Range.createFromParentsAndOffsets( root, 2, root, 6 ) );
+				model.change( writer => {
+					writer.setSelection( Range.createFromParentsAndOffsets( root, 2, root, 6 ) );
+				} );
 
 				expect( stringify( root, selection ) ).to.equal( 'நி[லைக்]கு' );
 			} );
@@ -562,10 +581,12 @@ describe( 'model test utils', () => {
 			} );
 
 			it( 'sets selection attributes', () => {
-				const result = parse( 'foo[]bar', model.schema, { selectionAttributes: {
-					bold: true,
-					italic: true
-				} } );
+				const result = parse( 'foo[]bar', model.schema, {
+					selectionAttributes: {
+						bold: true,
+						italic: true
+					}
+				} );
 
 				expect( stringify( result.model, result.selection ) ).to.equal( 'foo<$text bold="true" italic="true">[]</$text>bar' );
 			} );
@@ -583,9 +604,11 @@ describe( 'model test utils', () => {
 			} );
 
 			it( 'sets selection with attribute containing an element', () => {
-				const result = parse( 'x[<a></a>]', model.schema, { selectionAttributes: {
-					bold: true
-				} } );
+				const result = parse( 'x[<a></a>]', model.schema, {
+					selectionAttributes: {
+						bold: true
+					}
+				} );
 
 				expect( stringify( result.model, result.selection ) ).to.equal( 'x[<a></a>]' );
 				expect( result.selection.getAttribute( 'bold' ) ).to.be.true;

+ 10 - 19
packages/ckeditor5-engine/tests/dev-utils/view.js

@@ -61,7 +61,7 @@ describe( 'view test utils', () => {
 				const root = createAttachedRoot( viewDocument, element );
 				root.appendChildren( new Element( 'p' ) );
 
-				viewDocument.selection.addRange( Range.createFromParentsAndOffsets( root, 0, root, 1 ) );
+				viewDocument.selection.setTo( Range.createFromParentsAndOffsets( root, 0, root, 1 ) );
 
 				expect( getData( viewDocument, options ) ).to.equal( '[<p></p>]' );
 				sinon.assert.calledOnce( stringifySpy );
@@ -161,8 +161,7 @@ describe( 'view test utils', () => {
 			const b2 = new Element( 'b', null, text2 );
 			const p = new Element( 'p', null, [ b1, b2 ] );
 			const range = Range.createFromParentsAndOffsets( p, 1, p, 2 );
-			const selection = new Selection();
-			selection.addRange( range );
+			const selection = new Selection( [ range ] );
 			expect( stringify( p, selection ) ).to.equal( '<p><b>foobar</b>[<b>bazqux</b>]</p>' );
 		} );
 
@@ -171,8 +170,7 @@ describe( 'view test utils', () => {
 			const b = new Element( 'b', null, text );
 			const p = new Element( 'p', null, b );
 			const range = Range.createFromParentsAndOffsets( p, 0, text, 4 );
-			const selection = new Selection();
-			selection.addRange( range );
+			const selection = new Selection( [ range ] );
 
 			expect( stringify( p, selection ) ).to.equal( '<p>[<b>நிலை}க்கு</b></p>' );
 		} );
@@ -181,8 +179,7 @@ describe( 'view test utils', () => {
 			const text = new Text( 'foobar' );
 			const p = new Element( 'p', null, text );
 			const range = Range.createFromParentsAndOffsets( p, 0, p, 0 );
-			const selection = new Selection();
-			selection.addRange( range );
+			const selection = new Selection( [ range ] );
 			expect( stringify( p, selection ) ).to.equal( '<p>[]foobar</p>' );
 		} );
 
@@ -193,8 +190,7 @@ describe( 'view test utils', () => {
 			const b2 = new Element( 'b', null, text2 );
 			const p = new Element( 'p', null, [ b1, b2 ] );
 			const range = Range.createFromParentsAndOffsets( text1, 1, text1, 5 );
-			const selection = new Selection();
-			selection.addRange( range );
+			const selection = new Selection( [ range ] );
 			expect( stringify( p, selection ) ).to.equal( '<p><b>f{ooba}r</b><b>bazqux</b></p>' );
 		} );
 
@@ -205,8 +201,7 @@ describe( 'view test utils', () => {
 			const b2 = new Element( 'b', null, text2 );
 			const p = new Element( 'p', null, [ b1, b2 ] );
 			const range = Range.createFromParentsAndOffsets( text1, 1, text1, 5 );
-			const selection = new Selection();
-			selection.addRange( range );
+			const selection = new Selection( [ range ] );
 			expect( stringify( p, selection, { sameSelectionCharacters: true } ) )
 				.to.equal( '<p><b>f[ooba]r</b><b>bazqux</b></p>' );
 		} );
@@ -215,8 +210,7 @@ describe( 'view test utils', () => {
 			const text = new Text( 'foobar' );
 			const p = new Element( 'p', null, text );
 			const range = Range.createFromParentsAndOffsets( text, 0, text, 0 );
-			const selection = new Selection();
-			selection.addRange( range );
+			const selection = new Selection( [ range ] );
 			expect( stringify( p, selection ) ).to.equal( '<p>{}foobar</p>' );
 		} );
 
@@ -227,8 +221,7 @@ describe( 'view test utils', () => {
 			const b2 = new Element( 'b', null, text2 );
 			const p = new Element( 'p', null, [ b1, b2 ] );
 			const range = Range.createFromParentsAndOffsets( p, 0, text2, 5 );
-			const selection = new Selection();
-			selection.addRange( range );
+			const selection = new Selection( [ range ] );
 			expect( stringify( p, selection ) ).to.equal( '<p>[<b>foobar</b><b>bazqu}x</b></p>' );
 		} );
 
@@ -308,8 +301,7 @@ describe( 'view test utils', () => {
 			const p = new Element( 'p', null, [ b1, b2 ] );
 			const range1 = Range.createFromParentsAndOffsets( p, 0, p, 1 );
 			const range2 = Range.createFromParentsAndOffsets( p, 1, p, 1 );
-			const selection = new Selection();
-			selection.setRanges( [ range2, range1 ] );
+			const selection = new Selection( [ range2, range1 ] );
 
 			expect( stringify( p, selection ) ).to.equal( '<p>[<b>foobar</b>][]<b>bazqux</b></p>' );
 		} );
@@ -323,8 +315,7 @@ describe( 'view test utils', () => {
 			const range2 = Range.createFromParentsAndOffsets( text2, 0, text2, 3 );
 			const range3 = Range.createFromParentsAndOffsets( text2, 3, text2, 4 );
 			const range4 = Range.createFromParentsAndOffsets( p, 1, p, 1 );
-			const selection = new Selection();
-			selection.setRanges( [ range1, range2, range3, range4 ] );
+			const selection = new Selection( [ range1, range2, range3, range4 ] );
 
 			expect( stringify( p, selection ) ).to.equal( '<p>[<b>foobar</b>][]{baz}{q}ux</p>' );
 		} );

+ 2 - 233
packages/ckeditor5-engine/tests/model/document.js

@@ -14,7 +14,6 @@ import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import count from '@ckeditor/ckeditor5-utils/src/count';
 import { jsonParseStringify } from './_utils/utils';
-import { setData, getData } from '../../src/dev-utils/model';
 
 describe( 'Document', () => {
 	let model, doc;
@@ -168,236 +167,6 @@ describe( 'Document', () => {
 		} );
 	} );
 
-	describe( 'getNearestSelectionRange()', () => {
-		let selection;
-
-		beforeEach( () => {
-			model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
-
-			model.schema.register( 'emptyBlock', { allowIn: '$root' } );
-
-			model.schema.register( 'widget', {
-				allowIn: '$root',
-				isObject: true
-			} );
-
-			model.schema.register( 'blockWidget', {
-				allowIn: '$root',
-				allowContentOf: '$block',
-				isObject: true
-			} );
-
-			doc.createRoot();
-			selection = doc.selection;
-		} );
-
-		test(
-			'should return collapsed range if text node can be placed at that position - both',
-			'<paragraph>[]</paragraph>',
-			'both',
-			'<paragraph>[]</paragraph>'
-		);
-
-		test(
-			'should return collapsed range if text node can be placed at that position - forward',
-			'<paragraph>[]</paragraph>',
-			'forward',
-			'<paragraph>[]</paragraph>'
-		);
-
-		test(
-			'should return collapsed range if text node can be placed at that position - backward',
-			'<paragraph>[]</paragraph>',
-			'backward',
-			'<paragraph>[]</paragraph>'
-		);
-
-		test( 'should return null in empty document - both', '', 'both', null );
-
-		test( 'should return null in empty document - backward', '', 'backward', null );
-
-		test( 'should return null in empty document - forward', '', 'forward', null );
-
-		test(
-			'should find range before when searching both ways',
-			'<paragraph></paragraph>[]<paragraph></paragraph>',
-			'both',
-			'<paragraph>[]</paragraph><paragraph></paragraph>'
-		);
-
-		test(
-			'should find range before when searching backward',
-			'<paragraph></paragraph>[]<paragraph></paragraph>',
-			'backward',
-			'<paragraph>[]</paragraph><paragraph></paragraph>'
-		);
-
-		test(
-			'should find range after when searching forward',
-			'<paragraph></paragraph>[]<paragraph></paragraph>',
-			'forward',
-			'<paragraph></paragraph><paragraph>[]</paragraph>'
-		);
-
-		test(
-			'should find range after when searching both ways when it is closer',
-			'<paragraph></paragraph><emptyBlock></emptyBlock>[]<paragraph></paragraph>',
-			'both',
-			'<paragraph></paragraph><emptyBlock></emptyBlock><paragraph>[]</paragraph>'
-		);
-
-		test(
-			'should find range before when searching both ways when it is closer',
-			'<paragraph></paragraph><emptyBlock></emptyBlock>[]<emptyBlock></emptyBlock><emptyBlock></emptyBlock><paragraph></paragraph>',
-			'both',
-			'<paragraph>[]</paragraph><emptyBlock></emptyBlock><emptyBlock></emptyBlock><emptyBlock></emptyBlock><paragraph></paragraph>'
-		);
-
-		test(
-			'should return null if there is no valid range',
-			'[]<emptyBlock></emptyBlock>',
-			'both',
-			null
-		);
-
-		test(
-			'should return null if there is no valid range in given direction - backward',
-			'[]<paragraph></paragraph>',
-			'backward',
-			null
-		);
-
-		test(
-			'should return null if there is no valid range in given direction - forward',
-			'<paragraph></paragraph>[]',
-			'forward',
-			null
-		);
-
-		test(
-			'should move forward when placed at root start',
-			'[]<paragraph></paragraph><paragraph></paragraph>',
-			'both',
-			'<paragraph>[]</paragraph><paragraph></paragraph>'
-		);
-
-		test(
-			'should move backward when placed at root end',
-			'<paragraph></paragraph><paragraph></paragraph>[]',
-			'both',
-			'<paragraph></paragraph><paragraph>[]</paragraph>'
-		);
-
-		describe( 'in case of objects which do not allow text inside', () => {
-			test(
-				'should select nearest object (o[]o) - both',
-				'<widget></widget>[]<widget></widget>',
-				'both',
-				'[<widget></widget>]<widget></widget>'
-			);
-
-			test(
-				'should select nearest object (o[]o) - forward',
-				'<widget></widget>[]<widget></widget>',
-				'forward',
-				'<widget></widget>[<widget></widget>]'
-			);
-
-			test(
-				'should select nearest object (o[]o) - backward',
-				'<widget></widget>[]<widget></widget>',
-				'both',
-				'[<widget></widget>]<widget></widget>'
-			);
-
-			test(
-				'should select nearest object (p[]o) - forward',
-				'<paragraph></paragraph>[]<widget></widget>',
-				'forward',
-				'<paragraph></paragraph>[<widget></widget>]'
-			);
-
-			test(
-				'should select nearest object (o[]p) - both',
-				'<widget></widget>[]<paragraph></paragraph>',
-				'both',
-				'[<widget></widget>]<paragraph></paragraph>'
-			);
-
-			test(
-				'should select nearest object (o[]p) - backward',
-				'<widget></widget>[]<paragraph></paragraph>',
-				'backward',
-				'[<widget></widget>]<paragraph></paragraph>'
-			);
-
-			test(
-				'should select nearest object ([]o) - both',
-				'[]<widget></widget><paragraph></paragraph>',
-				'both',
-				'[<widget></widget>]<paragraph></paragraph>'
-			);
-
-			test(
-				'should select nearest object ([]o) - forward',
-				'[]<widget></widget><paragraph></paragraph>',
-				'forward',
-				'[<widget></widget>]<paragraph></paragraph>'
-			);
-
-			test(
-				'should select nearest object (o[]) - both',
-				'<paragraph></paragraph><widget></widget>[]',
-				'both',
-				'<paragraph></paragraph>[<widget></widget>]'
-			);
-
-			test(
-				'should select nearest object (o[]) - backward',
-				'<paragraph></paragraph><widget></widget>[]',
-				'both',
-				'<paragraph></paragraph>[<widget></widget>]'
-			);
-		} );
-
-		describe( 'in case of objects which allow text inside', () => {
-			test(
-				'should select nearest object which allows text (o[]o) - both',
-				'<blockWidget></blockWidget>[]<blockWidget></blockWidget>',
-				'both',
-				'[<blockWidget></blockWidget>]<blockWidget></blockWidget>'
-			);
-
-			test(
-				'should select nearest object (o[]p) - both',
-				'<blockWidget></blockWidget>[]<paragraph></paragraph>',
-				'both',
-				'[<blockWidget></blockWidget>]<paragraph></paragraph>'
-			);
-
-			test(
-				'should select nearest object which allows text ([]o) - both',
-				'[]<blockWidget></blockWidget><paragraph></paragraph>',
-				'both',
-				'[<blockWidget></blockWidget>]<paragraph></paragraph>'
-			);
-		} );
-
-		function test( testName, data, direction, expected ) {
-			it( testName, () => {
-				setData( model, data );
-				const range = doc.getNearestSelectionRange( selection.anchor, direction );
-
-				if ( expected === null ) {
-					expect( range ).to.be.null;
-				} else {
-					selection.setRanges( [ range ] );
-					expect( getData( model ) ).to.equal( expected );
-				}
-			} );
-		}
-	} );
-
 	describe( '_getDefaultRoot()', () => {
 		it( 'should return graveyard root if there are no other roots in the document', () => {
 			expect( doc._getDefaultRoot() ).to.equal( doc.graveyard );
@@ -578,8 +347,8 @@ describe( 'Document', () => {
 
 			doc.on( 'change', spy );
 
-			model.change( () => {
-				doc.selection.setRanges( [ Range.createFromParentsAndOffsets( root, 2, root, 2 ) ] );
+			model.change( writer => {
+				writer.setSelection( Range.createFromParentsAndOffsets( root, 2, root, 2 ) );
 			} );
 
 			expect( spy.calledOnce ).to.be.true;

+ 138 - 193
packages/ckeditor5-engine/tests/model/documentselection.js

@@ -22,8 +22,6 @@ import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import { wrapInDelta } from '../../tests/model/_utils/utils';
 import { setData, getData } from '../../src/dev-utils/model';
 
-import log from '@ckeditor/ckeditor5-utils/src/log';
-
 testUtils.createSinonSandbox();
 
 describe( 'DocumentSelection', () => {
@@ -121,7 +119,7 @@ describe( 'DocumentSelection', () => {
 		} );
 
 		it( 'should back off to the default algorithm if selection has ranges', () => {
-			selection.addRange( range );
+			selection._setTo( range );
 
 			expect( selection.isCollapsed ).to.be.false;
 		} );
@@ -143,7 +141,7 @@ describe( 'DocumentSelection', () => {
 		} );
 
 		it( 'should back off to the default algorithm if selection has ranges', () => {
-			selection.addRange( range );
+			selection._setTo( range );
 
 			expect( selection.anchor.isEqual( range.start ) ).to.be.true;
 		} );
@@ -166,7 +164,7 @@ describe( 'DocumentSelection', () => {
 		} );
 
 		it( 'should back off to the default algorithm if selection has ranges', () => {
-			selection.addRange( range );
+			selection._setTo( range );
 
 			expect( selection.focus.isEqual( range.end ) ).to.be.true;
 		} );
@@ -176,11 +174,14 @@ describe( 'DocumentSelection', () => {
 		it( 'should return proper range count', () => {
 			expect( selection.rangeCount ).to.equal( 1 );
 
-			selection.addRange( new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) );
+			selection._setTo( new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) );
 
 			expect( selection.rangeCount ).to.equal( 1 );
 
-			selection.addRange( new Range( new Position( root, [ 2 ] ), new Position( root, [ 2 ] ) ) );
+			selection._setTo( [
+				new Range( new Position( root, [ 2 ] ), new Position( root, [ 2 ] ) ),
+				new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) )
+			] );
 
 			expect( selection.rangeCount ).to.equal( 2 );
 		} );
@@ -188,7 +189,7 @@ describe( 'DocumentSelection', () => {
 
 	describe( 'hasOwnRange', () => {
 		it( 'should return true if selection has any ranges set', () => {
-			selection.addRange( new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) );
+			selection._setTo( new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) );
 
 			expect( selection.hasOwnRange ).to.be.true;
 		} );
@@ -198,44 +199,12 @@ describe( 'DocumentSelection', () => {
 		} );
 	} );
 
-	describe( 'addRange()', () => {
-		it( 'should convert added Range to LiveRange', () => {
-			selection.addRange( range );
-
-			expect( selection._ranges[ 0 ] ).to.be.instanceof( LiveRange );
-		} );
-
-		it( 'should throw an error when range is invalid', () => {
-			expect( () => {
-				selection.addRange( { invalid: 'range' } );
-			} ).to.throw( CKEditorError, /model-selection-added-not-range/ );
-		} );
-
-		it( 'should not add a range that is in graveyard', () => {
-			const spy = testUtils.sinon.stub( log, 'warn' );
-
-			selection.addRange( Range.createIn( doc.graveyard ) );
-
-			expect( selection._ranges.length ).to.equal( 0 );
-			expect( spy.calledOnce ).to.be.true;
-		} );
-
-		it( 'should refresh attributes', () => {
-			const spy = testUtils.sinon.spy( selection, '_updateAttributes' );
-
-			selection.addRange( range );
-
-			expect( spy.called ).to.be.true;
-		} );
-	} );
-
-	describe( 'setCollapsedAt()', () => {
+	describe( 'setTo - set collapsed at', () => {
 		it( 'detaches all existing ranges', () => {
-			selection.addRange( range );
-			selection.addRange( liveRange );
+			selection._setTo( [ range, liveRange ] );
 
 			const spy = testUtils.sinon.spy( LiveRange.prototype, 'detach' );
-			selection.setCollapsedAt( root );
+			selection._setTo( root );
 
 			expect( spy.calledTwice ).to.be.true;
 		} );
@@ -243,10 +212,9 @@ describe( 'DocumentSelection', () => {
 
 	describe( 'destroy()', () => {
 		it( 'should unbind all events', () => {
-			selection.addRange( liveRange );
-			selection.addRange( range );
+			selection._setTo( [ range, liveRange ] );
 
-			const ranges = selection._ranges;
+			const ranges = Array.from( selection._selection._ranges );
 
 			sinon.spy( ranges[ 0 ], 'detach' );
 			sinon.spy( ranges[ 1 ], 'detach' );
@@ -261,12 +229,12 @@ describe( 'DocumentSelection', () => {
 		} );
 	} );
 
-	describe( 'moveFocusTo()', () => {
+	describe( 'setFocus()', () => {
 		it( 'modifies default range', () => {
 			const startPos = selection.getFirstPosition();
 			const endPos = Position.createAt( root, 'end' );
 
-			selection.moveFocusTo( endPos );
+			selection._setFocus( endPos );
 
 			expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' );
 			expect( selection.focus.compareWith( endPos ) ).to.equal( 'same' );
@@ -278,30 +246,37 @@ describe( 'DocumentSelection', () => {
 			const newEndPos = Position.createAt( root, 4 );
 			const spy = testUtils.sinon.spy( LiveRange.prototype, 'detach' );
 
-			selection.addRange( new Range( startPos, endPos ) );
+			selection._setTo( new Range( startPos, endPos ) );
 
-			selection.moveFocusTo( newEndPos );
+			selection._setFocus( newEndPos );
 
 			expect( spy.calledOnce ).to.be.true;
 		} );
+
+		it( 'refreshes attributes', () => {
+			const spy = sinon.spy( selection._selection, '_updateAttributes' );
+
+			selection._setFocus( Position.createAt( root, 1 ) );
+
+			expect( spy.called ).to.be.true;
+		} );
 	} );
 
-	describe( 'removeAllRanges()', () => {
+	describe( 'setTo - remove all ranges', () => {
 		let spy, ranges;
 
 		beforeEach( () => {
-			selection.addRange( liveRange );
-			selection.addRange( range );
+			selection._setTo( [ liveRange, range ] );
 
 			spy = sinon.spy();
 			selection.on( 'change:range', spy );
 
-			ranges = Array.from( selection._ranges );
+			ranges = Array.from( selection._selection._ranges );
 
 			sinon.spy( ranges[ 0 ], 'detach' );
 			sinon.spy( ranges[ 1 ], 'detach' );
 
-			selection.removeAllRanges();
+			selection._setTo( null );
 		} );
 
 		afterEach( () => {
@@ -321,40 +296,54 @@ describe( 'DocumentSelection', () => {
 		} );
 
 		it( 'should refresh attributes', () => {
-			const spy = sinon.spy( selection, '_updateAttributes' );
+			const spy = sinon.spy( selection._selection, '_updateAttributes' );
 
-			selection.removeAllRanges();
+			selection._setTo( null );
 
 			expect( spy.called ).to.be.true;
 		} );
 	} );
 
-	describe( 'setRanges()', () => {
+	describe( 'setTo()', () => {
 		it( 'should throw an error when range is invalid', () => {
 			expect( () => {
-				selection.setRanges( [ { invalid: 'range' } ] );
+				selection._setTo( [ { invalid: 'range' } ] );
 			} ).to.throw( CKEditorError, /model-selection-added-not-range/ );
 		} );
 
+		it( 'should log a warning when trying to set selection to the graveyard', () => {
+			// eslint-disable-next-line no-undef
+			const warnStub = sinon.stub( console, 'warn' );
+
+			const range = new Range( new Position( model.document.graveyard, [ 0 ] ) );
+			selection._setTo( range );
+
+			expect( warnStub.calledOnce ).to.equal( true );
+			expect( warnStub.getCall( 0 ).args[ 0 ] ).to.match( /model-selection-range-in-graveyard/ );
+
+			expect( selection._ranges ).to.deep.equal( [] );
+
+			warnStub.restore();
+		} );
+
 		it( 'should detach removed ranges', () => {
-			selection.addRange( liveRange );
-			selection.addRange( range );
+			selection._setTo( [ liveRange, range ] );
 
-			const oldRanges = Array.from( selection._ranges );
+			const oldRanges = Array.from( selection._selection._ranges );
 
 			sinon.spy( oldRanges[ 0 ], 'detach' );
 			sinon.spy( oldRanges[ 1 ], 'detach' );
 
-			selection.setRanges( [] );
+			selection._setTo( [] );
 
 			expect( oldRanges[ 0 ].detach.called ).to.be.true;
 			expect( oldRanges[ 1 ].detach.called ).to.be.true;
 		} );
 
 		it( 'should refresh attributes', () => {
-			const spy = sinon.spy( selection, '_updateAttributes' );
+			const spy = sinon.spy( selection._selection, '_updateAttributes' );
 
-			selection.setRanges( [ range ] );
+			selection._setTo( [ range ] );
 
 			expect( spy.called ).to.be.true;
 		} );
@@ -365,7 +354,7 @@ describe( 'DocumentSelection', () => {
 
 			setData( model, 'f<$text italic="true">[o</$text><$text bold="true">ob]a</$text>r' );
 
-			selection.setRanges( [ Range.createFromPositionAndShift( selection.getLastRange().end, 0 ) ] );
+			selection._setTo( [ Range.createFromPositionAndShift( selection.getLastRange().end, 0 ) ] );
 
 			expect( selection.getAttribute( 'bold' ) ).to.equal( true );
 			expect( selection.hasAttribute( 'italic' ) ).to.equal( false );
@@ -393,16 +382,6 @@ describe( 'DocumentSelection', () => {
 		} );
 	} );
 
-	describe( 'createFromSelection()', () => {
-		it( 'should throw', () => {
-			selection.addRange( range, true );
-
-			expect( () => {
-				DocumentSelection.createFromSelection( selection );
-			} ).to.throw( CKEditorError, /^documentselection-cannot-create:/ );
-		} );
-	} );
-
 	describe( '_isStoreAttributeKey', () => {
 		it( 'should return true if given key is a key of an attribute stored in element by DocumentSelection', () => {
 			expect( DocumentSelection._isStoreAttributeKey( fooStoreAttrKey ) ).to.be.true;
@@ -413,6 +392,15 @@ describe( 'DocumentSelection', () => {
 		} );
 	} );
 
+	describe( 'getSelectedElement()', () => {
+		it( 'should return selected element', () => {
+			selection._setTo( liveRange );
+			const p = root.getChild( 0 );
+
+			expect( selection.getSelectedElement() ).to.equal( p );
+		} );
+	} );
+
 	// DocumentSelection uses LiveRanges so here are only simple test to see if integration is
 	// working well, without getting into complicated corner cases.
 	describe( 'after applying an operation should get updated and fire events', () => {
@@ -426,7 +414,7 @@ describe( 'DocumentSelection', () => {
 				new Text( 'xyz' )
 			] );
 
-			selection.addRange( new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 1, 4 ] ) ) );
+			selection._setTo( new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 1, 4 ] ) ) );
 
 			spyRange = sinon.spy();
 			selection.on( 'change:range', spyRange );
@@ -621,7 +609,7 @@ describe( 'DocumentSelection', () => {
 			} );
 
 			it( 'should not overwrite previously set attributes', () => {
-				selection.setAttribute( 'foo', 'xyz' );
+				selection._setAttribute( 'foo', 'xyz' );
 
 				const spyAttribute = sinon.spy();
 				selection.on( 'change:attribute', spyAttribute );
@@ -640,9 +628,29 @@ describe( 'DocumentSelection', () => {
 				expect( spyAttribute.called ).to.be.false;
 			} );
 
+			it( 'should not overwrite previously set attributes with same values', () => {
+				selection._setAttribute( 'foo', 'xyz' );
+
+				const spyAttribute = sinon.spy();
+				selection.on( 'change:attribute', spyAttribute );
+
+				model.applyOperation( wrapInDelta(
+					new AttributeOperation(
+						new Range( new Position( root, [ 0, 1 ] ), new Position( root, [ 0, 5 ] ) ),
+						'foo',
+						null,
+						'xyz',
+						doc.version
+					)
+				) );
+
+				expect( selection.getAttribute( 'foo' ) ).to.equal( 'xyz' );
+				expect( spyAttribute.called ).to.be.false;
+			} );
+
 			it( 'should not overwrite previously removed attributes', () => {
-				selection.setAttribute( 'foo', 'xyz' );
-				selection.removeAttribute( 'foo' );
+				selection._setAttribute( 'foo', 'xyz' );
+				selection._removeAttribute( 'foo' );
 
 				const spyAttribute = sinon.spy();
 				selection.on( 'change:attribute', spyAttribute );
@@ -664,7 +672,7 @@ describe( 'DocumentSelection', () => {
 
 		describe( 'RemoveOperation', () => {
 			it( 'fix selection range if it ends up in graveyard #1', () => {
-				selection.setCollapsedAt( new Position( root, [ 1, 3 ] ) );
+				selection._setTo( new Position( root, [ 1, 3 ] ) );
 
 				model.applyOperation( wrapInDelta(
 					new RemoveOperation(
@@ -679,7 +687,7 @@ describe( 'DocumentSelection', () => {
 			} );
 
 			it( 'fix selection range if it ends up in graveyard #2', () => {
-				selection.setRanges( [ new Range( new Position( root, [ 1, 2 ] ), new Position( root, [ 1, 4 ] ) ) ] );
+				selection._setTo( [ new Range( new Position( root, [ 1, 2 ] ), new Position( root, [ 1, 4 ] ) ) ] );
 
 				model.applyOperation( wrapInDelta(
 					new RemoveOperation(
@@ -694,7 +702,7 @@ describe( 'DocumentSelection', () => {
 			} );
 
 			it( 'fix selection range if it ends up in graveyard #3', () => {
-				selection.setRanges( [ new Range( new Position( root, [ 1, 1 ] ), new Position( root, [ 1, 2 ] ) ) ] );
+				selection._setTo( [ new Range( new Position( root, [ 1, 1 ] ), new Position( root, [ 1, 2 ] ) ) ] );
 
 				model.applyOperation( wrapInDelta(
 					new RemoveOperation(
@@ -751,94 +759,26 @@ describe( 'DocumentSelection', () => {
 			rangeInEmptyP = new Range( new Position( root, [ 1, 0 ] ), new Position( root, [ 1, 0 ] ) );
 
 			// I've lost 30 mins debugging why my tests fail and that was due to the above code reusing
-			// a root which wasn't empty (so the ranges didn't actualy make much sense).
+			// a root which wasn't empty (so the ranges didn't actually make much sense).
 			expect( root.childCount ).to.equal( 2 );
 		} );
 
-		describe( 'setAttribute()', () => {
-			it( 'should store attribute if the selection is in empty node', () => {
-				selection.setRanges( [ rangeInEmptyP ] );
-				selection.setAttribute( 'foo', 'bar' );
+		describe( '_setAttribute()', () => {
+			it( 'should set attribute', () => {
+				selection._setTo( [ rangeInEmptyP ] );
+				selection._setAttribute( 'foo', 'bar' );
 
 				expect( selection.getAttribute( 'foo' ) ).to.equal( 'bar' );
-
-				expect( emptyP.getAttribute( fooStoreAttrKey ) ).to.equal( 'bar' );
-			} );
-		} );
-
-		describe( 'setAttributesTo()', () => {
-			it( 'should fire change:attribute event with correct parameters', done => {
-				selection.setAttributesTo( { foo: 'bar', abc: 'def' } );
-
-				selection.on( 'change:attribute', ( evt, data ) => {
-					expect( data.directChange ).to.be.true;
-					expect( data.attributeKeys ).to.deep.equal( [ 'abc', 'xxx' ] );
-
-					done();
-				} );
-
-				selection.setAttributesTo( { foo: 'bar', xxx: 'yyy' } );
-			} );
-
-			it( 'should not fire change:attribute event if same attributes are set', () => {
-				selection.setAttributesTo( { foo: 'bar', abc: 'def' } );
-
-				const spy = sinon.spy();
-				selection.on( 'change:attribute', spy );
-
-				selection.setAttributesTo( { foo: 'bar', abc: 'def' } );
-
-				expect( spy.called ).to.be.false;
-			} );
-
-			it( 'should remove all stored attributes and store the given ones if the selection is in empty node', () => {
-				selection.setRanges( [ rangeInEmptyP ] );
-				selection.setAttribute( 'abc', 'xyz' );
-				selection.setAttributesTo( { foo: 'bar' } );
-
-				expect( selection.getAttribute( 'foo' ) ).to.equal( 'bar' );
-				expect( selection.getAttribute( 'abc' ) ).to.be.undefined;
-
-				expect( emptyP.getAttribute( fooStoreAttrKey ) ).to.equal( 'bar' );
-				expect( emptyP.hasAttribute( abcStoreAttrKey ) ).to.be.false;
 			} );
 		} );
 
 		describe( 'removeAttribute()', () => {
 			it( 'should remove attribute set on the text fragment', () => {
-				selection.setRanges( [ rangeInFullP ] );
-				selection.setAttribute( 'foo', 'bar' );
-				selection.removeAttribute( 'foo' );
-
-				expect( selection.getAttribute( 'foo' ) ).to.be.undefined;
-
-				expect( fullP.hasAttribute( fooStoreAttrKey ) ).to.be.false;
-			} );
-
-			it( 'should remove stored attribute if the selection is in empty node', () => {
-				selection.setRanges( [ rangeInEmptyP ] );
-				selection.setAttribute( 'foo', 'bar' );
-				selection.removeAttribute( 'foo' );
-
-				expect( selection.getAttribute( 'foo' ) ).to.be.undefined;
-
-				expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.false;
-			} );
-		} );
-
-		describe( 'clearAttributes()', () => {
-			it( 'should remove all stored attributes if the selection is in empty node', () => {
-				selection.setRanges( [ rangeInEmptyP ] );
-				selection.setAttribute( 'foo', 'bar' );
-				selection.setAttribute( 'abc', 'xyz' );
-
-				selection.clearAttributes();
+				selection._setTo( [ rangeInFullP ] );
+				selection._setAttribute( 'foo', 'bar' );
+				selection._removeAttribute( 'foo' );
 
 				expect( selection.getAttribute( 'foo' ) ).to.be.undefined;
-				expect( selection.getAttribute( 'abc' ) ).to.be.undefined;
-
-				expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.false;
-				expect( emptyP.hasAttribute( abcStoreAttrKey ) ).to.be.false;
 			} );
 		} );
 
@@ -867,50 +807,50 @@ describe( 'DocumentSelection', () => {
 			} );
 
 			it( 'if selection is a range, should find first character in it and copy it\'s attributes', () => {
-				selection.setRanges( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 5 ] ) ) ] );
+				selection._setTo( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 5 ] ) ) ] );
 
 				expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'b', true ] ] );
 
 				// Step into elements when looking for first character:
-				selection.setRanges( [ new Range( new Position( root, [ 5 ] ), new Position( root, [ 7 ] ) ) ] );
+				selection._setTo( [ new Range( new Position( root, [ 5 ] ), new Position( root, [ 7 ] ) ) ] );
 
 				expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'd', true ] ] );
 			} );
 
 			it( 'if selection is collapsed it should seek a character to copy that character\'s attributes', () => {
 				// Take styles from character before selection.
-				selection.setRanges( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 2 ] ) ) ] );
+				selection._setTo( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 2 ] ) ) ] );
 				expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'a', true ] ] );
 
 				// If there are none,
 				// Take styles from character after selection.
-				selection.setRanges( [ new Range( new Position( root, [ 3 ] ), new Position( root, [ 3 ] ) ) ] );
+				selection._setTo( [ new Range( new Position( root, [ 3 ] ), new Position( root, [ 3 ] ) ) ] );
 				expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'b', true ] ] );
 
 				// If there are none,
 				// Look from the selection position to the beginning of node looking for character to take attributes from.
-				selection.setRanges( [ new Range( new Position( root, [ 6 ] ), new Position( root, [ 6 ] ) ) ] );
+				selection._setTo( [ new Range( new Position( root, [ 6 ] ), new Position( root, [ 6 ] ) ) ] );
 				expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'c', true ] ] );
 
 				// If there are none,
 				// Look from the selection position to the end of node looking for character to take attributes from.
-				selection.setRanges( [ new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) ] );
+				selection._setTo( [ new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) ] );
 				expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'a', true ] ] );
 
 				// If there are no characters to copy attributes from, use stored attributes.
-				selection.setRanges( [ new Range( new Position( root, [ 0, 0 ] ), new Position( root, [ 0, 0 ] ) ) ] );
+				selection._setTo( [ new Range( new Position( root, [ 0, 0 ] ), new Position( root, [ 0, 0 ] ) ) ] );
 				expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [] );
 			} );
 
 			it( 'should overwrite any previously set attributes', () => {
-				selection.setCollapsedAt( new Position( root, [ 5, 0 ] ) );
+				selection._setTo( new Position( root, [ 5, 0 ] ) );
 
-				selection.setAttribute( 'x', true );
-				selection.setAttribute( 'y', true );
+				selection._setAttribute( 'x', true );
+				selection._setAttribute( 'y', true );
 
 				expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'd', true ], [ 'x', true ], [ 'y', true ] ] );
 
-				selection.setCollapsedAt( new Position( root, [ 1 ] ) );
+				selection._setTo( new Position( root, [ 1 ] ) );
 
 				expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'a', true ] ] );
 			} );
@@ -919,20 +859,20 @@ describe( 'DocumentSelection', () => {
 				const spy = sinon.spy();
 				selection.on( 'change:attribute', spy );
 
-				selection.setRanges( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 5 ] ) ) ] );
+				selection._setTo( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 5 ] ) ) ] );
 
 				expect( spy.calledOnce ).to.be.true;
 			} );
 
 			it( 'should not fire change:attribute event if attributes did not change', () => {
-				selection.setCollapsedAt( new Position( root, [ 5, 0 ] ) );
+				selection._setTo( new Position( root, [ 5, 0 ] ) );
 
 				expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'd', true ] ] );
 
 				const spy = sinon.spy();
 				selection.on( 'change:attribute', spy );
 
-				selection.setCollapsedAt( new Position( root, [ 5, 1 ] ) );
+				selection._setTo( new Position( root, [ 5, 1 ] ) );
 
 				expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'd', true ] ] );
 				expect( spy.called ).to.be.false;
@@ -1004,8 +944,11 @@ describe( 'DocumentSelection', () => {
 					batchTypes.push( batch.type );
 				} );
 
-				selection.setRanges( [ rangeInEmptyP ] );
-				selection.setAttribute( 'foo', 'bar' );
+				selection._setTo( [ rangeInEmptyP ] );
+
+				model.change( writer => {
+					writer.setSelectionAttribute( 'foo', 'bar' );
+				} );
 
 				expect( batchTypes ).to.deep.equal( [ 'default' ] );
 				expect( emptyP.getAttribute( fooStoreAttrKey ) ).to.equal( 'bar' );
@@ -1015,9 +958,9 @@ describe( 'DocumentSelection', () => {
 				// Dedupe batches by using a map (multiple change events will be fired).
 				const batchTypes = new Map();
 
-				selection.setRanges( [ rangeInEmptyP ] );
-				selection.setAttribute( 'foo', 'bar' );
-				selection.setAttribute( 'abc', 'bar' );
+				selection._setTo( rangeInEmptyP );
+				selection._setAttribute( 'foo', 'bar' );
+				selection._setAttribute( 'abc', 'bar' );
 
 				model.on( 'applyOperation', ( event, args ) => {
 					const operation = args[ 0 ];
@@ -1037,8 +980,8 @@ describe( 'DocumentSelection', () => {
 			} );
 
 			it( 'are removed when any content is moved into', () => {
-				selection.setRanges( [ rangeInEmptyP ] );
-				selection.setAttribute( 'foo', 'bar' );
+				selection._setTo( rangeInEmptyP );
+				selection._setAttribute( 'foo', 'bar' );
 
 				model.change( writer => {
 					writer.move( Range.createOn( fullP.getChild( 0 ) ), rangeInEmptyP.start );
@@ -1066,7 +1009,7 @@ describe( 'DocumentSelection', () => {
 			it( 'are removed even when there is no selection in it', () => {
 				emptyP.setAttribute( fooStoreAttrKey, 'bar' );
 
-				selection.setRanges( [ rangeInFullP ] );
+				selection._setTo( [ rangeInFullP ] );
 
 				model.change( writer => {
 					writer.insertText( 'x', rangeInEmptyP.start );
@@ -1095,10 +1038,10 @@ describe( 'DocumentSelection', () => {
 			} );
 
 			it( 'uses model change to clear attributes', () => {
-				selection.setRanges( [ rangeInEmptyP ] );
-				selection.setAttribute( 'foo', 'bar' );
+				selection._setTo( [ rangeInEmptyP ] );
 
 				model.change( writer => {
+					writer.setSelectionAttribute( 'foo', 'bar' );
 					writer.insertText( 'x', rangeInEmptyP.start );
 
 					// `emptyP` still has the attribute, because attribute clearing is in enqueued block.
@@ -1131,8 +1074,10 @@ describe( 'DocumentSelection', () => {
 			// Rename and some other deltas don't specify range in doc#change event.
 			// So let's see if there's no crash or something.
 			it( 'are not removed on rename', () => {
-				selection.setRanges( [ rangeInEmptyP ] );
-				selection.setAttribute( 'foo', 'bar' );
+				model.change( writer => {
+					writer.setSelection( rangeInEmptyP );
+					writer.setSelectionAttribute( 'foo', 'bar' );
+				} );
 
 				sinon.spy( model, 'enqueueChange' );
 
@@ -1151,11 +1096,11 @@ describe( 'DocumentSelection', () => {
 		root.appendChildren( '\uD83D\uDCA9' );
 
 		expect( () => {
-			doc.selection.setRanges( [ Range.createFromParentsAndOffsets( root, 0, root, 1 ) ] );
+			doc.selection._setTo( Range.createFromParentsAndOffsets( root, 0, root, 1 ) );
 		} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
 
 		expect( () => {
-			doc.selection.setRanges( [ Range.createFromParentsAndOffsets( root, 1, root, 2 ) ] );
+			doc.selection._setTo( Range.createFromParentsAndOffsets( root, 1, root, 2 ) );
 		} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
 	} );
 
@@ -1164,27 +1109,27 @@ describe( 'DocumentSelection', () => {
 		root.appendChildren( 'foo̻̐ͩbar' );
 
 		expect( () => {
-			doc.selection.setRanges( [ Range.createFromParentsAndOffsets( root, 3, root, 9 ) ] );
+			doc.selection._setTo( Range.createFromParentsAndOffsets( root, 3, root, 9 ) );
 		} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
 
 		expect( () => {
-			doc.selection.setRanges( [ Range.createFromParentsAndOffsets( root, 4, root, 9 ) ] );
+			doc.selection._setTo( Range.createFromParentsAndOffsets( root, 4, root, 9 ) );
 		} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
 
 		expect( () => {
-			doc.selection.setRanges( [ Range.createFromParentsAndOffsets( root, 5, root, 9 ) ] );
+			doc.selection._setTo( Range.createFromParentsAndOffsets( root, 5, root, 9 ) );
 		} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
 
 		expect( () => {
-			doc.selection.setRanges( [ Range.createFromParentsAndOffsets( root, 1, root, 3 ) ] );
+			doc.selection._setTo( Range.createFromParentsAndOffsets( root, 1, root, 3 ) );
 		} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
 
 		expect( () => {
-			doc.selection.setRanges( [ Range.createFromParentsAndOffsets( root, 1, root, 4 ) ] );
+			doc.selection._setTo( Range.createFromParentsAndOffsets( root, 1, root, 4 ) );
 		} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
 
 		expect( () => {
-			doc.selection.setRanges( [ Range.createFromParentsAndOffsets( root, 1, root, 5 ) ] );
+			doc.selection._setTo( Range.createFromParentsAndOffsets( root, 1, root, 5 ) );
 		} ).to.throw( CKEditorError, /document-selection-wrong-position/ );
 	} );
 } );

+ 402 - 8
packages/ckeditor5-engine/tests/model/schema.js

@@ -641,6 +641,169 @@ describe( 'Schema', () => {
 		} );
 	} );
 
+	describe( 'checkMerge()', () => {
+		beforeEach( () => {
+			schema.register( '$root' );
+			schema.register( '$block', {
+				allowIn: '$root',
+				isBlock: true
+			} );
+			schema.register( '$text', {
+				allowIn: '$block'
+			} );
+			schema.register( 'paragraph', {
+				inheritAllFrom: '$block'
+			} );
+			schema.register( 'listItem', {
+				inheritAllFrom: '$block'
+			} );
+			schema.register( 'blockQuote', {
+				allowWhere: '$block',
+				allowContentOf: '$root'
+			} );
+		} );
+
+		it( 'returns false if a block cannot be merged with other block (disallowed element is the first child)', () => {
+			const paragraph = new Element( 'paragraph', null, [
+				new Text( 'xyz' )
+			] );
+			const blockQuote = new Element( 'blockQuote', null, [ paragraph ] );
+			const listItem = new Element( 'listItem' );
+
+			expect( schema.checkMerge( listItem, blockQuote ) ).to.be.false;
+		} );
+
+		it( 'returns false if a block cannot be merged with other block (disallowed element is not the first child)', () => {
+			const paragraph = new Element( 'paragraph', null, [
+				new Text( 'foo' )
+			] );
+			const blockQuote = new Element( 'blockQuote', null, [
+				new Text( 'bar', { bold: true } ),
+				new Text( 'xyz' ),
+				paragraph
+			] );
+			const listItem = new Element( 'listItem' );
+
+			expect( schema.checkMerge( listItem, blockQuote ) ).to.be.false;
+		} );
+
+		it( 'returns true if a block can be merged with other block', () => {
+			const listItem = new Element( 'listItem' );
+			const listItemToMerge = new Element( 'listItem', null, [
+				new Text( 'xyz' )
+			] );
+
+			expect( schema.checkMerge( listItem, listItemToMerge ) ).to.be.true;
+		} );
+
+		it( 'return true if two elements between the position can be merged', () => {
+			const listItem = new Element( 'listItem', null, [
+				new Text( 'foo' )
+			] );
+			const listItemToMerge = new Element( 'listItem', null, [
+				new Text( 'bar' )
+			] );
+
+			// eslint-disable-next-line no-new
+			new Element( '$root', null, [
+				listItem, listItemToMerge
+			] );
+			const position = Position.createAfter( listItem );
+
+			expect( schema.checkMerge( position ) ).to.be.true;
+		} );
+
+		it( 'throws an error if there is no element before the position', () => {
+			const listItem = new Element( 'listItem', null, [
+				new Text( 'foo' )
+			] );
+
+			// eslint-disable-next-line no-new
+			new Element( '$root', null, [
+				listItem
+			] );
+
+			const position = Position.createBefore( listItem );
+
+			expect( () => {
+				expect( schema.checkMerge( position ) );
+			} ).to.throw( CKEditorError, /^schema-check-merge-no-element-before:/ );
+		} );
+
+		it( 'throws an error if the node before the position is not the element', () => {
+			const listItem = new Element( 'listItem', null, [
+				new Text( 'foo' )
+			] );
+
+			// eslint-disable-next-line no-new
+			new Element( '$root', null, [
+				new Text( 'bar' ),
+				listItem
+			] );
+
+			const position = Position.createBefore( listItem );
+
+			expect( () => {
+				expect( schema.checkMerge( position ) );
+			} ).to.throw( CKEditorError, /^schema-check-merge-no-element-before:/ );
+		} );
+
+		it( 'throws an error if there is no element after the position', () => {
+			const listItem = new Element( 'listItem', null, [
+				new Text( 'foo' )
+			] );
+
+			// eslint-disable-next-line no-new
+			new Element( '$root', null, [
+				listItem
+			] );
+
+			const position = Position.createAfter( listItem );
+
+			expect( () => {
+				expect( schema.checkMerge( position ) );
+			} ).to.throw( CKEditorError, /^schema-check-merge-no-element-after:/ );
+		} );
+
+		it( 'throws an error if the node after the position is not the element', () => {
+			const listItem = new Element( 'listItem', null, [
+				new Text( 'foo' )
+			] );
+
+			// eslint-disable-next-line no-new
+			new Element( '$root', null, [
+				listItem,
+				new Text( 'bar' )
+			] );
+
+			const position = Position.createBefore( listItem );
+
+			expect( () => {
+				expect( schema.checkMerge( position ) );
+			} ).to.throw( CKEditorError, /^schema-check-merge-no-element-before:/ );
+		} );
+
+		// This is an invalid case by definition – the baseElement should not contain disallowed elements
+		// in the first place. However, the check is focused on the elementToMerge's children so let's make sure
+		// that only them counts.
+		it( 'returns true if element to merge contains a valid content but base element contains disallowed elements', () => {
+			const listItem = new Element( 'listItem', null, [
+				new Text( 'foo' ),
+				new Element( 'paragraph', null, [
+					new Text( 'bar' )
+				] )
+			] );
+			const listItemToMerge = new Element( 'listItem', null, [
+				new Text( 'xyz' )
+			] );
+
+			expect( schema.checkMerge( listItem, listItemToMerge ) ).to.be.true;
+		} );
+
+		// The checkMerge() method should also check whether all ancestors of elementToMerge are allowed in their new
+		// context (now, we check only immediate children), but for now we ignore these cases.
+	} );
+
 	describe( 'getLimitElement()', () => {
 		let model, doc, root;
 
@@ -894,8 +1057,7 @@ describe( 'Schema', () => {
 			setData( model, '[<p>foo<img>xxx</img>bar</p>]' );
 
 			const validRanges = schema.getValidRanges( doc.selection.getRanges(), attribute );
-			const sel = new Selection();
-			sel.setRanges( validRanges );
+			const sel = new Selection( validRanges );
 
 			expect( stringify( root, sel ) ).to.equal( '[<p>foo<img>]xxx[</img>bar</p>]' );
 		} );
@@ -913,8 +1075,7 @@ describe( 'Schema', () => {
 			setData( model, '[<p>foo<img>xxx</img>bar</p>]' );
 
 			const validRanges = schema.getValidRanges( doc.selection.getRanges(), attribute );
-			const sel = new Selection();
-			sel.setRanges( validRanges );
+			const sel = new Selection( validRanges );
 
 			expect( stringify( root, sel ) ).to.equal( '[<p>foo]<img>[xxx]</img>[bar</p>]' );
 		} );
@@ -923,8 +1084,7 @@ describe( 'Schema', () => {
 			setData( model, '<p>[foo<img></img>bar]x[bar<img></img>foo]</p>' );
 
 			const validRanges = schema.getValidRanges( doc.selection.getRanges(), attribute );
-			const sel = new Selection();
-			sel.setRanges( validRanges );
+			const sel = new Selection( validRanges );
 
 			expect( stringify( root, sel ) ).to.equal( '<p>[foo]<img></img>[bar]x[bar]<img></img>[foo]</p>' );
 		} );
@@ -935,8 +1095,7 @@ describe( 'Schema', () => {
 			setData( model, '<p>[foo<img>bar]</img>bom</p>' );
 
 			const validRanges = schema.getValidRanges( doc.selection.getRanges(), attribute );
-			const sel = new Selection();
-			sel.setRanges( validRanges );
+			const sel = new Selection( validRanges );
 
 			expect( stringify( root, sel ) ).to.equal( '<p>[foo]<img>bar</img>bom</p>' );
 		} );
@@ -959,6 +1118,241 @@ describe( 'Schema', () => {
 		} );
 	} );
 
+	describe( 'getNearestSelectionRange()', () => {
+		let selection, model, doc;
+
+		beforeEach( () => {
+			model = new Model();
+			doc = model.document;
+			schema = model.schema;
+			model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+
+			model.schema.register( 'emptyBlock', { allowIn: '$root' } );
+
+			model.schema.register( 'widget', {
+				allowIn: '$root',
+				isObject: true
+			} );
+
+			model.schema.register( 'blockWidget', {
+				allowIn: '$root',
+				allowContentOf: '$block',
+				isObject: true
+			} );
+
+			doc.createRoot();
+			selection = doc.selection;
+		} );
+
+		test(
+			'should return collapsed range if text node can be placed at that position - both',
+			'<paragraph>[]</paragraph>',
+			'both',
+			'<paragraph>[]</paragraph>'
+		);
+
+		test(
+			'should return collapsed range if text node can be placed at that position - forward',
+			'<paragraph>[]</paragraph>',
+			'forward',
+			'<paragraph>[]</paragraph>'
+		);
+
+		test(
+			'should return collapsed range if text node can be placed at that position - backward',
+			'<paragraph>[]</paragraph>',
+			'backward',
+			'<paragraph>[]</paragraph>'
+		);
+
+		test( 'should return null in empty document - both', '', 'both', null );
+
+		test( 'should return null in empty document - backward', '', 'backward', null );
+
+		test( 'should return null in empty document - forward', '', 'forward', null );
+
+		test(
+			'should find range before when searching both ways',
+			'<paragraph></paragraph>[]<paragraph></paragraph>',
+			'both',
+			'<paragraph>[]</paragraph><paragraph></paragraph>'
+		);
+
+		test(
+			'should find range before when searching backward',
+			'<paragraph></paragraph>[]<paragraph></paragraph>',
+			'backward',
+			'<paragraph>[]</paragraph><paragraph></paragraph>'
+		);
+
+		test(
+			'should find range after when searching forward',
+			'<paragraph></paragraph>[]<paragraph></paragraph>',
+			'forward',
+			'<paragraph></paragraph><paragraph>[]</paragraph>'
+		);
+
+		test(
+			'should find range after when searching both ways when it is closer',
+			'<paragraph></paragraph><emptyBlock></emptyBlock>[]<paragraph></paragraph>',
+			'both',
+			'<paragraph></paragraph><emptyBlock></emptyBlock><paragraph>[]</paragraph>'
+		);
+
+		test(
+			'should find range before when searching both ways when it is closer',
+			'<paragraph></paragraph><emptyBlock></emptyBlock>[]<emptyBlock></emptyBlock><emptyBlock></emptyBlock><paragraph></paragraph>',
+			'both',
+			'<paragraph>[]</paragraph><emptyBlock></emptyBlock><emptyBlock></emptyBlock><emptyBlock></emptyBlock><paragraph></paragraph>'
+		);
+
+		test(
+			'should return null if there is no valid range',
+			'[]<emptyBlock></emptyBlock>',
+			'both',
+			null
+		);
+
+		test(
+			'should return null if there is no valid range in given direction - backward',
+			'[]<paragraph></paragraph>',
+			'backward',
+			null
+		);
+
+		test(
+			'should return null if there is no valid range in given direction - forward',
+			'<paragraph></paragraph>[]',
+			'forward',
+			null
+		);
+
+		test(
+			'should move forward when placed at root start',
+			'[]<paragraph></paragraph><paragraph></paragraph>',
+			'both',
+			'<paragraph>[]</paragraph><paragraph></paragraph>'
+		);
+
+		test(
+			'should move backward when placed at root end',
+			'<paragraph></paragraph><paragraph></paragraph>[]',
+			'both',
+			'<paragraph></paragraph><paragraph>[]</paragraph>'
+		);
+
+		describe( 'in case of objects which do not allow text inside', () => {
+			test(
+				'should select nearest object (o[]o) - both',
+				'<widget></widget>[]<widget></widget>',
+				'both',
+				'[<widget></widget>]<widget></widget>'
+			);
+
+			test(
+				'should select nearest object (o[]o) - forward',
+				'<widget></widget>[]<widget></widget>',
+				'forward',
+				'<widget></widget>[<widget></widget>]'
+			);
+
+			test(
+				'should select nearest object (o[]o) - backward',
+				'<widget></widget>[]<widget></widget>',
+				'both',
+				'[<widget></widget>]<widget></widget>'
+			);
+
+			test(
+				'should select nearest object (p[]o) - forward',
+				'<paragraph></paragraph>[]<widget></widget>',
+				'forward',
+				'<paragraph></paragraph>[<widget></widget>]'
+			);
+
+			test(
+				'should select nearest object (o[]p) - both',
+				'<widget></widget>[]<paragraph></paragraph>',
+				'both',
+				'[<widget></widget>]<paragraph></paragraph>'
+			);
+
+			test(
+				'should select nearest object (o[]p) - backward',
+				'<widget></widget>[]<paragraph></paragraph>',
+				'backward',
+				'[<widget></widget>]<paragraph></paragraph>'
+			);
+
+			test(
+				'should select nearest object ([]o) - both',
+				'[]<widget></widget><paragraph></paragraph>',
+				'both',
+				'[<widget></widget>]<paragraph></paragraph>'
+			);
+
+			test(
+				'should select nearest object ([]o) - forward',
+				'[]<widget></widget><paragraph></paragraph>',
+				'forward',
+				'[<widget></widget>]<paragraph></paragraph>'
+			);
+
+			test(
+				'should select nearest object (o[]) - both',
+				'<paragraph></paragraph><widget></widget>[]',
+				'both',
+				'<paragraph></paragraph>[<widget></widget>]'
+			);
+
+			test(
+				'should select nearest object (o[]) - backward',
+				'<paragraph></paragraph><widget></widget>[]',
+				'both',
+				'<paragraph></paragraph>[<widget></widget>]'
+			);
+		} );
+
+		describe( 'in case of objects which allow text inside', () => {
+			test(
+				'should select nearest object which allows text (o[]o) - both',
+				'<blockWidget></blockWidget>[]<blockWidget></blockWidget>',
+				'both',
+				'[<blockWidget></blockWidget>]<blockWidget></blockWidget>'
+			);
+
+			test(
+				'should select nearest object (o[]p) - both',
+				'<blockWidget></blockWidget>[]<paragraph></paragraph>',
+				'both',
+				'[<blockWidget></blockWidget>]<paragraph></paragraph>'
+			);
+
+			test(
+				'should select nearest object which allows text ([]o) - both',
+				'[]<blockWidget></blockWidget><paragraph></paragraph>',
+				'both',
+				'[<blockWidget></blockWidget>]<paragraph></paragraph>'
+			);
+		} );
+
+		function test( testName, data, direction, expected ) {
+			it( testName, () => {
+				setData( model, data );
+				const range = schema.getNearestSelectionRange( selection.anchor, direction );
+
+				if ( expected === null ) {
+					expect( range ).to.be.null;
+				} else {
+					model.change( writer => {
+						writer.setSelection( range );
+					} );
+					expect( getData( model ) ).to.equal( expected );
+				}
+			} );
+		}
+	} );
+
 	describe( 'removeDisallowedAttributes()', () => {
 		let model, doc, root;
 

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 280 - 290
packages/ckeditor5-engine/tests/model/selection.js


+ 45 - 3
packages/ckeditor5-engine/tests/model/utils/deletecontent.js

@@ -6,6 +6,7 @@
 import Model from '../../../src/model/model';
 import Position from '../../../src/model/position';
 import Range from '../../../src/model/range';
+import Selection from '../../../src/model/selection';
 import Element from '../../../src/model/element';
 import deleteContent from '../../../src/model/utils/deletecontent';
 import { setData, getData } from '../../../src/dev-utils/model';
@@ -40,6 +41,22 @@ describe( 'DataController utils', () => {
 				schema.extend( '$text', { allowIn: '$root' } );
 			} );
 
+			it( 'should be able to delete content at custom selection', () => {
+				setData( model, 'a[]bcd' );
+
+				const range = new Range(
+					new Position( doc.getRoot(), [ 2 ] ),
+					new Position( doc.getRoot(), [ 3 ] )
+				);
+
+				const selection = new Selection( [ range ] );
+
+				model.change( () => {
+					deleteContent( model, selection );
+					expect( getData( model ) ).to.equal( 'a[]bd' );
+				} );
+			} );
+
 			test(
 				'does nothing on collapsed selection',
 				'f[]oo',
@@ -335,7 +352,9 @@ describe( 'DataController utils', () => {
 						new Position( doc.getRoot(), [ 1, 0, 0, 1 ] ) // b]ar
 					);
 
-					doc.selection.setRanges( [ range ] );
+					model.change( writer => {
+						writer.setSelection( range );
+					} );
 
 					deleteContent( model, doc.selection );
 
@@ -380,7 +399,9 @@ describe( 'DataController utils', () => {
 						new Position( doc.getRoot(), [ 1, 1 ] ) // b]om
 					);
 
-					doc.selection.setRanges( [ range ] );
+					model.change( writer => {
+						writer.setSelection( range );
+					} );
 
 					deleteContent( model, doc.selection );
 
@@ -423,7 +444,9 @@ describe( 'DataController utils', () => {
 						new Position( doc.getRoot(), [ 1, 0, 0, 3 ] ) // bar]
 					);
 
-					doc.selection.setRanges( [ range ] );
+					model.change( writer => {
+						writer.setSelection( range );
+					} );
 
 					deleteContent( model, doc.selection );
 
@@ -582,6 +605,25 @@ describe( 'DataController utils', () => {
 					.to.equal( '<paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>z</paragraph>' );
 			} );
 
+			it( 'creates a paragraph when text is not allowed (custom selection)', () => {
+				setData(
+					model,
+					'[<paragraph>x</paragraph>]<paragraph>yyy</paragraph><paragraph>z</paragraph>',
+					{ rootName: 'bodyRoot' }
+				);
+
+				const root = doc.getRoot( 'bodyRoot' );
+
+				const selection = new Selection( [
+					new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) )
+				] );
+
+				deleteContent( model, selection );
+
+				expect( getData( model, { rootName: 'bodyRoot' } ) )
+					.to.equal( '[<paragraph>x</paragraph>]<paragraph></paragraph><paragraph>z</paragraph>' );
+			} );
+
 			it( 'creates a paragraph when text is not allowed (block widget selected)', () => {
 				setData(
 					model,

+ 18 - 0
packages/ckeditor5-engine/tests/model/utils/insertcontent.js

@@ -8,6 +8,8 @@ import insertContent from '../../../src/model/utils/insertcontent';
 import DocumentFragment from '../../../src/model/documentfragment';
 import Text from '../../../src/model/text';
 import Element from '../../../src/model/element';
+import Selection from '../../../src/model/selection';
+import Position from '../../../src/model/position';
 
 import { setData, getData, parse } from '../../../src/dev-utils/model';
 
@@ -29,6 +31,22 @@ describe( 'DataController utils', () => {
 			} );
 		} );
 
+		it( 'should be able to insert content at custom selection', () => {
+			model = new Model();
+			doc = model.document;
+			doc.createRoot();
+
+			model.schema.extend( '$text', { allowIn: '$root' } );
+			setData( model, 'a[]bc' );
+
+			const selection = new Selection( new Position( doc.getRoot(), [ 2 ] ) );
+
+			model.change( () => {
+				insertContent( model, new Text( 'x' ), selection );
+				expect( getData( model ) ).to.equal( 'a[]bxc' );
+			} );
+		} );
+
 		it( 'accepts DocumentFragment', () => {
 			model = new Model();
 			doc = model.document;

+ 2 - 2
packages/ckeditor5-engine/tests/model/utils/modifyselection.js

@@ -380,7 +380,7 @@ describe( 'DataController utils', () => {
 				// Creating new instance of selection instead of operation on module:engine/model/document~Document#selection.
 				// Document's selection will throw errors in some test cases (which are correct cases, but only for
 				// non-document selections).
-				const testSelection = Selection.createFromSelection( doc.selection );
+				const testSelection = new Selection( doc.selection );
 				modifySelection( model, testSelection, { unit: 'codePoint', direction: 'backward' } );
 
 				expect( stringify( doc.getRoot(), testSelection ) ).to.equal( '<p>foob[̂]ar</p>' );
@@ -550,7 +550,7 @@ describe( 'DataController utils', () => {
 			// Creating new instance of selection instead of operation on module:engine/model/document~Document#selection.
 			// Document's selection will throw errors in some test cases (which are correct cases, but only for
 			// non-document selections).
-			const testSelection = Selection.createFromSelection( doc.selection );
+			const testSelection = new Selection( doc.selection );
 			modifySelection( model, testSelection, options );
 
 			expect( stringify( doc.getRoot(), testSelection ) ).to.equal( output );

+ 196 - 1
packages/ckeditor5-engine/tests/model/writer.js

@@ -17,8 +17,8 @@ import Range from '../../src/model/range';
 
 import count from '@ckeditor/ckeditor5-utils/src/count';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-
 import { getNodesAndText } from '../../tests/model/_utils/utils';
+import DocumentSelection from '../../src/model/documentselection';
 
 describe( 'Writer', () => {
 	let model, doc, batch;
@@ -2026,6 +2026,177 @@ describe( 'Writer', () => {
 		} );
 	} );
 
+	describe( 'setSelection()', () => {
+		let root;
+
+		beforeEach( () => {
+			model.schema.register( 'p', { inheritAllFrom: '$block' } );
+			model.schema.extend( 'p', { allowIn: '$root' } );
+
+			root = doc.createRoot();
+			root.appendChildren( [
+				new Element( 'p' ),
+				new Element( 'p' ),
+				new Element( 'p', [], new Text( 'foo' ) )
+			] );
+		} );
+
+		it( 'should use DocumentSelection#_setTo method', () => {
+			const firstParagraph = root.getNodeByPath( [ 1 ] );
+
+			const setToSpy = sinon.spy( DocumentSelection.prototype, '_setTo' );
+			setSelection( firstParagraph );
+
+			expect( setToSpy.calledOnce ).to.be.true;
+			setToSpy.restore();
+		} );
+
+		it( 'should change document selection ranges', () => {
+			const range = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2, 2 ] ) );
+
+			setSelection( range, true );
+
+			expect( model.document.selection._ranges.length ).to.equal( 1 );
+			expect( model.document.selection._ranges[ 0 ].start.path ).to.deep.equal( [ 1 ] );
+			expect( model.document.selection._ranges[ 0 ].end.path ).to.deep.equal( [ 2, 2 ] );
+			expect( model.document.selection.isBackward ).to.be.true;
+		} );
+	} );
+
+	describe( 'setSelectionFocus()', () => {
+		let root;
+
+		beforeEach( () => {
+			model.schema.register( 'p', { inheritAllFrom: '$block' } );
+			model.schema.extend( 'p', { allowIn: '$root' } );
+
+			root = doc.createRoot();
+			root.appendChildren( [
+				new Element( 'p' ),
+				new Element( 'p' ),
+				new Element( 'p', [], new Text( 'foo' ) )
+			] );
+		} );
+
+		it( 'should use DocumentSelection#_setFocus method', () => {
+			const firstParagraph = root.getNodeByPath( [ 1 ] );
+
+			const setFocusSpy = sinon.spy( DocumentSelection.prototype, '_setFocus' );
+			setSelectionFocus( firstParagraph );
+
+			expect( setFocusSpy.calledOnce ).to.be.true;
+			setFocusSpy.restore();
+		} );
+
+		it( 'should change document selection ranges', () => {
+			setSelection( new Position( root, [ 1 ] ) );
+			setSelectionFocus( new Position( root, [ 2, 2 ] ) );
+
+			expect( model.document.selection._ranges.length ).to.equal( 1 );
+			expect( model.document.selection._ranges[ 0 ].start.path ).to.deep.equal( [ 1 ] );
+			expect( model.document.selection._ranges[ 0 ].end.path ).to.deep.equal( [ 2, 2 ] );
+		} );
+	} );
+
+	describe( 'setSelectionAttribute()', () => {
+		const fooStoreAttrKey = DocumentSelection._getStoreAttributeKey( 'foo' );
+		let root, rangeInEmptyP, emptyP;
+
+		beforeEach( () => {
+			model.schema.register( 'p', { inheritAllFrom: '$block' } );
+			model.schema.extend( 'p', { allowIn: '$root' } );
+
+			root = doc.createRoot();
+			root.appendChildren( [
+				new Element( 'p', [], [] ),
+				new Element( 'p' ),
+				new Element( 'p', [], new Text( 'foo' ) )
+			] );
+
+			rangeInEmptyP = new Range( new Position( root, [ 0, 0 ] ), new Position( root, [ 0, 0 ] ) );
+			emptyP = root.getChild( 0 );
+		} );
+
+		it( 'should store attribute if the selection is in empty node', () => {
+			setSelection( rangeInEmptyP );
+			setSelectionAttribute( 'foo', 'bar' );
+
+			expect( model.document.selection.getAttribute( 'foo' ) ).to.equal( 'bar' );
+
+			expect( emptyP.getAttribute( fooStoreAttrKey ) ).to.equal( 'bar' );
+		} );
+
+		it( 'should be able to store attributes from the given object', () => {
+			setSelection( rangeInEmptyP );
+			setSelectionAttribute( { key1: 'foo', key2: 'bar' } );
+
+			expect( model.document.selection.getAttribute( 'key1' ) ).to.equal( 'foo' );
+			expect( model.document.selection.getAttribute( 'key2' ) ).to.equal( 'bar' );
+		} );
+
+		it( 'should be able to store attributes from the given iterable', () => {
+			setSelection( rangeInEmptyP );
+			setSelectionAttribute( new Map( [ [ 'key1', 'foo' ], [ 'key2', 'bar' ] ] ) );
+
+			expect( model.document.selection.getAttribute( 'key1' ) ).to.equal( 'foo' );
+			expect( model.document.selection.getAttribute( 'key2' ) ).to.equal( 'bar' );
+		} );
+	} );
+
+	describe( 'removeSelectionAttribute()', () => {
+		const fooStoreAttrKey = DocumentSelection._getStoreAttributeKey( 'foo' );
+		let root, rangeInEmptyP, emptyP;
+
+		beforeEach( () => {
+			model.schema.register( 'p', { inheritAllFrom: '$block' } );
+			model.schema.extend( 'p', { allowIn: '$root' } );
+
+			root = doc.createRoot();
+			root.appendChildren( [
+				new Element( 'p', [], [] ),
+				new Element( 'p' ),
+				new Element( 'p', [], new Text( 'foo' ) )
+			] );
+
+			rangeInEmptyP = new Range( new Position( root, [ 0, 0 ] ), new Position( root, [ 0, 0 ] ) );
+			emptyP = root.getChild( 0 );
+		} );
+
+		it( 'should remove stored attribute if the selection is in empty node', () => {
+			setSelection( rangeInEmptyP );
+			setSelectionAttribute( 'foo', 'bar' );
+			removeSelectionAttribute( 'foo' );
+
+			expect( model.document.selection.getAttribute( 'foo' ) ).to.be.undefined;
+
+			expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.false;
+		} );
+
+		it( 'should remove all attributes from the given iterable', () => {
+			setSelection( rangeInEmptyP );
+			setSelectionAttribute( 'foo', 'bar' );
+			setSelectionAttribute( 'foo2', 'bar2' );
+			removeSelectionAttribute( [ 'foo', 'foo2' ] );
+
+			expect( model.document.selection.getAttribute( 'foo' ) ).to.be.undefined;
+			expect( model.document.selection.getAttribute( 'foo2' ) ).to.be.undefined;
+
+			expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.false;
+		} );
+
+		it( 'should do nothing if attribute does not exist in the selection', () => {
+			setSelection( rangeInEmptyP );
+			setSelectionAttribute( 'foo', 'bar' );
+			setSelectionAttribute( 'foo2', 'bar2' );
+			removeSelectionAttribute( [ 'foo', 'baz' ] );
+
+			expect( model.document.selection.getAttribute( 'foo' ) ).to.be.undefined;
+			expect( model.document.selection.getAttribute( 'foo2' ) ).to.equal( 'bar2' );
+
+			expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.false;
+		} );
+	} );
+
 	function createText( data, attributes ) {
 		return model.change( writer => {
 			return writer.createText( data, attributes );
@@ -2157,4 +2328,28 @@ describe( 'Writer', () => {
 			writer.removeMarker( markerOrName );
 		} );
 	}
+
+	function setSelection( selectable, backwardSelectionOrOffset ) {
+		model.enqueueChange( batch, writer => {
+			writer.setSelection( selectable, backwardSelectionOrOffset );
+		} );
+	}
+
+	function setSelectionFocus( itemOrPosition, offset ) {
+		model.enqueueChange( batch, writer => {
+			writer.setSelectionFocus( itemOrPosition, offset );
+		} );
+	}
+
+	function setSelectionAttribute( key, value ) {
+		model.enqueueChange( batch, writer => {
+			writer.setSelectionAttribute( key, value );
+		} );
+	}
+
+	function removeSelectionAttribute( key ) {
+		model.enqueueChange( batch, writer => {
+			writer.removeSelectionAttribute( key );
+		} );
+	}
 } );

+ 3 - 3
packages/ckeditor5-engine/tests/view/document/document.js

@@ -288,7 +288,7 @@ describe( 'Document', () => {
 				left: '-1000px'
 			} );
 
-			viewDocument.selection.addRange( range );
+			viewDocument.selection.setTo( range );
 
 			viewDocument.scrollToTheSelection();
 			sinon.assert.calledWithMatch( stub, sinon.match.number, sinon.match.number );
@@ -346,7 +346,7 @@ describe( 'Document', () => {
 			document.body.appendChild( domEditable );
 			viewEditable = createViewRoot( viewDocument, 'div', 'main' );
 			viewDocument.attachDomRoot( domEditable );
-			viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewEditable, 0, viewEditable, 0 ) );
+			viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( viewEditable, 0, viewEditable, 0 ) );
 		} );
 
 		afterEach( () => {
@@ -383,7 +383,7 @@ describe( 'Document', () => {
 
 		it( 'should log warning when no selection', () => {
 			const logSpy = testUtils.sinon.stub( log, 'warn' );
-			viewDocument.selection.removeAllRanges();
+			viewDocument.selection.setTo( null );
 
 			viewDocument.focus();
 			expect( logSpy.calledOnce ).to.be.true;

+ 1 - 2
packages/ckeditor5-engine/tests/view/document/jumpoverinlinefiller.js

@@ -112,8 +112,7 @@ describe( 'Document', () => {
 			const viewB = viewDocument.selection.getFirstPosition().parent;
 			const viewTextX = parse( 'x' );
 			viewB.appendChildren( viewTextX );
-			viewDocument.selection.removeAllRanges();
-			viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewTextX, 1, viewTextX, 1 ) );
+			viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( viewTextX, 1, viewTextX, 1 ) );
 
 			const domB = viewDocument.getDomRoot( 'main' ).querySelector( 'b' );
 			const domSelection = document.getSelection();

+ 13 - 13
packages/ckeditor5-engine/tests/view/document/jumpoveruielement.js

@@ -89,7 +89,7 @@ describe( 'Document', () => {
 				// <container:p>foo<ui:span>xxx</ui:span>{}bar</container:p>
 				const p = new ViewContainerElement( 'p', null, [ foo, ui, bar ] );
 				viewRoot.appendChildren( p );
-				viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( bar, 0, bar, 0 ) ] );
+				viewDocument.selection.setTo( [ ViewRange.createFromParentsAndOffsets( bar, 0, bar, 0 ) ] );
 
 				renderAndFireKeydownEvent( { keyCode: keyCodes.arrowleft } );
 
@@ -104,7 +104,7 @@ describe( 'Document', () => {
 				// <container:p>foo[]<ui:span>xxx</ui:span>bar</container:p>
 				const p = new ViewContainerElement( 'p', null, [ foo, ui, bar ] );
 				viewRoot.appendChildren( p );
-				viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( p, 1, p, 1 ) ] );
+				viewDocument.selection.setTo( [ ViewRange.createFromParentsAndOffsets( p, 1, p, 1 ) ] );
 
 				renderAndFireKeydownEvent();
 
@@ -121,7 +121,7 @@ describe( 'Document', () => {
 				// <container:p>foo{}<ui:span>xxx</ui:span>bar</container:p>
 				const p = new ViewContainerElement( 'p', null, [ foo, ui, bar ] );
 				viewRoot.appendChildren( p );
-				viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
+				viewDocument.selection.setTo( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
 
 				renderAndFireKeydownEvent();
 
@@ -138,7 +138,7 @@ describe( 'Document', () => {
 				// <container:p>foo{}<ui:span>xxx</ui:span><ui:span>yyy</ui:span>bar</container:p>'
 				const p = new ViewContainerElement( 'p', null, [ foo, ui, ui2, bar ] );
 				viewRoot.appendChildren( p );
-				viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
+				viewDocument.selection.setTo( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
 
 				renderAndFireKeydownEvent();
 
@@ -157,7 +157,7 @@ describe( 'Document', () => {
 				const div = new ViewContainerElement( 'div' );
 				viewRoot.appendChildren( p );
 				viewRoot.appendChildren( div );
-				viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
+				viewDocument.selection.setTo( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
 
 				renderAndFireKeydownEvent();
 
@@ -175,7 +175,7 @@ describe( 'Document', () => {
 				const b = new ViewAttribtueElement( 'b', null, foo );
 				const p = new ViewContainerElement( 'p', null, [ b, ui, bar ] );
 				viewRoot.appendChildren( p );
-				viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
+				viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) );
 
 				renderAndFireKeydownEvent();
 
@@ -193,7 +193,7 @@ describe( 'Document', () => {
 				const b = new ViewAttribtueElement( 'b', null, foo );
 				const p = new ViewContainerElement( 'p', null, [ b, ui, bar ] );
 				viewRoot.appendChildren( p );
-				viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( b, 1, b, 1 ) ] );
+				viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( b, 1, b, 1 ) );
 
 				renderAndFireKeydownEvent();
 
@@ -221,7 +221,7 @@ describe( 'Document', () => {
 				const p = new ViewContainerElement( 'p', null, [ i, ui, bar ] );
 
 				viewRoot.appendChildren( p );
-				viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
+				viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) );
 
 				renderAndFireKeydownEvent();
 
@@ -248,7 +248,7 @@ describe( 'Document', () => {
 				const p = new ViewContainerElement( 'p', null, [ foo, b1, ui, ui2, b2, bar ] );
 
 				viewRoot.appendChildren( p );
-				viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
+				viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) );
 
 				renderAndFireKeydownEvent();
 
@@ -276,7 +276,7 @@ describe( 'Document', () => {
 				const p = new ViewContainerElement( 'p', null, [ foo, b1, ui, ui2, b2, bar ] );
 
 				viewRoot.appendChildren( p );
-				viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
+				viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) );
 
 				renderAndFireKeydownEvent( { shiftKey: true } );
 
@@ -352,7 +352,7 @@ describe( 'Document', () => {
 				const p = new ViewContainerElement( 'p', null, [ foo, ui, bar ] );
 
 				viewRoot.appendChildren( p );
-				viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) ] );
+				viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) );
 
 				renderAndFireKeydownEvent( { shiftKey: true } );
 
@@ -377,7 +377,7 @@ describe( 'Document', () => {
 				const i = new ViewAttribtueElement( 'i', null, b );
 				const p = new ViewContainerElement( 'p', null, [ i, ui, bar ] );
 				viewRoot.appendChildren( p );
-				viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) ] );
+				viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) );
 
 				renderAndFireKeydownEvent( { shiftKey: true } );
 
@@ -403,7 +403,7 @@ describe( 'Document', () => {
 				const b2 = new ViewAttribtueElement( 'b' );
 				const p = new ViewContainerElement( 'p', null, [ foo, b1, ui, ui2, b2, bar ] );
 				viewRoot.appendChildren( p );
-				viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) ] );
+				viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) );
 
 				renderAndFireKeydownEvent( { shiftKey: true } );
 

+ 3 - 4
packages/ckeditor5-engine/tests/view/domconverter/binding.js

@@ -270,8 +270,7 @@ describe( 'DomConverter', () => {
 		beforeEach( () => {
 			viewElement = new ViewElement();
 			domEl = document.createElement( 'div' );
-			selection = new ViewSelection();
-			selection.addRange( ViewRange.createIn( viewElement ) );
+			selection = new ViewSelection( ViewRange.createIn( viewElement ) );
 			converter.bindFakeSelection( domEl, selection );
 		} );
 
@@ -282,9 +281,9 @@ describe( 'DomConverter', () => {
 		} );
 
 		it( 'should keep a copy of selection', () => {
-			const selectionCopy = ViewSelection.createFromSelection( selection );
+			const selectionCopy = new ViewSelection( selection );
 
-			selection.addRange( ViewRange.createIn( new ViewElement() ), true );
+			selection.setTo( ViewRange.createIn( new ViewElement() ), true );
 			const bindSelection = converter.fakeSelectionToView( domEl );
 
 			expect( bindSelection ).to.not.equal( selection );

+ 2 - 4
packages/ckeditor5-engine/tests/view/domconverter/dom-to-view.js

@@ -859,8 +859,7 @@ describe( 'DomConverter', () => {
 			domContainer.innerHTML = 'fake selection container';
 			document.body.appendChild( domContainer );
 
-			const viewSelection = new ViewSelection();
-			viewSelection.addRange( ViewRange.createIn( new ViewElement() ) );
+			const viewSelection = new ViewSelection( ViewRange.createIn( new ViewElement() ) );
 			converter.bindFakeSelection( domContainer, viewSelection );
 
 			const domRange = document.createRange();
@@ -881,8 +880,7 @@ describe( 'DomConverter', () => {
 			domContainer.innerHTML = 'fake selection container';
 			document.body.appendChild( domContainer );
 
-			const viewSelection = new ViewSelection();
-			viewSelection.addRange( ViewRange.createIn( new ViewElement() ) );
+			const viewSelection = new ViewSelection( ViewRange.createIn( new ViewElement() ) );
 			converter.bindFakeSelection( domContainer, viewSelection );
 
 			const domRange = document.createRange();

+ 6 - 6
packages/ckeditor5-engine/tests/view/editableelement.js

@@ -78,13 +78,13 @@ describe( 'EditableElement', () => {
 		it( 'should change isFocused on document render event', () => {
 			const rangeMain = Range.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 );
 			const rangeHeader = Range.createFromParentsAndOffsets( viewHeader, 0, viewHeader, 0 );
-			docMock.selection.addRange( rangeMain );
+			docMock.selection.setTo( rangeMain );
 			docMock.isFocused = true;
 
 			expect( viewMain.isFocused ).to.be.true;
 			expect( viewHeader.isFocused ).to.be.false;
 
-			docMock.selection.setRanges( [ rangeHeader ] );
+			docMock.selection.setTo( [ rangeHeader ] );
 			docMock.fire( 'render' );
 
 			expect( viewMain.isFocused ).to.be.false;
@@ -96,13 +96,13 @@ describe( 'EditableElement', () => {
 			const rangeHeader = Range.createFromParentsAndOffsets( viewHeader, 0, viewHeader, 0 );
 			docMock.render = sinon.spy();
 
-			docMock.selection.addRange( rangeMain );
+			docMock.selection.setTo( rangeMain );
 			docMock.isFocused = true;
 
 			expect( viewMain.isFocused ).to.be.true;
 			expect( viewHeader.isFocused ).to.be.false;
 
-			docMock.selection.setRanges( [ rangeHeader ] );
+			docMock.selection.setTo( [ rangeHeader ] );
 
 			viewHeader.on( 'change:isFocused', ( evt, propertyName, value ) => {
 				expect( value ).to.be.true;
@@ -116,7 +116,7 @@ describe( 'EditableElement', () => {
 		it( 'should change isFocused when document.isFocus changes', () => {
 			const rangeMain = Range.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 );
 			const rangeHeader = Range.createFromParentsAndOffsets( viewHeader, 0, viewHeader, 0 );
-			docMock.selection.addRange( rangeMain );
+			docMock.selection.setTo( rangeMain );
 			docMock.isFocused = true;
 
 			expect( viewMain.isFocused ).to.be.true;
@@ -127,7 +127,7 @@ describe( 'EditableElement', () => {
 			expect( viewMain.isFocused ).to.be.false;
 			expect( viewHeader.isFocused ).to.be.false;
 
-			docMock.selection.setRanges( [ rangeHeader ] );
+			docMock.selection.setTo( [ rangeHeader ] );
 
 			expect( viewMain.isFocused ).to.be.false;
 			expect( viewHeader.isFocused ).to.be.false;

+ 1 - 1
packages/ckeditor5-engine/tests/view/manual/fakeselection.js

@@ -39,7 +39,7 @@ viewDocument.on( 'mouseup', ( evt, data ) => {
 		console.log( 'Making selection around the <strong>.' );
 
 		const range = ViewRange.createOn( viewStrong );
-		viewDocument.selection.setRanges( [ range ] );
+		viewDocument.selection.setTo( [ range ] );
 		viewDocument.selection.setFake( true, { label: 'fake selection over bar' } );
 
 		viewDocument.render();

+ 2 - 2
packages/ckeditor5-engine/tests/view/observer/focusobserver.js

@@ -94,7 +94,7 @@ describe( 'FocusObserver', () => {
 		} );
 
 		it( 'should set isFocused to false on blur when selection in same editable', () => {
-			viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 ) );
+			viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 ) );
 
 			observer.onDomEvent( { type: 'focus', target: domMain } );
 
@@ -106,7 +106,7 @@ describe( 'FocusObserver', () => {
 		} );
 
 		it( 'should not set isFocused to false on blur when it is fired on other editable', () => {
-			viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 ) );
+			viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 ) );
 
 			observer.onDomEvent( { type: 'focus', target: domMain } );
 

+ 1 - 1
packages/ckeditor5-engine/tests/view/observer/mutationobserver.js

@@ -25,7 +25,7 @@ describe( 'MutationObserver', () => {
 
 		createViewRoot( viewDocument );
 		viewDocument.attachDomRoot( domEditor );
-		viewDocument.selection.removeAllRanges();
+		viewDocument.selection.setTo( null );
 		document.getSelection().removeAllRanges();
 
 		mutationObserver = viewDocument.getObserver( MutationObserver );

+ 6 - 6
packages/ckeditor5-engine/tests/view/observer/selectionobserver.js

@@ -41,7 +41,7 @@ describe( 'SelectionObserver', () => {
 
 		viewDocument.render();
 
-		viewDocument.selection.removeAllRanges();
+		viewDocument.selection.setTo( null );
 		domDocument.getSelection().removeAllRanges();
 
 		viewDocument.isFocused = true;
@@ -103,7 +103,7 @@ describe( 'SelectionObserver', () => {
 		setTimeout( done, 70 );
 
 		const viewBar = viewDocument.getRoot().getChild( 1 ).getChild( 0 );
-		viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewBar, 1, viewBar, 2 ) );
+		viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( viewBar, 1, viewBar, 2 ) );
 		viewDocument.render();
 	} );
 
@@ -162,7 +162,7 @@ describe( 'SelectionObserver', () => {
 		let counter = 70;
 
 		const viewFoo = viewDocument.getRoot().getChild( 0 ).getChild( 0 );
-		viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewFoo, 0, viewFoo, 0 ) );
+		viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( viewFoo, 0, viewFoo, 0 ) );
 
 		return new Promise( ( resolve, reject ) => {
 			testUtils.sinon.stub( log, 'warn' ).callsFake( msg => {
@@ -186,7 +186,7 @@ describe( 'SelectionObserver', () => {
 
 	it( 'should not be treated as an infinite loop if selection is changed only few times', done => {
 		const viewFoo = viewDocument.getRoot().getChild( 0 ).getChild( 0 );
-		viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewFoo, 0, viewFoo, 0 ) );
+		viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( viewFoo, 0, viewFoo, 0 ) );
 		const spy = testUtils.sinon.spy( log, 'warn' );
 
 		viewDocument.on( 'selectionChangeDone', () => {
@@ -319,8 +319,8 @@ describe( 'SelectionObserver', () => {
 			const viewAnchor = viewDocument.domConverter.domPositionToView( sel.anchorNode, sel.anchorOffset );
 			const viewFocus = viewDocument.domConverter.domPositionToView( sel.focusNode, sel.focusOffset );
 
-			viewSel.setCollapsedAt( viewAnchor );
-			viewSel.moveFocusTo( viewFocus );
+			viewSel.setTo( viewAnchor );
+			viewSel.setFocus( viewFocus );
 
 			viewDocument.render();
 		} );

+ 3 - 3
packages/ckeditor5-engine/tests/view/placeholder.js

@@ -100,7 +100,7 @@ describe( 'placeholder', () => {
 			expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' );
 			expect( element.hasClass( 'ck-placeholder' ) ).to.be.true;
 
-			viewDocument.selection.setRanges( [ ViewRange.createIn( element ) ] );
+			viewDocument.selection.setTo( [ ViewRange.createIn( element ) ] );
 			viewDocument.render();
 
 			expect( element.hasClass( 'ck-placeholder' ) ).to.be.false;
@@ -146,8 +146,8 @@ describe( 'placeholder', () => {
 			expect( secondElement.hasClass( 'ck-placeholder' ) ).to.be.true;
 
 			// Move selection to the elements with placeholders.
-			viewDocument.selection.setRanges( [ ViewRange.createIn( element ) ] );
-			secondDocument.selection.setRanges( [ ViewRange.createIn( secondElement ) ] );
+			viewDocument.selection.setTo( [ ViewRange.createIn( element ) ] );
+			secondDocument.selection.setTo( [ ViewRange.createIn( secondElement ) ] );
 
 			// Render changes.
 			viewDocument.render();

+ 25 - 40
packages/ckeditor5-engine/tests/view/renderer.js

@@ -122,7 +122,7 @@ describe( 'Renderer', () => {
 			renderer.markedAttributes.clear();
 			renderer.markedChildren.clear();
 
-			selection.removeAllRanges();
+			selection.setTo( null );
 			selection.setFake( false );
 
 			selectionEditable = viewRoot;
@@ -495,8 +495,7 @@ describe( 'Renderer', () => {
 			renderAndExpectNoChanges( renderer, domRoot );
 
 			// Step 3: <p>foo{}<b></b></p>
-			selection.removeAllRanges();
-			selection.addRange( ViewRange.createFromParentsAndOffsets( viewP.getChild( 0 ), 3, viewP.getChild( 0 ), 3 ) );
+			selection.setTo( ViewRange.createFromParentsAndOffsets( viewP.getChild( 0 ), 3, viewP.getChild( 0 ), 3 ) );
 
 			renderer.render();
 
@@ -549,8 +548,7 @@ describe( 'Renderer', () => {
 			renderAndExpectNoChanges( renderer, domRoot );
 
 			// Step 3: <p><b>{}foo</b></p>
-			selection.removeAllRanges();
-			selection.addRange( ViewRange.createFromParentsAndOffsets(
+			selection.setTo( ViewRange.createFromParentsAndOffsets(
 				viewP.getChild( 0 ).getChild( 0 ), 0, viewP.getChild( 0 ).getChild( 0 ), 0 ) );
 
 			renderer.render();
@@ -601,8 +599,7 @@ describe( 'Renderer', () => {
 			renderAndExpectNoChanges( renderer, domRoot );
 
 			// Step 3: <p><b>foo{}</b></p>
-			selection.removeAllRanges();
-			selection.addRange( ViewRange.createFromParentsAndOffsets(
+			selection.setTo( ViewRange.createFromParentsAndOffsets(
 				viewP.getChild( 0 ).getChild( 0 ), 3, viewP.getChild( 0 ).getChild( 0 ), 3 ) );
 
 			renderer.render();
@@ -674,9 +671,8 @@ describe( 'Renderer', () => {
 			expect( domP.childNodes[ 2 ].childNodes.length ).to.equal( 0 );
 
 			// Step 2: <p>foo<b></b><i>"FILLER{}"</i></p>
-			selection.removeAllRanges();
 			const viewI = viewP.getChild( 2 );
-			selection.addRange( ViewRange.createFromParentsAndOffsets( viewI, 0, viewI, 0 ) );
+			selection.setTo( ViewRange.createFromParentsAndOffsets( viewI, 0, viewI, 0 ) );
 
 			renderer.render();
 
@@ -709,14 +705,13 @@ describe( 'Renderer', () => {
 			// Step 2: Add text node.
 			const viewText = new ViewText( 'x' );
 			viewB.appendChildren( viewText );
-			selection.removeAllRanges();
-			selection.addRange( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
+			selection.setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
 
 			renderer.markToSync( 'children', viewB );
 			renderer.render();
 
 			// Step 3: Remove selection from the view.
-			selection.removeAllRanges();
+			selection.setTo( null );
 
 			renderer.render();
 
@@ -746,8 +741,7 @@ describe( 'Renderer', () => {
 			// Step 2: Remove the <b> and update the selection (<p>bar[]</p>).
 			viewP.removeChildren( 1 );
 
-			selection.removeAllRanges();
-			selection.addRange( ViewRange.createFromParentsAndOffsets( viewP, 1, viewP, 1 ) );
+			selection.setTo( ViewRange.createFromParentsAndOffsets( viewP, 1, viewP, 1 ) );
 
 			renderer.markToSync( 'children', viewP );
 			renderer.render();
@@ -784,8 +778,7 @@ describe( 'Renderer', () => {
 
 			viewP2.appendChildren( removedChildren );
 
-			selection.removeAllRanges();
-			selection.addRange( ViewRange.createFromParentsAndOffsets( viewP, 0, viewP, 0 ) );
+			selection.setTo( ViewRange.createFromParentsAndOffsets( viewP, 0, viewP, 0 ) );
 
 			renderer.markToSync( 'children', viewP );
 			renderer.markToSync( 'children', viewP2 );
@@ -822,8 +815,7 @@ describe( 'Renderer', () => {
 			const viewI = parse( '<attribute:i></attribute:i>' );
 			viewP.appendChildren( viewI );
 
-			selection.removeAllRanges();
-			selection.addRange( ViewRange.createFromParentsAndOffsets( viewI, 0, viewI, 0 ) );
+			selection.setTo( ViewRange.createFromParentsAndOffsets( viewI, 0, viewI, 0 ) );
 
 			renderer.markToSync( 'children', viewP );
 			renderer.render();
@@ -853,8 +845,7 @@ describe( 'Renderer', () => {
 			const viewAbc = parse( 'abc' );
 			viewP.appendChildren( viewAbc );
 
-			selection.removeAllRanges();
-			selection.addRange( ViewRange.createFromParentsAndOffsets( viewP, 3, viewP, 3 ) );
+			selection.setTo( ViewRange.createFromParentsAndOffsets( viewP, 3, viewP, 3 ) );
 
 			renderer.markToSync( 'children', viewP );
 			renderer.render();
@@ -897,8 +888,7 @@ describe( 'Renderer', () => {
 
 			const viewText = new ViewText( 'x' );
 			viewP.appendChildren( viewText );
-			selection.removeAllRanges();
-			selection.addRange( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
+			selection.setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
 
 			renderer.markToSync( 'children', viewP );
 			renderAndExpectNoChanges( renderer, domRoot );
@@ -928,8 +918,7 @@ describe( 'Renderer', () => {
 			// Add text node only in View <p>x{}</p>
 			const viewText = new ViewText( 'x' );
 			viewP.appendChildren( viewText );
-			selection.removeAllRanges();
-			selection.addRange( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
+			selection.setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
 
 			renderer.markToSync( 'children', viewP );
 			renderer.render();
@@ -976,8 +965,7 @@ describe( 'Renderer', () => {
 
 			viewP.removeChildren( 0 );
 
-			selection.removeAllRanges();
-			selection.addRange( ViewRange.createFromParentsAndOffsets( viewP, 0, viewP, 0 ) );
+			selection.setTo( ViewRange.createFromParentsAndOffsets( viewP, 0, viewP, 0 ) );
 
 			renderer.markToSync( 'children', viewP );
 			renderAndExpectNoChanges( renderer, domRoot );
@@ -1028,8 +1016,7 @@ describe( 'Renderer', () => {
 
 			const viewText = new ViewText( 'x' );
 			viewB.appendChildren( viewText );
-			selection.removeAllRanges();
-			selection.addRange( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
+			selection.setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
 
 			renderer.markToSync( 'children', viewP );
 			renderAndExpectNoChanges( renderer, domRoot );
@@ -1072,8 +1059,7 @@ describe( 'Renderer', () => {
 
 			const viewText = new ViewText( 'x' );
 			viewB.appendChildren( viewText );
-			selection.removeAllRanges();
-			selection.addRange( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
+			selection.setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
 
 			renderer.markToSync( 'children', viewB );
 			renderer.render();
@@ -1136,8 +1122,7 @@ describe( 'Renderer', () => {
 
 			const viewText = new ViewText( 'x' );
 			viewB.appendChildren( viewText );
-			selection.removeAllRanges();
-			selection.addRange( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
+			selection.setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) );
 
 			renderer.markToSync( 'text', viewText );
 			renderer.render();
@@ -1297,7 +1282,7 @@ describe( 'Renderer', () => {
 			// Remove filler.
 			domB.childNodes[ 0 ].data = '';
 
-			selection.removeAllRanges();
+			selection.setTo( null );
 			renderer.markToSync( 'children', viewB );
 
 			expect( () => {
@@ -1588,7 +1573,7 @@ describe( 'Renderer', () => {
 				selectionExtendSpy = sinon.spy( window.Selection.prototype, 'extend' );
 
 				// <container:p>foo<attribute:b>{}bar</attribute:b></container:p>
-				selection.setRanges( [
+				selection.setTo( [
 					new ViewRange( new ViewPosition( viewB.getChild( 0 ), 0 ), new ViewPosition( viewB.getChild( 0 ), 0 ) )
 				] );
 
@@ -1627,7 +1612,7 @@ describe( 'Renderer', () => {
 				selectionExtendSpy = sinon.spy( window.Selection.prototype, 'extend' );
 
 				// <container:p>foo{}<attribute:b></attribute:b></container:p>
-				selection.setRanges( [
+				selection.setTo( [
 					new ViewRange( new ViewPosition( viewP.getChild( 0 ), 3 ), new ViewPosition( viewP.getChild( 0 ), 3 ) )
 				] );
 
@@ -1667,7 +1652,7 @@ describe( 'Renderer', () => {
 				selectionExtendSpy = sinon.spy( window.Selection.prototype, 'extend' );
 
 				// <container:p>fo{o<attribute:b>b}ar</attribute:b></container:p>
-				selection.setRanges( [
+				selection.setTo( [
 					new ViewRange( new ViewPosition( viewP.getChild( 0 ), 2 ), new ViewPosition( viewB.getChild( 0 ), 1 ) )
 				] );
 
@@ -1748,7 +1733,7 @@ describe( 'Renderer', () => {
 				selectionExtendSpy = sinon.spy( window.Selection.prototype, 'extend' );
 
 				// <container:p>foo{<attribute:b>ba}r</attribute:b></container:p>
-				selection.setRanges( [
+				selection.setTo( [
 					new ViewRange( new ViewPosition( viewP.getChild( 0 ), 3 ), new ViewPosition( viewB.getChild( 0 ), 2 ) )
 				] );
 
@@ -1786,7 +1771,7 @@ describe( 'Renderer', () => {
 				selectionExtendSpy = sinon.spy( window.Selection.prototype, 'extend' );
 
 				// <container:p>foo<attribute:b>b{ar</attribute:b>}baz</container:p>
-				selection.setRanges( [
+				selection.setTo( [
 					new ViewRange( new ViewPosition( viewB.getChild( 0 ), 1 ), new ViewPosition( viewP.getChild( 2 ), 0 ) )
 				] );
 
@@ -1824,7 +1809,7 @@ describe( 'Renderer', () => {
 				selectionExtendSpy = sinon.spy( window.Selection.prototype, 'extend' );
 
 				// <container:p>foo{<attribute:b><attribute:i>ba}r</attribute:i></attribute:b></container:p>
-				selection.setRanges( [
+				selection.setTo( [
 					new ViewRange( new ViewPosition( viewP.getChild( 0 ), 3 ), new ViewPosition( viewI.getChild( 0 ), 2 ) )
 				] );
 
@@ -1861,7 +1846,7 @@ describe( 'Renderer', () => {
 				selectionExtendSpy = sinon.spy( window.Selection.prototype, 'extend' );
 
 				// <container:p>f{oo<attribute:b><attribute:i>bar</attribute:i></attribute:b>}baz</container:p>
-				selection.setRanges( [
+				selection.setTo( [
 					new ViewRange( new ViewPosition( viewP.getChild( 0 ), 1 ), new ViewPosition( viewP.getChild( 2 ), 0 ) )
 				] );
 

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 322 - 324
packages/ckeditor5-engine/tests/view/selection.js


이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.