2 * Copyright 2000-2015 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.intellij.psi.impl.source.text;
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;
55 public class BlockSupportImpl extends BlockSupport {
56 private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.source.text.BlockSupportImpl");
58 public BlockSupportImpl(Project project) {
59 project.getMessageBus().connect().subscribe(DocumentBulkUpdateListener.TOPIC, new DocumentBulkUpdateListener.Adapter() {
61 public void updateStarted(@NotNull final Document doc) {
62 doc.putUserData(DO_NOT_REPARSE_INCREMENTALLY, Boolean.TRUE);
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);
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;
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);
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.
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();
104 if (fileElement.getElementType() instanceof ITemplateDataElementType || isTooDeep(file)) {
105 // unable to perform incremental reparse for template data in JSP, or in exceptionally deep trees
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();
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;
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);
128 CharSequence newTextStr = newFileText.subSequence(start, end);
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);
136 if (holder.getTextLength() != newTextStr.length()) {
137 String details = ApplicationManager.getApplication().isInternal()
138 ? "text=" + newTextStr + "; treeText=" + holder.getText() + ";"
140 LOG.error("Inconsistent reparse: " + details + " type=" + elementType);
143 return Couple.of(node, chameleon);
148 node = node.getTreeParent();
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() +
156 "; file.class=" + file.getClass() +
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";
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())
173 private static DiffLog makeFullParse(ASTNode parent,
174 @NotNull CharSequence newFileText,
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());
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());
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);
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);
203 newFile.setOriginalFile(fileImpl);
205 final FileElement newFileElement = (FileElement)newFile.getNode();
206 final FileElement oldFileElement = (FileElement)fileImpl.getNode();
208 DiffLog diffLog = mergeTrees(fileImpl, oldFileElement, newFileElement, indicator);
210 ((PsiManagerEx)fileImpl.getManager()).getFileManager().setViewProvider(lightFile, null);
216 public static PsiFileImpl getFileCopy(PsiFileImpl originalFile, FileViewProvider providerCopy) {
217 FileViewProvider viewProvider = originalFile.getViewProvider();
218 Language language = originalFile.getLanguage();
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));
225 PsiFileImpl newFile = (PsiFileImpl)file;
227 if (newFile == null && language == PlainTextLanguage.INSTANCE && originalFile == viewProvider.getPsi(viewProvider.getBaseLanguage())) {
228 newFile = (PsiFileImpl)providerCopy.getPsi(providerCopy.getBaseLanguage());
231 if (newFile == null) {
232 throw new RuntimeException("View provider " + viewProvider + " refused to parse text with " + language + details(providerCopy, viewProvider));
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);
251 private static DiffLog replaceElementWithEvents(@NotNull CompositeElement oldRoot, @NotNull CompositeElement newRoot) {
252 DiffLog diffLog = new DiffLog();
253 diffLog.appendReplaceElementWithEvents(oldRoot, newRoot);
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());
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);
272 return treeChangeEvent;
274 newRoot.getFirstChildNode(); // maybe reparsed in PsiBuilderImpl and have thrown exception here
276 catch (ReparsedSuccessfullyException e) {
277 // reparsed in PsiBuilderImpl
278 return e.getDiffLog();
281 newRoot.putUserData(TREE_TO_BE_REPARSED, null);
284 final ASTShallowComparator comparator = new ASTShallowComparator(indicator);
285 final ASTStructure treeStructure = createInterruptibleASTStructure(newRoot, indicator);
287 DiffLog diffLog = new DiffLog();
288 diffTrees(oldRoot, diffLog, comparator, treeStructure, indicator);
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);
301 private static ASTStructure createInterruptibleASTStructure(@NotNull final ASTNode oldRoot, @NotNull final ProgressIndicator indicator) {
302 return new ASTStructure(oldRoot) {
304 public int getChildren(@NotNull ASTNode astNode, @NotNull Ref<ASTNode[]> into) {
305 indicator.checkCanceled();
306 return super.getChildren(astNode, into);
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);
315 boolean explicitlyMarkedDeep = Boolean.TRUE.equals(data);
317 if (explicitlyMarkedDeep || isTooDeep(fileImpl)) {
321 final ASTNode childNode = newRoot.getFirstChildNode(); // maybe reparsed in PsiBuilderImpl and have thrown exception here
322 boolean childTooDeep = isTooDeep(childNode);
324 childNode.putUserData(TREE_DEPTH_LIMIT_EXCEEDED, null);
325 fileImpl.putUserData(TREE_DEPTH_LIMIT_EXCEEDED, Boolean.TRUE);
330 public static void sendBeforeChildrenChangeEvent(@NotNull PsiManagerImpl manager, @NotNull PsiElement scope, boolean isGenericChange) {
331 if (!scope.isPhysical()) {
332 manager.beforeChange(false);
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);
346 public static void sendAfterChildrenChangedEvent(@NotNull PsiManagerImpl manager,
347 @NotNull PsiFile scope,
349 boolean isGenericChange) {
350 if (!scope.isPhysical()) {
351 manager.afterChange(false);
354 PsiTreeChangeEventImpl event = new PsiTreeChangeEventImpl(manager);
355 event.setParent(scope);
356 event.setFile(scope);
358 event.setOldLength(oldLength);
359 event.setGenericChange(isGenericChange);
360 manager.childrenChanged(event);