diff --git a/bundles/org.eclipse.ltk.core.refactoring/META-INF/MANIFEST.MF b/bundles/org.eclipse.ltk.core.refactoring/META-INF/MANIFEST.MF
index 9f0f659919b..91b818cdfc2 100644
--- a/bundles/org.eclipse.ltk.core.refactoring/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.ltk.core.refactoring/META-INF/MANIFEST.MF
@@ -3,7 +3,7 @@ Automatic-Module-Name: org.eclipse.ltk.core.refactoring
Bundle-ManifestVersion: 2
Bundle-Name: %pluginName
Bundle-SymbolicName: org.eclipse.ltk.core.refactoring; singleton:=true
-Bundle-Version: 3.15.200.qualifier
+Bundle-Version: 3.16.0.qualifier
Bundle-Activator: org.eclipse.ltk.internal.core.refactoring.RefactoringCorePlugin
Bundle-ActivationPolicy: lazy
Bundle-Vendor: %providerName
diff --git a/bundles/org.eclipse.ltk.core.refactoring/plugin.xml b/bundles/org.eclipse.ltk.core.refactoring/plugin.xml
index d59248c24ca..fe30354aad2 100644
--- a/bundles/org.eclipse.ltk.core.refactoring/plugin.xml
+++ b/bundles/org.eclipse.ltk.core.refactoring/plugin.xml
@@ -52,5 +52,8 @@
class="org.eclipse.ltk.internal.core.refactoring.resource.CopyProjectRefactoringContribution"
id="org.eclipse.ltk.core.refactoring.copyproject.resource">
+
\ No newline at end of file
diff --git a/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/core/refactoring/resource/CopyResourceChange.java b/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/core/refactoring/resource/CopyResourceChange.java
new file mode 100644
index 00000000000..a3d8894bc21
--- /dev/null
+++ b/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/core/refactoring/resource/CopyResourceChange.java
@@ -0,0 +1,156 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Felix Schmid
+ *
+ * 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:
+ * Felix Schmid - initial API and implementation and/or initial documentation
+ *******************************************************************************/
+package org.eclipse.ltk.core.refactoring.resource;
+
+import java.net.URI;
+import java.text.MessageFormat;
+
+import org.eclipse.core.runtime.Assert;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.core.runtime.SubMonitor;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IResource;
+
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.ChangeDescriptor;
+import org.eclipse.ltk.core.refactoring.CompositeChange;
+import org.eclipse.ltk.core.refactoring.NullChange;
+import org.eclipse.ltk.core.refactoring.RefactoringStatus;
+import org.eclipse.ltk.core.refactoring.participants.ReorgExecutionLog;
+import org.eclipse.ltk.internal.core.refactoring.BasicElementLabels;
+import org.eclipse.ltk.internal.core.refactoring.RefactoringCoreMessages;
+
+/**
+ * {@link Change} that copies a resource.
+ *
+ * @since 3.16
+ */
+public class CopyResourceChange extends ResourceChange {
+
+ private ChangeDescriptor descriptor;
+
+ private final IResource origin;
+
+ private final ReorgExecutionLog log;
+
+ private final IContainer destination;
+
+ public CopyResourceChange(final IResource origin, final ReorgExecutionLog log, final IContainer destination) {
+ Assert.isTrue(origin instanceof IFile || origin instanceof IFolder);
+ this.origin= origin;
+ this.log= log;
+ this.destination= destination;
+ setValidationMethod(VALIDATE_NOT_DIRTY);
+ }
+
+ @Override
+ public String getName() {
+ return MessageFormat.format(RefactoringCoreMessages.CopyResourceChange_name,
+ BasicElementLabels.getPathLabel(origin.getFullPath(), false),
+ BasicElementLabels.getResourceName(destination));
+ }
+
+ @Override
+ public final Change perform(final IProgressMonitor pm) throws CoreException, OperationCanceledException {
+ pm.beginTask(getName(), 2);
+ try {
+ String newName= log.getNewName(origin);
+ if (newName == null) {
+ newName= origin.getName();
+ }
+
+ final IResource resAtDest= destination.findMember(newName);
+ if (resAtDest != null && resAtDest.exists() && areEqualInWorkspaceOrOnDisk(origin, resAtDest)) {
+ return new NullChange();
+ }
+
+ final Change undoOverwrite= deleteIfAlreadyExists(resAtDest, SubMonitor.convert(pm, 1));
+
+ final IPath copyTo= destination.getFullPath().append(newName);
+ origin.copy(copyTo, getReorgFlags(), SubMonitor.convert(pm, 1));
+ log.markAsProcessed(origin);
+
+ if (undoOverwrite != null) {
+ return new CompositeChange(RefactoringCoreMessages.CopyResourceChange_undo_composite_name,
+ new Change[] { new DeleteResourceChange(copyTo, false), undoOverwrite });
+ }
+ return new DeleteResourceChange(copyTo, false);
+ } finally {
+ pm.done();
+ }
+ }
+
+ @Override
+ protected IResource getModifiedResource() {
+ return origin;
+ }
+
+ /**
+ * deletes a resource if it exists and returns a Change to undo the deletion
+ *
+ * @param resource the resource to delete
+ * @param pm the progress monitor
+ * @return returns an undo Change or null if nothing was deleted
+ * @throws CoreException thrown when the resource cannot be accessed
+ */
+ private Change deleteIfAlreadyExists(final IResource resource, final IProgressMonitor pm) throws CoreException {
+ if (resource == null || !resource.exists()) {
+ pm.done();
+ return null;
+ }
+ SubMonitor subMonitor= SubMonitor.convert(pm,
+ RefactoringCoreMessages.MoveResourceChange_progress_delete_destination, 3);
+ DeleteResourceChange deleteChange= new DeleteResourceChange(resource.getFullPath(), true);
+ deleteChange.initializeValidationData(subMonitor.newChild(1));
+ RefactoringStatus deleteStatus= deleteChange.isValid(subMonitor.newChild(1));
+ if (!deleteStatus.hasFatalError()) {
+ return deleteChange.perform(subMonitor.newChild(1));
+ }
+ return null;
+ }
+
+ private static boolean areEqualInWorkspaceOrOnDisk(final IResource r1, final IResource r2) {
+ if (r1 == null || r2 == null) {
+ return false;
+ }
+ if (r1.equals(r2)) {
+ return true;
+ }
+ final URI r1Location= r1.getLocationURI();
+ final URI r2Location= r2.getLocationURI();
+ if (r1Location == null || r2Location == null) {
+ return false;
+ }
+ return r1Location.equals(r2Location);
+ }
+
+ private static int getReorgFlags() {
+ return IResource.KEEP_HISTORY | IResource.SHALLOW;
+ }
+
+ @Override
+ public ChangeDescriptor getDescriptor() {
+ return descriptor;
+ }
+
+ public void setDescriptor(ChangeDescriptor descriptor) {
+ this.descriptor= descriptor;
+ }
+}
diff --git a/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/core/refactoring/resource/CopyResourcesDescriptor.java b/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/core/refactoring/resource/CopyResourcesDescriptor.java
new file mode 100644
index 00000000000..7e250c9de61
--- /dev/null
+++ b/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/core/refactoring/resource/CopyResourcesDescriptor.java
@@ -0,0 +1,124 @@
+/*******************************************************************************
+ * Copyright (c) 2007, 2026 IBM Corporation 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:
+ * IBM Corporation - initial API and implementation
+ * Felix Schmid - adapted for copy resource descriptor
+ *******************************************************************************/
+package org.eclipse.ltk.core.refactoring.resource;
+
+import java.text.MessageFormat;
+import java.util.Objects;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+
+import org.eclipse.ltk.core.refactoring.Refactoring;
+import org.eclipse.ltk.core.refactoring.RefactoringContribution;
+import org.eclipse.ltk.core.refactoring.RefactoringCore;
+import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
+import org.eclipse.ltk.core.refactoring.RefactoringStatus;
+import org.eclipse.ltk.core.refactoring.participants.CopyRefactoring;
+import org.eclipse.ltk.internal.core.refactoring.BasicElementLabels;
+import org.eclipse.ltk.internal.core.refactoring.RefactoringCoreMessages;
+import org.eclipse.ltk.internal.core.refactoring.resource.CopyResourcesProcessor;
+
+/**
+ * Refactoring descriptor for the copy resource refactoring.
+ *
+ * An instance of this refactoring descriptor may be obtained by calling
+ * {@link RefactoringContribution#createDescriptor()} on a refactoring contribution requested by
+ * invoking {@link RefactoringCore#getRefactoringContribution(String)} with the refactoring id
+ * ({@link #ID}).
+ *
+ *
+ * Note: this class is not intended to be subclassed or instantiated by clients.
+ *
+ *
+ * @since 3.16
+ *
+ * @noinstantiate This class is not intended to be instantiated by clients.
+ */
+public final class CopyResourcesDescriptor extends RefactoringDescriptor {
+ /**
+ * Refactoring id of the 'Copy Resource' refactoring (value:
+ * org.eclipse.ltk.core.refactoring.copy.resources).
+ *
+ * Clients may safely cast the obtained refactoring descriptor to
+ * {@link CopyResourcesDescriptor}.
+ *
+ */
+ public static final String ID= "org.eclipse.ltk.core.refactoring.copy.resources"; //$NON-NLS-1$
+
+ private IPath[] resourcePaths;
+
+ private IPath[] destinationPaths;
+
+ /**
+ * Creates a new refactoring descriptor.
+ *
+ * Clients should not instantiated this class but use
+ * {@link RefactoringCore#getRefactoringContribution(String)} with {@link #ID} to get the
+ * contribution that can create the descriptor.
+ *
+ */
+ public CopyResourcesDescriptor() {
+ super(ID, null, RefactoringCoreMessages.RenameResourceDescriptor_unnamed_descriptor, null,
+ RefactoringDescriptor.STRUCTURAL_CHANGE | RefactoringDescriptor.MULTI_CHANGE);
+ }
+
+ public IPath[] getResourcePaths() {
+ return resourcePaths;
+ }
+
+ public IPath[] getDestinationPaths() {
+ return destinationPaths;
+ }
+
+ @Override
+ public Refactoring createRefactoring(RefactoringStatus status) throws CoreException {
+ IWorkspaceRoot wsRoot= ResourcesPlugin.getWorkspace().getRoot();
+ IResource[] resources= new IResource[resourcePaths.length];
+ for (int i= 0; i < resourcePaths.length; i++) {
+ IResource resource= wsRoot.findMember(resourcePaths[i]);
+ if (resource == null || !resource.exists()) {
+ status.addFatalError(MessageFormat.format(
+ RefactoringCoreMessages.CopyResourcesDescriptor_error_resource_not_exists,
+ BasicElementLabels.getPathLabel(resourcePaths[i], false)));
+ return null;
+ }
+ resources[i]= resource;
+ }
+ return new CopyRefactoring(new CopyResourcesProcessor(resources, destinationPaths));
+ }
+
+ public void setResourcePaths(IPath[] resourcePaths) {
+ Objects.requireNonNull(resourcePaths);
+ this.resourcePaths= resourcePaths;
+ }
+
+ public void setResources(IResource[] resources) {
+ Objects.requireNonNull(resources);
+ IPath[] paths= new IPath[resources.length];
+ for (int i= 0; i < paths.length; i++) {
+ paths[i]= resources[i].getFullPath();
+ }
+ setResourcePaths(paths);
+ }
+
+ public void setDestinationPaths(IPath[] destinationPaths) {
+ Objects.requireNonNull(destinationPaths);
+ this.destinationPaths= destinationPaths;
+ }
+}
diff --git a/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/internal/core/refactoring/RefactoringCoreMessages.java b/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/internal/core/refactoring/RefactoringCoreMessages.java
index 7af984b0cfc..83485f7b551 100644
--- a/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/internal/core/refactoring/RefactoringCoreMessages.java
+++ b/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/internal/core/refactoring/RefactoringCoreMessages.java
@@ -47,6 +47,24 @@ public final class RefactoringCoreMessages extends NLS {
public static String CopyProjectProcessor_name;
+ public static String CopyResourceChange_name;
+
+ public static String CopyResourceChange_undo_composite_name;
+
+ public static String CopyResourcesDescriptor_error_resource_not_exists;
+
+ public static String CopyResourcesProcessor_create_task;
+
+ public static String CopyResourcesProcessor_description_multiple;
+
+ public static String CopyResourcesProcessor_description_single;
+
+ public static String CopyResourcesProcessor_destination_inside_moved;
+
+ public static String CopyResourcesProcessor_error_multiple_destinatinos;
+
+ public static String CopyResourcesProcessor_name;
+
public static String CreateChangeOperation_unknown_Refactoring;
public static String DefaultRefactoringDescriptor_cannot_create_refactoring;
diff --git a/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/internal/core/refactoring/RefactoringCoreMessages.properties b/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/internal/core/refactoring/RefactoringCoreMessages.properties
index d44f72aa7a2..1015e9a449d 100644
--- a/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/internal/core/refactoring/RefactoringCoreMessages.properties
+++ b/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/internal/core/refactoring/RefactoringCoreMessages.properties
@@ -173,3 +173,13 @@ MoveResourceProcessor_destination_same_as_moved=The destination contains a resou
MoveResourceProcessor_error_destination_not_exists=Destination does not exist
MoveResourceProcessor_error_invalid_destination=Invalid parent
MoveResourceProcessor_processor_name=Move Resources
+
+CopyResourceChange_name=Copy resource ''{0}'' to ''{1}''
+CopyResourceChange_undo_composite_name=Delete copy and restore overwritten resource.
+CopyResourcesDescriptor_error_resource_not_exists=The resource ''{0}'' to copy does not exist.
+CopyResourcesProcessor_create_task=Creating pending copies...
+CopyResourcesProcessor_description_multiple=Copy {0} resources to ''{1}''
+CopyResourcesProcessor_description_single=Copy ''{0}'' to ''{1}''
+CopyResourcesProcessor_destination_inside_moved=Destination is inside copied resource ''{0}''
+CopyResourcesProcessor_error_multiple_destinatinos=Can only copy to a single destination.
+CopyResourcesProcessor_name=Copy Resources
diff --git a/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/internal/core/refactoring/resource/CopyResourcesProcessor.java b/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/internal/core/refactoring/resource/CopyResourcesProcessor.java
new file mode 100644
index 00000000000..8b843750a8f
--- /dev/null
+++ b/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/internal/core/refactoring/resource/CopyResourcesProcessor.java
@@ -0,0 +1,234 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Felix Schmid
+ *
+ * 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:
+ * Felix Schmid - initial API and implementation and/or initial documentation
+ *******************************************************************************/
+package org.eclipse.ltk.internal.core.refactoring.resource;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.core.runtime.Assert;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.OperationCanceledException;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.CompositeChange;
+import org.eclipse.ltk.core.refactoring.RefactoringChangeDescriptor;
+import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
+import org.eclipse.ltk.core.refactoring.RefactoringStatus;
+import org.eclipse.ltk.core.refactoring.participants.CheckConditionsContext;
+import org.eclipse.ltk.core.refactoring.participants.CopyArguments;
+import org.eclipse.ltk.core.refactoring.participants.CopyParticipant;
+import org.eclipse.ltk.core.refactoring.participants.CopyProcessor;
+import org.eclipse.ltk.core.refactoring.participants.ParticipantManager;
+import org.eclipse.ltk.core.refactoring.participants.RefactoringParticipant;
+import org.eclipse.ltk.core.refactoring.participants.ReorgExecutionLog;
+import org.eclipse.ltk.core.refactoring.participants.SharableParticipants;
+import org.eclipse.ltk.core.refactoring.resource.CopyResourceChange;
+import org.eclipse.ltk.core.refactoring.resource.CopyResourcesDescriptor;
+import org.eclipse.ltk.core.refactoring.resource.Resources;
+import org.eclipse.ltk.internal.core.refactoring.BasicElementLabels;
+import org.eclipse.ltk.internal.core.refactoring.RefactoringCoreMessages;
+
+/**
+ * A copy processor for {@link IResource resources}. The processor will copy the resources and load
+ * copy participants.
+ *
+ * @since 3.16
+ */
+public final class CopyResourcesProcessor extends CopyProcessor {
+
+ private final IResource[] resources;
+
+ private final IPath[] destinationPaths;
+
+ private IContainer destination;
+
+ private final ReorgExecutionLog log;
+
+ public CopyResourcesProcessor(final IResource[] resources, final IPath[] destinationPaths) {
+ Assert.isNotNull(resources);
+ Assert.isNotNull(destinationPaths);
+ Assert.isTrue(resources.length == destinationPaths.length);
+ this.resources= resources;
+ this.destinationPaths= destinationPaths;
+ log = new ReorgExecutionLog();
+ }
+
+ @Override
+ public RefactoringStatus checkInitialConditions(final IProgressMonitor pm)
+ throws CoreException, OperationCanceledException {
+ pm.beginTask("", resources.length); //$NON-NLS-1$
+ try {
+ RefactoringStatus status= new RefactoringStatus();
+ if (destinationPaths.length == 0) {
+ return status; // nothing to copy
+ }
+
+ // check for unsaved changes
+ status.merge(RefactoringStatus.create(Resources.checkInSync(resources)));
+
+ // check if destination containers are consistent for all copy locations
+ IPath destPath= destinationPaths[0].removeLastSegments(1);
+ destination= (IContainer) ResourcesPlugin.getWorkspace().getRoot().findMember(destPath);
+ if (!destinationValid(status)) {
+ return status;
+ }
+
+ for (int i= 0; i < resources.length; i++) {
+ pm.worked(1);
+ if (!destinationPaths[i].removeLastSegments(1).equals(destPath)) {
+ status.addFatalError(RefactoringCoreMessages.CopyResourcesProcessor_error_multiple_destinatinos);
+ break;
+ }
+
+ String destName= destinationPaths[i].lastSegment();
+ if (!resources[i].getName().equals(destName)) {
+ // copy should use different name then origin
+ log.setNewName(resources[i], destName);
+ }
+ }
+ return status;
+ } finally {
+ pm.done();
+ }
+ }
+
+ @Override
+ public RefactoringStatus checkFinalConditions(final IProgressMonitor pm, final CheckConditionsContext context)
+ throws CoreException, OperationCanceledException {
+ return new RefactoringStatus();
+ }
+
+ public boolean destinationValid(RefactoringStatus status) {
+ if (destination == null || !destination.exists()) {
+ status.addFatalError(RefactoringCoreMessages.MoveResourceProcessor_error_destination_not_exists);
+ return false;
+ }
+ if (destination instanceof IWorkspaceRoot) {
+ status.addFatalError(RefactoringCoreMessages.MoveResourceProcessor_error_invalid_destination);
+ return false;
+ }
+
+ IPath destinationPath= destination.getFullPath();
+ for (IResource r : resources) {
+ IPath path= r.getFullPath();
+ if (path.isPrefixOf(destinationPath) || path.equals(destinationPath)) {
+ status.addFatalError(MessageFormat.format(
+ RefactoringCoreMessages.CopyResourcesProcessor_destination_inside_moved,
+ BasicElementLabels.getPathLabel(path, false)));
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public Change createChange(final IProgressMonitor pm) throws CoreException, OperationCanceledException {
+ pm.beginTask(RefactoringCoreMessages.CopyResourcesProcessor_create_task, resources.length);
+ try {
+ final CompositeChange compChange= new CompositeChange(getDescription());
+ compChange.markAsSynthetic();
+ RefactoringChangeDescriptor descriptor= new RefactoringChangeDescriptor(createDescriptor());
+
+ for (IResource resource : resources) {
+ pm.worked(1);
+ CopyResourceChange copyChange= new CopyResourceChange(resource, log, destination);
+ copyChange.setDescriptor(descriptor);
+ compChange.add(copyChange);
+ }
+ return compChange;
+ } finally {
+ pm.done();
+ }
+ }
+
+ @Override
+ public RefactoringParticipant[] loadParticipants(final RefactoringStatus status,
+ final SharableParticipants sharedParticipants) throws CoreException {
+ final List result= new ArrayList<>();
+ final String[] affectedNatures= ResourceProcessors.computeAffectedNatures(resources);
+ final CopyArguments copyArguments= new CopyArguments(destination, log);
+
+ for (IResource resource : resources) {
+ final CopyParticipant[] participants= ParticipantManager.loadCopyParticipants(status, this, resource,
+ copyArguments, affectedNatures, sharedParticipants);
+ result.addAll(Arrays.asList(participants));
+ }
+ return result.toArray(new RefactoringParticipant[result.size()]);
+ }
+
+ @Override
+ public Object[] getElements() {
+ return resources;
+ }
+
+ @Override
+ public String getIdentifier() {
+ return "org.eclipse.ltk.core.refactoring.copyResourceProcessor"; //$NON-NLS-1$
+ }
+
+ @Override
+ public String getProcessorName() {
+ return RefactoringCoreMessages.CopyResourcesProcessor_name;
+ }
+
+ @Override
+ public boolean isApplicable() throws CoreException {
+ for (IResource r : resources) {
+ if (!canCopy(r)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static boolean canCopy(IResource res) {
+ return (res instanceof IFile || res instanceof IFolder) && res.exists();
+ }
+
+ protected CopyResourcesDescriptor createDescriptor() {
+ CopyResourcesDescriptor descriptor= new CopyResourcesDescriptor();
+ descriptor.setProject(null);
+ descriptor.setDescription(getDescription());
+ descriptor.setComment(descriptor.getDescription());
+ descriptor.setFlags(RefactoringDescriptor.STRUCTURAL_CHANGE |
+ RefactoringDescriptor.MULTI_CHANGE | RefactoringDescriptor.BREAKING_CHANGE);
+
+ descriptor.setResources(resources);
+ descriptor.setDestinationPaths(destinationPaths);
+ return descriptor;
+ }
+
+ private String getDescription() {
+ if (resources.length == 1) {
+ return MessageFormat.format(RefactoringCoreMessages.CopyResourcesProcessor_description_single,
+ BasicElementLabels.getResourceName(resources[0]),
+ BasicElementLabels.getResourceName(destination));
+ } else {
+ return MessageFormat.format(RefactoringCoreMessages.CopyResourcesProcessor_description_multiple,
+ resources.length,
+ BasicElementLabels.getResourceName(destination));
+ }
+ }
+}
diff --git a/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/internal/core/refactoring/resource/CopyResourcesRefactoringContribution.java b/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/internal/core/refactoring/resource/CopyResourcesRefactoringContribution.java
new file mode 100644
index 00000000000..04960e344a7
--- /dev/null
+++ b/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/internal/core/refactoring/resource/CopyResourcesRefactoringContribution.java
@@ -0,0 +1,122 @@
+/*******************************************************************************
+ * Copyright (c) 2007, 2026 IBM Corporation 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:
+ * IBM Corporation - initial API and implementation
+ * Felix Schmid - adapted for copy resource refactoring contribution
+ *******************************************************************************/
+package org.eclipse.ltk.internal.core.refactoring.resource;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.core.runtime.IPath;
+
+import org.eclipse.ltk.core.refactoring.RefactoringContribution;
+import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
+import org.eclipse.ltk.core.refactoring.resource.CopyResourcesDescriptor;
+
+/**
+ * Refactoring contribution for the copy resources refactoring.
+ *
+ * @since 3.16
+ */
+public class CopyResourcesRefactoringContribution extends RefactoringContribution {
+
+ /**
+ * Key used for the number of resource to be copied
+ */
+ private static final String ATTRIBUTE_NUMBER_OF_RESOURCES= "resources"; //$NON-NLS-1$
+
+ /**
+ * Key prefix used for the paths of the resources to be copied
+ *
+ * The element arguments are simply distinguished by appending a number to the argument name,
+ * e.g. element1. The indices of this argument are one-based.
+ *
+ */
+ private static final String ATTRIBUTE_ELEMENT= "element"; //$NON-NLS-1$
+
+ /**
+ * Key prefix used for the destination paths of the resources to be copied
+ *
+ * The element arguments are simply distinguished by appending a number to the argument name,
+ * e.g. element1. The indices of this argument are one-based.
+ *
+ */
+ private static final String ATTRIBUTE_DESTINATION= "destination"; //$NON-NLS-1$
+
+ @Override
+ public Map retrieveArgumentMap(RefactoringDescriptor descriptor) {
+ if (descriptor instanceof CopyResourcesDescriptor copyDesc) {
+ HashMap map= new HashMap<>();
+ String project= copyDesc.getProject();
+
+ IPath[] resourcePaths= copyDesc.getResourcePaths();
+ map.put(ATTRIBUTE_NUMBER_OF_RESOURCES, String.valueOf(resourcePaths.length));
+ storePaths(ATTRIBUTE_ELEMENT, resourcePaths, map, project);
+ storePaths(ATTRIBUTE_DESTINATION, copyDesc.getDestinationPaths(), map, project);
+ return map;
+ }
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public RefactoringDescriptor createDescriptor() {
+ return new CopyResourcesDescriptor();
+ }
+
+ @Override
+ public RefactoringDescriptor createDescriptor(String id, String project, String description, String comment,
+ Map arguments, int flags) throws IllegalArgumentException {
+ try {
+ int numResources= Integer.parseInt(arguments.get(ATTRIBUTE_NUMBER_OF_RESOURCES));
+ if (numResources < 0 || numResources > 100000) {
+ throw new IllegalArgumentException("Can not restore CopyResourceDescriptor from map, number of moved elements invalid"); //$NON-NLS-1$
+ }
+
+ IPath[] resourcePaths= loadPaths(ATTRIBUTE_ELEMENT, numResources, arguments, project);
+ IPath[] destinationPaths= loadPaths(ATTRIBUTE_DESTINATION, numResources, arguments, project);
+
+ if (resourcePaths.length > 0) {
+ CopyResourcesDescriptor descriptor= new CopyResourcesDescriptor();
+ descriptor.setProject(project);
+ descriptor.setDescription(description);
+ descriptor.setComment(comment);
+ descriptor.setFlags(flags);
+ descriptor.setResourcePaths(resourcePaths);
+ descriptor.setDestinationPaths(destinationPaths);
+ return descriptor;
+ }
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("Can not restore CopyResourceDescriptor from map"); //$NON-NLS-1$
+ }
+ throw new IllegalArgumentException("Can not restore CopyResourceDescriptor from map"); //$NON-NLS-1$
+ }
+
+ private void storePaths(String keyPrefix, IPath[] paths, Map arguments, String project) {
+ for (int i= 0; i < paths.length; i++) {
+ arguments.put(keyPrefix + (i + 1), ResourceProcessors.resourcePathToHandle(project, paths[i]));
+ }
+ }
+
+ private IPath[] loadPaths(String keyPrefix, int pathCount, Map arguments, String project) {
+ IPath[] paths= new IPath[pathCount];
+ for (int i= 0; i < pathCount; i++) {
+ String path= arguments.get(keyPrefix + String.valueOf(i + 1));
+ if (path == null) {
+ throw new IllegalArgumentException("Can not restore CopyResourceDescriptor from map, path missing"); //$NON-NLS-1$
+ }
+ paths[i]= ResourceProcessors.handleToResourcePath(project, path);
+ }
+ return paths;
+ }
+}
diff --git a/bundles/org.eclipse.ltk.ui.refactoring/META-INF/MANIFEST.MF b/bundles/org.eclipse.ltk.ui.refactoring/META-INF/MANIFEST.MF
index dea006509ad..ae7f9a413c0 100644
--- a/bundles/org.eclipse.ltk.ui.refactoring/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.ltk.ui.refactoring/META-INF/MANIFEST.MF
@@ -3,7 +3,7 @@ Automatic-Module-Name: org.eclipse.ltk.ui.refactoring
Bundle-ManifestVersion: 2
Bundle-Name: %pluginName
Bundle-SymbolicName: org.eclipse.ltk.ui.refactoring; singleton:=true
-Bundle-Version: 3.14.100.qualifier
+Bundle-Version: 3.14.200.qualifier
Bundle-Activator: org.eclipse.ltk.internal.ui.refactoring.RefactoringUIPlugin
Bundle-ActivationPolicy: lazy
Bundle-Vendor: %providerName
diff --git a/bundles/org.eclipse.ltk.ui.refactoring/plugin.properties b/bundles/org.eclipse.ltk.ui.refactoring/plugin.properties
index e9c2307299a..18cc431f4d3 100644
--- a/bundles/org.eclipse.ltk.ui.refactoring/plugin.properties
+++ b/bundles/org.eclipse.ltk.ui.refactoring/plugin.properties
@@ -20,6 +20,9 @@ changeViewerExtensionPoint= Refactoring Change Viewer
RefactoringPropertyPage_name=Refactoring History
RefactoringPropertyPage_keywords=refactoring history team comment share
refactoring.category=Refactoring
+copyResources.name=Copy Resources
+copyResources.description=Copy the selected resources and notify LTK participants.
+copyResources.commandParameter.destinationPaths=The destination paths of the copies.
deleteResources.name=Delete Resources
deleteResources.description=Delete the selected resources and notify LTK participants.
moveResources.name=Move Resources
diff --git a/bundles/org.eclipse.ltk.ui.refactoring/plugin.xml b/bundles/org.eclipse.ltk.ui.refactoring/plugin.xml
index f6de5371b47..7f2d14588a9 100644
--- a/bundles/org.eclipse.ltk.ui.refactoring/plugin.xml
+++ b/bundles/org.eclipse.ltk.ui.refactoring/plugin.xml
@@ -148,5 +148,17 @@
optional="false">
+
+
+
+
diff --git a/bundles/org.eclipse.ltk.ui.refactoring/src/org/eclipse/ltk/internal/ui/refactoring/RefactoringUIMessages.java b/bundles/org.eclipse.ltk.ui.refactoring/src/org/eclipse/ltk/internal/ui/refactoring/RefactoringUIMessages.java
index 8a503ded5ce..7bc95ff269c 100644
--- a/bundles/org.eclipse.ltk.ui.refactoring/src/org/eclipse/ltk/internal/ui/refactoring/RefactoringUIMessages.java
+++ b/bundles/org.eclipse.ltk.ui.refactoring/src/org/eclipse/ltk/internal/ui/refactoring/RefactoringUIMessages.java
@@ -58,6 +58,8 @@ public final class RefactoringUIMessages extends NLS {
public static String ComparePreviewer_refactored_source;
+ public static String CopyResourcesHandler_problem_occurred;
+
public static String DeleteResourcesHandler_title;
public static String DeleteResourcesWizard_label_multi;
diff --git a/bundles/org.eclipse.ltk.ui.refactoring/src/org/eclipse/ltk/internal/ui/refactoring/RefactoringUIMessages.properties b/bundles/org.eclipse.ltk.ui.refactoring/src/org/eclipse/ltk/internal/ui/refactoring/RefactoringUIMessages.properties
index 2cddb283aa3..2c155c8fca3 100644
--- a/bundles/org.eclipse.ltk.ui.refactoring/src/org/eclipse/ltk/internal/ui/refactoring/RefactoringUIMessages.properties
+++ b/bundles/org.eclipse.ltk.ui.refactoring/src/org/eclipse/ltk/internal/ui/refactoring/RefactoringUIMessages.properties
@@ -223,3 +223,5 @@ MoveResourcesWizard_description_single=&Choose destination for ''{0}'':
MoveResourcesWizard_error_no_selection=Select an resource.
MoveResourcesWizard_page_title=Move Resources
MoveResourcesWizard_window_title=Move Resources
+
+CopyResourcesHandler_problem_occurred=Problem Occurred
diff --git a/bundles/org.eclipse.ltk.ui.refactoring/src/org/eclipse/ltk/internal/ui/refactoring/actions/CopyResourcesHandler.java b/bundles/org.eclipse.ltk.ui.refactoring/src/org/eclipse/ltk/internal/ui/refactoring/actions/CopyResourcesHandler.java
new file mode 100644
index 00000000000..aedc68bbe04
--- /dev/null
+++ b/bundles/org.eclipse.ltk.ui.refactoring/src/org/eclipse/ltk/internal/ui/refactoring/actions/CopyResourcesHandler.java
@@ -0,0 +1,86 @@
+/*******************************************************************************
+ * Copyright (c) 2024, 2026 Vector Informatik GmbH 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:
+ * Vector Informatik GmbH - initial implementation
+ * Felix Schmid - adapted for copy resource handler
+ *******************************************************************************/
+package org.eclipse.ltk.internal.ui.refactoring.actions;
+
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.NullProgressMonitor;
+
+import org.eclipse.core.resources.IResource;
+
+import org.eclipse.jface.dialogs.ErrorDialog;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+
+import org.eclipse.ui.handlers.HandlerUtil;
+
+import org.eclipse.ltk.core.refactoring.CheckConditionsOperation;
+import org.eclipse.ltk.core.refactoring.CreateChangeOperation;
+import org.eclipse.ltk.core.refactoring.PerformChangeOperation;
+import org.eclipse.ltk.core.refactoring.RefactoringCore;
+import org.eclipse.ltk.core.refactoring.RefactoringStatus;
+import org.eclipse.ltk.core.refactoring.participants.CopyRefactoring;
+import org.eclipse.ltk.internal.core.refactoring.resource.CopyResourcesProcessor;
+import org.eclipse.ltk.internal.ui.refactoring.RefactoringUIMessages;
+import org.eclipse.ltk.ui.refactoring.RefactoringUI;
+
+public class CopyResourcesHandler extends AbstractResourcesHandler {
+
+ private static final String LTK_COPY_RESOURCE_COMMAND_DESTINATION_KEY = "org.eclipse.ltk.ui.refactoring.commands.copyResources.destinationPaths.parameter.key"; //$NON-NLS-1$
+
+ @Override
+ public Object execute(ExecutionEvent event) throws ExecutionException {
+ ISelection sel= HandlerUtil.getCurrentSelection(event);
+ Object dest= HandlerUtil.getVariable(event, LTK_COPY_RESOURCE_COMMAND_DESTINATION_KEY);
+ Shell shell= HandlerUtil.getActiveShell(event);
+
+ if (sel instanceof IStructuredSelection selection && dest instanceof IPath[] destPaths) {
+ IResource[] resources= getSelectedResources(selection);
+ CopyRefactoring copyRefactoring= new CopyRefactoring(new CopyResourcesProcessor(resources, destPaths));
+ try {
+ CreateChangeOperation create= new CreateChangeOperation(
+ new CheckConditionsOperation(copyRefactoring, CheckConditionsOperation.ALL_CONDITIONS),
+ RefactoringStatus.FATAL);
+
+ PerformChangeOperation perform= new PerformChangeOperation(create);
+ perform.setUndoManager(RefactoringCore.getUndoManager(), copyRefactoring.getName());
+ perform.run(new NullProgressMonitor());
+
+ if (perform.getConditionCheckingStatus().getSeverity() >= RefactoringStatus.WARNING) {
+ openErrorDialog(shell, perform.getConditionCheckingStatus());
+ }
+ } catch (CoreException e) {
+ openErrorDialog(shell, e.getStatus());
+ }
+ }
+ return null;
+ }
+
+ private void openErrorDialog(Shell shell, RefactoringStatus status) {
+ Display.getDefault().asyncExec(() -> RefactoringUI.createLightWeightStatusDialog(status, shell,
+ RefactoringUIMessages.CopyResourcesHandler_problem_occurred).open());
+ }
+
+ private void openErrorDialog(Shell shell, IStatus status) {
+ Display.getDefault().asyncExec(() -> ErrorDialog.openError(shell, null, null, status));
+ }
+}
diff --git a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CopyFilesAndFoldersOperation.java b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CopyFilesAndFoldersOperation.java
index 82504e776a5..ec6bb904f44 100644
--- a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CopyFilesAndFoldersOperation.java
+++ b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CopyFilesAndFoldersOperation.java
@@ -77,6 +77,7 @@
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.actions.LTKLauncher;
import org.eclipse.ui.internal.ide.dialogs.IDEResourceInfoUtils;
import org.eclipse.ui.statushandlers.StatusManager;
import org.eclipse.ui.wizards.datatransfer.FileStoreStructureProvider;
@@ -1241,6 +1242,14 @@ boolean isDestinationSameAsSource(IResource[] sourceResources,
private boolean performCopy(IResource[] resources, IPath destination,
IProgressMonitor monitor) {
try {
+ IPath[] destPaths = new IPath[resources.length];
+ for (int i = 0; i < resources.length; i++) {
+ destPaths[i] = destination.append(resources[i].getName());
+ }
+ if (LTKLauncher.copyResources(resources, destPaths)) {
+ return true;
+ }
+
AbstractWorkspaceOperation op = getUndoableCopyOrMoveOperation(
resources, destination);
op.setModelProviderIds(getModelProviderIds());
@@ -1301,6 +1310,11 @@ private boolean performCopyWithAutoRename(IResource[] resources,
workspace);
}
}
+
+ if (LTKLauncher.copyResources(resources, destinationPaths)) {
+ return true;
+ }
+
CopyResourcesOperation op = new CopyResourcesOperation(resources,
destinationPaths,
IDEWorkbenchMessages.CopyFilesAndFoldersOperation_copyTitle);
diff --git a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/ide/undo/CopyResourcesOperation.java b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/ide/undo/CopyResourcesOperation.java
index 9c82d6b85e1..8aeac8df1cd 100644
--- a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/ide/undo/CopyResourcesOperation.java
+++ b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/ide/undo/CopyResourcesOperation.java
@@ -145,7 +145,6 @@ protected void doExecute(IProgressMonitor monitor, IAdaptable uiInfo)
*/
protected void copy(IProgressMonitor monitor, IAdaptable uiInfo)
throws CoreException {
-
SubMonitor subMonitor = SubMonitor.convert(monitor,
resources.length + (resourceDescriptions != null ? resourceDescriptions.length : 0));
subMonitor.setTaskName(UndoMessages.AbstractResourcesOperation_CopyingResourcesProgress);
diff --git a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/actions/LTKLauncher.java b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/actions/LTKLauncher.java
index 99f18dd0c94..39369a5da23 100644
--- a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/actions/LTKLauncher.java
+++ b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/actions/LTKLauncher.java
@@ -28,6 +28,7 @@
import org.eclipse.core.commands.common.NotDefinedException;
import org.eclipse.core.expressions.EvaluationContext;
import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.IPath;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StructuredSelection;
@@ -50,6 +51,8 @@ public class LTKLauncher {
private static final String LTK_COPY_PROJECT_ID = "org.eclipse.ltk.ui.refactoring.commands.copyProject"; //$NON-NLS-1$
private static final String LTK_COPY_PROJECT_COMMAND_NEWNAME_KEY = "org.eclipse.ltk.ui.refactoring.commands.copyProject.newName.parameter.key"; //$NON-NLS-1$
private static final String LTK_COPY_PROJECT_COMMAND_NEWLOCATION_KEY = "org.eclipse.ltk.ui.refactoring.commands.copyProject.newLocation.parameter.key"; //$NON-NLS-1$
+ private static final String LTK_COPY_RESOURCES_ID = "org.eclipse.ltk.ui.refactoring.commands.copyResources"; //$NON-NLS-1$
+ private static final String LTK_COPY_RESOURCES_COMMAND_DESTINATION_KEY = "org.eclipse.ltk.ui.refactoring.commands.copyResources.destinationPaths.parameter.key"; //$NON-NLS-1$
/**
* Open the LTK delete resources wizard if available.
@@ -127,6 +130,12 @@ public static boolean copyProject(IProject project, String newName, IPath newLoc
return runCommand(LTK_COPY_PROJECT_ID, new StructuredSelection(project), commandParameters);
}
+ public static boolean copyResources(IResource[] resources, IPath[] destinationPaths) {
+ Map commandParameters = new HashMap<>();
+ commandParameters.put(LTK_COPY_RESOURCES_COMMAND_DESTINATION_KEY, destinationPaths);
+ return runCommand(LTK_COPY_RESOURCES_ID, new StructuredSelection(resources), commandParameters);
+ }
+
private static boolean runCommand(String commandId, IStructuredSelection selection,
Map commandParameters) {
diff --git a/tests/org.eclipse.ltk.core.refactoring.tests/src/org/eclipse/ltk/core/refactoring/tests/participants/CopyRefactoringWithRefUpdateTest.java b/tests/org.eclipse.ltk.core.refactoring.tests/src/org/eclipse/ltk/core/refactoring/tests/participants/CopyRefactoringWithRefUpdateTest.java
new file mode 100644
index 00000000000..7f527441973
--- /dev/null
+++ b/tests/org.eclipse.ltk.core.refactoring.tests/src/org/eclipse/ltk/core/refactoring/tests/participants/CopyRefactoringWithRefUpdateTest.java
@@ -0,0 +1,220 @@
+/*******************************************************************************
+ * Copyright (c) 2024, 2026 Advantest Europe GmbH 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:
+ * Raghunandana Murthappa
+ * Felix Schmid - adapted for copy refactoring test
+ *******************************************************************************/
+package org.eclipse.ltk.core.refactoring.tests.participants;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.Test;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.OperationCanceledException;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.ResourcesPlugin;
+
+import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.text.edits.ReplaceEdit;
+
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.CheckConditionsOperation;
+import org.eclipse.ltk.core.refactoring.PerformRefactoringOperation;
+import org.eclipse.ltk.core.refactoring.RefactoringStatus;
+import org.eclipse.ltk.core.refactoring.TextFileChange;
+import org.eclipse.ltk.core.refactoring.participants.CheckConditionsContext;
+import org.eclipse.ltk.core.refactoring.participants.CopyArguments;
+import org.eclipse.ltk.core.refactoring.participants.CopyParticipant;
+import org.eclipse.ltk.core.refactoring.participants.CopyProcessor;
+import org.eclipse.ltk.core.refactoring.participants.CopyRefactoring;
+import org.eclipse.ltk.core.refactoring.participants.RefactoringParticipant;
+import org.eclipse.ltk.core.refactoring.participants.ReorgExecutionLog;
+import org.eclipse.ltk.core.refactoring.participants.SharableParticipants;
+import org.eclipse.ltk.core.refactoring.resource.CopyResourceChange;
+import org.eclipse.ltk.core.refactoring.tests.util.SimpleTestProject;
+
+class CopyRefactoringWithRefUpdateTest {
+
+ private SimpleTestProject project;
+
+ private static class RefUpdateParticipant extends CopyParticipant {
+ private IFile file;
+ private IContainer destination;
+
+ @Override
+ protected boolean initialize(Object element) {
+ file= (IFile) element;
+ destination= (IContainer) getArguments().getDestination();
+ return true;
+ }
+
+ @Override
+ public String getName() {
+ return "copy participant";
+ }
+
+ @Override
+ public RefactoringStatus checkConditions(IProgressMonitor pm, CheckConditionsContext context) throws OperationCanceledException {
+ return new RefactoringStatus();
+ }
+
+ @Override
+ public Change createChange(IProgressMonitor pm) throws CoreException, OperationCanceledException {
+ IPath path = destination.getFullPath().append(file.getName());
+ return new CopyFileChange(path);
+ }
+ }
+
+ private static class CopyFileChange extends Change {
+
+ private final IPath newFile;
+
+ protected CopyFileChange(final IPath newFile) {
+ this.newFile = newFile;
+ }
+
+ @Override
+ public void initializeValidationData(final IProgressMonitor pm) {
+ // nothing to do
+ }
+
+ @Override
+ public RefactoringStatus isValid(final IProgressMonitor pm) throws CoreException, OperationCanceledException {
+ return new RefactoringStatus();
+ }
+
+ @Override
+ public String getName() {
+ return "copy file change";
+ }
+
+ @Override
+ public Change perform(final IProgressMonitor pm) throws CoreException {
+ IFile file = (IFile) ResourcesPlugin.getWorkspace().getRoot().findMember(newFile);
+
+ TextFileChange result= new TextFileChange("", file);
+ MultiTextEdit root= new MultiTextEdit();
+ root.addChild(new ReplaceEdit(9, 12, "destFolder"));
+ result.setEdit(root);
+ result.perform(pm);
+
+ return null; // no undo change necessary, the element will be deleted
+ }
+
+ @Override
+ public IResource getModifiedElement() {
+ return null; // not needed for test
+ }
+
+ @Override
+ public Object[] getAffectedObjects() {
+ return null; // not needed for test
+ }
+ }
+
+ private static class TestCopyProcessor extends CopyProcessor {
+
+ private IFile origin;
+ private IFolder destination;
+ private ReorgExecutionLog log;
+
+ public TestCopyProcessor(IFile origin, IFolder destination) {
+ this.origin = origin;
+ this.destination = destination;
+ log = new ReorgExecutionLog();
+ }
+
+ @Override
+ public Object[] getElements() {
+ return new Object[] { origin };
+ }
+
+ @Override
+ public String getIdentifier() {
+ return "org.eclipse.ltk.core.refactoring.tests.CopyProcessor";
+ }
+
+ @Override
+ public String getProcessorName() {
+ return "copy processor";
+ }
+
+ @Override
+ public boolean isApplicable() throws CoreException {
+ return true;
+ }
+
+ @Override
+ public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException, OperationCanceledException {
+ return new RefactoringStatus();
+ }
+
+ @Override
+ public RefactoringStatus checkFinalConditions(IProgressMonitor pm, CheckConditionsContext context) throws CoreException, OperationCanceledException {
+ return new RefactoringStatus();
+ }
+
+ @Override
+ public Change createChange(IProgressMonitor pm) throws CoreException, OperationCanceledException {
+ return new CopyResourceChange(origin, log, destination);
+ }
+
+ @Override
+ public RefactoringParticipant[] loadParticipants(RefactoringStatus status, SharableParticipants sharedParticipants) throws CoreException {
+ RefUpdateParticipant participant= new RefUpdateParticipant();
+ participant.initialize(this, origin, new CopyArguments(destination, log));
+ return new RefactoringParticipant[] { participant };
+ }
+ }
+
+ @BeforeEach
+ void setUp() throws Exception {
+ project= new SimpleTestProject();
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ project.delete();
+ }
+
+ @Test
+ void testCopyRefactoringWithParticipants() throws Exception {
+ IFolder srcFold= project.createFolder("originFolder");
+ IFolder destination= project.createFolder("destFolder");
+ // the copy should specify the package as "destFolder", while the origin says "originFolder"
+ IFile origin= project.createFile(srcFold, "testFile.txt", "package: originFolder");
+
+ CopyRefactoring refactoring= new CopyRefactoring(new TestCopyProcessor(origin, destination));
+ PerformRefactoringOperation op= new PerformRefactoringOperation(refactoring, CheckConditionsOperation.ALL_CONDITIONS);
+ ResourcesPlugin.getWorkspace().run(op, null);
+
+ IFile copy = this.project.getProject().getFolder("destFolder").getFile("testFile.txt");
+ assertTrue(copy.exists(), "File is not copied");
+
+ // original file was not changed
+ String originContent= project.getContent(origin);
+ assertEquals("package: originFolder", originContent);
+
+ // package of copy was changed
+ String copyContent = project.getContent(copy);
+ assertEquals("package: destFolder", copyContent);
+ }
+}
diff --git a/tests/org.eclipse.ltk.core.refactoring.tests/src/org/eclipse/ltk/core/refactoring/tests/resource/ResourceRefactoringTests.java b/tests/org.eclipse.ltk.core.refactoring.tests/src/org/eclipse/ltk/core/refactoring/tests/resource/ResourceRefactoringTests.java
index 8b075f415a7..dc8a578209f 100644
--- a/tests/org.eclipse.ltk.core.refactoring.tests/src/org/eclipse/ltk/core/refactoring/tests/resource/ResourceRefactoringTests.java
+++ b/tests/org.eclipse.ltk.core.refactoring.tests/src/org/eclipse/ltk/core/refactoring/tests/resource/ResourceRefactoringTests.java
@@ -49,7 +49,10 @@
import org.eclipse.ltk.core.refactoring.RefactoringCore;
import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
+import org.eclipse.ltk.core.refactoring.participants.ReorgExecutionLog;
import org.eclipse.ltk.core.refactoring.resource.CopyProjectDescriptor;
+import org.eclipse.ltk.core.refactoring.resource.CopyResourceChange;
+import org.eclipse.ltk.core.refactoring.resource.CopyResourcesDescriptor;
import org.eclipse.ltk.core.refactoring.resource.DeleteResourcesDescriptor;
import org.eclipse.ltk.core.refactoring.resource.MoveRenameResourceDescriptor;
import org.eclipse.ltk.core.refactoring.resource.MoveResourceChange;
@@ -299,6 +302,225 @@ public void testMoveRenameRefactoring3() throws Exception {
assertEquals(content2, fProject.getContent(file2));
}
+ @Test
+ void testCopyChangeFile() throws Exception {
+ String content= "hello";
+
+ IFolder testFolder= fProject.createFolder("test");
+ IFile file= fProject.createFile(testFolder, "myFile.txt", content);
+ IFolder destination= fProject.createFolder("dest");
+ ReorgExecutionLog log = new ReorgExecutionLog();
+
+ Change undoChange= perform(new CopyResourceChange(file, log, destination));
+
+ IResource copiedResource= assertCopy(file, log, destination);
+
+ perform(undoChange);
+
+ assertFalse(copiedResource.exists());
+ }
+
+ @Test
+ void testCopyChangeFolder() throws Exception {
+ String content= "hello";
+
+ IFolder testFolder= fProject.createFolder("test");
+ fProject.createFile(testFolder, "myFile.txt", content);
+ IFolder destination= fProject.createFolder("dest");
+ ReorgExecutionLog log = new ReorgExecutionLog();
+
+ Change undoChange= perform(new CopyResourceChange(testFolder, log, destination));
+
+ IFolder copiedResource= (IFolder) assertCopy(testFolder, log, destination);
+ assertTrue(copiedResource.getFile("myFile.txt").exists());
+
+ perform(undoChange);
+
+ assertFalse(copiedResource.exists());
+ assertTrue(testFolder.getFile("myFile.txt").exists());
+ }
+
+ @Test
+ void testCopyChangeOverwrite() throws Exception {
+ String content1= "hello";
+ String content2= "world";
+
+ IFolder testFolder= fProject.createFolder("test");
+ IFile file1= fProject.createFile(testFolder, "myFile.txt", content1);
+
+ IFolder destination= fProject.createFolder("dest");
+ IFile file2= fProject.createFile(destination, "myFile.txt", content2);
+
+ ReorgExecutionLog log = new ReorgExecutionLog();
+
+ Change undoChange= perform(new CopyResourceChange(file1, log, destination));
+
+ assertCopy(file1, log, destination);
+
+ perform(undoChange);
+
+ assertEquals(content2, fProject.getContent(file2));
+ }
+
+ @Test
+ void testCopyRefactoringFile() throws Exception {
+ String content= "hello";
+
+ IFolder testFolder= fProject.createFolder("test");
+ IFile file= fProject.createFile(testFolder, "myFile.txt", content);
+
+ IFolder destination= fProject.createFolder("dest");
+
+ RefactoringContribution contribution= RefactoringCore.getRefactoringContribution(CopyResourcesDescriptor.ID);
+ CopyResourcesDescriptor descriptor= (CopyResourcesDescriptor) contribution.createDescriptor();
+ ReorgExecutionLog log = new ReorgExecutionLog();
+
+ descriptor.setResources(new IResource[] { file });
+ descriptor.setDestinationPaths(new IPath[] { destination.getFullPath().append("myFile.txt") });
+
+ Change undoChange= perform(descriptor);
+
+ IResource copiedResource= assertCopy(file, log, destination);
+
+ perform(undoChange);
+
+ assertFalse(copiedResource.exists());
+ }
+
+ @Test
+ void testCopyRefactoringFolder() throws Exception {
+ String content= "hello";
+
+ IFolder testFolder= fProject.createFolder("test");
+ fProject.createFile(testFolder, "myFile.txt", content);
+ IFolder destination= fProject.createFolder("dest");
+
+ RefactoringContribution contribution= RefactoringCore.getRefactoringContribution(CopyResourcesDescriptor.ID);
+ CopyResourcesDescriptor descriptor= (CopyResourcesDescriptor) contribution.createDescriptor();
+ ReorgExecutionLog log = new ReorgExecutionLog();
+
+ descriptor.setResources(new IResource[] { testFolder });
+ descriptor.setDestinationPaths(new IPath[] { destination.getFullPath().append("test") });
+
+ Change undoChange= perform(descriptor);
+
+ IFolder copiedResource= (IFolder) assertCopy(testFolder, log, destination);
+ assertTrue(copiedResource.getFile("myFile.txt").exists());
+
+ perform(undoChange);
+
+ assertFalse(copiedResource.exists());
+ assertTrue(testFolder.getFile("myFile.txt").exists());
+ }
+
+ @Test
+ void testCopyRefactoringOverwrite() throws Exception {
+ String content1= "hello";
+ String content2= "world";
+
+ IFolder testFolder= fProject.createFolder("test");
+ IFile file1= fProject.createFile(testFolder, "myFile.txt", content1);
+
+ IFolder destination= fProject.createFolder("dest");
+ IFile file2= fProject.createFile(destination, "myFile.txt", content2);
+
+ RefactoringContribution contribution= RefactoringCore.getRefactoringContribution(CopyResourcesDescriptor.ID);
+ CopyResourcesDescriptor descriptor= (CopyResourcesDescriptor) contribution.createDescriptor();
+ ReorgExecutionLog log = new ReorgExecutionLog();
+
+ descriptor.setResources(new IResource[] { file1 });
+ descriptor.setDestinationPaths(new IPath[] { destination.getFullPath().append("myFile.txt") });
+
+ Change undoChange= perform(descriptor);
+
+ assertCopy(file1, log, destination);
+
+ perform(undoChange);
+
+ assertEquals(content2, fProject.getContent(file2));
+ }
+
+ @Test
+ void testCopyRenameRefactoringFile() throws Exception {
+ String content= "hello";
+
+ IFolder testFolder= fProject.createFolder("test");
+ IFile file= fProject.createFile(testFolder, "myFile.txt", content);
+
+ IFolder destination= fProject.createFolder("dest");
+
+ RefactoringContribution contribution= RefactoringCore.getRefactoringContribution(CopyResourcesDescriptor.ID);
+ CopyResourcesDescriptor descriptor= (CopyResourcesDescriptor) contribution.createDescriptor();
+ ReorgExecutionLog log = new ReorgExecutionLog();
+ log.setNewName(file, "newNameFile.txt");
+
+ descriptor.setResources(new IResource[] { file });
+ descriptor.setDestinationPaths(new IPath[] { destination.getFullPath().append("newNameFile.txt") });
+
+ Change undoChange= perform(descriptor);
+
+ IResource copiedResource= assertCopy(file, log, destination);
+
+ perform(undoChange);
+
+ assertFalse(copiedResource.exists());
+ }
+
+ @Test
+ void testCopyRenameRefactoringFolder() throws Exception {
+ String content= "hello";
+
+ IFolder testFolder= fProject.createFolder("test");
+ fProject.createFile(testFolder, "myFile.txt", content);
+ IFolder destination= fProject.createFolder("dest");
+
+ RefactoringContribution contribution= RefactoringCore.getRefactoringContribution(CopyResourcesDescriptor.ID);
+ CopyResourcesDescriptor descriptor= (CopyResourcesDescriptor) contribution.createDescriptor();
+ ReorgExecutionLog log = new ReorgExecutionLog();
+ log.setNewName(testFolder, "newNameFolder");
+
+ descriptor.setResources(new IResource[] { testFolder });
+ descriptor.setDestinationPaths(new IPath[] { destination.getFullPath().append("newNameFolder") });
+
+ Change undoChange= perform(descriptor);
+
+ IFolder copiedResource= (IFolder) assertCopy(testFolder, log, destination);
+ assertTrue(copiedResource.getFile("myFile.txt").exists());
+
+ perform(undoChange);
+
+ assertFalse(copiedResource.exists());
+ assertTrue(testFolder.getFile("myFile.txt").exists());
+ }
+
+ @Test
+ void testCopyRenameRefactoringOverwrite() throws Exception {
+ String content1= "hello";
+ String content2= "world";
+
+ IFolder testFolder= fProject.createFolder("test");
+ IFile file1= fProject.createFile(testFolder, "myFile.txt", content1);
+
+ IFolder destination= fProject.createFolder("dest");
+ IFile file2= fProject.createFile(destination, "myFile2.txt", content2);
+
+ RefactoringContribution contribution= RefactoringCore.getRefactoringContribution(CopyResourcesDescriptor.ID);
+ CopyResourcesDescriptor descriptor= (CopyResourcesDescriptor) contribution.createDescriptor();
+ ReorgExecutionLog log = new ReorgExecutionLog();
+ log.setNewName(file1, "myFile2.txt");
+
+ descriptor.setResources(new IResource[] { file1 });
+ descriptor.setDestinationPaths(new IPath[] { destination.getFullPath().append("myFile2.txt") });
+
+ Change undoChange= perform(descriptor);
+
+ assertCopy(file1, log, destination);
+
+ perform(undoChange);
+
+ assertEquals(content2, fProject.getContent(file2));
+ }
+
@Test
public void testDeleteRefactoring1_bug343584() throws Exception {
IFolder testFolder= fProject.createFolder("test");
@@ -457,6 +679,24 @@ private IResource assertMove(IResource source, IContainer destination, String co
return res;
}
+ private IResource assertCopy(IResource source, ReorgExecutionLog log, IContainer destination) throws CoreException, IOException {
+ String newName = log.getNewName(source);
+ if (newName == null) {
+ newName = source.getName();
+ }
+
+ IResource res= destination.findMember(newName);
+
+ assertTrue(source.exists());
+ assertNotNull(res);
+ assertEquals(res.getType(), source.getType());
+
+ if (source instanceof IFile file) {
+ assertEquals(fProject.getContent(file), fProject.getContent((IFile) res));
+ }
+ return res;
+ }
+
private IResource assertMoveRename(IResource source, IContainer destination, String newName, String content) throws CoreException, IOException {
IResource res= destination.findMember(newName);