diff --git a/java/java-tests/testSrc/com/intellij/roots/AutomaticModuleUnloaderTest.kt b/java/java-tests/testSrc/com/intellij/roots/AutomaticModuleUnloaderTest.kt new file mode 100644 index 000000000000..249752504939 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/roots/AutomaticModuleUnloaderTest.kt @@ -0,0 +1,137 @@ +// Copyright 2000-2017 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. +package com.intellij.roots + +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.components.stateStore +import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleManager +import com.intellij.openapi.module.StdModuleTypes +import com.intellij.openapi.module.impl.ModuleManagerImpl +import com.intellij.openapi.module.impl.ModulePath +import com.intellij.openapi.module.impl.UnloadedModuleDescriptionImpl +import com.intellij.openapi.project.ex.ProjectManagerEx +import com.intellij.openapi.project.impl.ProjectManagerImpl +import com.intellij.openapi.roots.ModuleRootModificationUtil +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.io.FileUtil +import com.intellij.testFramework.ModuleTestCase +import java.io.File +import java.util.* + +/** + * @author nik + */ +class AutomaticModuleUnloaderTest : ModuleTestCase() { + fun `test unload simple module`() { + createModule("a") + createModule("b") + val moduleManager = ModuleManager.getInstance(project) + moduleManager.setUnloadedModules(listOf("a")) + createModule("c") + + val moduleFiles = createNewModuleFiles(listOf("d")) {} + reloadProjectWithNewModules(moduleFiles) + + ModuleTestCase.assertSameElements(moduleManager.unloadedModuleDescriptions.map { it.name }, "a", "d") + } + + fun `test unload modules with dependencies between them`() { + createModule("a") + createModule("b") + doTest("a", listOf("c", "d"), { modules -> + ModuleRootModificationUtil.updateModel(modules["c"]!!) { + it.addModuleOrderEntry(modules["d"]!!) + } + },"a", "c", "d") + } + + fun `test do not unload module if loaded module depends on it`() { + createModule("a") + val b = createModule("b") + ModuleRootModificationUtil.updateModel(b) { + it.addInvalidModuleEntry("d") + } + doTest("a", listOf("d"), {}, "a") + } + + fun `test unload module if only unloaded module depends on it`() { + val a = createModule("a") + createModule("b") + ModuleRootModificationUtil.updateModel(a) { + it.addInvalidModuleEntry("d") + } + doTest("a", listOf("d"), {}, "a", "d") + } + + fun `test do not unload modules if loaded module depends on them transitively`() { + createModule("a") + val b = createModule("b") + ModuleRootModificationUtil.updateModel(b) { + it.addInvalidModuleEntry("d") + } + + doTest("a", listOf("c", "d"), { modules -> + ModuleRootModificationUtil.updateModel(modules["d"]!!) { + it.addModuleOrderEntry(modules["c"]!!) + } + }, "a") + } + + fun `test unload module if loaded module transitively depends on it via previosly unloaded module`() { + val a = createModule("a") + val b = createModule("b") + ModuleRootModificationUtil.addDependency(a, b) + ModuleRootModificationUtil.updateModel(b) { + it.addInvalidModuleEntry("c") + } + doTest("b", listOf("c"), {}, "b", "c") + } + + private fun doTest(initiallyUnloaded: String, + newModulesName: List, + setup: (Map) -> Unit, + vararg expectedUnloadedModules: String) { + val moduleManager = ModuleManager.getInstance(project) + moduleManager.setUnloadedModules(listOf(initiallyUnloaded)) + + val moduleFiles = createNewModuleFiles(newModulesName, setup) + reloadProjectWithNewModules(moduleFiles) + + ModuleTestCase.assertSameElements(moduleManager.unloadedModuleDescriptions.map { it.name }, *expectedUnloadedModules) + + } + + private fun createNewModuleFiles(moduleNames: List, setup: (Map) -> Unit): List { + val newModulesProjectDir = FileUtil.createTempDirectory("newModules", "") + val moduleFiles = moduleNames.map { File(newModulesProjectDir, "$it.iml") } + val projectManager = ProjectManagerEx.getInstanceEx() as ProjectManagerImpl + val project = projectManager.createProject("newModules", newModulesProjectDir.absolutePath)!! + try { + val modules = runWriteAction { + moduleFiles.map { + ModuleManager.getInstance(project).newModule(it.absolutePath, StdModuleTypes.JAVA.id) + } + } + setup(ModuleManager.getInstance(project).modules.associateBy { it.name }) + modules.forEach { + it.stateStore.save(mutableListOf()) + } + } + finally { + projectManager.forceCloseProject(project, true) + runWriteAction { Disposer.dispose(project) } + } + return moduleFiles + } + + private fun reloadProjectWithNewModules(moduleFiles: List) { + val moduleManager = ModuleManagerImpl.getInstanceImpl(myProject) + val modulePaths = LinkedHashSet() + moduleManager.modules.forEach { it.stateStore.save(mutableListOf()) } + moduleManager.modules.mapTo(modulePaths) { ModulePath(it.moduleFilePath, null) } + moduleManager.unloadedModuleDescriptions.mapTo(modulePaths) { (it as UnloadedModuleDescriptionImpl).modulePath } + moduleFiles.mapTo(modulePaths) { ModulePath(FileUtil.toSystemIndependentName(it.absolutePath), null) } + moduleManager.loadStateFromModulePaths(modulePaths) + } + +} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/openapi/module/impl/AutomaticModuleUnloader.kt b/platform/lang-impl/src/com/intellij/openapi/module/impl/AutomaticModuleUnloader.kt new file mode 100644 index 000000000000..fe6370303f4f --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/module/impl/AutomaticModuleUnloader.kt @@ -0,0 +1,160 @@ +// Copyright 2000-2017 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. +package com.intellij.openapi.module.impl + +import com.intellij.notification.Notification +import com.intellij.notification.NotificationAction +import com.intellij.notification.NotificationGroup +import com.intellij.notification.NotificationType +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.components.* +import com.intellij.openapi.module.ModuleDescription +import com.intellij.openapi.module.ModuleManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ui.configuration.ConfigureUnloadedModulesDialog +import com.intellij.util.xmlb.annotations.AbstractCollection +import com.intellij.util.xmlb.annotations.Tag +import com.intellij.xml.util.XmlStringUtil + +/** + * If some modules were unloaded and new modules appears after loading project configuration, automatically unloads those which + * aren't required for loaded modules. + * + * @author nik + */ +@State(name = "AutomaticModuleUnloader", storages = arrayOf(Storage(StoragePathMacros.WORKSPACE_FILE))) +class AutomaticModuleUnloader(private val project: Project) : PersistentStateComponent { + private val loadedModulesListStorage = LoadedModulesListStorage() + + fun processNewModules(modulesToLoad: Set, modulesToUnload: List): UnloadedModulesListChange { + val oldLoaded = loadedModulesListStorage.modules.toSet() + if (oldLoaded.isEmpty() || modulesToLoad.all { it.moduleName in oldLoaded }) { + return UnloadedModulesListChange(emptyList(), emptyList(), emptyList()) + } + + val moduleDescriptions = LinkedHashMap(modulesToLoad.size + modulesToUnload.size) + UnloadedModuleDescriptionImpl.createFromPaths(modulesToLoad, project).associateByTo(moduleDescriptions) { it.name } + modulesToUnload.associateByTo(moduleDescriptions) { it.name } + + val oldLoadedWithDependencies = HashSet() + val explicitlyUnloaded = modulesToUnload.mapTo(HashSet()) { it.name } + for (name in oldLoaded) { + processTransitiveDependencies(name, moduleDescriptions, explicitlyUnloaded, oldLoadedWithDependencies) + } + + val newLoadedNames = oldLoadedWithDependencies.mapTo(LinkedHashSet()) { it.name } + val toLoad = modulesToLoad.filter { it.moduleName in newLoadedNames && it.moduleName !in oldLoaded} + val toUnload = modulesToLoad.filter { it.moduleName !in newLoadedNames } + loadedModulesListStorage.modules.clear() + modulesToLoad.filter { it.moduleName in newLoadedNames }.mapTo(loadedModulesListStorage.modules) { it.moduleName } + val change = UnloadedModulesListChange(toLoad, toUnload, toUnload.map { moduleDescriptions[it.moduleName]!! }) + fireNotifications(change) + return change + } + + private fun processTransitiveDependencies(name: String, moduleDescriptions: Map, + explicitlyUnloaded: Set, result: MutableSet) { + if (name in explicitlyUnloaded) return + + val module = moduleDescriptions[name] + if (module == null || !result.add(module)) return + + module.dependencyModuleNames.forEach { + processTransitiveDependencies(it, moduleDescriptions, explicitlyUnloaded, result) + } + } + + private fun fireNotifications(change: UnloadedModulesListChange) { + if (change.toLoad.isEmpty() && change.toUnload.isEmpty()) return + + val messages = ArrayList() + val actions = ArrayList() + populateNotification(change.toUnload, messages, actions, "Load", {"Load $it back"}, change.toLoad.isEmpty(), {"unloaded"}) { + it.removeAll(change.toUnload.map { it.moduleName }) + } + populateNotification(change.toLoad, messages, actions, "Unload", {"Unload $it"}, change.toUnload.isEmpty(), {"loaded because some other modules depend on $it"}) { + it.addAll(change.toLoad.map { it.moduleName }) + } + actions.add(object: NotificationAction("Configure Unloaded Modules") { + override fun actionPerformed(e: AnActionEvent, notification: Notification) { + val ok = ConfigureUnloadedModulesDialog(project, null).showAndGet() + if (ok) { + notification.expire() + } + } + }) + + NOTIFICATION_GROUP.createNotification("New Modules are Added", XmlStringUtil.wrapInHtml(messages.joinToString("
")), + NotificationType.INFORMATION, null) + .apply { + actions.forEach { addAction(it) } + } + .notify(project) + } + + private fun populateNotification(modules: List, + messages: ArrayList, + actions: ArrayList, + revertActionName: String, + revertActionShortText: (String) -> String, + useShortActionText: Boolean, + statusDescription: (String) -> String, + revertAction: (MutableList) -> Unit) { + when { + modules.size == 1 -> { + val moduleName = modules.single().moduleName + messages.add("Newly added module '$moduleName' was automatically ${statusDescription("it")}.") + val text = if (useShortActionText) revertActionShortText("it") else "$revertActionName '$moduleName' module" + actions.add(createAction(text, revertAction)) + } + modules.size == 2 -> { + val names = "'${modules[0].moduleName}' and '${modules[1].moduleName}'" + messages.add("Newly added modules $names were automatically ${statusDescription("them")}.") + val text = if (useShortActionText) revertActionShortText("them") else "$revertActionName modules $names" + actions.add(createAction(text, revertAction)) + } + modules.size > 2 -> { + val names = "'${modules.first().moduleName}' and ${modules.size - 1} more modules" + messages.add("$names were automatically ${statusDescription("them")}.") + val text = if (useShortActionText) revertActionShortText("them") else "$revertActionName $names" + actions.add(createAction(text, revertAction)) + } + } + } + + fun createAction(text: String, action: (MutableList) -> Unit) = object : NotificationAction(text) { + override fun actionPerformed(e: AnActionEvent, notification: Notification) { + val unloaded = ArrayList() + val moduleManager = ModuleManager.getInstance(project) + moduleManager.unloadedModuleDescriptions.mapTo(unloaded) { it.name } + action(unloaded) + moduleManager.setUnloadedModules(unloaded) + notification.expire() + } + } + + fun setLoadedModules(modules: List) { + loadedModulesListStorage.modules.clear() + loadedModulesListStorage.modules.addAll(modules) + } + + override fun getState() = loadedModulesListStorage + + override fun loadState(state: LoadedModulesListStorage?) { + setLoadedModules(state?.modules ?: emptyList()) + } + + companion object { + @JvmStatic + fun getInstance(project: Project) = project.service() + + private val NOTIFICATION_GROUP = NotificationGroup.balloonGroup("Automatic Module Unloading") + } +} + +class LoadedModulesListStorage { + @Tag("loaded-modules") + @AbstractCollection(surroundWithTag = false, elementTag = "module", elementValueAttribute = "name") + var modules: MutableList = ArrayList() +} + +class UnloadedModulesListChange(val toLoad: List, val toUnload: List, val toUnloadDescriptions: List) diff --git a/platform/lang-impl/src/com/intellij/openapi/module/impl/ModuleManagerComponent.java b/platform/lang-impl/src/com/intellij/openapi/module/impl/ModuleManagerComponent.java index 16bc48431f8b..014a1aa95bfc 100644 --- a/platform/lang-impl/src/com/intellij/openapi/module/impl/ModuleManagerComponent.java +++ b/platform/lang-impl/src/com/intellij/openapi/module/impl/ModuleManagerComponent.java @@ -36,7 +36,10 @@ import com.intellij.util.messages.MessageHandler; import org.jetbrains.annotations.NotNull; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * @author yole @@ -80,6 +83,13 @@ public class ModuleManagerComponent extends ModuleManagerImpl { myMessageBusConnection.subscribe(VirtualFileManager.VFS_CHANGES, new ModuleFileListener(this)); } + @Override + protected void unloadNewlyAddedModulesIfPossible(Set modulesToLoad, List modulesToUnload) { + UnloadedModulesListChange change = AutomaticModuleUnloader.getInstance(myProject).processNewModules(modulesToLoad, modulesToUnload); + modulesToLoad.removeAll(change.getToUnload()); + modulesToUnload.addAll(change.getToUnloadDescriptions()); + } + @Override protected void showUnknownModuleTypeNotification(@NotNull List modulesWithUnknownTypes) { if (!ApplicationManager.getApplication().isHeadlessEnvironment() && !modulesWithUnknownTypes.isEmpty()) { @@ -120,6 +130,16 @@ public class ModuleManagerComponent extends ModuleManagerImpl { return createModule(filePath); } + @Override + protected void setUnloadedModuleNames(@NotNull List unloadedModuleNames) { + super.setUnloadedModuleNames(unloadedModuleNames); + if (!unloadedModuleNames.isEmpty()) { + List loadedModules = new ArrayList<>(myModuleModel.myModules.keySet()); + loadedModules.removeAll(new HashSet<>(unloadedModuleNames)); + AutomaticModuleUnloader.getInstance(myProject).setLoadedModules(loadedModules); + } + } + @Override protected boolean isUnknownModuleType(@NotNull Module module) { return ModuleType.get(module) instanceof UnknownModuleType; diff --git a/platform/platform-resources/src/META-INF/LangExtensions.xml b/platform/platform-resources/src/META-INF/LangExtensions.xml index ae882fb3a86f..b1aa2640efe4 100644 --- a/platform/platform-resources/src/META-INF/LangExtensions.xml +++ b/platform/platform-resources/src/META-INF/LangExtensions.xml @@ -254,6 +254,7 @@ serviceImplementation="com.intellij.facet.impl.invalid.InvalidFacetManagerImpl"/> + diff --git a/platform/projectModel-impl/src/com/intellij/openapi/module/impl/ModuleManagerImpl.java b/platform/projectModel-impl/src/com/intellij/openapi/module/impl/ModuleManagerImpl.java index 903b57fd6f56..191bc9e3d206 100644 --- a/platform/projectModel-impl/src/com/intellij/openapi/module/impl/ModuleManagerImpl.java +++ b/platform/projectModel-impl/src/com/intellij/openapi/module/impl/ModuleManagerImpl.java @@ -60,6 +60,7 @@ import gnu.trove.TObjectHashingStrategy; import org.jdom.Element; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; import java.io.FileNotFoundException; import java.io.IOException; @@ -165,8 +166,17 @@ public abstract class ModuleManagerImpl extends ModuleManager implements Disposa @Override public void loadState(Element state) { + loadState(getPathsToModuleFiles(state)); + } + + @TestOnly + public void loadStateFromModulePaths(LinkedHashSet modulePaths) { + loadState(modulePaths); + } + + private void loadState(LinkedHashSet modulePaths) { boolean isFirstLoadState = myModulePathsToLoad == null; - myModulePathsToLoad = getPathsToModuleFiles(state); + myModulePathsToLoad = modulePaths; Set unloadedModuleNames = new HashSet<>(UnloadedModulesListStorage.getInstance(myProject).getUnloadedModuleNames()); Iterator iterator = myModulePathsToLoad.iterator(); List unloadedModulePaths = new ArrayList<>(); @@ -177,9 +187,12 @@ public abstract class ModuleManagerImpl extends ModuleManager implements Disposa iterator.remove(); } } - List descriptions = UnloadedModuleDescriptionImpl.createFromPaths(unloadedModulePaths, this); + List unloaded = new ArrayList<>(UnloadedModuleDescriptionImpl.createFromPaths(unloadedModulePaths, this)); + if (!unloaded.isEmpty()) { + unloadNewlyAddedModulesIfPossible(myModulePathsToLoad, unloaded); + } myUnloadedModules.clear(); - for (UnloadedModuleDescriptionImpl description : descriptions) { + for (UnloadedModuleDescriptionImpl description : unloaded) { myUnloadedModules.put(description.getName(), description); } @@ -219,6 +232,9 @@ public abstract class ModuleManagerImpl extends ModuleManager implements Disposa myModulePathsToLoad.clear(); } + protected void unloadNewlyAddedModulesIfPossible(Set modulesToLoad, List modulesToUnload) { + } + @NotNull // returns mutable linked hash set public static LinkedHashSet getPathsToModuleFiles(@NotNull Element element) { @@ -964,7 +980,7 @@ public abstract class ModuleManagerImpl extends ModuleManager implements Disposa fireModulesRenamed(modules, oldNames); cleanCachedStuff(); UnloadedModulesListStorage unloadedModulesListStorage = UnloadedModulesListStorage.getInstance(myProject); - unloadedModulesListStorage.setUnloadedModuleNames(ContainerUtil.filter(unloadedModulesListStorage.getUnloadedModuleNames(), myUnloadedModules::containsKey)); + setUnloadedModuleNames(ContainerUtil.filter(unloadedModulesListStorage.getUnloadedModuleNames(), myUnloadedModules::containsKey)); }, false, true); } @@ -1016,8 +1032,8 @@ public abstract class ModuleManagerImpl extends ModuleManager implements Disposa return; } - UnloadedModulesListStorage.getInstance(myProject).setUnloadedModuleNames(unloadedModuleNames); - + setUnloadedModuleNames(unloadedModuleNames); + final ModifiableModuleModel model = getModifiableModel(); Map toLoad = new LinkedHashMap<>(myUnloadedModules); myUnloadedModules.clear(); @@ -1049,6 +1065,10 @@ public abstract class ModuleManagerImpl extends ModuleManager implements Disposa myModulePathsToLoad.clear(); } + protected void setUnloadedModuleNames(@NotNull List unloadedModuleNames) { + UnloadedModulesListStorage.getInstance(myProject).setUnloadedModuleNames(unloadedModuleNames); + } + public void setModuleGroupPath(Module module, String[] groupPath) { myModuleModel.setModuleGroupPath(module, groupPath); } diff --git a/platform/projectModel-impl/src/com/intellij/openapi/module/impl/UnloadedModuleDescriptionImpl.kt b/platform/projectModel-impl/src/com/intellij/openapi/module/impl/UnloadedModuleDescriptionImpl.kt index e638cec8e873..3dca6d491e00 100644 --- a/platform/projectModel-impl/src/com/intellij/openapi/module/impl/UnloadedModuleDescriptionImpl.kt +++ b/platform/projectModel-impl/src/com/intellij/openapi/module/impl/UnloadedModuleDescriptionImpl.kt @@ -46,7 +46,7 @@ class UnloadedModuleDescriptionImpl(val modulePath: ModulePath, companion object { @JvmStatic - fun createFromPaths(paths: List, parentDisposable: Disposable): List { + fun createFromPaths(paths: Collection, parentDisposable: Disposable): List { val pathVariables = JpsGlobalLoader.computeAllPathVariables(PathManager.getOptionsPath()) val modules = JpsProjectLoader.loadModules(paths.map { Paths.get(it.path) }, null, pathVariables) val pathsByName = paths.associateBy { it.moduleName }