Cscott has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/83851


Change subject: WIP: Basic metadata support for `ve.dm.Transaction.transpose()`.
......................................................................

WIP: Basic metadata support for `ve.dm.Transaction.transpose()`.

Change-Id: Id1b9000db551c69250c3b93bb977fd4307e7a758
---
M modules/ve/dm/ve.dm.Transaction.transpose.js
M modules/ve/test/dm/ve.dm.Transaction.transpose.test.js
2 files changed, 339 insertions(+), 68 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/VisualEditor 
refs/changes/51/83851/1

diff --git a/modules/ve/dm/ve.dm.Transaction.transpose.js 
b/modules/ve/dm/ve.dm.Transaction.transpose.js
index f07777b..a605515 100644
--- a/modules/ve/dm/ve.dm.Transaction.transpose.js
+++ b/modules/ve/dm/ve.dm.Transaction.transpose.js
@@ -23,6 +23,45 @@
        }
 
        /*
+        * Position data type.
+        *
+        * Maintain position within data/metadata.  Conceptually, metadata
+        * positions precede data positions, so that the data at offset
+        * `n` occurs at position `(n, MAX_INT)`.
+        */
+       function Position(dataPos, metadataPos) {
+               this.data = dataPos || 0;
+               this.meta = metadataPos || 0;
+       }
+       Position.prototype.add = function( d, m ) {
+               if ( d instanceof Position ) {
+                       return this.add( d.data, d.meta );
+               }
+               if ( d === 0 ) {
+                       return new Position( this.data, this.meta + ( m || 0 ) 
);
+               }
+               return new Position( this.data + d, ( m || 0 ) );
+       };
+       Position.prototype.subtract = function( d, m ) {
+               if ( d instanceof Position ) {
+                       return this.subtract( d.data, d.meta );
+               }
+               return this.add( -d, -m );
+       };
+       Position.prototype.lt = function( d, m ) {
+               if ( d instanceof Position ) {
+                       return this.lt( d.data, d.meta );
+               }
+               if ( this.data === d ) {
+                       return this.meta < ( m || 0 );
+               }
+               return this.data < d;
+       };
+       Position.prototype.toString = function() {
+               return 'Position('+this.data+','+this.meta+')';
+       };
+
+       /*
         * Helper function for `transposeOne()`.
         *
         * We preprocess transactions' operation lists to annotate them with
@@ -30,28 +69,124 @@
         * call the result a 'map'.
         */
        function mkMap( tx ) {
-               var oldPos = 0;
-               return tx.operations.map( function( op ) {
+               var r = [], oldPos = new Position( 0, 0 ), j, meta, inMeta = 
false;
+               // start with sentinel
+               r.push( {
+                       op: { type: 'retain', length: 1 },
+                       correction: new Position( -1 ),
+                       start: new Position( -1 )
+               } );
+               $.each( tx.operations, function( i, op ) {
                        var m = {
                                op: op,
                                start: oldPos
                        };
                        switch ( op.type ) {
+
                        case 'retain':
-                               m.correction = 0;
-                               oldPos += op.length;
+                               m.correction = new Position( 0 );
+                               if ( inMeta ) {
+                                       inMeta = false;
+                                       // add sentinel covering "data after 
metadata" realignment
+                                       m.start = oldPos.add( 1 );
+                                       m.correction = new Position( -1 );
+                                       r.push( {
+                                               op: { type: 'retainData' },
+                                               start: oldPos,
+                                               correction: new Position( 0 )
+                                       } );
+                                       if ( op.length === 1 ) {
+                                               oldPos = m.start;
+                                               return;
+                                       }
+                               }
+                               oldPos = oldPos.add( op.length );
                                break;
+
                        case 'replace':
+                               if ( inMeta ) {
+                                       throw new Error( 'replace at unaligned 
position' );
+                               }
+                               // combine data and metadata
+                               m.remove = [];
+                               meta = ( op.removeMetadata === undefined ) ? [] 
: op.removeMetadata;
+                               for ( j = 0; j < op.remove.length; j++ ) {
+                                       m.remove.push( [ meta[ j ], op.remove[ 
j ] ] );
+                               }
+                               m.insert = [];
+                               meta = ( op.insertMetadata === undefined ) ? [] 
: op.insertMetadata;
+                               for ( j = 0; j < op.insert.length; j++ ) {
+                                       m.insert.push( [ meta[ j ], op.insert[ 
j ] ] );
+                               }
+                               oldPos = oldPos.add( op.remove.length );
+                               break;
+
+                       case 'replaceMetadata':
+                               if ( !inMeta ) {
+                                       inMeta = true;
+                                       // add sentinel
+                                       r.push( {
+                                               op: { type: 'retainMetadata', 
length: 1 },
+                                               correction: new Position( 0, -1 
),
+                                               start: oldPos.subtract( 0, 1 )
+                                       } );
+                               }
                                m.remove = op.remove.slice( 0 );
                                m.insert = op.insert.slice( 0 );
-                               oldPos += op.remove.length;
+                               oldPos = oldPos.add( 0, op.remove.length );
                                break;
+
+                       case 'retainMetadata':
+                               if ( !inMeta ) {
+                                       inMeta = true;
+                                       // add sentinel
+                                       r.push( {
+                                               op: { type: 'retainMetadata', 
length: 1 },
+                                               correction: new Position( 0, -1 
),
+                                               start: oldPos.subtract( 0, 1 )
+                                       } );
+                               }
+                               m.correction = new Position( 0 );
+                               oldPos = oldPos.add( 0, op.length );
+                               break;
+
                        default:
                                throw new Error( 'unhandled type: ' + op.type );
                        }
-                       m.end = oldPos;
-                       return m;
+                       r.push( m );
                } );
+               // add sentinel at end
+               r.push( {
+                       op: { type: 'retain', length: 1 },
+                       correction: new Position( -1 ),
+                       start: oldPos,
+                       end: oldPos.add( 1 )
+               } );
+               // link up 'start' and 'end' fields
+               $.each( r, function( i, op ) {
+                       if ( i < r.length - 1 ) {
+                               op.end = r[ i + 1 ].start;
+                       }
+               } );
+               return r;
+       }
+
+       /*
+        * Helper function: change insert/remove to replace/insert.
+        */
+       function mkReplaceExtra( insert, remove, preferB ) {
+               var delItems, extraItems;
+               // slice handles the case where remove.length < insert.length 
fine.
+               delItems = insert.slice( 0, remove.length );
+               while ( delItems.length < remove.length ) {
+                       if ( preferB ) {
+                               delItems.push( DELETED );
+                       } else {
+                               delItems.unshift( DELETED );
+                       }
+               }
+               extraItems = insert.slice( remove.length );
+               return { replace: delItems, extra: extraItems };
        }
 
        /*
@@ -75,6 +210,34 @@
        }
 
        /*
+        * Helper function: split replace/insert.
+        */
+       function pushReplaceMerged( tx, remove, insert ) {
+               var cnt, i, r;
+               // optimize insert/remove sets in A (avoid removing then 
reinserting)
+               cnt = countCommonPrefixSuffix( remove, insert );
+               tx.pushRetain( cnt[ 0 ] ); // common prefix length
+               r = { remove: [], removeMetadata: [], insert: [], 
insertMetadata: [] };
+               for ( i = cnt[ 0 ]; i + cnt[ 1 ] < remove.length ; i++ ) {
+                       r.removeMetadata.push( remove[ i ][ 0 ] );
+                       r.remove.push( remove[ i ][ 1 ] );
+               }
+               for ( i = cnt[ 0 ]; i + cnt[ 1 ] < insert.length ; i++ ) {
+                       r.insertMetadata.push( insert[ i ][ 0 ] );
+                       r.insert.push( insert[ i ][ 1 ] );
+               }
+               if ( ve.compare( r.removeMetadata, new Array( r.remove.length ) 
) &&
+                        ve.compare( r.insertMetadata, new Array( 
r.insert.length ) )
+                  ) {
+                       r.removeMetadata = r.insertMetadata = undefined;
+               }
+               tx.pushReplaceInternal(
+                       r.remove, r.insert, r.removeMetadata, r.insertMetadata
+               );
+               tx.pushRetain( cnt[ 1 ] ); // common suffix length
+       }
+
+       /*
         * Helper function for `transpose()`.
         *
         * Transposing `a` by `b` is almost the same as transposing `b` by `a`; 
we just
@@ -83,96 +246,204 @@
         * full `transpose` result.
         */
        function transposeOne( aMap, bMap, preferB ) {
-               var r, i, j, k, aOp, bOp, insPos, delItems, lookup, pos, 
extraItems, cnt;
+               // preferB means insert extraItems *before* pos
+               // !preferB means insert extraItems *after* pos-1
+               var r, i, j, k, aOp, bOp, insPos, items, lookup, pos, cnt;
                // go through operations in b, applying them to a.
                // Do this in reverse order so that we avoid changing the 
offsets
                // of operations we haven't yet processed.
                j = aMap.length - 1;
                lookup = function( pos ) {
                        // find the location in aMap corresponding to pos
-                       while ( j >= 0 && aMap[ j ].start > pos ) {
+                       while ( j >= 0 && pos.lt( aMap[ j ].start ) ) {
                                j--;
                        }
-               };
-               // allow corrections before start
-               aMap[ -1 ] = { correction: 0 };
-               // push fake 'retain 0' at end of aMap to allow corrections 
after end
-               pos = aMap.length > 0 ? aMap[ aMap.length - 1 ].end : 0;
-               aMap[ ++j ] = {
-                       correction: 0,
-                       op: { type: 'retain', length: 0 },
-                       start: pos,
-                       end: pos
                };
 
                for ( i = bMap.length - 1; i >= 0; i-- ) {
                        bOp = bMap[ i ].op;
-                       if ( bOp.type !== 'replace') { continue; }
                        pos = bMap[ i ].end;
-                       // slice handles the case where remove.length < 
insert.length fine.
-                       delItems = bMap[ i ].insert.slice( 0, bMap[ i 
].remove.length );
-                       while ( delItems.length < bMap[ i ].remove.length ) {
-                               if ( preferB ) {
-                                       delItems.push( DELETED );
-                               } else {
-                                       delItems.unshift( DELETED );
+                       if ( bOp.type === 'replaceMetadata' ) {
+                               items = mkReplaceExtra(
+                                       bMap[ i ].insert, bMap[ i ].remove, 
preferB
+                               );
+                               if ( !preferB ) {
+                                       pos = pos.subtract( 0, 1 );
                                }
-                       }
-                       extraItems = bMap[ i ].insert.slice( bMap[ i 
].remove.length );
-                       // insert extraItems:
-                       // preferB means insert extraItems *before* pos
-                       // !preferB means insert extraItems *after* pos-1
-                       if ( !preferB ) {
-                               pos--;
-                       }
-                       lookup( pos ); // find location in aMap corresponding 
to pos
-                       if ( j >= 0 && aMap[ j ].op.type === 'replace' ) {
-                               insPos = pos - aMap[ j ].start;
-                               if ( !preferB ) { insPos++; }
-                               ve.batchSplice(
-                                       aMap[ j ].remove, insPos, 0, extraItems
+                               lookup( pos );
+                               if ( j >= 0 ) {
+                                       switch ( aMap[ j ].op.type ) {
+                                       case 'retain':
+                                               // no change needed!
+                                               break;
+                                       case 'retainMetadata':
+                                       case 'retainFinal':
+                                       case 'retainData':
+                                               aMap[ j ].correction.meta += 
items.extra.length;
+                                               break;
+                                       case 'replaceMetadata':
+                                               insPos = pos.meta - aMap[ j 
].start.meta;
+                                               if ( !preferB ) { insPos++; }
+                                               ve.batchSplice(
+                                                       aMap[ j ].remove, 
insPos, 0, items.extra
+                                               );
+                                               insPos = preferB ? 0 : aMap[ j 
].insert.length;
+                                               ve.batchSplice(
+                                                       aMap[ j ].insert, 
insPos, 0, items.extra
+                                               );
+                                               break;
+                                       case 'replace':
+                                               debugger;
+                                               break;
+                                       default:
+                                               throw new Error( 'waaaa' );
+                                       }
+                               } else {
+                                       aMap[ j ].correction.meta += 
items.extra.length;
+                               }
+                               if ( preferB ) {
+                                       pos = pos.subtract( 0, 1 );
+                               }
+                               // now handle removed/replaced items
+                               k = items.replace.length - 1;
+                               for ( ; !pos.lt( bMap[ i ].start ); pos = 
pos.subtract( 0, 1 ), k-- ) {
+                                       lookup( pos ); // find location in aMap 
corresponding to pos
+                                       if ( j >= 0 ) {
+                                               switch( aMap[ j ].op.type ) {
+                                               case 'replaceMetadata':
+                                                       aMap[ j ].remove[ 
pos.meta - aMap[ j ].start.meta ] =
+                                                               items.replace[ 
k ];
+                                                       break;
+                                               case 'retainMetadata':
+                                               case 'retainData':
+                                                       if ( items.replace[ k ] 
=== DELETED ) {
+                                                               aMap[ j 
].correction.meta--;
+                                                       }
+                                                       break;
+                                               case 'replace':
+                                               case 'retain':
+                                                       debugger;
+                                                       break;
+                                               default:
+                                                       throw new Error( 'waaa' 
);
+                                               }
+                                       } else if ( items.replace[ k ] === 
DELETED ) {
+                                               aMap[ j ].correction.meta--;
+                                       }
+                               }
+                       } else if ( bOp.type === 'replace' ) {
+                               items = mkReplaceExtra(
+                                       bMap[ i ].insert, bMap[ i ].remove, 
preferB
                                );
-                               insPos = preferB ? 0 : aMap[ j ].insert.length;
-                               ve.batchSplice(
-                                       aMap[ j ].insert, insPos, 0, extraItems
-                               );
-                       } else {
-                               aMap[ j ].correction += extraItems.length;
-                       }
-                       if ( preferB ) {
-                               pos--;
-                       }
-                       // now handle removed/replaced items.
-                       k = delItems.length - 1;
-                       for ( ; pos >= bMap[ i ].start ; pos--, k-- ) {
+                               // insert extraItems:
+                               if ( !preferB ) {
+                                       pos = pos.subtract( 1 );
+                               }
                                lookup( pos ); // find location in aMap 
corresponding to pos
-                               if ( j >= 0 && aMap[ j ].op.type === 'replace' 
) {
-                                       aMap[ j ].remove[ pos - aMap[ j ].start 
] =
-                                               delItems[ k ];
-                               } else if ( delItems[ k ] === DELETED ) {
-                                       aMap[ j ].correction--;
+                               if ( j >= 0 ) {
+                                       switch ( aMap[ j ].op.type ) {
+                                       case 'replaceMetadata':
+                                       case 'retainMetadata':
+                                       case 'retainData':
+                                               debugger;
+                                               break;
+                                       case 'retain':
+                                       case 'retainFinal':
+                                               aMap[ j ].correction.data += 
items.extra.length;
+                                               break;
+                                       case 'replace':
+                                               insPos = pos.data - aMap[ j 
].start.data;
+                                               if ( !preferB ) { insPos++; }
+                                               ve.batchSplice(
+                                                       aMap[ j ].remove, 
insPos, 0, items.extra
+                                               );
+                                               insPos = preferB ? 0 : aMap[ j 
].insert.length;
+                                               ve.batchSplice(
+                                                       aMap[ j ].insert, 
insPos, 0, items.extra
+                                               );
+                                               break;
+                                       default:
+                                               throw new Error( 'waaa' );
+                                       }
+                               } else {
+                                       aMap[ j ].correction.data += 
items.extra.length;
+                               }
+                               if ( preferB ) {
+                                       pos = pos.subtract( 1 );
+                               }
+                               // now handle removed/replaced items.
+                               k = items.replace.length - 1;
+                               for ( ; !pos.lt( bMap[ i ].start ); pos = 
pos.subtract( 1 ), k-- ) {
+                                       lookup( pos ); // find location in aMap 
corresponding to pos
+                                       if ( j >= 0 ) {
+                                               switch( aMap[ j ].op.type ) {
+                                               case 'replace':
+                                                       aMap[ j ].remove[ 
pos.data - aMap[ j ].start.data ] =
+                                                               items.replace[ 
k ];
+                                                       break;
+                                               case 'retain':
+                                                       if ( items.replace[ k ] 
=== DELETED ) {
+                                                               aMap[ j 
].correction.data--;
+                                                       }
+                                                       break;
+                                               case 'replaceMetadata':
+                                               case 'retainMetadata':
+                                               case 'retainData':
+                                                       // we're replacing or 
deleting an element which
+                                                       // is targeted by a 
metadata operation.
+                                                       // we need to shift 
this metadata operation.
+                                                       debugger;
+                                                       break;
+                                               default:
+                                                       throw new Error( 'waaa' 
);
+                                               }
+                                       } else if ( items.replace[ k ] === 
DELETED ) {
+                                               aMap[ j ].correction.data--;
+                                       }
                                }
                        }
                }
                r = new ve.dm.Transaction();
-               r.pushRetain( aMap[ -1 ].correction );
                for ( i = 0; i < aMap.length; i++ ) {
                        aOp = aMap[ i ].op;
                        switch ( aOp.type ) {
-                       case 'retain':
-                               r.pushRetain( aMap[ i ].op.length + aMap[ i 
].correction );
+
+                       case 'retainData':
+                               // skip past any added metadata
+                               r.pushRetainMetadata( aMap[ i ].correction.meta 
);
+                               // skip past data-after-metadata
+                               r.pushRetain( 1 );
                                break;
+
+                       case 'retain':
+                       case 'retainMetadata':
+                       case 'retainFinal':
+                               pos = ( aOp.type === 'retain' ) ?
+                                       new Position( aMap[ i ].op.length ) :
+                                       new Position( 0, aMap[ i ].op.length );
+                               pos = pos.add( aMap[ i ].correction );
+                               r.pushRetain( pos.data );
+                               r.pushRetainMetadata( pos.meta );
+                               break;
+
                        case 'replace':
-                               // optimize insert/remove sets in A (avoid 
removing then reinserting)
                                aMap[ i ].remove = aMap[ i ].remove.filter( 
isNotDeleted );
-                               cnt = countCommonPrefixSuffix( aMap[ i 
].remove, aMap[ i ].insert );
-                               r.pushRetain( cnt[ 0 ] ); // common prefix 
length
-                               r.pushReplaceInternal(
+                               pushReplaceMerged( r, aMap[ i ].remove, aMap[ i 
].insert );
+                               break;
+
+                       case 'replaceMetadata':
+                               aMap[ i ].remove = aMap[ i ].remove.filter( 
isNotDeleted );
+                               cnt = countCommonPrefixSuffix(
+                                       aMap[ i ].remove, aMap[ i ].insert
+                               );
+                               r.pushRetainMetadata( cnt[ 0 ] ); // common 
prefix length
+                               r.pushReplaceMetadata(
                                        aMap[ i ].remove.slice( cnt[ 0 ], aMap[ 
i ].remove.length - cnt[ 1 ] ),
                                        aMap[ i ].insert.slice( cnt[ 0 ], aMap[ 
i ].insert.length - cnt[ 1 ] )
                                );
-                               r.pushRetain( cnt[ 1 ] ); // common suffix 
length
+                               r.pushRetainMetadata( cnt[ 1 ] ); // common 
suffix length
                                break;
+
                        default:
                                throw new Error( 'more work to do!' );
                        }
diff --git a/modules/ve/test/dm/ve.dm.Transaction.transpose.test.js 
b/modules/ve/test/dm/ve.dm.Transaction.transpose.test.js
index 505f560..9275c7e 100644
--- a/modules/ve/test/dm/ve.dm.Transaction.transpose.test.js
+++ b/modules/ve/test/dm/ve.dm.Transaction.transpose.test.js
@@ -10,7 +10,7 @@
 /* Helper methods */
 
 // Don't enable certain tests, since transpose doesn't (yet) support metadata
-var disableMetadataTests = true;
+var disableMetadataTests = false;
 
 /* Tests */
 

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: Id1b9000db551c69250c3b93bb977fd4307e7a758
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Cscott <canan...@wikimedia.org>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to