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