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