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..25748b2f443 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 @@ -16,7 +16,6 @@ package org.eclipse.ui.actions; import java.util.ArrayList; -import java.util.Iterator; import java.util.List; import org.eclipse.core.resources.IFile; @@ -179,8 +178,9 @@ protected void invokeOperation(IResource resource, IProgressMonitor monitor) thr */ @Override public void run() { - // Get the items to close. - List projects = getSelectedResources(); + // Get the items to close (only projects: a mixed selection, e.g. Ctrl+A, + // may also contain files or non-resource elements). + List projects = getActionResources(); if (projects == null || projects.isEmpty()) { // no action needs to be taken since no projects are selected return; @@ -229,14 +229,14 @@ protected boolean updateSelection(IStructuredSelection s) { // don't call super since we want to enable if open project is selected. setText(defaultText); setToolTipText(defaultToolTip); - if (!selectionIsOfType(IResource.PROJECT)) { + List projects = getSelectedResources().stream() + .filter(IProject.class::isInstance).map(IProject.class::cast).toList(); + if (projects.isEmpty()) { return false; } boolean hasOpenProjects = false; - Iterator resources = getSelectedResources().iterator(); - while (resources.hasNext()) { - IProject currentResource = (IProject) resources.next(); + for (IProject currentResource : projects) { if (currentResource.isOpen()) { if (hasOpenProjects) { setText(pluralText); @@ -258,7 +258,7 @@ public synchronized void resourceChanged(IResourceChangeEvent event) { // Warning: code duplicated in OpenResourceAction List sel = getSelectedResources(); // don't bother looking at delta if selection not applicable - if (selectionIsOfType(IResource.PROJECT)) { + if (sel.stream().anyMatch(IProject.class::isInstance)) { IResourceDelta delta = event.getDelta(); if (delta != null) { IResourceDelta[] projDeltas = delta.getAffectedChildren(IResourceDelta.CHANGED); @@ -280,6 +280,13 @@ protected synchronized List getSelectedResources() { return super.getSelectedResources(); } + @Override + protected List getActionResources() { + // The close operation only ever applies to projects; drop any non-project + // elements of a mixed selection so execution does not fail with a cast. + return super.getActionResources().stream().filter(IProject.class::isInstance).toList(); + } + @Override protected synchronized List getSelectedNonResources() { return super.getSelectedNonResources(); 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 2e0901417b5..de537b3d86d 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 @@ -241,7 +241,8 @@ protected List getSelectedResources() { @Override public void resourceChanged(IResourceChangeEvent event) { // don't bother looking at delta if selection not applicable - if (selectionIsOfType(IResource.PROJECT)) { + List selectedResources = super.getSelectedResources(); + if (selectedResources.stream().anyMatch(IProject.class::isInstance)) { IResourceDelta delta = event.getDelta(); if (delta != null) { IResourceDelta[] projDeltas = delta.getAffectedChildren(IResourceDelta.CHANGED); 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 cbe3618076f..252a4cede64 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 @@ -139,7 +139,7 @@ protected String getProblemsTitle() { private boolean hasOtherClosedProjects() { //count the closed projects in the selection int closedInSelection = 0; - for (IResource project : getSelectedResources()) { + for (IResource project : getActionResources()) { if (!((IProject) project).isOpen()) { closedInSelection++; } @@ -154,6 +154,13 @@ protected void invokeOperation(IResource resource, IProgressMonitor monitor) thr ((IProject) resource).open(IResource.BACKGROUND_REFRESH, monitor); } + @Override + protected List getActionResources() { + // The open operation only ever applies to projects; drop any non-project + // elements of a mixed selection so execution does not fail with a cast. + return super.getActionResources().stream().filter(IProject.class::isInstance).toList(); + } + /** * Returns the preference for whether to open required projects when opening * a project. Consults the preference and prompts the user if necessary. @@ -190,7 +197,7 @@ public void resourceChanged(IResourceChangeEvent event) { // Warning: code duplicated in CloseResourceAction List sel = getSelectedResources(); // don't bother looking at delta if selection not applicable - if (selectionIsOfType(IResource.PROJECT)) { + if (sel.stream().anyMatch(IProject.class::isInstance)) { IResourceDelta delta = event.getDelta(); if (delta != null) { IResourceDelta[] projDeltas = delta.getAffectedChildren(IResourceDelta.CHANGED); @@ -304,13 +311,15 @@ protected boolean updateSelection(IStructuredSelection s) { // selected. setText(IDEWorkbenchMessages.OpenResourceAction_text); setToolTipText(IDEWorkbenchMessages.OpenResourceAction_toolTip); - if (!selectionIsOfType(IResource.PROJECT)) { + List projects = getSelectedResources().stream() + .filter(IProject.class::isInstance).map(IProject.class::cast).toList(); + if (projects.isEmpty()) { return false; } boolean hasClosedProjects = false; - for (IResource currentResource : getSelectedResources()) { - if (!((IProject) currentResource).isOpen()) { + for (IProject currentResource : projects) { + if (!currentResource.isOpen()) { if (hasClosedProjects) { setText(IDEWorkbenchMessages.OpenResourceAction_text_plural); setToolTipText(IDEWorkbenchMessages.OpenResourceAction_toolTip_plural); diff --git a/bundles/org.eclipse.ui.navigator.resources/META-INF/MANIFEST.MF b/bundles/org.eclipse.ui.navigator.resources/META-INF/MANIFEST.MF index a26be904ac0..eb4dacd798b 100644 --- a/bundles/org.eclipse.ui.navigator.resources/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.ui.navigator.resources/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %Plugin.name Bundle-SymbolicName: org.eclipse.ui.navigator.resources; singleton:=true -Bundle-Version: 3.10.100.qualifier +Bundle-Version: 3.10.200.qualifier Bundle-Activator: org.eclipse.ui.internal.navigator.resources.plugin.WorkbenchNavigatorPlugin Bundle-Vendor: %Plugin.providerName Bundle-Localization: plugin diff --git a/bundles/org.eclipse.ui.navigator.resources/plugin.xml b/bundles/org.eclipse.ui.navigator.resources/plugin.xml index 7eec1464661..d6007f86353 100644 --- a/bundles/org.eclipse.ui.navigator.resources/plugin.xml +++ b/bundles/org.eclipse.ui.navigator.resources/plugin.xml @@ -237,13 +237,8 @@ class="org.eclipse.ui.internal.navigator.resources.actions.ResourceMgmtActionProvider" id="org.eclipse.ui.navigator.resources.ResourceMgmtActions"> - - - - - - - + + diff --git a/tests/org.eclipse.ui.tests.navigator/src/org/eclipse/ui/tests/navigator/resources/ResourceMgmtActionProviderTests.java b/tests/org.eclipse.ui.tests.navigator/src/org/eclipse/ui/tests/navigator/resources/ResourceMgmtActionProviderTests.java index 128451a678b..cfc57fdc63d 100644 --- a/tests/org.eclipse.ui.tests.navigator/src/org/eclipse/ui/tests/navigator/resources/ResourceMgmtActionProviderTests.java +++ b/tests/org.eclipse.ui.tests.navigator/src/org/eclipse/ui/tests/navigator/resources/ResourceMgmtActionProviderTests.java @@ -10,10 +10,14 @@ *******************************************************************************/ package org.eclipse.ui.tests.navigator.resources; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import java.util.List; + import org.eclipse.core.resources.ICommand; +import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IProjectDescription; @@ -28,6 +32,8 @@ import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.ui.actions.ActionContext; +import org.eclipse.ui.actions.CloseResourceAction; +import org.eclipse.ui.actions.OpenResourceAction; import org.eclipse.ui.internal.navigator.NavigatorContentService; import org.eclipse.ui.internal.navigator.extensions.CommonActionExtensionSite; import org.eclipse.ui.internal.navigator.resources.actions.ResourceMgmtActionProvider; @@ -118,6 +124,120 @@ public void testFillContextMenu_openProjectNoBuilderSelection() throws CoreExcep } } + /** + * Test for a file selected together with an open project: Close Project must + * be both present and enabled. Regression test for the bug where + * selectionIsOfType(PROJECT) disabled the action for any mixed selection. + * + * @throws CoreException + */ + @Test + public void testFillContextMenu_fileAndOpenProjectSelection_closeProjectEnabled() throws CoreException { + // _p1 is already open; _project has a known 'src' folder + files + IProject openProj = ResourcesPlugin.getWorkspace().getRoot().getProject("Test"); + openProj.open(null); + // Select a file alongside a project (the typical Ctrl+A expanded scenario) + ResourceMgmtActionProvider provider = providerForObjects(_p1, openProj.getFile(".project")); + provider.fillContextMenu(manager); + assertTrue(menuHasContribution("org.eclipse.ui.CloseResourceAction"), + "Close Project should be in the menu"); + assertTrue(isMenuContributionEnabled("org.eclipse.ui.CloseResourceAction"), + "Close Project should be enabled when open projects are in the selection"); + assertTrue(menuHasContribution("org.eclipse.ui.CloseUnrelatedProjectsAction"), + "Close Unrelated Projects should be in the menu"); + assertTrue(isMenuContributionEnabled("org.eclipse.ui.CloseUnrelatedProjectsAction"), + "Close Unrelated Projects should be enabled when open projects are in the selection"); + } + + /** + * Test for mixed selection: an open project alongside a non-adaptable element + * (e.g. a working set header from Ctrl+A in Project Explorer). Close Project + * and Refresh must still appear — regression test for issue #3790. + * + * @throws CoreException + */ + @Test + public void testFillContextMenu_mixedSelectionOpenProjectAndNonAdaptableElement() throws CoreException { + IProject openProj = ResourcesPlugin.getWorkspace().getRoot().getProject("Test"); + openProj.open(null); + // Plain Object does not implement IAdaptable, so it is never resolved to a + // project — it counts as a non-project element in the selection. + Object nonResource = new Object(); + ResourceMgmtActionProvider provider = providerForObjects(openProj, nonResource); + provider.fillContextMenu(manager); + checkMenuHasCorrectContributions(false, true, false, true, true); + } + + /** + * Test for a fully expanded selection: two open projects plus child resources + * from both (simulating Ctrl+A when both projects are expanded). Close Project + * must still appear for the open projects in the selection. + * + * @throws CoreException + */ + @Test + public void testFillContextMenu_twoOpenProjectsWithChildResourcesSelection() throws CoreException { + // _p1 and _p2 are already opened in setUp() + IFolder srcFolder = _project.getFolder("src"); + IFolder binFolder = _project.getFolder("bin"); + ResourceMgmtActionProvider provider = providerForObjects(_p1, _p2, srcFolder, binFolder); + provider.fillContextMenu(manager); + checkMenuHasCorrectContributions(false, true, false, true, true); + } + + /** + * Regression test for the ClassCastException that the always-on provider + * enablement could expose: when Close Project is invoked on a mixed selection + * (project plus a file plus a non-resource element, as produced by Ctrl+A), + * the action must reduce the selection to projects only. Otherwise run() casts + * every selected resource to IProject while building the scheduling rule. + * + * @throws CoreException + */ + @Test + public void testCloseResourceAction_actionResourcesContainProjectsOnly() throws CoreException { + IProject openProj = ResourcesPlugin.getWorkspace().getRoot().getProject("Test"); + openProj.open(null); + IFile projectFile = openProj.getFile(".project"); + StructuredSelection mixed = new StructuredSelection(new Object[] { openProj, projectFile, new Object() }); + + var action = new CloseResourceAction(() -> _commonNavigator.getViewSite().getShell()) { + List exposedActionResources() { + return getActionResources(); + } + }; + action.selectionChanged(mixed); + + assertEquals(List.of(openProj), action.exposedActionResources(), + "Close Project must operate on projects only, not files or non-resource elements"); + } + + /** + * Regression test for the ClassCastException in OpenResourceAction on a mixed + * selection: the action must reduce the selection to projects only, otherwise + * hasOtherClosedProjects() casts a non-project resource to IProject while + * opening projects with their references. + * + * @throws CoreException + */ + @Test + public void testOpenResourceAction_actionResourcesContainProjectsOnly() throws CoreException { + IProject openProj = ResourcesPlugin.getWorkspace().getRoot().getProject("Test"); + openProj.open(null); + IFile projectFile = openProj.getFile(".project"); + StructuredSelection mixed = new StructuredSelection(new Object[] { openProj, projectFile, new Object() }); + + var action = new OpenResourceAction(() -> _commonNavigator.getViewSite().getShell()) { + List exposedActionResources() { + return getActionResources(); + } + }; + action.selectionChanged(mixed); + + assertEquals(List.of(openProj), action.exposedActionResources(), + "Open Project must operate on projects only, not files or non-resource elements"); + } + /** * Test for 'open project' that doesn't have a builder attached - only 'open * project' should be disabled @@ -158,6 +278,19 @@ public void testFillContextMenu_openProjectWithBuilderSelection() throws CoreExc } } + /* + * Return a provider for a mixed/arbitrary selection (Object[]) + */ + private ResourceMgmtActionProvider providerForObjects(Object... selectedElements) { + ICommonActionExtensionSite cfg = new CommonActionExtensionSite("NA", "NA", + CommonViewerSiteFactory.createCommonViewerSite(_commonNavigator.getViewSite()), + (NavigatorContentService) _contentService, _viewer); + ResourceMgmtActionProvider provider = new ResourceMgmtActionProvider(); + provider.setContext(new ActionContext(new StructuredSelection(selectedElements))); + provider.init(cfg); + return provider; + } + /* * Return a provider, given the selected navigator items */ @@ -206,4 +339,16 @@ private boolean menuHasContribution(String contribution) { return false; } + /* + * Check whether the named menu entry is enabled + */ + private boolean isMenuContributionEnabled(String contribution) { + for (IContributionItem thisItem : manager.getItems()) { + if (thisItem.getId() != null && thisItem.getId().equals(contribution)) { + return thisItem.isEnabled(); + } + } + return false; + } + }