EA-56496 (diagnostic)
[idea/community.git] / platform / core-impl / src / com / intellij / psi / impl / source / text / BlockSupportImpl.java
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.intellij.psi.impl.source.text;
18
19 import com.intellij.lang.ASTNode;
20 import com.intellij.lang.Language;
21 import com.intellij.openapi.application.ApplicationManager;
22 import com.intellij.openapi.diagnostic.Attachment;
23 import com.intellij.openapi.diagnostic.Logger;
24 import com.intellij.openapi.editor.Document;
25 import com.intellij.openapi.editor.ex.DocumentBulkUpdateListener;
26 import com.intellij.openapi.fileTypes.FileType;
27 import com.intellij.openapi.fileTypes.PlainTextLanguage;
28 import com.intellij.openapi.progress.ProgressIndicator;
29 import com.intellij.openapi.project.Project;
30 import com.intellij.openapi.util.Couple;
31 import com.intellij.openapi.util.Ref;
32 import com.intellij.openapi.util.TextRange;
33 import com.intellij.psi.*;
34 import com.intellij.psi.impl.PsiManagerEx;
35 import com.intellij.psi.impl.PsiManagerImpl;
36 import com.intellij.psi.impl.PsiTreeChangeEventImpl;
37 import com.intellij.psi.impl.source.DummyHolder;
38 import com.intellij.psi.impl.source.DummyHolderFactory;
39 import com.intellij.psi.impl.source.PsiFileImpl;
40 import com.intellij.psi.impl.source.tree.*;
41 import com.intellij.psi.templateLanguages.ITemplateDataElementType;
42 import com.intellij.psi.text.BlockSupport;
43 import com.intellij.psi.tree.IElementType;
44 import com.intellij.psi.tree.IReparseableElementType;
45 import com.intellij.testFramework.LightVirtualFile;
46 import com.intellij.util.CharTable;
47 import com.intellij.util.IncorrectOperationException;
48 import com.intellij.util.diff.DiffTree;
49 import com.intellij.util.diff.DiffTreeChangeBuilder;
50 import com.intellij.util.diff.FlyweightCapableTreeStructure;
51 import com.intellij.util.diff.ShallowNodeComparator;
52 import org.jetbrains.annotations.NotNull;
53 import org.jetbrains.annotations.Nullable;
54
55 public class BlockSupportImpl extends BlockSupport {
56   private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.source.text.BlockSupportImpl");
57
58   public BlockSupportImpl(Project project) {
59     project.getMessageBus().connect().subscribe(DocumentBulkUpdateListener.TOPIC, new DocumentBulkUpdateListener.Adapter() {
60       @Override
61       public void updateStarted(@NotNull final Document doc) {
62         doc.putUserData(DO_NOT_REPARSE_INCREMENTALLY, Boolean.TRUE);
63       }
64     });
65   }
66
67   @Override
68   public void reparseRange(PsiFile file, int startOffset, int endOffset, CharSequence newTextS) throws IncorrectOperationException {
69     LOG.assertTrue(file.isValid());
70     final PsiFileImpl psiFile = (PsiFileImpl)file;
71     final Document document = psiFile.getViewProvider().getDocument();
72     assert document != null;
73     document.replaceString(startOffset, endOffset, newTextS);
74     PsiDocumentManager.getInstance(psiFile.getProject()).commitDocument(document);
75   }
76
77   @Override
78   @NotNull
79   public DiffLog reparseRange(@NotNull final PsiFile file,
80                               @NotNull TextRange changedPsiRange,
81                               @NotNull final CharSequence newFileText,
82                               @NotNull final ProgressIndicator indicator) {
83     final PsiFileImpl fileImpl = (PsiFileImpl)file;
84     
85     final Couple<ASTNode> reparseableRoots = findReparseableRoots(fileImpl, changedPsiRange, newFileText);
86     return reparseableRoots != null
87            ? mergeTrees(fileImpl, reparseableRoots.first, reparseableRoots.second, indicator)
88            : makeFullParse(fileImpl.getTreeElement(), newFileText, newFileText.length(), fileImpl, indicator);
89   }
90
91   /**
92    * This method searches ast node that could be reparsed incrementally and returns pair of target reparseable node and new replacement node.
93    * Returns null if there is no any chance to make incremental parsing.
94    */
95   @Nullable
96   public Couple<ASTNode> findReparseableRoots(@NotNull PsiFileImpl file,
97                                               @NotNull TextRange changedPsiRange,
98                                               @NotNull CharSequence newFileText) {
99     Project project = file.getProject();
100     final FileElement fileElement = file.getTreeElement();
101     final CharTable charTable = fileElement.getCharTable();
102     int lengthShift = newFileText.length() - fileElement.getTextLength();
103
104     if (fileElement.getElementType() instanceof ITemplateDataElementType || isTooDeep(file)) {
105       // unable to perform incremental reparse for template data in JSP, or in exceptionally deep trees
106       return null;
107     }
108
109     final ASTNode leafAtStart = fileElement.findLeafElementAt(Math.max(0, changedPsiRange.getStartOffset() - 1));
110     final ASTNode leafAtEnd = fileElement.findLeafElementAt(Math.min(changedPsiRange.getEndOffset(), fileElement.getTextLength() - 1));
111     ASTNode node = leafAtStart != null && leafAtEnd != null ? TreeUtil.findCommonParent(leafAtStart, leafAtEnd) : fileElement;
112     Language baseLanguage = file.getViewProvider().getBaseLanguage();
113
114     while (node != null && !(node instanceof FileElement)) {
115       IElementType elementType = node.getElementType();
116       if (elementType instanceof IReparseableElementType) {
117         final TextRange textRange = node.getTextRange();
118         final IReparseableElementType reparseable = (IReparseableElementType)elementType;
119
120         if (baseLanguage.isKindOf(reparseable.getLanguage()) && textRange.getLength() + lengthShift > 0) {
121           final int start = textRange.getStartOffset();
122           final int end = start + textRange.getLength() + lengthShift;
123           if (end > newFileText.length()) {
124             reportInconsistentLength(file, newFileText, node, start, end);
125             break;
126           }
127
128           CharSequence newTextStr = newFileText.subSequence(start, end);
129
130           if (reparseable.isParsable(node.getTreeParent(), newTextStr, baseLanguage, project)) {
131             ASTNode chameleon = reparseable.createNode(newTextStr);
132             if (chameleon != null) {
133               DummyHolder holder = DummyHolderFactory.createHolder(file.getManager(), null, node.getPsi(), charTable);
134               holder.getTreeElement().rawAddChildren((TreeElement)chameleon);
135
136               if (holder.getTextLength() != newTextStr.length()) {
137                 String details = ApplicationManager.getApplication().isInternal()
138                                  ? "text=" + newTextStr + "; treeText=" + holder.getText() + ";"
139                                  : "";
140                 LOG.error("Inconsistent reparse: " + details + " type=" + elementType);
141               }
142
143               return Couple.of(node, chameleon);
144             }
145           }
146         }
147       }
148       node = node.getTreeParent();
149     }
150     return null;
151   }
152
153   private static void reportInconsistentLength(PsiFile file, CharSequence newFileText, ASTNode node, int start, int end) {
154     String message = "Index out of bounds: type=" + node.getElementType() +
155                      "; file=" + file +
156                      "; file.class=" + file.getClass() +
157                      "; start=" + start +
158                      "; end=" + end +
159                      "; length=" + node.getTextLength();
160     String newTextBefore = newFileText.subSequence(0, start).toString();
161     String oldTextBefore = file.getText().subSequence(0, start).toString();
162     if (oldTextBefore.equals(newTextBefore)) {
163       message += "; oldTextBefore==newTextBefore";
164     }
165     LOG.error(message,
166               new Attachment(file.getName() + "_oldNodeText.txt", node.getText()),
167               new Attachment(file.getName() + "_oldFileText.txt", file.getText()),
168               new Attachment(file.getName() + "_newFileText.txt", newFileText.toString())
169     );
170   }
171
172   @NotNull
173   private static DiffLog makeFullParse(ASTNode parent,
174                                        @NotNull CharSequence newFileText,
175                                        int textLength,
176                                        @NotNull PsiFileImpl fileImpl,
177                                        @NotNull ProgressIndicator indicator) {
178     if (fileImpl instanceof PsiCodeFragment) {
179       final FileElement holderElement = new DummyHolder(fileImpl.getManager(), null).getTreeElement();
180       holderElement.rawAddChildren(fileImpl.createContentLeafElement(holderElement.getCharTable().intern(newFileText, 0, textLength)));
181       DiffLog diffLog = new DiffLog();
182       diffLog.appendReplaceFileElement((FileElement)parent, (FileElement)holderElement.getFirstChildNode());
183
184       return diffLog;
185     }
186     else {
187       FileViewProvider viewProvider = fileImpl.getViewProvider();
188       viewProvider.getLanguages();
189       FileType fileType = viewProvider.getVirtualFile().getFileType();
190       String fileName = fileImpl.getName();
191       final LightVirtualFile lightFile = new LightVirtualFile(fileName, fileType, newFileText, viewProvider.getVirtualFile().getCharset(),
192                                                               fileImpl.getViewProvider().getModificationStamp());
193       lightFile.setOriginalFile(viewProvider.getVirtualFile());
194
195       FileViewProvider copy = viewProvider.createCopy(lightFile);
196       if (copy.isEventSystemEnabled()) {
197         throw new AssertionError("Copied view provider must be non-physical for reparse to deliver correct events: " + viewProvider);
198       }
199       copy.getLanguages();
200       SingleRootFileViewProvider.doNotCheckFileSizeLimit(lightFile); // optimization: do not convert file contents to bytes to determine if we should codeinsight it
201       PsiFileImpl newFile = getFileCopy(fileImpl, copy);
202
203       newFile.setOriginalFile(fileImpl);
204
205       final FileElement newFileElement = (FileElement)newFile.getNode();
206       final FileElement oldFileElement = (FileElement)fileImpl.getNode();
207
208       DiffLog diffLog = mergeTrees(fileImpl, oldFileElement, newFileElement, indicator);
209
210       ((PsiManagerEx)fileImpl.getManager()).getFileManager().setViewProvider(lightFile, null);
211       return diffLog;
212     }
213   }
214
215   @NotNull
216   public static PsiFileImpl getFileCopy(PsiFileImpl originalFile, FileViewProvider providerCopy) {
217     FileViewProvider viewProvider = originalFile.getViewProvider();
218     Language language = originalFile.getLanguage();
219
220     PsiFile file = providerCopy.getPsi(language);
221     if (file != null && !(file instanceof PsiFileImpl)) {
222       throw new RuntimeException("View provider " + viewProvider + " refused to provide PsiFileImpl for " + language + details(providerCopy, viewProvider));
223     }
224
225     PsiFileImpl newFile = (PsiFileImpl)file;
226
227     if (newFile == null && language == PlainTextLanguage.INSTANCE && originalFile == viewProvider.getPsi(viewProvider.getBaseLanguage())) {
228       newFile = (PsiFileImpl)providerCopy.getPsi(providerCopy.getBaseLanguage());
229     }
230
231     if (newFile == null) {
232       throw new RuntimeException("View provider " + viewProvider + " refused to parse text with " + language + details(providerCopy, viewProvider));
233     }
234
235     return newFile;
236   }
237
238   private static String details(FileViewProvider providerCopy, FileViewProvider viewProvider) {
239     return "; languages: " + viewProvider.getLanguages() +
240            "; base: " + viewProvider.getBaseLanguage() +
241            "; copy: " + providerCopy +
242            "; copy.base: " + providerCopy.getBaseLanguage() +
243            "; vFile: " + viewProvider.getVirtualFile() +
244            "; copy.vFile: " + providerCopy.getVirtualFile() +
245            "; fileType: " + viewProvider.getVirtualFile().getFileType() +
246            "; copy.original(): " +
247            (providerCopy.getVirtualFile() instanceof LightVirtualFile ? ((LightVirtualFile)providerCopy.getVirtualFile()).getOriginalFile() : null);
248   }
249
250   @NotNull
251   private static DiffLog replaceElementWithEvents(@NotNull CompositeElement oldRoot, @NotNull CompositeElement newRoot) {
252     DiffLog diffLog = new DiffLog();
253     diffLog.appendReplaceElementWithEvents(oldRoot, newRoot);
254     return diffLog;
255   }
256
257   @NotNull
258   public static DiffLog mergeTrees(@NotNull final PsiFileImpl fileImpl,
259                                    @NotNull final ASTNode oldRoot,
260                                    @NotNull final ASTNode newRoot,
261                                    @NotNull ProgressIndicator indicator) {
262     if (newRoot instanceof FileElement) {
263       ((FileElement)newRoot).setCharTable(fileImpl.getTreeElement().getCharTable());
264     }
265
266     try {
267       newRoot.putUserData(TREE_TO_BE_REPARSED, oldRoot);
268       if (isReplaceWholeNode(fileImpl, newRoot)) {
269         DiffLog treeChangeEvent = replaceElementWithEvents((CompositeElement)oldRoot, (CompositeElement)newRoot);
270         fileImpl.putUserData(TREE_DEPTH_LIMIT_EXCEEDED, Boolean.TRUE);
271
272         return treeChangeEvent;
273       }
274       newRoot.getFirstChildNode();  // maybe reparsed in PsiBuilderImpl and have thrown exception here
275     }
276     catch (ReparsedSuccessfullyException e) {
277       // reparsed in PsiBuilderImpl
278       return e.getDiffLog();
279     }
280     finally {
281       newRoot.putUserData(TREE_TO_BE_REPARSED, null);
282     }
283
284     final ASTShallowComparator comparator = new ASTShallowComparator(indicator);
285     final ASTStructure treeStructure = createInterruptibleASTStructure(newRoot, indicator);
286
287     DiffLog diffLog = new DiffLog();
288     diffTrees(oldRoot, diffLog, comparator, treeStructure, indicator);
289     return diffLog;
290   }
291
292   public static <T> void diffTrees(@NotNull final ASTNode oldRoot,
293                                    @NotNull final DiffTreeChangeBuilder<ASTNode, T> builder,
294                                    @NotNull final ShallowNodeComparator<ASTNode, T> comparator,
295                                    @NotNull final FlyweightCapableTreeStructure<T> newTreeStructure,
296                                    @NotNull ProgressIndicator indicator) {
297     TreeUtil.ensureParsedRecursivelyCheckingProgress(oldRoot, indicator);
298     DiffTree.diff(createInterruptibleASTStructure(oldRoot, indicator), newTreeStructure, comparator, builder);
299   }
300
301   private static ASTStructure createInterruptibleASTStructure(@NotNull final ASTNode oldRoot, @NotNull final ProgressIndicator indicator) {
302     return new ASTStructure(oldRoot) {
303       @Override
304       public int getChildren(@NotNull ASTNode astNode, @NotNull Ref<ASTNode[]> into) {
305         indicator.checkCanceled();
306         return super.getChildren(astNode, into);
307       }
308     };
309   }
310
311   private static boolean isReplaceWholeNode(@NotNull PsiFileImpl fileImpl, @NotNull ASTNode newRoot) throws ReparsedSuccessfullyException {
312     final Boolean data = fileImpl.getUserData(DO_NOT_REPARSE_INCREMENTALLY);
313     if (data != null) fileImpl.putUserData(DO_NOT_REPARSE_INCREMENTALLY, null);
314
315     boolean explicitlyMarkedDeep = Boolean.TRUE.equals(data);
316
317     if (explicitlyMarkedDeep || isTooDeep(fileImpl)) {
318       return true;
319     }
320
321     final ASTNode childNode = newRoot.getFirstChildNode();  // maybe reparsed in PsiBuilderImpl and have thrown exception here
322     boolean childTooDeep = isTooDeep(childNode);
323     if (childTooDeep) {
324       childNode.putUserData(TREE_DEPTH_LIMIT_EXCEEDED, null);
325       fileImpl.putUserData(TREE_DEPTH_LIMIT_EXCEEDED, Boolean.TRUE);
326     }
327     return childTooDeep;
328   }
329
330   public static void sendBeforeChildrenChangeEvent(@NotNull PsiManagerImpl manager, @NotNull PsiElement scope, boolean isGenericChange) {
331     if (!scope.isPhysical()) {
332       manager.beforeChange(false);
333       return;
334     }
335     PsiTreeChangeEventImpl event = new PsiTreeChangeEventImpl(manager);
336     event.setParent(scope);
337     event.setFile(scope.getContainingFile());
338     TextRange range = scope.getTextRange();
339     event.setOffset(range == null ? 0 : range.getStartOffset());
340     event.setOldLength(scope.getTextLength());
341     // the "generic" event is being sent on every PSI change. It does not carry any specific info except the fact that "something has changed"
342     event.setGenericChange(isGenericChange);
343     manager.beforeChildrenChange(event);
344   }
345
346   public static void sendAfterChildrenChangedEvent(@NotNull PsiManagerImpl manager,
347                                                    @NotNull PsiFile scope,
348                                                    int oldLength,
349                                                    boolean isGenericChange) {
350     if (!scope.isPhysical()) {
351       manager.afterChange(false);
352       return;
353     }
354     PsiTreeChangeEventImpl event = new PsiTreeChangeEventImpl(manager);
355     event.setParent(scope);
356     event.setFile(scope);
357     event.setOffset(0);
358     event.setOldLength(oldLength);
359     event.setGenericChange(isGenericChange);
360     manager.childrenChanged(event);
361   }
362 }