| /* |
| * 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); |
| } |
| } |