8
0
Pārlūkot izejas kodu

Merge pull request #1805 from ckeditor/i/5649

Other: Split debug tools between the engine files. You do not need to add debug plugin to the editor if you want to use `--debug engine` building flag anymore. Closes ckeditor/ckeditor5#5649.
Piotr Jasiun 6 gadi atpakaļ
vecāks
revīzija
79d2336c2f
29 mainītis faili ar 457 papildinājumiem un 1299 dzēšanām
  1. 13 1
      packages/ckeditor5-engine/src/controller/editingcontroller.js
  2. 0 588
      packages/ckeditor5-engine/src/dev-utils/enableenginedebug.js
  3. 103 0
      packages/ckeditor5-engine/src/dev-utils/utils.js
  4. 7 0
      packages/ckeditor5-engine/src/model/document.js
  5. 40 0
      packages/ckeditor5-engine/src/model/documentfragment.js
  6. 63 0
      packages/ckeditor5-engine/src/model/element.js
  7. 34 1
      packages/ckeditor5-engine/src/model/model.js
  8. 6 0
      packages/ckeditor5-engine/src/model/operation/attributeoperation.js
  9. 10 0
      packages/ckeditor5-engine/src/model/operation/detachoperation.js
  10. 6 0
      packages/ckeditor5-engine/src/model/operation/insertoperation.js
  11. 5 0
      packages/ckeditor5-engine/src/model/operation/markeroperation.js
  12. 6 0
      packages/ckeditor5-engine/src/model/operation/mergeoperation.js
  13. 8 0
      packages/ckeditor5-engine/src/model/operation/moveoperation.js
  14. 4 0
      packages/ckeditor5-engine/src/model/operation/nooperation.js
  15. 4 0
      packages/ckeditor5-engine/src/model/operation/operation.js
  16. 5 0
      packages/ckeditor5-engine/src/model/operation/renameoperation.js
  17. 6 0
      packages/ckeditor5-engine/src/model/operation/rootattributeoperation.js
  18. 6 0
      packages/ckeditor5-engine/src/model/operation/splitoperation.js
  19. 8 0
      packages/ckeditor5-engine/src/model/position.js
  20. 8 0
      packages/ckeditor5-engine/src/model/range.js
  21. 8 0
      packages/ckeditor5-engine/src/model/rootelement.js
  22. 14 0
      packages/ckeditor5-engine/src/model/text.js
  23. 15 0
      packages/ckeditor5-engine/src/model/textproxy.js
  24. 6 0
      packages/ckeditor5-engine/src/view/document.js
  25. 20 0
      packages/ckeditor5-engine/src/view/documentfragment.js
  26. 28 0
      packages/ckeditor5-engine/src/view/element.js
  27. 12 0
      packages/ckeditor5-engine/src/view/text.js
  28. 12 0
      packages/ckeditor5-engine/src/view/textproxy.js
  29. 0 709
      packages/ckeditor5-engine/tests/dev-utils/enableenginedebug.js

+ 13 - 1
packages/ckeditor5-engine/src/controller/editingcontroller.js

@@ -17,6 +17,8 @@ import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import { convertSelectionChange } from '../conversion/upcasthelpers';
 
