Esanders has uploaded a new change for review.
https://gerrit.wikimedia.org/r/125923
Change subject: Add system to dm.Surface for staging changes
......................................................................
Add system to dm.Surface for staging changes
Pushing to the staging state stack causes all transactions
to be stored in a list at the top of the stack. Popping the stack
the stack undoes those transactions and returns them. Applying
moves the transactions down the stack or into the undo stack.
Change-Id: I607d37591eb3e239a59047be0472627d48d267b1
---
M modules/ve/dm/ve.dm.Surface.js
M modules/ve/test/dm/ve.dm.Surface.test.js
M modules/ve/ui/tools/ve.ui.HistoryTool.js
3 files changed, 243 insertions(+), 24 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/VisualEditor/VisualEditor
refs/changes/23/125923/1
diff --git a/modules/ve/dm/ve.dm.Surface.js b/modules/ve/dm/ve.dm.Surface.js
index b4fcbcb..08d4a59 100644
--- a/modules/ve/dm/ve.dm.Surface.js
+++ b/modules/ve/dm/ve.dm.Surface.js
@@ -21,9 +21,10 @@
// Properties
this.documentModel = doc;
this.metaList = new ve.dm.MetaList( this );
- this.selection = new ve.Range( 1, 1 );
+ this.selection = new ve.Range( 1 );
this.selectedNodes = {};
this.newTransactions = [];
+ this.stagingStack = [];
this.undoStack = [];
this.undoIndex = 0;
this.historyTrackingInterval = null;
@@ -77,20 +78,24 @@
* Disable changes.
*
* @method
+ * @fires history
*/
ve.dm.Surface.prototype.disable = function () {
this.stopHistoryTracking();
this.enabled = false;
+ this.emit( 'history' );
};
/**
* Enable changes.
*
* @method
+ * @fires history
*/
ve.dm.Surface.prototype.enable = function () {
this.enabled = true;
this.startHistoryTracking();
+ this.emit( 'history' );
};
/**
@@ -148,6 +153,125 @@
return this.undoStack.slice( 0 ).concat( [{ 'transactions':
this.newTransactions.slice( 0 ) }] );
} else {
return this.undoStack.slice( 0 );
+ }
+};
+
+/**
+ * If the surface in staging mode.
+ *
+ * @returns {boolean} The surface in staging mode
+ */
+ve.dm.Surface.prototype.isStaging = function () {
+ return this.stagingStack.length > 0;
+};
+
+/**
+ * Get the staging transactions at the current staging stack depth
+ *
+ * The array is returned by reference so it can be pushed to.
+ *
+ * @returns {ve.dm.Transactions[]|undefined} Staging transactions, or
undefined if not staging
+ */
+ve.dm.Surface.prototype.getStagingTransactions = function () {
+ return this.stagingStack[this.stagingStack.length - 1];
+};
+
+/**
+ * Push another level of staging to the staging stack
+ *
+ * @fires history
+ */
+ve.dm.Surface.prototype.pushStaging = function () {
+ // If we're starting staging stop history tracking
+ if ( !this.isStaging() ) {
+ // Set a breakpoint to make sure newTransactions is clear
+ this.breakpoint();
+ this.stopHistoryTracking();
+ this.emit( 'history' );
+ }
+ this.stagingStack.push( [] );
+};
+
+/**
+ * Pop a level of staging from the staging stack
+ *
+ * @fires history
+ * @returns {ve.dm.Transactions[]|undefined} Staging transactions, or
undefined if not staging
+ */
+ve.dm.Surface.prototype.popStaging = function () {
+ if ( !this.isStaging() ) {
+ return;
+ }
+
+ var i, transaction,
+ reverseTransactions = [],
+ transactions = this.stagingStack.pop();
+
+ // Not applying, so rollback transactions
+ for ( i = transactions.length - 1; i >= 0; i-- ) {
+ transaction = transactions[i].reversed();
+ reverseTransactions.push( transaction );
+ }
+ this.changeInternal( reverseTransactions, undefined, true );
+
+ if ( !this.isStaging() ) {
+ this.startHistoryTracking();
+ this.emit( 'history' );
+ }
+
+ return transactions;
+};
+
+/**
+ * Apply a level of staging from the staging stack
+ *
+ * @fires history
+ */
+ve.dm.Surface.prototype.applyStaging = function () {
+ if ( !this.isStaging() ) {
+ return;
+ }
+
+ var transactions = this.stagingStack.pop();
+
+ if ( this.isStaging() ) {
+ // Move transactions to the next item down in the staging stack
+ Array.prototype.push.apply( this.getStagingTransactions(),
transactions );
+ } else {
+ // Move transactions to the undo stack
+ this.newTransactions = transactions;
+ this.breakpoint();
+ }
+
+ if ( !this.isStaging() ) {
+ this.startHistoryTracking();
+ this.emit( 'history' );
+ }
+};
+
+/**
+ * Pop the staging stack until empty
+ *
+ * @returns {ve.dm.Transactions[]|undefined} Staging transactions, or
undefined if not staging
+ */
+ve.dm.Surface.prototype.popAllStaging = function () {
+ if ( !this.isStaging() ) {
+ return;
+ }
+
+ var transactions = [];
+ while ( this.isStaging() ) {
+ ve.batchSplice( transactions, 0, 0, this.popStaging() );
+ }
+ return transactions;
+};
+
+/**
+ * Apply the staging stack until empty
+ */
+ve.dm.Surface.prototype.applyAllStaging = function () {
+ while ( this.isStaging() ) {
+ this.applyStaging();
}
};
@@ -230,13 +354,23 @@
};
/**
- * Check if there is a state to redo.
+ * Check if redo is allowed in the current state.
*
* @method
- * @returns {boolean} Has a future state
+ * @returns {boolean} Can redo
*/
-ve.dm.Surface.prototype.hasFutureState = function () {
- return this.undoIndex > 0;
+ve.dm.Surface.prototype.canRedo = function () {
+ return this.undoIndex > 0 && this.enabled && !this.isStaging();
+};
+
+/**
+ * Check if undo is allowed in the current state.
+ *
+ * @method
+ * @returns {boolean} Can undo
+ */
+ve.dm.Surface.prototype.canUndo = function () {
+ return this.hasBeenModified() && this.enabled && !this.isStaging();
};
/**
@@ -245,7 +379,7 @@
* @method
* @returns {boolean} Has a past state
*/
-ve.dm.Surface.prototype.hasPastState = function () {
+ve.dm.Surface.prototype.hasBeenModified = function () {
return this.undoStack.length - this.undoIndex > 0 ||
!!this.newTransactions.length;
};
@@ -493,11 +627,15 @@
for ( i = 0, len = transactions.length; i < len; i++ ) {
if ( !transactions[i].isNoOp() ) {
if ( !skipUndoStack ) {
- this.truncateUndoStack();
- if ( !this.newTransactions.length ) {
- this.selectionBefore =
selectionBefore;
+ if ( this.isStaging() ) {
+
this.getStagingTransactions().push( transactions[i] );
+ } else {
+ this.truncateUndoStack();
+ if (
!this.newTransactions.length ) {
+ this.selectionBefore =
selectionBefore;
+ }
+ this.newTransactions.push(
transactions[i] );
}
- this.newTransactions.push(
transactions[i] );
}
// The .commit() call below indirectly invokes
setSelection()
this.documentModel.commit( transactions[i] );
@@ -567,7 +705,7 @@
*/
ve.dm.Surface.prototype.undo = function () {
var i, item, transaction, transactions = [];
- if ( !this.enabled || !this.hasPastState() ) {
+ if ( !this.canUndo() ) {
return;
}
@@ -594,7 +732,7 @@
*/
ve.dm.Surface.prototype.redo = function () {
var item;
- if ( !this.enabled || !this.hasFutureState() ) {
+ if ( !this.canRedo() ) {
return;
}
diff --git a/modules/ve/test/dm/ve.dm.Surface.test.js
b/modules/ve/test/dm/ve.dm.Surface.test.js
index 77daa72..f70bbce 100644
--- a/modules/ve/test/dm/ve.dm.Surface.test.js
+++ b/modules/ve/test/dm/ve.dm.Surface.test.js
@@ -8,14 +8,15 @@
QUnit.module( 've.dm.Surface' );
ve.dm.SurfaceStub = function VeDmSurfaceStub( data ) {
+ var doc;
if ( data !== undefined ) {
- this.dm = new ve.dm.Document( data );
+ doc = new ve.dm.Document( data );
} else {
- this.dm = new ve.dm.Document( [{ 'type': 'paragraph' }, 'h',
'i', { 'type': '/paragraph' }] );
+ doc = new ve.dm.Document( [{ 'type': 'paragraph' }, 'h', 'i', {
'type': '/paragraph' }] );
}
// Inheritance
- ve.dm.Surface.call( this, this.dm );
+ ve.dm.Surface.call( this, doc );
};
OO.inheritClass( ve.dm.SurfaceStub, ve.dm.Surface );
@@ -33,14 +34,14 @@
} );
QUnit.test( 'change/setSelection events', 3, function ( assert ) {
- var tx, surface = new ve.dm.SurfaceStub(),
+ var surface = new ve.dm.SurfaceStub(),
+ doc = surface.getDocument(),
+ // docmentUpdate doesn't fire for no-op transactions, so make
sure there's something there
+ tx = ve.dm.Transaction.newFromInsertion( doc, 3, [ 'i' ] ),
events = {
'documentUpdate': 0,
'select': 0
};
-
- // docmentUpdate doesn't fire for no-op transactions, so make sure
there's something there
- tx = ve.dm.Transaction.newFromInsertion( surface.getDocument(), 3, [
'i' ] );
surface.on( 'documentUpdate', function () {
events.documentUpdate++;
@@ -58,7 +59,8 @@
QUnit.test( 'breakpoint', 7, function ( assert ) {
var surface = new ve.dm.SurfaceStub(),
- tx = new ve.dm.Transaction.newFromInsertion( surface.dm, 1,
['x'] ),
+ doc = surface.getDocument(),
+ tx = new ve.dm.Transaction.newFromInsertion( doc, 1, ['x'] ),
selection = new ve.Range( 1, 1 );
assert.equal( surface.breakpoint(), false, 'Returns false if no
transactions applied' );
@@ -80,10 +82,89 @@
assert.deepEqual( surface.newTransactions, [], 'New transactions match
after breakpoint' );
} );
+QUnit.test( 'staging', 23, function ( assert ) {
+ var tx1, tx2,
+ surface = new ve.dm.SurfaceStub(),
+ fragment = surface.getFragment( new ve.Range( 1, 3 ) ),
+ doc = surface.getDocument();
+
+ assert.equal( surface.isStaging(), false, 'isStaging false when not
staging' );
+ assert.equal( surface.getStagingTransactions(), undefined,
'getStagingTransactions undefined when not staging' );
+
+ surface.change( new ve.dm.Transaction.newFromInsertion( doc, 1, ['a'] )
);
+
+ surface.pushStaging();
+ assert.equal( surface.isStaging(), true, 'isStaging true after
pushStaging' );
+ assert.deepEqual( surface.getStagingTransactions(), [],
'getStagingTransactions empty array after pushStaging' );
+
+ tx1 = new ve.dm.Transaction.newFromInsertion( doc, 2, ['b'] );
+ surface.change( tx1 );
+
+ assert.equal( fragment.getText(), 'abhi', 'document contents match
after first transaction' );
+ assert.deepEqual( surface.getStagingTransactions(), [tx1],
'getStagingTransactions contains first transaction after change' );
+
+ surface.pushStaging();
+ assert.equal( surface.isStaging(), true, 'isStaging true after nested
pushStaging' );
+ assert.deepEqual( surface.getStagingTransactions(), [],
'getStagingTransactions empty array after nested pushStaging' );
+
+ tx2 = new ve.dm.Transaction.newFromInsertion( doc, 3, ['c'] );
+ surface.change( tx2 );
+
+ assert.equal( fragment.getText(), 'abchi', 'document contents match
after second transaction' );
+ assert.deepEqual( surface.getStagingTransactions(), [tx2],
'getStagingTransactions contains second transaction after change in nested
staging' );
+
+ assert.deepEqual( surface.popStaging(), [tx2], 'popStaging returns
second transaction list' );
+ assert.equal( surface.isStaging(), true, 'isStaging true after nested
popStaging' );
+ assert.equal( fragment.getText(), 'abhi', 'document contents match
after nested popStaging' );
+
+ assert.deepEqual( surface.popStaging(), [tx1], 'popStaging returns
first transaction list' );
+ assert.equal( surface.isStaging(), false, 'isStaging false after outer
popStaging' );
+ assert.equal( fragment.getText(), 'ahi', 'document contents match after
outer popStaging' );
+
+ surface.pushStaging();
+ tx1 = new ve.dm.Transaction.newFromInsertion( doc, 2, ['b'] );
+ surface.change( tx1 );
+
+ surface.pushStaging();
+ tx2 = new ve.dm.Transaction.newFromInsertion( doc, 3, ['c'] );
+ surface.change( tx2 );
+
+ assert.deepEqual( surface.popAllStaging(), [tx1, tx2], 'popAllStaging
returns full transaction list' );
+ assert.equal( fragment.getText(), 'ahi', 'document contents match after
outer clearStaging' );
+
+ surface.pushStaging();
+ tx1 = new ve.dm.Transaction.newFromInsertion( doc, 2, ['b'] );
+ surface.change( tx1 );
+
+ surface.pushStaging();
+ tx2 = new ve.dm.Transaction.newFromInsertion( doc, 3, ['c'] );
+ surface.change( tx2 );
+
+ surface.applyStaging();
+ assert.deepEqual( surface.getStagingTransactions(), [tx1, tx2],
'applyStaging merges transactions' );
+
+ surface.applyStaging();
+ assert.equal( surface.isStaging(), false, 'isStaging false after outer
applyStaging' );
+ assert.equal( fragment.getText(), 'abchi', 'document contents changed
after applyStaging' );
+
+ surface.pushStaging();
+ tx1 = new ve.dm.Transaction.newFromInsertion( doc, 4, ['d'] );
+ surface.change( tx1 );
+
+ surface.pushStaging();
+ tx2 = new ve.dm.Transaction.newFromInsertion( doc, 5, ['e'] );
+ surface.change( tx2 );
+
+ surface.applyAllStaging();
+ assert.equal( surface.isStaging(), false, 'isStaging false after outer
applyAllStaging' );
+ assert.equal( fragment.getText(), 'abcdehi', 'document contents changed
after applyAllStaging' );
+
+} );
+
// TODO: ve.dm.Surface#getHistory
// TODO: ve.dm.Surface#purgeHistory
-// TODO: ve.dm.Surface#hasFutureState
-// TODO: ve.dm.Surface#hasPastState
+// TODO: ve.dm.Surface#canRedo
+// TODO: ve.dm.Surface#canUndo
// TODO: ve.dm.Surface#truncateUndoStack
// TODO: ve.dm.Surface#undo
// TODO: ve.dm.Surface#redo
diff --git a/modules/ve/ui/tools/ve.ui.HistoryTool.js
b/modules/ve/ui/tools/ve.ui.HistoryTool.js
index 2e7cef7..e0ff818 100644
--- a/modules/ve/ui/tools/ve.ui.HistoryTool.js
+++ b/modules/ve/ui/tools/ve.ui.HistoryTool.js
@@ -79,7 +79,7 @@
ve.ui.UndoHistoryTool.static.icon = 'undo';
ve.ui.UndoHistoryTool.static.title =
OO.ui.deferMsg( 'visualeditor-historybutton-undo-tooltip' );
-ve.ui.UndoHistoryTool.static.check = 'hasPastState';
+ve.ui.UndoHistoryTool.static.check = 'canUndo';
ve.ui.UndoHistoryTool.static.commandName = 'undo';
ve.ui.toolFactory.register( ve.ui.UndoHistoryTool );
@@ -101,6 +101,6 @@
ve.ui.RedoHistoryTool.static.icon = 'redo';
ve.ui.RedoHistoryTool.static.title =
OO.ui.deferMsg( 'visualeditor-historybutton-redo-tooltip' );
-ve.ui.RedoHistoryTool.static.check = 'hasFutureState';
+ve.ui.RedoHistoryTool.static.check = 'canRedo';
ve.ui.RedoHistoryTool.static.commandName = 'redo';
ve.ui.toolFactory.register( ve.ui.RedoHistoryTool );
--
To view, visit https://gerrit.wikimedia.org/r/125923
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I607d37591eb3e239a59047be0472627d48d267b1
Gerrit-PatchSet: 1
Gerrit-Project: VisualEditor/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Esanders <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits