diff --git a/platform/lang-impl/src/com/intellij/openapi/roots/ui/configuration/ConfigureUnloadedModulesDialog.kt b/platform/lang-impl/src/com/intellij/openapi/roots/ui/configuration/ConfigureUnloadedModulesDialog.kt index c0cb6a59c811..b1ce4c0ac9c8 100644 --- a/platform/lang-impl/src/com/intellij/openapi/roots/ui/configuration/ConfigureUnloadedModulesDialog.kt +++ b/platform/lang-impl/src/com/intellij/openapi/roots/ui/configuration/ConfigureUnloadedModulesDialog.kt @@ -15,6 +15,7 @@ */ package com.intellij.openapi.roots.ui.configuration +import com.intellij.CommonBundle import com.intellij.icons.AllIcons import com.intellij.ide.projectView.impl.ModuleGroup import com.intellij.ide.projectView.impl.ModuleGroupingImplementation @@ -24,6 +25,7 @@ import com.intellij.openapi.module.impl.LoadedModuleDescriptionImpl import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectBundle import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.Messages import com.intellij.openapi.ui.VerticalFlowLayout import com.intellij.openapi.util.text.NaturalComparator import com.intellij.openapi.wm.IdeFocusManager @@ -32,6 +34,7 @@ import com.intellij.ui.TreeSpeedSearch import com.intellij.ui.components.JBLabel import com.intellij.ui.components.JBScrollPane import com.intellij.ui.treeStructure.Tree +import com.intellij.util.graph.* import com.intellij.util.ui.GridBag import com.intellij.util.ui.tree.TreeUtil import com.intellij.xml.util.XmlStringUtil @@ -55,6 +58,8 @@ class ConfigureUnloadedModulesDialog(private val project: Project, selectedModul private val unloadedModulesTree = ModuleDescriptionsTree(project) private val moduleDescriptions = ModuleManager.getInstance(project).allModuleDescriptions.associateBy { it.name } private val statusLabel = JBLabel() + /** graph contains an edge a -> b if b depends on a */ + private val dependentsGraph by lazy { buildGraph() } init { title = ProjectBundle.message("module.load.unload.dialog.title") @@ -64,6 +69,18 @@ class ConfigureUnloadedModulesDialog(private val project: Project, selectedModul init() } + private fun buildGraph(): Graph { + return GraphGenerator.generate(CachingSemiGraph.cache(object: InboundSemiGraph { + override fun getNodes(): Collection { + return moduleDescriptions.values + } + + override fun getIn(node: ModuleDescription): Iterator { + return node.dependencyModuleNames.asIterable().mapNotNull { moduleDescriptions[it] }.iterator() + } + })) + } + override fun createCenterPanel(): JComponent? { val buttonsPanel = JPanel(VerticalFlowLayout()) val moveToUnloadedButton = JButton(ProjectBundle.message("module.unload.button.text")) @@ -71,10 +88,22 @@ class ConfigureUnloadedModulesDialog(private val project: Project, selectedModul val moveAllToUnloadedButton = JButton(ProjectBundle.message("module.unload.all.button.text")) val moveAllToLoadedButton = JButton(ProjectBundle.message("module.load.all.button.text")) moveToUnloadedButton.addActionListener { - moveSelectedNodes(loadedModulesTree, unloadedModulesTree) + val modulesToMove = includeMissingModules(loadedModulesTree.selectedModules, unloadedModulesTree.getAllModules(), + dependentsGraph, + ProjectBundle.message("module.unload.dependents.dialog.title"), + { selectedSize, additionalSize, additionalFirst -> ProjectBundle.message("module.unload.dependents.dialog.text", selectedSize, additionalSize, additionalFirst)}, + ProjectBundle.message("module.unload.with.dependents.button.text"), + ProjectBundle.message("module.unload.without.dependents.button.text")) + moveModules(modulesToMove, loadedModulesTree, unloadedModulesTree) } moveToLoadedButton.addActionListener { - moveSelectedNodes(unloadedModulesTree, loadedModulesTree) + val modulesToMove = includeMissingModules(unloadedModulesTree.selectedModules, loadedModulesTree.getAllModules(), + GraphAlgorithms.getInstance().invertEdgeDirections(dependentsGraph), + ProjectBundle.message("module.load.dependencies.dialog.title"), + { selectedSize, additionalSize, additionalFirst -> ProjectBundle.message("module.load.dependencies.dialog.text", selectedSize, additionalSize, additionalFirst)}, + ProjectBundle.message("module.load.with.dependencies.button.text"), + ProjectBundle.message("module.load.without.dependencies.button.text")) + moveModules(modulesToMove, unloadedModulesTree, loadedModulesTree) } moveAllToUnloadedButton.addActionListener { moveAllNodes(loadedModulesTree, unloadedModulesTree) @@ -104,6 +133,35 @@ class ConfigureUnloadedModulesDialog(private val project: Project, selectedModul return mainPanel } + private fun includeMissingModules(selected: List, availableTargetModules: List, + dependenciesGraph: Graph, + dialogTitle: String, dialogMessage: (Int, Int, String) -> String, yesButtonText: String, + noButtonText: String): Collection { + val additional = computeDependenciesToMove(selected, availableTargetModules, dependenciesGraph) + if (additional.isNotEmpty()) { + val answer = Messages.showYesNoCancelDialog(project, dialogMessage(selected.size, additional.size, additional.first().name), + dialogTitle, yesButtonText, noButtonText, CommonBundle.getCancelButtonText(), null) + if (answer == Messages.YES) { + return selected + additional + } + if (answer == Messages.CANCEL) { + return emptyList() + } + } + return selected + } + + private fun computeDependenciesToMove(modulesToMove: Collection, availableModules: Collection, + graph: Graph): Set { + val result = LinkedHashSet() + for (module in modulesToMove) { + GraphAlgorithms.getInstance().collectOutsRecursively(graph, module, result) + } + result.removeAll(modulesToMove) + result.removeAll(availableModules) + return result + } + private fun moveAllNodes(from: ModuleDescriptionsTree, to: ModuleDescriptionsTree) { from.removeAllNodes() to.fillTree(moduleDescriptions.values) @@ -112,11 +170,11 @@ class ConfigureUnloadedModulesDialog(private val project: Project, selectedModul } } - private fun moveSelectedNodes(from: ModuleDescriptionsTree, to: ModuleDescriptionsTree) { - val selected = from.selectedModules + private fun moveModules(modulesToMove: Collection, from: ModuleDescriptionsTree, to: ModuleDescriptionsTree) { + if (modulesToMove.isEmpty()) return val oldSelectedRow = from.tree.selectionModel.leadSelectionRow - from.removeModules(selected) - val modules = to.addModules(selected) + from.removeModules(modulesToMove) + val modules = to.addModules(modulesToMove) modules.firstOrNull()?.let { TreeUtil.selectNode(to.tree, it)} IdeFocusManager.getInstance(project).requestFocus(from.tree, true).doWhenDone { from.tree.selectionModel.selectionPath = from.tree.getPathForRow(oldSelectedRow.coerceAtMost(from.tree.rowCount-1)) @@ -165,11 +223,11 @@ private class ModuleDescriptionsTree(project: Project) { tree.expandPath(TreePath(root)) } - fun addModules(modules: List): List { + fun addModules(modules: Collection): List { return modules.map { helper.createModuleNode(it, root, model) } } - fun removeModules(modules: List) { + fun removeModules(modules: Collection) { val names = modules.mapTo(HashSet()) { it.name } val toRemove = findNodes { it.moduleDescription.name in names } for (node in toRemove) { diff --git a/platform/projectModel-impl/src/messages/ProjectBundle.properties b/platform/projectModel-impl/src/messages/ProjectBundle.properties index dcf942e10ada..ab0045beceaf 100644 --- a/platform/projectModel-impl/src/messages/ProjectBundle.properties +++ b/platform/projectModel-impl/src/messages/ProjectBundle.properties @@ -133,6 +133,16 @@ module.unloaded.label.text=Unloaded modules module.unloaded.explanation=Content of unloaded modules is effectively excluded from this copy of the project: 'Find' actions won't search \ for results in these modules, refactoring actions won't update usages in them, their sources won't be compiled, etc. Use this on the parts of \ the project you are currently not working on to make IDE work faster and exclude uninteresting elements from UI. +module.load.dependencies.dialog.text=The selected {0, choice, 1#module depends|2#modules depend} on {1, choice, 1#module ''''{2}'''' which is|2#{1} modules which are} \ + currently unloaded. If a loaded module depends on an unloaded module it may lead to compilations errors. What do you want? +module.load.dependencies.dialog.title=Load Required Modules +module.load.with.dependencies.button.text=Load All Required Modules +module.load.without.dependencies.button.text=Load Selected Modules Only +module.unload.dependents.dialog.text={1, choice, 1#Module ''''{2}'''' depends|2#{1} modules depend} on the selected {0, choice, 1#module|2#modules}. \ + If the selected {0, choice, 1#module|2#modules} become unloaded it may cause compilations errors in the dependent modules. What do you want? +module.unload.dependents.dialog.title=Unload Dependent Modules +module.unload.with.dependents.button.text=Unload All Dependent Modules +module.unload.without.dependents.button.text=Unload Selected Modules Only button.text.attach.files=Attach &Files or Directories... library.attach.files.action=Attach Files or Directories