From ccda0fc145f1c15b76269b441622801b1242bb82 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Fri, 12 Dec 2025 09:44:33 +0100 Subject: [PATCH 1/2] Enable parallel refresh by scheduling one WorkspaceJob per project Previously a single WorkspaceJob was scheduled with a combined MultiRule across all selected projects. This still refreshed all resources sequentially and held every project lock simultaneously, which was more restrictive than needed. Now RefreshAction.run() groups resources by their scheduling rule (project or workspace root) and calls scheduleRefreshJob() once per group. Each group gets its own WorkspaceModifyOperation protected by only that project's rule, so independent projects can be refreshed in parallel while concurrent workspace access to a single project is still prevented. Two new protected hook methods are added to RefreshAction: - createRefreshJob(): builds the WorkspaceModifyOperation-wrapped WorkspaceJob without scheduling it, so callers can attach IJobChangeListeners before scheduling (no race condition). - scheduleRefreshJob(): default implementation calls createRefreshJob().schedule(); subclasses override to customise. ResourceMgmtActionProvider no longer duplicates the run() logic. It overrides scheduleRefreshJob() instead, attaches a JobChangeAdapter that triggers a Navigator viewer refresh after each project's job completes, then schedules the job. Co-Authored-By: Claude Sonnet 4.6 --- .../org/eclipse/ui/actions/RefreshAction.java | 139 ++++++++++++------ .../actions/ResourceMgmtActionProvider.java | 55 ++----- 2 files changed, 114 insertions(+), 80 deletions(-) diff --git a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/RefreshAction.java b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/RefreshAction.java index 94707e8c36d2..58a8b67dfdc5 100644 --- a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/RefreshAction.java +++ b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/RefreshAction.java @@ -18,14 +18,14 @@ import java.lang.reflect.InvocationTargetException; import java.net.URI; import java.util.ArrayList; -import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import org.eclipse.core.filesystem.IFileInfo; import org.eclipse.core.filesystem.IFileStore; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; -import org.eclipse.core.resources.IResourceRuleFactory; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.resources.WorkspaceJob; @@ -49,6 +49,7 @@ import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.internal.ide.IDEWorkbenchMessages; +import org.eclipse.ui.internal.ide.IDEWorkbenchPlugin; import org.eclipse.ui.internal.ide.IIDEHelpContextIds; import org.eclipse.ui.internal.ide.StatusUtil; import org.eclipse.ui.internal.ide.dialogs.IDEResourceInfoUtils; @@ -217,41 +218,110 @@ final public void refreshAll() { @Override final protected IRunnableWithProgress createOperation(final IStatus[] errorStatus) { - ISchedulingRule rule = null; - IResourceRuleFactory factory = ResourcesPlugin.getWorkspace().getRuleFactory(); - List actionResources = new ArrayList<>(getActionResources()); if (shouldPerformResourcePruning()) { actionResources = pruneResources(actionResources); } final List resources = actionResources; - Iterator res = resources.iterator(); - while (res.hasNext()) { - rule = MultiRule.combine(rule, factory.refreshRule(res.next())); + ISchedulingRule rule = null; + for (IResource resource : resources) { + ISchedulingRule newRule = (resource.getType() == IResource.ROOT) ? resource : resource.getProject(); + rule = MultiRule.combine(rule, newRule); } + return new WorkspaceModifyOperation(rule) { @Override public void execute(IProgressMonitor mon) { SubMonitor subMonitor = SubMonitor.convert(mon, resources.size()); - MultiStatus errors = null; subMonitor.setTaskName(getOperationMessage()); - Iterator resourcesEnum = resources.iterator(); - while (resourcesEnum.hasNext()) { + List errors = new ArrayList<>(); + for (IResource resource : resources) { try { - IResource resource = resourcesEnum.next(); refreshResource(resource, subMonitor.split(1)); } catch (CoreException e) { - errors = recordError(errors, e); + errors.add(e.getStatus()); } } - if (errors != null) { - errorStatus[0] = errors; + if (!errors.isEmpty()) { + MultiStatus multiStatus = new MultiStatus(IDEWorkbenchPlugin.IDE_WORKBENCH, IStatus.ERROR, + getProblemsMessage(), null); + for (IStatus s : errors) { + multiStatus.merge(s); + } + errorStatus[0] = multiStatus; } } }; } + /** + * Creates a {@link WorkspaceJob} that refreshes the given resources under the + * given scheduling rule. The job is not yet scheduled when returned, allowing + * callers to attach listeners before scheduling. + * + * @param resources resources to refresh; must not be null + * @param rule scheduling rule for the job (a project or workspace root) + * @return the created but unscheduled job + * @since 3.23 + */ + protected WorkspaceJob createRefreshJob(List resources, ISchedulingRule rule) { + final IStatus[] errorStatus = { Status.OK_STATUS }; + WorkspaceModifyOperation op = new WorkspaceModifyOperation(rule) { + @Override + public void execute(IProgressMonitor mon) { + SubMonitor subMonitor = SubMonitor.convert(mon, resources.size()); + subMonitor.setTaskName(getOperationMessage()); + List errors = new ArrayList<>(); + for (IResource resource : resources) { + try { + refreshResource(resource, subMonitor.split(1)); + } catch (CoreException e) { + errors.add(e.getStatus()); + } + } + if (!errors.isEmpty()) { + MultiStatus multiStatus = new MultiStatus(IDEWorkbenchPlugin.IDE_WORKBENCH, IStatus.ERROR, + getProblemsMessage(), null); + for (IStatus s : errors) { + multiStatus.merge(s); + } + errorStatus[0] = multiStatus; + } + } + }; + WorkspaceJob job = new WorkspaceJob("refresh") { //$NON-NLS-1$ + @Override + public IStatus runInWorkspace(IProgressMonitor monitor) throws CoreException { + try { + op.run(monitor); + } catch (InvocationTargetException e) { + String msg = NLS.bind(IDEWorkbenchMessages.WorkspaceAction_logTitle, getClass().getName(), + e.getTargetException()); + return StatusUtil.newStatus(IStatus.ERROR, msg, e.getTargetException()); + } catch (InterruptedException e) { + return Status.CANCEL_STATUS; + } + return errorStatus[0]; + } + }; + job.setRule(op.getRule()); + job.setUser(true); + return job; + } + + /** + * Creates and schedules a {@link WorkspaceJob} for the given resources. + * Subclasses may override to attach listeners before the job is scheduled. + * + * @param resources resources to refresh; must not be null + * @param rule scheduling rule for the job (a project or workspace root) + * @since 3.23 + */ + protected void scheduleRefreshJob(List resources, ISchedulingRule rule) { + createRefreshJob(resources, rule).schedule(); + } + /** * Refresh the resource (with a check for deleted projects). *

@@ -285,32 +355,19 @@ protected void refreshResource(IResource resource, IProgressMonitor monitor) thr @Override public void run() { - final IStatus[] errorStatus = new IStatus[1]; - errorStatus[0] = Status.OK_STATUS; - final WorkspaceModifyOperation op = (WorkspaceModifyOperation) createOperation(errorStatus); - WorkspaceJob job = new WorkspaceJob("refresh") { //$NON-NLS-1$ - - @Override - public IStatus runInWorkspace(IProgressMonitor monitor) throws CoreException { - try { - op.run(monitor); - } catch (InvocationTargetException e) { - String msg = NLS.bind( - IDEWorkbenchMessages.WorkspaceAction_logTitle, getClass() - .getName(), e.getTargetException()); - throw new CoreException(StatusUtil.newStatus(IStatus.ERROR, msg, e.getTargetException())); - } catch (InterruptedException e) { - return Status.CANCEL_STATUS; - } - return errorStatus[0]; - } - - }; - ISchedulingRule rule = op.getRule(); - if (rule != null) { - job.setRule(rule); + List actionResources = new ArrayList<>(getActionResources()); + if (shouldPerformResourcePruning()) { + actionResources = pruneResources(actionResources); + } + // Group resources by scheduling rule so each project can be refreshed in + // parallel while still holding a project-level rule during its refresh. + Map> byRule = new LinkedHashMap<>(); + for (IResource resource : actionResources) { + ISchedulingRule rule = (resource.getType() == IResource.ROOT) ? resource : resource.getProject(); + byRule.computeIfAbsent(rule, r -> new ArrayList<>()).add(resource); + } + for (Map.Entry> entry : byRule.entrySet()) { + scheduleRefreshJob(entry.getValue(), entry.getKey()); } - job.setUser(true); - job.schedule(); } } diff --git a/bundles/org.eclipse.ui.navigator.resources/src/org/eclipse/ui/internal/navigator/resources/actions/ResourceMgmtActionProvider.java b/bundles/org.eclipse.ui.navigator.resources/src/org/eclipse/ui/internal/navigator/resources/actions/ResourceMgmtActionProvider.java index 281115512933..f80a884797a1 100644 --- a/bundles/org.eclipse.ui.navigator.resources/src/org/eclipse/ui/internal/navigator/resources/actions/ResourceMgmtActionProvider.java +++ b/bundles/org.eclipse.ui.navigator.resources/src/org/eclipse/ui/internal/navigator/resources/actions/ResourceMgmtActionProvider.java @@ -15,7 +15,6 @@ package org.eclipse.ui.internal.navigator.resources.actions; -import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -23,21 +22,20 @@ import org.eclipse.core.resources.ICommand; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IncrementalProjectBuilder; +import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.resources.WorkspaceJob; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IAdaptable; -import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.IJobChangeEvent; import org.eclipse.core.runtime.jobs.ISchedulingRule; +import org.eclipse.core.runtime.jobs.JobChangeAdapter; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.Separator; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.StructuredViewer; import org.eclipse.jface.window.IShellProvider; -import org.eclipse.osgi.util.NLS; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IActionBars; import org.eclipse.ui.IWorkbenchCommandConstants; @@ -48,11 +46,8 @@ import org.eclipse.ui.actions.CloseUnrelatedProjectsAction; import org.eclipse.ui.actions.OpenResourceAction; import org.eclipse.ui.actions.RefreshAction; -import org.eclipse.ui.actions.WorkspaceModifyOperation; import org.eclipse.ui.ide.IDEActionFactory; import org.eclipse.ui.internal.ide.IDEWorkbenchPlugin; -import org.eclipse.ui.internal.navigator.NavigatorPlugin; -import org.eclipse.ui.internal.navigator.resources.plugin.WorkbenchNavigatorMessages; import org.eclipse.ui.navigator.CommonActionProvider; import org.eclipse.ui.navigator.ICommonActionExtensionSite; import org.eclipse.ui.navigator.ICommonMenuConstants; @@ -215,40 +210,22 @@ protected void makeActions() { refreshAction = new RefreshAction(sp) { @Override - public void run() { - final IStatus[] errorStatus = new IStatus[1]; - errorStatus[0] = Status.OK_STATUS; - final WorkspaceModifyOperation op = (WorkspaceModifyOperation) createOperation(errorStatus); - WorkspaceJob job = new WorkspaceJob("refresh") { //$NON-NLS-1$ - + protected void scheduleRefreshJob(List resources, ISchedulingRule rule) { + WorkspaceJob job = createRefreshJob(resources, rule); + job.addJobChangeListener(new JobChangeAdapter() { @Override - public IStatus runInWorkspace(IProgressMonitor monitor) throws CoreException { - try { - op.run(monitor); - if (shell != null && !shell.isDisposed()) { - shell.getDisplay().asyncExec(() -> { - StructuredViewer viewer = getActionSite().getStructuredViewer(); - if (viewer != null && viewer.getControl() != null - && !viewer.getControl().isDisposed()) { - viewer.refresh(); - } - }); - } - } catch (InvocationTargetException e) { - String msg = NLS.bind(WorkbenchNavigatorMessages.ResourceMgmtActionProvider_logTitle, getClass().getName(), e.getTargetException()); - throw new CoreException(new Status(IStatus.ERROR, NavigatorPlugin.PLUGIN_ID, IStatus.ERROR, msg, e.getTargetException())); - } catch (InterruptedException e) { - return Status.CANCEL_STATUS; + public void done(IJobChangeEvent event) { + if (shell != null && !shell.isDisposed()) { + shell.getDisplay().asyncExec(() -> { + StructuredViewer viewer = getActionSite().getStructuredViewer(); + if (viewer != null && viewer.getControl() != null + && !viewer.getControl().isDisposed()) { + viewer.refresh(); + } + }); } - return errorStatus[0]; } - - }; - ISchedulingRule rule = op.getRule(); - if (rule != null) { - job.setRule(rule); - } - job.setUser(true); + }); job.schedule(); } }; From 1153544ce1bebaec18e0455585f7f0bb10ef45d6 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Fri, 20 Feb 2026 14:35:40 +0100 Subject: [PATCH 2/2] Include project name in refresh job title shown in Progress view When multiple projects are refreshed simultaneously, the Progress view previously showed identical "refresh" entries for every job. Each job now uses "refresh (ProjectName)" so users can see which project is being refreshed. Jobs covering the workspace root retain the plain "refresh" title. Co-Authored-By: Claude Sonnet 4.6 --- .../extensions/org/eclipse/ui/actions/RefreshAction.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/RefreshAction.java b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/RefreshAction.java index 58a8b67dfdc5..5981e6fb6fab 100644 --- a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/RefreshAction.java +++ b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/RefreshAction.java @@ -290,7 +290,8 @@ public void execute(IProgressMonitor mon) { } } }; - WorkspaceJob job = new WorkspaceJob("refresh") { //$NON-NLS-1$ + String jobName = (rule instanceof IProject p) ? "refresh (" + p.getName() + ")" : "refresh"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + WorkspaceJob job = new WorkspaceJob(jobName) { @Override public IStatus runInWorkspace(IProgressMonitor monitor) throws CoreException { try {