+// @if CK_DEBUG_ENGINE // const { dumpTrees, initDocumentDumping } = require( '../dev-utils/utils' );
+
 /**
  * Controller for the editing pipeline. The editing pipeline controls {@link ~EditingController#model model} rendering,
  * including selection handling. It also creates the {@link ~EditingController#view view} which builds a
@@ -73,7 +75,7 @@ export default class EditingController {
 		// model's change, they might trigger view rendering before the conversion is completed (e.g. before the selection
 		// is converted). We disable rendering for the length of the outermost model change() block to prevent that.
 		//
-		// See  https://github.com/ckeditor/ckeditor5-engine/issues/1528
+		// See https://github.com/ckeditor/ckeditor5-engine/issues/1528
 		this.listenTo( this.model, '_beforeChanges', () => {
 			this.view._disableRendering( true );
 		}, { priority: 'highest' } );
@@ -121,6 +123,16 @@ export default class EditingController {
 
 			return viewRoot;
 		} );
+
+		// @if CK_DEBUG_ENGINE // initDocumentDumping( this.model.document );
+		// @if CK_DEBUG_ENGINE // initDocumentDumping( this.view.document );
+
+		// @if CK_DEBUG_ENGINE // dumpTrees( this.model.document, this.model.document.version );
+		// @if CK_DEBUG_ENGINE // dumpTrees( this.view.document, this.model.document.version );
+
+		// @if CK_DEBUG_ENGINE // this.model.document.on( 'change', () => {
+		// @if CK_DEBUG_ENGINE //	dumpTrees( this.view.document, this.model.document.version );
+		// @if CK_DEBUG_ENGINE // }, { priority: 'lowest' } );
 	}
 
 	/**

+ 0 - 588
packages/ckeditor5-engine/src/dev-utils/enableenginedebug.js

@@ -1,588 +0,0 @@
-/**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
-
-/**
- * @module engine/dev-utils/enableenginedebug
- */
-
-/* global console */
-
-import OperationReplayer from './operationreplayer';
-
-import ModelPosition from '../model/position';
-import ModelRange from '../model/range';
-import ModelText from '../model/text';
-import ModelTextProxy from '../model/textproxy';
-import ModelElement from '../model/element';
-import Operation from '../model/operation/operation';
-import AttributeOperation from '../model/operation/attributeoperation';
-import DetachOperation from '../model/operation/detachoperation';
-import InsertOperation from '../model/operation/insertoperation';
-import MarkerOperation from '../model/operation/markeroperation';
-import MoveOperation from '../model/operation/moveoperation';
-import NoOperation from '../model/operation/nooperation';
-import RenameOperation from '../model/operation/renameoperation';
-import RootAttributeOperation from '../model/operation/rootattributeoperation';
-import SplitOperation from '../model/operation/splitoperation';
-import MergeOperation from '../model/operation/mergeoperation';
-import Model from '../model/model';
-import ModelDocument from '../model/document';
-import ModelDocumentFragment from '../model/documentfragment';
-import ModelRootElement from '../model/rootelement';
-
-import ViewDocument from '../view/document';
-import ViewElement from '../view/element';
-import ViewText from '../view/text';
-import ViewTextProxy from '../view/textproxy';
-import ViewDocumentFragment from '../view/documentfragment';
-
-import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import Editor from '@ckeditor/ckeditor5-core/src/editor/editor';
-
-// Sandbox class allows creating mocks of the functions and restoring these mocks to the original values.
-class Sandbox {
-	constructor() {
-		// An array that contains functions which restore the original values of mocked objects.
-		// @private
-		// @type {Array.<Function>}
-		this._restores = [];
-	}
-
-	// Creates a new mock.
-	//
-	// @param {Object} object Object to mock.
-	// @param {String} methodName Function to mock.
-	// @param {Function} fakeMethod Function that will be executed.
-	mock( object, methodName, fakeMethod ) {
-		const originalMethod = object[ methodName ];
-
-		object[ methodName ] = fakeMethod;
-
-		this._restores.unshift( () => {
-			if ( originalMethod ) {
-				object[ methodName ] = originalMethod;
-			} else {
-				delete object[ methodName ];
-			}
-		} );
-	}
-
-	// Restores all mocked functions.
-	restore() {
-		for ( const restore of this._restores ) {
-			restore();
-		}
-
-		this._restores = [];
-	}
-}
-
-const sandbox = new Sandbox();
-
-const treeDump = Symbol( '_treeDump' );
-
-// Maximum number of stored states of model and view document.
-const maxTreeDumpLength = 20;
-
-// Separator used to separate stringified operations.
-const LOG_SEPARATOR = '-------';
-
-// Specified whether debug tools were already enabled.
-let enabled = false;
-
-// Logging function used to log debug messages.
-let logger = console;
-
-/**
- * Enhances model classes with logging methods. Returns a plugin that should be loaded in the editor to
- * enable debugging features.
- *
- * Every operation applied on {@link module:engine/model/document~Document model.Document} is logged.
- *
- * The following classes are expanded with `log` and meaningful `toString` methods:
- * * {@link module:engine/model/position~Position model.Position},
- * * {@link module:engine/model/range~Range model.Range},
- * * {@link module:engine/model/text~Text model.Text},
- * * {@link module:engine/model/element~Element model.Element},
- * * {@link module:engine/model/rootelement~RootElement model.RootElement},
- * * {@link module:engine/model/documentfragment~DocumentFragment model.DocumentFragment},
- * * {@link module:engine/model/document~Document model.Document},
- * * all {@link module:engine/model/operation/operation~Operation operations}
- * * {@link module:engine/view/element~Element view.Element},
- * * {@link module:engine/view/documentfragment~DocumentFragment view.DocumentFragment},
- * * {@link module:engine/view/document~Document view.Document}.
- *
- * Additionally, the following logging utility methods are added:
- * * {@link module:engine/model/text~Text model.Text} `logExtended`,
- * * {@link module:engine/model/element~Element model.Element} `logExtended`,
- * * {@link module:engine/model/element~Element model.Element} `logAll`.
- *
- * Additionally, the following classes are expanded with `logTree` and `printTree` methods:
- * * {@link module:engine/model/element~Element model.Element},
- * * {@link module:engine/model/documentfragment~DocumentFragment model.DocumentFragment},
- * * {@link module:engine/view/element~Element view.Element},
- * * {@link module:engine/view/documentfragment~DocumentFragment view.DocumentFragment}.
- *
- * Finally, the following methods are added to {@link module:core/editor/editor~Editor}: `logModel`, `logView`, `logDocuments`.
- * All those methods take one parameter, which is the version of the {@link module:engine/model/document~Document model document}
- * for which the model or view document state should be logged.
- *
- * @param {Object} [_logger] An object with functions used to log messages and errors. By default messages are logged to the console.
- * If specified, it is expected to have `log()` and `error()` methods.
- * @returns {module:engine/dev-utils/enableenginedebug~DebugPlugin} The plugin to be loaded into the editor.
- */
-export default function enableEngineDebug( _logger = console ) {
-	logger = _logger;
-
-	if ( !enabled ) {
-		enabled = true;
-
-		enableLoggingTools();
-		enableDocumentTools();
-		enableReplayerTools();
-	}
-
-	return DebugPlugin;
-}
-
-/**
- * Restores all methods that have been overwritten.
- */
-export function disableEngineDebug() {
-	sandbox.restore();
-	enabled = false;
-}
-
-function enableLoggingTools() {
-	sandbox.mock( ModelPosition.prototype, 'toString', function() {
-		return `${ this.root } [ ${ this.path.join( ', ' ) } ]`;
-	} );
-
-	sandbox.mock( ModelPosition.prototype, 'log', function() {
-		logger.log( 'ModelPosition: ' + this );
-	} );
-
-	sandbox.mock( ModelRange.prototype, 'toString', function() {
-		return `${ this.root } [ ${ this.start.path.join( ', ' ) } ] - [ ${ this.end.path.join( ', ' ) } ]`;
-	} );
-
-	sandbox.mock( ModelRange.prototype, 'log', function() {
-		logger.log( 'ModelRange: ' + this );
-	} );
-
-	sandbox.mock( ModelText.prototype, 'toString', function() {
-		return `#${ this.data }`;
-	} );
-
-	sandbox.mock( ModelText.prototype, 'logExtended', function() {
-		logger.log( `ModelText: ${ this }, attrs: ${ mapString( this.getAttributes() ) }` );
-	} );
-
-	sandbox.mock( ModelText.prototype, 'log', function() {
-		logger.log( 'ModelText: ' + this );
-	} );
-
-	sandbox.mock( ModelTextProxy.prototype, 'toString', function() {
-		return `#${ this.data }`;
-	} );
-
-	sandbox.mock( ModelTextProxy.prototype, 'logExtended', function() {
-		logger.log( `ModelTextProxy: ${ this }, attrs: ${ mapString( this.getAttributes() ) }` );
-	} );
-
-	sandbox.mock( ModelTextProxy.prototype, 'log', function() {
-		logger.log( 'ModelTextProxy: ' + this );
-	} );
-
-	sandbox.mock( ModelElement.prototype, 'toString', function() {
-		return `<${ this.rootName || this.name }>`;
-	} );
-
-	sandbox.mock( ModelElement.prototype, 'log', function() {
-		logger.log( 'ModelElement: ' + this );
-	} );
-
-	sandbox.mock( ModelElement.prototype, 'logExtended', function() {
-		logger.log( `ModelElement: ${ this }, ${ this.childCount } children, attrs: ${ mapString( this.getAttributes() ) }` );
-	} );
-
-	sandbox.mock( ModelElement.prototype, 'logAll', function() {
-		logger.log( '--------------------' );
-
-		this.logExtended();
-		logger.log( 'List of children:' );
-
-		for ( const child of this.getChildren() ) {
-			child.log();
-		}
-	} );
-
-	sandbox.mock( ModelElement.prototype, 'printTree', function( level = 0 ) {
-		let string = '';
-
-		string += '\t'.repeat( level ) + `<${ this.rootName || this.name }${ mapToTags( this.getAttributes() ) }>`;
-
-		for ( const child of this.getChildren() ) {
-			string += '\n';
-
-			if ( child.is( 'text' ) ) {
-				const textAttrs = mapToTags( child._attrs );
-
-				string += '\t'.repeat( level + 1 );
-
-				if ( textAttrs !== '' ) {
-					string += `<$text${ textAttrs }>` + child.data + '</$text>';
-				} else {
-					string += child.data;
-				}
-			} else {
-				string += child.printTree( level + 1 );
-			}
-		}
-
-		if ( this.childCount ) {
-			string += '\n' + '\t'.repeat( level );
-		}
-
-		string += `</${ this.rootName || this.name }>`;
-
-		return string;
-	} );
-
-	sandbox.mock( ModelElement.prototype, 'logTree', function() {
-		logger.log( this.printTree() );
-	} );
-
-	sandbox.mock( ModelRootElement.prototype, 'toString', function() {
-		return this.rootName;
-	} );
-
-	sandbox.mock( ModelRootElement.prototype, 'log', function() {
-		logger.log( 'ModelRootElement: ' + this );
-	} );
-
-	sandbox.mock( ModelDocumentFragment.prototype, 'toString', function() {
-		return 'documentFragment';
-	} );
-
-	sandbox.mock( ModelDocumentFragment.prototype, 'log', function() {
-		logger.log( 'ModelDocumentFragment: ' + this );
-	} );
-
-	sandbox.mock( ModelDocumentFragment.prototype, 'printTree', function() {
-		let string = 'ModelDocumentFragment: [';
-
-		for ( const child of this.getChildren() ) {
-			string += '\n';
-
-			if ( child.is( 'text' ) ) {
-				const textAttrs = mapToTags( child._attrs );
-
-				string += '\t'.repeat( 1 );
-
-				if ( textAttrs !== '' ) {
-					string += `<$text${ textAttrs }>` + child.data + '</$text>';
-				} else {
-					string += child.data;
-				}
-			} else {
-				string += child.printTree( 1 );
-			}
-		}
-
-		string += '\n]';
-
-		return string;
-	} );
-
-	sandbox.mock( ModelDocumentFragment.prototype, 'logTree', function() {
-		logger.log( this.printTree() );
-	} );
-
-	sandbox.mock( Operation.prototype, 'log', function() {
-		logger.log( this.toString() );
-	} );
-
-	sandbox.mock( AttributeOperation.prototype, 'toString', function() {
-		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
-			`"${ this.key }": ${ JSON.stringify( this.oldValue ) } -> ${ JSON.stringify( this.newValue ) }, ${ this.range }`;
-	} );
-
-	sandbox.mock( DetachOperation.prototype, 'toString', function() {
-		const range = ModelRange._createFromPositionAndShift( this.sourcePosition, this.howMany );
-		const nodes = Array.from( range.getItems() );
-		const nodeString = nodes.length > 1 ? `[ ${ nodes.length } ]` : nodes[ 0 ];
-
-		return getClassName( this ) + `( ${ this.baseVersion } ): ${ nodeString } -> ${ range }`;
-	} );
-
-	sandbox.mock( InsertOperation.prototype, 'toString', function() {
-		const nodeString = this.nodes.length > 1 ? `[ ${ this.nodes.length } ]` : this.nodes.getNode( 0 );
-
-		return getClassName( this ) + `( ${ this.baseVersion } ): ${ nodeString } -> ${ this.position }`;
-	} );
-
-	sandbox.mock( MarkerOperation.prototype, 'toString', function() {
-		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
-			`"${ this.name }": ${ this.oldRange } -> ${ this.newRange }`;
-	} );
-
-	sandbox.mock( MoveOperation.prototype, 'toString', function() {
-		const range = ModelRange._createFromPositionAndShift( this.sourcePosition, this.howMany );
-
-		return getClassName( this ) + `( ${ this.baseVersion } ): ${ range } -> ${ this.targetPosition }`;
-	} );
-
-	sandbox.mock( NoOperation.prototype, 'toString', function() {
-		return `NoOperation( ${ this.baseVersion } )`;
-	} );
-
-	sandbox.mock( RenameOperation.prototype, 'toString', function() {
-		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
-			`${ this.position }: "${ this.oldName }" -> "${ this.newName }"`;
-	} );
-
-	sandbox.mock( RootAttributeOperation.prototype, 'toString', function() {
-		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
-			`"${ this.key }": ${ JSON.stringify( this.oldValue ) } -> ${ JSON.stringify( this.newValue ) }, ${ this.root.rootName }`;
-	} );
-
-	sandbox.mock( MergeOperation.prototype, 'toString', function() {
-		return getClassName( this ) + `( ${ this.baseVersion } ): ` +
-			`${ this.sourcePosition } -> ${ this.targetPosition } ( ${ this.howMany } ), ${ this.graveyardPosition }`;
-	} );
-
-	sandbox.mock( SplitOperation.prototype, 'toString', function() {
-		return getClassName( this ) + `( ${ this.baseVersion } ): ${ this.splitPosition } ` +
-			`( ${ this.howMany } ) -> ${ this.insertionPosition }${ this.graveyardPosition ? ' with ' + this.graveyardPosition : '' }`;
-	} );
-
-	sandbox.mock( ViewText.prototype, 'toString', function() {
-		return `#${ this.data }`;
-	} );
-
-	sandbox.mock( ViewText.prototype, 'logExtended', function() {
-		logger.log( 'ViewText: ' + this );
-	} );
-
-	sandbox.mock( ViewText.prototype, 'log', function() {
-		logger.log( 'ViewText: ' + this );
-	} );
-
-	sandbox.mock( ViewTextProxy.prototype, 'toString', function() {
-		return `#${ this.data }`;
-	} );
-
-	sandbox.mock( ViewTextProxy.prototype, 'logExtended', function() {
-		logger.log( 'ViewTextProxy: ' + this );
-	} );
-
-	sandbox.mock( ViewTextProxy.prototype, 'log', function() {
-		logger.log( 'ViewTextProxy: ' + this );
-	} );
-
-	sandbox.mock( ViewElement.prototype, 'printTree', function( level = 0 ) {
-		let string = '';
-
-		string += '\t'.repeat( level ) + `<${ this.name }${ mapToTags( this.getAttributes() ) }>`;
-
-		for ( const child of this.getChildren() ) {
-			if ( child.is( 'text' ) ) {
-				string += '\n' + '\t'.repeat( level + 1 ) + child.data;
-			} else {
-				string += '\n' + child.printTree( level + 1 );
-			}
-		}
-
-		if ( this.childCount ) {
-			string += '\n' + '\t'.repeat( level );
-		}
-
-		string += `</${ this.name }>`;
-
-		return string;
-	} );
-
-	sandbox.mock( ViewElement.prototype, 'logTree', function() {
-		logger.log( this.printTree() );
-	} );
-
-	sandbox.mock( ViewDocumentFragment.prototype, 'printTree', function() {
-		let string = 'ViewDocumentFragment: [';
-
-		for ( const child of this.getChildren() ) {
-			if ( child.is( 'text' ) ) {
-				string += '\n' + '\t'.repeat( 1 ) + child.data;
-			} else {
-				string += '\n' + child.printTree( 1 );
-			}
-		}
-
-		string += '\n]';
-
-		return string;
-	} );
-
-	sandbox.mock( ViewDocumentFragment.prototype, 'logTree', function() {
-		logger.log( this.printTree() );
-	} );
-}
-
-function enableReplayerTools() {
-	const _modelApplyOperation = Model.prototype.applyOperation;
-
-	sandbox.mock( Model.prototype, 'applyOperation', function( operation ) {
-		if ( !this._appliedOperations ) {
-			this._appliedOperations = [];
-		}
-
-		this._appliedOperations.push( operation );
-
-		return _modelApplyOperation.call( this, operation );
-	} );
-
-	sandbox.mock( Model.prototype, 'getAppliedOperations', function() {
-		if ( !this._appliedOperations ) {
-			return '';
-		}
-
-		return this._appliedOperations.map( JSON.stringify ).join( LOG_SEPARATOR );
-	} );
-
-	sandbox.mock( Model.prototype, 'createReplayer', function( stringifiedOperations ) {
-		return new OperationReplayer( this, LOG_SEPARATOR, stringifiedOperations );
-	} );
-}
-
-function enableDocumentTools() {
-	const _modelApplyOperation = Model.prototype.applyOperation;
-
-	sandbox.mock( Model.prototype, 'applyOperation', function( operation ) {
-		logger.log( 'Applying ' + operation );
-
-		if ( !this._operationLogs ) {
-			this._operationLogs = [];
-		}
-
-		this._operationLogs.push( JSON.stringify( operation ) );
-
-		return _modelApplyOperation.call( this, operation );
-	} );
-
-	sandbox.mock( ModelDocument.prototype, 'log', function( version = null ) {
-		version = version === null ? this.version : version;
-
-		logDocument( this, version );
-	} );
-
-	sandbox.mock( ViewDocument.prototype, 'log', function( version ) {
-		logDocument( this, version );
-	} );
-
-	sandbox.mock( Editor.prototype, 'logModel', function( version = null ) {
-		version = version === null ? this.model.document.version : version;
-
-		this.model.document.log( version );
-	} );
-
-	sandbox.mock( Editor.prototype, 'logView', function( version ) {
-		this.editing.view.document.log( version );
-	} );
-
-	sandbox.mock( Editor.prototype, 'logDocuments', function( version = null ) {
-		version = version === null ? this.model.document.version : version;
-
-		this.logModel( version );
-		this.logView( version );
-	} );
-
-	function logDocument( document, version ) {
-		logger.log( '--------------------' );
-
-		if ( document[ treeDump ][ version ] ) {
-			logger.log( document[ treeDump ][ version ] );
-		} else {
-			logger.log( 'Tree log unavailable for given version: ' + version );
-		}
-	}
-}
-
-/**
- * Plugin that enables debugging features on the editor's model and view documents.
- */
-class DebugPlugin extends Plugin {
-	constructor( editor ) {
-		super( editor );
-
-		const model = this.editor.model;
-		const modelDocument = model.document;
-		const view = this.editor.editing.view;
-		const viewDocument = view.document;
-
-		modelDocument[ treeDump ] = [];
-		viewDocument[ treeDump ] = [];
-
-		dumpTrees( modelDocument, modelDocument.version );
-		dumpTrees( viewDocument, modelDocument.version );
-
-		model.on( 'applyOperation', () => {
-			dumpTrees( modelDocument, modelDocument.version );
-		}, { priority: 'lowest' } );
-
-		model.document.on( 'change', () => {
-			dumpTrees( viewDocument, modelDocument.version );
-		}, { priority: 'lowest' } );
-	}
-}
-
-// Helper function, stores the `document` state for a given `version` as a string in a private property.
-function dumpTrees( document, version ) {
-	let string = '';
-
-	for ( const root of document.roots ) {
-		string += root.printTree() + '\n';
-	}
-
-	document[ treeDump ][ version ] = string.substr( 0, string.length - 1 ); // Remove the last "\n".
-
-	const overflow = document[ treeDump ].length - maxTreeDumpLength;
-
-	if ( overflow > 0 ) {
-		document[ treeDump ][ overflow - 1 ] = null;
-	}
-}
-
-// Helper function, returns the class name of a given `Operation`.
-// @param {module:engine/model/operation/operation~Operation}
-// @returns {String} Class name.
-function getClassName( obj ) {
-	return obj.constructor.className;
-}
-
-// Helper function, converts a map to the {"key1":"value1","key2":"value2"} format.
-// @param {Map} map Map to convert.
-// @returns {String} Converted map.
-function mapString( map ) {
-	const obj = {};
-
-	for ( const entry of map ) {
-		obj[ entry[ 0 ] ] = entry[ 1 ];
-	}
-
-	return JSON.stringify( obj );
-}
-
-// Helper function, converts a map to the key1="value1" key2="value1" format.
-// @param {Map} map Map to convert.
-// @returns {String} Converted map.
-function mapToTags( map ) {
-	let string = '';
-
-	for ( const entry of map ) {
-		string += ` ${ entry[ 0 ] }=${ JSON.stringify( entry[ 1 ] ) }`;
-	}
-
-	return string;
-}

