blob: dd5ad7414835d04db5de14da84c17ef389054bcc [file] [log] [blame]
/*
* 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();
}
}