524238ad7dc38e838fce65929c6f256d776f0db3
[idea/community.git] / platform / lang-impl / src / com / intellij / openapi / roots / impl / ProjectRootManagerComponent.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.openapi.roots.impl;
3
4 import com.intellij.ProjectTopics;
5 import com.intellij.openapi.Disposable;
6 import com.intellij.openapi.application.*;
7 import com.intellij.openapi.components.ProjectComponent;
8 import com.intellij.openapi.components.impl.stores.BatchUpdateListener;
9 import com.intellij.openapi.diagnostic.Logger;
10 import com.intellij.openapi.fileTypes.FileTypeEvent;
11 import com.intellij.openapi.fileTypes.FileTypeListener;
12 import com.intellij.openapi.fileTypes.FileTypeManager;
13 import com.intellij.openapi.module.Module;
14 import com.intellij.openapi.module.ModuleManager;
15 import com.intellij.openapi.module.impl.ModuleEx;
16 import com.intellij.openapi.project.DumbModeTask;
17 import com.intellij.openapi.project.DumbServiceImpl;
18 import com.intellij.openapi.project.Project;
19 import com.intellij.openapi.roots.AdditionalLibraryRootsProvider;
20 import com.intellij.openapi.roots.ModuleRootManager;
21 import com.intellij.openapi.roots.OrderRootType;
22 import com.intellij.openapi.roots.WatchedRootsProvider;
23 import com.intellij.openapi.startup.StartupManager;
24 import com.intellij.openapi.util.Disposer;
25 import com.intellij.openapi.util.Pair;
26 import com.intellij.openapi.util.io.FileUtil;
27 import com.intellij.openapi.vfs.*;
28 import com.intellij.openapi.vfs.impl.VirtualFilePointerContainerImpl;
29 import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
30 import com.intellij.openapi.vfs.pointers.VirtualFilePointer;
31 import com.intellij.openapi.vfs.pointers.VirtualFilePointerContainer;
32 import com.intellij.openapi.vfs.pointers.VirtualFilePointerListener;
33 import com.intellij.openapi.vfs.pointers.VirtualFilePointerManager;
34 import com.intellij.project.ProjectKt;
35 import com.intellij.ui.GuiUtils;
36 import com.intellij.util.ConcurrencyUtil;
37 import com.intellij.util.concurrency.AppExecutorUtil;
38 import com.intellij.util.containers.ContainerUtil;
39 import com.intellij.util.indexing.FileBasedIndex;
40 import com.intellij.util.indexing.FileBasedIndexImpl;
41 import com.intellij.util.indexing.FileBasedIndexProjectHandler;
42 import com.intellij.util.indexing.UnindexedFilesUpdater;
43 import com.intellij.util.messages.MessageBusConnection;
44 import gnu.trove.THashSet;
45 import org.jetbrains.annotations.CalledInAwt;
46 import org.jetbrains.annotations.NotNull;
47
48 import java.io.File;
49 import java.util.Collection;
50 import java.util.Collections;
51 import java.util.List;
52 import java.util.Set;
53 import java.util.concurrent.CompletableFuture;
54 import java.util.concurrent.ExecutorService;
55 import java.util.concurrent.Future;
56
57 /**
58  * ProjectRootManager extended with ability to watch events.
59  */
60 public class ProjectRootManagerComponent extends ProjectRootManagerImpl implements ProjectComponent, Disposable {
61   private static final Logger LOG = Logger.getInstance(ProjectRootManagerComponent.class);
62   private static final boolean LOG_CACHES_UPDATE =
63     ApplicationManager.getApplication().isInternal() && !ApplicationManager.getApplication().isUnitTestMode();
64
65   private final ExecutorService myExecutor = AppExecutorUtil.createBoundedApplicationPoolExecutor("Project Root Manager", 1);
66   private Future<?> myCollectWatchRootsFuture = CompletableFuture.completedFuture(null);
67
68   private boolean myPointerChangesDetected;
69   private int myInsideRefresh;
70   @NotNull
71   private Set<LocalFileSystem.WatchRequest> myRootsToWatch = new THashSet<>();
72   private Disposable myRootPointersDisposable = Disposer.newDisposable(); // accessed in EDT
73
74   public ProjectRootManagerComponent(@NotNull Project project) {
75     super(project);
76
77     MessageBusConnection connection = project.getMessageBus().connect(this);
78     connection.subscribe(FileTypeManager.TOPIC, new FileTypeListener() {
79       @Override
80       public void beforeFileTypesChanged(@NotNull FileTypeEvent event) {
81         beforeRootsChange(true);
82       }
83
84       @Override
85       public void fileTypesChanged(@NotNull FileTypeEvent event) {
86         rootsChanged(true);
87       }
88     });
89
90     VirtualFileManager.getInstance().addVirtualFileManagerListener(new VirtualFileManagerListener() {
91       @Override
92       public void afterRefreshFinish(boolean asynchronous) {
93         doUpdateOnRefresh();
94       }
95     }, project);
96
97     if (!myProject.isDefault()) {
98       StartupManager.getInstance(project).registerStartupActivity(() -> myStartupActivityPerformed = true);
99     }
100
101     connection.subscribe(BatchUpdateListener.TOPIC, new BatchUpdateListener() {
102       @Override
103       public void onBatchUpdateStarted() {
104         myRootsChanged.levelUp();
105         myFileTypesChanged.levelUp();
106       }
107
108       @Override
109       public void onBatchUpdateFinished() {
110         myRootsChanged.levelDown();
111         myFileTypesChanged.levelDown();
112       }
113     });
114   }
115
116   @Override
117   public void projectOpened() {
118     addRootsToWatch();
119     ApplicationManager.getApplication().addApplicationListener(new AppListener(), myProject);
120   }
121
122   @Override
123   public void projectClosed() {
124     LocalFileSystem.getInstance().removeWatchedRoots(myRootsToWatch);
125   }
126
127   @CalledInAwt
128   private void addRootsToWatch() {
129     if (myProject.isDefault()) return;
130
131     Disposable prev = myRootPointersDisposable;
132     Disposable next = Disposer.newDisposable();
133
134     Application application = ApplicationManager.getApplication();
135     ExecutorService service = application.isUnitTestMode() ? ConcurrencyUtil.newSameThreadExecutorService() : myExecutor;
136
137     myCollectWatchRootsFuture.cancel(false);
138     myCollectWatchRootsFuture = service.submit(() -> {
139       if (myProject.isDisposed()) return;
140       Pair<Set<String>, Set<String>> pair = ReadAction.compute(() -> collectWatchRoots(next));
141       GuiUtils.invokeLaterIfNeeded(() -> {
142         myRootPointersDisposable = next;
143         Disposer.dispose(prev); // dispose after the re-creating container to keep VFPs from disposing and re-creating back
144         myRootsToWatch = LocalFileSystem.getInstance().replaceWatchedRoots(myRootsToWatch, pair.first, pair.second);
145       }, ModalityState.any());
146     });
147   }
148
149   private void beforeRootsChange(boolean fileTypes) {
150     if (myProject.isDisposed()) return;
151     getBatchSession(fileTypes).beforeRootsChanged();
152   }
153
154   private void rootsChanged(boolean fileTypes) {
155     getBatchSession(fileTypes).rootsChanged();
156   }
157
158   private void doUpdateOnRefresh() {
159     if (ApplicationManager.getApplication().isUnitTestMode() && (!myStartupActivityPerformed || myProject.isDisposed())) {
160       return; // in test mode suppress addition to a queue unless project is properly initialized
161     }
162     if (myProject.isDefault()) {
163       return;
164     }
165
166     if (LOG_CACHES_UPDATE || LOG.isDebugEnabled()) {
167       LOG.debug("refresh");
168     }
169     DumbServiceImpl dumbService = DumbServiceImpl.getInstance(myProject);
170     DumbModeTask task = FileBasedIndexProjectHandler.createChangedFilesIndexingTask(myProject);
171     if (task != null) {
172       dumbService.queueTask(task);
173     }
174   }
175
176   @Override
177   protected void fireBeforeRootsChangeEvent(boolean fileTypes) {
178     isFiringEvent = true;
179     try {
180       myProject.getMessageBus().syncPublisher(ProjectTopics.PROJECT_ROOTS).beforeRootsChange(new ModuleRootEventImpl(myProject, fileTypes));
181     }
182     finally {
183       isFiringEvent = false;
184     }
185   }
186
187   @Override
188   protected void fireRootsChangedEvent(boolean fileTypes) {
189     isFiringEvent = true;
190     try {
191       myProject.getMessageBus().syncPublisher(ProjectTopics.PROJECT_ROOTS).rootsChanged(new ModuleRootEventImpl(myProject, fileTypes));
192     }
193     finally {
194       isFiringEvent = false;
195     }
196
197     synchronizeRoots();
198     addRootsToWatch();
199   }
200
201   private Pair<Set<String>, Set<String>> collectWatchRoots(@NotNull Disposable disposable) {
202     Set<String> recursivePaths = new THashSet<>(FileUtil.PATH_HASHING_STRATEGY);
203     Set<String> flatPaths = new THashSet<>(FileUtil.PATH_HASHING_STRATEGY);
204
205     String projectFilePath = myProject.getProjectFilePath();
206     if (projectFilePath != null && !Project.DIRECTORY_STORE_FOLDER.equals(new File(projectFilePath).getParentFile().getName())) {
207       flatPaths.add(FileUtil.toSystemIndependentName(projectFilePath));
208       String wsFilePath = ProjectKt.getStateStore(myProject).getWorkspaceFilePath();  // may not exist yet
209       if (wsFilePath != null) {
210         flatPaths.add(FileUtil.toSystemIndependentName(wsFilePath));
211       }
212     }
213
214     for (AdditionalLibraryRootsProvider extension : AdditionalLibraryRootsProvider.EP_NAME.getExtensions()) {
215       Collection<VirtualFile> toWatch = extension.getRootsToWatch(myProject);
216       if (!toWatch.isEmpty()) {
217         recursivePaths.addAll(ContainerUtil.map(toWatch, VirtualFile::getPath));
218       }
219     }
220
221     for (WatchedRootsProvider extension : WatchedRootsProvider.EP_NAME.getExtensions(myProject)) {
222       Set<String> toWatch = extension.getRootsToWatch();
223       if (!toWatch.isEmpty()) {
224         recursivePaths.addAll(ContainerUtil.map(toWatch, FileUtil::toSystemIndependentName));
225       }
226     }
227
228     
229     List<String> recursiveUrls = ContainerUtil.map(recursivePaths, VfsUtilCore::pathToUrl);
230     Set<String> excludedUrls = new THashSet<>();
231     // changes in files provided by this method should be watched manually because no-one's bothered to set up correct pointers for them
232     for (DirectoryIndexExcludePolicy excludePolicy : DirectoryIndexExcludePolicy.EP_NAME.getExtensions(myProject)) {
233       Collections.addAll(excludedUrls, excludePolicy.getExcludeUrlsForProject());
234     }
235
236     // avoid creating empty unnecessary container
237     if (!recursiveUrls.isEmpty() || !flatPaths.isEmpty() || !excludedUrls.isEmpty()) {
238       Disposer.register(this, disposable);
239       // creating a container with these URLs with the sole purpose to get events to getRootsValidityChangedListener() when these roots change
240       VirtualFilePointerContainer container =
241         VirtualFilePointerManager.getInstance().createContainer(disposable, getRootsValidityChangedListener());
242
243       ((VirtualFilePointerContainerImpl)container).addAllJarDirectories(recursiveUrls, true);
244       flatPaths.forEach(path -> container.add(VfsUtilCore.pathToUrl(path)));
245       ((VirtualFilePointerContainerImpl)container).addAll(excludedUrls);
246     }
247
248     // module roots already fire validity change events, see usages of ProjectRootManagerComponent.getRootsValidityChangedListener
249     collectModuleWatchRoots(recursivePaths, flatPaths);
250     
251     return Pair.create(recursivePaths, flatPaths);
252   }
253
254   private void collectModuleWatchRoots(@NotNull Set<String> recursivePaths, @NotNull Set<String> flatPaths) {
255     Set<String> urls = ContainerUtil.newTroveSet(FileUtil.PATH_HASHING_STRATEGY);
256
257     for (Module module : ModuleManager.getInstance(myProject).getModules()) {
258       ModuleRootManager rootManager = ModuleRootManager.getInstance(module);
259
260       ContainerUtil.addAll(urls, rootManager.getContentRootUrls());
261
262       rootManager.orderEntries().withoutModuleSourceEntries().withoutDepModules().forEach(entry -> {
263         for (OrderRootType type : OrderRootType.getAllTypes()) {
264           ContainerUtil.addAll(urls, entry.getUrls(type));
265         }
266         return true;
267       });
268     }
269
270     for (String url : urls) {
271       String protocol = VirtualFileManager.extractProtocol(url);
272       if (protocol == null || StandardFileSystems.FILE_PROTOCOL.equals(protocol)) {
273         recursivePaths.add(extractLocalPath(url));
274       }
275       else if (StandardFileSystems.JAR_PROTOCOL.equals(protocol)) {
276         flatPaths.add(extractLocalPath(url));
277       }
278       else if (StandardFileSystems.JRT_PROTOCOL.equals(protocol)) {
279         recursivePaths.add(extractLocalPath(url));
280       }
281     }
282   }
283
284   private void synchronizeRoots() {
285     if (!myStartupActivityPerformed) return;
286
287     if (LOG_CACHES_UPDATE || LOG.isDebugEnabled()) {
288       LOG.debug(new Throwable("sync roots"));
289     }
290     else if (!ApplicationManager.getApplication().isUnitTestMode()) {
291       LOG.info("project roots have changed");
292     }
293
294     DumbServiceImpl dumbService = DumbServiceImpl.getInstance(myProject);
295     if (FileBasedIndex.getInstance() instanceof FileBasedIndexImpl) {
296       dumbService.queueTask(new UnindexedFilesUpdater(myProject));
297     }
298   }
299
300   @Override
301   protected void clearScopesCaches() {
302     super.clearScopesCaches();
303     LibraryScopeCache.getInstance(myProject).clear();
304   }
305
306   @Override
307   public void clearScopesCachesForModules() {
308     super.clearScopesCachesForModules();
309     Module[] modules = ModuleManager.getInstance(myProject).getModules();
310     for (Module module : modules) {
311       ((ModuleEx)module).clearScopesCache();
312     }
313   }
314
315   @Override
316   public void markRootsForRefresh() {
317     Set<String> paths = new THashSet<>(FileUtil.PATH_HASHING_STRATEGY);
318     collectModuleWatchRoots(paths, paths);
319
320     LocalFileSystem fs = LocalFileSystem.getInstance();
321     for (String path : paths) {
322       VirtualFile root = fs.findFileByPath(path);
323       if (root instanceof NewVirtualFile) {
324         ((NewVirtualFile)root).markDirtyRecursively();
325       }
326     }
327   }
328
329   @Override
330   public void dispose() {
331     myCollectWatchRootsFuture.cancel(false);
332     myExecutor.shutdownNow();
333     assertListenersAreDisposed();
334   }
335
336   private class AppListener implements ApplicationListener {
337     @Override
338     public void beforeWriteActionStart(@NotNull Object action) {
339       myInsideRefresh++;
340     }
341
342     @Override
343     public void writeActionFinished(@NotNull Object action) {
344       if (--myInsideRefresh == 0 && myPointerChangesDetected) {
345         myPointerChangesDetected = false;
346         incModificationCount();
347
348         myProject.getMessageBus().syncPublisher(ProjectTopics.PROJECT_ROOTS).rootsChanged(new ModuleRootEventImpl(myProject, false));
349
350         synchronizeRoots();
351         addRootsToWatch();
352       }
353     }
354   }
355
356   private final VirtualFilePointerListener myRootsChangedListener = new VirtualFilePointerListener() {
357     @Override
358     public void beforeValidityChanged(@NotNull VirtualFilePointer[] pointers) {
359       if (myProject.isDisposed()) {
360         return;
361       }
362
363       if (myInsideRefresh == 0) {
364         beforeRootsChange(false);
365         if (LOG_CACHES_UPDATE || LOG.isDebugEnabled()) {
366           LOG.debug(new Throwable(pointers.length > 0 ? pointers[0].getPresentableUrl():""));
367         }
368       }
369       else if (!myPointerChangesDetected) {
370         //this is the first pointer changing validity
371         myPointerChangesDetected = true;
372         myProject.getMessageBus().syncPublisher(ProjectTopics.PROJECT_ROOTS).beforeRootsChange(new ModuleRootEventImpl(myProject, false));
373         if (LOG_CACHES_UPDATE || LOG.isDebugEnabled()) {
374           LOG.debug(new Throwable(pointers.length > 0 ? pointers[0].getPresentableUrl() : ""));
375         }
376       }
377     }
378
379     @Override
380     public void validityChanged(@NotNull VirtualFilePointer[] pointers) {
381       if (myProject.isDisposed()) {
382         return;
383       }
384
385       if (myInsideRefresh > 0) {
386         clearScopesCaches();
387       }
388       else {
389         rootsChanged(false);
390       }
391     }
392   };
393
394   @NotNull
395   @Override
396   public VirtualFilePointerListener getRootsValidityChangedListener() {
397     return myRootsChangedListener;
398   }
399 }