mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-15 18:39:52 +00:00
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:
parent
5e4f0293f2
commit
80db7a593e
|
@ -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 )
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue