| /* |
| * 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.codeInsight.daemon.impl; |
| |
| import com.intellij.codeInsight.daemon.ChangeLocalityDetector; |
| import com.intellij.openapi.Disposable; |
| import com.intellij.openapi.application.Application; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.application.ModalityState; |
| import com.intellij.openapi.editor.Document; |
| import com.intellij.openapi.editor.Editor; |
| import com.intellij.openapi.editor.EditorFactory; |
| import com.intellij.openapi.editor.event.DocumentAdapter; |
| import com.intellij.openapi.editor.event.DocumentEvent; |
| import com.intellij.openapi.editor.ex.EditorMarkupModel; |
| import com.intellij.openapi.extensions.ExtensionPointName; |
| import com.intellij.openapi.extensions.Extensions; |
| import com.intellij.openapi.fileEditor.FileEditorManager; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.Key; |
| import com.intellij.openapi.util.Pair; |
| import com.intellij.openapi.util.TextRange; |
| import com.intellij.psi.*; |
| import com.intellij.psi.impl.PsiDocumentManagerBase; |
| import com.intellij.psi.impl.PsiDocumentManagerImpl; |
| import com.intellij.psi.impl.PsiDocumentTransactionListener; |
| import com.intellij.psi.impl.PsiTreeChangeEventImpl; |
| import com.intellij.util.SmartList; |
| import com.intellij.util.containers.ContainerUtil; |
| import com.intellij.util.messages.MessageBusConnection; |
| import gnu.trove.THashMap; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.List; |
| import java.util.Map; |
| |
| public class PsiChangeHandler extends PsiTreeChangeAdapter implements Disposable { |
| private static final ExtensionPointName<ChangeLocalityDetector> EP_NAME = ExtensionPointName.create("com.intellij.daemon.changeLocalityDetector"); |
| private /*NOT STATIC!!!*/ final Key<Boolean> UPDATE_ON_COMMIT_ENGAGED = Key.create("UPDATE_ON_COMMIT_ENGAGED"); |
| |
| private final Project myProject; |
| private final Map<Document, List<Pair<PsiElement, Boolean>>> changedElements = new THashMap<Document, List<Pair<PsiElement, Boolean>>>(); |
| private final FileStatusMap myFileStatusMap; |
| |
| public PsiChangeHandler(@NotNull Project project, |
| @NotNull final PsiDocumentManagerImpl documentManager, |
| @NotNull EditorFactory editorFactory, |
| @NotNull MessageBusConnection connection, |
| @NotNull FileStatusMap fileStatusMap) { |
| myProject = project; |
| myFileStatusMap = fileStatusMap; |
| editorFactory.getEventMulticaster().addDocumentListener(new DocumentAdapter() { |
| @Override |
| public void beforeDocumentChange(DocumentEvent e) { |
| final Document document = e.getDocument(); |
| if (documentManager.getSynchronizer().isInSynchronization(document)) return; |
| if (documentManager.getCachedPsiFile(document) == null) return; |
| if (document.getUserData(UPDATE_ON_COMMIT_ENGAGED) == null) { |
| document.putUserData(UPDATE_ON_COMMIT_ENGAGED, Boolean.TRUE); |
| PsiDocumentManagerBase.addRunOnCommit(document, new Runnable() { |
| @Override |
| public void run() { |
| updateChangesForDocument(document); |
| document.putUserData(UPDATE_ON_COMMIT_ENGAGED, null); |
| } |
| }); |
| } |
| } |
| }, this); |
| |
| connection.subscribe(PsiDocumentTransactionListener.TOPIC, new PsiDocumentTransactionListener() { |
| @Override |
| public void transactionStarted(@NotNull final Document doc, @NotNull final PsiFile file) { |
| } |
| |
| @Override |
| public void transactionCompleted(@NotNull final Document doc, @NotNull final PsiFile file) { |
| updateChangesForDocument(doc); |
| } |
| }); |
| } |
| |
| @Override |
| public void dispose() { |
| } |
| |
| private void updateChangesForDocument(@NotNull final Document document) { |
| if (DaemonListeners.isUnderIgnoredAction(null)) return; |
| List<Pair<PsiElement, Boolean>> toUpdate = changedElements.get(document); |
| if (toUpdate == null) { |
| // The document has been changed, but psi hasn't |
| // We may still need to rehighlight the file if there were changes inside highlighted ranges. |
| if (UpdateHighlightersUtil.isWhitespaceOptimizationAllowed(document)) return; |
| |
| // don't create PSI for files in other projects |
| PsiElement file = PsiDocumentManager.getInstance(myProject).getCachedPsiFile(document); |
| if (file == null) return; |
| |
| toUpdate = ContainerUtil.newArrayList(Pair.create(file, true)); |
| } |
| Application application = ApplicationManager.getApplication(); |
| final Editor editor = FileEditorManager.getInstance(myProject).getSelectedTextEditor(); |
| if (editor != null && !application.isUnitTestMode()) { |
| application.invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| if (!editor.isDisposed()) { |
| EditorMarkupModel markupModel = (EditorMarkupModel)editor.getMarkupModel(); |
| PsiFile file = PsiDocumentManager.getInstance(myProject).getPsiFile(editor.getDocument()); |
| TrafficLightRenderer.setOrRefreshErrorStripeRenderer(markupModel, myProject, editor.getDocument(), file); |
| } |
| } |
| }, ModalityState.stateForComponent(editor.getComponent()), myProject.getDisposed()); |
| } |
| |
| for (Pair<PsiElement, Boolean> changedElement : toUpdate) { |
| PsiElement element = changedElement.getFirst(); |
| Boolean whiteSpaceOptimizationAllowed = changedElement.getSecond(); |
| updateByChange(element, document, whiteSpaceOptimizationAllowed); |
| } |
| changedElements.remove(document); |
| } |
| |
| @Override |
| public void childAdded(@NotNull PsiTreeChangeEvent event) { |
| queueElement(event.getParent(), true, event); |
| } |
| |
| @Override |
| public void childRemoved(@NotNull PsiTreeChangeEvent event) { |
| queueElement(event.getParent(), true, event); |
| } |
| |
| @Override |
| public void childReplaced(@NotNull PsiTreeChangeEvent event) { |
| queueElement(event.getNewChild(), typesEqual(event.getNewChild(), event.getOldChild()), event); |
| } |
| |
| private static boolean typesEqual(final PsiElement newChild, final PsiElement oldChild) { |
| return newChild != null && oldChild != null && newChild.getClass() == oldChild.getClass(); |
| } |
| |
| @Override |
| public void childrenChanged(@NotNull PsiTreeChangeEvent event) { |
| if (((PsiTreeChangeEventImpl)event).isGenericChange()) { |
| return; |
| } |
| queueElement(event.getParent(), true, event); |
| } |
| |
| @Override |
| public void beforeChildMovement(@NotNull PsiTreeChangeEvent event) { |
| queueElement(event.getOldParent(), true, event); |
| queueElement(event.getNewParent(), true, event); |
| } |
| |
| @Override |
| public void beforeChildrenChange(@NotNull PsiTreeChangeEvent event) { |
| // this event sent always before every PSI change, even not significant one (like after quick typing/backspacing char) |
| // mark file dirty just in case |
| PsiFile psiFile = event.getFile(); |
| if (psiFile != null) { |
| myFileStatusMap.markFileScopeDirtyDefensively(psiFile); |
| } |
| } |
| |
| @Override |
| public void propertyChanged(@NotNull PsiTreeChangeEvent event) { |
| String propertyName = event.getPropertyName(); |
| if (!propertyName.equals(PsiTreeChangeEvent.PROP_WRITABLE)) { |
| myFileStatusMap.markAllFilesDirty(); |
| } |
| } |
| |
| private void queueElement(PsiElement child, final boolean whitespaceOptimizationAllowed, PsiTreeChangeEvent event) { |
| PsiFile file = event.getFile(); |
| if (file == null) file = child.getContainingFile(); |
| if (file == null) { |
| myFileStatusMap.markAllFilesDirty(); |
| return; |
| } |
| |
| if (!child.isValid()) return; |
| Document document = PsiDocumentManager.getInstance(myProject).getCachedDocument(file); |
| if (document != null) { |
| List<Pair<PsiElement, Boolean>> toUpdate = changedElements.get(document); |
| if (toUpdate == null) { |
| toUpdate = new SmartList<Pair<PsiElement, Boolean>>(); |
| changedElements.put(document, toUpdate); |
| } |
| toUpdate.add(Pair.create(child, whitespaceOptimizationAllowed)); |
| } |
| } |
| |
| private void updateByChange(@NotNull PsiElement child, @NotNull final Document document, final boolean whitespaceOptimizationAllowed) { |
| final PsiFile file; |
| try { |
| file = child.getContainingFile(); |
| } |
| catch (PsiInvalidElementAccessException e) { |
| myFileStatusMap.markAllFilesDirty(); |
| return; |
| } |
| if (file == null || file instanceof PsiCompiledElement) { |
| myFileStatusMap.markAllFilesDirty(); |
| return; |
| } |
| |
| int fileLength = file.getTextLength(); |
| if (!file.getViewProvider().isPhysical()) { |
| myFileStatusMap.markFileScopeDirty(document, new TextRange(0, fileLength), fileLength); |
| return; |
| } |
| |
| PsiElement element = whitespaceOptimizationAllowed && UpdateHighlightersUtil.isWhitespaceOptimizationAllowed(document) ? child : child.getParent(); |
| while (true) { |
| if (element == null || element instanceof PsiFile || element instanceof PsiDirectory) { |
| myFileStatusMap.markAllFilesDirty(); |
| return; |
| } |
| |
| final PsiElement scope = getChangeHighlightingScope(element); |
| if (scope != null) { |
| myFileStatusMap.markFileScopeDirty(document, scope.getTextRange(), fileLength); |
| return; |
| } |
| |
| element = element.getParent(); |
| } |
| } |
| |
| @Nullable |
| private static PsiElement getChangeHighlightingScope(PsiElement element) { |
| DefaultChangeLocalityDetector defaultDetector = null; |
| for (ChangeLocalityDetector detector : Extensions.getExtensions(EP_NAME)) { |
| if (detector instanceof DefaultChangeLocalityDetector) { |
| // run default detector last |
| assert defaultDetector == null : defaultDetector; |
| defaultDetector = (DefaultChangeLocalityDetector)detector; |
| continue; |
| } |
| final PsiElement scope = detector.getChangeHighlightingDirtyScopeFor(element); |
| if (scope != null) return scope; |
| } |
| assert defaultDetector != null : "com.intellij.codeInsight.daemon.impl.DefaultChangeLocalityDetector is unregistered"; |
| return defaultDetector.getChangeHighlightingDirtyScopeFor(element); |
| } |
| } |