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.
This commit is contained in:
nik
2017-11-17 12:46:37 +03:00
parent d2847285f8
commit c1dbfea842
7 changed files with 157 additions and 27 deletions
@@ -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<Module> modules = new ArrayList<>();
List<UnloadedModuleDescription> 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);
}
@@ -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<List<UnloadedModuleDescription>> UNLOADED_MODULES_CONTEXT_KEY = DataKey.create("context.unloaded.modules.list");
public static ProjectView getInstance(Project project) {
return ServiceManager.getService(project, ProjectView.class);
}
@@ -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<ModuleGroup> 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<UnloadedModuleDescription> getSelectedUnloadedModules() {
final AbstractProjectViewPane viewPane = getCurrentProjectViewPane();
if (viewPane == null) return Collections.emptyList();
List<UnloadedModuleDescription> 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 <T> List<T> getSelectedElements(@NotNull Class<T> klass) {
List<T> result = new ArrayList<>();
@@ -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<UnloadedModuleDescription> 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<ModuleDescription> moduleDescriptions = new ArrayList<>();
final Module[] modules = LangDataKeys.MODULE_CONTEXT_ARRAY.getData(dataContext);
if (modules != null) {
moduleDescriptions.addAll(ContainerUtil.map(modules, LoadedModuleDescriptionImpl::new));
}
List<UnloadedModuleDescription> 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<Module, ModifiableRootModel> otherModuleRootModels = new HashMap<>();
Set<String> moduleNamesToDelete = Arrays.stream(modules).map(Module::getName).collect(Collectors.toSet());
Set<String> 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);
}
}
@@ -194,4 +194,8 @@ public abstract class ModuleManager extends SimpleModificationTracker {
*/
@ApiStatus.Experimental
public abstract void setUnloadedModules(@NotNull List<String> unloadedModuleNames);
@ApiStatus.Experimental
public void removeUnloadedModules(@NotNull Collection<UnloadedModuleDescription> unloadedModules) {
}
}
@@ -57,4 +57,7 @@ class EmptyModuleManager(project: Project, messageBus: MessageBus) : ModuleManag
override fun getUnloadedModuleDescriptions() = emptyList<UnloadedModuleDescription>()
override fun getUnloadedModuleDescription(moduleName: String) = null
override fun removeUnloadedModules(unloadedModules: MutableCollection<UnloadedModuleDescription>) {
}
}
@@ -1068,6 +1068,15 @@ public abstract class ModuleManagerImpl extends ModuleManager implements Disposa
myModulePathsToLoad.clear();
}
@Override
public void removeUnloadedModules(@NotNull Collection<UnloadedModuleDescription> unloadedModules) {
ApplicationManager.getApplication().assertWriteAccessAllowed();
for (UnloadedModuleDescription module : unloadedModules) {
myUnloadedModules.remove(module.getName());
}
setUnloadedModuleNames(new ArrayList<>(myUnloadedModules.keySet()));
}
protected void setUnloadedModuleNames(@NotNull List<String> unloadedModuleNames) {
UnloadedModulesListStorage.getInstance(myProject).setUnloadedModuleNames(unloadedModuleNames);
}