blob: 80d62f72d72bab06bc667aad666e5670ee48e920 [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.psi.impl.source.text;
import com.intellij.lang.ASTNode;
import com.intellij.lang.Language;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Attachment;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.ex.DocumentBulkUpdateListener;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.PlainTextLanguage;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.impl.PsiManagerEx;
import com.intellij.psi.impl.PsiManagerImpl;
import com.intellij.psi.impl.PsiTreeChangeEventImpl;
import com.intellij.psi.impl.source.DummyHolder;
import com.intellij.psi.impl.source.DummyHolderFactory;
import com.intellij.psi.impl.source.PsiFileImpl;
import com.intellij.psi.impl.source.tree.*;
import com.intellij.psi.templateLanguages.ITemplateDataElementType;
import com.intellij.psi.text.BlockSupport;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.IReparseableElementType;
import com.intellij.testFramework.LightVirtualFile;
import com.intellij.util.CharTable;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.diff.DiffTree;
import com.intellij.util.diff.DiffTreeChangeBuilder;
import com.intellij.util.diff.FlyweightCapableTreeStructure;
import com.intellij.util.diff.ShallowNodeComparator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class BlockSupportImpl extends BlockSupport {
private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.source.text.BlockSupportImpl");
public BlockSupportImpl(Project project) {
project.getMessageBus().connect().subscribe(DocumentBulkUpdateListener.TOPIC, new DocumentBulkUpdateListener.Adapter() {
@Override
public void updateStarted(@NotNull final Document doc) {
doc.putUserData(DO_NOT_REPARSE_INCREMENTALLY, Boolean.TRUE);
}
});
}
@Override
public void reparseRange(PsiFile file, int startOffset, int endOffset, CharSequence newTextS) throws IncorrectOperationException {
LOG.assertTrue(file.isValid());
final PsiFileImpl psiFile = (PsiFileImpl)file;
final Document document = psiFile.getViewProvider().getDocument();
assert document != null;
document.replaceString(startOffset, endOffset, newTextS);
PsiDocumentManager.getInstance(psiFile.getProject()).commitDocument(document);
}
@Override
@NotNull
public DiffLog reparseRange(@NotNull final PsiFile file,
@NotNull TextRange changedPsiRange,
@NotNull final CharSequence newFileText,
@NotNull final ProgressIndicator indicator) {
final PsiFileImpl fileImpl = (PsiFileImpl)file;
Project project = fileImpl.getProject();
final FileElement treeFileElement = fileImpl.getTreeElement();
final CharTable charTable = treeFileElement.getCharTable();
final int textLength = newFileText.length();
int lengthShift = textLength - treeFileElement.getTextLength();
if (treeFileElement.getElementType() instanceof ITemplateDataElementType || isTooDeep(file)) {
// unable to perform incremental reparse for template data in JSP, or in exceptionally deep trees
return makeFullParse(treeFileElement, newFileText, textLength, fileImpl, indicator);
}
final ASTNode leafAtStart = treeFileElement.findLeafElementAt(Math.max(0, changedPsiRange.getStartOffset() - 1));
final ASTNode leafAtEnd = treeFileElement.findLeafElementAt(changedPsiRange.getEndOffset());
ASTNode node = leafAtStart != null && leafAtEnd != null ? TreeUtil.findCommonParent(leafAtStart, leafAtEnd) : treeFileElement;
Language baseLanguage = file.getViewProvider().getBaseLanguage();
while (node != null && !(node instanceof FileElement)) {
IElementType elementType = node.getElementType();
if (elementType instanceof IReparseableElementType) {
final TextRange textRange = node.getTextRange();
final IReparseableElementType reparseable = (IReparseableElementType)elementType;
if (baseLanguage.isKindOf(reparseable.getLanguage()) && textRange.getLength() + lengthShift > 0) {
final int start = textRange.getStartOffset();
final int end = start + textRange.getLength() + lengthShift;
if (end > newFileText.length()) {
reportInconsistentLength(file, newFileText, node, start, end);
break;
}
CharSequence newTextStr = newFileText.subSequence(start, end);
if (reparseable.isParsable(node.getTreeParent(), newTextStr, baseLanguage, project)) {
ASTNode chameleon = reparseable.createNode(newTextStr);
if (chameleon != null) {
DummyHolder holder = DummyHolderFactory.createHolder(fileImpl.getManager(), null, node.getPsi(), charTable);
holder.getTreeElement().rawAddChildren((TreeElement)chameleon);
if (holder.getTextLength() != newTextStr.length()) {
String details = ApplicationManager.getApplication().isInternal()
? "text=" + newTextStr + "; treeText=" + holder.getText() + ";"
: "";
LOG.error("Inconsistent reparse: " + details + " type=" + elementType);
}
return mergeTrees(fileImpl, node, chameleon, indicator);
}
}
}
}
node = node.getTreeParent();
}
return makeFullParse(node, newFileText, textLength, fileImpl, indicator);
}
private static void reportInconsistentLength(PsiFile file, CharSequence newFileText, ASTNode node, int start, int end) {
String message = "Index out of bounds: type=" + node.getElementType() +
"; file=" + file +
"; file.class=" + file.getClass() +
"; start=" + start +
"; end=" + end +
"; length=" + node.getTextLength();
String newTextBefore = newFileText.subSequence(0, start).toString();
String oldTextBefore = file.getText().subSequence(0, start).toString();
if (oldTextBefore.equals(newTextBefore)) {
message += "; oldTextBefore==newTextBefore";
}
LOG.error(message,
new Attachment(file.getName() + "_oldNodeText.txt", node.getText()),
new Attachment(file.getName() + "_oldFileText.txt", file.getText()),
new Attachment(file.getName() + "_newFileText.txt", newFileText.toString())
);
}
@NotNull
private static DiffLog makeFullParse(ASTNode parent,
@NotNull CharSequence newFileText,
int textLength,
@NotNull PsiFileImpl fileImpl,
@NotNull ProgressIndicator indicator) {
if (fileImpl instanceof PsiCodeFragment) {
final FileElement holderElement = new DummyHolder(fileImpl.getManager(), null).getTreeElement();
holderElement.rawAddChildren(fileImpl.createContentLeafElement(holderElement.getCharTable().intern(newFileText, 0, textLength)));
DiffLog diffLog = new DiffLog();
diffLog.appendReplaceFileElement((FileElement)parent, (FileElement)holderElement.getFirstChildNode());
return diffLog;
}
else {
FileViewProvider viewProvider = fileImpl.getViewProvider();
viewProvider.getLanguages();
FileType fileType = viewProvider.getVirtualFile().getFileType();
String fileName = fileImpl.getName();
final LightVirtualFile lightFile = new LightVirtualFile(fileName, fileType, newFileText, viewProvider.getVirtualFile().getCharset(),
fileImpl.getViewProvider().getModificationStamp());
lightFile.setOriginalFile(viewProvider.getVirtualFile());
FileViewProvider copy = viewProvider.createCopy(lightFile);
copy.getLanguages();
SingleRootFileViewProvider.doNotCheckFileSizeLimit(lightFile); // optimization: do not convert file contents to bytes to determine if we should codeinsight it
PsiFileImpl newFile = getFileCopy(fileImpl, copy);
newFile.setOriginalFile(fileImpl);
final FileElement newFileElement = (FileElement)newFile.getNode();
final FileElement oldFileElement = (FileElement)fileImpl.getNode();
assert oldFileElement != null && newFileElement != null;
DiffLog diffLog = mergeTrees(fileImpl, oldFileElement, newFileElement, indicator);
((PsiManagerEx)fileImpl.getManager()).getFileManager().setViewProvider(lightFile, null);
return diffLog;
}
}
@NotNull
public static PsiFileImpl getFileCopy(PsiFileImpl originalFile, FileViewProvider providerCopy) {
FileViewProvider viewProvider = originalFile.getViewProvider();
Language language = originalFile.getLanguage();
PsiFileImpl newFile = (PsiFileImpl)providerCopy.getPsi(language);
if (newFile == null && language == PlainTextLanguage.INSTANCE && originalFile == viewProvider.getPsi(viewProvider.getBaseLanguage())) {
newFile = (PsiFileImpl)providerCopy.getPsi(providerCopy.getBaseLanguage());
}
if (newFile == null) {
throw new RuntimeException("View provider " + viewProvider + " refused to parse text with " + language +
"; languages: " + viewProvider.getLanguages() +
"; base: " + viewProvider.getBaseLanguage() +
"; copy: " + providerCopy +
"; copy.base: " + providerCopy.getBaseLanguage() +
"; vFile: " + viewProvider.getVirtualFile() +
"; copy.vFile: " + providerCopy.getVirtualFile() +
"; fileType: " + viewProvider.getVirtualFile().getFileType() +
"; copy.original(): " +
(providerCopy.getVirtualFile() instanceof LightVirtualFile ? ((LightVirtualFile)providerCopy.getVirtualFile()).getOriginalFile() : null));
}
return newFile;
}
@NotNull
private static DiffLog replaceElementWithEvents(final CompositeElement oldRoot,
final CompositeElement newRoot) {
DiffLog diffLog = new DiffLog();
diffLog.appendReplaceElementWithEvents(oldRoot, newRoot);
return diffLog;
}
@NotNull
public static DiffLog mergeTrees(@NotNull final PsiFileImpl fileImpl,
@NotNull final ASTNode oldRoot,
@NotNull final ASTNode newRoot,
@NotNull ProgressIndicator indicator) {
if (newRoot instanceof FileElement) {
((FileElement)newRoot).setCharTable(fileImpl.getTreeElement().getCharTable());
}
try {
newRoot.putUserData(TREE_TO_BE_REPARSED, oldRoot);
if (isReplaceWholeNode(fileImpl, newRoot)) {
DiffLog treeChangeEvent = replaceElementWithEvents((CompositeElement)oldRoot, (CompositeElement)newRoot);
fileImpl.putUserData(TREE_DEPTH_LIMIT_EXCEEDED, Boolean.TRUE);
return treeChangeEvent;
}
newRoot.getFirstChildNode(); // maybe reparsed in PsiBuilderImpl and have thrown exception here
}
catch (ReparsedSuccessfullyException e) {
// reparsed in PsiBuilderImpl
return e.getDiffLog();
}
finally {
newRoot.putUserData(TREE_TO_BE_REPARSED, null);
}
final ASTShallowComparator comparator = new ASTShallowComparator(indicator);
final ASTStructure treeStructure = createInterruptibleASTStructure(newRoot, indicator);
DiffLog diffLog = new DiffLog();
diffTrees(oldRoot, diffLog, comparator, treeStructure, indicator);
return diffLog;
}
public static <T> void diffTrees(@NotNull final ASTNode oldRoot,
@NotNull final DiffTreeChangeBuilder<ASTNode, T> builder,
@NotNull final ShallowNodeComparator<ASTNode, T> comparator,
@NotNull final FlyweightCapableTreeStructure<T> newTreeStructure,
final ProgressIndicator indicator) {
TreeUtil.ensureParsedRecursivelyCheckingProgress(oldRoot, indicator);
DiffTree.diff(createInterruptibleASTStructure(oldRoot, indicator), newTreeStructure, comparator, builder);
}
private static ASTStructure createInterruptibleASTStructure(@NotNull final ASTNode oldRoot, @Nullable final ProgressIndicator indicator) {
return new ASTStructure(oldRoot) {
@Override
public int getChildren(@NotNull ASTNode astNode, @NotNull Ref<ASTNode[]> into) {
if (indicator != null) {
indicator.checkCanceled();
}
return super.getChildren(astNode, into);
}
};
}
private static boolean isReplaceWholeNode(@NotNull PsiFileImpl fileImpl, @NotNull ASTNode newRoot) throws ReparsedSuccessfullyException{
final Boolean data = fileImpl.getUserData(DO_NOT_REPARSE_INCREMENTALLY);
if (data != null) fileImpl.putUserData(DO_NOT_REPARSE_INCREMENTALLY, null);
boolean explicitlyMarkedDeep = Boolean.TRUE.equals(data);
if (explicitlyMarkedDeep || isTooDeep(fileImpl)) {
return true;
}
final ASTNode childNode = newRoot.getFirstChildNode(); // maybe reparsed in PsiBuilderImpl and have thrown exception here
boolean childTooDeep = isTooDeep(childNode);
if (childTooDeep) {
childNode.putUserData(TREE_DEPTH_LIMIT_EXCEEDED, null);
fileImpl.putUserData(TREE_DEPTH_LIMIT_EXCEEDED, Boolean.TRUE);
}
return childTooDeep;
}
public static void sendBeforeChildrenChangeEvent(@NotNull PsiManagerImpl manager, @NotNull PsiElement scope, boolean isGenericChange) {
if(!scope.isPhysical()) {
manager.beforeChange(false);
return;
}
PsiTreeChangeEventImpl event = new PsiTreeChangeEventImpl(manager);
event.setParent(scope);
event.setFile(scope.getContainingFile());
TextRange range = scope.getTextRange();
event.setOffset(range == null ? 0 : range.getStartOffset());
event.setOldLength(scope.getTextLength());
// the "generic" event is being sent on every PSI change. It does not carry any specific info except the fact that "something has changed"
event.setGenericChange(isGenericChange);
manager.beforeChildrenChange(event);
}
public static void sendAfterChildrenChangedEvent(@NotNull PsiManagerImpl manager,
@NotNull PsiFile scope,
int oldLength,
boolean isGenericChange) {
if(!scope.isPhysical()) {
manager.afterChange(false);
return;
}
PsiTreeChangeEventImpl event = new PsiTreeChangeEventImpl(manager);
event.setParent(scope);
event.setFile(scope);
event.setOffset(0);
event.setOldLength(oldLength);
event.setGenericChange(isGenericChange);
manager.childrenChanged(event);
}
}