Browse Source

Merge pull request #1237 from ckeditor/t/1209

Other: Moved selection methods to `Writer`, introduced `LiveSelection`. Closes #1209.
Piotr Jasiun 7 years ago
parent
commit
6f38219059
45 changed files with 2258 additions and 1905 deletions
  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. 471 311
      packages/ckeditor5-engine/src/model/documentselection.js
  8. 137 215
      packages/ckeditor5-engine/src/model/selection.js
  9. 14 3
      packages/ckeditor5-engine/src/model/utils/deletecontent.js
  10. 8 2
      packages/ckeditor5-engine/src/model/utils/insertcontent.js
  11. 8 1
      packages/ckeditor5-engine/src/model/utils/modifyselection.js
  12. 155 0
      packages/ckeditor5-engine/src/model/writer.js
  13. 5 4
      packages/ckeditor5-engine/src/view/domconverter.js
  14. 3 3
      packages/ckeditor5-engine/src/view/observer/fakeselectionobserver.js
  15. 2 2
      packages/ckeditor5-engine/src/view/observer/mutationobserver.js
  16. 123 143
      packages/ckeditor5-engine/src/view/selection.js
  17. 1 1
      packages/ckeditor5-engine/src/view/writer.js
  18. 22 19
      packages/ckeditor5-engine/tests/controller/editingcontroller.js
  19. 7 8
      packages/ckeditor5-engine/tests/conversion/buildmodelconverter.js
  20. 57 47
      packages/ckeditor5-engine/tests/conversion/model-selection-to-view-converters.js
  21. 31 20
      packages/ckeditor5-engine/tests/conversion/modelconversiondispatcher.js
  22. 19 17
      packages/ckeditor5-engine/tests/conversion/view-selection-to-model-converters.js
  23. 56 33
      packages/ckeditor5-engine/tests/dev-utils/model.js
  24. 10 19
      packages/ckeditor5-engine/tests/dev-utils/view.js
  25. 5 3
      packages/ckeditor5-engine/tests/model/document.js
  26. 138 193
      packages/ckeditor5-engine/tests/model/documentselection.js
  27. 4 8
      packages/ckeditor5-engine/tests/model/schema.js
  28. 285 372
      packages/ckeditor5-engine/tests/model/selection.js
  29. 45 3
      packages/ckeditor5-engine/tests/model/utils/deletecontent.js
  30. 18 0
      packages/ckeditor5-engine/tests/model/utils/insertcontent.js
  31. 2 2
      packages/ckeditor5-engine/tests/model/utils/modifyselection.js
  32. 196 1
      packages/ckeditor5-engine/tests/model/writer.js
  33. 3 3
      packages/ckeditor5-engine/tests/view/document/document.js
  34. 1 2
      packages/ckeditor5-engine/tests/view/document/jumpoverinlinefiller.js
  35. 13 13
      packages/ckeditor5-engine/tests/view/document/jumpoveruielement.js
  36. 3 4
      packages/ckeditor5-engine/tests/view/domconverter/binding.js
  37. 2 4
      packages/ckeditor5-engine/tests/view/domconverter/dom-to-view.js
  38. 6 6
      packages/ckeditor5-engine/tests/view/editableelement.js
  39. 1 1
      packages/ckeditor5-engine/tests/view/manual/fakeselection.js
  40. 2 2
      packages/ckeditor5-engine/tests/view/observer/focusobserver.js
  41. 1 1
      packages/ckeditor5-engine/tests/view/observer/mutationobserver.js
  42. 6 6
      packages/ckeditor5-engine/tests/view/observer/selectionobserver.js
  43. 3 3
      packages/ckeditor5-engine/tests/view/placeholder.js
  44. 25 40
      packages/ckeditor5-engine/tests/view/renderer.js
  45. 324 345
      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,

File diff suppressed because it is too large
+ 471 - 311
packages/ckeditor5-engine/src/model/documentselection.js


+ 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.
@@ -335,93 +404,6 @@ export default class Selection {
 	}
 
 	/**
-	 * 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.
 	 *
 	 * The location can be specified in the same form as {@link module:engine/model/position~Position.createAt} parameters.
@@ -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>}
 	 */
@@ -504,23 +490,6 @@ export default class Selection {
 	}
 
 	/**
-	 * 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.
 	 *
 	 * If given attribute was set on the selection, fires the {@link #event:change} event with
@@ -556,35 +525,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.
 	 * Returns `null` if there is no selected element.
@@ -676,20 +616,6 @@ 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.
-	 *
-	 * @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 ) );
 	}
@@ -730,19 +652,10 @@ 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 ) {
@@ -751,6 +664,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 ) {

+ 8 - 2
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.

+ 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,7 +394,7 @@ export default class Selection {
 	 *
 	 * @fires change
 	 */
-	removeAllRanges() {
+	_removeAllRanges() {
 		if ( this._ranges.length ) {
 			this._ranges = [];
 			this.fire( 'change' );
@@ -405,117 +402,86 @@ export default class 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 (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>' );
 		} );

+ 5 - 3
packages/ckeditor5-engine/tests/model/document.js

@@ -391,7 +391,9 @@ describe( 'Document', () => {
 				if ( expected === null ) {
 					expect( range ).to.be.null;
 				} else {
-					selection.setRanges( [ range ] );
+					model.change( writer => {
+						writer.setSelection( range );
+					} );
 					expect( getData( model ) ).to.equal( expected );
 				}
 			} );
@@ -578,8 +580,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/ );
 	} );
 } );

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

@@ -1057,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>]' );
 		} );
@@ -1076,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>]' );
 		} );
@@ -1086,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>' );
 		} );
@@ -1098,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>' );
 		} );

File diff suppressed because it is too large
+ 285 - 372
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 ) )
 				] );
 

File diff suppressed because it is too large
+ 324 - 345
packages/ckeditor5-engine/tests/view/selection.js