Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

T/undo 2 #499

Merged
merged 15 commits into from
Jun 24, 2016
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/datacontroller.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ export default class DataController {
const modelRoot = this.model.getRoot( rootName );

this.model.enqueueChanges( () => {
this.model.batch()
// Initial batch should be ignored by features like undo, etc.
this.model.batch( 'ignore' )
.remove( ModelRange.createFromElement( modelRoot ) )
.insert( ModelPosition.createAt( modelRoot, 0 ), this.parse( data ) );
} );
Expand Down
40 changes: 33 additions & 7 deletions src/model/batch.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,27 +35,53 @@ export default class Batch {
* Creates Batch instance. Not recommended to use directly, use {@link engine.model.Document#batch} instead.
*
* @param {engine.model.Document} doc Document which this Batch changes.
* @param {'ignore'|'undo'|'redo'|'default'} [type='default'] Type of the batch.
*/
constructor( doc ) {
constructor( doc, type = 'default' ) {
/**
* Document which this Batch changes.
* Document which this batch changes.
*
* @member {engine.model.Document} engine.model.Batch#doc
* @readonly
* @member {engine.model.Document} engine.model.Batch#doc
*/
this.doc = doc;

/**
* Array of deltas which compose Batch.
* Array of deltas which compose this batch.
*
* @member {Array.<engine.model.delta.Delta>} engine.model.Batch#deltas
* @readonly
* @member {Array.<engine.model.delta.Delta>} engine.model.Batch#deltas
*/
this.deltas = [];

/**
* Type of the batch.
*
* Can be one of the following values:
* * `'default'` - all "normal" batches. Most commonly used type.
* * `'undo'` - batch created by undo command.
* * `'redo'` - batch created by redo command.
* * `'ignore'` - batch that should be ignored by other features.
*
* @readonly
* @member {'ignore'|'undo'|'redo'|'default'} engine.model.Batch#type
*/
this.type = type;
}

/**
* Returns this batch base version, which is equal to the base version of first delta in the batch.
* If there are no deltas in the batch, it returns `null`.
*
* @readonly
* @type {Number|null}
*/
get baseVersion() {
return this.deltas.length > 0 ? this.deltas[ 0 ].baseVersion : null;
}

/**
* Adds delta to the Batch instance. All modification methods (insert, remove, split, etc.) use this method
* Adds delta to the batch instance. All modification methods (insert, remove, split, etc.) use this method
* to add created deltas.
*
* @param {engine.model.delta.Delta} delta Delta to add.
Expand All @@ -81,7 +107,7 @@ export default class Batch {
}

/**
* Function to register Batch methods. To make code scalable Batch do not have modification
* Function to register batch methods. To make code scalable Batch do not have modification
* methods built in. They can be registered using this method.
*
* This method checks if there is no naming collision and throws `batch-register-taken` if the method name
Expand Down
150 changes: 150 additions & 0 deletions src/model/compressedhistory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/**
* @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 its base version accordingly.
*
* @see engine.model.History
* @memberOf engine.model
*/
export default class CompressedHistory extends History {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CompressedHistory has nothing to do with compression. Also I do not see any reason to keep CompressedHistory and History separated.

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;
}

/**
* Removes delta from the history. This happens i.e., when a delta is undone by another delta. Both undone delta and
* undoing delta should be removed so they won't have an 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 removed, deltas between them should probably get updated. See
* {@link engine.model.CompressedHistory#updateDelta}.
*
* **Note:** if delta with `baseVersion` got {@link engine.model.CompressedHistory#updateDelta updated} by multiple
* deltas, all updated deltas will be removed.
*
* @param {Number} baseVersion Base version of a delta to be removed.
*/
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=0] Start of base versions range to check.
* @param {Number} [to=Number.POSITIVE_INFINITY] End of base versions range to check.
* @returns {Iterator.<Number>} Base versions of deltas marked as reversed.
*/
*getInactiveBaseVersions( from = 0, to = Number.POSITIVE_INFINITY ) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something not bangla here.

for ( let baseVersion of this._inactiveBaseVersions ) {
if ( baseVersion >= from && baseVersion < to ) {
yield baseVersion;
}
}
}

/**
* Updates {@link engine.model.History#_historyPoints} structure.
*
* @private
* @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.
*/
_updateHistoryPointsAfter( baseVersion, changeBy ) {
for ( let key of this._historyPoints.keys() ) {
if ( key > baseVersion ) {
this._historyPoints.set( key, this._historyPoints.get( key ) + changeBy );
}
}
}
}
11 changes: 10 additions & 1 deletion src/model/delta/basic-transformations.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,10 +230,19 @@ addTransformationCase( UnwrapDelta, SplitDelta, ( a, b, isStrong ) => {
// If incoming unwrap delta tries to unwrap node that got split we should unwrap the original node and the split copy.
// This can be achieved either by reverting split and applying unwrap to singular node, or creating additional unwrap delta.
if ( compareArrays( a.position.path, b.position.getParentPath() ) === 'SAME' ) {
return [
const transformed = [
b.getReversed(),
a.clone()
];

// It's a kind of magic-magic-magic-maaaaagiiic!
transformed[ 1 ].operations[ 1 ].targetPosition.path[ 0 ]++;
// But seriously, we have to fix RemoveOperation in the second delta because reversed UnwrapDelta creates
// MergeDelta which also has RemoveOperation. Those two operations cannot point to the same "holder" element
// in the graveyard, so we fix it by hand. This is the only case where it happens in "special" transformation
// cases, and it won't happen for "default" transformation apart of RemoveDelta, where it is okay.

return transformed;
}

return defaultTransform( a, b, isStrong );
Expand Down
19 changes: 13 additions & 6 deletions src/model/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 );
}

/**
Expand Down Expand Up @@ -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.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can hide an error this way. Silent exit is never a good solution. If you want to be save throw error.

this.history.addDelta( operation.delta );
}

const batch = operation.delta && operation.delta.batch;

Expand All @@ -172,10 +178,11 @@ export default class Document {
/**
* Creates a {@link engine.model.Batch} instance which allows to change the document.
*
* @param {String} [type] Batch type. See {@link engine.model.Batch#type}.
* @returns {engine.model.Batch} Batch instance.
*/
batch() {
return new Batch( this );
batch( type ) {
return new Batch( this, type );
}

/**
Expand Down
Loading