jenkins-bot has submitted this change and it was merged.

Change subject: Rewrite MetaList.onTransact
......................................................................


Rewrite MetaList.onTransact

The previous implementation couldn't deal with transactions that
replaced both data and metadata at the same time (rather than replacing
data while moving metadata around), and extending its approach to
deal with that case would have made it much more complex.

So I rewrote the algorithm from scratch. The previous implementation
scheduled deferred moves for existing items, but immediately processed
insertions and removals. This is problematic for replacements and
maintaining the order in the binary search list. So instead, this new
implementation builds an array representing what the new item list
should be, then processes insertions, removals and moves in the correct
order to achieve that state.

It looks like the previous implementation didn't always work correctly,
which was masked because the test suite passed full=false to
assertItemsMatchMetadata(). This rewrite fixes this.

Also remove setMove/applyMove from MetaItem, because we don't need them
anymore and they're evil anyway; and add isAttached(), because the new
algorithm needs it.

Change-Id: I899d2b3c94c2cfa55823879bca95456750f64382
---
M modules/ve/dm/ve.dm.MetaItem.js
M modules/ve/dm/ve.dm.MetaList.js
M modules/ve/test/dm/ve.dm.MetaList.test.js
3 files changed, 179 insertions(+), 127 deletions(-)

Approvals:
  Esanders: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/modules/ve/dm/ve.dm.MetaItem.js b/modules/ve/dm/ve.dm.MetaItem.js
