Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -229,14 +228,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<IProject> projects = getSelectedResources().stream()
.filter(IProject.class::isInstance).map(IProject.class::cast).toList();
if (projects.isEmpty()) {
return false;
}
Comment on lines +231 to 235
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updateSelection() now enables the action when any project is selected, even in mixed selections. However, run()/runInBackground() still uses getSelectedResources() and then casts every element to IProject (e.g., when building scheduling rules), which will throw ClassCastException if the selection includes non-project resources (files/folders) as allowed by the new enablement. Filter to IProject consistently (ideally by overriding getActionResources() to return only projects and using that list throughout run()).

Copilot uses AI. Check for mistakes.

boolean hasOpenProjects = false;
Iterator<? extends IResource> resources = getSelectedResources().iterator();
while (resources.hasNext()) {
IProject currentResource = (IProject) resources.next();
for (IProject currentResource : projects) {
if (currentResource.isOpen()) {
if (hasOpenProjects) {
setText(pluralText);
Expand All @@ -258,7 +257,7 @@ public synchronized void resourceChanged(IResourceChangeEvent event) {
// Warning: code duplicated in OpenResourceAction
List<? extends IResource> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,8 @@ protected List<? extends IResource> getSelectedResources() {
@Override
public void resourceChanged(IResourceChangeEvent event) {
// don't bother looking at delta if selection not applicable
if (selectionIsOfType(IResource.PROJECT)) {
List<? extends IResource> selectedResources = super.getSelectedResources();
if (selectedResources.stream().anyMatch(IProject.class::isInstance)) {
IResourceDelta delta = event.getDelta();
Comment on lines 262 to 266
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CloseUnrelatedProjectsAction is now enabled for mixed selections via provider changes, but updateSelection() still depends on selectionIsOfType(IResource.PROJECT), which returns false when the selection contains any non-resource element (e.g., working set headers). That means this action can still appear but be disabled for the exact scenario in issue #3790. Consider aligning enablement logic with Open/CloseResourceAction (e.g., enable when any selected resource is a project, ignoring non-resources).

Copilot uses AI. Check for mistakes.
if (delta != null) {
IResourceDelta[] projDeltas = delta.getAffectedChildren(IResourceDelta.CHANGED);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ public void resourceChanged(IResourceChangeEvent event) {
// Warning: code duplicated in CloseResourceAction
List<? extends IResource> 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);
Expand Down Expand Up @@ -304,13 +304,15 @@ protected boolean updateSelection(IStructuredSelection s) {
// selected.
setText(IDEWorkbenchMessages.OpenResourceAction_text);
setToolTipText(IDEWorkbenchMessages.OpenResourceAction_toolTip);
if (!selectionIsOfType(IResource.PROJECT)) {
List<IProject> projects = getSelectedResources().stream()
.filter(IProject.class::isInstance).map(IProject.class::cast).toList();
if (projects.isEmpty()) {
Comment on lines +307 to +309
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allowing mixed selections means internal logic must also tolerate non-project resources. hasOtherClosedProjects() still iterates getSelectedResources() and blindly casts each entry to IProject, which can now be an IFile/IFolder and will cause a ClassCastException when opening projects with references from a mixed selection. Update this logic to only consider selected IProject instances (similar to the new projects filtering in updateSelection()).

Copilot uses AI. Check for mistakes.
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);
Expand Down
9 changes: 2 additions & 7 deletions bundles/org.eclipse.ui.navigator.resources/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -237,13 +237,8 @@
class="org.eclipse.ui.internal.navigator.resources.actions.ResourceMgmtActionProvider"
id="org.eclipse.ui.navigator.resources.ResourceMgmtActions">
<enablement>
<or>
<adapt type="org.eclipse.core.resources.IResource" />
<adapt type="java.util.Collection">
<count value="0" />
</adapt>
<adapt type="org.eclipse.ui.IWorkingSet" />
</or>
<!-- Always enabled: fillContextMenu() filters to applicable projects -->
<or/>
Comment on lines +240 to +241
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switching the provider enablement to unconditional () makes these actions show up for selections containing non-resource elements, but the underlying actions still need to ignore non-resource selection entries for enablement and execution (not just menu population). Please double-check that the actions added by ResourceMgmtActionProvider are enabled and runnable when the selection includes non-IResource/IWorkingSet elements (e.g., working set headers), otherwise this can regress into visible-but-disabled actions.

Suggested change
<!-- Always enabled: fillContextMenu() filters to applicable projects -->
<or/>
<or>
<adapt type="org.eclipse.core.resources.IResource"/>
<instanceof value="org.eclipse.ui.IWorkingSet"/>
</or>

Copilot uses AI. Check for mistakes.
</enablement>
</actionProvider>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,67 @@ 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);
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This regression test covers visibility for a mixed selection that includes a non-adaptable element, but it doesn't assert enablement. Given the goal of issue #3790, it would be valuable to also assert that Close Project / Close Unrelated Projects are enabled (not just present) when an open project is part of the selection; otherwise the menu items could still be shown but unusable.

Suggested change
checkMenuHasCorrectContributions(false, true, false, true, true);
checkMenuHasCorrectContributions(false, true, false, true, true);
assertTrue(isMenuContributionEnabled("org.eclipse.ui.CloseResourceAction"),
"Close Project should be enabled when an open project is selected with a non-adaptable element");
assertTrue(isMenuContributionEnabled("org.eclipse.ui.CloseUnrelatedProjectsAction"),
"Close Unrelated Projects should be enabled when an open project is selected with a non-adaptable element");

Copilot uses AI. Check for mistakes.
}

/**
* 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);
}

/**
* Test for 'open project' that doesn't have a builder attached - only 'open
* project' should be disabled
Expand Down Expand Up @@ -158,6 +219,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
*/
Expand Down Expand Up @@ -206,4 +280,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;
}

}
Loading