do not invoke later in tests
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / daemon / impl / PsiChangeHandler.java
1 /*
2  * Copyright 2000-2009 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.codeInsight.daemon.impl;
18
19 import com.intellij.codeInsight.daemon.ChangeLocalityDetector;
20 import com.intellij.codeInspection.SuppressionUtil;
21 import com.intellij.openapi.Disposable;
22 import com.intellij.openapi.application.Application;
23 import com.intellij.openapi.application.ApplicationManager;
24 import com.intellij.openapi.application.ModalityState;
25 import com.intellij.openapi.editor.Document;
26 import com.intellij.openapi.editor.Editor;
27 import com.intellij.openapi.editor.EditorFactory;
28 import com.intellij.openapi.editor.event.DocumentAdapter;
29 import com.intellij.openapi.editor.event.DocumentEvent;
30 import com.intellij.openapi.editor.ex.EditorMarkupModel;
31 import com.intellij.openapi.extensions.ExtensionPointName;
32 import com.intellij.openapi.extensions.Extensions;
33 import com.intellij.openapi.fileEditor.FileEditorManager;
34 import com.intellij.openapi.project.Project;
35 import com.intellij.openapi.util.Key;
36 import com.intellij.openapi.util.Pair;
37 import com.intellij.openapi.util.TextRange;
38 import com.intellij.psi.*;
39 import com.intellij.psi.impl.PsiDocumentManagerImpl;
40 import com.intellij.psi.impl.PsiDocumentTransactionListener;
41 import com.intellij.util.SmartList;
42 import com.intellij.util.messages.MessageBusConnection;
43 import gnu.trove.THashMap;
44 import org.jetbrains.annotations.NotNull;
45 import org.jetbrains.annotations.Nullable;
46
47 import java.util.List;
48 import java.util.Map;
49
50 public class PsiChangeHandler extends PsiTreeChangeAdapter implements Disposable {
51   private static final ExtensionPointName<ChangeLocalityDetector> EP_NAME = ExtensionPointName.create("com.intellij.daemon.changeLocalityDetector");
52   private /*NOT STATIC!!!*/ final Key<Boolean> UPDATE_ON_COMMIT_ENGAGED = Key.create("UPDATE_ON_COMMIT_ENGAGED");
53
54   private final Project myProject;
55   private final DaemonCodeAnalyzerImpl myDaemonCodeAnalyzer;
56   private final Map<Document, List<Pair<PsiElement, Boolean>>> changedElements = new THashMap<Document, List<Pair<PsiElement, Boolean>>>();
57   private final FileStatusMap myFileStatusMap;
58
59   public PsiChangeHandler(Project project, DaemonCodeAnalyzerImpl daemonCodeAnalyzer, final PsiDocumentManagerImpl documentManager, EditorFactory editorFactory, MessageBusConnection connection) {
60     myProject = project;
61     myDaemonCodeAnalyzer = daemonCodeAnalyzer;
62     myFileStatusMap = daemonCodeAnalyzer.getFileStatusMap();
63     editorFactory.getEventMulticaster().addDocumentListener(new DocumentAdapter() {
64       @Override
65       public void beforeDocumentChange(DocumentEvent e) {
66         final Document document = e.getDocument();
67         if (documentManager.getSynchronizer().isInSynchronization(document)) return;
68         if (documentManager.getCachedPsiFile(document) == null) return;
69         if (document.getUserData(UPDATE_ON_COMMIT_ENGAGED) == null) {
70           document.putUserData(UPDATE_ON_COMMIT_ENGAGED, Boolean.TRUE);
71           documentManager.addRunOnCommit(document, new Runnable() {
72             public void run() {
73               updateChangesForDocument(document);
74               document.putUserData(UPDATE_ON_COMMIT_ENGAGED, null);
75             }
76           });
77         }
78       }
79     }, this);
80
81     connection.subscribe(PsiDocumentTransactionListener.TOPIC, new PsiDocumentTransactionListener() {
82       public void transactionStarted(final Document doc, final PsiFile file) {
83       }
84
85       public void transactionCompleted(final Document doc, final PsiFile file) {
86         updateChangesForDocument(doc);
87       }
88     });
89   }
90
91   public void dispose() {
92   }
93
94   private void updateChangesForDocument(@NotNull Document document) {
95     List<Pair<PsiElement, Boolean>> toUpdate = changedElements.get(document);
96     if (toUpdate != null) {
97       for (Pair<PsiElement, Boolean> changedElement : toUpdate) {
98         PsiElement element = changedElement.getFirst();
99         Boolean whiteSpaceOptimizationAllowed = changedElement.getSecond();
100         updateByChange(element, whiteSpaceOptimizationAllowed);
101       }
102       changedElements.remove(document);
103     }
104   }
105
106   public void childAdded(PsiTreeChangeEvent event) {
107     queueElement(event.getParent(), true, event);
108   }
109
110   public void childRemoved(PsiTreeChangeEvent event) {
111     queueElement(event.getParent(), true, event);
112   }
113
114   public void childReplaced(PsiTreeChangeEvent event) {
115     queueElement(event.getNewChild(), typesEqual(event.getNewChild(), event.getOldChild()), event);
116   }
117
118   private static boolean typesEqual(final PsiElement newChild, final PsiElement oldChild) {
119     return newChild != null && oldChild != null && newChild.getClass() == oldChild.getClass();
120   }
121
122   public void childrenChanged(PsiTreeChangeEvent event) {
123     queueElement(event.getParent(), true, event);
124   }
125
126   public void beforeChildMovement(PsiTreeChangeEvent event) {
127     queueElement(event.getOldParent(), true, event);
128     queueElement(event.getNewParent(), true, event);
129   }
130
131   public void beforeChildrenChange(PsiTreeChangeEvent event) {
132     // this event sent always before every PSI change, even not significant one (like after quick typing/backspacing char)
133     // mark file dirty just in case
134     PsiFile psiFile = event.getFile();
135     if (psiFile != null) {
136       myFileStatusMap.markFileScopeDirtyDefensively(psiFile);
137     }
138   }
139
140   public void propertyChanged(PsiTreeChangeEvent event) {
141     String propertyName = event.getPropertyName();
142     if (!propertyName.equals(PsiTreeChangeEvent.PROP_WRITABLE)) {
143       myFileStatusMap.markAllFilesDirty();
144       myDaemonCodeAnalyzer.stopProcess(true);
145     }
146   }
147
148   private void queueElement(PsiElement child, final boolean whitespaceOptimizationAllowed, PsiTreeChangeEvent event) {
149     PsiFile file = event.getFile();
150     if (file == null) file = child.getContainingFile();
151     if (file == null) {
152       myFileStatusMap.markAllFilesDirty();
153       return;
154     }
155
156     if (!child.isValid()) return;
157     Document document = PsiDocumentManager.getInstance(myProject).getCachedDocument(file);
158     if (document != null) {
159       List<Pair<PsiElement, Boolean>> toUpdate = changedElements.get(document);
160       if (toUpdate == null) {
161         toUpdate = new SmartList<Pair<PsiElement, Boolean>>();
162         changedElements.put(document, toUpdate);
163       }
164       toUpdate.add(Pair.create(child, whitespaceOptimizationAllowed));
165     }
166   }
167
168   private void updateByChange(PsiElement child, final boolean whitespaceOptimizationAllowed) {
169     final Editor editor = FileEditorManager.getInstance(myProject).getSelectedTextEditor();
170     Application application = ApplicationManager.getApplication();
171     if (editor != null && !application.isUnitTestMode()) {
172       application.invokeLater(new Runnable() {
173         public void run() {
174           if (myProject.isDisposed()) return;
175           EditorMarkupModel markupModel = (EditorMarkupModel)editor.getMarkupModel();
176           markupModel.setErrorStripeRenderer(markupModel.getErrorStripeRenderer());
177         }
178       }, ModalityState.stateForComponent(editor.getComponent()));
179     }
180
181     PsiFile file = child.getContainingFile();
182     if (file == null || file instanceof PsiCompiledElement) {
183       myFileStatusMap.markAllFilesDirty();
184       return;
185     }
186
187     Document document = PsiDocumentManager.getInstance(myProject).getCachedDocument(file);
188     if (document == null) return;
189
190     int fileLength = file.getTextLength();
191     if (!file.getViewProvider().isPhysical()) {
192       myFileStatusMap.markFileScopeDirty(document, new TextRange(0, fileLength), fileLength);
193       return;
194     }
195
196     // optimization
197     if (whitespaceOptimizationAllowed && UpdateHighlightersUtil.isWhitespaceOptimizationAllowed(document)) {
198       if (child instanceof PsiWhiteSpace ||
199           child instanceof PsiComment && !child.getText().contains(SuppressionUtil.SUPPRESS_INSPECTIONS_TAG_NAME)) {
200         myFileStatusMap.markFileScopeDirty(document, child.getTextRange(), fileLength);
201         return;
202       }
203     }
204
205     PsiElement element = child;
206     while (true) {
207       if (element instanceof PsiFile || element instanceof PsiDirectory) {
208         myFileStatusMap.markAllFilesDirty();
209         return;
210       }
211
212       final PsiElement scope = getChangeHighlightingScope(element);
213       if (scope != null) {
214         myFileStatusMap.markFileScopeDirty(document, scope.getTextRange(), fileLength);
215         return;
216       }
217
218       element = element.getParent();
219     }
220   }
221
222   @Nullable
223   private static PsiElement getChangeHighlightingScope(PsiElement element) {
224     for (ChangeLocalityDetector detector : Extensions.getExtensions(EP_NAME)) {
225       final PsiElement scope = detector.getChangeHighlightingDirtyScopeFor(element);
226       if (scope != null) return scope;
227     }
228     return null;
229   }
230 }