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 @@ -179,8 +178,9 @@ protected void invokeOperation(IResource resource, IProgressMonitor monitor) thr
*/
@Override
public void run() {
// Get the items to close.
List<? extends IResource> 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<? extends IResource> projects = getActionResources();
if (projects == null || projects.isEmpty()) {
// no action needs to be taken since no projects are selected
return;
Expand Down Expand Up @@ -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<IProject> projects = getSelectedResources().stream()
.filter(IProject.class::isInstance).map(IProject.class::cast).toList();
if (projects.isEmpty()) {
return false;
}
Comment thread
vogella marked this conversation as resolved.

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 +258,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 All @@ -280,6 +280,13 @@ protected synchronized List<? extends IResource> getSelectedResources() {
return super.getSelectedResources();
}

@Override
protected List<? extends IResource> 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,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 thread
vogella marked this conversation as resolved.
if (delta != null) {
IResourceDelta[] projDeltas = delta.getAffectedChildren(IResourceDelta.CHANGED);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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++;
}
Expand All @@ -154,6 +154,13 @@ protected void invokeOperation(IResource resource, IProgressMonitor monitor) thr
((IProject) resource).open(IResource.BACKGROUND_REFRESH, monitor);
}

@Override
protected List<? extends IResource> 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.
Expand Down Expand Up @@ -190,7 +197,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 +311,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 thread
vogella marked this conversation as resolved.
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
Original file line number Diff line number Diff line change
Expand Up @@ -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
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 thread
vogella marked this conversation as resolved.
</enablement>
</actionProvider>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Comment thread
vogella marked this conversation as resolved.
}

/**
* 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<? extends IResource> 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<? extends IResource> 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
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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;
}

}
Loading