| /* |
| * 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.execution; |
| |
| import com.google.common.collect.ImmutableSet; |
| import com.intellij.AppTopics; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.application.ModalityState; |
| import com.intellij.openapi.editor.Document; |
| import com.intellij.openapi.editor.EditorFactory; |
| import com.intellij.openapi.editor.event.DocumentAdapter; |
| import com.intellij.openapi.editor.event.DocumentEvent; |
| import com.intellij.openapi.fileEditor.FileDocumentManager; |
| import com.intellij.openapi.fileEditor.FileDocumentManagerAdapter; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.Computable; |
| import com.intellij.openapi.util.Condition; |
| import com.intellij.openapi.util.Key; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.problems.WolfTheProblemSolver; |
| import com.intellij.psi.PsiDocumentManager; |
| import com.intellij.psi.PsiFile; |
| import com.intellij.psi.util.CachedValue; |
| import com.intellij.psi.util.CachedValueProvider; |
| import com.intellij.psi.util.CachedValuesManager; |
| import com.intellij.psi.util.PsiTreeUtil; |
| import com.intellij.util.Alarm; |
| import com.intellij.util.Consumer; |
| import com.intellij.util.messages.MessageBusConnection; |
| import gnu.trove.THashSet; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.Collection; |
| import java.util.Set; |
| |
| public class DelayedDocumentWatcher { |
| |
| private static final Key<CachedValue<Boolean>> CONTAINS_ERROR_ELEMENT = Key.create("CONTAINS_ERROR_ELEMENT"); |
| |
| // All instance fields are be accessed from EDT |
| private final Project myProject; |
| private final Alarm myAlarm; |
| private final int myDelayMillis; |
| private final Consumer<Set<VirtualFile>> myConsumer; |
| private final Condition<VirtualFile> myChangedFileFilter; |
| private final MyDocumentAdapter myListener; |
| private final Runnable myAlarmRunnable; |
| |
| private final Set<VirtualFile> myChangedFiles = new THashSet<VirtualFile>(); |
| private boolean myDocumentSavingInProgress = false; |
| private MessageBusConnection myConnection; |
| private int myEventCount = 0; |
| |
| public DelayedDocumentWatcher(@NotNull Project project, |
| int delayMillis, |
| @NotNull Consumer<Set<VirtualFile>> consumer, |
| @Nullable Condition<VirtualFile> changedFileFilter) { |
| myProject = project; |
| myAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD, myProject); |
| myDelayMillis = delayMillis; |
| myConsumer = consumer; |
| myChangedFileFilter = changedFileFilter; |
| myListener = new MyDocumentAdapter(); |
| myAlarmRunnable = new MyRunnable(); |
| } |
| |
| @NotNull |
| public Project getProject() { |
| return myProject; |
| } |
| |
| public void activate() { |
| EditorFactory.getInstance().getEventMulticaster().addDocumentListener(myListener, myProject); |
| if (myConnection == null) { |
| myConnection = ApplicationManager.getApplication().getMessageBus().connect(myProject); |
| myConnection.subscribe(AppTopics.FILE_DOCUMENT_SYNC, new FileDocumentManagerAdapter() { |
| @Override |
| public void beforeAllDocumentsSaving() { |
| myDocumentSavingInProgress = true; |
| ApplicationManager.getApplication().invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| myDocumentSavingInProgress = false; |
| } |
| }, ModalityState.any()); |
| } |
| }); |
| } |
| } |
| |
| public void deactivate() { |
| EditorFactory.getInstance().getEventMulticaster().removeDocumentListener(myListener); |
| if (myConnection != null) { |
| myConnection.disconnect(); |
| myConnection = null; |
| } |
| } |
| |
| private class MyDocumentAdapter extends DocumentAdapter { |
| @Override |
| public void documentChanged(DocumentEvent event) { |
| if (myDocumentSavingInProgress) { |
| /** When {@link FileDocumentManager#saveAllDocuments} is called, |
| * {@link com.intellij.openapi.editor.impl.TrailingSpacesStripper} can change a document. |
| * These needless 'documentChanged' events should be filtered out. |
| */ |
| return; |
| } |
| final Document document = event.getDocument(); |
| final VirtualFile file = FileDocumentManager.getInstance().getFile(document); |
| if (file == null) { |
| return; |
| } |
| if (!myChangedFiles.contains(file)) { |
| // optimization: if possible, avoid possible expensive 'myChangedFileFilter.value(file)' call |
| if (myChangedFileFilter != null && !myChangedFileFilter.value(file)) { |
| return; |
| } |
| |
| myChangedFiles.add(file); |
| } |
| |
| myAlarm.cancelRequest(myAlarmRunnable); |
| myAlarm.addRequest(myAlarmRunnable, myDelayMillis); |
| myEventCount++; |
| } |
| } |
| |
| private class MyRunnable implements Runnable { |
| @Override |
| public void run() { |
| final int oldEventCount = myEventCount; |
| final Set<VirtualFile> copy = ImmutableSet.copyOf(myChangedFiles); |
| asyncCheckErrors(copy, new Consumer<Boolean>() { |
| @Override |
| public void consume(Boolean errorsFound) { |
| if (myEventCount != oldEventCount) { |
| // 'documentChanged' event was raised during async checking files for errors |
| // Do nothing in that case, this method will be invoked subsequently. |
| return; |
| } |
| if (errorsFound) { |
| // Do nothing, if some changed file has syntax errors. |
| // This method will be invoked subsequently, when syntax errors are fixed. |
| return; |
| } |
| myChangedFiles.clear(); |
| myConsumer.consume(copy); |
| } |
| }); |
| } |
| } |
| |
| private void asyncCheckErrors(@NotNull final Collection<VirtualFile> files, |
| @NotNull final Consumer<Boolean> errorsFoundConsumer) { |
| ApplicationManager.getApplication().executeOnPooledThread(new Runnable() { |
| @Override |
| public void run() { |
| final boolean errorsFound = ApplicationManager.getApplication().runReadAction(new Computable<Boolean>() { |
| @Override |
| public Boolean compute() { |
| for (VirtualFile file : files) { |
| if (hasErrors(file)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| }); |
| ApplicationManager.getApplication().invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| errorsFoundConsumer.consume(errorsFound); |
| } |
| }, ModalityState.any()); |
| } |
| }); |
| } |
| |
| // This method is called in a background thread with a read lock acquired |
| private boolean hasErrors(@NotNull VirtualFile file) { |
| if (!file.isValid()) { |
| return false; |
| } |
| // don't use 'WolfTheProblemSolver.hasSyntaxErrors(file)' if possible |
| Document document = FileDocumentManager.getInstance().getDocument(file); |
| if (document != null) { |
| final PsiFile psiFile = PsiDocumentManager.getInstance(myProject).getPsiFile(document); |
| if (psiFile != null) { |
| CachedValuesManager cachedValuesManager = CachedValuesManager.getManager(myProject); |
| return cachedValuesManager.getCachedValue( |
| psiFile, |
| CONTAINS_ERROR_ELEMENT, |
| new CachedValueProvider<Boolean>() { |
| @Override |
| public Result<Boolean> compute() { |
| boolean error = PsiTreeUtil.hasErrorElements(psiFile); |
| return Result.create(error, psiFile); |
| } |
| }, |
| false |
| ); |
| } |
| } |
| return WolfTheProblemSolver.getInstance(myProject).hasSyntaxErrors(file); |
| } |
| |
| } |