+ 103 - 0
packages/ckeditor5-engine/src/dev-utils/utils.js

@@ -0,0 +1,103 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * Note: This package is used only internally for debugging purposes and should not be used
+ * in other environments. It uses a few special methods not existing in the default
+ * building process. That is also why there are no tests for this file.
+ *
+ * @module engine/dev-utils/utils
+ */
+
+/* globals console */
+
+/**
+ * Helper function, converts a map to the 'key1="value1" key2="value1"' format.
+ *
+ * @private
+ * @param {Map} map Map to convert.
+ * @returns {String} Converted map.
+ */
+export function convertMapToTags( map ) {
+	let string = '';
+
+	for ( const entry of map ) {
+		string += ` ${ entry[ 0 ] }=${ JSON.stringify( entry[ 1 ] ) }`;
+	}
+
+	return string;
+}
+
+/**
+ * Helper function, converts a map to the '{"key1":"value1","key2":"value2"}' format.
+ *
+ * @private
+ * @param {Map} map Map to convert.
+ * @returns {String} Converted map.
+ */
+export function convertMapToStringifiedObject( map ) {
+	const obj = {};
+
+	for ( const entry of map ) {
+		obj[ entry[ 0 ] ] = entry[ 1 ];
+	}
+
+	return JSON.stringify( obj );
+}
+
+const treeDump = Symbol( '_treeDump' );
+const maxTreeDumpLength = 20;
+
+/**
+ * Helper function that stores the `document` state for a given `version`.
+ *
+ * @private
+ * @param {*} document
+ * @param {*} version
+ */
+export function dumpTrees( document, version ) {
+	console.log( document, version );
+
+	let string = '';
+
+	for ( const root of document.roots ) {
+		string += root.printTree() + '\n';
+	}
+
+	document[ treeDump ][ version ] = string.substr( 0, string.length - 1 ); // Remove the last "\n".
+
+	const overflow = document[ treeDump ].length - maxTreeDumpLength;
+
+	if ( overflow > 0 ) {
+		document[ treeDump ][ overflow - 1 ] = null;
+	}
+}
+
+/**
+ * Helper function that initializes document dumping.
+ *
+ * @private
+ * @param {*} document
+ */
+export function initDocumentDumping( document ) {
+	document[ treeDump ] = [];
+}
+
+/**
+ * Helper function that logs document for the given version.
+ *
+ * @private
+ * @param {*} document
+ * @param {*} version
+ */
+export function logDocument( document, version ) {
+	console.log( '--------------------' );
+
+	if ( document[ treeDump ][ version ] ) {
+		console.log( document[ treeDump ][ version ] );
+	} else {
+		console.log( 'Tree log unavailable for given version: ' + version );
+	}
+}

+ 7 - 0
packages/ckeditor5-engine/src/model/document.js

@@ -18,6 +18,8 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import { isInsideSurrogatePair, isInsideCombinedSymbol } from '@ckeditor/ckeditor5-utils/src/unicode';
 import { clone } from 'lodash-es';
 
+// @if CK_DEBUG_ENGINE // const { logDocument } = require( '../dev-utils/utils' );
+
 const graveyardName = '$graveyard';
 
 /**
@@ -465,6 +467,11 @@ export default class Document {
 	 * @event change:data
 	 * @param {module:engine/model/batch~Batch} batch The batch that was used in the executed changes block.
 	 */
+
+	// @if CK_DEBUG_ENGINE // log( version = null ) {
+	// @if CK_DEBUG_ENGINE // 	version = version === null ? this.version : version;
+	// @if CK_DEBUG_ENGINE // 	logDocument( this, version );
+	// @if CK_DEBUG_ENGINE // }
 }
 
 mix( Document, EmitterMixin );

+ 40 - 0
packages/ckeditor5-engine/src/model/documentfragment.js

@@ -13,6 +13,8 @@ import Text from './text';
 import TextProxy from './textproxy';
 import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
 
