81369fcd59af717e9d10eb66afd34e9408f1c8d6
[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     PsiFileImpl newFile = (PsiFileImpl)providerCopy.getPsi(language);
220
221     if (newFile == null && language == PlainTextLanguage.INSTANCE && originalFile == viewProvider.getPsi(viewProvider.getBaseLanguage())) {
222       newFile = (PsiFileImpl)providerCopy.getPsi(providerCopy.getBaseLanguage());
223     }
224
225     if (newFile == null) {
226       throw new RuntimeException("View provider " + viewProvider + " refused to parse text with " + language +
227                                  "; languages: " + viewProvider.getLanguages() +
228                                  "; base: " + viewProvider.getBaseLanguage() +
229                                  "; copy: " + providerCopy +
230                                  "; copy.base: " + providerCopy.getBaseLanguage() +
231                                  "; vFile: " + viewProvider.getVirtualFile() +
232                                  "; copy.vFile: " + providerCopy.getVirtualFile() +
233                                  "; fileType: " + viewProvider.getVirtualFile().getFileType() +
234                                  "; copy.original(): " +
235                                  (providerCopy.getVirtualFile() instanceof LightVirtualFile ? ((LightVirtualFile)providerCopy.getVirtualFile()).getOriginalFile() : null));
236     }
237
238     return newFile;
239   }
240
241   @NotNull
242   private static DiffLog replaceElementWithEvents(@NotNull CompositeElement oldRoot,
243                                                   @NotNull CompositeElement newRoot) {
244     DiffLog diffLog = new DiffLog();
245     diffLog.appendReplaceElementWithEvents(oldRoot, newRoot);
246     return diffLog;
247   }
248
249   @NotNull
250   public static DiffLog mergeTrees(@NotNull final PsiFileImpl fileImpl,
251                                    @NotNull final ASTNode oldRoot,
252                                    @NotNull final ASTNode newRoot,
253                                    @NotNull ProgressIndicator indicator) {
254     if (newRoot instanceof FileElement) {
255       ((FileElement)newRoot).setCharTable(fileImpl.getTreeElement().getCharTable());
256     }
257
258     try {
259       newRoot.putUserData(TREE_TO_BE_REPARSED, oldRoot);
260       if (isReplaceWholeNode(fileImpl, newRoot)) {
261         DiffLog treeChangeEvent = replaceElementWithEvents((CompositeElement)oldRoot, (CompositeElement)newRoot);
262         fileImpl.putUserData(TREE_DEPTH_LIMIT_EXCEEDED, Boolean.TRUE);
263
264         return treeChangeEvent;
265       }
266       newRoot.getFirstChildNode();  // maybe reparsed in PsiBuilderImpl and have thrown exception here
267     }
268     catch (ReparsedSuccessfullyException e) {
269       // reparsed in PsiBuilderImpl
270       return e.getDiffLog();
271     }
272     finally {
273       newRoot.putUserData(TREE_TO_BE_REPARSED, null);
274     }
275
276     final ASTShallowComparator comparator = new ASTShallowComparator(indicator);
277     final ASTStructure treeStructure = createInterruptibleASTStructure(newRoot, indicator);
278
279     DiffLog diffLog = new DiffLog();
280     diffTrees(oldRoot, diffLog, comparator, treeStructure, indicator);
281     return diffLog;
282   }
283
284   public static <T> void diffTrees(@NotNull final ASTNode oldRoot,
285                                    @NotNull final DiffTreeChangeBuilder<ASTNode, T> builder,
286                                    @NotNull final ShallowNodeComparator<ASTNode, T> comparator,
287                                    @NotNull final FlyweightCapableTreeStructure<T> newTreeStructure,
288                                    @NotNull ProgressIndicator indicator) {
289     TreeUtil.ensureParsedRecursivelyCheckingProgress(oldRoot, indicator);
290     DiffTree.diff(createInterruptibleASTStructure(oldRoot, indicator), newTreeStructure, comparator, builder);
291   }
292
293   private static ASTStructure createInterruptibleASTStructure(@NotNull final ASTNode oldRoot, @NotNull final ProgressIndicator indicator) {
294     return new ASTStructure(oldRoot) {
295       @Override
296       public int getChildren(@NotNull ASTNode astNode, @NotNull Ref<ASTNode[]> into) {
297         indicator.checkCanceled();
298         return super.getChildren(astNode, into);
299       }
300     };
301   }
302
303   private static boolean isReplaceWholeNode(@NotNull PsiFileImpl fileImpl, @NotNull ASTNode newRoot) throws ReparsedSuccessfullyException {
304     final Boolean data = fileImpl.getUserData(DO_NOT_REPARSE_INCREMENTALLY);
305     if (data != null) fileImpl.putUserData(DO_NOT_REPARSE_INCREMENTALLY, null);
306
307     boolean explicitlyMarkedDeep = Boolean.TRUE.equals(data);
308
309     if (explicitlyMarkedDeep || isTooDeep(fileImpl)) {
310       return true;
311     }
312
313     final ASTNode childNode = newRoot.getFirstChildNode();  // maybe reparsed in PsiBuilderImpl and have thrown exception here
314     boolean childTooDeep = isTooDeep(childNode);
315     if (childTooDeep) {
316       childNode.putUserData(TREE_DEPTH_LIMIT_EXCEEDED, null);
317       fileImpl.putUserData(TREE_DEPTH_LIMIT_EXCEEDED, Boolean.TRUE);
318     }
319     return childTooDeep;
320   }
321
322   public static void sendBeforeChildrenChangeEvent(@NotNull PsiManagerImpl manager, @NotNull PsiElement scope, boolean isGenericChange) {
323     if (!scope.isPhysical()) {
324       manager.beforeChange(false);
325       return;
326     }
327     PsiTreeChangeEventImpl event = new PsiTreeChangeEventImpl(manager);
328     event.setParent(scope);
329     event.setFile(scope.getContainingFile());
330     TextRange range = scope.getTextRange();
331     event.setOffset(range == null ? 0 : range.getStartOffset());
332     event.setOldLength(scope.getTextLength());
333     // the "generic" event is being sent on every PSI change. It does not carry any specific info except the fact that "something has changed"
334     event.setGenericChange(isGenericChange);
335     manager.beforeChildrenChange(event);
336   }
337
338   public static void sendAfterChildrenChangedEvent(@NotNull PsiManagerImpl manager,
339                                                    @NotNull PsiFile scope,
340                                                    int oldLength,
341                                                    boolean isGenericChange) {
342     if (!scope.isPhysical()) {
343       manager.afterChange(false);
344       return;
345     }
346     PsiTreeChangeEventImpl event = new PsiTreeChangeEventImpl(manager);
347     event.setParent(scope);
348     event.setFile(scope);
349     event.setOffset(0);
350     event.setOldLength(oldLength);
351     event.setGenericChange(isGenericChange);
352     manager.childrenChanged(event);
353   }
354 }