1a573cd5b663ac2e79b503a73e1589d58c92f1a6
[idea/community.git] / platform / platform-impl / src / com / intellij / execution / DelayedDocumentWatcher.java
1 /*
2  * Copyright 2000-2014 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 package com.intellij.execution;
17
18 import com.intellij.AppTopics;
19 import com.intellij.openapi.Disposable;
20 import com.intellij.openapi.application.ApplicationManager;
21 import com.intellij.openapi.application.ModalityState;
22 import com.intellij.openapi.editor.Document;
23 import com.intellij.openapi.editor.EditorFactory;
24 import com.intellij.openapi.editor.event.DocumentAdapter;
25 import com.intellij.openapi.editor.event.DocumentEvent;
26 import com.intellij.openapi.fileEditor.FileDocumentManager;
27 import com.intellij.openapi.fileEditor.FileDocumentManagerAdapter;
28 import com.intellij.openapi.project.Project;
29 import com.intellij.openapi.util.Computable;
30 import com.intellij.openapi.util.Condition;
31 import com.intellij.openapi.util.Disposer;
32 import com.intellij.openapi.vfs.VirtualFile;
33 import com.intellij.util.Alarm;
34 import com.intellij.util.Consumer;
35 import com.intellij.util.PsiErrorElementUtil;
36 import com.intellij.util.messages.MessageBusConnection;
37 import gnu.trove.THashSet;
38 import org.jetbrains.annotations.NotNull;
39 import org.jetbrains.annotations.Nullable;
40
41 import java.util.Collection;
42 import java.util.Set;
43
44 public class DelayedDocumentWatcher {
45
46   // All instance fields are be accessed from EDT
47   private final Project myProject;
48   private final Alarm myAlarm;
49   private final int myDelayMillis;
50   private final Consumer<Integer> myModificationStampConsumer;
51   private final Condition<VirtualFile> myChangedFileFilter;
52   private final MyDocumentAdapter myListener;
53   private final Runnable myAlarmRunnable;
54
55   private final Set<VirtualFile> myChangedFiles = new THashSet<VirtualFile>();
56   private boolean myDocumentSavingInProgress = false;
57   private MessageBusConnection myConnection;
58   private int myModificationStamp = 0;
59   private Disposable myListenerDisposable;
60
61   public DelayedDocumentWatcher(@NotNull Project project,
62                                 int delayMillis,
63                                 @NotNull Consumer<Integer> modificationStampConsumer,
64                                 @Nullable Condition<VirtualFile> changedFileFilter) {
65     myProject = project;
66     myAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD, myProject);
67     myDelayMillis = delayMillis;
68     myModificationStampConsumer = modificationStampConsumer;
69     myChangedFileFilter = changedFileFilter;
70     myListener = new MyDocumentAdapter();
71     myAlarmRunnable = new MyRunnable();
72   }
73
74   @NotNull
75   public Project getProject() {
76     return myProject;
77   }
78
79   public void activate() {
80     if (myConnection == null) {
81       myListenerDisposable = Disposer.newDisposable();
82       Disposer.register(myProject, myListenerDisposable);
83       EditorFactory.getInstance().getEventMulticaster().addDocumentListener(myListener, myListenerDisposable);
84       myConnection = ApplicationManager.getApplication().getMessageBus().connect(myProject);
85       myConnection.subscribe(AppTopics.FILE_DOCUMENT_SYNC, new FileDocumentManagerAdapter() {
86         @Override
87         public void beforeAllDocumentsSaving() {
88           myDocumentSavingInProgress = true;
89           ApplicationManager.getApplication().invokeLater(new Runnable() {
90             @Override
91             public void run() {
92               myDocumentSavingInProgress = false;
93             }
94           }, ModalityState.any());
95         }
96       });
97     }
98   }
99
100   public void deactivate() {
101     if (myConnection != null) {
102       if (myListenerDisposable != null) {
103         Disposer.dispose(myListenerDisposable);
104         myListenerDisposable = null;
105       }
106       myConnection.disconnect();
107       myConnection = null;
108     }
109   }
110
111   public boolean isUpToDate(int modificationStamp) {
112     return myModificationStamp == modificationStamp;
113   }
114
115   private class MyDocumentAdapter extends DocumentAdapter {
116     @Override
117     public void documentChanged(DocumentEvent event) {
118       if (myDocumentSavingInProgress) {
119         /** When {@link FileDocumentManager#saveAllDocuments} is called,
120          *  {@link com.intellij.openapi.editor.impl.TrailingSpacesStripper} can change a document.
121          *  These needless 'documentChanged' events should be filtered out.
122          */
123         return;
124       }
125       final Document document = event.getDocument();
126       final VirtualFile file = FileDocumentManager.getInstance().getFile(document);
127       if (file == null) {
128         return;
129       }
130       if (!myChangedFiles.contains(file)) {
131         // optimization: if possible, avoid possible expensive 'myChangedFileFilter.value(file)' call
132         if (myChangedFileFilter != null && !myChangedFileFilter.value(file)) {
133           return;
134         }
135
136         myChangedFiles.add(file);
137       }
138
139       myAlarm.cancelRequest(myAlarmRunnable);
140       myAlarm.addRequest(myAlarmRunnable, myDelayMillis);
141       myModificationStamp++;
142     }
143   }
144
145   private class MyRunnable implements Runnable {
146     @Override
147     public void run() {
148       final int oldModificationStamp = myModificationStamp;
149       asyncCheckErrors(myChangedFiles, new Consumer<Boolean>() {
150         @Override
151         public void consume(Boolean errorsFound) {
152           if (myModificationStamp != oldModificationStamp) {
153             // 'documentChanged' event was raised during async checking files for errors
154             // Do nothing in that case, this method will be invoked subsequently.
155             return;
156           }
157           if (errorsFound) {
158             // Do nothing, if some changed file has syntax errors.
159             // This method will be invoked subsequently, when syntax errors are fixed.
160             return;
161           }
162           myChangedFiles.clear();
163           myModificationStampConsumer.consume(myModificationStamp);
164         }
165       });
166     }
167   }
168
169   private void asyncCheckErrors(@NotNull final Collection<VirtualFile> files,
170                                 @NotNull final Consumer<Boolean> errorsFoundConsumer) {
171     ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
172       @Override
173       public void run() {
174         final boolean errorsFound = ApplicationManager.getApplication().runReadAction(new Computable<Boolean>() {
175           @Override
176           public Boolean compute() {
177             for (VirtualFile file : files) {
178               if (PsiErrorElementUtil.hasErrors(myProject, file)) {
179                 return true;
180               }
181             }
182             return false;
183           }
184         });
185         ApplicationManager.getApplication().invokeLater(new Runnable() {
186           @Override
187           public void run() {
188             errorsFoundConsumer.consume(errorsFound);
189           }
190         }, ModalityState.any());
191       }
192     });
193   }
194 }