+// @if CK_DEBUG_ENGINE // const { stringifyMap } = require( '../dev-utils/utils' );
+
 /**
  * DocumentFragment represents a part of model which does not have a common root but it's top-level nodes
  * can be seen as siblings. In other words, it is a detached part of model tree, without a root.
@@ -318,6 +320,44 @@ export default class DocumentFragment {
 
 		return nodes;
 	}
+
+	// @if CK_DEBUG_ENGINE // toString() {
+	// @if CK_DEBUG_ENGINE // 	return 'documentFragment';
+	// @if CK_DEBUG_ENGINE // }
+
+	// @if CK_DEBUG_ENGINE // log() {
+	// @if CK_DEBUG_ENGINE // 	console.log( 'ModelDocumentFragment: ' + this );
+	// @if CK_DEBUG_ENGINE // }
+
+	// @if CK_DEBUG_ENGINE // printTree() {
+	// @if CK_DEBUG_ENGINE //	let string = 'ModelDocumentFragment: [';
+
+	// @if CK_DEBUG_ENGINE //	for ( const child of this.getChildren() ) {
+	// @if CK_DEBUG_ENGINE //		string += '\n';
+
+	// @if CK_DEBUG_ENGINE //		if ( child.is( 'text' ) ) {
+	// @if CK_DEBUG_ENGINE //			const textAttrs = stringifyMap( child._attrs );
+
+	// @if CK_DEBUG_ENGINE //			string += '\t'.repeat( 1 );
+
+	// @if CK_DEBUG_ENGINE //			if ( textAttrs !== '' ) {
+	// @if CK_DEBUG_ENGINE //				string += `<$text${ textAttrs }>` + child.data + '</$text>';
+	// @if CK_DEBUG_ENGINE //			} else {
+	// @if CK_DEBUG_ENGINE //				string += child.data;
+	// @if CK_DEBUG_ENGINE //			}
+	// @if CK_DEBUG_ENGINE //		} else {
+	// @if CK_DEBUG_ENGINE //			string += child.printTree( 1 );
+	// @if CK_DEBUG_ENGINE //		}
+	// @if CK_DEBUG_ENGINE //	}
+
+	// @if CK_DEBUG_ENGINE //	string += '\n]';
+
+	// @if CK_DEBUG_ENGINE //	return string;
+	// @if CK_DEBUG_ENGINE // }
+
+	// @if CK_DEBUG_ENGINE // logTree() {
+	// @if CK_DEBUG_ENGINE // 	console.log( this.printTree() );
+	// @if CK_DEBUG_ENGINE // }
 }
 
 // Converts strings to Text and non-iterables to arrays.

+ 63 - 0
packages/ckeditor5-engine/src/model/element.js

@@ -13,6 +13,8 @@ import Text from './text';
 import TextProxy from './textproxy';
 import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
 
+// @if CK_DEBUG_ENGINE // const { stringifyMap, convertMapToStringifiedObject, convertMapToTags } = require( '../dev-utils/utils' );
+
 /**
  * Model element. Type of {@link module:engine/model/node~Node node} that has a {@link module:engine/model/element~Element#name name} and
  * {@link module:engine/model/element~Element#getChildren child nodes}.
@@ -322,6 +324,67 @@ export default class Element extends Node {
 
 		return new Element( json.name, json.attributes, children );
 	}
+
+	// @if CK_DEBUG_ENGINE // toString() {
+	// @if CK_DEBUG_ENGINE // 	return `<${ this.rootName || this.name }>`;
+	// @if CK_DEBUG_ENGINE // }
+
+	// @if CK_DEBUG_ENGINE // log() {
+	// @if CK_DEBUG_ENGINE // 	console.log( 'ModelElement: ' + this );
+	// @if CK_DEBUG_ENGINE // }
+
+	// @if CK_DEBUG_ENGINE // logExtended() {
+	// @if CK_DEBUG_ENGINE // 	console.log( `ModelElement: ${ this }, ${ this.childCount } children,
+	// @if CK_DEBUG_ENGINE //		attrs: ${ convertMapToStringifiedObject( this.getAttributes() ) }` );
+	// @if CK_DEBUG_ENGINE // }
+
+	// @if CK_DEBUG_ENGINE // logAll() {
+	// @if CK_DEBUG_ENGINE // 	console.log( '--------------------' );
+	// @if CK_DEBUG_ENGINE //
+	// @if CK_DEBUG_ENGINE // 	this.logExtended();
+	// @if CK_DEBUG_ENGINE //	console.log( 'List of children:' );
+	// @if CK_DEBUG_ENGINE //
+	// @if CK_DEBUG_ENGINE // 	for ( const child of this.getChildren() ) {
+	// @if CK_DEBUG_ENGINE // 		child.log();
+	// @if CK_DEBUG_ENGINE // 	}
+	// @if CK_DEBUG_ENGINE // }
+
+	// @if CK_DEBUG_ENGINE // printTree( level = 0) {
+	// @if CK_DEBUG_ENGINE // 	let string = '';
+
+	// @if CK_DEBUG_ENGINE // 	string += '\t'.repeat( level );
+	// @if CK_DEBUG_ENGINE // 	string += `<${ this.rootName || this.name }${ convertMapToTags( this.getAttributes() ) }>`;
+
+	// @if CK_DEBUG_ENGINE // 	for ( const child of this.getChildren() ) {
+	// @if CK_DEBUG_ENGINE // 		string += '\n';
+
+	// @if CK_DEBUG_ENGINE // 		if ( child.is( 'text' ) ) {
+	// @if CK_DEBUG_ENGINE // 			const textAttrs = convertMapToTags( child._attrs );
+
+	// @if CK_DEBUG_ENGINE // 			string += '\t'.repeat( level + 1 );
+
+	// @if CK_DEBUG_ENGINE // 			if ( textAttrs !== '' ) {
+	// @if CK_DEBUG_ENGINE // 				string += `<$text${ textAttrs }>` + child.data + '</$text>';
+	// @if CK_DEBUG_ENGINE // 			} else {
+	// @if CK_DEBUG_ENGINE // 				string += child.data;
+	// @if CK_DEBUG_ENGINE // 			}
+	// @if CK_DEBUG_ENGINE // 		} else {
+	// @if CK_DEBUG_ENGINE // 			string += child.printTree( level + 1 );
+	// @if CK_DEBUG_ENGINE // 		}
+	// @if CK_DEBUG_ENGINE // 	}
+
+	// @if CK_DEBUG_ENGINE // 	if ( this.childCount ) {
+	// @if CK_DEBUG_ENGINE // 		string += '\n' + '\t'.repeat( level );
+	// @if CK_DEBUG_ENGINE // 	}
+
+	// @if CK_DEBUG_ENGINE // 	string += `</${ this.rootName || this.name }>`;
+
+	// @if CK_DEBUG_ENGINE // 	return string;
+	// @if CK_DEBUG_ENGINE // }
+
+	// @if CK_DEBUG_ENGINE // logTree() {
+	// @if CK_DEBUG_ENGINE // 	console.log( this.printTree() );
+	// @if CK_DEBUG_ENGINE // }
 }
 
 // Converts strings to Text and non-iterables to arrays.

+ 34 - 1
packages/ckeditor5-engine/src/model/model.js

@@ -26,6 +26,9 @@ import getSelectedContent from './utils/getselectedcontent';
 import { injectSelectionPostFixer } from './utils/selection-post-fixer';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
+// @if CK_DEBUG_ENGINE // const { dumpTrees } = require( '../dev-utils/utils' );
+// @if CK_DEBUG_ENGINE // const { OperationReplayer } = require( '../dev-utils/operationreplayer' ).default;
+
 /**
  * Editor's data model. Read about the model in the
  * {@glink framework/guides/architecture/editing-engine engine architecture guide}.
@@ -116,6 +119,10 @@ export default class Model {
 		} );
 
 		injectSelectionPostFixer( this );
+
+		// @if CK_DEBUG_ENGINE // this.on( 'applyOperation', () => {
+		// @if CK_DEBUG_ENGINE // 	dumpTrees( this.document, this.document.version );
+		// @if CK_DEBUG_ENGINE // }, { priority: 'lowest' } );
 	}
 
 	/**
@@ -233,9 +240,35 @@ export default class Model {
 	 * @param {module:engine/model/operation/operation~Operation} operation The operation to apply.
 	 */
 	applyOperation( operation ) {
+		// @if CK_DEBUG_ENGINE // console.log( 'Applying ' + operation );
+
+		// @if CK_DEBUG_ENGINE // if ( !this._operationLogs ) {
+		// @if CK_DEBUG_ENGINE //	this._operationLogs = [];
+		// @if CK_DEBUG_ENGINE // }
+
+		// @if CK_DEBUG_ENGINE // this._operationLogs.push( JSON.stringify( operation ) );
+
+		// @if CK_DEBUG_ENGINE //if ( !this._appliedOperations ) {
+		// @if CK_DEBUG_ENGINE //	this._appliedOperations = [];
+		// @if CK_DEBUG_ENGINE //}
+
+		// @if CK_DEBUG_ENGINE //this._appliedOperations.push( operation );
+
 		operation._execute();
 	}
 
