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..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 @@ -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,13 @@ * 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.Iterator; +import java.util.LinkedHashMap; import java.util.List; import org.eclipse.core.resources.IFile; @@ -35,10 +37,15 @@ 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 +55,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 +80,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 +198,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 +238,86 @@ 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 = NestedProjects.below(projects, true); + 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; + } + /** * 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/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 1e6e940802c..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,16 @@ 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. + */ + 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..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,8 @@ 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 a5132dcef7d..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; @@ -335,6 +342,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 +576,8 @@ 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; 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..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,10 @@ public class IDEWorkspacePreferencePage extends PreferencePage implements IWorkb private ComboFieldEditor openReferencesEditor; + private ComboFieldEditor openNestedEditor; + + private ComboFieldEditor closeNestedEditor; + private StringFieldEditor systemExplorer; private ComboFieldEditor missingNatureSeverityCombo; @@ -144,6 +148,8 @@ 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); @@ -236,6 +242,38 @@ 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. + */ + 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 +600,8 @@ protected void performDefaults() { encodingEditor.loadDefault(); lineSeparatorEditor.loadDefault(); openReferencesEditor.loadDefault(); + openNestedEditor.loadDefault(); + closeNestedEditor.loadDefault(); missingNatureSeverityCombo.loadDefault(); missingEncodingSeverityCombo.loadDefault(); @@ -628,6 +668,8 @@ 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 f267fd94efb..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 @@ -294,6 +301,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 +553,8 @@ 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 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..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 @@ -67,6 +67,8 @@ SaveablesListTest.class, 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); + } +}