Przeglądaj źródła

Added: engine.model.CompressedHistory.

Szymon Cofalik 9 lat temu
rodzic
commit
568ea54b1e

+ 159 - 0
packages/ckeditor5-engine/src/model/compressedhistory.js

@@ -0,0 +1,159 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import History from './history.js';
+
+/**
+ * `CompressedHistory` keeps deltas applied to the {@link engine.model.Document document} in their transformed state. Deltas
+ * stored in `CompressedHistory` might get updated, split into more deltas or removed as other deltas are applied to the document.
+ * Modifying the original deltas history results in a compressed version of history and makes some scripts faster and easier to implement.
+ *
+ * **Note:** deltas kept in `CompressedHistory` should be used only to transform deltas. Do not use `CompressedHistory` to get original
+ * delta (especially basing on its {@link engine.model.delta.Delta#baseVersion baseVersion}). Do not trust base versions of deltas
+ * returned by `CompressedHistory`. After transforming your delta by deltas from `CompressedHistory`, fix it's base version accordingly.
+ *
+ * @see engine.model.History
+ * @memberOf engine.model
+ */
+export default class CompressedHistory extends History {
+	constructor() {
+		super();
+
+		/**
+		 * Stores base versions of deltas which has been marked as inactive.
+		 * @private
+		 * @member {Array.<Number>} engine.model.CompressedHistory#_inactiveBaseVersions
+		 */
+		this._inactiveBaseVersions = [];
+	}
+
+	/**
+	 * Returns one or more deltas from compressed history that bases on given `baseVersion`. Most often it will be just
+	 * one delta, but if that delta got updated by multiple deltas, all of those updated deltas will be returned.
+	 *
+	 * @see engine.model.CompressedHistory#updateDelta
+	 * @param {Number} baseVersion Base version of the delta to retrieve.
+	 * @returns {Array.<engine.model.delta.Delta>|null} Delta with given base version or null if no such delta is in history.
+	 */
+	getDelta( baseVersion ) {
+		let index = this._historyPoints.get( baseVersion );
+
+		if ( index === undefined ) {
+			return null;
+		}
+
+		const deltas = [];
+
+		for ( index; index < this._deltas.length; index++ ) {
+			const delta = this._deltas[ index ];
+
+			if ( delta.baseVersion != baseVersion ) {
+				break;
+			}
+
+			deltas.push( delta );
+		}
+
+		return deltas.length === 0 ? null : deltas;
+	}
+
+	/**
+	 * Marks delta as reversed/reversing delta and makes it "inactive". This happens i.e., when a delta is undone by
+	 * another delta. Both undone delta and undoing delta should be marked as inactive nad have no impact on transforming
+	 * other deltas.
+	 *
+	 * **Note:** using this method does not change the state of {@link engine.model.Document model}. It just affects
+	 * the state of `CompressedHistory`.
+	 *
+	 * **Note:** when some deltas are marked as "inactive", deltas between them should probably get updated. See
+	 * {@link engine.model.CompressedHistory#updateDelta}.
+	 *
+	 * **Note:** if delta with `baseVersion` got updated by multiple deltas, all updated deltas will be marked as
+	 * "inactive".
+	 *
+	 * Due to how nodes removing works, we can't just removed those deltas from history. Here is explanation:
+	 * since a delta got undone, both it and it's undoing counterpart should not have any impact on transformations of other
+	 * deltas. State of tree structure should look like "nothing happened". Unfortunately, this is not true for inserting nodes,
+	 * because they are not removed from the tree structure. Instead, removing nodes actually moves them to
+	 * {@link engine.model.Document#graveyard graveyard root}. When {@link engine.model.delta.InsertDelta InsertDelta} is
+	 * reversed (by {@link engine.model.delta.RemoveDelta RemoveDelta} the state of the tree is not like before insert delta
+	 * happened - there is an additional node in graveyard root. This means that it is incorrect to just remove inactive deltas from
+	 * history. Instead, for graveyard-related operations fake fix operations are created. Non-graveyard-related operations are removed.
+	 *
+	 * @param {Number} baseVersion Base version of a delta to be marked as reversed/reversing delta.
+	 */
+	removeDelta( baseVersion ) {
+		this.updateDelta( baseVersion, [] );
+		this._inactiveBaseVersions.push( baseVersion );
+	}
+
+	/**
+	 * Substitutes delta from compressed history by one or more given deltas.
+	 *
+	 * **Note:** if delta with `baseVersion` was already updated by multiple deltas, all updated deltas will be removed
+	 * and new deltas will be inserted at their position.
+	 *
+	 * **Note:** delta marked as reversed won't get updated.
+	 *
+	 * @param {Number} baseVersion Base version of a delta to update.
+	 * @param {Iterable.<engine.model.delta.Delta>} updatedDeltas Deltas to be inserted in place of updated delta.
+	 */
+	updateDelta( baseVersion, updatedDeltas ) {
+		if ( this._inactiveBaseVersions.indexOf( baseVersion ) != -1 ) {
+			return;
+		}
+
+		const deltas = this.getDelta( baseVersion );
+
+		// If there are no deltas, stop executing function as there is nothing to mark.
+		if ( deltas === null ) {
+			return;
+		}
+
+		// Make sure that every updated delta has correct `baseVersion`.
+		// This is crucial for algorithms in `CompressedHistory` and algorithms using `CompressedHistory`.
+		for ( let delta of updatedDeltas ) {
+			delta.baseVersion = baseVersion;
+		}
+
+		// Put updated deltas in place of old deltas.
+		this._deltas.splice( this._getIndex( baseVersion ), deltas.length, ...updatedDeltas );
+
+		// Update history points.
+		this._updateHistoryPointsAfter( baseVersion, updatedDeltas.length - deltas.length );
+	}
+
+	/**
+	 * Returns base versions of deltas which has been marked as reversed, in given base versions range.
+	 *
+	 * @param {Number} from Start of base versions range to check.
+	 * @param {Number} to End of base versions range to check.
+	 * @returns {Iterator.<Number>} Base versions of deltas marked as reversed.
+	 */
+	*getInactiveBaseVersions( from = 0, to = Number.POSITIVE_INFINITY ) {
+		for ( let baseVersion of this._inactiveBaseVersions ) {
+			if ( baseVersion >= from && baseVersion < to ) {
+				yield baseVersion;
+			}
+		}
+	}
+
+	/**
+	 * Updates {@link engine.model.History#_historyPoints} structure.
+	 *
+	 * @param {Number} baseVersion Base version of delta after which history points should be updated.
+	 * @param {Number} changeBy By how much change history points. Can be a negative value.
+	 * @private
+	 */
+	_updateHistoryPointsAfter( baseVersion, changeBy ) {
+		for ( let key of this._historyPoints.keys() ) {
+			if ( key > baseVersion ) {
+				this._historyPoints.set( key, this._historyPoints.get( key ) + changeBy );
+			}
+		}
+	}
+}

