/*
* Copyright 2000-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.util.diff;
import com.intellij.openapi.util.Ref;
import com.intellij.util.ThreeState;
import com.intellij.util.text.CharArrayUtil;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
/**
* @author max
*/
public class DiffTree<OT, NT> {
private static final int CHANGE_PARENT_VERSUS_CHILDREN_THRESHOLD = 20;
private final FlyweightCapableTreeStructure<OT> myOldTree;
private final FlyweightCapableTreeStructure<NT> myNewTree;
private final ShallowNodeComparator<OT, NT> myComparator;
private final List<Ref<OT[]>> myOldChildrenLists = new ArrayList<Ref<OT[]>>();
private final List<Ref<NT[]>> myNewChildrenLists = new ArrayList<Ref<NT[]>>();
private final CharSequence myOldText;
private final CharSequence myNewText;
private final int myOldTreeStart;
private final int myNewTreeStart;
private DiffTree(@NotNull FlyweightCapableTreeStructure<OT> oldTree,
@NotNull FlyweightCapableTreeStructure<NT> newTree,
@NotNull ShallowNodeComparator<OT, NT> comparator,
@NotNull CharSequence oldText) {
myOldTree = oldTree;
myNewTree = newTree;
myComparator = comparator;
myOldText = oldText;
myOldTreeStart = oldTree.getStartOffset(oldTree.getRoot());
myNewText = newTree.toString(newTree.getRoot());
myNewTreeStart = newTree.getStartOffset(newTree.getRoot());
}
public static <OT, NT> void diff(@NotNull FlyweightCapableTreeStructure<OT> oldTree,
@NotNull FlyweightCapableTreeStructure<NT> newTree,
@NotNull ShallowNodeComparator<OT, NT> comparator,
@NotNull DiffTreeChangeBuilder<OT, NT> consumer,
@NotNull CharSequence oldText) {
final DiffTree<OT, NT> tree = new DiffTree<OT, NT>(oldTree, newTree, comparator, oldText);
tree.build(oldTree.getRoot(), newTree.getRoot(), 0, consumer);
}
private enum CompareResult {
EQUAL, // 100% equal
DRILL_DOWN_NEEDED, // element types are equal, but elements are composite
TYPE_ONLY, // only element types are equal
NOT_EQUAL, // 100% different
}
@NotNull
private static <OT, NT> DiffTreeChangeBuilder<OT, NT> emptyConsumer() {
//noinspection unchecked
return EMPTY_CONSUMER;
}
private static final DiffTreeChangeBuilder EMPTY_CONSUMER = new DiffTreeChangeBuilder() {
@Override
public void nodeReplaced(@NotNull Object oldChild, @NotNull Object newChild) {
}
@Override
public void nodeDeleted(@NotNull Object oldParent, @NotNull Object oldNode) {
}
@Override
public void nodeInserted(@NotNull Object oldParent, @NotNull Object newNode, int pos) {
}
};
@NotNull
private CompareResult build(@NotNull OT oldN, @NotNull NT newN, int level, @NotNull DiffTreeChangeBuilder<OT, NT> consumer) {
OT oldNode = myOldTree.prepareForGetChildren(oldN);
NT newNode = myNewTree.prepareForGetChildren(newN);
if (level == myNewChildrenLists.size()) {
myNewChildrenLists.add(new Ref<NT[]>());
myOldChildrenLists.add(new Ref<OT[]>());
}
final Ref<OT[]> oldChildrenR = myOldChildrenLists.get(level);
int oldChildrenSize = myOldTree.getChildren(oldNode, oldChildrenR);
final OT[] oldChildren = oldChildrenR.get();
final Ref<NT[]> newChildrenR = myNewChildrenLists.get(level);
int newChildrenSize = myNewTree.getChildren(newNode, newChildrenR);
final NT[] newChildren = newChildrenR.get();
CompareResult result;
if (Math.abs(oldChildrenSize - newChildrenSize) > CHANGE_PARENT_VERSUS_CHILDREN_THRESHOLD) {
consumer.nodeReplaced(oldNode, newNode);
result = CompareResult.NOT_EQUAL;
}
else if (oldChildrenSize == 0 && newChildrenSize == 0) {
if (!myComparator.hashCodesEqual(oldNode, newNode) || !myComparator.typesEqual(oldNode, newNode)) {
consumer.nodeReplaced(oldNode, newNode);
result = CompareResult.NOT_EQUAL;
}
else {
result = CompareResult.EQUAL;
}
}
else {
final ShallowNodeComparator<OT, NT> comparator = myComparator;
int minSize = Math.min(oldChildrenSize, newChildrenSize);
int suffixLength = match(oldChildren, oldChildrenSize - 1, newChildren, newChildrenSize - 1, level, -1, minSize);
// for equal size old and new children we have to compare one element less because it was already checked in (unsuccessful) suffix match
int maxPrefixLength = minSize - suffixLength - (oldChildrenSize == newChildrenSize && suffixLength < minSize ? 1 : 0);
int prefixLength = match(oldChildren, 0, newChildren, 0, level, 1, maxPrefixLength);
if (oldChildrenSize == newChildrenSize && suffixLength + prefixLength == oldChildrenSize) {
result = CompareResult.EQUAL;
}
else if (consumer == emptyConsumer()) {
result = CompareResult.NOT_EQUAL;
}
else {
int oldIndex = prefixLength;
int newIndex = prefixLength;
while (oldIndex < oldChildrenSize - suffixLength || newIndex < newChildrenSize - suffixLength) {
OT oldChild1 = oldIndex < oldChildrenSize - suffixLength ? oldChildren[oldIndex] : null;
OT oldChild2 = oldIndex < oldChildrenSize - suffixLength - 1 ? oldChildren[oldIndex + 1] : null;
OT oldChild3 = oldIndex < oldChildrenSize - suffixLength - 2 ? oldChildren[oldIndex + 2] : null;
NT newChild1 = newIndex < newChildrenSize - suffixLength ? newChildren[newIndex] : null;
NT newChild2 = newIndex < newChildrenSize - suffixLength - 1 ? newChildren[newIndex + 1] : null;
NT newChild3 = newIndex < newChildrenSize - suffixLength - 2 ? newChildren[newIndex + 2] : null;
CompareResult c11 = looksEqual(comparator, oldChild1, newChild1);
if (c11 == CompareResult.EQUAL || c11 == CompareResult.DRILL_DOWN_NEEDED) {
if (c11 == CompareResult.DRILL_DOWN_NEEDED) {
build(oldChild1, newChild1, level + 1, consumer);
}
oldIndex++;
newIndex++;
continue;
}
if (c11 == CompareResult.TYPE_ONLY) {
CompareResult c21 = looksEqual(comparator, oldChild2, newChild1);
if (c21 == CompareResult.EQUAL || c21 == CompareResult.DRILL_DOWN_NEEDED) {
consumer.nodeDeleted(oldNode, oldChild1);
oldIndex++;
continue;
}
CompareResult c12 = looksEqual(comparator, oldChild1, newChild2);
if (c12 == CompareResult.EQUAL || c12 == CompareResult.DRILL_DOWN_NEEDED) {
consumer.nodeInserted(oldNode, newChild1, newIndex);
newIndex++;
continue;
}
consumer.nodeReplaced(oldChild1, newChild1);
oldIndex++;
newIndex++;
continue;
}
CompareResult c12 = looksEqual(comparator, oldChild1, newChild2);
if (c12 == CompareResult.EQUAL || c12 == CompareResult.DRILL_DOWN_NEEDED) {
consumer.nodeInserted(oldNode, newChild1, newIndex);
newIndex++;
continue;
}
CompareResult c21 = looksEqual(comparator, oldChild2, newChild1);
if (c21 == CompareResult.EQUAL || c21 == CompareResult.DRILL_DOWN_NEEDED || c21 == CompareResult.TYPE_ONLY) {
consumer.nodeDeleted(oldNode, oldChild1);
oldIndex++;
continue;
}
if (c12 == CompareResult.TYPE_ONLY) {
consumer.nodeInserted(oldNode, newChild1, newIndex);
newIndex++;
continue;
}
if (oldChild1 == null) {
consumer.nodeInserted(oldNode, newChild1, newIndex);
newIndex++;
continue;
}
if (newChild1 == null) {
consumer.nodeDeleted(oldNode, oldChild1);
oldIndex++;
continue;
}
// check that maybe two children are inserted/deleted
// (which frequently is a case when e.g. a PsiMethod inserted, the trailing PsiWhiteSpace is appended too)
if (oldChild3 != null || newChild3 != null) {
CompareResult c13 = looksEqual(comparator, oldChild1, newChild3);
if (c13 == CompareResult.EQUAL || c13 == CompareResult.DRILL_DOWN_NEEDED || c13 == CompareResult.TYPE_ONLY) {
consumer.nodeInserted(oldNode, newChild1, newIndex);
newIndex++;
consumer.nodeInserted(oldNode, newChild2, newIndex);
newIndex++;
continue;
}
CompareResult c31 = looksEqual(comparator, oldChild3, newChild1);
if (c31 == CompareResult.EQUAL || c31 == CompareResult.DRILL_DOWN_NEEDED || c31 == CompareResult.TYPE_ONLY) {
consumer.nodeDeleted(oldNode, oldChild1);
consumer.nodeDeleted(oldNode, oldChild2);
oldIndex++;
oldIndex++;
continue;
}
}
// last resort: maybe the last elements are more similar?
OT oldLastChild = oldIndex < oldChildrenSize - suffixLength ? oldChildren[oldChildrenSize - suffixLength - 1] : null;
NT newLastChild = newIndex < newChildrenSize - suffixLength ? newChildren[newChildrenSize - suffixLength - 1] : null;
CompareResult c = oldLastChild == null || newLastChild == null ? CompareResult.NOT_EQUAL : looksEqual(comparator, oldLastChild, newLastChild);
if (c == CompareResult.EQUAL || c == CompareResult.TYPE_ONLY || c == CompareResult.DRILL_DOWN_NEEDED) {
if (c == CompareResult.DRILL_DOWN_NEEDED) {
build(oldLastChild, newLastChild, level + 1, consumer);
}
else {
consumer.nodeReplaced(oldLastChild, newLastChild);
}
suffixLength++;
continue;
}
consumer.nodeReplaced(oldChild1, newChild1);
oldIndex++;
newIndex++;
}
result = CompareResult.NOT_EQUAL;
}
}
myOldTree.disposeChildren(oldChildren, oldChildrenSize);
myNewTree.disposeChildren(newChildren, newChildrenSize);
return result;
}
// tries to match as many nodes as possible from the beginning (if step=1) of from the end (if step =-1)
// returns number of nodes matched
private int match(OT[] oldChildren,
int oldIndex,
NT[] newChildren,
int newIndex,
int level,
int step, // 1 if we go from the start to the end; -1 if we go from the end to the start
int maxLength) {
int delta = 0;
while (delta != maxLength*step) {
OT oldChild = oldChildren[oldIndex + delta];
NT newChild = newChildren[newIndex + delta];
CompareResult c11 = looksEqual(myComparator, oldChild, newChild);
if (c11 == CompareResult.DRILL_DOWN_NEEDED) {
c11 = textMatch(oldChild, newChild) ? build(oldChild, newChild, level + 1, DiffTree.<OT, NT>emptyConsumer()) : CompareResult.NOT_EQUAL;
assert c11 != CompareResult.DRILL_DOWN_NEEDED;
}
if (c11 != CompareResult.EQUAL) {
break;
}
delta += step;
}
return delta*step;
}
private boolean textMatch(OT oldChild, NT newChild) {
int oldStart = myOldTree.getStartOffset(oldChild) - myOldTreeStart;
int oldEnd = myOldTree.getEndOffset(oldChild) - myOldTreeStart;
int newStart = myNewTree.getStartOffset(newChild) - myNewTreeStart;
int newEnd = myNewTree.getEndOffset(newChild) - myNewTreeStart;
// drill down only if node texts match, but when they do, match all the way down unconditionally
return CharArrayUtil.regionMatches(myOldText, oldStart, oldEnd, myNewText, newStart, newEnd);
}
@NotNull
private CompareResult looksEqual(@NotNull ShallowNodeComparator<OT, NT> comparator, OT oldChild1, NT newChild1) {
if (oldChild1 == null || newChild1 == null) {
return oldChild1 == newChild1 ? CompareResult.EQUAL : CompareResult.NOT_EQUAL;
}
if (!comparator.typesEqual(oldChild1, newChild1)) return CompareResult.NOT_EQUAL;
ThreeState ret = comparator.deepEqual(oldChild1, newChild1);
if (ret == ThreeState.YES) return CompareResult.EQUAL;
if (ret == ThreeState.UNSURE) return CompareResult.DRILL_DOWN_NEEDED;
return CompareResult.TYPE_ONLY;
}
}