1f8360a13f1be580ec88b3d540f54f4349afa60e
[idea/community.git] / java / compiler / impl / src / com / intellij / compiler / backwardRefs / DirtyScopeHolder.java
1 // Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.compiler.backwardRefs;
3
4 import com.intellij.ProjectTopics;
5 import com.intellij.compiler.CompilerConfiguration;
6 import com.intellij.compiler.CompilerReferenceService;
7 import com.intellij.compiler.backwardRefs.view.DirtyScopeTestInfo;
8 import com.intellij.openapi.Disposable;
9 import com.intellij.openapi.application.ReadAction;
10 import com.intellij.openapi.compiler.options.ExcludeEntryDescription;
11 import com.intellij.openapi.compiler.options.ExcludedEntriesListener;
12 import com.intellij.openapi.editor.Document;
13 import com.intellij.openapi.fileEditor.FileDocumentManager;
14 import com.intellij.openapi.fileTypes.FileType;
15 import com.intellij.openapi.fileTypes.FileTypeRegistry;
16 import com.intellij.openapi.module.Module;
17 import com.intellij.openapi.module.ModuleManager;
18 import com.intellij.openapi.progress.ProcessCanceledException;
19 import com.intellij.openapi.progress.ProgressManager;
20 import com.intellij.openapi.project.Project;
21 import com.intellij.openapi.roots.ModuleRootEvent;
22 import com.intellij.openapi.roots.ModuleRootListener;
23 import com.intellij.openapi.util.UserDataHolderBase;
24 import com.intellij.openapi.util.io.FileUtil;
25 import com.intellij.openapi.vfs.AsyncFileListener;
26 import com.intellij.openapi.vfs.VirtualFile;
27 import com.intellij.openapi.vfs.VirtualFileManager;
28 import com.intellij.openapi.vfs.newvfs.events.*;
29 import com.intellij.psi.PsiDocumentManager;
30 import com.intellij.psi.PsiFile;
31 import com.intellij.psi.search.GlobalSearchScope;
32 import com.intellij.psi.util.CachedValueProvider;
33 import com.intellij.psi.util.CachedValuesManager;
34 import com.intellij.psi.util.PsiModificationTracker;
35 import com.intellij.util.SmartList;
36 import com.intellij.util.containers.ContainerUtil;
37 import com.intellij.util.messages.MessageBusConnection;
38 import gnu.trove.THashSet;
39 import org.jetbrains.annotations.Contract;
40 import org.jetbrains.annotations.NotNull;
41 import org.jetbrains.annotations.Nullable;
42 import org.jetbrains.annotations.TestOnly;
43
44 import java.util.*;
45 import java.util.function.BiConsumer;
46
47 @SuppressWarnings("WeakerAccess")
48 public class DirtyScopeHolder extends UserDataHolderBase implements AsyncFileListener {
49   private final CompilerReferenceServiceBase<?> myService;
50   private final FileDocumentManager myFileDocManager;
51   private final PsiDocumentManager myPsiDocManager;
52   private final Object myLock = new Object();
53
54   private final Set<Module> myVFSChangedModules = new HashSet<>(); // guarded by myLock
55
56   private final Set<Module> myChangedModulesDuringCompilation = new HashSet<>(); // guarded by myLock
57
58   private final List<ExcludeEntryDescription> myExcludedDescriptions = new SmartList<>(); // guarded by myLock
59   private boolean myCompilationPhase; // guarded by myLock
60   private volatile GlobalSearchScope myExcludedFilesScope; // calculated outside myLock
61   private final Set<String> myCompilationAffectedModules = ContainerUtil.newConcurrentSet(); // used outside myLock
62   private final FileTypeRegistry myFileTypeRegistry = FileTypeRegistry.getInstance();
63
64
65   DirtyScopeHolder(@NotNull CompilerReferenceServiceBase<?> service,
66                    @NotNull FileDocumentManager fileDocumentManager,
67                    @NotNull PsiDocumentManager psiDocumentManager,
68                    @NotNull BiConsumer<? super MessageBusConnection, ? super Set<String>> compilationAffectedModulesSubscription) {
69     myService = service;
70     myFileDocManager = fileDocumentManager;
71     myPsiDocManager = psiDocumentManager;
72
73     if (CompilerReferenceService.isEnabled()) {
74       final MessageBusConnection connect = service.getProject().getMessageBus().connect();
75       connect.subscribe(ExcludedEntriesListener.TOPIC, new ExcludedEntriesListener() {
76         @Override
77         public void onEntryAdded(@NotNull ExcludeEntryDescription description) {
78           synchronized (myLock) {
79             if (myCompilationPhase) {
80               myExcludedDescriptions.add(description);
81             }
82           }
83         }
84       });
85
86       compilationAffectedModulesSubscription.accept(connect, myCompilationAffectedModules);
87
88       connect.subscribe(ProjectTopics.PROJECT_ROOTS, new ModuleRootListener() {
89         @Override
90         public void beforeRootsChange(@NotNull ModuleRootEvent event) {
91           final Module[] modules = ModuleManager.getInstance(myService.getProject()).getModules();
92           synchronized (myLock) {
93             ContainerUtil.addAll(myVFSChangedModules, modules);
94           }
95         }
96       });
97     }
98   }
99
100   void compilerActivityStarted() {
101     final ExcludeEntryDescription[] excludeEntryDescriptions =
102       CompilerConfiguration.getInstance(myService.getProject()).getExcludedEntriesConfiguration().getExcludeEntryDescriptions();
103     synchronized (myLock) {
104       myCompilationPhase = true;
105       Collections.addAll(myExcludedDescriptions, excludeEntryDescriptions);
106       myExcludedFilesScope = null;
107       myCompilationAffectedModules.clear();
108     }
109   }
110
111   public void upToDateChecked(boolean isUpToDate) {
112     final Module[] modules = ReadAction.compute(() -> {
113       final Project project = myService.getProject();
114       if (project.isDisposed()) {
115         return null;
116       }
117       return ModuleManager.getInstance(project).getModules();
118     });
119     if (modules == null) return;
120     compilationFinished(() -> {
121       if (!isUpToDate) {
122         ContainerUtil.addAll(myVFSChangedModules, modules);
123       }
124     });
125   }
126
127   void compilerActivityFinished() {
128     final List<Module> compiledModules = ReadAction.compute(() -> {
129       final Project project = myService.getProject();
130       if (project.isDisposed()) {
131         return null;
132       }
133       final ModuleManager moduleManager = ModuleManager.getInstance(myService.getProject());
134       return ContainerUtil.map(myCompilationAffectedModules, moduleManager::findModuleByName);
135     });
136     compilationFinished(() -> {
137       if (compiledModules == null) return;
138       myVFSChangedModules.removeAll(compiledModules);
139     });
140   }
141
142   private void compilationFinished(@NotNull Runnable action) {
143     ExcludeEntryDescription[] descriptions;
144     synchronized (myLock) {
145       myCompilationPhase = false;
146       action.run();
147       myVFSChangedModules.addAll(myChangedModulesDuringCompilation);
148       myChangedModulesDuringCompilation.clear();
149       descriptions = myExcludedDescriptions.toArray(new ExcludeEntryDescription[0]);
150       myExcludedDescriptions.clear();
151     }
152     myCompilationAffectedModules.clear();
153     myExcludedFilesScope = ExcludedFromCompileFilesUtil.getExcludedFilesScope(descriptions, myService.getFileTypes(), myService.getProject(), myService.getFileIndex());
154   }
155
156   @NotNull
157   public GlobalSearchScope getDirtyScope() {
158     final Project project = myService.getProject();
159     return ReadAction.compute(() -> {
160       synchronized (myLock) {
161         if (myCompilationPhase) {
162           return GlobalSearchScope.allScope(project);
163         }
164         if (project.isDisposed()) throw new ProcessCanceledException();
165         return CachedValuesManager.getManager(project).getCachedValue(this, () ->
166           CachedValueProvider.Result
167             .create(calculateDirtyScope(), PsiModificationTracker.MODIFICATION_COUNT, VirtualFileManager.getInstance(), myService));
168       }
169     });
170   }
171
172   @NotNull
173   private GlobalSearchScope calculateDirtyScope() {
174     final Set<Module> dirtyModules = getAllDirtyModules();
175     if (dirtyModules.isEmpty()) return myExcludedFilesScope;
176     GlobalSearchScope dirtyModuleScope = GlobalSearchScope.union(dirtyModules
177                                                                    .stream()
178                                                                    .map(Module::getModuleWithDependentsScope)
179                                                                    .toArray(GlobalSearchScope[]::new));
180     return dirtyModuleScope.union(myExcludedFilesScope);
181   }
182
183   @NotNull
184   public Set<Module> getAllDirtyModules() {
185     final Set<Module> dirtyModules = new THashSet<>(myVFSChangedModules);
186     for (Document document : myFileDocManager.getUnsavedDocuments()) {
187       final VirtualFile file = myFileDocManager.getFile(document);
188       if (file == null) continue;
189       final Module m = getModuleForSourceContentFile(file);
190       if (m != null) dirtyModules.add(m);
191     }
192     for (Document document : myPsiDocManager.getUncommittedDocuments()) {
193       final PsiFile psiFile = myPsiDocManager.getPsiFile(document);
194       if (psiFile == null) continue;
195       final VirtualFile file = psiFile.getVirtualFile();
196       if (file == null) continue;
197       final Module m = getModuleForSourceContentFile(file);
198       if (m != null) dirtyModules.add(m);
199     }
200     return dirtyModules;
201   }
202
203   public boolean contains(@NotNull VirtualFile file) {
204     return getDirtyScope().contains(file);
205   }
206
207   @Nullable
208   @Override
209   public ChangeApplier prepareChange(@NotNull List<? extends VFileEvent> events) {
210     final List<Module> modulesToBeMarkedDirty = getModulesToBeMarkedDirtyBefore(events);
211
212     return new ChangeApplier() {
213       @Override
214       public void beforeVfsChange() {
215         modulesToBeMarkedDirty.forEach(DirtyScopeHolder.this::addToDirtyModules);
216       }
217
218       @Override
219       public void afterVfsChange() {
220         if (!myService.getProject().isDisposed()) {
221           after(events);
222         }
223       }
224     };
225   }
226
227   private void after(@NotNull List<? extends VFileEvent> events) {
228     for (VFileEvent event : events) {
229       if (event instanceof VFileCreateEvent) {
230         VirtualFile parent = ((VFileCreateEvent)event).getParent();
231         String fileName = ((VFileCreateEvent)event).getChildName();
232         Module module = getModuleForSourceContentFile(parent, fileName);
233         if (module != null) {
234           addToDirtyModules(module);
235         }
236       }
237       else if (event instanceof VFileCopyEvent || event instanceof VFileMoveEvent) {
238         VirtualFile file = event.getFile();
239         if (file != null) {
240           fileChanged(file);
241         }
242       }
243       else {
244         if (event instanceof VFilePropertyChangeEvent) {
245           VFilePropertyChangeEvent pce = (VFilePropertyChangeEvent)event;
246           String propertyName = pce.getPropertyName();
247           if (VirtualFile.PROP_NAME.equals(propertyName) || VirtualFile.PROP_SYMLINK_TARGET.equals(propertyName)) {
248             fileChanged(pce.getFile());
249           }
250         }
251       }
252     }
253   }
254
255   @Contract(pure=true)
256   @NotNull
257   private List<Module> getModulesToBeMarkedDirtyBefore(@NotNull List<? extends VFileEvent> events) {
258     final List<Module> modulesToBeMarkedDirty = new ArrayList<>();
259
260     for (VFileEvent event : events) {
261       ProgressManager.checkCanceled();
262
263       if (event instanceof VFileDeleteEvent || event instanceof VFileMoveEvent || event instanceof VFileContentChangeEvent) {
264         VirtualFile file = event.getFile();
265         if (file != null) {
266           final Module module = getModuleForSourceContentFile(file);
267           ContainerUtil.addIfNotNull(modulesToBeMarkedDirty, module);
268         }
269       }
270       else if (event instanceof VFilePropertyChangeEvent) {
271         VFilePropertyChangeEvent pce = (VFilePropertyChangeEvent)event;
272         String propertyName = pce.getPropertyName();
273         if (VirtualFile.PROP_NAME.equals(propertyName) || VirtualFile.PROP_SYMLINK_TARGET.equals(propertyName)) {
274           final String path = pce.getFile().getPath();
275           for (Module module : ModuleManager.getInstance(myService.getProject()).getModules()) {
276             if (FileUtil.isAncestor(path, module.getModuleFilePath(), true)) {
277               modulesToBeMarkedDirty.add(module);
278             }
279           }
280         }
281       }
282     }
283     return modulesToBeMarkedDirty;
284   }
285
286   public void installVFSListener(@NotNull Disposable parentDisposable) {
287     VirtualFileManager.getInstance().addAsyncFileListener(this, parentDisposable);
288   }
289
290   private void fileChanged(@NotNull VirtualFile file) {
291     final Module module = getModuleForSourceContentFile(file);
292     if (module != null) {
293       addToDirtyModules(module);
294     }
295   }
296
297   private void addToDirtyModules(@NotNull Module module) {
298     synchronized (myLock) {
299       if (myCompilationPhase) {
300         myChangedModulesDuringCompilation.add(module);
301       }
302       else {
303         myVFSChangedModules.add(module);
304       }
305     }
306   }
307
308   private Module getModuleForSourceContentFile(@NotNull VirtualFile file) {
309     return getModuleForSourceContentFile(file, file.getNameSequence());
310   }
311   private Module getModuleForSourceContentFile(@NotNull VirtualFile parent, @NotNull CharSequence fileName) {
312     FileType fileType = myFileTypeRegistry.getFileTypeByFileName(fileName);
313     if (myService.getFileTypes().contains(fileType) && myService.getFileIndex().isInSourceContent(parent)) {
314       return myService.getFileIndex().getModuleForFile(parent);
315     }
316     return null;
317   }
318
319   @TestOnly
320   @NotNull
321   Set<Module> getAllDirtyModulesForTest() {
322     synchronized (myLock) {
323       return getAllDirtyModules();
324     }
325   }
326
327   @SuppressWarnings("unchecked")
328   @NotNull
329   DirtyScopeTestInfo getState() {
330     synchronized (myLock) {
331       final Module[] vfsChangedModules = myVFSChangedModules.toArray(Module.EMPTY_ARRAY);
332       final List<Module> unsavedChangedModuleList = new ArrayList<>(getAllDirtyModules());
333       ContainerUtil.removeAll(unsavedChangedModuleList, vfsChangedModules);
334       final Module[] unsavedChangedModules = unsavedChangedModuleList.toArray(Module.EMPTY_ARRAY);
335       final List<VirtualFile> excludedFiles = myExcludedFilesScope instanceof Iterable ? ContainerUtil.newArrayList((Iterable<VirtualFile>)myExcludedFilesScope) : Collections.emptyList();
336       return new DirtyScopeTestInfo(vfsChangedModules, unsavedChangedModules, excludedFiles.toArray(VirtualFile.EMPTY_ARRAY), getDirtyScope());
337     }
338   }
339 }