+	// @if CK_DEBUG_ENGINE // getAppliedOperation() {
+	// @if CK_DEBUG_ENGINE //	if ( !this._appliedOperations ) {
+	// @if CK_DEBUG_ENGINE //		return '';
+	// @if CK_DEBUG_ENGINE //	}
+
+	// @if CK_DEBUG_ENGINE //	return this._appliedOperations.map( JSON.stringify ).join( '-------' );
+	// @if CK_DEBUG_ENGINE // }
+
+	// @if CK_DEBUG_ENGINE // createReplayer( stringifiedOperations ) {
+	// @if CK_DEBUG_ENGINE //	return new OperationReplayer( this, '-------', stringifiedOperations );
+	// @if CK_DEBUG_ENGINE // }
+
 	/**
 	 * Inserts content at the position in the editor specified by the selection, as one would expect the paste
 	 * functionality to work.
@@ -269,7 +302,7 @@ export default class Model {
 	 *
 	 *		// Let's create a document fragment containing such content as:
 	 *		//
-	 *		// <paragrap>foo</paragraph>
+	 *		// <paragraph>foo</paragraph>
 	 *		// <blockQuote>
 	 *		//    <paragraph>bar</paragraph>
 	 *		// </blockQuote>

+ 6 - 0
packages/ckeditor5-engine/src/model/operation/attributeoperation.js

@@ -194,4 +194,10 @@ export default class AttributeOperation extends Operation {
 	static fromJSON( json, document ) {
 		return new AttributeOperation( Range.fromJSON( json.range, document ), json.key, json.oldValue, json.newValue, json.baseVersion );
 	}
+
+	// @if CK_DEBUG_ENGINE // toString() {
+	// @if CK_DEBUG_ENGINE // 	return `AttributeOperation( ${ this.baseVersion } ): ` +
+	// @if CK_DEBUG_ENGINE //		`"${ this.key }": ${ JSON.stringify( this.oldValue ) }` +
+	// @if CK_DEBUG_ENGINE //		` -> ${ JSON.stringify( this.newValue ) }, ${ this.range }`;
+	// @if CK_DEBUG_ENGINE // }
 }

+ 10 - 0
packages/ckeditor5-engine/src/model/operation/detachoperation.js

@@ -12,6 +12,8 @@ import Range from '../range';
 import { _remove } from './utils';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
+// @if CK_DEBUG_ENGINE // const ModelRange = require( '../range' ).default;
+
 /**
  * Operation to permanently remove node from detached root.
  * Note this operation is only a local operation and won't be send to the other clients.
@@ -90,4 +92,12 @@ export default class DetachOperation extends Operation {
 	static get className() {
 		return 'DetachOperation';
 	}
+
+	// @if CK_DEBUG_ENGINE // toString() {
+	// @if CK_DEBUG_ENGINE // const range = ModelRange._createFromPositionAndShift( this.sourcePosition, this.howMany );
+	// @if CK_DEBUG_ENGINE //	const nodes = Array.from( range.getItems() );
+	// @if CK_DEBUG_ENGINE //	const nodeString = nodes.length > 1 ? `[ ${ nodes.length } ]` : nodes[ 0 ];
+
+	// @if CK_DEBUG_ENGINE //	return `DetachOperation( ${ this.baseVersion } ): ${ nodeString } -> ${ range }`;
+	// @if CK_DEBUG_ENGINE // }
 }

+ 6 - 0
packages/ckeditor5-engine/src/model/operation/insertoperation.js

@@ -179,4 +179,10 @@ export default class InsertOperation extends Operation {
 
 		return insert;
 	}
+
+	// @if CK_DEBUG_ENGINE // toString() {
+	// @if CK_DEBUG_ENGINE // 	const nodeString = this.nodes.length > 1 ? `[ ${ this.nodes.length } ]` : this.nodes.getNode( 0 );
+
+	// @if CK_DEBUG_ENGINE //	return `InsertOperation( ${ this.baseVersion } ): ${ nodeString } -> ${ this.position }`;
+	// @if CK_DEBUG_ENGINE // }
 }

+ 5 - 0
packages/ckeditor5-engine/src/model/operation/markeroperation.js

@@ -146,4 +146,9 @@ export default class MarkerOperation extends Operation {
 			json.baseVersion
 		);
 	}
+
+	// @if CK_DEBUG_ENGINE // toString() {
+	// @if CK_DEBUG_ENGINE // 	return `MarkerOperation( ${ this.baseVersion } ): ` +
+	// @if CK_DEBUG_ENGINE //		`"${ this.name }": ${ this.oldRange } -> ${ this.newRange }`;
+	// @if CK_DEBUG_ENGINE // }
 }

+ 6 - 0
packages/ckeditor5-engine/src/model/operation/mergeoperation.js

@@ -210,4 +210,10 @@ export default class MergeOperation extends Operation {
 
 		return new this( sourcePosition, json.howMany, targetPosition, graveyardPosition, json.baseVersion );
 	}
+
+	// @if CK_DEBUG_ENGINE // toString() {
+	// @if CK_DEBUG_ENGINE // 	return `MergeOperation( ${ this.baseVersion } ): ` +
+	// @if CK_DEBUG_ENGINE //		`${ this.sourcePosition } -> ${ this.targetPosition }` +
+	// @if CK_DEBUG_ENGINE //		` ( ${ this.howMany } ), ${ this.graveyardPosition }`;
+	// @if CK_DEBUG_ENGINE // }
 }

+ 8 - 0
packages/ckeditor5-engine/src/model/operation/moveoperation.js

@@ -14,6 +14,8 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import compareArrays from '@ckeditor/ckeditor5-utils/src/comparearrays';
 import { _move } from './utils';
 
+// @if CK_DEBUG_ENGINE // const ModelRange = require( '../range' ).default;
+
 /**
  * Operation to move a range of {@link module:engine/model/item~Item model items}
  * to given {@link module:engine/model/position~Position target position}.
@@ -198,4 +200,10 @@ export default class MoveOperation extends Operation {
 
 		return new this( sourcePosition, json.howMany, targetPosition, json.baseVersion );
 	}
+
+	// @if CK_DEBUG_ENGINE // toString() {
+	// @if CK_DEBUG_ENGINE // 	const range = ModelRange._createFromPositionAndShift( this.sourcePosition, this.howMany );
+
+	// @if CK_DEBUG_ENGINE //	return `MoveOperation( ${ this.baseVersion } ): ${ range } -> ${ this.targetPosition }`;
+	// @if CK_DEBUG_ENGINE // }
 }

+ 4 - 0
packages/ckeditor5-engine/src/model/operation/nooperation.js

@@ -51,4 +51,8 @@ export default class NoOperation extends Operation {
 	static get className() {
 		return 'NoOperation';
 	}
+
+	// @if CK_DEBUG_ENGINE // toString() {
+	// @if CK_DEBUG_ENGINE // 	return `NoOperation( ${ this.baseVersion } )`;
+	// @if CK_DEBUG_ENGINE // }
 }

+ 4 - 0
packages/ckeditor5-engine/src/model/operation/operation.js

@@ -132,4 +132,8 @@ export default class Operation {
 	static fromJSON( json ) {
 		return new this( json.baseVersion );
 	}
+
+	// @if CK_DEBUG_ENGINE // log() {
+	// @if CK_DEBUG_ENGINE // 	console.log( this.toString() );
+	// @if CK_DEBUG_ENGINE // }
 }

+ 5 - 0
packages/ckeditor5-engine/src/model/operation/renameoperation.js

@@ -147,4 +147,9 @@ export default class RenameOperation extends Operation {
 	static fromJSON( json, document ) {
 		return new RenameOperation( Position.fromJSON( json.position, document ), json.oldName, json.newName, json.baseVersion );
 	}
+
+	// @if CK_DEBUG_ENGINE // toString() {
+	// @if CK_DEBUG_ENGINE // 	return `RenameOperation( ${ this.baseVersion } ): ` +
+	// @if CK_DEBUG_ENGINE //		`${ this.position }: "${ this.oldName }" -> "${ this.newName }"`;
+	// @if CK_DEBUG_ENGINE // }
 }

+ 6 - 0
packages/ckeditor5-engine/src/model/operation/rootattributeoperation.js

@@ -207,4 +207,10 @@ export default class RootAttributeOperation extends Operation {
 
 		return new RootAttributeOperation( document.getRoot( json.root ), json.key, json.oldValue, json.newValue, json.baseVersion );
 	}
+
+	// @if CK_DEBUG_ENGINE // toString() {
+	// @if CK_DEBUG_ENGINE // 	return `RootAttributeOperation( ${ this.baseVersion } ): ` +
+	// @if CK_DEBUG_ENGINE //		`"${ this.key }": ${ JSON.stringify( this.oldValue ) }` +
+	// @if CK_DEBUG_ENGINE //		` -> ${ JSON.stringify( this.newValue ) }, ${ this.root.rootName }`;
+	// @if CK_DEBUG_ENGINE // }
 }

+ 6 - 0
packages/ckeditor5-engine/src/model/operation/splitoperation.js

@@ -250,4 +250,10 @@ export default class SplitOperation extends Operation {
 
 		return split;
 	}
+
+	// @if CK_DEBUG_ENGINE // toString() {
+	// @if CK_DEBUG_ENGINE // 	return `SplitOperation( ${ this.baseVersion } ): ${ this.splitPosition } ` +
+	// @if CK_DEBUG_ENGINE //		`( ${ this.howMany } ) -> ${ this.insertionPosition }` +
+	// @if CK_DEBUG_ENGINE //		`${ this.graveyardPosition ? ' with ' + this.graveyardPosition : '' }`;
+	// @if CK_DEBUG_ENGINE // }
 }

+ 8 - 0
packages/ckeditor5-engine/src/model/position.js

@@ -1014,6 +1014,14 @@ export default class Position {
 
 		return new Position( doc.getRoot( json.root ), json.path, json.stickiness );
 	}
+
+	// @if CK_DEBUG_ENGINE // toString() {
+	// @if CK_DEBUG_ENGINE // 	return `${ this.root } [ ${ this.path.join( ', ' ) } ]`;
+	// @if CK_DEBUG_ENGINE // }
+
+	// @if CK_DEBUG_ENGINE // log() {
+	// @if CK_DEBUG_ENGINE // 	console.log( 'ModelPosition: ' + this );
+	// @if CK_DEBUG_ENGINE // }
 }
 
 /**

+ 8 - 0
packages/ckeditor5-engine/src/model/range.js

@@ -958,4 +958,12 @@ export default class Range {
 	static fromJSON( json, doc ) {
 		return new this( Position.fromJSON( json.start, doc ), Position.fromJSON( json.end, doc ) );
 	}
+
+	// @if CK_DEBUG_ENGINE // toString() {
+	// @if CK_DEBUG_ENGINE // 	return `${ this.root } [ ${ this.start.path.join( ', ' ) } ] - [ ${ this.end.path.join( ', ' ) } ]`;
+	// @if CK_DEBUG_ENGINE // }
+
+	// @if CK_DEBUG_ENGINE // log() {
+	// @if CK_DEBUG_ENGINE // 	console.log( 'ModelPosition: ' + this );
+	// @if CK_DEBUG_ENGINE // }
 }

+ 8 - 0
packages/ckeditor5-engine/src/model/rootelement.js

@@ -98,4 +98,12 @@ export default class RootElement extends Element {
 	toJSON() {
 		return this.rootName;
 	}
+
+	// @if CK_DEBUG_ENGINE // toString() {
+	// @if CK_DEBUG_ENGINE // 	return this.rootName;
+	// @if CK_DEBUG_ENGINE // }
+
+	// @if CK_DEBUG_ENGINE // log() {
+	// @if CK_DEBUG_ENGINE // 	console.log( 'ModelRootElement: ' + this );
+	// @if CK_DEBUG_ENGINE // }
 }

+ 14 - 0
packages/ckeditor5-engine/src/model/text.js

@@ -9,6 +9,8 @@
 
 import Node from './node';
 
+// @if CK_DEBUG_ENGINE // const { convertMapToStringifiedObject } = require( '../dev-utils/utils' );
+
 /**
  * Model text node. Type of {@link module:engine/model/node~Node node} that contains {@link module:engine/model/text~Text#data text data}.
  *
@@ -115,4 +117,16 @@ export default class Text extends Node {
 	static fromJSON( json ) {
 		return new Text( json.data, json.attributes );
 	}
+
+	// @if CK_DEBUG_ENGINE // toString() {
+	// @if CK_DEBUG_ENGINE // 	return `#${ this.data }`;
+	// @if CK_DEBUG_ENGINE // }
+
+	// @if CK_DEBUG_ENGINE // logExtended() {
+	// @if CK_DEBUG_ENGINE // 	console.log( `ModelText: ${ this }, attrs: ${ convertMapToStringifiedObject( this.getAttributes() ) }` );
+	// @if CK_DEBUG_ENGINE // }
+
+	// @if CK_DEBUG_ENGINE // log() {
+	// @if CK_DEBUG_ENGINE // 	console.log( 'ModelText: ' + this );
+	// @if CK_DEBUG_ENGINE // }
 }

+ 15 - 0
packages/ckeditor5-engine/src/model/textproxy.js

@@ -9,6 +9,8 @@
 
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
+// @if CK_DEBUG_ENGINE // const { convertMapToStringifiedObject } = require( '../dev-utils/utils' );
+
 /**
  * `TextProxy` represents a part of {@link module:engine/model/text~Text text node}.
  *
@@ -267,4 +269,17 @@ export default class TextProxy {
 	getAttributeKeys() {
 		return this.textNode.getAttributeKeys();
 	}
+
+	// @if CK_DEBUG_ENGINE // toString() {
+	// @if CK_DEBUG_ENGINE // 	return `#${ this.data }`;
+	// @if CK_DEBUG_ENGINE // }
+
+	// @if CK_DEBUG_ENGINE // log() {
+	// @if CK_DEBUG_ENGINE // 	console.log( 'ModelTextProxy: ' + this );
+	// @if CK_DEBUG_ENGINE // }
+
+	// @if CK_DEBUG_ENGINE // logExtended() {
+	// @if CK_DEBUG_ENGINE // 	console.log( `ModelTextProxy: ${ this }, ` +
+	// @if CK_DEBUG_ENGINE // 		`attrs: ${ convertMapToStringifiedObject( this.getAttributes() ) }` );
+	// @if CK_DEBUG_ENGINE // }
 }

+ 6 - 0
packages/ckeditor5-engine/src/view/document.js

@@ -12,6 +12,8 @@ import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
 
+// @if CK_DEBUG_ENGINE // const { logDocument } = require( '../dev-utils/utils' );
+
 /**
  * Document class creates an abstract layer over the content editable area, contains a tree of view elements and
  * {@link module:engine/view/documentselection~DocumentSelection view selection} associated with this document.
@@ -185,6 +187,10 @@ export default class Document {
 	 *
 	 * @event layoutChanged
 	 */
+
+	// @if CK_DEBUG_ENGINE // log( version ) {
+	// @if CK_DEBUG_ENGINE //	logDocument( this, version );
+	// @if CK_DEBUG_ENGINE // }
 }
 
 mix( Document, ObservableMixin );

+ 20 - 0
packages/ckeditor5-engine/src/view/documentfragment.js

@@ -210,6 +210,26 @@ export default class DocumentFragment {
 	_fireChange( type, node ) {
 		this.fire( 'change:' + type, node );
 	}
+
+	// @if CK_DEBUG_ENGINE // printTree() {
+	// @if CK_DEBUG_ENGINE //	let string = 'ViewDocumentFragment: [';
+
+	// @if CK_DEBUG_ENGINE //	for ( const child of this.getChildren() ) {
+	// @if CK_DEBUG_ENGINE //		if ( child.is( 'text' ) ) {
+	// @if CK_DEBUG_ENGINE //			string += '\n' + '\t'.repeat( 1 ) + child.data;
+	// @if CK_DEBUG_ENGINE //		} else {
+	// @if CK_DEBUG_ENGINE //			string += '\n' + child.printTree( 1 );
+	// @if CK_DEBUG_ENGINE //		}
+	// @if CK_DEBUG_ENGINE //	}
+
+	// @if CK_DEBUG_ENGINE //	string += '\n]';
+
+	// @if CK_DEBUG_ENGINE //	return string;
+	// @if CK_DEBUG_ENGINE // }
+
+	// @if CK_DEBUG_ENGINE // logTree() {
+	// @if CK_DEBUG_ENGINE // 	console.log( this.printTree() );
+	// @if CK_DEBUG_ENGINE // }
 }
 
 mix( DocumentFragment, EmitterMixin );