index 86a60ce..91657c9 100644
--- a/modules/ve/dm/ve.dm.MetaItem.js
+++ b/modules/ve/dm/ve.dm.MetaItem.js
@@ -129,39 +129,6 @@
 };
 
 /**
- * Queue up a change to the item's offset and index.
- * @param {number} offset New offset
- * @param {number} index New index
- */
-ve.dm.MetaItem.prototype.setMove = function ( offset, index ) {
-       this.move = {
-               'offset': offset,
-               'index': index
-       };
-};
-
-/**
- * Whether or not a move is pending.
- * @returns {boolean} A move is pending
- */
-ve.dm.MetaItem.prototype.isMovePending = function () {
-       return this.move !== null;
-};
-
-/**
- * Apply the pending move and clear it.
- * @throws No move pending
- */
-ve.dm.MetaItem.prototype.applyMove = function () {
-       if ( this.move === null ) {
-               throw new Error( 'No move pending' );
-       }
-       this.setOffset( this.move.offset );
-       this.setIndex( this.move.index );
-       this.move = null;
-};
-
-/**
  * Attach this item to a MetaList.
  * @param {ve.dm.MetaList} list Parent list to attach to
  * @param {number} offset Offset of this item in the parent list's document
@@ -187,3 +154,11 @@
                this.index = null;
        }
 };
+
+/**
+ * Check whether this item is attached to a MetaList.
+ * @returns {boolean} Whether item is attached
+ */
+ve.dm.MetaItem.prototype.isAttached = function () {
+       return this.list !== null;
+};
diff --git a/modules/ve/dm/ve.dm.MetaList.js b/modules/ve/dm/ve.dm.MetaList.js
index 4096de4..e4c0ba6 100644
--- a/modules/ve/dm/ve.dm.MetaList.js
+++ b/modules/ve/dm/ve.dm.MetaList.js
@@ -80,122 +80,183 @@
  * @emits remove
  */
 ve.dm.MetaList.prototype.onTransact = function ( tx, reversed ) {
-       var i, j, k, ilen, jlen, klen, ins, rm, itemIndex, item,
-               offset = 0, newOffset = 0, index = 0, ops = tx.getOperations(),
-               insertedItems = [], removedItems = [];
-       // Look for replaceMetadata operations in the transaction and 
insert/remove items as appropriate
-       // This requires we also inspect retain, replace and replaceMetadata 
operations in order to
-       // track the offset and index. We track the pre-transaction offset, we 
need to do that in
-       // order to remove items correctly. This also means inserted items are 
initially at the wrong
-       // offset, but we translate it later.
+       var i, ilen, j, jlen, k, klen, item, ins, rm, insMeta, rmMeta,
+               numItems = this.items.length,
+               itemIndex = 0, // Current index into this.items
+               offset = 0, // Current pre-transaction offset
+               newOffset = 0, // Current post-transaction offset
+               index = 0, // Current pre-transaction index
+               newIndex = 0, // Current post-transaction index
+               // Array of items that should appear in this.items after we're 
done. This includes newly
+               // inserted items as well as existing items that aren't being 
removed.
+               // [ { item: ve.dm.MetaItem, offset: offset to move to, index: 
index to move to } ]
+               newItems = [],
+               removedItems = [], // Array of items that should be removed 
from this.items
+               events = [], // Array of events that we should emit when we're 
done
+               ops = tx.getOperations();
+
+       // Go through the transaction operations and plan out where to add, 
remove and move items. We
+       // don't actually touch this.items yet, otherwise we 1) get it out of 
order which breaks
+       // findItem() and 2) lose information about what the pre-transaction 
state of this.items was.
        for ( i = 0, ilen = ops.length; i < ilen; i++ ) {
                switch ( ops[i].type ) {
                        case 'retain':
+                               // Advance itemIndex through the retain and 
update items we encounter along the way
+                               for ( ;
+                                       itemIndex < numItems && 
this.items[itemIndex].offset < offset + ops[i].length;
+                                       itemIndex++
+                               ) {
+                                       // Plan to move this item to the 
post-transaction offset and index
+                                       newItems.push( {
+                                               'item': this.items[itemIndex],
+                                               'offset': 
this.items[itemIndex].offset + newOffset - offset,
+                                               'index': 
this.items[itemIndex].offset === offset ?
+                                                       // Adjust index for 
insertions or removals that happened at this offset
+                                                       newIndex - index + 
this.items[itemIndex].index :
+                                                       // Offset is retained 
over completely, don't adjust index
+                                                       
this.items[itemIndex].index } );
+                               }
+
                                offset += ops[i].length;
                                newOffset += ops[i].length;
                                index = 0;
+                               newIndex = 0;
                                break;
+
+                       case 'retainMetadata':
+                               // Advance itemIndex through the retain and 
update items we encounter along the way
+                               for ( ;
+                                       itemIndex < numItems && 
this.items[itemIndex].offset === offset &&
+                                               this.items[itemIndex].index < 
index + ops[i].length;
+                                       itemIndex++
+                               ) {
+                                       newItems.push( {
+                                               'item': this.items[itemIndex],
+                                               'offset': newOffset,
+                                               'index': 
this.items[itemIndex].index + newIndex - index
+                                       } );
+                               }
+
+                               index += ops[i].length;
+                               newIndex += ops[i].length;
+                               break;
+
                        case 'replace':
-                               // if we have metadata replace info we can 
calculate the new
-                               // offset and index directly
-                               ins = reversed ? ops[i].removeMetadata : 
ops[i].insertMetadata;
-                               rm = reversed ? ops[i].insertMetadata : 
ops[i].removeMetadata;
-                               if ( rm === undefined ) {
-                                       // no impact on metadata.
-                                       /* jshint noempty: false */
-                               } else if ( ins.length === 0 ) {
-                                       for ( j = 0, jlen = rm.length; j < 
jlen; j++ ) {
-                                               if ( rm[j] !== undefined ) {
-                                                       for ( k = 0, klen = 
rm[j].length; k < klen; k++ ) {
-                                                               item = 
this.deleteRemovedItem( offset + j, k );
-                                                               
removedItems.push( { 'item': item, 'offset': offset + j, 'index': k } );
-                                                       }
-                                               }
+                               ins = reversed ? ops[i].remove : ops[i].insert;
+                               rm = reversed ? ops[i].insert : ops[i].remove;
+                               if ( ops[i].removeMetadata !== undefined ) {
+                                       insMeta = reversed ? 
ops[i].removeMetadata : ops[i].insertMetadata;
+                                       rmMeta = reversed ? 
ops[i].insertMetadata : ops[i].removeMetadata;
+
+                                       // Process removed metadata
+                                       for ( ;
+                                               itemIndex < numItems &&
+                                                       
this.items[itemIndex].offset < offset + rmMeta.length;
+                                               itemIndex++
+                                       ) {
+                                               removedItems.push( 
this.items[itemIndex] );
                                        }
-                               } else if ( rm.length === 0 ) {
-                                       itemIndex = -Math.pow(2, 53); // INT_MIN
-                                       for ( j = 0, jlen = ins.length; j < 
jlen; j++ ) {
-                                               if ( ins[j] !== undefined ) {
-                                                       for ( k = 0, klen = 
ins[j].length; k < klen; k++ ) {
-                                                               item = 
ve.dm.metaItemFactory.createFromElement( ins[j][k] );
-                                                               // offset is 
pre-transaction, but we'll fix it up w/ setMove
-                                                               
this.addInsertedItem( offset, itemIndex++, item );
-                                                               item.setMove( 
newOffset + j, k );
-                                                               
insertedItems.push( { 'item': item } );
+
+                                       // Process inserted metadata
+                                       for ( j = 0, jlen = insMeta.length; j < 
jlen; j++ ) {
+                                               if ( insMeta[j] ) {
+                                                       for ( k = 0, klen = 
insMeta[j].length; k < klen; k++ ) {
+                                                               item = 
ve.dm.metaItemFactory.createFromElement( insMeta[j][k] );
+                                                               newItems.push( {
+                                                                       'item': 
item,
+                                                                       
'offset': newOffset + j,
+                                                                       
'index': k
+                                                               } );
                                                        }
                                                }
                                        }
                                } else {
-                                       // find the first itemIndex - the rest 
should be in order after it
-                                       for ( j = 0, jlen = rm.length; j < 
jlen; j++ ) {
-                                               if ( rm[j] !== undefined ) {
-                                                       itemIndex = 
this.findItem( offset + j, 0 );
-                                                       break;
-                                               }
-                                       }
-                                       // iterate through all the inserted 
metaItems
-                                       for ( j = 0, jlen = ins.length; j < 
jlen; j++ ) {
-                                               item = ins[j];
-                                               if ( item !== undefined ) {
-                                                       for ( k = 0, klen = 
item.length; k < klen; k++ ) {
-                                                               // Queue up the 
move for later so we don't break the metaItem ordering
-                                                               
this.items[itemIndex].setMove( newOffset + j, k );
-                                                               itemIndex++;
-                                                       }
-                                               }
+                                       // No metadata handling specified, 
which means we just have to deal with offset
+                                       // adjustments, same as a retain
+                                       for ( ;
+                                                       itemIndex < numItems &&
+                                                               
this.items[itemIndex].offset < offset + rm.length;
+                                                       itemIndex++
+                                       ) {
+                                               newItems.push( {
+                                                       'item': 
this.items[itemIndex],
+                                                       'offset': 
this.items[itemIndex].offset + newOffset - offset,
+                                                       'index': 
this.items[itemIndex].index
+                                               } );
                                        }
                                }
-                               offset += reversed ? ops[i].insert.length : 
ops[i].remove.length;
-                               newOffset += reversed ? ops[i].remove.length : 
ops[i].insert.length;
-                               index = 0;
+
+                               offset += rm.length;
+                               newOffset += ins.length;
                                break;
-                       case 'retainMetadata':
-                               index += ops[i].length;
-                               break;
+
                        case 'replaceMetadata':
-                               ins = reversed ? ops[i].remove : ops[i].insert;
-                               rm = reversed ? ops[i].insert : ops[i].remove;
-                               for ( j = 0, jlen = rm.length; j < jlen; j++ ) {
-                                       item = this.deleteRemovedItem( offset, 
index + j );
-                                       removedItems.push( { 'item': item, 
'offset': offset, 'index': index } );
+                               insMeta = reversed ? ops[i].remove : 
ops[i].insert;
+                               rmMeta = reversed ? ops[i].insert : 
ops[i].remove;
+
+                               // Process removed items
+                               for ( ;
+                                       itemIndex < numItems && 
this.items[itemIndex].offset === offset &&
+                                               this.items[itemIndex].index < 
index + rmMeta.length;
+                                       itemIndex++
+                               ) {
+                                       removedItems.push( 
this.items[itemIndex] );
                                }
-                               for ( j = 0, jlen = ins.length; j < jlen; j++ ) 
{
-                                       item = 
ve.dm.metaItemFactory.createFromElement( ins[j] );
-                                       // offset and index are 
pre-transaction, but we'll fix them later
-                                       this.addInsertedItem( offset, index + 
j, item );
-                                       insertedItems.push( { 'item': item } );
+
+                               // Process inserted items
+                               for ( j = 0, jlen = insMeta.length; j < jlen; 
j++ ) {
+                                       item = 
ve.dm.metaItemFactory.createFromElement( insMeta[j] );
+                                       newItems.push( { 'item': item, 
'offset': newOffset, 'index': newIndex + j } );
                                }
-                               index += rm.length;
+
+                               index += rmMeta.length;
+                               newIndex += insMeta.length;
                                break;
                }
        }
-
-       // Translate the offsets of all items, and reindex them too
-       // Reindexing is simple because the above ensures the items are already 
in the right order
-       offset = -1;
-       index = 0;
-       for ( i = 0, ilen = this.items.length; i < ilen; i++ ) {
-               if ( this.items[i].isMovePending() ) {
-                       // move was calculated from metadata replace info, just 
apply it
-                       this.items[i].applyMove();
-               } else {
-                       // otherwise calculate the new offset from the 
transaction
-                       this.items[i].setOffset( tx.translateOffset( 
this.items[i].getOffset(), reversed ) );
-                       if ( this.items[i].getOffset() === offset ) {
-                               index++;
-                       } else {
-                               index = 0;
-                       }
-                       this.items[i].setIndex( index );
-               }
-               offset = this.items[i].getOffset();
+       // Update the remaining items that the transaction didn't touch or 
retain over
+       for ( ; itemIndex < numItems; itemIndex++ ) {
+               newItems.push( {
+                       'item': this.items[itemIndex],
+                       'offset': this.items[itemIndex].offset + newOffset - 
offset,
+                       'index': this.items[itemIndex].offset === offset ?
+                               newIndex - index + this.items[itemIndex].index :
+                               this.items[itemIndex].index
+               } );
        }
 
-       for ( i = 0, ilen = insertedItems.length; i < ilen; i++ ) {
-               this.emit( 'insert', insertedItems[i].item );
-       }
+       // Process the changes, and queue up events. We emit the events at the 
end when the MetaList
+       // is back in a consistent state
+
+       // Remove removed items
        for ( i = 0, ilen = removedItems.length; i < ilen; i++ ) {
-               this.emit( 'remove', removedItems[i].item, 
removedItems[i].offset, removedItems[i].index );
+               this.deleteRemovedItem( removedItems[i].offset, 
removedItems[i].index );
+               events.push( [
+                       'remove', removedItems[i].item, removedItems[i].offset, 
removedItems[i].index
+               ] );
+       }
+
+       // Move moved items (these appear as inserted items that are already 
attached)
+       for ( i = 0, ilen = newItems.length; i < ilen; i++ ) {
+               if ( newItems[i].item.isAttached() ) {
+                       if ( newItems[i].offset !== newItems[i].item.offset || 
newItems[i].index !== newItems[i].item.index ) {
+                               this.deleteRemovedItem( 
newItems[i].item.offset, newItems[i].item.index );
+                               this.addInsertedItem( newItems[i].offset, 
newItems[i].index, newItems[i].item );
+                       }
+               }
+       }
+
+       // Insert new items
+       for ( i = 0, ilen = newItems.length; i < ilen; i++ ) {
+               if ( !newItems[i].item.isAttached() ) {
+                       this.addInsertedItem( newItems[i].offset, 
newItems[i].index, newItems[i].item );
+                       events.push( [ 'insert', newItems[i].item ] );
+               }
+       }
+
+       // Emit events
+       for ( i = 0, ilen = events.length; i < ilen; i++ ) {
+               this.emit.apply( this, events[i] );
        }
 };
 
diff --git a/modules/ve/test/dm/ve.dm.MetaList.test.js 
b/modules/ve/test/dm/ve.dm.MetaList.test.js
index cc8480d..4c9214e 100644
--- a/modules/ve/test/dm/ve.dm.MetaList.test.js
+++ b/modules/ve/test/dm/ve.dm.MetaList.test.js
@@ -56,6 +56,22 @@
                                'msg': 'Transaction inserting, replacing and 
removing text'
                        },
                        {
+                               'calls': [
+                                       [ 'pushRetain', 1 ],
+                                       [
+                                               'pushReplace', doc, 1, 9,
+                                               [ 'f', 'O', 'O', 'b', 'A', 'R', 
'b', 'A', 'Z' ],
+                                               [
+                                                       undefined,
+                                                       [ 
ve.dm.example.withMetaMetaData[9][0], ve.dm.example.withMetaMetaData[7][0] ],
+                                                       undefined, undefined, 
undefined, [ ve.dm.example.withMetaMetaData[4][0] ],
+                                                       undefined, undefined, 
undefined
+                                               ]
+                                       ]
+                               ],
+                               'msg': 'Transaction replacing text and metadata 
at the same time'
+                       },
+                       {
                                // delta: 0
                                'calls': [
                                        [ 'pushRetainMetadata', 1 ],
@@ -115,7 +131,7 @@
        ];
        // HACK: This works because most transactions above don't change the 
document length, and the
        // ones that do change it cancel out
-       QUnit.expect( cases.length*( 4*doc.metadata.getTotalDataLength() + 2 ) 
);
+       QUnit.expect( cases.length*( 8*doc.metadata.getTotalDataLength() + 2 ) 
);
 
        for ( i = 0; i < cases.length; i++ ) {
                tx = new ve.dm.Transaction();
@@ -127,9 +143,9 @@
                list = new ve.dm.MetaList( surface );
                // Test both the transaction-via-surface and 
transaction-via-document flows
                surface.change( tx );
-               assertItemsMatchMetadata( assert, doc.metadata, list, 
cases[i].msg, false );
+               assertItemsMatchMetadata( assert, doc.metadata, list, 
cases[i].msg, true );
                doc.rollback( tx );
-               assertItemsMatchMetadata( assert, doc.metadata, list, 
cases[i].msg + ' (rollback)', false );
+               assertItemsMatchMetadata( assert, doc.metadata, list, 
cases[i].msg + ' (rollback)', true );
        }
 } );
 

-- 
To view, visit https://gerrit.wikimedia.org/r/82786
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I899d2b3c94c2cfa55823879bca95456750f64382
Gerrit-PatchSet: 7
Gerrit-Project: mediawiki/extensions/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Catrope <[email protected]>
Gerrit-Reviewer: Esanders <[email protected]>
Gerrit-Reviewer: jenkins-bot

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to