blob: 4ba8b7b03a8d65eb842d68ae8dc4127652c54274 [file] [log] [blame]
/*
* 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.openapi.roots.impl;
import com.intellij.ProjectTopics;
import com.intellij.ide.caches.CacheUpdater;
import com.intellij.openapi.application.ApplicationAdapter;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.impl.stores.BatchUpdateListener;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.fileTypes.FileTypeEvent;
import com.intellij.openapi.fileTypes.FileTypeListener;
import com.intellij.openapi.fileTypes.FileTypeManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.module.impl.ModuleEx;
import com.intellij.openapi.project.DumbModeTask;
import com.intellij.openapi.project.DumbServiceImpl;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.*;
import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.startup.StartupManager;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vfs.*;
import com.intellij.openapi.vfs.ex.VirtualFileManagerAdapter;
import com.intellij.openapi.vfs.pointers.VirtualFilePointer;
import com.intellij.openapi.vfs.pointers.VirtualFilePointerListener;
import com.intellij.util.SystemProperties;
import com.intellij.util.containers.HashSet;
import com.intellij.util.indexing.FileBasedIndexProjectHandler;
import com.intellij.util.indexing.UnindexedFilesUpdater;
import com.intellij.util.messages.MessageBusConnection;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
/**
* ProjectRootManager extended with ability to watch events.
*/
public class ProjectRootManagerComponent extends ProjectRootManagerImpl {
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.roots.impl.ProjectManagerComponent");
private static final boolean ourScheduleCacheUpdateInDumbMode = SystemProperties.getBooleanProperty(
"idea.schedule.cache.update.in.dumb.mode", true);
private boolean myPointerChangesDetected = false;
private int myInsideRefresh = 0;
private final BatchUpdateListener myHandler;
private final MessageBusConnection myConnection;
protected final List<CacheUpdater> myRootsChangeUpdaters = new ArrayList<CacheUpdater>();
protected final List<CacheUpdater> myRefreshCacheUpdaters = new ArrayList<CacheUpdater>();
private Set<LocalFileSystem.WatchRequest> myRootsToWatch = new THashSet<LocalFileSystem.WatchRequest>();
private final boolean myDoLogCachesUpdate;
public ProjectRootManagerComponent(Project project, StartupManager startupManager) {
super(project);
myConnection = project.getMessageBus().connect(project);
myConnection.subscribe(FileTypeManager.TOPIC, new FileTypeListener() {
@Override
public void beforeFileTypesChanged(@NotNull FileTypeEvent event) {
beforeRootsChange(true);
}
@Override
public void fileTypesChanged(@NotNull FileTypeEvent event) {
rootsChanged(true);
}
});
VirtualFileManager.getInstance().addVirtualFileManagerListener(new VirtualFileManagerAdapter() {
@Override
public void afterRefreshFinish(boolean asynchronous) {
doUpdateOnRefresh();
}
}, project);
startupManager.registerStartupActivity(new Runnable() {
@Override
public void run() {
myStartupActivityPerformed = true;
}
});
myHandler = new BatchUpdateListener() {
@Override
public void onBatchUpdateStarted() {
myRootsChanged.levelUp();
myFileTypesChanged.levelUp();
}
@Override
public void onBatchUpdateFinished() {
myRootsChanged.levelDown();
myFileTypesChanged.levelDown();
}
};
myConnection.subscribe(VirtualFilePointerListener.TOPIC, new MyVirtualFilePointerListener());
myDoLogCachesUpdate = ApplicationManager.getApplication().isInternal() && !ApplicationManager.getApplication().isUnitTestMode();
}
public void registerRootsChangeUpdater(CacheUpdater updater) {
myRootsChangeUpdaters.add(updater);
}
public void unregisterRootsChangeUpdater(CacheUpdater updater) {
boolean removed = myRootsChangeUpdaters.remove(updater);
LOG.assertTrue(removed);
}
public void registerRefreshUpdater(CacheUpdater updater) {
myRefreshCacheUpdaters.add(updater);
}
public void unregisterRefreshUpdater(CacheUpdater updater) {
boolean removed = myRefreshCacheUpdaters.remove(updater);
LOG.assertTrue(removed);
}
@Override
public void initComponent() {
super.initComponent();
myConnection.subscribe(BatchUpdateListener.TOPIC, myHandler);
}
@Override
public void projectOpened() {
super.projectOpened();
addRootsToWatch();
AppListener applicationListener = new AppListener();
ApplicationManager.getApplication().addApplicationListener(applicationListener, myProject);
}
@Override
public void projectClosed() {
super.projectClosed();
LocalFileSystem.getInstance().removeWatchedRoots(myRootsToWatch);
}
@Override
protected void addRootsToWatch() {
final Pair<Set<String>, Set<String>> roots = getAllRoots(false);
if (roots == null) return;
myRootsToWatch = LocalFileSystem.getInstance().replaceWatchedRoots(myRootsToWatch, roots.first, roots.second);
}
private void beforeRootsChange(boolean fileTypes) {
if (myProject.isDisposed()) return;
getBatchSession(fileTypes).beforeRootsChanged();
}
private void rootsChanged(boolean fileTypes) {
getBatchSession(fileTypes).rootsChanged();
}
private void doUpdateOnRefresh() {
if (ApplicationManager.getApplication().isUnitTestMode() && (!myStartupActivityPerformed || myProject.isDisposed())) {
return; // in test mode suppress addition to a queue unless project is properly initialized
}
if (myProject.isDefault()) {
return;
}
if (myDoLogCachesUpdate) LOG.info("refresh");
DumbServiceImpl dumbService = DumbServiceImpl.getInstance(myProject);
DumbModeTask task = FileBasedIndexProjectHandler.createChangedFilesIndexingTask(myProject);
if (task != null) {
dumbService.queueTask(task);
}
if (myRefreshCacheUpdaters.size() == 0) {
return;
}
if (ourScheduleCacheUpdateInDumbMode) {
dumbService.queueCacheUpdateInDumbMode(myRefreshCacheUpdaters);
}
else {
dumbService.queueCacheUpdate(myRefreshCacheUpdaters);
}
}
private boolean affectsRoots(VirtualFilePointer[] pointers) {
Pair<Set<String>, Set<String>> roots = getAllRoots(true);
if (roots == null) return false;
for (VirtualFilePointer pointer : pointers) {
final String path = url2path(pointer.getUrl());
if (roots.first.contains(path) || roots.second.contains(path)) return true;
}
return false;
}
@Override
protected void fireBeforeRootsChangeEvent(boolean fileTypes) {
isFiringEvent = true;
try {
myProject.getMessageBus()
.syncPublisher(ProjectTopics.PROJECT_ROOTS)
.beforeRootsChange(new ModuleRootEventImpl(myProject, fileTypes));
}
finally {
isFiringEvent= false;
}
}
@Override
protected void fireRootsChangedEvent(boolean fileTypes) {
isFiringEvent = true;
try {
myProject.getMessageBus()
.syncPublisher(ProjectTopics.PROJECT_ROOTS)
.rootsChanged(new ModuleRootEventImpl(myProject, fileTypes));
}
finally {
isFiringEvent = false;
}
}
private static String url2path(String url) {
String path = VfsUtilCore.urlToPath(url);
int separatorIndex = path.indexOf(JarFileSystem.JAR_SEPARATOR);
if (separatorIndex < 0) return path;
return path.substring(0, separatorIndex);
}
@Nullable
private Pair<Set<String>, Set<String>> getAllRoots(boolean includeSourceRoots) {
if (myProject.isDefault()) return null;
final Set<String> recursive = new HashSet<String>();
final Set<String> flat = new HashSet<String>();
final String projectFilePath = myProject.getProjectFilePath();
final File projectDirFile = new File(projectFilePath).getParentFile();
if (projectDirFile != null && projectDirFile.getName().equals(Project.DIRECTORY_STORE_FOLDER)) {
recursive.add(projectDirFile.getAbsolutePath());
}
else {
flat.add(projectFilePath);
final VirtualFile workspaceFile = myProject.getWorkspaceFile();
if (workspaceFile != null) {
flat.add(workspaceFile.getPath());
}
}
for (WatchedRootsProvider extension : Extensions.getExtensions(WatchedRootsProvider.EP_NAME, myProject)) {
recursive.addAll(extension.getRootsToWatch());
}
final Module[] modules = ModuleManager.getInstance(myProject).getModules();
for (Module module : modules) {
final ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(module);
addRootsToTrack(moduleRootManager.getContentRootUrls(), recursive, flat);
if (includeSourceRoots) {
addRootsToTrack(moduleRootManager.getSourceRootUrls(), recursive, flat);
}
flat.add(module.getModuleFilePath());
final OrderEntry[] orderEntries = moduleRootManager.getOrderEntries();
for (OrderEntry entry : orderEntries) {
if (entry instanceof LibraryOrderEntry) {
final Library library = ((LibraryOrderEntry)entry).getLibrary();
if (library != null) {
for (OrderRootType orderRootType : OrderRootType.getAllTypes()) {
addRootsToTrack(library.getUrls(orderRootType), recursive, flat);
}
}
}
else if (entry instanceof JdkOrderEntry) {
for (OrderRootType orderRootType : OrderRootType.getAllTypes()) {
addRootsToTrack(((JdkOrderEntry)entry).getRootUrls(orderRootType), recursive, flat);
}
}
}
}
return Pair.create(recursive, flat);
}
@Override
protected void doSynchronizeRoots() {
if (!myStartupActivityPerformed) return;
if (myDoLogCachesUpdate) LOG.info(new Throwable("sync roots"));
DumbServiceImpl dumbService = DumbServiceImpl.getInstance(myProject);
dumbService.queueTask(new UnindexedFilesUpdater(myProject, false));
if (myRootsChangeUpdaters.isEmpty()) return;
if (ourScheduleCacheUpdateInDumbMode) {
dumbService.queueCacheUpdateInDumbMode(myRootsChangeUpdaters);
} else {
dumbService.queueCacheUpdate(myRootsChangeUpdaters);
}
}
private static void addRootsToTrack(final String[] urls, final Collection<String> recursive, final Collection<String> flat) {
for (String url : urls) {
if (url != null) {
final String protocol = VirtualFileManager.extractProtocol(url);
if (protocol == null || LocalFileSystem.PROTOCOL.equals(protocol)) {
recursive.add(extractLocalPath(url));
}
else if (JarFileSystem.PROTOCOL.equals(protocol)) {
flat.add(extractLocalPath(url));
}
}
}
}
@Override
protected void clearScopesCaches() {
super.clearScopesCaches();
LibraryScopeCache.getInstance(myProject).clear();
}
@Override
public void clearScopesCachesForModules() {
super.clearScopesCachesForModules();
Module[] modules = ModuleManager.getInstance(myProject).getModules();
for (Module module : modules) {
((ModuleEx)module).clearScopesCache();
}
}
private class AppListener extends ApplicationAdapter {
@Override
public void beforeWriteActionStart(Object action) {
myInsideRefresh++;
}
@Override
public void writeActionFinished(Object action) {
if (--myInsideRefresh == 0) {
if (myPointerChangesDetected) {
myPointerChangesDetected = false;
myProject.getMessageBus().syncPublisher(ProjectTopics.PROJECT_ROOTS).rootsChanged(new ModuleRootEventImpl(myProject, false));
doSynchronizeRoots();
addRootsToWatch();
}
}
}
}
private class MyVirtualFilePointerListener implements VirtualFilePointerListener {
@Override
public void beforeValidityChanged(@NotNull VirtualFilePointer[] pointers) {
if (!myProject.isDisposed()) {
if (myInsideRefresh == 0) {
if (affectsRoots(pointers)) {
beforeRootsChange(false);
if (myDoLogCachesUpdate) LOG.info(new Throwable(pointers.length > 0 ? pointers[0].getPresentableUrl():""));
}
}
else if (!myPointerChangesDetected) {
//this is the first pointer changing validity
if (affectsRoots(pointers)) {
myPointerChangesDetected = true;
myProject.getMessageBus().syncPublisher(ProjectTopics.PROJECT_ROOTS).beforeRootsChange(new ModuleRootEventImpl(myProject, false));
if (myDoLogCachesUpdate) LOG.info(new Throwable(pointers.length > 0 ? pointers[0].getPresentableUrl():""));
}
}
}
}
@Override
public void validityChanged(@NotNull VirtualFilePointer[] pointers) {
if (!myProject.isDisposed()) {
if (myInsideRefresh > 0) {
clearScopesCaches();
}
else if (affectsRoots(pointers)) {
rootsChanged(false);
}
}
}
}
}