From c1dbfea842655d1db2afa2c1162d2aeb131a3138 Mon Sep 17 00:00:00 2001 From: nik Date: Fri, 17 Nov 2017 12:46:07 +0300 Subject: [PATCH] project view: support deleting unloaded modules Previously when a user press 'Delete' on a content root from an unloaded module in 'Project View', IDE will suggest to delete a directory. It may be unexpected and even lead to data loss if the user don't read the message carefully (IDEA-182200). Now it'll suggest to remove the unloaded module from the project. --- .../actions/ModuleDeleteProviderTest.java | 81 ++++++++++++++++--- .../intellij/ide/projectView/ProjectView.java | 8 ++ .../ide/projectView/impl/ProjectViewImpl.java | 32 +++++++- .../actions/ModuleDeleteProvider.java | 47 +++++++---- .../openapi/module/ModuleManager.java | 4 + .../openapi/module/EmptyModuleManager.kt | 3 + .../module/impl/ModuleManagerImpl.java | 9 +++ 7 files changed, 157 insertions(+), 27 deletions(-) diff --git a/java/java-tests/testSrc/com/intellij/openapi/roots/ui/configuration/actions/ModuleDeleteProviderTest.java b/java/java-tests/testSrc/com/intellij/openapi/roots/ui/configuration/actions/ModuleDeleteProviderTest.java index 381fedbf915e..4dccc099bd23 100644 --- a/java/java-tests/testSrc/com/intellij/openapi/roots/ui/configuration/actions/ModuleDeleteProviderTest.java +++ b/java/java-tests/testSrc/com/intellij/openapi/roots/ui/configuration/actions/ModuleDeleteProviderTest.java @@ -1,10 +1,12 @@ // 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.roots.ui.configuration.actions; +import com.intellij.ide.projectView.ProjectView; import com.intellij.openapi.actionSystem.CommonDataKeys; import com.intellij.openapi.actionSystem.LangDataKeys; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleManager; +import com.intellij.openapi.module.UnloadedModuleDescription; import com.intellij.openapi.roots.ModuleRootManager; import com.intellij.openapi.roots.ModuleRootModificationUtil; import com.intellij.openapi.ui.Messages; @@ -12,6 +14,10 @@ import com.intellij.openapi.ui.TestDialog; import com.intellij.testFramework.MapDataContext; import com.intellij.testFramework.PlatformTestCase; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + /** * @author nik */ @@ -23,10 +29,10 @@ public class ModuleDeleteProviderTest extends PlatformTestCase { } public void testSimple() { - Module a = createModule("a"); - assertNotNull(ModuleManager.getInstance(myProject).findModuleByName("a")); - deleteModules(a); - assertNull(ModuleManager.getInstance(myProject).findModuleByName("a")); + createModule("a"); + assertNotNull(getModuleManager().findModuleByName("a")); + deleteModules("a"); + assertNull(getModuleManager().findModuleByName("a")); } public void testDeleteDependency() { @@ -34,7 +40,7 @@ public class ModuleDeleteProviderTest extends PlatformTestCase { Module b = createModule("b"); ModuleRootModificationUtil.addDependency(a, b); assertSameElements(ModuleRootManager.getInstance(a).getDependencyModuleNames(), "b"); - deleteModules(b); + deleteModules("b"); assertEmpty(ModuleRootManager.getInstance(a).getDependencyModuleNames()); } @@ -45,17 +51,72 @@ public class ModuleDeleteProviderTest extends PlatformTestCase { ModuleRootModificationUtil.addDependency(myModule, a); ModuleRootModificationUtil.addDependency(myModule, b); assertSameElements(ModuleRootManager.getInstance(myModule).getDependencyModuleNames(), "a", "b"); - deleteModules(a, b); - assertNull(ModuleManager.getInstance(myProject).findModuleByName("a")); - assertNull(ModuleManager.getInstance(myProject).findModuleByName("b")); + deleteModules("a", "b"); + assertNull(getModuleManager().findModuleByName("a")); + assertNull(getModuleManager().findModuleByName("b")); assertEmpty(ModuleRootManager.getInstance(myModule).getDependencyModuleNames()); } - private void deleteModules(Module... modules) { + public void testUnloaded() { + createModule("a"); + getModuleManager().setUnloadedModules(Arrays.asList("a")); + assertNotNull(getModuleManager().getUnloadedModuleDescription("a")); + deleteModules("a"); + assertNull(getModuleManager().getUnloadedModuleDescription("a")); + } + + public void testDeleteDependencyOnUnloadedModule() { + Module a = createModule("a"); + Module b = createModule("b"); + ModuleRootModificationUtil.addDependency(a, b); + getModuleManager().setUnloadedModules(Arrays.asList("b")); + assertSameElements(ModuleRootManager.getInstance(a).getDependencyModuleNames(), "b"); + deleteModules("b"); + assertEmpty(ModuleRootManager.getInstance(a).getDependencyModuleNames()); + } + + public void testLoadedAndUnloadedModule() { + Module a = createModule("a"); + Module b = createModule("b"); + ModuleRootModificationUtil.addDependency(a, b); + ModuleRootModificationUtil.addDependency(myModule, a); + ModuleRootModificationUtil.addDependency(myModule, b); + getModuleManager().setUnloadedModules(Arrays.asList("a")); + assertSameElements(ModuleRootManager.getInstance(myModule).getDependencyModuleNames(), "a", "b"); + deleteModules("a", "b"); + assertNull(getModuleManager().findModuleByName("a")); + assertNull(getModuleManager().findModuleByName("b")); + assertEmpty(getModuleManager().getUnloadedModuleDescriptions()); + assertEmpty(ModuleRootManager.getInstance(myModule).getDependencyModuleNames()); + } + + private ModuleManager getModuleManager() { + return ModuleManager.getInstance(myProject); + } + + private void deleteModules(String... names) { ModuleDeleteProvider provider = new ModuleDeleteProvider(); MapDataContext dataContext = new MapDataContext(); dataContext.put(CommonDataKeys.PROJECT, myProject); - dataContext.put(LangDataKeys.MODULE_CONTEXT_ARRAY, modules); + List modules = new ArrayList<>(); + List unloaded = new ArrayList<>(); + for (String name : names) { + UnloadedModuleDescription description = getModuleManager().getUnloadedModuleDescription(name); + if (description != null) { + unloaded.add(description); + } + else { + Module module = getModuleManager().findModuleByName(name); + assertNotNull("Module " + name + " not found", module); + modules.add(module); + } + } + if (!modules.isEmpty()) { + dataContext.put(LangDataKeys.MODULE_CONTEXT_ARRAY, modules.toArray(Module.EMPTY_ARRAY)); + } + if (!unloaded.isEmpty()) { + dataContext.put(ProjectView.UNLOADED_MODULES_CONTEXT_KEY, unloaded); + } assertTrue(provider.canDeleteElement(dataContext)); provider.deleteElement(dataContext); } diff --git a/platform/lang-impl/src/com/intellij/ide/projectView/ProjectView.java b/platform/lang-impl/src/com/intellij/ide/projectView/ProjectView.java index 80a69dac46c6..17230cdda58c 100644 --- a/platform/lang-impl/src/com/intellij/ide/projectView/ProjectView.java +++ b/platform/lang-impl/src/com/intellij/ide/projectView/ProjectView.java @@ -18,7 +18,9 @@ package com.intellij.ide.projectView; import com.intellij.ide.SelectInTarget; import com.intellij.ide.projectView.impl.AbstractProjectViewPane; +import com.intellij.openapi.actionSystem.DataKey; import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.module.UnloadedModuleDescription; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.ActionCallback; import com.intellij.openapi.vfs.VirtualFile; @@ -27,8 +29,14 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Collection; +import java.util.List; public abstract class ProjectView { + /** + * Use this key to get unloaded modules which content roots are selected in Project View + */ + public static final DataKey> UNLOADED_MODULES_CONTEXT_KEY = DataKey.create("context.unloaded.modules.list"); + public static ProjectView getInstance(Project project) { return ServiceManager.getService(project, ProjectView.class); } diff --git a/platform/lang-impl/src/com/intellij/ide/projectView/impl/ProjectViewImpl.java b/platform/lang-impl/src/com/intellij/ide/projectView/impl/ProjectViewImpl.java index 68de580e2ef9..14b84f572dd4 100644 --- a/platform/lang-impl/src/com/intellij/ide/projectView/impl/ProjectViewImpl.java +++ b/platform/lang-impl/src/com/intellij/ide/projectView/impl/ProjectViewImpl.java @@ -51,7 +51,9 @@ import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.fileEditor.TextEditor; import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx; import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.module.ModuleUtilCore; +import com.intellij.openapi.module.UnloadedModuleDescription; import com.intellij.openapi.project.DumbAware; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.*; @@ -87,6 +89,7 @@ import com.intellij.ui.tree.TreeVisitor; import com.intellij.util.ArrayUtil; import com.intellij.util.IJSwingUtilities; import com.intellij.util.PlatformIcons; +import com.intellij.util.SmartList; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.JBIterable; import com.intellij.util.messages.MessageBusConnection; @@ -1104,7 +1107,7 @@ public class ProjectViewImpl extends ProjectView implements PersistentStateCompo } if (PlatformDataKeys.DELETE_ELEMENT_PROVIDER.is(dataId)) { final Module[] modules = getSelectedModules(); - if (modules != null) { + if (modules != null || !getSelectedUnloadedModules().isEmpty()) { return myDeleteModuleProvider; } final LibraryOrderEntry orderEntry = getSelectedLibrary(); @@ -1152,6 +1155,9 @@ public class ProjectViewImpl extends ProjectView implements PersistentStateCompo if (LangDataKeys.MODULE_CONTEXT_ARRAY.is(dataId)) { return getSelectedModules(); } + if (UNLOADED_MODULES_CONTEXT_KEY.is(dataId)) { + return Collections.unmodifiableList(getSelectedUnloadedModules()); + } if (ModuleGroup.ARRAY_DATA_KEY.is(dataId)) { final List selectedElements = getSelectedElements(ModuleGroup.class); return selectedElements.isEmpty() ? null : selectedElements.toArray(new ModuleGroup[selectedElements.size()]); @@ -1260,6 +1266,21 @@ public class ProjectViewImpl extends ProjectView implements PersistentStateCompo return result.toArray(new Module[result.size()]); } } + + private List getSelectedUnloadedModules() { + final AbstractProjectViewPane viewPane = getCurrentProjectViewPane(); + if (viewPane == null) return Collections.emptyList(); + List result = new SmartList<>(); + for (Object element : viewPane.getSelectedElements()) { + if (element instanceof PsiDirectory) { + ContainerUtil.addIfNotNull(result, getUnloadedModuleByContentRoot(((PsiDirectory)element).getVirtualFile())); + } + else if (element instanceof VirtualFile) { + ContainerUtil.addIfNotNull(result, getUnloadedModuleByContentRoot((VirtualFile)element)); + } + } + return result; + } } /** Project view has the same node for module and its single content root @@ -1280,6 +1301,15 @@ public class ProjectViewImpl extends ProjectView implements PersistentStateCompo return null; } + @Nullable + private UnloadedModuleDescription getUnloadedModuleByContentRoot(@NotNull VirtualFile file) { + String moduleName = ProjectRootsUtil.findUnloadedModuleByContentRoot(file, myProject); + if (moduleName != null) { + return ModuleManager.getInstance(myProject).getUnloadedModuleDescription(moduleName); + } + return null; + } + @NotNull private List getSelectedElements(@NotNull Class klass) { List result = new ArrayList<>(); diff --git a/platform/lang-impl/src/com/intellij/openapi/roots/ui/configuration/actions/ModuleDeleteProvider.java b/platform/lang-impl/src/com/intellij/openapi/roots/ui/configuration/actions/ModuleDeleteProvider.java index c2d732cc70f5..0f69824e17de 100644 --- a/platform/lang-impl/src/com/intellij/openapi/roots/ui/configuration/actions/ModuleDeleteProvider.java +++ b/platform/lang-impl/src/com/intellij/openapi/roots/ui/configuration/actions/ModuleDeleteProvider.java @@ -18,14 +18,14 @@ package com.intellij.openapi.roots.ui.configuration.actions; import com.intellij.ide.DeleteProvider; import com.intellij.ide.TitledHandler; +import com.intellij.ide.projectView.ProjectView; import com.intellij.openapi.actionSystem.CommonDataKeys; import com.intellij.openapi.actionSystem.DataContext; import com.intellij.openapi.actionSystem.LangDataKeys; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.command.CommandProcessor; -import com.intellij.openapi.module.ModifiableModuleModel; -import com.intellij.openapi.module.Module; -import com.intellij.openapi.module.ModuleManager; +import com.intellij.openapi.module.*; +import com.intellij.openapi.module.impl.LoadedModuleDescriptionImpl; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectBundle; import com.intellij.openapi.roots.ModifiableRootModel; @@ -39,6 +39,7 @@ import com.intellij.project.ProjectKt; import com.intellij.projectImport.ProjectAttachProcessor; import com.intellij.util.PathUtilRt; import com.intellij.util.PlatformUtils; +import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; import java.util.*; @@ -48,7 +49,8 @@ public class ModuleDeleteProvider implements DeleteProvider, TitledHandler { @Override public boolean canDeleteElement(@NotNull DataContext dataContext) { final Module[] modules = LangDataKeys.MODULE_CONTEXT_ARRAY.getData(dataContext); - return modules != null && !containsPrimaryModule(modules); + List unloadedModules = ProjectView.UNLOADED_MODULES_CONTEXT_KEY.getData(dataContext); + return modules != null && !containsPrimaryModule(modules) || unloadedModules != null && !unloadedModules.isEmpty(); } private static boolean containsPrimaryModule(Module[] modules) { @@ -73,12 +75,21 @@ public class ModuleDeleteProvider implements DeleteProvider, TitledHandler { @Override public void deleteElement(@NotNull DataContext dataContext) { - final Module[] modules = LangDataKeys.MODULE_CONTEXT_ARRAY.getData(dataContext); - assert modules != null; final Project project = CommonDataKeys.PROJECT.getData(dataContext); assert project != null; - String names = StringUtil.join(Arrays.asList(modules), module -> "\'" + module.getName() + "\'", ", "); - int ret = Messages.showOkCancelDialog(getConfirmationText(modules, names), getActionTitle(), Messages.getQuestionIcon()); + + List moduleDescriptions = new ArrayList<>(); + final Module[] modules = LangDataKeys.MODULE_CONTEXT_ARRAY.getData(dataContext); + if (modules != null) { + moduleDescriptions.addAll(ContainerUtil.map(modules, LoadedModuleDescriptionImpl::new)); + } + List unloadedModules = ProjectView.UNLOADED_MODULES_CONTEXT_KEY.getData(dataContext); + if (unloadedModules != null) { + moduleDescriptions.addAll(unloadedModules); + } + + String names = StringUtil.join(moduleDescriptions, description -> "\'" + description.getName() + "\'", ", "); + int ret = Messages.showOkCancelDialog(getConfirmationText(names, moduleDescriptions.size()), getActionTitle(), Messages.getQuestionIcon()); if (ret != Messages.OK) return; CommandProcessor.getInstance().executeCommand(project, () -> { final Runnable action = () -> { @@ -86,28 +97,33 @@ public class ModuleDeleteProvider implements DeleteProvider, TitledHandler { final Module[] currentModules = moduleManager.getModules(); final ModifiableModuleModel modifiableModuleModel = moduleManager.getModifiableModel(); final Map otherModuleRootModels = new HashMap<>(); - Set moduleNamesToDelete = Arrays.stream(modules).map(Module::getName).collect(Collectors.toSet()); + Set moduleNamesToDelete = moduleDescriptions.stream().map(ModuleDescription::getName).collect(Collectors.toSet()); for (final Module otherModule : currentModules) { if (!moduleNamesToDelete.contains(otherModule.getName())) { otherModuleRootModels.put(otherModule, ModuleRootManager.getInstance(otherModule).getModifiableModel()); } } removeDependenciesOnModules(moduleNamesToDelete, otherModuleRootModels.values()); - for (final Module module : modules) { - modifiableModuleModel.disposeModule(module); + if (modules != null) { + for (final Module module : modules) { + modifiableModuleModel.disposeModule(module); + } } final ModifiableRootModel[] modifiableRootModels = otherModuleRootModels.values().toArray(new ModifiableRootModel[otherModuleRootModels.size()]); ModifiableModelCommitter.multiCommit(modifiableRootModels, modifiableModuleModel); + if (unloadedModules != null) { + moduleManager.removeUnloadedModules(unloadedModules); + } }; ApplicationManager.getApplication().runWriteAction(action); }, ProjectBundle.message("module.remove.command"), null); } - private static String getConfirmationText(Module[] modules, String names) { + private static String getConfirmationText(String names, int numberOfModules) { if (ProjectAttachProcessor.canAttachToProject()) { - return ProjectBundle.message("project.remove.confirmation.prompt", names, modules.length); + return ProjectBundle.message("project.remove.confirmation.prompt", names, numberOfModules); } - return ProjectBundle.message("module.remove.confirmation.prompt", names, modules.length); + return ProjectBundle.message("module.remove.confirmation.prompt", names, numberOfModules); } @Override @@ -127,8 +143,7 @@ public class ModuleDeleteProvider implements DeleteProvider, TitledHandler { for (final ModifiableRootModel modifiableRootModel : otherModuleRootModels) { final OrderEntry[] orderEntries = modifiableRootModel.getOrderEntries(); for (final OrderEntry orderEntry : orderEntries) { - if (orderEntry instanceof ModuleOrderEntry && orderEntry.isValid() && - moduleNamesToRemove.contains(((ModuleOrderEntry)orderEntry).getModuleName())) { + if (orderEntry instanceof ModuleOrderEntry && moduleNamesToRemove.contains(((ModuleOrderEntry)orderEntry).getModuleName())) { modifiableRootModel.removeOrderEntry(orderEntry); } } diff --git a/platform/projectModel-api/src/com/intellij/openapi/module/ModuleManager.java b/platform/projectModel-api/src/com/intellij/openapi/module/ModuleManager.java index 8b51b206c968..8a8028d91252 100644 --- a/platform/projectModel-api/src/com/intellij/openapi/module/ModuleManager.java +++ b/platform/projectModel-api/src/com/intellij/openapi/module/ModuleManager.java @@ -194,4 +194,8 @@ public abstract class ModuleManager extends SimpleModificationTracker { */ @ApiStatus.Experimental public abstract void setUnloadedModules(@NotNull List unloadedModuleNames); + + @ApiStatus.Experimental + public void removeUnloadedModules(@NotNull Collection unloadedModules) { + } } diff --git a/platform/projectModel-impl/src/com/intellij/openapi/module/EmptyModuleManager.kt b/platform/projectModel-impl/src/com/intellij/openapi/module/EmptyModuleManager.kt index a2e67b6bfc73..7b41113790a3 100644 --- a/platform/projectModel-impl/src/com/intellij/openapi/module/EmptyModuleManager.kt +++ b/platform/projectModel-impl/src/com/intellij/openapi/module/EmptyModuleManager.kt @@ -57,4 +57,7 @@ class EmptyModuleManager(project: Project, messageBus: MessageBus) : ModuleManag override fun getUnloadedModuleDescriptions() = emptyList() override fun getUnloadedModuleDescription(moduleName: String) = null + + override fun removeUnloadedModules(unloadedModules: MutableCollection) { + } } \ No newline at end of file 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 03f40655d69a..56f073da061d 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 @@ -1068,6 +1068,15 @@ public abstract class ModuleManagerImpl extends ModuleManager implements Disposa myModulePathsToLoad.clear(); } + @Override + public void removeUnloadedModules(@NotNull Collection unloadedModules) { + ApplicationManager.getApplication().assertWriteAccessAllowed(); + for (UnloadedModuleDescription module : unloadedModules) { + myUnloadedModules.remove(module.getName()); + } + setUnloadedModuleNames(new ArrayList<>(myUnloadedModules.keySet())); + } + protected void setUnloadedModuleNames(@NotNull List unloadedModuleNames) { UnloadedModulesListStorage.getInstance(myProject).setUnloadedModuleNames(unloadedModuleNames); }