jenkins-bot has submitted this change and it was merged. ( https://gerrit.wikimedia.org/r/368587 )
Change subject: VisualDiff: Refactor for historical diffs ...................................................................... VisualDiff: Refactor for historical diffs Make computeDiff and associated functions more generic, so they can be used for the document diff and the internal list diff. Change-Id: Idf7f694080f8f633147dd861ea0180140f16c1d8 --- M src/dm/ve.dm.VisualDiff.js M src/ui/elements/ve.ui.DiffElement.js 2 files changed, 265 insertions(+), 138 deletions(-) Approvals: jenkins-bot: Verified Jforrester: Looks good to me, approved diff --git a/src/dm/ve.dm.VisualDiff.js b/src/dm/ve.dm.VisualDiff.js index 8104d02..a2c006e 100644 --- a/src/dm/ve.dm.VisualDiff.js +++ b/src/dm/ve.dm.VisualDiff.js @@ -34,7 +34,11 @@ this.freezeInternalListIndices( this.oldDoc ); this.freezeInternalListIndices( this.newDoc ); - this.computeDiff(); + this.diff = { + docDiff: this.computeDiff( this.oldDocChildren, this.newDocChildren ), + internalListDiff: this.getInternalListDiffInfo() + }; + }; /** @@ -84,91 +88,97 @@ }; /** - * Get the diff between the two documents, in two steps: (1) Compare the children - * of the old and new document nodes and record any pair where the old child and + * Get the diff between the two trees, in two steps: (1) Compare the children + * of the old and new root nodes and record any pair where the old child and * new child are identical. (If an old child is identical to two new children, it * will be paired with the first one only.) (2) If any children of the old or new - * document nodes remain unpaired, decide whether they are an old child that has + * root nodes remain unpaired, decide whether they are an old child that has * been removed, a new child that has been inserted, or a pair in which the old * child was changed into the new child. + * + * @param {Array} oldRootChildren Children of the old root node + * @param {Array} newRootChildren Children of the new root node + * @returns {Object} Object containing diff information */ -ve.dm.VisualDiff.prototype.computeDiff = function () { +ve.dm.VisualDiff.prototype.computeDiff = function ( oldRootChildren, newRootChildren ) { var i, ilen, j, jlen, - oldDocChildrenToDiff = [], - newDocChildrenToDiff = []; + oldRootChildrenToDiff = [], + newRootChildrenToDiff = [], + diff = { + rootChildrenOldToNew: {}, + rootChildrenNewToOld: {}, + rootChildrenRemove: [], + rootChildrenInsert: [] + }; - this.diff = { - docChildrenOldToNew: {}, - docChildrenNewToOld: {}, - docChildrenRemove: [], - docChildrenInsert: [], - internalListDiff: this.getInternalListDiffInfo() - }; + // STEP 1: Find identical root-node children - // STEP 1: Find identical document-node children + for ( i = 0, ilen = oldRootChildren.length; i < ilen; i++ ) { + for ( j = 0, jlen = newRootChildren.length; j < jlen; j++ ) { + if ( !diff.rootChildrenNewToOld.hasOwnProperty( j ) && + this.compareRootChildren( oldRootChildren[ i ], newRootChildren[ j ] ) ) { - for ( i = 0, ilen = this.oldDocChildren.length; i < ilen; i++ ) { - for ( j = 0, jlen = this.newDocChildren.length; j < jlen; j++ ) { - if ( !this.diff.docChildrenNewToOld.hasOwnProperty( j ) && - this.compareDocChildren( this.oldDocChildren[ i ], this.newDocChildren[ j ] ) ) { - - this.diff.docChildrenOldToNew[ i ] = j; - this.diff.docChildrenNewToOld[ j ] = i; + diff.rootChildrenOldToNew[ i ] = j; + diff.rootChildrenNewToOld[ j ] = i; break; } // If no new nodes equalled the old node, add it to nodes to diff if ( j === jlen - 1 ) { - oldDocChildrenToDiff.push( i ); + oldRootChildrenToDiff.push( i ); } } } for ( j = 0; j < jlen; j++ ) { - if ( !this.diff.docChildrenNewToOld.hasOwnProperty( j ) ) { - newDocChildrenToDiff.push( j ); + if ( !diff.rootChildrenNewToOld.hasOwnProperty( j ) ) { + newRootChildrenToDiff.push( j ); } } - // STEP 2: Find removed, inserted and modified document-node children + // STEP 2: Find removed, inserted and modified root-node children - if ( oldDocChildrenToDiff.length !== 0 || newDocChildrenToDiff.length !== 0 ) { + if ( oldRootChildrenToDiff.length !== 0 || newRootChildrenToDiff.length !== 0 ) { - if ( oldDocChildrenToDiff.length === 0 ) { + if ( oldRootChildrenToDiff.length === 0 ) { // Everything new is an insert - this.diff.docChildrenInsert = newDocChildrenToDiff; + diff.rootChildrenInsert = newRootChildrenToDiff; - } else if ( newDocChildrenToDiff.length === 0 ) { + } else if ( newRootChildrenToDiff.length === 0 ) { // Everything old is a remove - this.diff.docChildrenRemove = oldDocChildrenToDiff; + diff.rootChildrenRemove = oldRootChildrenToDiff; } else { // Find out which remaining docChildren are removed, inserted or modified - this.findModifiedDocChildren( oldDocChildrenToDiff, newDocChildrenToDiff ); + this.findModifiedRootChildren( + oldRootChildrenToDiff, newRootChildrenToDiff, oldRootChildren, newRootChildren, diff + ); } } + + return diff; }; /** - * Compare the linear data for two nodes + * Compare the linear data for two root-child nodes * - * @param {ve.dm.Node} oldDocChild Child of the old document node - * @param {ve.dm.Node} newDocChild Child of the new document node + * @param {ve.dm.Node} oldRootChild Child of the old root node + * @param {ve.dm.Node} newRootChild Child of the new root node * @return {boolean} The linear data is the same */ -ve.dm.VisualDiff.prototype.compareDocChildren = function ( oldDocChild, newDocChild ) { +ve.dm.VisualDiff.prototype.compareRootChildren = function ( oldRootChild, newRootChild ) { var i, ilen, oldData, newData, oldStore, newStore; - if ( oldDocChild.length !== newDocChild.length || !oldDocChild.isDiffComparable( newDocChild ) ) { + if ( oldRootChild.length !== newRootChild.length || !oldRootChild.isDiffComparable( newRootChild ) ) { return false; } - oldData = this.oldDoc.getData( oldDocChild.getOuterRange() ); - newData = this.newDoc.getData( newDocChild.getOuterRange() ); + oldData = this.oldDoc.getData( oldRootChild.getOuterRange() ); + newData = this.newDoc.getData( newRootChild.getOuterRange() ); if ( JSON.stringify( oldData ) === JSON.stringify( newData ) ) { return true; @@ -190,8 +200,8 @@ }; /** - * Diff each child of the old document node against each child of the new - * document; but if the differs decide that an old child is similar enough to a + * Diff each child of the old root node against each child of the new root + * node; but if the differs decide that an old child is similar enough to a * new child, record these as a change from the old child to the new child and * don't diff any more children against either child. * @@ -199,40 +209,39 @@ * similar to two of the new children), but diffing every old child against * every new child could have a heavy performance cost. * - * @param {Array} oldDocChildren The children of the old document with no - * identical partners in the new document - * @param {Array} newDocChildren The children of the new document with no - * identical partners in the old document + * @param {Array} oldIndices Indices of the old root children diff + * @param {Array} newIndices Indices of the new root children diff + * @param {Array} oldRootChildren Children of the old root node + * @param {Array} newRootChildren Children of the new root node + * @param {Object} diff Object that will contain information about the diff */ -ve.dm.VisualDiff.prototype.findModifiedDocChildren = function ( oldDocChildren, newDocChildren ) { - var diff, i, j, - ilen = oldDocChildren.length, - jlen = newDocChildren.length; +ve.dm.VisualDiff.prototype.findModifiedRootChildren = function ( oldIndices, newIndices, oldRootChildren, newRootChildren, diff ) { + var diffResults, i, j, + ilen = oldIndices.length, + jlen = newIndices.length; for ( i = 0; i < ilen; i++ ) { for ( j = 0; j < jlen; j++ ) { - if ( oldDocChildren[ i ] !== null && newDocChildren[ j ] !== null ) { + if ( oldIndices[ i ] !== null && newIndices[ j ] !== null ) { - // Try to diff the nodes. If they are too different, diff will be false - diff = this.getDocChildDiff( - this.oldDocChildren[ oldDocChildren[ i ] ], - this.newDocChildren[ newDocChildren[ j ] ] - ); + diffResults = this.getDocChildDiff( oldRootChildren[ oldIndices[ i ] ], newRootChildren[ newIndices[ j ] ] ); - if ( diff ) { - this.diff.docChildrenOldToNew[ oldDocChildren[ i ] ] = { - node: newDocChildren[ j ], - diff: diff, + if ( diffResults ) { + diff.rootChildrenOldToNew[ oldIndices[ i ] ] = { + node: newIndices[ j ], + diff: diffResults, // TODO: Neaten this - correspondingNodes: this.treeDiffer.Differ.prototype.getCorrespondingNodes( diff.treeDiff, diff.oldTree.orderedNodes.length, diff.newTree.orderedNodes.length ) + correspondingNodes: this.treeDiffer.Differ.prototype.getCorrespondingNodes( + diffResults.treeDiff, diffResults.oldTree.orderedNodes.length, diffResults.newTree.orderedNodes.length + ) }; - this.diff.docChildrenNewToOld[ newDocChildren[ j ] ] = { - node: oldDocChildren[ i ] + diff.rootChildrenNewToOld[ newIndices[ j ] ] = { + node: oldIndices[ i ] }; - oldDocChildren[ i ] = null; - newDocChildren[ j ] = null; + oldIndices[ i ] = null; + newIndices[ j ] = null; break; } @@ -243,20 +252,20 @@ // Any nodes remaining in the 'toDiff' arrays are removes and inserts for ( i = 0; i < ilen; i++ ) { - if ( oldDocChildren[ i ] !== null ) { - this.diff.docChildrenRemove.push( oldDocChildren[ i ] ); + if ( oldIndices[ i ] !== null ) { + diff.rootChildrenRemove.push( oldIndices[ i ] ); } } for ( j = 0; j < jlen; j++ ) { - if ( newDocChildren[ j ] !== null ) { - this.diff.docChildrenInsert.push( newDocChildren[ j ] ); + if ( newIndices[ j ] !== null ) { + diff.rootChildrenInsert.push( newIndices[ j ] ); } } }; /** - * Get the diff between a child of the old coument node and a child of the new + * Get the diff between a child of the old document node and a child of the new * document node. There are three steps: (1) Do a tree diff to find the minimal * transactions between the old child and the new child. Allowed transactions * are: remove a node, insert a node, or change an old node to a new node. (The @@ -446,86 +455,204 @@ * @return {Object} Internal list diff object */ ve.dm.VisualDiff.prototype.getInternalListDiffInfo = function () { - var i, ilen, diff, internalListDiffGroup, - group, groupIndexOrder, nodeIndex, item, + var i, ilen, j, jlen, diff, item, group, + newItems, oldItems, nodeIndices, oldDocNodeGroups = this.oldDoc.getInternalList().getNodeGroups(), newDocNodeGroups = this.newDoc.getInternalList().getNodeGroups(), - retainedInternalListItems = {}, - removedInternalListItems = {}, - internalListDiffInfo = []; + oldDocInternalListItems, + newDocInternalListItems, + groups = [], + internalListDiffInfo = {}; - // Find all nodes referenced by the new document's internal list + function getInternalListItemsToDiff( indexOrder, nodes ) { + var i, ilen, nodeIndex, + internalListItems = { + toDiff: [], + indices: [] + }; + + for ( i = 0, ilen = indexOrder.length; i < ilen; i++ ) { + nodeIndex = indexOrder[ i ]; + if ( nodeIndex !== null ) { + internalListItems.toDiff.push( nodes[ nodeIndex ] ); + internalListItems.indices.push( { + indexOrder: i, + nodeIndex: nodeIndex + } ); + } + } + + return internalListItems; + } + + function getInternalListItems( indexOrder, nodes, action ) { + var i, ilen, nodeIndex, internalListItems = []; + + for ( i = 0, ilen = indexOrder.length; i < ilen; i++ ) { + nodeIndex = indexOrder[ i ]; + if ( nodeIndex !== null ) { + internalListItems.push( { + diff: action, + indexOrder: i, + nodeIndex: nodeIndex + } ); + } + } + + return internalListItems; + } + + function containsDiff( diffObject ) { + var i; + + for ( i in diffObject ) { + if ( typeof diffObject[ i ] !== 'number' ) { + return true; + } + } + + return false; + } + + // Find all groups common to old and new docs + // Also find inserted groups for ( group in newDocNodeGroups ) { - groupIndexOrder = newDocNodeGroups[ group ].indexOrder; - internalListDiffInfo[ group ] = []; - for ( i = 0, ilen = groupIndexOrder.length; i < ilen; i++ ) { - nodeIndex = groupIndexOrder[ i ]; - retainedInternalListItems[ nodeIndex ] = true; - internalListDiffInfo[ group ].push( { - indexOrder: i, - nodeIndex: nodeIndex, - removed: false + if ( group in oldDocNodeGroups ) { + groups.push( { + group: group, + action: 'diff' } ); - internalListDiffInfo[ group ].changes = false; // Becomes true later if there are changes + } else { + groups.push( { + group: group, + action: 'insert' + } ); } } - // Find all nodes referenced by the old document's internal list + // Find removed groups for ( group in oldDocNodeGroups ) { - groupIndexOrder = oldDocNodeGroups[ group ].indexOrder; - for ( i = 0; i < groupIndexOrder.length - 1; i++ ) { - nodeIndex = groupIndexOrder[ i ]; - if ( retainedInternalListItems[ nodeIndex ] ) { - continue; - } - // TODO: Work out what should happen if the whole list group was removed - if ( internalListDiffInfo[ group ] === undefined ) { - internalListDiffInfo[ group ] = []; - internalListDiffInfo[ group ].changes = false; // Becomes true later - } - internalListDiffInfo[ group ].push( { - indexOrder: i, - nodeIndex: nodeIndex, - removed: true + if ( !( group in newDocNodeGroups ) ) { + groups.push( { + group: group, + action: 'remove' } ); - removedInternalListItems[ nodeIndex ] = group; } } - for ( group in internalListDiffInfo ) { - internalListDiffGroup = internalListDiffInfo[ group ].sort( function ( a, b ) { - if ( a.indexOrder === b.indexOrder ) { - return a.removed ? 1 : -1; - } - return a.indexOrder > b.indexOrder ? 1 : -1; - } ); - for ( i = 0, ilen = internalListDiffGroup.length; i < ilen; i++ ) { - item = internalListDiffGroup[ i ]; - if ( item.nodeIndex > this.oldDocInternalListNode.children.length - 1 ) { - // Item was inserted - item.diff = 1; - internalListDiffGroup.changes = true; - } else if ( item.nodeIndex in removedInternalListItems ) { - // Item was removed - item.diff = -1; - internalListDiffGroup.changes = true; - } else { - // Item corresponds to the item in the old document's internal list with - // the same index. They could be the same or changed. - diff = this.getDocChildDiff( - this.oldDocInternalListNode.children[ item.nodeIndex ], - this.newDocInternalListNode.children[ item.nodeIndex ], - 0 + // TODO: Work out how to calculate moves; for now, keep in the same order + // as new doc, with removes at the end + + // Diff the internal list items for each group + for ( i = 0, ilen = groups.length; i < ilen; i++ ) { + group = groups[ i ]; + switch ( group.action ) { + case 'diff': + + // Get old and new doc internal list items for this group + oldDocInternalListItems = getInternalListItemsToDiff( + oldDocNodeGroups[ group.group ].indexOrder, + this.oldDocInternalListNode.children ); - if ( diff.diffInfo && diff.diffInfo.length > 0 ) { - // Item was changed from an old item - item.diff = diff; - internalListDiffGroup.changes = true; - } else { - // Item is unchanged - item.diff = 0; + newDocInternalListItems = getInternalListItemsToDiff( + newDocNodeGroups[ group.group ].indexOrder, + this.newDocInternalListNode.children + ); + + // Diff internal list items + diff = this.computeDiff( + oldDocInternalListItems.toDiff, + newDocInternalListItems.toDiff, + this.internalListDiff + ); + + // Check there actually are any changes + if ( + ( diff.rootChildrenRemove.length > 0 || diff.rootChildrenInsert.length > 0 ) || + ( containsDiff( diff.rootChildrenOldToNew ) ) + ) { + + // There are changes. + // Mark each new doc internal list item as unchanged, changed or inserted + newItems = newDocInternalListItems.indices; + nodeIndices = {}; // For finding removed internalListItems + for ( j = 0, jlen = newItems.length; j < jlen; j++ ) { + item = newItems[ j ]; + if ( typeof diff.rootChildrenNewToOld[ item.indexOrder ] === 'number' ) { + + // Item hasn't changed + item.diff = 0; + nodeIndices[ item.nodeIndex ] = true; + + } else if ( diff.rootChildrenNewToOld[ item.indexOrder ] === undefined ) { + + // Item was inserted + item.diff = 1; + nodeIndices[ item.nodeIndex ] = true; + + } else { + + // Item has changed + // (The diff object is stored in rootChildrenOldToNew) + item.diff = diff.rootChildrenOldToNew[ + diff.rootChildrenNewToOld[ item.indexOrder ].node + ].diff; + nodeIndices[ item.nodeIndex ] = true; + + } + } + + // Add all removed items and mark as removed + oldItems = oldDocInternalListItems.indices; + for ( j = 0, jlen = oldItems.length; j < jlen; j++ ) { + item = oldItems[ j ]; + if ( !( item.nodeIndex in nodeIndices ) ) { + + // Item was removed + item.diff = -1; + newItems.push( item ); + + } + } + + // Sort internal list items by index order + internalListDiffInfo[ group.group ] = newItems.sort( function ( a, b ) { + if ( a.indexOrder === b.indexOrder ) { + + // When index order is the same, put removed item first + return a.diff === -1 ? -1 : 1; + + } + return a.indexOrder > b.indexOrder ? 1 : -1; + } ); + + internalListDiffInfo[ group.group ].changes = true; } - } + + break; + + case 'insert': + + // Get new doc internal list items for this group and mark as inserted + internalListDiffInfo[ group.group ] = getInternalListItems( + newDocNodeGroups[ group.group ].indexOrder, + this.newDocInternalListNode.children, + 1 + ); + internalListDiffInfo[ group.group ].changes = true; + break; + + case 'remove': + + // Get old doc internal list items for this group and mark as removed + internalListDiffInfo[ group.group ] = getInternalListItems( + oldDocNodeGroups[ group.group ].indexOrder, + this.oldDocInternalListNode.children, + -1 + ); + internalListDiffInfo[ group.group ].changes = true; + break; + } } diff --git a/src/ui/elements/ve.ui.DiffElement.js b/src/ui/elements/ve.ui.DiffElement.js index 92e2a9c..e52a4c1 100644 --- a/src/ui/elements/ve.ui.DiffElement.js +++ b/src/ui/elements/ve.ui.DiffElement.js @@ -34,10 +34,10 @@ this.oldDocInternalListNode = visualDiff.oldDocInternalListNode; // Diff - this.oldToNew = diff.docChildrenOldToNew; - this.newToOld = diff.docChildrenNewToOld; - this.insert = diff.docChildrenInsert; - this.remove = diff.docChildrenRemove; + this.oldToNew = diff.docDiff.rootChildrenOldToNew; + this.newToOld = diff.docDiff.rootChildrenNewToOld; + this.insert = diff.docDiff.rootChildrenInsert; + this.remove = diff.docDiff.rootChildrenRemove; this.internalListDiff = diff.internalListDiff; this.$overlays = $( '<div>' ).addClass( 've-ui-diffElement-overlays' ); -- To view, visit https://gerrit.wikimedia.org/r/368587 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: Idf7f694080f8f633147dd861ea0180140f16c1d8 Gerrit-PatchSet: 5 Gerrit-Project: VisualEditor/VisualEditor Gerrit-Branch: master Gerrit-Owner: Tchanders <thalia.e.c...@googlemail.com> Gerrit-Reviewer: Esanders <esand...@wikimedia.org> Gerrit-Reviewer: Jforrester <jforres...@wikimedia.org> Gerrit-Reviewer: Tchanders <thalia.e.c...@googlemail.com> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits