浏览代码

Changed: Introduced `ContextFactory` in OT algorithms. Cleaned up mess with "context" names.

Szymon Cofalik 7 年之前
父节点
当前提交
d770977a5a

+ 206 - 215
packages/ckeditor5-engine/src/model/operation/transform.js

@@ -302,7 +302,9 @@ export function transformSets( operationsA, operationsB, options ) {
 		originalOperationsBCount: operationsB.length
 	};
 
-	const context = initializeContext( operationsA, operationsB, options );
+	const contextFactory = new ContextFactory( options.document, options.useContext );
+	contextFactory.setOriginalOperations( operationsA );
+	contextFactory.setOriginalOperations( operationsB );
 
 	// Index of currently transformed operation `a`.
 	let i = 0;
@@ -323,35 +325,16 @@ export function transformSets( operationsA, operationsB, options ) {
 
 		const opB = operationsB[ indexB ];
 
-		// Evaluate `TransformationContext` objects for operation transformation.
-		const contextAB = {
-			aIsStrong: true,
-			aWasUndone: context.wasUndone( opA ),
-			bWasUndone: context.wasUndone( opB ),
-			abRelation: context.getRelation( opA, opB ),
-			baRelation: context.getRelation( opB, opA )
-		};
-
-		const contextBA = {
-			aIsStrong: false,
-			aWasUndone: context.wasUndone( opB ),
-			bWasUndone: context.wasUndone( opA ),
-			abRelation: context.getRelation( opB, opA ),
-			baRelation: context.getRelation( opA, opB )
-		};
-
 		// Transform `a` by `b` and `b` by `a`.
-		const newOpsA = transform( opA, opB, contextAB );
-		const newOpsB = transform( opB, opA, contextBA );
+		const newOpsA = transform( opA, opB, contextFactory.getContext( opA, opB, true ) );
+		const newOpsB = transform( opB, opA, contextFactory.getContext( opB, opA, false ) );
 		// As a result we get one or more `newOpsA` and one or more `newOpsB` operations.
 
 		// Update contextual information about operations.
-		if ( options.useContext ) {
-			updateRelations( opA, opB, context );
+		contextFactory.updateRelation( opA, opB );
 
-			updateOriginalOperation( opA, newOpsA, context );
-			updateOriginalOperation( opB, newOpsB, context );
-		}
+		contextFactory.setOriginalOperations( newOpsA, opA );
+		contextFactory.setOriginalOperations( newOpsB, opB );
 
 		// For new `a` operations, update their index of the next operation `b` to transform them by.
 		//
@@ -391,92 +374,177 @@ export function transformSets( operationsA, operationsB, options ) {
 	return { operationsA, operationsB };
 }
 
-/**
- * An utility function that updates {@link module:engine/model/operation/operation~Operation#baseVersion base versions}
- * of passed operations.
- *
- * The function simply sets `baseVersion` as a base version of the first passed operation and then increments it for
- * each following operation in `operations`.
- *
- * @private
- * @param {Array.<module:engine/model/operation/operation~Operation>} operations Operations to update.
- * @param {Number} baseVersion Base version to set for the first operation in `operations`.
- */
-function updateBaseVersions( operations, baseVersion ) {
-	for ( const operation of operations ) {
-		operation.baseVersion = baseVersion++;
+// Gathers additional data about operations processed during transformation. Can be used to obtain contextual information
+// about two operations that are about to be transformed. This contextual information can be used for better conflict resolution.
+class ContextFactory {
+	// Creates `ContextFactory` instance.
+	//
+	// @param {module:engine/model/document~Document} document Document which the operations change.
+	// @param {Boolean} useContext Whether during transformation additional context information should be gathered and used.
+	constructor( document, useContext ) {
+		// `model.History` instance which information about undone operations will be taken from.
+		this._history = document.history;
+
+		// Whether additional context should be used.
+		this._useContext = useContext;
+
+		// For each operation that is created during transformation process, we keep a reference to the original operation
+		// which it comes from. The original operation works as a kind of "identifier". Every contextual information
+		// gathered during transformation that we want to save for given operation, is actually saved for the original operation.
+		// This way no matter if operation `a` is cloned, then transformed, even breaks, we still have access to the previously
+		// gathered data through original operation reference.
+		this._originalOperations = new Map();
+
+		// Relations is a double-map structure (maps in map) where for two operations we store how those operations were related
+		// to each other. Those relations are evaluated during transformation process. For every transformated pair of operations
+		// we keep relations between them.
+		this._relations = new Map();
 	}
-}
 
-/**
- * Adds `howMany` instances of {@link module:engine/model/operation/nooperation~NoOperation} to `operations` set.
- *
- * @private
- * @param {Array.<module:engine/model/operation/operation~Operation>} operations
- * @param {Number} howMany
- */
-function padWithNoOps( operations, howMany ) {
-	for ( let i = 0; i < howMany; i++ ) {
-		operations.push( new NoOperation( 0 ) );
+	// Rewrites information about original operation to the new operations.
+	//
+	// Used when `newOps` are generated from `oldOp` (during transformation). It takes `oldOp`'s original operation and
+	// sets it as `newOps` original operation.
+	//
+	// It also means that if an operation is broken into multiple during transformation, all those broken "pieces" are pointing
+	// to the same operation as their original operation.
+	//
+	// @param {Array.<module:engine/model/operation/operation~Operation>} operations
+	// @param {module:engine/model/operation/operation~Operation|null} [takeFrom=null]
+	setOriginalOperations( operations, takeFrom = null ) {
+		const originalOperation = takeFrom ? this._originalOperations.get( takeFrom ) : null;
+
+		for ( const operation of operations ) {
+			this._originalOperations.set( operation, originalOperation || operation );
+		}
 	}
-}
 
-/**
- * Initializes transformation process data used to evaluate context between two transformed operations.
- *
- * @param {Array.<module:engine/model/operation/operation~Operation>} operationsA
- * @param {Array.<module:engine/model/operation/operation~Operation>} operationsB
- * @param {Object} options Additional transformation options. See {@link module:engine/model/operation/transform~transformSets}
- * @returns {Object} Transformation process data object.
- */
-function initializeContext( operationsA, operationsB, options ) {
-	const context = {};
+	// Saves a relation between operations `opA` and `opB`.
+	//
+	// Relations are then later used to help solve conflicts when operations are transformed.
+	//
+	// @param {module:engine/model/operation/operation~Operation} opA
+	// @param {module:engine/model/operation/operation~Operation} opB
+	updateRelation( opA, opB ) {
+		// The use of relations is described in a bigger detail in transformation functions.
+		//
+		// In brief, this function, for specified pairs of operation types, checks how positions defined in those operations relate.
+		// Then those relations are saved. For example, for two move operations, it is saved if one of those operations target
+		// position is before the other operation source position. This kind of information gives contextual information when
+		// transformation is used during undo. Similar checks are done for other pairs of operations.
+		//
+		switch ( opA.constructor ) {
+			case MoveOperation: {
+				switch ( opB.constructor ) {
+					case MergeOperation: {
+						if ( opA.targetPosition.isEqual( opB.sourcePosition ) || opB.movedRange.containsPosition( opA.targetPosition ) ) {
+							this._setRelation( opA, opB, 'insertAtSource' );
+						} else if ( opA.targetPosition.isEqual( opB.deletionPosition ) ) {
+							this._setRelation( opA, opB, 'insertBetween' );
+						}
+
+						break;
+					}
+
+					case MoveOperation: {
+						if ( opA.targetPosition.isEqual( opB.sourcePosition ) || opA.targetPosition.isBefore( opB.sourcePosition ) ) {
+							this._setRelation( opA, opB, 'insertBefore' );
+						} else {
+							this._setRelation( opA, opB, 'insertAfter' );
+						}
+
+						break;
+					}
+
+					case UnwrapOperation: {
+						const isInside = opA.targetPosition.hasSameParentAs( opB.position );
+
+						if ( isInside ) {
+							this._setRelation( opA, opB, 'insertInside' );
+						}
+
+						break;
+					}
+				}
+
+				break;
+			}
 
-	// For each operation that is created during transformation process, we keep a reference to the original operation
-	// which it comes from. The original operation works as a kind of "identifier". Every contextual information
-	// gathered during transformation that we want to save for given operation, is actually saved for the original operation.
-	// This way no matter if operation `a` is cloned, then transformed, even breaks, we still have access to the previously
-	// gathered data through original operation reference.
-	context.originalOperations = new Map();
+			case SplitOperation: {
+				switch ( opB.constructor ) {
+					case MergeOperation: {
+						if ( opA.position.isBefore( opB.sourcePosition ) ) {
+							this._setRelation( opA, opB, 'splitBefore' );
+						}
 
-	// At the beginning for each operation, set the original operation reference to itself.
-	for ( const op of operationsA.concat( operationsB ) ) {
-		context.originalOperations.set( op, op );
+						break;
+					}
+
+					case MoveOperation: {
+						if ( opA.position.isEqual( opB.sourcePosition ) || opA.position.isBefore( opB.sourcePosition ) ) {
+							this._setRelation( opA, opB, 'splitBefore' );
+						}
+
+						break;
+					}
+				}
+
+				break;
+			}
+		}
 	}
 
-	context.document = options.document;
+	// Evaluates and returns contextual information about two given operations `opA` and `opB` which are about to be transformed.
+	//
+	// @param {module:engine/model/operation/operation~Operation} opA
+	// @param {module:engine/model/operation/operation~Operation} opB
+	// @returns {module:engine/model/operation/transform~TransformationContext}
+	getContext( opA, opB, aIsStrong ) {
+		if ( !this._useContext ) {
+			// Additional contextual data is `false` or `null` if additional context is not used.
 
-	// Relations is a double-map structure (maps in map) where for two operations we store how those operations were related
-	// to each other. Those relations are evaluated during transformation process. For every transformated pair of operations
-	// we keep relations between them.
-	context.relations = new Map();
+			return {
+				aIsStrong,
+				aWasUndone: false,
+				bWasUndone: false,
+				abRelation: null,
+				baRelation: null
+			};
+		}
+
+		return {
+			aIsStrong,
+			aWasUndone: this._wasUndone( opA ),
+			bWasUndone: this._wasUndone( opB ),
+			abRelation: this._getRelation( opA, opB ),
+			baRelation: this._getRelation( opB, opA )
+		};
+	}
 
 	// Returns whether given operation `op` has already been undone.
 	//
 	// This is only used when additional context mode is on (options.useContext == true).
 	//
 	// Information whether an operation was undone gives more context when making a decision when two operations are in conflict.
-	context.wasUndone = function( op ) {
-		// If additional context is not used, just return false.
-		if ( !options.useContext ) {
-			return false;
-		}
-
+	//
+	// @param {module:engine/model/operation/operation~Operation} op
+	// @returns {Boolean}
+	_wasUndone( op ) {
 		// For `op`, get its original operation. After all, if `op` is a clone (or even transformed clone) of another
 		// operation, literally `op` couldn't be undone. It was just generated. If anything, it was the operation it origins
 		// from which was undone. So get that original operation.
-		const originalOp = this.originalOperations.get( op );
+		const originalOp = this._originalOperations.get( op );
 
 		// And check with the document if the original operation was undone.
-		return this.document.history.isUndoneOperation( originalOp );
-	};
+		return this._history.isUndoneOperation( originalOp );
+	}
 
 	// Returns a relation between `opA` and an operation which is undone by `opB`. This can be `String` value if a relation
 	// was set earlier or `null` if there was no relation between those operations.
 	//
 	// This is only used when additional context mode is on (options.useContext == true).
 	//
-	// This is a little tricky to understand, so let's compare it to `wasUndone()`.
+	// This is a little tricky to understand, so let's compare it to `ContextFactory#_wasUndone`.
 	//
 	// When `wasUndone( opB )` is used, we check if the `opB` has already been undone. It is obvious, that the
 	// undoing operation must happen after the undone operation. So, essentially, we have `opB`, we take document history,
@@ -485,7 +553,7 @@ function initializeContext( operationsA, operationsB, options ) {
 	// Relations is a backward process to `wasUndone()`.
 	//
 	// Long story short - using relations is asking what happened in the past. Looking back. This time we have an undoing
-	// operation `opB` which has undone some other operation. When there is a transformation `opA` * `opB` and there is
+	// operation `opB` which has undone some other operation. When there is a transformation `opA` x `opB` and there is
 	// a conflict to solve and `opB` is an undoing operation, we can look back in the history and see what was a relation
 	// between `opA` and the operation which `opB` undone. Basing on that relation from the past, we can now make
 	// a better decision when resolving a conflict between two operations, because we know more about the context of
@@ -493,23 +561,22 @@ function initializeContext( operationsA, operationsB, options ) {
 	//
 	// This is why this function does not return a relation directly between `opA` and `opB` because we need to look
 	// back to search for a meaningful contextual information.
-	context.getRelation = function( opA, opB ) {
-		// If additional context is not used, there is no relation.
-		if ( !options.useContext ) {
-			return null;
-		}
-
+	//
+	// @param {module:engine/model/operation/operation~Operation} opA
+	// @param {module:engine/model/operation/operation~Operation} opB
+	// @returns {String|null}
+	_getRelation( opA, opB ) {
 		// Get the original operation. Similarly as in `wasUndone()` it is used as an universal identifier for stored data.
-		const origB = this.originalOperations.get( opB );
-		const undoneB = this.document.history.getUndoneOperation( origB );
+		const origB = this._originalOperations.get( opB );
+		const undoneB = this._history.getUndoneOperation( origB );
 
 		// If `opB` is not undoing any operation, there is no relation.
 		if ( !undoneB ) {
 			return null;
 		}
 
-		const origA = this.originalOperations.get( opA );
-		const relationsA = this.relations.get( origA );
+		const origA = this._originalOperations.get( opA );
+		const relationsA = this._relations.get( origA );
 
 		// Get all relations for `opA`, and check if there is a relation with `opB`-undone-counterpart. If so, return it.
 		if ( relationsA ) {
@@ -517,148 +584,72 @@ function initializeContext( operationsA, operationsB, options ) {
 		}
 
 		return null;
-	};
-
-	return context;
-}
-
-/**
- * Saves a relation between operations `opA` and `opB`.
- *
- * Relations are then later used to help solve conflicts when operations are transformed.
- *
- * @private
- * @param {module:engine/model/operation/operation~Operation} opA
- * @param {module:engine/model/operation/operation~Operation} opB
- * @param {Object} context
- */
-function updateRelations( opA, opB, context ) {
-	// The use of relations is described in a bigger detail in transformation functions.
-	//
-	// In brief, this function, for specified pairs of operation types, checks how positions defined in those operations relate.
-	// Then those relations are saved. For example, for two move operations, it is saved if one of those operations target
-	// position is before the other operation source position. This kind of information gives contextual information when
-	// transformation is used during undo. Similar checks are done for other pairs of operations.
-	//
-	switch ( opA.constructor ) {
-		case MoveOperation: {
-			switch ( opB.constructor ) {
-				case MergeOperation: {
-					if ( opA.targetPosition.isEqual( opB.sourcePosition ) || opB.movedRange.containsPosition( opA.targetPosition ) ) {
-						setRelation( context, opA, opB, 'insertAtSource' );
-					} else if ( opA.targetPosition.isEqual( opB.deletionPosition ) ) {
-						setRelation( context, opA, opB, 'insertBetween' );
-					}
-
-					break;
-				}
-
-				case MoveOperation: {
-					if ( opA.targetPosition.isEqual( opB.sourcePosition ) || opA.targetPosition.isBefore( opB.sourcePosition ) ) {
-						setRelation( context, opA, opB, 'insertBefore' );
-					} else {
-						setRelation( context, opA, opB, 'insertAfter' );
-					}
-
-					break;
-				}
-
-				case UnwrapOperation: {
-					const isInside = opA.targetPosition.hasSameParentAs( opB.position );
+	}
 
-					if ( isInside ) {
-						setRelation( context, opA, opB, 'insertInside' );
-					}
+	// Helper function for `ContextFactory#updateRelations`.
+	//
+	// @private
+	// @param {module:engine/model/operation/operation~Operation} opA
+	// @param {module:engine/model/operation/operation~Operation} opB
+	// @param {String} relation
+	_setRelation( opA, opB, relation ) {
+		// As always, setting is for original operations, not the clones/transformed operations.
+		const origA = this._originalOperations.get( opA );
+		const origB = this._originalOperations.get( opB );
 
-					break;
-				}
-			}
+		let relationsA = this._relations.get( origA );
 
-			break;
+		if ( !relationsA ) {
+			relationsA = new Map();
+			this._relations.set( origA, relationsA );
 		}
 
-		case SplitOperation: {
-			switch ( opB.constructor ) {
-				case MergeOperation: {
-					if ( opA.position.isBefore( opB.sourcePosition ) ) {
-						setRelation( context, opA, opB, 'splitBefore' );
-					}
-
-					break;
-				}
-
-				case MoveOperation: {
-					if ( opA.position.isEqual( opB.sourcePosition ) || opA.position.isBefore( opB.sourcePosition ) ) {
-						setRelation( context, opA, opB, 'splitBefore' );
-					}
-
-					break;
-				}
-			}
-
-			break;
-		}
+		relationsA.set( origB, relation );
 	}
 }
 
 /**
- * Helper function for {@link module:engine/model/operation/transform~updateRelations}.
+ * Holds additional contextual information about a transformed pair of operations (`a` and `b`). Those information
+ * can be used for better conflict resolving.
  *
- * @private
- * @param {Object} context
- * @param {module:engine/model/operation/operation~Operation} opA
- * @param {module:engine/model/operation/operation~Operation} opB
- * @param {String} relation
+ * @typedef {Object} module:engine/model/operation/transform~TransformationContext
+ *
+ * @property {Boolean} aIsStrong Whether `a` is strong operation in this transformation, or weak.
+ * @property {Boolean} aWasUndone Whether `a` operation was undone.
+ * @property {Boolean} bWasUndone Whether `b` operation was undone.
+ * @property {String|null} abRelation The relation between `a` operation and an operation undone by `b` operation.
+ * @property {String|null} baRelation The relation between `b` operation and an operation undone by `a` operation.
  */
-function setRelation( context, opA, opB, relation ) {
-	// As always, setting is for original operations, not the clones/transformed operations.
-	const origA = context.originalOperations.get( opA );
-	const origB = context.originalOperations.get( opB );
-
-	let relationsA = context.relations.get( origA );
-
-	if ( !relationsA ) {
-		relationsA = new Map();
-		context.relations.set( origA, relationsA );
-	}
-
-	relationsA.set( origB, relation );
-}
 
 /**
- * Rewrites information about original operation to the new operations.
- *
- * Used when `newOps` are generated from `oldOp` (during transformation). It takes `oldOp`'s original operation and
- * sets it as `newOps` original operation.
+ * An utility function that updates {@link module:engine/model/operation/operation~Operation#baseVersion base versions}
+ * of passed operations.
  *
- * It also means that if an operation is broken into multiple during transformation, all those broken "pieces" are pointing
- * to the same operation as their original operation.
+ * The function simply sets `baseVersion` as a base version of the first passed operation and then increments it for
+ * each following operation in `operations`.
  *
  * @private
- * @param {module:engine/model/operation/operation~Operation} oldOp
- * @param {Array.<module:engine/model/operation/operation~Operation>} newOps
- * @param {Object} context
+ * @param {Array.<module:engine/model/operation/operation~Operation>} operations Operations to update.
+ * @param {Number} baseVersion Base version to set for the first operation in `operations`.
  */
-function updateOriginalOperation( oldOp, newOps, context ) {
-	const originalOp = context.originalOperations.get( oldOp );
-
-	for ( const op of newOps ) {
-		context.originalOperations.set( op, originalOp );
+function updateBaseVersions( operations, baseVersion ) {
+	for ( const operation of operations ) {
+		operation.baseVersion = baseVersion++;
 	}
 }
 
 /**
- * Holds additional contextual information about a transformed pair of operations (`a` and `b`). Those information
- * can be used for better conflict resolving.
- *
- * @typedef {Object} module:engine/model/operation/transform~TransformationContext
+ * Adds `howMany` instances of {@link module:engine/model/operation/nooperation~NoOperation} to `operations` set.
  *
- * @property {Boolean} aIsStrong Whether `a` is strong operation in this transformation, or weak.
- * @property {Boolean} aWasUndone Whether `a` operation was undone.
- * @property {Boolean} bWasUndone Whether `b` operation was undone.
- * @property {String|null} abRelation The relation between `a` operation and an operation undone by `b` operation.
- * @property {String|null} baRelation The relation between `b` operation and an operation undone by `a` operation.
+ * @private
+ * @param {Array.<module:engine/model/operation/operation~Operation>} operations
+ * @param {Number} howMany
  */
+function padWithNoOps( operations, howMany ) {
+	for ( let i = 0; i < howMany; i++ ) {
+		operations.push( new NoOperation( 0 ) );
+	}
+}
 
 // -----------------------
 

+ 1 - 0
packages/ckeditor5-engine/tests/model/operation/transform/utils.js

@@ -285,6 +285,7 @@ export function syncClients() {
 			let remoteOperationsTransformed = null;
 
 			const options = {
+				document: localClient.document,
 				useContext: false,
 				padWithNoOps: true
 			};