/*
* Copyright 2000-2016 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.openapi.application.ApplicationAdapter;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.ProjectComponent;
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.ModuleRootManager;
import com.intellij.openapi.roots.OrderEntry;
import com.intellij.openapi.roots.OrderRootType;
import com.intellij.openapi.roots.WatchedRootsProvider;
import com.intellij.openapi.startup.StartupManager;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.*;
import com.intellij.openapi.vfs.ex.VirtualFileManagerAdapter;
import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
import com.intellij.openapi.vfs.pointers.VirtualFilePointer;
import com.intellij.openapi.vfs.pointers.VirtualFilePointerListener;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.HashSet;
import com.intellij.util.indexing.FileBasedIndex;
import com.intellij.util.indexing.FileBasedIndexImpl;
import com.intellij.util.indexing.FileBasedIndexProjectHandler;
import com.intellij.util.indexing.UnindexedFilesUpdater;
import com.intellij.util.messages.MessageBusConnection;
import consulo.annotations.RequiredWriteAction;
import consulo.roots.ContentFolderScopes;
import consulo.roots.OrderEntryWithTracking;
import consulo.vfs.ArchiveFileSystem;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.Collection;
import java.util.Set;
/**
* ProjectRootManager extended with ability to watch events.
*/
public class ProjectRootManagerComponent extends ProjectRootManagerImpl implements ProjectComponent {
private static final Logger LOG = Logger.getInstance(ProjectRootManagerComponent.class);
private boolean myPointerChangesDetected = false;
private int myInsideRefresh = 0;
private final BatchUpdateListener myHandler;
private final MessageBusConnection myConnection;
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);
if (!project.isDefault()) {
startupManager.registerStartupActivity(() -> myStartupActivityPerformed = true);
}
myHandler = new BatchUpdateListener() {
@Override
public void onBatchUpdateStarted() {
myRootsChanged.levelUp();
myFileTypesChanged.levelUp();
}
@Override
@RequiredWriteAction
public void onBatchUpdateFinished() {
myRootsChanged.levelDown();
myFileTypesChanged.levelDown();
}
};
myConnection.subscribe(VirtualFilePointerListener.TOPIC, new MyVirtualFilePointerListener());
myDoLogCachesUpdate = ApplicationManager.getApplication().isInternal() && !ApplicationManager.getApplication().isUnitTestMode();
}
@Override
public void disposeComponent() {
}
@NotNull
@Override
public String getComponentName() {
return "ProjectRootManager";
}
@Override
public void initComponent() {
myConnection.subscribe(BatchUpdateListener.TOPIC, myHandler);
}
@Override
public void projectOpened() {
addRootsToWatch();
ApplicationManager.getApplication().addApplicationListener(new AppListener(), myProject);
}
@Override
public void 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);
}
@RequiredWriteAction
private void beforeRootsChange(boolean fileTypes) {
if (myProject.isDisposed()) return;
getBatchSession(fileTypes).beforeRootsChanged();
}
@RequiredWriteAction
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.debug("refresh");
DumbServiceImpl dumbService = DumbServiceImpl.getInstance(myProject);
DumbModeTask task = FileBasedIndexProjectHandler.createChangedFilesIndexingTask(myProject);
if (task != null) {
dumbService.queueTask(task);
}
}
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(StandardFileSystems.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 = projectFilePath == null ? null : 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());
}
addRootsFromModules(includeSourceRoots, recursive, flat);
return Pair.create(recursive, flat);
}
private void addRootsFromModules(boolean includeSourceRoots, Set<String> recursive, Set<String> flat) {
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.getContentFolderUrls(ContentFolderScopes.all(false)), recursive, flat);
}
final OrderEntry[] orderEntries = moduleRootManager.getOrderEntries();
for (OrderEntry entry : orderEntries) {
if (entry instanceof OrderEntryWithTracking) {
for (OrderRootType orderRootType : OrderRootType.getAllTypes()) {
addRootsToTrack(entry.getUrls(orderRootType), recursive, flat);
}
}
}
}
}
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 {
VirtualFileSystem fileSystem = VirtualFileManager.getInstance().getFileSystem(protocol);
if (fileSystem instanceof ArchiveFileSystem) {
flat.add(extractLocalPath(url));
}
}
}
}
}
@Override
protected void doSynchronizeRoots() {
if (!myStartupActivityPerformed) return;
if (myDoLogCachesUpdate) {
LOG.debug(new Throwable("sync roots"));
}
else if (!ApplicationManager.getApplication().isUnitTestMode()) LOG.info("project roots have changed");
DumbServiceImpl dumbService = DumbServiceImpl.getInstance(myProject);
if (FileBasedIndex.getInstance() instanceof FileBasedIndexImpl) {
dumbService.queueTask(new UnindexedFilesUpdater(myProject));
}
}
@Override
public void markRootsForRefresh() {
Set<String> paths = ContainerUtil.newTroveSet(FileUtil.PATH_HASHING_STRATEGY);
addRootsFromModules(false, paths, paths);
LocalFileSystem fs = LocalFileSystem.getInstance();
for (String path : paths) {
VirtualFile root = fs.findFileByPath(path);
if (root instanceof NewVirtualFile) {
((NewVirtualFile)root).markDirtyRecursively();
}
}
}
@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(@NotNull Object action) {
myInsideRefresh++;
}
@Override
public void writeActionFinished(@NotNull 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.debug(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.debug(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);
}
}
}
}
}