From ff991e4f5c11a482ab63fe7c67409fd2dc035367 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Tue, 1 Sep 2026 17:17:35 +0200 Subject: [PATCH 1/2] Ask before closing projects nested below the selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close Project closed exactly the selected projects, so a project that physically contains other projects left those open. The action now offers to close the nested projects too, in a dialog with "Close Selected Only" as the default, "Close Including Nested" and Cancel. A "Remember my decision" toggle stores the answer in a new workspace preference, shown next to the setting for opening referenced projects. Nothing is asked when no open project is nested below the selection. CloseUnrelatedProjectsAction opts out, it already confirms the projects it computes. Assisted-by: multiple AI agents and layers of automated tooling 🤖 --- .../ui/actions/CloseResourceAction.java | 137 +++++++++++++++++- .../actions/CloseUnrelatedProjectsAction.java | 12 +- .../internal/ide/IDEInternalPreferences.java | 5 + .../ide/IDEPreferenceInitializer.java | 1 + .../ui/internal/ide/IDEWorkbenchMessages.java | 8 + .../dialogs/IDEWorkspacePreferencePage.java | 21 +++ .../ui/internal/ide/messages.properties | 8 + .../internal/CloseResourceActionTest.java | 110 ++++++++++++++ .../ui/tests/internal/InternalTestSuite.java | 1 + 9 files changed, 301 insertions(+), 2 deletions(-) create mode 100644 tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/CloseResourceActionTest.java diff --git a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseResourceAction.java b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseResourceAction.java index fd6aee00f48..6a8b36f371e 100644 --- a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseResourceAction.java +++ b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseResourceAction.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2017 IBM Corporation and others. + * Copyright (c) 2000, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -12,11 +12,14 @@ * IBM Corporation - initial API and implementation * Andrey Loskutov - Bug 41431, 462760, 461786 * Lucas Bullen (Red Hat Inc.) - Bug 522096 - "Close Projects" on working set + * Lars Vogel - ask before closing nested projects *******************************************************************************/ package org.eclipse.ui.actions; import java.util.ArrayList; +import java.util.Collections; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import org.eclipse.core.resources.IFile; @@ -31,14 +34,20 @@ import org.eclipse.core.resources.mapping.ResourceChangeValidator; import org.eclipse.core.runtime.Adapters; import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.SafeRunner; import org.eclipse.core.runtime.jobs.ISchedulingRule; import org.eclipse.core.runtime.jobs.MultiRule; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.dialogs.MessageDialogWithToggle; +import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.util.SafeRunnable; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.window.IShellProvider; import org.eclipse.osgi.util.NLS; +import org.eclipse.swt.SWT; import org.eclipse.swt.custom.BusyIndicator; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IEditorInput; @@ -48,7 +57,9 @@ import org.eclipse.ui.PartInitException; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.ide.IDE; +import org.eclipse.ui.internal.ide.IDEInternalPreferences; import org.eclipse.ui.internal.ide.IDEWorkbenchMessages; +import org.eclipse.ui.internal.ide.IDEWorkbenchPlugin; import org.eclipse.ui.internal.ide.IIDEHelpContextIds; /** @@ -71,6 +82,9 @@ public class CloseResourceAction extends WorkspaceAction implements IResourceCha private String[] modelProviderIds; + /** Projects the last {@link #run()} decided to close, may be wider than the selection. */ + private List projectsToClose; + /** * Creates a new action. * @@ -186,6 +200,13 @@ public void run() { return; } + projects = promptForProjectsToClose(projects); + if (projects == null) { + // the user cancelled the operation + return; + } + projectsToClose = projects; + final IResource[] projectArray = projects.toArray(new IResource[projects.size()]); if (!IDE.saveAllEditors(projectArray, true)) { @@ -219,6 +240,120 @@ protected boolean shouldPerformResourcePruning() { return false; } + @Override + protected List getActionResources() { + if (projectsToClose != null) { + return projectsToClose; + } + return super.getActionResources(); + } + + /** + * Offers to close open projects nested below the selected ones, unless the + * preference already decides. + * + * @return the projects to close, or null if the user cancelled + */ + private List promptForProjectsToClose(List projects) { + if (!promptForRelatedProjects()) { + return projects; + } + List nestedProjects = computeNestedOpenProjects(projects); + if (nestedProjects.isEmpty()) { + return projects; + } + IPreferenceStore store = IDEWorkbenchPlugin.getDefault().getPreferenceStore(); + String key = IDEInternalPreferences.CLOSE_NESTED_PROJECTS; + String value = store.getString(key); + if (IDEInternalPreferences.PSPM_NEVER.equals(value)) { + return projects; + } + if (!IDEInternalPreferences.PSPM_ALWAYS.equals(value)) { + // the map fixes the button ids, so the toggle stores ALWAYS or NEVER + LinkedHashMap buttons = new LinkedHashMap<>(); + buttons.put(IDEWorkbenchMessages.CloseResourceAction_closeSelectedOnly, + Integer.valueOf(IDialogConstants.NO_ID)); + buttons.put(IDEWorkbenchMessages.CloseResourceAction_closeIncludingNested, + Integer.valueOf(IDialogConstants.YES_ID)); + buttons.put(IDialogConstants.CANCEL_LABEL, Integer.valueOf(IDialogConstants.CANCEL_ID)); + MessageDialogWithToggle dialog = MessageDialogWithToggle.open(MessageDialog.QUESTION, + getShell(), IDEWorkbenchMessages.CloseResourceAction_nestedTitle, + nestedMessage(projects, nestedProjects), null, false, store, key, SWT.SHEET, buttons); + switch (dialog.getReturnCode()) { + case IDialogConstants.YES_ID: + break; + case IDialogConstants.NO_ID: + return projects; + default: + return null; + } + } + List allProjects = new ArrayList<>(projects); + allProjects.addAll(nestedProjects); + return allProjects; + } + + /** + * @return the question asked when the selection nests further open projects + */ + private static String nestedMessage(List projects, List nestedProjects) { + boolean oneProject = projects.size() == 1; + if (nestedProjects.size() == 1) { + return oneProject + ? NLS.bind(IDEWorkbenchMessages.CloseResourceAction_closeOneNestedBelowProject, + projects.get(0).getName()) + : IDEWorkbenchMessages.CloseResourceAction_closeOneNestedBelowSelection; + } + Integer count = Integer.valueOf(nestedProjects.size()); + return oneProject + ? NLS.bind(IDEWorkbenchMessages.CloseResourceAction_closeNestedBelowProject, count, + projects.get(0).getName()) + : NLS.bind(IDEWorkbenchMessages.CloseResourceAction_closeNestedBelowSelection, count); + } + + /** + * @return true to ask about projects implied by the selection but + * not part of it. Subclasses computing and confirming the projects to + * close themselves answer false. + */ + boolean promptForRelatedProjects() { + return true; + } + + /** + * @param projects the projects the user selected + * @return the open projects located inside the given ones, but not among them + */ + private static List computeNestedOpenProjects(List projects) { + List locations = new ArrayList<>(projects.size()); + for (IResource project : projects) { + IPath location = project.getLocation(); + if (location != null) { + locations.add(location); + } + } + if (locations.isEmpty()) { + return Collections.emptyList(); + } + List nestedProjects = new ArrayList<>(); + for (IProject project : ResourcesPlugin.getWorkspace().getRoot().getProjects()) { + if (!project.isOpen() || projects.contains(project)) { + continue; + } + IPath location = project.getLocation(); + if (location == null) { + continue; + } + for (IPath selected : locations) { + if (!selected.equals(location) && selected.isPrefixOf(location)) { + nestedProjects.add(project); + break; + } + } + } + return nestedProjects; + } + /** * The CloseResourceAction implementation of this * SelectionListenerAction method ensures that this action is diff --git a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseUnrelatedProjectsAction.java b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseUnrelatedProjectsAction.java index 65f94ba9a21..5f6abf13f52 100644 --- a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseUnrelatedProjectsAction.java +++ b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseUnrelatedProjectsAction.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2006, 2017 IBM Corporation and others. + * Copyright (c) 2006, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -14,6 +14,7 @@ * Andrey Loskutov - generified interface, bug 462760 * Mickael Istria (Red Hat Inc.) - Bug 486901 * Lucas Bullen (Red Hat Inc.) - Bug 522096 - "Close Projects" on working set + * Lars Vogel - ask before closing nested projects *******************************************************************************/ package org.eclipse.ui.actions; @@ -113,6 +114,15 @@ public void run() { } } + /** + * The projects to close come from the project graph, not from the selection, + * and {@link #promptForConfirmation()} already confirms them. + */ + @Override + boolean promptForRelatedProjects() { + return false; + } + /** * Rebuilds the project graph before the projects to close are computed from it. * Enablement may answer from a stale graph, but closing projects should not. A diff --git a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEInternalPreferences.java b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEInternalPreferences.java index 1e6e940802c..30450234621 100644 --- a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEInternalPreferences.java +++ b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEInternalPreferences.java @@ -55,6 +55,11 @@ public interface IDEInternalPreferences { */ String CLOSE_UNRELATED_PROJECTS = "CLOSE_UNRELATED_PROJECTS"; //$NON-NLS-1$ + /** + * (String) Whether to close projects nested below a project that is closed. + */ + String CLOSE_NESTED_PROJECTS = "CLOSE_NESTED_PROJECTS"; //$NON-NLS-1$ + String PSPM_PROMPT = MessageDialogWithToggle.PROMPT; String PSPM_ALWAYS = MessageDialogWithToggle.ALWAYS; diff --git a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEPreferenceInitializer.java b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEPreferenceInitializer.java index 7978720f57e..808c77b0d95 100644 --- a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEPreferenceInitializer.java +++ b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEPreferenceInitializer.java @@ -58,6 +58,7 @@ public void initializeDefaultPreferences() { node.put(IDEInternalPreferences.OPEN_REQUIRED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); node.putBoolean(IDEInternalPreferences.CLOSE_UNRELATED_PROJECTS, false); + node.put(IDEInternalPreferences.CLOSE_NESTED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); node.putBoolean(IDEInternalPreferences.WARN_ABOUT_WORKSPACE_INCOMPATIBILITY, true); diff --git a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEWorkbenchMessages.java b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEWorkbenchMessages.java index a5132dcef7d..e2bcdd5a22c 100644 --- a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEWorkbenchMessages.java +++ b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEWorkbenchMessages.java @@ -335,6 +335,13 @@ public class IDEWorkbenchMessages extends NLS { public static String CloseResourceAction_problemMessage; public static String CloseResourceAction_operationMessage; public static String CloseResourceAction_operationMessage_plural; + public static String CloseResourceAction_nestedTitle; + public static String CloseResourceAction_closeOneNestedBelowProject; + public static String CloseResourceAction_closeNestedBelowProject; + public static String CloseResourceAction_closeOneNestedBelowSelection; + public static String CloseResourceAction_closeNestedBelowSelection; + public static String CloseResourceAction_closeIncludingNested; + public static String CloseResourceAction_closeSelectedOnly; public static String CloseUnrelatedProjectsAction_text; public static String CloseUnrelatedProjectsAction_text_plural; @@ -562,6 +569,7 @@ public class IDEWorkbenchMessages extends NLS { public static String IDEWorkspacePreference_otherLineDelim; public static String IDEWorkspacePreference_relatedLink; public static String IDEWorkspacePreference_openReferencedProjects; + public static String IDEWorkspacePreference_closeNestedProjects; public static String IDEWorkspacePreference_closeUnrelatedProjectsToolTip; public static String IDEWorkspacePreference_windowTitleGroupText; public static String IDEWorkspacePreference_showLocationInWindowTitle; diff --git a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/dialogs/IDEWorkspacePreferencePage.java b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/dialogs/IDEWorkspacePreferencePage.java index c37d677330d..18a681ed952 100644 --- a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/dialogs/IDEWorkspacePreferencePage.java +++ b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/dialogs/IDEWorkspacePreferencePage.java @@ -110,6 +110,8 @@ public class IDEWorkspacePreferencePage extends PreferencePage implements IWorkb private ComboFieldEditor openReferencesEditor; + private ComboFieldEditor closeNestedEditor; + private StringFieldEditor systemExplorer; private ComboFieldEditor missingNatureSeverityCombo; @@ -144,6 +146,7 @@ protected Control createContents(Composite parent) { Composite comboParent = new Composite(composite, SWT.NONE); comboParent.setLayout(new GridLayout(2, false)); createOpenPrefControls(comboParent); + createCloseNestedPrefControls(comboParent); createMissingNaturePref(comboParent); createMissingEncodingPref(comboParent); @@ -236,6 +239,22 @@ private void createOpenPrefControls(Composite parent) { openReferencesEditor.load(); } + /** + * Creates controls for the preference to close nested projects. + */ + private void createCloseNestedPrefControls(Composite parent) { + String name = IDEInternalPreferences.CLOSE_NESTED_PROJECTS; + String label = IDEWorkbenchMessages.IDEWorkspacePreference_closeNestedProjects; + String[][] namesAndValues = { + { Action.removeMnemonics(IDEWorkbenchMessages.Always), IDEInternalPreferences.PSPM_ALWAYS }, + { Action.removeMnemonics(IDEWorkbenchMessages.Never), IDEInternalPreferences.PSPM_NEVER }, + { Action.removeMnemonics(IDEWorkbenchMessages.Prompt), IDEInternalPreferences.PSPM_PROMPT } }; + closeNestedEditor = new ComboFieldEditorInGrid(name, label, namesAndValues, parent); + closeNestedEditor.setPreferenceStore(getIDEPreferenceStore()); + closeNestedEditor.setPage(this); + closeNestedEditor.load(); + } + /** * Creates controls for the preference to close unrelated projects. * @param parent The parent control @@ -562,6 +581,7 @@ protected void performDefaults() { encodingEditor.loadDefault(); lineSeparatorEditor.loadDefault(); openReferencesEditor.loadDefault(); + closeNestedEditor.loadDefault(); missingNatureSeverityCombo.loadDefault(); missingEncodingSeverityCombo.loadDefault(); @@ -628,6 +648,7 @@ public boolean performOk() { encodingEditor.store(); lineSeparatorEditor.store(); openReferencesEditor.store(); + closeNestedEditor.store(); missingNatureSeverityCombo.store(); missingEncodingSeverityCombo.store(); diff --git a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/messages.properties b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/messages.properties index f267fd94efb..309a88d16c5 100644 --- a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/messages.properties +++ b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/messages.properties @@ -294,6 +294,13 @@ CloseResourceAction_title = Close Problems CloseResourceAction_problemMessage = Problems occurred closing the selected resources. CloseResourceAction_operationMessage = Closing project... CloseResourceAction_operationMessage_plural = Closing projects... +CloseResourceAction_nestedTitle = Close Nested Projects +CloseResourceAction_closeOneNestedBelowProject = Also close 1 nested project below ''{0}''? +CloseResourceAction_closeNestedBelowProject = Also close {0} nested projects below ''{1}''? +CloseResourceAction_closeOneNestedBelowSelection = Also close 1 nested project below the selected projects? +CloseResourceAction_closeNestedBelowSelection = Also close {0} nested projects below the selected projects? +CloseResourceAction_closeIncludingNested = Close &Including Nested +CloseResourceAction_closeSelectedOnly = Close &Selected Only CloseUnrelatedProjectsAction_text = Close &Unrelated Project CloseUnrelatedProjectsAction_text_plural = Close &Unrelated Projects @@ -539,6 +546,7 @@ IDEWorkspacePreference_defaultLineDelimProj=Inh&erited from container ({0}) IDEWorkspacePreference_otherLineDelim= Ot&her: IDEWorkspacePreference_relatedLink = See {0} for workspace startup and shutdown preferences. IDEWorkspacePreference_openReferencedProjects = Open referenced projects when a project is opened: +IDEWorkspacePreference_closeNestedProjects = Close nested projects when a project is closed: IDEWorkspacePreference_closeUnrelatedProjectsToolTip = Close unrelated projects without prompt IDEWorkspacePreference_windowTitleGroupText=Window title IDEWorkspacePreference_showLocationInWindowTitle=Show &full workspace path: diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/CloseResourceActionTest.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/CloseResourceActionTest.java new file mode 100644 index 00000000000..3dd1e3cdc07 --- /dev/null +++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/CloseResourceActionTest.java @@ -0,0 +1,110 @@ +/******************************************************************************* + * Copyright (c) 2026 Lars Vogel and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Lars Vogel - initial API and implementation + *******************************************************************************/ +package org.eclipse.ui.tests.internal; + +import static org.eclipse.ui.tests.harness.util.UITestUtil.waitForJobs; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.actions.CloseResourceAction; +import org.eclipse.ui.internal.ide.IDEInternalPreferences; +import org.eclipse.ui.internal.ide.IDEWorkbenchPlugin; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Closing a project whose location contains further open projects. + */ +public class CloseResourceActionTest extends ResourceActionTest { + + private IProject parent; + private IProject nested; + private IPreferenceStore store; + private String oldPreference; + + @Before + public void createProjects() throws CoreException { + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + parent = workspace.getRoot().getProject("CloseResourceActionTest_parent"); + parent.create(null); + parent.open(null); + nested = workspace.getRoot().getProject("CloseResourceActionTest_nested"); + IProjectDescription description = workspace.newProjectDescription(nested.getName()); + description.setLocation(parent.getLocation().append(nested.getName())); + nested.create(description, null); + nested.open(null); + store = IDEWorkbenchPlugin.getDefault().getPreferenceStore(); + oldPreference = store.getString(IDEInternalPreferences.CLOSE_NESTED_PROJECTS); + } + + @After + public void deleteProjects() throws CoreException { + store.setValue(IDEInternalPreferences.CLOSE_NESTED_PROJECTS, oldPreference); + nested.delete(true, null); + parent.delete(true, null); + waitForJobs(0, 30_000); + } + + @Test + public void testClosesOnlySelectedProjectByPreference() { + store.setValue(IDEInternalPreferences.CLOSE_NESTED_PROJECTS, IDEInternalPreferences.PSPM_NEVER); + close(parent); + assertFalse(parent.isOpen()); + assertTrue(nested.isOpen()); + } + + @Test + public void testClosesNestedProjectsByPreference() { + store.setValue(IDEInternalPreferences.CLOSE_NESTED_PROJECTS, IDEInternalPreferences.PSPM_ALWAYS); + close(parent); + assertFalse(parent.isOpen()); + assertFalse(nested.isOpen()); + } + + @Test + public void testDoesNotPromptWhenNestedProjectIsSelected() { + store.setValue(IDEInternalPreferences.CLOSE_NESTED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); + close(parent, nested); + assertFalse(parent.isOpen()); + assertFalse(nested.isOpen()); + } + + @Test + public void testDoesNotPromptWithoutNestedProjects() { + store.setValue(IDEInternalPreferences.CLOSE_NESTED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); + close(nested); + assertTrue(parent.isOpen()); + assertFalse(nested.isOpen()); + } + + private static void close(IProject... projects) { + Shell activeShell = Display.getCurrent().getActiveShell(); + CloseResourceAction action = new CloseResourceAction(() -> activeShell); + action.selectionChanged(new StructuredSelection(projects)); + assertTrue(action.isEnabled()); + action.run(); + processUIEvents(); + waitForJobs(0, 30_000); + } +} diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/InternalTestSuite.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/InternalTestSuite.java index ff24811f308..9143cef72ba 100644 --- a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/InternalTestSuite.java +++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/InternalTestSuite.java @@ -67,6 +67,7 @@ SaveablesListTest.class, Bug540297WorkbenchPageFindViewTest.class, Bug549139Test.class, + CloseResourceActionTest.class, LargeFileLimitsPreferenceHandlerTest.class, WorkbookEditorsHandlerTest.class, ScopeAreaTest.class, From 7d6838e7ae3765ed2d74de660411109f0cb7830e Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Thu, 3 Sep 2026 00:32:52 +0200 Subject: [PATCH 2/2] Ask before opening projects nested below the selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close Project can take nested modules along, but Open Project brought back only the root, so the modules stayed closed. The action now offers to open them too, with Open Selected Only as the default, and a Remember my decision toggle backed by a new workspace preference next to the one for closing. The location matching moves into a helper shared by both actions. Assisted-by: multiple AI agents and layers of automated tooling 🤖 --- .../ui/actions/CloseResourceAction.java | 38 +----- .../eclipse/ui/actions/NestedProjects.java | 69 +++++++++++ .../ui/actions/OpenResourceAction.java | 92 ++++++++++++-- .../internal/ide/IDEInternalPreferences.java | 5 + .../ide/IDEPreferenceInitializer.java | 1 + .../ui/internal/ide/IDEWorkbenchMessages.java | 8 ++ .../dialogs/IDEWorkspacePreferencePage.java | 21 ++++ .../ui/internal/ide/messages.properties | 8 ++ .../ui/tests/internal/InternalTestSuite.java | 1 + .../internal/OpenResourceActionTest.java | 112 ++++++++++++++++++ 10 files changed, 305 insertions(+), 50 deletions(-) create mode 100644 bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/NestedProjects.java create mode 100644 tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/OpenResourceActionTest.java diff --git a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseResourceAction.java b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseResourceAction.java index 6a8b36f371e..6ae4b66f2ef 100644 --- a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseResourceAction.java +++ b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseResourceAction.java @@ -17,7 +17,6 @@ package org.eclipse.ui.actions; import java.util.ArrayList; -import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; @@ -34,7 +33,6 @@ import org.eclipse.core.resources.mapping.ResourceChangeValidator; import org.eclipse.core.runtime.Adapters; import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.SafeRunner; import org.eclipse.core.runtime.jobs.ISchedulingRule; @@ -258,7 +256,7 @@ private List promptForProjectsToClose(List nestedProjects = computeNestedOpenProjects(projects); + List nestedProjects = NestedProjects.below(projects, true); if (nestedProjects.isEmpty()) { return projects; } @@ -320,40 +318,6 @@ boolean promptForRelatedProjects() { return true; } - /** - * @param projects the projects the user selected - * @return the open projects located inside the given ones, but not among them - */ - private static List computeNestedOpenProjects(List projects) { - List locations = new ArrayList<>(projects.size()); - for (IResource project : projects) { - IPath location = project.getLocation(); - if (location != null) { - locations.add(location); - } - } - if (locations.isEmpty()) { - return Collections.emptyList(); - } - List nestedProjects = new ArrayList<>(); - for (IProject project : ResourcesPlugin.getWorkspace().getRoot().getProjects()) { - if (!project.isOpen() || projects.contains(project)) { - continue; - } - IPath location = project.getLocation(); - if (location == null) { - continue; - } - for (IPath selected : locations) { - if (!selected.equals(location) && selected.isPrefixOf(location)) { - nestedProjects.add(project); - break; - } - } - } - return nestedProjects; - } - /** * The CloseResourceAction implementation of this * SelectionListenerAction method ensures that this action is diff --git a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/NestedProjects.java b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/NestedProjects.java new file mode 100644 index 00000000000..b881dbd3387 --- /dev/null +++ b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/NestedProjects.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * Copyright (c) 2026 Lars Vogel and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Lars Vogel - initial API and implementation + *******************************************************************************/ +package org.eclipse.ui.actions; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.IPath; + +/** + * Finds projects located inside other projects, such as Maven or Gradle + * modules below their root. + */ +final class NestedProjects { + + private NestedProjects() { + } + + /** + * Returns the open or closed projects located inside the given ones but not + * among them, parents before their children. + */ + static List below(List projects, boolean open) { + List locations = new ArrayList<>(projects.size()); + for (IResource project : projects) { + IPath location = project.getLocation(); + if (location != null) { + locations.add(location); + } + } + if (locations.isEmpty()) { + return Collections.emptyList(); + } + List nestedProjects = new ArrayList<>(); + for (IProject project : ResourcesPlugin.getWorkspace().getRoot().getProjects()) { + if (project.isOpen() != open || projects.contains(project)) { + continue; + } + IPath location = project.getLocation(); + if (location == null) { + continue; + } + for (IPath selected : locations) { + if (!selected.equals(location) && selected.isPrefixOf(location)) { + nestedProjects.add(project); + break; + } + } + } + nestedProjects.sort(Comparator.comparingInt(project -> project.getLocation().segmentCount())); + return nestedProjects; + } +} diff --git a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/OpenResourceAction.java b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/OpenResourceAction.java index dd0a8693885..21f4ca26476 100644 --- a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/OpenResourceAction.java +++ b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/OpenResourceAction.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2015 IBM Corporation and others. + * Copyright (c) 2000, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -12,10 +12,12 @@ * IBM Corporation - initial API and implementation * Mohamed Tarief , IBM - Bug 139211 * Lucas Bullen (Red Hat Inc.) - Bug 522096 - "Close Projects" on working set + * Lars Vogel - ask before opening nested projects *******************************************************************************/ package org.eclipse.ui.actions; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; import org.eclipse.core.resources.IProject; @@ -34,11 +36,13 @@ import org.eclipse.core.runtime.SubMonitor; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.dialogs.MessageDialogWithToggle; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.window.IShellProvider; import org.eclipse.jface.window.Window; +import org.eclipse.osgi.util.NLS; import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; @@ -134,19 +138,16 @@ protected String getProblemsTitle() { } /** - * Returns whether there are closed projects in the workspace that are - * not part of the current selection. + * Returns whether there are closed projects in the workspace that are not + * among the given ones. */ - private boolean hasOtherClosedProjects() { - //count the closed projects in the selection + private boolean hasOtherClosedProjects(List projects) { int closedInSelection = 0; - for (IResource project : getSelectedResources()) { + for (IResource project : projects) { if (!((IProject) project).isOpen()) { closedInSelection++; } } - //there are other closed projects if the selection does - //not contain all closed projects in the workspace return closedInSelection < countClosedProjects(); } @@ -209,8 +210,13 @@ public void resourceChanged(IResourceChangeEvent event) { @Override public void run() { + List projects = promptForProjectsToOpen(getActionResources()); + if (projects == null) { + // the user cancelled the operation + return; + } try { - runOpenWithReferences(); + runOpenWithReferences(projects); } catch (OperationCanceledException e) { //just return when canceled } @@ -225,10 +231,70 @@ private static void logOpenFailure(IProject project, CoreException e) { } /** - * Opens the selected projects, and all related projects, in the background. + * Offers to open closed projects nested below the selected ones, unless the + * preference already decides. + * + * @return the projects to open, or null if the user cancelled + */ + private List promptForProjectsToOpen(List projects) { + List nestedProjects = NestedProjects.below(projects, false); + if (nestedProjects.isEmpty()) { + return projects; + } + IPreferenceStore store = IDEWorkbenchPlugin.getDefault().getPreferenceStore(); + String key = IDEInternalPreferences.OPEN_NESTED_PROJECTS; + String value = store.getString(key); + if (IDEInternalPreferences.PSPM_NEVER.equals(value)) { + return projects; + } + if (!IDEInternalPreferences.PSPM_ALWAYS.equals(value)) { + // the map fixes the button ids, so the toggle stores ALWAYS or NEVER + LinkedHashMap buttons = new LinkedHashMap<>(); + buttons.put(IDEWorkbenchMessages.OpenResourceAction_openSelectedOnly, + Integer.valueOf(IDialogConstants.NO_ID)); + buttons.put(IDEWorkbenchMessages.OpenResourceAction_openIncludingNested, + Integer.valueOf(IDialogConstants.YES_ID)); + buttons.put(IDialogConstants.CANCEL_LABEL, Integer.valueOf(IDialogConstants.CANCEL_ID)); + MessageDialogWithToggle dialog = MessageDialogWithToggle.open(MessageDialog.QUESTION, getShell(), + IDEWorkbenchMessages.OpenResourceAction_nestedTitle, nestedMessage(projects, nestedProjects), + null, false, store, key, SWT.SHEET, buttons); + switch (dialog.getReturnCode()) { + case IDialogConstants.YES_ID: + break; + case IDialogConstants.NO_ID: + return projects; + default: + return null; + } + } + List allProjects = new ArrayList<>(projects); + allProjects.addAll(nestedProjects); + return allProjects; + } + + /** + * @return the question asked when the selection nests further closed projects + */ + private static String nestedMessage(List projects, List nestedProjects) { + boolean oneProject = projects.size() == 1; + if (nestedProjects.size() == 1) { + return oneProject + ? NLS.bind(IDEWorkbenchMessages.OpenResourceAction_openOneNestedBelowProject, + projects.get(0).getName()) + : IDEWorkbenchMessages.OpenResourceAction_openOneNestedBelowSelection; + } + Integer count = Integer.valueOf(nestedProjects.size()); + return oneProject + ? NLS.bind(IDEWorkbenchMessages.OpenResourceAction_openNestedBelowProject, count, + projects.get(0).getName()) + : NLS.bind(IDEWorkbenchMessages.OpenResourceAction_openNestedBelowSelection, count); + } + + /** + * Opens the given projects, and all related projects, in the background. */ - private void runOpenWithReferences() { - final List resources = new ArrayList<>(getActionResources()); + private void runOpenWithReferences(List projects) { + final List resources = new ArrayList<>(projects); Job job = new WorkspaceJob(removeMnemonics(getText())) { private boolean openProjectReferences = true; private boolean hasPrompted = false; @@ -256,7 +322,7 @@ private void doOpenWithReferences(IProject project, IProgressMonitor mon) throws break; } } - if (openProjectReferences && hasOtherClosedProjects()) { + if (openProjectReferences && hasOtherClosedProjects(resources)) { Display.getDefault().syncExec(() -> { try { openProjectReferences = promptToOpenWithReferences(); diff --git a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEInternalPreferences.java b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEInternalPreferences.java index 30450234621..d2f33bc2f1f 100644 --- a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEInternalPreferences.java +++ b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEInternalPreferences.java @@ -55,6 +55,11 @@ public interface IDEInternalPreferences { */ String CLOSE_UNRELATED_PROJECTS = "CLOSE_UNRELATED_PROJECTS"; //$NON-NLS-1$ + /** + * (String) Whether to open projects nested below a project that is opened. + */ + String OPEN_NESTED_PROJECTS = "OPEN_NESTED_PROJECTS"; //$NON-NLS-1$ + /** * (String) Whether to close projects nested below a project that is closed. */ diff --git a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEPreferenceInitializer.java b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEPreferenceInitializer.java index 808c77b0d95..201ae2f3067 100644 --- a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEPreferenceInitializer.java +++ b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEPreferenceInitializer.java @@ -58,6 +58,7 @@ public void initializeDefaultPreferences() { node.put(IDEInternalPreferences.OPEN_REQUIRED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); node.putBoolean(IDEInternalPreferences.CLOSE_UNRELATED_PROJECTS, false); + node.put(IDEInternalPreferences.OPEN_NESTED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); node.put(IDEInternalPreferences.CLOSE_NESTED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); node.putBoolean(IDEInternalPreferences.WARN_ABOUT_WORKSPACE_INCOMPATIBILITY, true); diff --git a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEWorkbenchMessages.java b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEWorkbenchMessages.java index e2bcdd5a22c..dd7a3a7240d 100644 --- a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEWorkbenchMessages.java +++ b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEWorkbenchMessages.java @@ -323,6 +323,13 @@ public class IDEWorkbenchMessages extends NLS { public static String OpenResourceAction_operationMessage; public static String OpenResourceAction_operationMessage_plural; public static String OpenResourceAction_openRequiredProjects; + public static String OpenResourceAction_nestedTitle; + public static String OpenResourceAction_openOneNestedBelowProject; + public static String OpenResourceAction_openNestedBelowProject; + public static String OpenResourceAction_openOneNestedBelowSelection; + public static String OpenResourceAction_openNestedBelowSelection; + public static String OpenResourceAction_openIncludingNested; + public static String OpenResourceAction_openSelectedOnly; public static String CloseResourceAction_text; public static String CloseResourceAction_text_plural; @@ -569,6 +576,7 @@ public class IDEWorkbenchMessages extends NLS { public static String IDEWorkspacePreference_otherLineDelim; public static String IDEWorkspacePreference_relatedLink; public static String IDEWorkspacePreference_openReferencedProjects; + public static String IDEWorkspacePreference_openNestedProjects; public static String IDEWorkspacePreference_closeNestedProjects; public static String IDEWorkspacePreference_closeUnrelatedProjectsToolTip; public static String IDEWorkspacePreference_windowTitleGroupText; diff --git a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/dialogs/IDEWorkspacePreferencePage.java b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/dialogs/IDEWorkspacePreferencePage.java index 18a681ed952..4cdd557ac1d 100644 --- a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/dialogs/IDEWorkspacePreferencePage.java +++ b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/dialogs/IDEWorkspacePreferencePage.java @@ -110,6 +110,8 @@ public class IDEWorkspacePreferencePage extends PreferencePage implements IWorkb private ComboFieldEditor openReferencesEditor; + private ComboFieldEditor openNestedEditor; + private ComboFieldEditor closeNestedEditor; private StringFieldEditor systemExplorer; @@ -146,6 +148,7 @@ protected Control createContents(Composite parent) { Composite comboParent = new Composite(composite, SWT.NONE); comboParent.setLayout(new GridLayout(2, false)); createOpenPrefControls(comboParent); + createOpenNestedPrefControls(comboParent); createCloseNestedPrefControls(comboParent); createMissingNaturePref(comboParent); createMissingEncodingPref(comboParent); @@ -239,6 +242,22 @@ private void createOpenPrefControls(Composite parent) { openReferencesEditor.load(); } + /** + * Creates controls for the preference to open nested projects. + */ + private void createOpenNestedPrefControls(Composite parent) { + String name = IDEInternalPreferences.OPEN_NESTED_PROJECTS; + String label = IDEWorkbenchMessages.IDEWorkspacePreference_openNestedProjects; + String[][] namesAndValues = { + { Action.removeMnemonics(IDEWorkbenchMessages.Always), IDEInternalPreferences.PSPM_ALWAYS }, + { Action.removeMnemonics(IDEWorkbenchMessages.Never), IDEInternalPreferences.PSPM_NEVER }, + { Action.removeMnemonics(IDEWorkbenchMessages.Prompt), IDEInternalPreferences.PSPM_PROMPT } }; + openNestedEditor = new ComboFieldEditorInGrid(name, label, namesAndValues, parent); + openNestedEditor.setPreferenceStore(getIDEPreferenceStore()); + openNestedEditor.setPage(this); + openNestedEditor.load(); + } + /** * Creates controls for the preference to close nested projects. */ @@ -581,6 +600,7 @@ protected void performDefaults() { encodingEditor.loadDefault(); lineSeparatorEditor.loadDefault(); openReferencesEditor.loadDefault(); + openNestedEditor.loadDefault(); closeNestedEditor.loadDefault(); missingNatureSeverityCombo.loadDefault(); missingEncodingSeverityCombo.loadDefault(); @@ -648,6 +668,7 @@ public boolean performOk() { encodingEditor.store(); lineSeparatorEditor.store(); openReferencesEditor.store(); + openNestedEditor.store(); closeNestedEditor.store(); missingNatureSeverityCombo.store(); missingEncodingSeverityCombo.store(); diff --git a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/messages.properties b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/messages.properties index 309a88d16c5..e6456f65ab9 100644 --- a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/messages.properties +++ b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/messages.properties @@ -282,6 +282,13 @@ OpenResourceAction_problemMessage = Problems occurred opening the selected resou OpenResourceAction_operationMessage = Opening project... OpenResourceAction_operationMessage_plural = Opening projects... OpenResourceAction_openRequiredProjects = Should referenced projects also be opened where applicable? +OpenResourceAction_nestedTitle = Open Nested Projects +OpenResourceAction_openOneNestedBelowProject = Also open 1 nested project below ''{0}''? +OpenResourceAction_openNestedBelowProject = Also open {0} nested projects below ''{1}''? +OpenResourceAction_openOneNestedBelowSelection = Also open 1 nested project below the selected projects? +OpenResourceAction_openNestedBelowSelection = Also open {0} nested projects below the selected projects? +OpenResourceAction_openIncludingNested = Open &Including Nested +OpenResourceAction_openSelectedOnly = Open &Selected Only CloseResourceAction_text = Clo&se Project CloseResourceAction_text_plural = Clo&se Projects @@ -546,6 +553,7 @@ IDEWorkspacePreference_defaultLineDelimProj=Inh&erited from container ({0}) IDEWorkspacePreference_otherLineDelim= Ot&her: IDEWorkspacePreference_relatedLink = See {0} for workspace startup and shutdown preferences. IDEWorkspacePreference_openReferencedProjects = Open referenced projects when a project is opened: +IDEWorkspacePreference_openNestedProjects = Open nested projects when a project is opened: IDEWorkspacePreference_closeNestedProjects = Close nested projects when a project is closed: IDEWorkspacePreference_closeUnrelatedProjectsToolTip = Close unrelated projects without prompt IDEWorkspacePreference_windowTitleGroupText=Window title diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/InternalTestSuite.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/InternalTestSuite.java index 9143cef72ba..1cf77ef6180 100644 --- a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/InternalTestSuite.java +++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/InternalTestSuite.java @@ -68,6 +68,7 @@ Bug540297WorkbenchPageFindViewTest.class, Bug549139Test.class, CloseResourceActionTest.class, + OpenResourceActionTest.class, LargeFileLimitsPreferenceHandlerTest.class, WorkbookEditorsHandlerTest.class, ScopeAreaTest.class, diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/OpenResourceActionTest.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/OpenResourceActionTest.java new file mode 100644 index 00000000000..70260f7665a --- /dev/null +++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/OpenResourceActionTest.java @@ -0,0 +1,112 @@ +/******************************************************************************* + * Copyright (c) 2026 Lars Vogel and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Lars Vogel - initial API and implementation + *******************************************************************************/ +package org.eclipse.ui.tests.internal; + +import static org.eclipse.ui.tests.harness.util.UITestUtil.waitForJobs; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.actions.OpenResourceAction; +import org.eclipse.ui.internal.ide.IDEInternalPreferences; +import org.eclipse.ui.internal.ide.IDEWorkbenchPlugin; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Opening a project whose location contains further closed projects. + */ +public class OpenResourceActionTest extends ResourceActionTest { + + private IProject parent; + private IProject nested; + private IPreferenceStore store; + private String oldPreference; + + @Before + public void createProjects() throws CoreException { + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + parent = workspace.getRoot().getProject("OpenResourceActionTest_parent"); + parent.create(null); + parent.open(null); + nested = workspace.getRoot().getProject("OpenResourceActionTest_nested"); + IProjectDescription description = workspace.newProjectDescription(nested.getName()); + description.setLocation(parent.getLocation().append(nested.getName())); + nested.create(description, null); + nested.open(null); + nested.close(null); + parent.close(null); + store = IDEWorkbenchPlugin.getDefault().getPreferenceStore(); + oldPreference = store.getString(IDEInternalPreferences.OPEN_NESTED_PROJECTS); + } + + @After + public void deleteProjects() throws CoreException { + store.setValue(IDEInternalPreferences.OPEN_NESTED_PROJECTS, oldPreference); + nested.delete(true, null); + parent.delete(true, null); + waitForJobs(0, 30_000); + } + + @Test + public void testOpensOnlySelectedProjectByPreference() { + store.setValue(IDEInternalPreferences.OPEN_NESTED_PROJECTS, IDEInternalPreferences.PSPM_NEVER); + open(parent); + assertTrue(parent.isOpen()); + assertFalse(nested.isOpen()); + } + + @Test + public void testOpensNestedProjectsByPreference() { + store.setValue(IDEInternalPreferences.OPEN_NESTED_PROJECTS, IDEInternalPreferences.PSPM_ALWAYS); + open(parent); + assertTrue(parent.isOpen()); + assertTrue(nested.isOpen()); + } + + @Test + public void testDoesNotPromptWhenNestedProjectIsSelected() { + store.setValue(IDEInternalPreferences.OPEN_NESTED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); + open(parent, nested); + assertTrue(parent.isOpen()); + assertTrue(nested.isOpen()); + } + + @Test + public void testDoesNotPromptWithoutNestedProjects() { + store.setValue(IDEInternalPreferences.OPEN_NESTED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); + open(nested); + assertFalse(parent.isOpen()); + assertTrue(nested.isOpen()); + } + + private static void open(IProject... projects) { + Shell activeShell = Display.getCurrent().getActiveShell(); + OpenResourceAction action = new OpenResourceAction(() -> activeShell); + action.selectionChanged(new StructuredSelection(projects)); + assertTrue(action.isEnabled()); + action.run(); + processUIEvents(); + waitForJobs(0, 30_000); + } +}