| /* |
| * Copyright 2000-2014 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 = Pair.create(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(); |
| } |
| } |