+ 28 - 0
packages/ckeditor5-engine/src/view/element.js

@@ -15,6 +15,8 @@ import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
 import Matcher from './matcher';
 import { isPlainObject } from 'lodash-es';
 
+// @if CK_DEBUG_ENGINE // const { convertMapToTags } = require( '../dev-utils/utils' );
+
 /**
  * View element.
  *
@@ -772,6 +774,32 @@ export default class Element extends Node {
 	 * @abstract
 	 * @method module:engine/view/element~Element#getFillerOffset
 	 */
+
+	// @if CK_DEBUG_ENGINE // printTree( level = 0) {
+	// @if CK_DEBUG_ENGINE // 	let string = '';
+
+	// @if CK_DEBUG_ENGINE //	string += '\t'.repeat( level ) + `<${ this.name }${ convertMapToTags( this.getAttributes() ) }>`;
+
+	// @if CK_DEBUG_ENGINE //	for ( const child of this.getChildren() ) {
+	// @if CK_DEBUG_ENGINE //		if ( child.is( 'text' ) ) {
+	// @if CK_DEBUG_ENGINE //			string += '\n' + '\t'.repeat( level + 1 ) + child.data;
+	// @if CK_DEBUG_ENGINE //		} else {
+	// @if CK_DEBUG_ENGINE //			string += '\n' + child.printTree( level + 1 );
+	// @if CK_DEBUG_ENGINE //		}
+	// @if CK_DEBUG_ENGINE //	}
+
+	// @if CK_DEBUG_ENGINE //	if ( this.childCount ) {
+	// @if CK_DEBUG_ENGINE //		string += '\n' + '\t'.repeat( level );
+	// @if CK_DEBUG_ENGINE //	}
+
+	// @if CK_DEBUG_ENGINE //	string += `</${ this.name }>`;
+
+	// @if CK_DEBUG_ENGINE //	return string;
+	// @if CK_DEBUG_ENGINE // }
+
+	// @if CK_DEBUG_ENGINE // logTree() {
+	// @if CK_DEBUG_ENGINE // 	console.log( this.printTree() );
+	// @if CK_DEBUG_ENGINE // }
 }
 
 // Parses attributes provided to the element constructor before they are applied to an element. If attributes are passed

+ 12 - 0
packages/ckeditor5-engine/src/view/text.js

@@ -127,4 +127,16 @@ export default class Text extends Node {
 	_clone() {
 		return new Text( this.data );
 	}
+
+	// @if CK_DEBUG_ENGINE // toString() {
+	// @if CK_DEBUG_ENGINE // 	return `#${ this.data }`;
+	// @if CK_DEBUG_ENGINE // }
+
+	// @if CK_DEBUG_ENGINE // log() {
+	// @if CK_DEBUG_ENGINE // 	console.log( 'ViewText: ' + this );
+	// @if CK_DEBUG_ENGINE // }
+
+	// @if CK_DEBUG_ENGINE // logExtended() {
+	// @if CK_DEBUG_ENGINE // 	console.log( 'ViewText: ' + this );
+	// @if CK_DEBUG_ENGINE // }
 }

+ 12 - 0
packages/ckeditor5-engine/src/view/textproxy.js

@@ -179,4 +179,16 @@ export default class TextProxy {
 
 		return ancestors;
 	}
+
+	// @if CK_DEBUG_ENGINE // toString() {
+	// @if CK_DEBUG_ENGINE // 	return `#${ this.data }`;
+	// @if CK_DEBUG_ENGINE // }
+
+	// @if CK_DEBUG_ENGINE // log() {
+	// @if CK_DEBUG_ENGINE // 	console.log( 'ViewTextProxy: ' + this );
+	// @if CK_DEBUG_ENGINE // }
+
+	// @if CK_DEBUG_ENGINE // logExtended() {
+	// @if CK_DEBUG_ENGINE // 	console.log( 'ViewTextProxy: ' + this );
+	// @if CK_DEBUG_ENGINE // }
 }

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