+ 10 - 4
packages/ckeditor5-engine/src/model/document.js

@@ -11,7 +11,7 @@ import transformations from './delta/basic-transformations.js'; // jshint ignore
 
 import RootElement from './rootelement.js';
 import Batch from './batch.js';
-import History from './history.js';
+import CompressedHistory from './compressedhistory.js';
 import Selection from './selection.js';
 import EmitterMixin from '../../utils/emittermixin.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
@@ -108,10 +108,13 @@ export default class Document {
 		/**
 		 * Document's history.
 		 *
+		 * This is a compressed document history. It means that stored deltas might be removed or different
+		 * than originally applied deltas.
+		 *
 		 * @readonly
-		 * @member {engine.model.History} engine.model.Document#history
+		 * @member {engine.model.CompressedHistory} engine.model.Document#history
 		 */
-		this.history = new History();
+		this.history = new CompressedHistory( this );
 	}
 
 	/**
@@ -159,7 +162,10 @@ export default class Document {
 
 		this.version++;
 
-		this.history.addOperation( operation );
+		if ( operation.delta ) {
+			// Right now I can't imagine operations without deltas, but let's be safe.
+			this.history.addDelta( operation.delta );
+		}
 
 		const batch = operation.delta && operation.delta.batch;
 

+ 170 - 0
packages/ckeditor5-engine/tests/model/compressedhistory.js

@@ -0,0 +1,170 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import CompressedHistory from '/ckeditor5/engine/model/compressedhistory.js';
+import Delta from '/ckeditor5/engine/model/delta/delta.js';
+import Operation from '/ckeditor5/engine/model/operation/operation.js';
+
+describe( 'CompressedHistory', () => {
+	let history;
+
+	beforeEach( () => {
+		history = new CompressedHistory();
+	} );
+
+	describe( 'updateDelta', () => {
+		it( 'should substitute delta from history by given deltas', () => {
+			history.addDelta( getDelta( 0 ) );
+
+			const deltas = getDeltaSet();
+			history.updateDelta( 0, deltas );
+
+			const historyDeltas = Array.from( history.getDeltas() );
+			expect( historyDeltas ).to.deep.equal( deltas );
+		} );
+
+		it( 'should substitute all updated deltas by new deltas', () => {
+			history.addDelta( getDelta( 0 ) );
+
+			// Change original single delta to three deltas generated by `getDeltaSet`.
+			// All those deltas should now be seen under base version 0.
+			history.updateDelta( 0, getDeltaSet() );
+
+			const deltas = getDeltaSet();
+			// Change all three deltas from base version 0 to new set of deltas.
+			history.updateDelta( 0, deltas );
+
+			const historyDeltas = Array.from( history.getDeltas() );
+			expect( historyDeltas ).to.deep.equal( deltas );
+		} );
+
+		it( 'should do nothing if deltas for given base version has not been found in history', () => {
+			history.addDelta( getDelta( 0 ) );
+			history.removeDelta( 0 );
+
+			const deltas = getDeltaSet();
+
+			history.updateDelta( 0, deltas );
+
+			expect( Array.from( history.getDeltas() ).length ).to.equal( 0 );
+		} );
+	} );
+
+	describe( 'removeDelta', () => {
+		it( 'should remove deltas that do not have graveyard related operations', () => {
+			for ( let delta of getDeltaSet() ) {
+				history.addDelta( delta );
+			}
+
+			history.removeDelta( 3 );
+
+			const deltas = Array.from( history.getDeltas() );
+			expect( deltas.length ).to.equal( 2 );
+		} );
+
+		it( 'should remove multiple updated deltas', () => {
+			let delta = getDelta( 0 );
+			history.addDelta( delta );
+
+			let updatedDeltas = getDeltaSet( 0 );
+
+			history.updateDelta( 0, updatedDeltas );
+			history.removeDelta( 0 );
+
+			const deltas = Array.from( history.getDeltas() );
+			expect( deltas.length ).to.equal( 0 );
+		} );
+
+		it( 'should do nothing if deltas for given base version has not been found in history', () => {
+			const deltas = getDeltaSet();
+
+			for ( let delta of deltas ) {
+				history.addDelta( delta );
+			}
+
+			history.removeDelta( 12 );
+
+			expect( Array.from( history.getDeltas() ) ).to.deep.equal( deltas );
+		} );
+	} );
+
+	describe( 'getDelta', () => {
+		it( 'should return array with one delta with given base version', () => {
+			let delta = getDelta( 0 );
+			history.addDelta( delta );
+
+			const historyDelta = history.getDelta( 0 );
+			expect( historyDelta ).to.deep.equal( [ delta ] );
+		} );
+
+		it( 'should return array with all updated deltas of delta with given base version', () => {
+			let delta = getDelta( 0 );
+			history.addDelta( delta );
+
+			let deltas = getDeltaSet();
+			history.updateDelta( 0, deltas );
+
+			const historyDelta = history.getDelta( 0 );
+			expect( historyDelta ).to.deep.equal( deltas );
+		} );
+
+		it( 'should return null if delta has not been found in history', () => {
+			expect( history.getDelta( -1 ) ).to.be.null;
+			expect( history.getDelta( 2 ) ).to.be.null;
+			expect( history.getDelta( 20 ) ).to.be.null;
+		} );
+
+		it( 'should return null if delta has been removed by removeDelta', () => {
+			let delta = getDelta( 0 );
+			history.addDelta( delta );
+			history.removeDelta( 0 );
+
+			expect( history.getDelta( 0 ) ).to.be.null;
+		} );
+	} );
+
+	describe( 'getInactiveBaseVersions', () => {
+		it( 'should return inactive base versions from given base versions range', () => {
+			for ( let delta of getDeltaSet() ) {
+				history.addDelta( delta );
+			}
+
+			history.removeDelta( 3 );
+			history.removeDelta( 6 );
+
+			expect( Array.from( history.getInactiveBaseVersions() ) ).to.deep.equal( [ 3, 6 ] );
+			expect( Array.from( history.getInactiveBaseVersions( 0 ) ) ).to.deep.equal( [ 3, 6 ] );
+			expect( Array.from( history.getInactiveBaseVersions( 3 ) ) ).to.deep.equal( [ 3, 6 ] );
+			expect( Array.from( history.getInactiveBaseVersions( 0, 3 ) ) ).to.deep.equal( [] );
+			expect( Array.from( history.getInactiveBaseVersions( 0, 4 ) ) ).to.deep.equal( [ 3 ] );
+			expect( Array.from( history.getInactiveBaseVersions( 0, 6 ) ) ).to.deep.equal( [ 3 ] );
+			expect( Array.from( history.getInactiveBaseVersions( 4 ) ) ).to.deep.equal( [ 6 ] );
+			expect( Array.from( history.getInactiveBaseVersions( 6 ) ) ).to.deep.equal( [ 6 ] );
+			expect( Array.from( history.getInactiveBaseVersions( 9 ) ) ).to.deep.equal( [] );
+		} );
+	} );
+} );
+
+function getDeltaSet() {
+	const deltas = [];
+
+	deltas.push( getDelta( 0 ) );
+	deltas.push( getDelta( 3 ) );
+	deltas.push( getDelta( 6 ) );
+
+	return deltas;
+}
+
+function getDelta( baseVersion ) {
+	const delta = new Delta();
+
+	for ( let i = 0; i < 3; i++ ) {
+		delta.addOperation( new Operation( i + baseVersion ) );
+	}
+
+	return delta;
+}

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

@@ -12,6 +12,7 @@ import Schema from '/ckeditor5/engine/model/schema.js';
 import Composer from '/ckeditor5/engine/model/composer/composer.js';
 import RootElement from '/ckeditor5/engine/model/rootelement.js';
 import Batch from '/ckeditor5/engine/model/batch.js';
+import Delta from '/ckeditor5/engine/model/delta/delta.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
 import count from '/ckeditor5/utils/count.js';
 
@@ -114,15 +115,18 @@ describe( 'Document', () => {
 			const changeCallback = sinon.spy();
 			const type = 't';
 			const data = { data: 'x' };
-			const batch = 'batch';
+			const batch = new Batch();
+			const delta = new Delta();
 
 			let operation = {
 				type: type,
-				delta: { batch: batch },
 				baseVersion: 0,
 				_execute: sinon.stub().returns( data )
 			};
 
+			delta.addOperation( operation );
+			batch.addDelta( delta );
+
 			doc.on( 'change', changeCallback );
 			doc.applyOperation( operation );