Rethink of structural replacement code

Splits and merges now work, or at least the tests for it pass

The strategy I used is to gather the affected ranges for each of the
following:
* removed stuff
* the entirety of each node touched by a non-zero removal
* if the inserted data busts out of its parent, the entirety of that
  parent node (the 'scope')
then get the covering range of all those ranges, and rebuild that.

Change-Id: I7c3b421abc0ba134157ac8b59042675bb1b5073c
This commit is contained in:
Catrope 2012-05-16 16:38:58 -07:00
parent 5e4f0293f2
commit 80db7a593e

View file

@ -254,7 +254,7 @@ ve.dm.TransactionProcessor.prototype.replace = function( op ) {
insert = this.reversed ? op.remove : op.insert, insert = this.reversed ? op.remove : op.insert,
removeHasStructure = ve.dm.Document.containsElementData( remove ), removeHasStructure = ve.dm.Document.containsElementData( remove ),
insertHasStructure = ve.dm.Document.containsElementData( insert ), insertHasStructure = ve.dm.Document.containsElementData( insert ),
node, scope, selection; node, selection;
// Figure out if this is a structural insert or a content insert // Figure out if this is a structural insert or a content insert
if ( !removeHasStructure && !insertHasStructure ) { if ( !removeHasStructure && !insertHasStructure ) {
// Content replacement // Content replacement
@ -285,7 +285,12 @@ ve.dm.TransactionProcessor.prototype.replace = function( op ) {
startOffset = this.cursor, startOffset = this.cursor,
adjustment = 0, adjustment = 0,
i, i,
type; type,
prevCursor,
affectedRanges = [],
scope,
minInsertLevel = 0,
coveringRange;
while ( true ) { while ( true ) {
if ( operation.type == 'replace' ) { if ( operation.type == 'replace' ) {
@ -293,8 +298,30 @@ ve.dm.TransactionProcessor.prototype.replace = function( op ) {
opInsert = this.reversed ? operation.remove : operation.insert; opInsert = this.reversed ? operation.remove : operation.insert;
// Update the linear model for this insert // Update the linear model for this insert
ve.batchSplice( this.document.data, this.cursor, opRemove.length, opInsert ); ve.batchSplice( this.document.data, this.cursor, opRemove.length, opInsert );
affectedRanges.push( new ve.Range( this.cursor, this.cursor + opRemove.length ) );
prevCursor = this.cursor;
this.cursor += opInsert.length; this.cursor += opInsert.length;
adjustment += opInsert.length - opRemove.length;
// Paint the removed selection, figure out which nodes were
// covered, and add their ranges to the affected ranges list
if ( opRemove.length > 0 ) {
selection = this.document.selectNodes( new ve.Range(
prevCursor - adjustment,
prevCursor + opRemove.length - adjustment
), 'siblings' );
for ( i = 0; i < selection.length; i++ ) {
// .nodeRange is the inner range, we need the
// outer range (including opening and closing)
if ( selection[i].node.isWrapped() ) {
affectedRanges.push( new ve.Range(
selection[i].nodeRange.start - 1,
selection[i].nodeRange.end + 1
) );
} else {
affectedRanges.push( selection[i].nodeRange );
}
}
}
// Walk through the remove and insert data // Walk through the remove and insert data
// and keep track of the element depth change (level) // and keep track of the element depth change (level)
@ -312,6 +339,10 @@ ve.dm.TransactionProcessor.prototype.replace = function( op ) {
removeLevel++; removeLevel++;
} }
} }
// Keep track of the scope of the insertion
// Normally this is the node we're inserting into, except if the
// insertion closes elements it doesn't open (i.e. splits elements),
// in which case it's the affected ancestor
for ( i = 0; i < opInsert.length; i++ ) { for ( i = 0; i < opInsert.length; i++ ) {
type = opInsert[i].type; type = opInsert[i].type;
if ( type === undefined ) { if ( type === undefined ) {
@ -319,11 +350,27 @@ ve.dm.TransactionProcessor.prototype.replace = function( op ) {
} else if ( type.charAt( 0 ) === '/' ) { } else if ( type.charAt( 0 ) === '/' ) {
// Closing element // Closing element
insertLevel--; insertLevel--;
if ( insertLevel < minInsertLevel ) {
// Closing an unopened element at a higher
// (more negative) level than before
// Lazy-initialize scope
scope = scope || this.document.getNodeFromOffset( prevCursor );
// Push the full range of the old scope as an affected range
scopeStart = this.document.getDocumentNode().getOffsetFromNode( scope );
scopeEnd = scopeStart + scope.getOuterLength();
affectedRanges.push( new ve.Range( scopeStart, scopeEnd ) );
// Update scope
scope = scope.getParent() || scope;
}
} else { } else {
// Opening element // Opening element
insertLevel++; insertLevel++;
} }
} }
// Update adjustment
adjustment += opInsert.length - opRemove.length;
} else { } else {
// We know that other operations won't cause adjustments, so we // We know that other operations won't cause adjustments, so we
// don't have to update adjustment // don't have to update adjustment
@ -342,30 +389,11 @@ ve.dm.TransactionProcessor.prototype.replace = function( op ) {
} }
} }
// TODO this handles splitting nodes but not merging nodes // From all the affected ranges we have gathered, compute a range that covers all
// Figure out in which node the start was // of them, and rebuild that
selection = this.document.selectNodes( new ve.Range( startOffset, startOffset ) ); coveringRange = ve.Range.newCoveringRange( affectedRanges );
node = selection[0].node; this.synchronizer.pushRebuild( coveringRange, new ve.Range( coveringRange.start,
// Figure out what the scope of the insertion is coveringRange.end + adjustment )
// FIXME should be opInsert instead of op.insert, but the latter makes rollbacks );
// of splits work by accident. Will fix this when implementing merges
scope = ve.dm.Document.getScope( node, op.insert );
if ( scope === node ) {
// Simple case: no splits occurred, we can just rebuild the affected range
this.synchronizer.pushRebuild(
new ve.Range( startOffset, this.cursor - adjustment ),
new ve.Range( startOffset, this.cursor )
);
} else {
// A split occurred. Rebuild the entirety of scope
// TODO do something better to get the offset, possibly via getScope()
// or through whatever we have to do for deletion painting
var scopeStart = this.document.getDocumentNode().getOffsetFromNode( scope );
var scopeEnd = scopeStart + scope.getOuterLength();
this.synchronizer.pushRebuild(
new ve.Range( scopeStart, scopeEnd ),
new ve.Range( scopeStart, scopeEnd + adjustment )
);
}
} }
}; };