qualified module names: support cases when a module works as a module group (IDEA-166061)

If name of one module is prefix of name of another, the node for the first module will be shown as a parent node for the second module. Common code which create module and module group nodes extracted to ModuleGroupingTreeHelper class.
This commit is contained in:
nik
2017-01-31 17:29:49 +03:00
parent 05d0c4839c
commit 57d4009d36
8 changed files with 661 additions and 137 deletions
@@ -119,7 +119,7 @@ public class IdeaProjectSettingsService extends ProjectSettingsService implement
@Override
public boolean processModulesMoved(final Module[] modules, @Nullable final ModuleGroup targetGroup) {
final ModuleStructureConfigurable rootConfigurable = ModuleStructureConfigurable.getInstance(myProject);
if (rootConfigurable.updateProjectTree(modules, targetGroup)) { //inside project root editor
if (rootConfigurable.updateProjectTree(modules)) { //inside project root editor
if (targetGroup != null) {
rootConfigurable.selectNodeInTree(targetGroup.toString());
}
@@ -24,8 +24,7 @@ import com.intellij.icons.AllIcons;
import com.intellij.ide.IdeBundle;
import com.intellij.ide.highlighter.ModuleFileType;
import com.intellij.ide.projectView.impl.ModuleGroup;
import com.intellij.ide.projectView.impl.ModuleGroupUtil;
import com.intellij.openapi.module.ModuleGrouper;
import com.intellij.ide.projectView.impl.ModuleGroupingTreeHelper;
import com.intellij.ide.util.projectWizard.ModuleBuilder;
import com.intellij.ide.util.projectWizard.NamePathComponent;
import com.intellij.ide.util.projectWizard.ProjectWizardUtil;
@@ -54,6 +53,7 @@ import com.intellij.openapi.ui.*;
import com.intellij.openapi.util.ActionCallback;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.NullableComputable;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
@@ -94,6 +94,9 @@ public class ModuleStructureConfigurable extends BaseStructureConfigurable imple
if (editableObject2 instanceof Module && editableObject1 instanceof String) return 1;
if (editableObject1 instanceof Module && editableObject2 instanceof String) return -1;
if (editableObject2 instanceof Module && editableObject1 instanceof Facet) return 1;
if (editableObject1 instanceof Module && editableObject2 instanceof Facet) return -1;
if (editableObject2 instanceof ModuleGroup && editableObject1 instanceof String) return 1;
if (editableObject1 instanceof ModuleGroup && editableObject2 instanceof String) return -1;
@@ -166,7 +169,7 @@ public class ModuleStructureConfigurable extends BaseStructureConfigurable imple
protected void loadTree() {
createProjectNodes();
((DefaultTreeModel)myTree.getModel()).reload();
getTreeModel().reload();
myUiDisposed = false;
}
@@ -217,27 +220,12 @@ public class ModuleStructureConfigurable extends BaseStructureConfigurable imple
private void createProjectNodes() {
final Map<ModuleGroup, MyNode> moduleGroup2NodeMap = new HashMap<>();
final Module[] modules = myModuleManager.getModules();
for (final Module module : modules) {
ModuleGrouper moduleGrouper = getModuleGrouper();
MyNode moduleNode = new ModuleNode(new ModuleConfigurable(myContext.myModulesConfigurator, module, TREE_UPDATER, moduleGrouper));
boolean nodesAdded = myFacetEditorFacade.addFacetsNodes(module, moduleNode);
nodesAdded |= addNodesFromExtensions(module, moduleNode);
if (nodesAdded) {
myTree.setShowsRootHandles(true);
}
final List<String> groupPath = myHideModuleGroups || myFlattenModules ? Collections.emptyList() : moduleGrouper.getGroupPath(module);
if (groupPath.isEmpty()) {
myRoot.add(moduleNode);
}
else {
final MyNode moduleGroupNode = ModuleGroupUtil
.buildModuleGroupPath(new ModuleGroup(groupPath), myRoot, moduleGroup2NodeMap,
parentChildRelation -> parentChildRelation.getParent().add(parentChildRelation.getChild()),
ModuleStructureConfigurable::createModuleGroupNode);
moduleGroupNode.add(moduleNode);
}
ModuleGrouper moduleGrouper = getModuleGrouper();
ModuleGroupingTreeHelper<MyNode> helper = ModuleGroupingTreeHelper.forEmptyTree(!myHideModuleGroups && !myFlattenModules,
moduleGrouper, ModuleStructureConfigurable::createModuleGroupNode, this::createModuleNode, getNodeComparator());
helper.createModuleNodes(Arrays.asList(myModuleManager.getModules()), myRoot, getTreeModel());
if (containsSecondLevelNodes(myRoot)) {
myTree.setShowsRootHandles(true);
}
sortDescendants(myRoot);
if (myProject.isDefault()) { //do not add modules node in case of template project
@@ -252,13 +240,34 @@ public class ModuleStructureConfigurable extends BaseStructureConfigurable imple
//myProjectNode.add(myLevel2Nodes.get(LibraryTablesRegistrar.PROJECT_LEVEL));
}
private static boolean containsSecondLevelNodes(TreeNode rootNode) {
int count = rootNode.getChildCount();
for (int i = 0; i < count; i++) {
TreeNode child = rootNode.getChildAt(i);
if (child.getChildCount() > 0) {
return true;
}
}
return false;
}
@NotNull
private ModuleNode createModuleNode(Module module, ModuleGrouper moduleGrouper) {
ModuleConfigurable configurable = new ModuleConfigurable(myContext.myModulesConfigurator, module, TREE_UPDATER, moduleGrouper);
List<String> groupPath = moduleGrouper.getModuleAsGroupPath(module);
ModuleNode node = new ModuleNode(configurable, groupPath != null ? new ModuleGroup(groupPath) : null);
myFacetEditorFacade.addFacetsNodes(module, node);
addNodesFromExtensions(module, node);
return node;
}
@NotNull
private static MyNode createModuleGroupNode(ModuleGroup moduleGroup) {
final NamedConfigurable moduleGroupConfigurable = new TextConfigurable<>(moduleGroup, moduleGroup.toString(),
ProjectBundle.message("module.group.banner.text", moduleGroup.toString()),
ProjectBundle.message("project.roots.module.groups.text"),
PlatformIcons.CLOSED_MODULE_GROUP_ICON);
return new ModuleGroupNode(moduleGroupConfigurable, moduleGroup);
return new ModuleGroupNodeImpl(moduleGroupConfigurable, moduleGroup);
}
private void addRootNodesFromExtensions(final MyNode root, final Project project) {
@@ -267,53 +276,35 @@ public class ModuleStructureConfigurable extends BaseStructureConfigurable imple
}
}
private boolean addNodesFromExtensions(final Module module, final MyNode moduleNode) {
boolean nodesAdded = false;
private void addNodesFromExtensions(final Module module, final MyNode moduleNode) {
for (final ModuleStructureExtension extension : ModuleStructureExtension.EP_NAME.getExtensions()) {
nodesAdded |= extension.addModuleNodeChildren(module, moduleNode, TREE_UPDATER);
extension.addModuleNodeChildren(module, moduleNode, TREE_UPDATER);
}
return nodesAdded;
}
public boolean updateProjectTree(final Module[] modules, final ModuleGroup group) {
public boolean updateProjectTree(final Module[] modules) {
if (myRoot.getChildCount() == 0) return false; //isn't visible
final MyNode[] nodes = new MyNode[modules.length];
int i = 0;
List<Pair<MyNode, Module>> nodes = new ArrayList<>(modules.length);
for (Module module : modules) {
MyNode node = findModuleNode(module);
LOG.assertTrue(node != null, "Module " + module.getName() + " is not in project.");
node.removeFromParent();
nodes[i++] = node;
nodes.add(Pair.create(node, module));
}
for (final MyNode moduleNode : nodes) {
List<String> groupPath = myHideModuleGroups || myFlattenModules
? Collections.emptyList()
: group != null ? group.getGroupPathList() : Collections.emptyList();
if (groupPath.isEmpty()) {
myRoot.add(moduleNode);
}
else {
final MyNode moduleGroupNode = getOrCreateModuleGroupNode(groupPath, null);
moduleGroupNode.add(moduleNode);
}
Module module = (Module)moduleNode.getConfigurable().getEditableObject();
myFacetEditorFacade.addFacetsNodes(module, moduleNode);
addNodesFromExtensions(module, moduleNode);
}
sortDescendants(myRoot);
ModuleGroupingTreeHelper<MyNode> helper = createGroupingHelper();
helper.moveModuleNodesToProperGroup(nodes, myRoot, getTreeModel(), myTree);
return true;
}
private MyNode getOrCreateModuleGroupNode(List<String> groupPath, @Nullable DefaultTreeModel treeModel) {
return ModuleGroupUtil.updateModuleGroupPath(new ModuleGroup(groupPath), myRoot, group1 -> findNodeByObject(myRoot, group1),
parentChildRelation -> {
MyNode parent = parentChildRelation.getParent();
parent.add(parentChildRelation.getChild());
if (treeModel != null) {
treeModel.nodesWereInserted(parent, new int[]{parent.getChildCount() - 1});
}
},
ModuleStructureConfigurable::createModuleGroupNode);
private DefaultTreeModel getTreeModel() {
return (DefaultTreeModel)myTree.getModel();
}
@NotNull
private ModuleGroupingTreeHelper<MyNode> createGroupingHelper() {
return ModuleGroupingTreeHelper.forTree(myRoot, (node) -> node instanceof ModuleGroupNode ? ((ModuleGroupNode)node).getModuleGroup() : null,
(node) -> node instanceof ModuleNode ? ((ModuleNode)node).getModule() : null,
!myHideModuleGroups && !myFlattenModules, getModuleGrouper(), ModuleStructureConfigurable::createModuleGroupNode,
this::createModuleNode, getNodeComparator());
}
@Override
@@ -336,7 +327,7 @@ public class ModuleStructureConfigurable extends BaseStructureConfigurable imple
if (node != null) {
final TreeNode parent = node.getParent();
node.removeFromParent();
((DefaultTreeModel)myTree.getModel()).reload(parent);
getTreeModel().reload(parent);
}
myContext.getDaemonAnalyzer().removeElement(new LibraryProjectStructureElement(myContext, library));
}
@@ -553,7 +544,6 @@ public class ModuleStructureConfigurable extends BaseStructureConfigurable imple
}
private void addModuleNode(final Module module) {
MyNode node = new ModuleNode(new ModuleConfigurable(myContext.myModulesConfigurator, module, TREE_UPDATER, getModuleGrouper()));
final TreePath selectionPath = myTree.getSelectionPath();
MyNode parent = null;
if (selectionPath != null) {
@@ -573,10 +563,8 @@ public class ModuleStructureConfigurable extends BaseStructureConfigurable imple
}
}
if (parent == null) parent = myRoot;
addNode(node, parent);
myFacetEditorFacade.addFacetsNodes(module, node);
addNodesFromExtensions(module, node);
((DefaultTreeModel)myTree.getModel()).reload(parent);
MyNode node = createModuleNode(module, getModuleGrouper());
TreeUtil.insertNode(parent, node, getTreeModel(), getNodeComparator());
selectNodeInTree(node);
final ProjectStructureDaemonAnalyzer daemonAnalyzer = myContext.getDaemonAnalyzer();
daemonAnalyzer.queueUpdate(new ModuleProjectStructureElement(myContext, module));
@@ -632,9 +620,16 @@ public class ModuleStructureConfigurable extends BaseStructureConfigurable imple
}
}
private class ModuleNode extends MyNode {
public ModuleNode(@NotNull ModuleConfigurable configurable) {
private class ModuleNode extends MyNode implements ModuleGroupNode {
private final ModuleGroup myModuleAsGroup;
public ModuleNode(@NotNull ModuleConfigurable configurable, @Nullable ModuleGroup moduleAsGroup) {
super(configurable);
myModuleAsGroup = moduleAsGroup;
}
public ModuleGroup getModuleGroup() {
return myModuleAsGroup;
}
@NotNull
@@ -658,45 +653,36 @@ public class ModuleStructureConfigurable extends BaseStructureConfigurable imple
@Override
protected void reloadNode(DefaultTreeModel treeModel) {
TreeNode parent = getParent();
List<String> actualPath = parent instanceof ModuleGroupNode ? ((ModuleGroupNode)parent).myModuleGroup.getGroupPathList() : Collections.emptyList();
List<String> expectedPath = getModuleGrouper().getGroupPath(getModule());
if (!actualPath.equals(expectedPath)) {
TreePath path = myTree.getSelectionPath();
boolean wasSelected = path != null && this.equals(path.getLastPathComponent());
boolean autoScrollWasEnabled = myAutoScrollEnabled;
try {
myAutoScrollEnabled = false;
MyNode newParent = getOrCreateModuleGroupNode(expectedPath, treeModel);
treeModel.removeNodeFromParent(this);
while (parent instanceof ModuleGroupNode && parent.getChildCount() == 0) {
TreeNode grandParent = parent.getParent();
treeModel.removeNodeFromParent((MutableTreeNode)parent);
parent = grandParent;
}
newParent.add(this);
treeModel.nodesWereInserted(newParent, new int[]{newParent.getChildCount() - 1});
if (wasSelected) {
myTree.expandPath(TreeUtil.getPath(myRoot, newParent));
myTree.setSelectionPath(TreeUtil.getPath(myRoot, this));
}
}
finally {
myAutoScrollEnabled = autoScrollWasEnabled;
}
boolean autoScrollWasEnabled = myAutoScrollEnabled;
try {
myAutoScrollEnabled = false;
ModuleGroupingTreeHelper<MyNode> helper = createGroupingHelper();
MyNode newNode = helper.moveModuleNodeToProperGroup(this, getModule(), myRoot, treeModel, myTree);
treeModel.reload(newNode);
}
finally {
myAutoScrollEnabled = autoScrollWasEnabled;
}
super.reloadNode(treeModel);
}
}
private static class ModuleGroupNode extends MyNode {
private interface ModuleGroupNode extends MutableTreeNode {
@Nullable
ModuleGroup getModuleGroup();
}
private static class ModuleGroupNodeImpl extends MyNode implements ModuleGroupNode {
private final ModuleGroup myModuleGroup;
public ModuleGroupNode(@NotNull NamedConfigurable configurable, ModuleGroup moduleGroup) {
public ModuleGroupNodeImpl(@NotNull NamedConfigurable configurable, @NotNull ModuleGroup moduleGroup) {
super(configurable, true);
myModuleGroup = moduleGroup;
}
@Override
public ModuleGroup getModuleGroup() {
return myModuleGroup;
}
}
private class FacetInModuleRemoveHandler extends RemoveConfigurableHandler<Facet> {
@@ -796,7 +782,7 @@ public class ModuleStructureConfigurable extends BaseStructureConfigurable imple
@Override
public void setSelected(AnActionEvent e, boolean state) {
myFlattenModules = state;
regroupModules(myFlattenModules);
regroupModules();
}
}
@@ -830,39 +816,22 @@ public class ModuleStructureConfigurable extends BaseStructureConfigurable imple
@Override
public void setSelected(AnActionEvent e, boolean state) {
myHideModuleGroups = state;
regroupModules(state);
regroupModules();
}
}
private void regroupModules(boolean removeGroupNodes) {
private void regroupModules() {
DefaultMutableTreeNode selection = null;
final TreePath selectionPath = myTree.getSelectionPath();
if (selectionPath != null) {
selection = (DefaultMutableTreeNode)selectionPath.getLastPathComponent();
}
ModuleGrouper grouper = getModuleGrouper();
for (Module module : grouper.getAllModules()) {
final List<String> groupPath = grouper.getGroupPath(module);
updateProjectTree(new Module[]{module}, !groupPath.isEmpty() ? new ModuleGroup(groupPath) : null);
}
if (removeGroupNodes) {
removeModuleGroups();
}
createGroupingHelper().moveAllModuleNodesToProperGroups(myRoot, getTreeModel());
if (selection != null) {
TreeUtil.selectInTree(selection, true, myTree);
}
}
private void removeModuleGroups() {
for (int i = myRoot.getChildCount() - 1; i >= 0; i--) {
final MyNode node = (MyNode)myRoot.getChildAt(i);
if (node.getConfigurable().getEditableObject() instanceof ModuleGroup) {
node.removeFromParent();
}
}
((DefaultTreeModel)myTree.getModel()).reload(myRoot);
}
@Override
protected AbstractAddGroup createAddAction() {
return new AbstractAddGroup(ProjectBundle.message("add.new.header.text")) {
@@ -0,0 +1,242 @@
/*
* Copyright 2000-2017 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.
*/
/*
* Copyright 2000-2017 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.ide.projectView.impl
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleGrouper
import com.intellij.openapi.util.Pair
import com.intellij.util.ui.tree.TreeUtil
import org.jetbrains.annotations.TestOnly
import java.util.*
import javax.swing.JTree
import javax.swing.tree.DefaultTreeModel
import javax.swing.tree.MutableTreeNode
import javax.swing.tree.TreeNode
/**
* Provides methods to build trees where nodes are grouped by modules (and optionally by module groups).
*
* @author nik
*/
class ModuleGroupingTreeHelper<N: MutableTreeNode> private constructor(
private val groupingEnabled: Boolean,
private val grouper: ModuleGrouper,
private val moduleGroupNodeFactory: (ModuleGroup) -> N,
private val moduleNodeFactory: (Module, ModuleGrouper) -> N,
private val nodeComparator: Comparator<in N>
) {
private val nodeForGroup = HashMap<ModuleGroup, N>()
private val nodeData = HashMap<N, ModuleTreeNodeData>()
companion object {
@JvmStatic
fun <N : MutableTreeNode> forEmptyTree(groupingEnabled: Boolean, grouper: ModuleGrouper,
moduleGroupNodeFactory: (ModuleGroup) -> N, moduleNodeFactory: (Module, ModuleGrouper) -> N,
nodeComparator: Comparator<in N>) =
ModuleGroupingTreeHelper(groupingEnabled, grouper, moduleGroupNodeFactory, moduleNodeFactory, nodeComparator)
@JvmStatic
fun <N : MutableTreeNode> forTree(rootNode: N, moduleGroupByNode: (N) -> ModuleGroup?, moduleByNode: (N) -> Module?,
groupingEnabled: Boolean, grouper: ModuleGrouper,
moduleGroupNodeFactory: (ModuleGroup) -> N, moduleNodeFactory: (Module, ModuleGrouper) -> N,
nodeComparator: Comparator<in N>): ModuleGroupingTreeHelper<N> {
val helper = ModuleGroupingTreeHelper(groupingEnabled, grouper, moduleGroupNodeFactory, moduleNodeFactory, nodeComparator)
TreeUtil.traverse(rootNode) { node ->
@Suppress("UNCHECKED_CAST")
val group = moduleGroupByNode(node as N)
val module = moduleByNode(node)
if (group != null) {
helper.nodeForGroup[group] = node
}
if (group != null || module != null) {
helper.nodeData[node] = ModuleTreeNodeData(module, group)
}
true
}
return helper
}
}
fun createModuleNodes(modules: Collection<Module>, rootNode: N, model: DefaultTreeModel): List<N> {
val nodes = modules.map { createModuleNode(it, rootNode, model) }
TreeUtil.sortRecursively(rootNode, nodeComparator)
model.nodeStructureChanged(rootNode)
return nodes
}
private fun createModuleNode(module: Module, rootNode: N, model: DefaultTreeModel): N {
val group = ModuleGroup(grouper.getGroupPath(module))
val parentNode = getOrCreateNodeForModuleGroup(group, rootNode, model, true)
val moduleNode = moduleNodeFactory(module, grouper)
insertModuleNode(moduleNode, parentNode, module, model, true)
return moduleNode
}
/**
* If [bulkOperation] is true, no events will be fired and new node will be added into arbitrary place in the children list
*/
private fun insertModuleNode(moduleNode: N, parentNode: N, module: Module, model: DefaultTreeModel, bulkOperation: Boolean) {
val moduleAsGroup = grouper.getModuleAsGroupPath(module)
if (moduleAsGroup != null) {
val moduleGroup = ModuleGroup(moduleAsGroup)
val oldModuleGroupNode = nodeForGroup[moduleGroup]
if (oldModuleGroupNode != null) {
moveChildren(oldModuleGroupNode, moduleNode, model)
model.removeNodeFromParent(oldModuleGroupNode)
removeNode(oldModuleGroupNode)
}
nodeForGroup[moduleGroup] = moduleNode
nodeData[moduleNode] = ModuleTreeNodeData(module, moduleGroup)
}
else {
nodeData[moduleNode] = ModuleTreeNodeData(module, null)
}
insertNode(moduleNode, parentNode, model, bulkOperation)
}
private fun moveChildren(fromNode: N, toNode: N, model: DefaultTreeModel) {
val children = TreeUtil.childrenToArray(fromNode)
moveChildren(children, toNode, model)
}
private fun moveChildren(children: List<TreeNode>, toNode: N, model: DefaultTreeModel) {
TreeUtil.addChildrenTo(toNode, children)
TreeUtil.sortChildren(toNode, nodeComparator)
model.nodeStructureChanged(toNode)
}
/**
* If [bulkOperation] is true, no events will be fired and new node will be added into arbitrary place in the children list
*/
private fun getOrCreateNodeForModuleGroup(group: ModuleGroup, rootNode: N, model: DefaultTreeModel, bulkOperation: Boolean): N {
if (!groupingEnabled) return rootNode
var parentNode = rootNode
val path = group.groupPathList
for (i in path.indices) {
val current = ModuleGroup(path.subList(0, i+1))
var node = nodeForGroup[current]
if (node == null) {
node = moduleGroupNodeFactory(current)
insertNode(node, parentNode, model, bulkOperation)
nodeForGroup[current] = node
nodeData[node] = ModuleTreeNodeData(null,group)
}
parentNode = node
}
return parentNode
}
private fun insertNode(node: N, parentNode: N, model: DefaultTreeModel, bulkOperation: Boolean) {
if (bulkOperation) {
parentNode.insert(node, parentNode.childCount)
}
else {
TreeUtil.insertNode(node, parentNode, model, nodeComparator)
}
}
fun moveAllModuleNodesToProperGroups(rootNode: N, model: DefaultTreeModel) {
val modules = nodeData.values.map { it.module }.filterNotNull()
nodeData.keys.forEach { it.removeFromParent() }
nodeData.clear()
nodeForGroup.clear()
createModuleNodes(modules, rootNode, model)
}
fun moveModuleNodesToProperGroup(nodes: List<Pair<N, Module>>, rootNode: N, model: DefaultTreeModel, tree: JTree) {
nodes.forEach {
moveModuleNodeToProperGroup(it.first, it.second, rootNode, model, tree)
}
}
fun moveModuleNodeToProperGroup(node: N, module: Module, rootNode: N, model: DefaultTreeModel, tree: JTree): N {
val actualGroup = ModuleGroup(grouper.getGroupPath(module))
val parent = node.parent
val expectedParent = if (groupingEnabled) nodeForGroup[actualGroup] else rootNode
if (expectedParent == parent) return node
val selectionPath = tree.selectionPath
val wasSelected = selectionPath?.lastPathComponent == node
model.removeNodeFromParent(node)
val nodeAsGroup = nodeData[node]?.group
removeNode(node)
if (nodeAsGroup != null) {
val childrenToKeep = TreeUtil.childrenToArray(node).filter { it in nodeData }
if (childrenToKeep.isNotEmpty()) {
val newGroupNode = getOrCreateNodeForModuleGroup(nodeAsGroup, rootNode, model, false)
moveChildren(childrenToKeep, newGroupNode, model)
}
}
removeEmptySyntheticModuleGroupNodes(parent, model)
val newParent = getOrCreateNodeForModuleGroup(actualGroup, rootNode, model, false)
val newNode = moduleNodeFactory(module, grouper)
insertModuleNode(newNode, newParent, module, model, false)
if (wasSelected) {
tree.expandPath(TreeUtil.getPath(rootNode, newParent))
tree.selectionPath = TreeUtil.getPath(rootNode, newNode)
}
return newNode
}
private fun removeEmptySyntheticModuleGroupNodes(parentNode: TreeNode?, model: DefaultTreeModel) {
var parent = parentNode
while (parent is MutableTreeNode && parent in nodeData && nodeData[parent]?.module == null && parent.childCount == 0) {
val grandParent = parent.parent
model.removeNodeFromParent(parent)
removeNode(parent as N)
parent = grandParent
}
}
private fun removeNode(node: N) {
val group = nodeData.remove(node)?.group
if (group != null) {
nodeForGroup.remove(group)
}
}
@TestOnly
fun getNodeForGroupMap() = Collections.unmodifiableMap(nodeForGroup)
@TestOnly
fun getModuleByNodeMap() = nodeData.mapValues { it.value.module }.filterValues { it != null }
@TestOnly
fun getGroupByNodeMap() = nodeData.mapValues { it.value.group }.filterValues { it != null }
}
private class ModuleTreeNodeData(val module: Module?, val group: ModuleGroup?)
@@ -0,0 +1,258 @@
/*
* Copyright 2000-2017 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.ide.projectView
import com.intellij.ide.projectView.impl.ModuleGroup
import com.intellij.ide.projectView.impl.ModuleGroupingTreeHelper
import com.intellij.module.getQualifiedNameModuleGrouper
import com.intellij.module.renameModule
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleGrouper
import com.intellij.openapi.util.Pair
import com.intellij.testFramework.PlatformTestCase
import com.intellij.testFramework.PlatformTestUtil
import com.intellij.ui.treeStructure.Tree
import com.intellij.util.ui.tree.TreeUtil
import junit.framework.TestCase
import java.util.*
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeModel
/**
* @author nik
*/
class ModuleGroupingTreeHelperTest: PlatformTestCase() {
private lateinit var tree: Tree
private lateinit var root: MockModuleTreeNode
private lateinit var model: DefaultTreeModel
override fun setUp() {
super.setUp()
root = MockModuleTreeNode("root")
model = DefaultTreeModel(root)
tree = Tree(model)
}
fun `test disabled grouping`() {
createHelper(false).createModuleNodes("a.main")
assertTreeEqual("""
-root
a.main""")
createHelperFromTree(true).moveAllModuleNodesAndCheckResult("""
-root
-a
a.main""")
}
fun `test single module`() {
createHelper().createModuleNodes("a.main")
assertTreeEqual("""
-root
-a
a.main""")
createHelperFromTree(false).moveAllModuleNodesAndCheckResult("""
-root
a.main""")
}
fun `test two modules`() {
createHelper().createModuleNodes("a.main", "a.util")
assertTreeEqual("""
-root
-a
a.main
a.util
""")
createHelperFromTree(false).moveAllModuleNodesAndCheckResult("""
-root
a.main
a.util""")
}
fun `test two groups`() {
createHelper().createModuleNodes("a.main", "b.util", "a.util", "b.main")
assertTreeEqual("""
-root
-a
a.main
a.util
-b
b.main
b.util
""")
createHelperFromTree(false).moveAllModuleNodesAndCheckResult("""
-root
a.main
a.util
b.main
b.util
""")
}
fun `test module as a group`() {
createHelper().createModuleNodes("a.impl", "a", "a.tests")
assertTreeEqual("""
-root
-a
a.impl
a.tests
""")
createHelperFromTree(false).moveAllModuleNodesAndCheckResult("""
-root
a
a.impl
a.tests""")
}
fun `test move module node to new group`() {
val nodes = createHelper().createModuleNodes("main", "util")
assertTreeEqual("""
-root
main
util""")
val node = nodes.find { it.second.name == "main" }!!
renameModule(node.second, "a.main")
moveModuleNodeToProperGroupAndCheckResult(node, """
-root
-a
a.main
util""")
}
fun `test move module node from parent module`() {
val nodes = createHelper().createModuleNodes("a", "a.main")
assertTreeEqual("""
-root
-a
a.main""")
val node = nodes.find { it.second.name == "a.main" }!!
renameModule(node.second, "main")
moveModuleNodeToProperGroupAndCheckResult(node, """
-root
a
main""")
}
fun `test move module node to parent module`() {
val nodes = createHelper().createModuleNodes("a", "main")
val node = nodes.find { it.second.name == "main" }!!
renameModule(node.second, "a.main")
moveModuleNodeToProperGroupAndCheckResult(node, """
-root
-a
a.main""")
}
fun `test module node become parent module`() {
val nodes = createHelper().createModuleNodes("b", "a.main")
val node = nodes.find { it.second.name == "b" }!!
renameModule(node.second, "a")
moveModuleNodeToProperGroupAndCheckResult(node, """
-root
-a
a.main""")
}
fun `test parent module become ordinary module`() {
val nodes = createHelper().createModuleNodes("a", "a.main")
val node = nodes.find { it.second.name == "a" }!!
renameModule(node.second, "b")
moveModuleNodeToProperGroupAndCheckResult(node, """
-root
-a
a.main
b""")
}
private fun moveModuleNodeToProperGroupAndCheckResult(node: Pair<MockModuleTreeNode, Module>,
expected: String) {
val helper = createHelperFromTree()
helper.checkConsistency()
helper.moveModuleNodeToProperGroup(node.first, node.second, root, model, tree)
assertTreeEqual(expected)
helper.checkConsistency()
}
private fun ModuleGroupingTreeHelper<MockModuleTreeNode>.moveAllModuleNodesAndCheckResult(expected: String) {
checkConsistency()
moveAllModuleNodesToProperGroups(root, model)
assertTreeEqual(expected)
checkConsistency()
}
private fun ModuleGroupingTreeHelper<MockModuleTreeNode>.createModuleNodes(vararg names: String): List<Pair<MockModuleTreeNode, Module>> {
val modules = createModules(*names)
val nodes = createModuleNodes(modules, root, model)
return nodes.map { Pair(it, (it as MockModuleNode).module)}
}
private fun assertTreeEqual(expected: String) {
TreeUtil.expandAll(tree)
PlatformTestUtil.assertTreeEqual(tree, expected.trimIndent() + "\n")
}
private fun createHelper(enableGrouping: Boolean = true): ModuleGroupingTreeHelper<MockModuleTreeNode> {
val grouper = getQualifiedNameModuleGrouper(myProject)
return ModuleGroupingTreeHelper.forEmptyTree(enableGrouping, grouper, ::MockModuleGroupNode, ::MockModuleNode, nodeComparator)
}
private fun createHelperFromTree(enableGrouping: Boolean = true): ModuleGroupingTreeHelper<MockModuleTreeNode> {
val grouper = getQualifiedNameModuleGrouper(myProject)
return ModuleGroupingTreeHelper.forTree(root, { it.moduleGroup }, { (it as? MockModuleNode)?.module },
enableGrouping, grouper, ::MockModuleGroupNode, ::MockModuleNode, nodeComparator)
}
private fun createModules(vararg names: String) = names.map { createModule(it) }
private fun ModuleGroupingTreeHelper<MockModuleTreeNode>.checkConsistency() {
val expectedNodeForGroup = HashMap<ModuleGroup, MockModuleTreeNode>(getNodeForGroupMap())
val expectedGroupByNode = HashMap<MockModuleTreeNode, ModuleGroup>(getGroupByNodeMap())
val expectedModuleByNode = HashMap<MockModuleTreeNode, Module>(getModuleByNodeMap())
TreeUtil.traverse(root, { o ->
val node = o as MockModuleTreeNode
if (node == root) return@traverse true
TestCase.assertSame(node, expectedNodeForGroup[node.moduleGroup])
expectedNodeForGroup.remove(node.moduleGroup)
TestCase.assertEquals(node.moduleGroup, expectedGroupByNode[node])
expectedGroupByNode.remove(node)
if (node is MockModuleNode) {
TestCase.assertEquals(node.module, expectedModuleByNode[node])
expectedModuleByNode.remove(node)
}
true
})
assertEmpty("Unexpected nodes in helper", expectedNodeForGroup.entries)
assertEmpty("Unexpected groups in helper", expectedGroupByNode.entries)
assertEmpty("Unexpected modules in helper", expectedModuleByNode.entries)
}
}
private val nodeComparator = Comparator.comparing { node: MockModuleTreeNode -> node.text }
private open class MockModuleTreeNode(userObject: Any, val text: String = userObject.toString()): DefaultMutableTreeNode(text) {
open val moduleGroup: ModuleGroup? = null
override fun toString() = text
}
private class MockModuleNode(val module: Module, grouper: ModuleGrouper): MockModuleTreeNode(module, module.name) {
override val moduleGroup = grouper.getModuleAsGroupPath(module)?.let(::ModuleGroup)
}
private class MockModuleGroupNode(override val moduleGroup: ModuleGroup): MockModuleTreeNode(moduleGroup)
@@ -332,16 +332,28 @@ public final class TreeUtil {
}
public static void sort(@NotNull final DefaultMutableTreeNode node, @Nullable Comparator comparator) {
final List<TreeNode> children = childrenToArray(node);
Collections.sort(children, comparator);
node.removeAllChildren();
addChildrenTo(node, children);
sortRecursively(node, comparator);
}
public static <T extends MutableTreeNode> void sortRecursively(@NotNull T node, @Nullable Comparator<? super T> comparator) {
sortChildren(node, comparator);
for (int i = 0; i < node.getChildCount(); i++) {
sort((DefaultMutableTreeNode) node.getChildAt(i), comparator);
//noinspection unchecked
sortRecursively((T) node.getChildAt(i), comparator);
}
}
public static void addChildrenTo(@NotNull final MutableTreeNode node, @NotNull final List<TreeNode> children) {
public static <T extends MutableTreeNode> void sortChildren(@NotNull T node, @Nullable Comparator<? super T> comparator) {
//noinspection unchecked
final List<T> children = (List)childrenToArray(node);
Collections.sort(children, comparator);
for (int i = node.getChildCount() - 1; i >= 0; i--) {
node.remove(i);
}
addChildrenTo(node, children);
}
public static void addChildrenTo(@NotNull final MutableTreeNode node, @NotNull final List<? extends TreeNode> children) {
for (final Object aChildren : children) {
final MutableTreeNode child = (MutableTreeNode)aChildren;
node.insert(child, node.getChildCount());
@@ -17,7 +17,6 @@ package com.intellij.module
import com.intellij.ide.projectView.impl.ModuleGroup
import com.intellij.openapi.module.ModuleGrouper
import com.intellij.openapi.util.registry.Registry
import com.intellij.testFramework.PlatformTestCase
import org.junit.Assert
import org.junit.Test
@@ -59,14 +58,5 @@ class GroupModulesByQualifiedNamesTest : PlatformTestCase() {
}
private val grouper: ModuleGrouper
get() {
val property = Registry.get("project.qualified.module.names")
return try {
property.setValue(true)
ModuleGrouper.instanceFor(myProject)
}
finally {
property.setValue(false)
}
}
get() = getQualifiedNameModuleGrouper(myProject)
}
@@ -43,6 +43,11 @@ abstract class ModuleGrouper {
abstract fun getGroupPathByModuleName(name: String): List<String>
/**
* If [module] itself can be considered as a group, returns its groups. Otherwise returns null.
*/
abstract fun getModuleAsGroupPath(module: Module): List<String>?
abstract fun getAllModules(): Array<Module>
companion object {
@@ -76,6 +81,8 @@ private class QualifiedNameGrouper(project: Project, model: ModifiableModuleMode
override fun getShortenedNameByFullModuleName(name: String) = StringUtil.getShortName(name)
override fun getGroupPathByModuleName(name: String) = name.split('.').dropLast(1)
override fun getModuleAsGroupPath(module: Module) = getModuleName(module).split('.')
}
private class ExplicitModuleGrouper(project: Project, model: ModifiableModuleModel?): ModuleGrouperBase(project, model) {
@@ -87,4 +94,6 @@ private class ExplicitModuleGrouper(project: Project, model: ModifiableModuleMod
override fun getShortenedNameByFullModuleName(name: String) = name
override fun getGroupPathByModuleName(name: String): List<String> = emptyList()
override fun getModuleAsGroupPath(module: Module) = null
}
@@ -0,0 +1,44 @@
/*
* Copyright 2000-2017 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.module
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleGrouper
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.registry.Registry
/**
* @author nik
*/
fun getQualifiedNameModuleGrouper(project: Project): ModuleGrouper {
val property = Registry.get("project.qualified.module.names")
return try {
property.setValue(true)
ModuleGrouper.instanceFor(project)
}
finally {
property.setValue(false)
}
}
fun renameModule(module: Module, newName: String) {
val model = ModuleManager.getInstance(module.project).modifiableModel
model.renameModule(module, newName)
runWriteAction { model.commit() }
}