From e77ea7bd0c1c9b20fcd372d8668ffacf0866309f Mon Sep 17 00:00:00 2001 From: Felix Schmid <50250674+Felix-Schmid@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:05:02 +0200 Subject: [PATCH] Add copy refactoring infrastructure to LTK Version bump(s) for 4.41 stream Add "since" to javadoc of new APIs Fix copyright header year Fix bundle version Improve entry location for LTK copy refactoring Add tests for LTK copy refactoring Fix --- .../META-INF/MANIFEST.MF | 2 +- .../plugin.xml | 3 + .../resource/CopyResourceChange.java | 156 ++++++++++++ .../resource/CopyResourcesDescriptor.java | 124 +++++++++ .../refactoring/RefactoringCoreMessages.java | 18 ++ .../RefactoringCoreMessages.properties | 10 + .../resource/CopyResourcesProcessor.java | 234 +++++++++++++++++ .../CopyResourcesRefactoringContribution.java | 122 +++++++++ .../META-INF/MANIFEST.MF | 2 +- .../plugin.properties | 3 + .../org.eclipse.ltk.ui.refactoring/plugin.xml | 12 + .../ui/refactoring/RefactoringUIMessages.java | 2 + .../RefactoringUIMessages.properties | 2 + .../actions/CopyResourcesHandler.java | 86 +++++++ .../actions/CopyFilesAndFoldersOperation.java | 14 + .../ui/ide/undo/CopyResourcesOperation.java | 1 - .../ui/internal/ide/actions/LTKLauncher.java | 9 + .../CopyRefactoringWithRefUpdateTest.java | 220 ++++++++++++++++ .../resource/ResourceRefactoringTests.java | 240 ++++++++++++++++++ 19 files changed, 1257 insertions(+), 3 deletions(-) create mode 100644 bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/core/refactoring/resource/CopyResourceChange.java create mode 100644 bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/core/refactoring/resource/CopyResourcesDescriptor.java create mode 100644 bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/internal/core/refactoring/resource/CopyResourcesProcessor.java create mode 100644 bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/internal/core/refactoring/resource/CopyResourcesRefactoringContribution.java create mode 100644 bundles/org.eclipse.ltk.ui.refactoring/src/org/eclipse/ltk/internal/ui/refactoring/actions/CopyResourcesHandler.java create mode 100644 tests/org.eclipse.ltk.core.refactoring.tests/src/org/eclipse/ltk/core/refactoring/tests/participants/CopyRefactoringWithRefUpdateTest.java 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);