/* * Copyright 2000-2009 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.pom.tree.events.impl; import com.intellij.lang.ASTNode; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Pair; import com.intellij.pom.tree.events.ChangeInfo; import com.intellij.pom.tree.events.ReplaceChangeInfo; import com.intellij.pom.tree.events.TreeChange; import com.intellij.psi.impl.source.tree.LeafElement; import com.intellij.psi.impl.source.tree.TreeElement; import gnu.trove.THashMap; import org.jetbrains.annotations.NotNull; import java.util.*; public class TreeChangeImpl implements TreeChange { private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.source.tree.events.impl.TreeChangeImpl"); private final Map<ASTNode, ChangeInfo> myChanges = new THashMap<ASTNode, ChangeInfo>(); private final List<Pair<ASTNode,Integer>> mySortedChanges = new ArrayList<Pair<ASTNode, Integer>>(); // change, oldoffset private final ASTNode myParent; private static boolean ourDoChecks = ApplicationManager.getApplication().isEAP(); public TreeChangeImpl(ASTNode parent) { myParent = parent; } @Override public void addChange(ASTNode child, @NotNull ChangeInfo changeInfo) { LOG.assertTrue(child.getTreeParent() == myParent); final ChangeInfo current = myChanges.get(child); if(current != null && changeInfo.getChangeType() == ChangeInfo.CONTENTS_CHANGED){ return; } if(changeInfo.getChangeType() == ChangeInfo.REPLACE){ final ReplaceChangeInfoImpl replaceChangeInfo = (ReplaceChangeInfoImpl)changeInfo; final ASTNode replaced = replaceChangeInfo.getReplaced(); final ChangeInfo replacedInfo = myChanges.get(replaced); if(replacedInfo == null){ addChangeInternal(child, changeInfo); } else{ switch(replacedInfo.getChangeType()){ case ChangeInfo.REPLACE: replaceChangeInfo.setOldLength(replacedInfo.getOldLength()); replaceChangeInfo.setReplaced(((ReplaceChangeInfo)replacedInfo).getReplaced()); break; case ChangeInfo.ADD: changeInfo = ChangeInfoImpl.create(ChangeInfo.ADD, replaced); removeChangeInternal(replaced); break; } addChangeInternal(child, changeInfo); } return; } if(current != null && current.getChangeType() == ChangeInfo.REMOVED){ if(changeInfo.getChangeType() == ChangeInfo.ADD){ if (!(child instanceof LeafElement)) { // remove/add -> changed changeInfo = ChangeInfoImpl.create(ChangeInfo.CONTENTS_CHANGED, child); ((ChangeInfoImpl)changeInfo).setOldLength(current.getOldLength()); myChanges.put(child, changeInfo); } else { removeChangeInternal(child); } } return; } // add + remove == no op if(current != null && current.getChangeType() == ChangeInfo.ADD){ if(changeInfo.getChangeType() == ChangeInfo.REMOVED){ removeChangeInternal(child); } return; } if(changeInfo.getChangeType() == ChangeInfo.REMOVED){ if(child instanceof LeafElement){ final CharSequence charTabIndex = child.getChars(); if(checkLeaf(child.getTreeNext(), charTabIndex) || checkLeaf(child.getTreePrev(), charTabIndex)) return; } addChangeInternal(child, changeInfo); if (current != null) { ((ChangeInfoImpl)changeInfo).setOldLength(current.getOldLength()); } return; } if(current == null){ addChangeInternal(child, changeInfo); } } private void addChangeInternal(ASTNode child, ChangeInfo info){ if(!myChanges.containsKey(child)){ final int nodeOffset = getNodeOldOffset(child, info); addChangeAtOffset(child, nodeOffset); } myChanges.put(child, info); } private static boolean ourReportedDifferentAddChangeAtOffsetOptimization = false; private void addChangeAtOffset(final ASTNode child, final int nodeOffset) { int optimizedIndex = haveNotCalculated; Pair<ASTNode, Integer> element = new Pair<ASTNode, Integer>(child, Integer.valueOf(nodeOffset)); if (mySortedChanges.size() > 0) { // check adding at end Pair<ASTNode, Integer> pair = mySortedChanges.get(mySortedChanges.size() - 1); if (pair.getFirst() == child.getTreePrev() && pair.getSecond() <= nodeOffset) { optimizedIndex = mySortedChanges.size(); if (!ourDoChecks) { mySortedChanges.add(element); return; } } } int index = 0; for (Pair<ASTNode, Integer> pair : mySortedChanges) { if(child == pair.getFirst()) return; if(nodeOffset < pair.getSecond().intValue() || nodeOffset == pair.getSecond().intValue() && isAfter(pair.getFirst(), child)){ break; } index++; } int insertionIndex = optimizedIndex != haveNotCalculated ? optimizedIndex:index; if (insertionIndex == mySortedChanges.size()) mySortedChanges.add(element); else mySortedChanges.add(insertionIndex, element); if (optimizedIndex != haveNotCalculated && index != optimizedIndex && !ourReportedDifferentAddChangeAtOffsetOptimization) { ASTNode prev = child.getTreePrev(); Pair<ASTNode, Integer> pair = mySortedChanges.get(index); ChangeInfo prevChange = myChanges.get(prev); ChangeInfo prevChange2 = myChanges.get(pair.getFirst()); LOG.error("Failed to calculate optimized index for add change at offset: prev node:"+prev + ", prev change:" + prevChange + ",prev change length:" + (prevChange != null ? prevChange.getOldLength() : null) + ", prev text length:" + prev.getTextLength() + ",prev offset:" + mySortedChanges.get(mySortedChanges.size() - 1).getSecond() + ", node:" + child + ", nodeOffset:" + nodeOffset + ", optimizedIndex:"+optimizedIndex + ", real index:" + index + ", same node:" + (pair.getFirst() == child) + ", at place:"+ pair.getSecond() + ", node:" +pair.getFirst() + ", change:"+prevChange2 + ", prevChange oldLength:" + (prevChange2 != null ? prevChange2.getOldLength():null) + ", prevchange length2:" + pair.getFirst().getTextLength() + "," + toString()); ourReportedDifferentAddChangeAtOffsetOptimization = true; } } private static boolean isAfter(final ASTNode what, final ASTNode afterWhat) { ASTNode current = afterWhat.getTreeNext(); while(current != null){ if(current == what) return true; current = current.getTreeNext(); if(current != null && current.getTextLength() != 0) break; } return false; } private void removeChangeInternal(ASTNode child){ myChanges.remove(child); for(int i = 0, n = mySortedChanges.size(); i < n; i++){ if(child == mySortedChanges.get(i).getFirst()){ mySortedChanges.remove(i); break; } } } private boolean checkLeaf(final ASTNode treeNext, final CharSequence charTabIndex) { if(!(treeNext instanceof LeafElement)) return false; final ChangeInfo right = myChanges.get(treeNext); if(right != null && right.getChangeType() == ChangeInfo.ADD){ if(charTabIndex == treeNext.getChars()){ removeChangeInternal(treeNext); return true; } } return false; } @Override @NotNull public TreeElement[] getAffectedChildren() { final TreeElement[] treeElements = new TreeElement[myChanges.size()]; int index = 0; for (final Pair<ASTNode, Integer> pair : mySortedChanges) { treeElements[index++] = (TreeElement)pair.getFirst(); } return treeElements; } @Override public ChangeInfo getChangeByChild(ASTNode child) { return myChanges.get(child); } @Override public int getChildOffsetInNewTree(@NotNull ASTNode child) { return myParent.getStartOffset() + getNewOffset(child); } @Override public void composite(@NotNull TreeChange treeChange) { final TreeChangeImpl change = (TreeChangeImpl)treeChange; final Set<Map.Entry<ASTNode,ChangeInfo>> entries = change.myChanges.entrySet(); for (final Map.Entry<ASTNode, ChangeInfo> entry : entries) { addChange(entry.getKey(), entry.getValue()); } } @Override public boolean isEmpty() { return false; } @Override public void removeChange(ASTNode beforeEqualDepth) { removeChangeInternal(beforeEqualDepth); } @Override public void add(@NotNull final TreeChange value) { final TreeChangeImpl impl = (TreeChangeImpl)value; LOG.assertTrue(impl.myParent == myParent); for (final Pair<ASTNode, Integer> pair : impl.mySortedChanges) { final ASTNode child = pair.getFirst(); ChangeInfo change = impl.myChanges.get(child); if (change.getChangeType() == ChangeInfo.REMOVED) { final ChangeInfo oldChange = myChanges.get(child); if (oldChange != null) { switch (oldChange.getChangeType()) { case ChangeInfo.ADD: removeChangeInternal(child); break; case ChangeInfo.REPLACE: final ASTNode replaced = ((ReplaceChangeInfo)oldChange).getReplaced(); removeChangeInternal(child); myChanges.put(replaced, ChangeInfoImpl.create(ChangeInfo.REMOVED, replaced)); addChangeAtOffset(replaced, getOldOffset(pair.getSecond().intValue())); break; case ChangeInfo.CONTENTS_CHANGED: ((ChangeInfoImpl)change).setOldLength(oldChange.getOldLength()); myChanges.put(child, change); break; } } else { myChanges.put(child, change); addChangeAtOffset(child, getOldOffset(pair.getSecond().intValue())); } } else if (change.getChangeType() == ChangeInfo.REPLACE) { ReplaceChangeInfo replaceChangeInfo = (ReplaceChangeInfo)change; final ASTNode replaced = replaceChangeInfo.getReplaced(); final ChangeInfo oldChange = myChanges.get(replaced); if (oldChange != null) { switch (oldChange.getChangeType()) { case ChangeInfo.ADD: change = ChangeInfoImpl.create(ChangeInfo.ADD, child); break; case ChangeInfo.CONTENTS_CHANGED: ((ChangeInfoImpl)change).setOldLength(oldChange.getOldLength()); break; case ChangeInfo.REPLACE: final ASTNode oldReplaced = ((ReplaceChangeInfo)oldChange).getReplaced(); ReplaceChangeInfoImpl rep = new ReplaceChangeInfoImpl(child); rep.setReplaced(oldReplaced); change = rep; break; } removeChangeInternal(replaced); } addChange(child, change); } else { addChange(child, change); } } } @Override public int getOldLength() { int oldLength = ((TreeElement)myParent).getNotCachedLength(); for (final Map.Entry<ASTNode, ChangeInfo> entry : myChanges.entrySet()) { final ASTNode key = entry.getKey(); final ChangeInfo change = entry.getValue(); final int length = ((TreeElement)key).getNotCachedLength(); switch (change.getChangeType()) { case ChangeInfo.ADD: oldLength -= length; break; case ChangeInfo.REMOVED: oldLength += length; break; case ChangeInfo.REPLACE: case ChangeInfo.CONTENTS_CHANGED: oldLength += change.getOldLength() - length; break; } } return oldLength; } private static int getNewLength(ChangeInfo change, ASTNode node){ if(change.getChangeType() == ChangeInfo.REMOVED) return 0; return node.getTextLength(); } private static final int haveNotCalculated = -1; private static boolean ourReportedDifferentOptimizedNodeOldOffset = false; private int getOptimizedNodeOldOffset(ASTNode child, ChangeInfo changeInfo) { // we usually add / remove ranges so old offset can be tried to calculate from change with previous sibling ASTNode prevSibling = child.getTreePrev(); if (prevSibling != null) { if (mySortedChanges.size() > 0) { Pair<ASTNode, Integer> pair = mySortedChanges.get(mySortedChanges.size() - 1); if (pair.getFirst() == prevSibling) { ChangeInfo prevSiblingChange = myChanges.get(prevSibling); if ((prevSiblingChange.getChangeType() == ChangeInfo.REMOVED && changeInfo.getChangeType() == ChangeInfo.REMOVED ) || (prevSiblingChange.getChangeType() == ChangeInfo.ADD && changeInfo.getChangeType() == ChangeInfo.ADD ) ) { int optimizedResult = pair.getSecond() + prevSiblingChange.getOldLength(); if (ourDoChecks && !ourReportedDifferentOptimizedNodeOldOffset) { int oldOffset = calculateOldOffsetLinearly(child); if (optimizedResult != oldOffset) { LOG.error("Failed optimized node old offset check:"+changeInfo + ", previous:" + prevSibling + "," + prevSiblingChange); ourReportedDifferentOptimizedNodeOldOffset = true; optimizedResult = oldOffset; } } return optimizedResult; } } } } return haveNotCalculated; } private int getNodeOldOffset(ASTNode child, ChangeInfo changeInfo){ LOG.assertTrue(child.getTreeParent() == myParent); int oldOffsetInParent = getOptimizedNodeOldOffset(child, changeInfo); if (oldOffsetInParent == haveNotCalculated) { oldOffsetInParent = calculateOldOffsetLinearly(child); } return oldOffsetInParent; } private int calculateOldOffsetLinearly(ASTNode child) { int oldOffsetInParent = 0; // find last changed element before child ASTNode current = myParent.getFirstChildNode(); // calculate not changed elements while(current != child) { if (!myChanges.containsKey(current)) { oldOffsetInParent += current.getTextLength(); } current = current.getTreeNext(); } for (Pair<ASTNode, Integer> offset : mySortedChanges) { if(offset.getSecond() > oldOffsetInParent) break; final ChangeInfo change = myChanges.get(offset.getFirst()); oldOffsetInParent += change.getOldLength(); } return oldOffsetInParent; } private int getOldOffset(int offset){ for (Pair<ASTNode, Integer> pair : mySortedChanges) { if(pair.getSecond() > offset) break; final ChangeInfo change = myChanges.get(pair.getFirst()); offset += change.getOldLength() - getNewLength(change, pair.getFirst()); } return offset; } private int myLastOffsetInNewTree; private ASTNode myLastNode; private static boolean ourReportedDifferentEnableGetNewOffset = false; private int getNewOffset(ASTNode node){ int optimizedResult = haveNotCalculated; ASTNode prev = node.getTreePrev(); if (myLastNode == prev) { ChangeInfo prevChangeInfo = myChanges.get(prev); ChangeInfo changeInfo = myChanges.get(node); // newoffset of removed element is the same of removed previous sibling if (prevChangeInfo != null && changeInfo != null && prevChangeInfo.getChangeType() == ChangeInfo.REMOVED && changeInfo.getChangeType() == ChangeInfo.REMOVED ) { optimizedResult = myLastOffsetInNewTree; myLastNode = node; myLastOffsetInNewTree = optimizedResult; if (!ourDoChecks) return optimizedResult; } } int currentOffsetInNewTree = 0; try { ASTNode current = myParent.getFirstChildNode(); int i = 0; Pair<ASTNode, Integer> currentChange = i < mySortedChanges.size() ? mySortedChanges.get(i) : null; int currentOldOffset = 0; while(current != null) { boolean counted = false; while(currentChange != null && currentOldOffset == currentChange.getSecond().intValue()){ if(currentChange.getFirst() == node) { myLastNode = node; myLastOffsetInNewTree = currentOffsetInNewTree; return currentOffsetInNewTree; } if(current == currentChange.getFirst()){ final int textLength = current.getTextLength(); counted = true; current = current.getTreeNext(); currentOffsetInNewTree += textLength; } final ChangeInfo changeInfo = myChanges.get(currentChange.getFirst()); currentOldOffset += changeInfo.getOldLength(); ++i; currentChange = i < mySortedChanges.size() ? mySortedChanges.get(i) : null; } if(current == null) break; if(!counted){ final int textLength = current.getTextLength(); currentOldOffset += textLength; current = current.getTreeNext(); currentOffsetInNewTree += textLength; } } } finally { if (optimizedResult != haveNotCalculated && optimizedResult != currentOffsetInNewTree && !ourReportedDifferentEnableGetNewOffset ) { LOG.error("Failed to calculate optimized getNewOffset:"+myChanges.get(node) + "," + prev + "," + myChanges.get(prev)); ourReportedDifferentEnableGetNewOffset = true; currentOffsetInNewTree = optimizedResult; // always use optimized result } } return currentOffsetInNewTree; } @SuppressWarnings({"HardCodedStringLiteral"}) public String toString(){ final StringBuilder buffer = new StringBuilder(); final Iterator<Pair<ASTNode, Integer>> iterator = mySortedChanges.iterator(); while (iterator.hasNext()) { final Pair<ASTNode, Integer> pair = iterator.next(); final ASTNode node = pair.getFirst(); buffer.append("("); buffer.append(node.getElementType().toString()); buffer.append(" at ").append(pair.getSecond()).append(", "); ChangeInfo child = getChangeByChild(node); buffer.append(child != null ? child.toString():"null"); buffer.append(")"); if(iterator.hasNext()) buffer.append(", "); } return buffer.toString(); } }