@@ -1,709 +0,0 @@
-/**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
-
-import { default as enableEngineDebug, disableEngineDebug } from '../../src/dev-utils/enableenginedebug';
-import Editor from '@ckeditor/ckeditor5-core/src/editor/editor';
-import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
-import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-
-import ModelPosition from '../../src/model/position';
-import ModelRange from '../../src/model/range';
-import ModelText from '../../src/model/text';
-import ModelTextProxy from '../../src/model/textproxy';
-import ModelElement from '../../src/model/element';
-import AttributeOperation from '../../src/model/operation/attributeoperation';
-import DetachOperation from '../../src/model/operation/detachoperation';
-import InsertOperation from '../../src/model/operation/insertoperation';
-import MarkerOperation from '../../src/model/operation/markeroperation';
-import MoveOperation from '../../src/model/operation/moveoperation';
-import NoOperation from '../../src/model/operation/nooperation';
-import RenameOperation from '../../src/model/operation/renameoperation';
-import RootAttributeOperation from '../../src/model/operation/rootattributeoperation';
-import MergeOperation from '../../src/model/operation/mergeoperation';
-import SplitOperation from '../../src/model/operation/splitoperation';
-import Model from '../../src/model/model';
-import ModelDocumentFragment from '../../src/model/documentfragment';
-
-import ViewDocument from '../../src/view/document';
-import ViewAttributeElement from '../../src/view/attributeelement';
-import ViewContainerElement from '../../src/view/containerelement';
-import ViewText from '../../src/view/text';
-import ViewTextProxy from '../../src/view/textproxy';
-import ViewDocumentFragment from '../../src/view/documentfragment';
-import ViewElement from '../../src/view/element';
-
-import createViewRoot from '../view/_utils/createroot';
-import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
-
-describe( 'enableEngineDebug', () => {
-	testUtils.createSinonSandbox();
-
-	afterEach( () => {
-		disableEngineDebug();
-	} );
-
-	it( 'should return plugin class', () => {
-		const result = enableEngineDebug();
-
-		expect( result.prototype ).to.be.instanceof( Plugin );
-	} );
-
-	it( 'should not throw when called multiple times', () => {
-		enableEngineDebug();
-		enableEngineDebug();
-		const result = enableEngineDebug();
-
-		expect( result.prototype ).to.be.instanceof( Plugin );
-	} );
-} );
-
-describe( 'disableEngineDebug', () => {
-	testUtils.createSinonSandbox();
-
-	it( 'restores modified stubs', () => {
-		expect( ModelPosition.prototype.log ).to.equal( undefined, 'Initial value (model/position)' );
-		expect( ModelElement.prototype.printTree ).to.equal( undefined, 'Initial value (model/element)' );
-		expect( ViewElement.prototype.printTree ).to.equal( undefined, 'Initial value (view/element)' );
-		expect( Model.prototype.createReplayer ).to.equal( undefined, 'Initial value (model/document)' );
-		expect( Editor.prototype.logDocuments ).to.equal( undefined, 'Initial value (core~editor/editor)' );
-
-		enableEngineDebug();
-
-		expect( ModelPosition.prototype.log ).to.be.a( 'function', 'After enabling engine debug (model/position)' );
-		expect( ModelElement.prototype.printTree ).to.be.a( 'function', 'After enabling engine debug (model/element)' );
-		expect( ViewElement.prototype.printTree ).to.be.a( 'function', 'After enabling engine debug (view/element)' );
-		expect( Model.prototype.createReplayer ).to.be.a( 'function', 'After enabling engine debug (model/document)' );
-		expect( Editor.prototype.logDocuments ).to.be.a( 'function', 'After enabling engine debug (core~editor/editor)' );
-
-		disableEngineDebug();
-
-		expect( ModelPosition.prototype.log ).to.equal( undefined, 'After disabling engine debug (model/position)' );
-		expect( ModelElement.prototype.printTree ).to.equal( undefined, 'After disabling engine debug (model/element)' );
-		expect( ViewElement.prototype.printTree ).to.equal( undefined, 'After disabling engine debug (view/element)' );
-		expect( Model.prototype.createReplayer ).to.equal( undefined, 'After disabling engine debug (model/document)' );
-		expect( Editor.prototype.logDocuments ).to.equal( undefined, 'After disabling engine debug (core~editor/editor)' );
-	} );
-} );
-
-describe( 'debug tools', () => {
-	let DebugPlugin, log, error;
-
-	testUtils.createSinonSandbox();
-
-	before( () => {
-		log = sinon.spy();
-		error = sinon.spy();
-		DebugPlugin = enableEngineDebug( { log, error } );
-	} );
-
-	after( () => {
-		disableEngineDebug();
-	} );
-
-	afterEach( () => {
-		log.resetHistory();
-	} );
-
-	describe( 'should provide logging tools', () => {
-		let model, modelDoc, modelRoot, modelElement, modelDocFrag;
-
-		beforeEach( () => {
-			model = new Model();
-			modelDoc = model.document;
-			modelRoot = modelDoc.createRoot();
-			modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foo' ) );
-			modelDocFrag = new ModelDocumentFragment( [ new ModelText( 'bar' ) ] );
-		} );
-
-		it( 'for ModelText', () => {
-			const foo = new ModelText( 'foo', { foo: 'bar' } );
-
-			expect( foo.toString() ).to.equal( '#foo' );
-
-			foo.log();
-			expect( log.calledWithExactly( 'ModelText: #foo' ) ).to.be.true;
-
-			foo.logExtended();
-			expect( log.calledWithExactly( 'ModelText: #foo, attrs: {"foo":"bar"}' ) ).to.be.true;
-		} );
-
-		it( 'for ModelTextProxy', () => {
-			const foo = new ModelText( 'foo', { foo: 'bar' } );
-			const proxy = new ModelTextProxy( foo, 1, 1 );
-
-			expect( proxy.toString() ).to.equal( '#o' );
-
-			proxy.log();
-			expect( log.calledWithExactly( 'ModelTextProxy: #o' ) ).to.be.true;
-
-			proxy.logExtended();
-			expect( log.calledWithExactly( 'ModelTextProxy: #o, attrs: {"foo":"bar"}' ) ).to.be.true;
-		} );
-
-		it( 'for ModelElement', () => {
-			const paragraph = new ModelElement( 'paragraph', { foo: 'bar' }, new ModelText( 'foo' ) );
-
-			expect( paragraph.toString() ).to.equal( '<paragraph>' );
-
-			paragraph.log();
-			expectLog( 'ModelElement: <paragraph>' );
-
-			paragraph.logExtended();
-			expectLog( 'ModelElement: <paragraph>, 1 children, attrs: {"foo":"bar"}' );
-
-			sinon.spy( paragraph, 'logExtended' );
-			paragraph.logAll();
-			expect( paragraph.logExtended.called ).to.be.true;
-			expectLog( 'ModelText: #foo' );
-		} );
-
-		it( 'for ModelRootElement', () => {
-			modelRoot.log();
-			expectLog( 'ModelRootElement: main' );
-		} );
-
-		it( 'for ModelDocumentFragment', () => {
-			modelDocFrag.log();
-			expectLog( 'ModelDocumentFragment: documentFragment' );
-		} );
-
-		it( 'for ModelPosition', () => {
-			const posInRoot = new ModelPosition( modelRoot, [ 0, 1, 0 ] );
-			const posInElement = new ModelPosition( modelElement, [ 0 ] );
-			const posInDocFrag = new ModelPosition( modelDocFrag, [ 2, 3 ] );
-
-			expect( posInRoot.toString() ).to.equal( 'main [ 0, 1, 0 ]' );
-			expect( posInElement.toString() ).to.equal( '<paragraph> [ 0 ]' );
-			expect( posInDocFrag.toString() ).to.equal( 'documentFragment [ 2, 3 ]' );
-
-			posInRoot.log();
-			expectLog( 'ModelPosition: main [ 0, 1, 0 ]' );
-		} );
-
-		it( 'for ModelRange', () => {
-			const rangeInRoot = ModelRange._createIn( modelRoot );
-			const rangeInElement = ModelRange._createIn( modelElement );
-			const rangeInDocFrag = ModelRange._createIn( modelDocFrag );
-
-			expect( rangeInRoot.toString() ).to.equal( 'main [ 0 ] - [ 0 ]' );
-			expect( rangeInElement.toString() ).to.equal( '<paragraph> [ 0 ] - [ 3 ]' );
-			expect( rangeInDocFrag.toString() ).to.equal( 'documentFragment [ 0 ] - [ 3 ]' );
-
-			rangeInRoot.log();
-			expectLog( 'ModelRange: main [ 0 ] - [ 0 ]' );
-		} );
-
-		it( 'for ViewText', () => {
-			const foo = new ViewText( 'foo' );
-
-			expect( foo.toString() ).to.equal( '#foo' );
-
-			foo.log();
-			expect( log.calledWithExactly( 'ViewText: #foo' ) ).to.be.true;
-
-			foo.logExtended();
-			expect( log.calledWithExactly( 'ViewText: #foo' ) ).to.be.true;
-		} );
-
-		it( 'for ViewTextProxy', () => {
-			const foo = new ViewText( 'foo', { foo: 'bar' } );
-			const proxy = new ViewTextProxy( foo, 1, 1 );
-
-			expect( proxy.toString() ).to.equal( '#o' );
-
-			proxy.log();
-			expect( log.calledWithExactly( 'ViewTextProxy: #o' ) ).to.be.true;
-
-			proxy.logExtended();
-			expect( log.calledWithExactly( 'ViewTextProxy: #o' ) ).to.be.true;
-		} );
-
-		describe( 'for operations', () => {
-			beforeEach( () => {
-				modelRoot._appendChild( [ new ModelText( 'foobar' ) ] );
-			} );
-
-			it( 'AttributeOperation', () => {
-				const op = new AttributeOperation( ModelRange._createIn( modelRoot ), 'key', null, { foo: 'bar' }, 0 );
-
-				expect( op.toString() ).to.equal( 'AttributeOperation( 0 ): "key": null -> {"foo":"bar"}, main [ 0 ] - [ 6 ]' );
-
-				op.log();
-				expect( log.calledWithExactly( op.toString() ) ).to.be.true;
-			} );
-
-			it( 'DetachOperation (text node)', () => {
-				const op = new DetachOperation( ModelPosition._createAt( modelRoot, 0 ), 3 );
-
-				expect( op.toString() ).to.equal( 'DetachOperation( null ): #foo -> main [ 0 ] - [ 3 ]' );
-
-				op.log();
-				expect( log.calledWithExactly( op.toString() ) ).to.be.true;
-			} );
-
-			it( 'DetachOperation (element)', () => {
-				const element = new ModelElement( 'element' );
-				modelRoot._insertChild( 0, element );
-
-				const op = new DetachOperation( ModelPosition._createBefore( element ), 1 );
-
-				expect( op.toString() ).to.equal( 'DetachOperation( null ): <element> -> main [ 0 ] - [ 1 ]' );
-
-				op.log();
-				expect( log.calledWithExactly( op.toString() ) ).to.be.true;
-			} );
-
-			it( 'DetachOperation (multiple nodes)', () => {
-				const element = new ModelElement( 'element' );
-				modelRoot._insertChild( 0, element );
-
-				const op = new DetachOperation( ModelPosition._createBefore( element ), 2 );
-
-				expect( op.toString() ).to.equal( 'DetachOperation( null ): [ 2 ] -> main [ 0 ] - [ 2 ]' );
-
-				op.log();
-				expect( log.calledWithExactly( op.toString() ) ).to.be.true;
-			} );
-
-			it( 'InsertOperation (text node)', () => {
-				const op = new InsertOperation( ModelPosition._createAt( modelRoot, 3 ), [ new ModelText( 'abc' ) ], 0 );
-
-				expect( op.toString() ).to.equal( 'InsertOperation( 0 ): #abc -> main [ 3 ]' );
-
-				op.log();
-				expect( log.calledWithExactly( op.toString() ) ).to.be.true;
-			} );
-
-			it( 'InsertOperation (element)', () => {
-				const op = new InsertOperation( ModelPosition._createAt( modelRoot, 3 ), [ new ModelElement( 'paragraph' ) ], 0 );
-
-				expect( op.toString() ).to.equal( 'InsertOperation( 0 ): <paragraph> -> main [ 3 ]' );
-
-				op.log();
-				expect( log.calledWithExactly( op.toString() ) ).to.be.true;
-			} );
-
-			it( 'InsertOperation (multiple nodes)', () => {
-				const nodes = [ new ModelText( 'x' ), new ModelElement( 'y' ), new ModelText( 'z' ) ];
-				const op = new InsertOperation( ModelPosition._createAt( modelRoot, 3 ), nodes, 0 );
-
-				expect( op.toString() ).to.equal( 'InsertOperation( 0 ): [ 3 ] -> main [ 3 ]' );
-
-				op.log();
-				expect( log.calledWithExactly( op.toString() ) ).to.be.true;
-			} );
-
-			it( 'MarkerOperation', () => {
-				const op = new MarkerOperation( 'marker', null, ModelRange._createIn( modelRoot ), modelDoc.markers, false, 0 );
-
-				expect( op.toString() ).to.equal( 'MarkerOperation( 0 ): "marker": null -> main [ 0 ] - [ 6 ]' );
-
-				op.log();
-				expect( log.calledWithExactly( op.toString() ) ).to.be.true;
-			} );
-
-			it( 'MoveOperation', () => {
-				const op = new MoveOperation( ModelPosition._createAt( modelRoot, 1 ), 2, ModelPosition._createAt( modelRoot, 6 ), 0 );
-
-				expect( op.toString() ).to.equal( 'MoveOperation( 0 ): main [ 1 ] - [ 3 ] -> main [ 6 ]' );
-
-				op.log();
-				expect( log.calledWithExactly( op.toString() ) ).to.be.true;
-			} );
-
-			it( 'NoOperation', () => {
-				const op = new NoOperation( 0 );
-
-				expect( op.toString() ).to.equal( 'NoOperation( 0 )' );
-
-				op.log();
-				expect( log.calledWithExactly( op.toString() ) ).to.be.true;
-			} );
-
-			it( 'RenameOperation', () => {
-				const op = new RenameOperation( ModelPosition._createAt( modelRoot, 1 ), 'old', 'new', 0 );
-
-				expect( op.toString() ).to.equal( 'RenameOperation( 0 ): main [ 1 ]: "old" -> "new"' );
-
-				op.log();
-				expect( log.calledWithExactly( op.toString() ) ).to.be.true;
-			} );
-
-			it( 'RootAttributeOperation', () => {
-				const op = new RootAttributeOperation( modelRoot, 'key', 'old', null, 0 );
-
-				expect( op.toString() ).to.equal( 'RootAttributeOperation( 0 ): "key": "old" -> null, main' );
-
-				op.log();
-				expect( log.calledWithExactly( op.toString() ) ).to.be.true;
-			} );
-
-			it( 'MergeOperation', () => {
-				const op = new MergeOperation(
-					new ModelPosition( modelRoot, [ 1, 0 ] ),
-					2,
-					new ModelPosition( modelRoot, [ 0, 2 ] ),
-					new ModelPosition( modelDoc.graveyard, [ 0 ] ),
-					0
-				);
-
-				expect( op.toString() ).to.equal(
-					'MergeOperation( 0 ): main [ 1, 0 ] -> main [ 0, 2 ] ( 2 ), $graveyard [ 0 ]'
-				);
-
-				op.log();
-				expect( log.calledWithExactly( op.toString() ) ).to.be.true;
-			} );
-
-			it( 'SplitOperation without graveyard position', () => {
-				const position = new ModelPosition( modelRoot, [ 1, 4 ] );
-				const op = new SplitOperation( position, 6, null, 0 );
-
-				expect( op.toString() ).to.equal(
-					'SplitOperation( 0 ): main [ 1, 4 ] ( 6 ) -> main [ 2 ]'
-				);
-
-				op.log();
-				expect( log.calledWithExactly( op.toString() ) ).to.be.true;
-			} );
-
-			it( 'SplitOperation with graveyard position', () => {
-				const position = new ModelPosition( modelRoot, [ 1, 4 ] );
-				const op = new SplitOperation( position, 6, new ModelPosition( modelDoc.graveyard, [ 0 ] ), 0 );
-
-				expect( op.toString() ).to.equal(
-					'SplitOperation( 0 ): main [ 1, 4 ] ( 6 ) -> main [ 2 ] with $graveyard [ 0 ]'
-				);
-
-				op.log();
-				expect( log.calledWithExactly( op.toString() ) ).to.be.true;
-			} );
-		} );
-
-		it( 'for applied operations', () => {
-			const op = new InsertOperation( ModelPosition._createAt( modelRoot, 0 ), [ new ModelText( 'foo' ) ], 0 );
-
-			model.applyOperation( op );
-
-			expect( log.calledWithExactly( 'Applying InsertOperation( 0 ): #foo -> main [ 0 ]' ) ).to.be.true;
-		} );
-	} );
-
-	describe( 'should provide tree printing tools', () => {
-		it( 'for model', () => {
-			const model = new Model();
-			const modelDoc = model.document;
-			const modelRoot = modelDoc.createRoot();
-
-			modelRoot._appendChild( [
-				new ModelElement( 'paragraph', { foo: 'bar' }, [
-					new ModelText( 'This is ' ), new ModelText( 'bold', { bold: true } ), new ModelText( '.' )
-				] ),
-				new ModelElement( 'listItem', { listType: 'numbered', listIndent: 0 }, new ModelText( 'One.' ) ),
-			] );
-
-			const modelRootTree = modelRoot.printTree();
-
-			expect( modelRootTree ).to.equal(
-				'<main>' +
-				'\n\t<paragraph foo="bar">' +
-				'\n\t\tThis is ' +
-				'\n\t\t<$text bold=true>bold</$text>' +
-				'\n\t\t.' +
-				'\n\t</paragraph>' +
-				'\n\t<listItem listType="numbered" listIndent=0>' +
-				'\n\t\tOne.' +
-				'\n\t</listItem>' +
-				'\n</main>'
-			);
-
-			modelRoot.logTree();
-			expect( log.calledWithExactly( modelRootTree ) ).to.be.true;
-
-			const modelParagraph = modelRoot.getChild( 0 );
-			const modelParagraphTree = modelParagraph.printTree();
-			expect( modelParagraphTree ).to.equal(
-				'<paragraph foo="bar">' +
-				'\n\tThis is ' +
-				'\n\t<$text bold=true>bold</$text>' +
-				'\n\t.' +
-				'\n</paragraph>'
-			);
-
-			log.resetHistory();
-			modelParagraph.logTree();
-			expect( log.calledWithExactly( modelParagraphTree ) ).to.be.true;
-
-			const modelDocFrag = new ModelDocumentFragment( [
-				new ModelText( 'This is ' ), new ModelText( 'bold', { bold: true } ), new ModelText( '.' ),
-				new ModelElement( 'paragraph', { foo: 'bar' }, [
-					new ModelText( 'This is ' ), new ModelText( 'bold', { bold: true } ), new ModelText( '.' )
-				] )
-			] );
-
-			const modelDocFragTree = modelDocFrag.printTree();
-			expect( modelDocFragTree ).to.equal(
-				'ModelDocumentFragment: [' +
-				'\n\tThis is ' +
-				'\n\t<$text bold=true>bold</$text>' +
-				'\n\t.' +
-				'\n\t<paragraph foo="bar">' +
-				'\n\t\tThis is ' +
-				'\n\t\t<$text bold=true>bold</$text>' +
-				'\n\t\t.' +
-				'\n\t</paragraph>' +
-				'\n]'
-			);
-
-			log.resetHistory();
-			modelDocFrag.logTree();
-			expect( log.calledWithExactly( modelDocFragTree ) ).to.be.true;
-		} );
-
-		it( 'for view', () => {
-			const viewDoc = new ViewDocument();
-			const viewRoot = createViewRoot( viewDoc );
-
-			viewRoot._appendChild( [
-				new ViewContainerElement( 'p', { foo: 'bar' }, [
-					new ViewText( 'This is ' ), new ViewAttributeElement( 'b', null, new ViewText( 'bold' ) ), new ViewText( '.' )
-				] ),
-				new ViewContainerElement( 'ol', null, [
-					new ViewContainerElement( 'li', null, new ViewText( 'One.' ) )
-				] )
-			] );
-
-			const viewRootTree = viewRoot.printTree();
-
-			expect( viewRootTree ).to.equal(
-				'<div>' +
-				'\n\t<p foo="bar">' +
-				'\n\t\tThis is ' +
-				'\n\t\t<b>' +
-				'\n\t\t\tbold' +
-				'\n\t\t</b>' +
-				'\n\t\t.' +
-				'\n\t</p>' +
-				'\n\t<ol>' +
-				'\n\t\t<li>' +
-				'\n\t\t\tOne.' +
-				'\n\t\t</li>' +
-				'\n\t</ol>' +
-				'\n</div>'
-			);
-
-			viewRoot.logTree();
-			expect( log.calledWithExactly( viewRootTree ) ).to.be.true;
-
-			const viewParagraph = viewRoot.getChild( 0 );
-			const viewParagraphTree = viewParagraph.printTree();
-			expect( viewParagraphTree ).to.equal(
-				'<p foo="bar">' +
-				'\n\tThis is ' +
-				'\n\t<b>' +
-				'\n\t\tbold' +
-				'\n\t</b>' +
-				'\n\t.' +
-				'\n</p>'
-			);
-
-			log.resetHistory();
-			viewParagraph.logTree();
-			expect( log.calledWithExactly( viewParagraphTree ) ).to.be.true;
-
-			const viewDocFrag = new ViewDocumentFragment( [
-				new ViewText( 'Text.' ),
-				new ViewContainerElement( 'p', { foo: 'bar' }, [
-					new ViewText( 'This is ' ), new ViewAttributeElement( 'b', null, new ViewText( 'bold' ) ), new ViewText( '.' )
-				] )
-			] );
-
-			const viewDocFragTree = viewDocFrag.printTree();
-			expect( viewDocFragTree ).to.equal(
-				'ViewDocumentFragment: [' +
-				'\n\tText.' +
-				'\n\t<p foo="bar">' +
-				'\n\t\tThis is ' +
-				'\n\t\t<b>' +
-				'\n\t\t\tbold' +
-				'\n\t\t</b>' +
-				'\n\t\t.' +
-				'\n\t</p>' +
-				'\n]'
-			);
-
-			log.resetHistory();
-			viewDocFrag.logTree();
-			expect( log.calledWithExactly( viewDocFragTree ) ).to.be.true;
-		} );
-	} );
-
-	describe( 'should store model and view trees state', () => {
-		let editor;
-
-		beforeEach( () => {
-			return VirtualTestEditor.create( {
-				plugins: [ DebugPlugin ]
-			} ).then( _editor => {
-				editor = _editor;
-			} );
-		} );
-
-		it( 'should store model and view state after each applied operation', () => {
-			const model = editor.model;
-			const modelDoc = model.document;
-			const modelRoot = modelDoc.getRoot();
-			const view = editor.editing.view;
-			const viewDoc = view.document;
-
-			// Reset model document version to ensure it will start at 0.
-			model.document.version = 0;
-
-			model.change( () => {
-				const insert = new InsertOperation( ModelPosition._createAt( modelRoot, 0 ), new ModelText( 'foobar' ), 0 );
-				model.applyOperation( insert );
-
-				const graveyard = modelDoc.graveyard;
-				const move = new MoveOperation( ModelPosition._createAt( modelRoot, 1 ), 2, ModelPosition._createAt( graveyard, 0 ), 1 );
-				model.applyOperation( move );
-			} );
-
-			log.resetHistory();
-
-			modelDoc.log( 0 );
-			expectLog(
-				'<$graveyard></$graveyard>' +
-				'\n<main></main>'
-			);
-
-			modelDoc.log( 1 );
-			expectLog(
-				'<$graveyard></$graveyard>' +
-				'\n<main>' +
-				'\n\tfoobar' +
-				'\n</main>'
-			);
-
-			modelDoc.log( 2 );
-			expectLog(
-				'<$graveyard>' +
-				'\n\too' +
-				'\n</$graveyard>' +
-				'\n<main>' +
-				'\n\tfbar' +
-				'\n</main>'
-			);
-
-			modelDoc.log();
-			expectLog(
-				'<$graveyard>' +
-				'\n\too' +
-				'\n</$graveyard>' +
-				'\n<main>' +
-				'\n\tfbar' +
-				'\n</main>'
-			);
-
-			viewDoc.log( 0 );
-			expectLog(
-				'<$root></$root>'
-			);
-
-			viewDoc.log( 2 );
-			expectLog(
-				'<$root>' +
-				'\n\tfbar' +
-				'\n</$root>'
-			);
-
-			sinon.spy( modelDoc, 'log' );
-			sinon.spy( viewDoc, 'log' );
-
-			editor.logModel( 1 );
-			expect( modelDoc.log.calledWithExactly( 1 ), 1 ).to.be.true;
-
-			editor.logView( 2 );
-			expect( viewDoc.log.calledWithExactly( 2 ), 2 ).to.be.true;
-
-			modelDoc.log.resetHistory();
-			viewDoc.log.resetHistory();
-
-			editor.logModel();
-			expect( modelDoc.log.calledWithExactly( 2 ), 3 ).to.be.true;
-
-			modelDoc.log.resetHistory();
-			viewDoc.log.resetHistory();
-
-			editor.logDocuments();
-			expect( modelDoc.log.calledWithExactly( 2 ), 4 ).to.be.true;
-			expect( viewDoc.log.calledWithExactly( 2 ), 5 ).to.be.true;
-
-			modelDoc.log.resetHistory();
-			viewDoc.log.resetHistory();
-
-			editor.logDocuments( 1 );
-			expect( modelDoc.log.calledWithExactly( 1 ), 6 ).to.be.true;
-			expect( viewDoc.log.calledWithExactly( 1 ), 7 ).to.be.true;
-		} );
-
-		it( 'should remove old states', () => {
-			const model = editor.model;
-			const modelDoc = model.document;
-			const modelRoot = model.document.getRoot();
-
-			for ( let i = 0; i < 25; i++ ) {
-				const insert = new InsertOperation( ModelPosition._createAt( modelRoot, 0 ), new ModelText( 'foobar' ), modelDoc.version );
-				model.applyOperation( insert );
-			}
-
-			modelDoc.log( 0 );
-			expectLog( 'Tree log unavailable for given version: 0' );
-		} );
-	} );
-
-	describe( 'should provide methods for operation replayer', () => {
-		it( 'getAppliedOperations()', () => {
-			const model = new Model();
-			const modelDoc = model.document;
-
-			expect( model.getAppliedOperations() ).to.equal( '' );
-
-			const otherRoot = modelDoc.createRoot( '$root', 'otherRoot' );
-			const element = new ModelElement( 'paragraph' );
-
-			otherRoot._appendChild( element );
-
-			const insert = new InsertOperation( ModelPosition._createAt( element, 0 ), new ModelText( 'foo' ), 0 );
-			model.applyOperation( insert );
-
-			const stringifiedOperations = model.getAppliedOperations();
-
-			expect( stringifiedOperations ).to.equal( JSON.stringify( insert ) );
-		} );
-
-		it( 'createReplayer()', () => {
-			const model = new Model();
-			const modelDoc = model.document;
-
-			const otherRoot = modelDoc.createRoot( '$root', 'otherRoot' );
-			const element = new ModelElement( 'paragraph' );
-
-			otherRoot._appendChild( element );
-
-			const insert = new InsertOperation( ModelPosition._createAt( element, 0 ), new ModelText( 'foo' ), 0 );
-			model.applyOperation( insert );
-
-			const stringifiedOperations = model.getAppliedOperations();
-			const operationReplayer = model.createReplayer( stringifiedOperations );
-
-			expect( operationReplayer.getOperationsToReplay() ).to.deep.equal( [ JSON.parse( stringifiedOperations ) ] );
-		} );
-	} );
-
-	function expectLog( expectedLogMsg ) {
-		expect( log.calledWithExactly( expectedLogMsg ) ).to.be.true;
-		log.resetHistory();
-	}
-} );