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 3d9e6012b46..efd1ad715c4 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.14.600.qualifier
+Bundle-Version: 3.15.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 cf650f00d49..d59248c24ca 100644
--- a/bundles/org.eclipse.ltk.core.refactoring/plugin.xml
+++ b/bundles/org.eclipse.ltk.core.refactoring/plugin.xml
@@ -48,5 +48,9 @@
+
+
\ No newline at end of file
diff --git a/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/core/refactoring/resource/CopyProjectChange.java b/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/core/refactoring/resource/CopyProjectChange.java
new file mode 100644
index 00000000000..841e8a563e7
--- /dev/null
+++ b/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/core/refactoring/resource/CopyProjectChange.java
@@ -0,0 +1,153 @@
+/*******************************************************************************
+ * Copyright (c) 2024 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
+ *******************************************************************************/
+package org.eclipse.ltk.core.refactoring.resource;
+
+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.IStatus;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.core.runtime.Platform;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.SubMonitor;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IProjectDescription;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceVisitor;
+
+import org.eclipse.core.filebuffers.FileBuffers;
+import org.eclipse.core.filebuffers.ITextFileBuffer;
+import org.eclipse.core.filebuffers.LocationKind;
+
+import org.eclipse.ltk.core.refactoring.Change;
+import org.eclipse.ltk.core.refactoring.ChangeDescriptor;
+import org.eclipse.ltk.internal.core.refactoring.BasicElementLabels;
+import org.eclipse.ltk.internal.core.refactoring.Messages;
+import org.eclipse.ltk.internal.core.refactoring.RefactoringCoreMessages;
+import org.eclipse.ltk.internal.core.refactoring.RefactoringCorePlugin;
+
+/**
+ * {@link Change} that copies a project
+ *
+ * @since 3.15
+ */
+public class CopyProjectChange extends ResourceChange {
+
+ private final IProject fSourceProject;
+
+ private ChangeDescriptor fDescriptor;
+
+ private String fNewName;
+
+ private IPath fNewLocation;
+
+ /**
+ * Copy a project.
+ *
+ * @param resourcePath the project path
+ * @param newLocation location of the new project
+ * @param newName name of the new project
+ */
+ public CopyProjectChange(IProject resourcePath, IPath newLocation, String newName) {
+ Assert.isNotNull(resourcePath);
+ fNewName= newName;
+ fNewLocation= newLocation;
+ fSourceProject= resourcePath;
+ setValidationMethod(SAVE_IF_DIRTY);
+ }
+
+ @Override
+ protected IResource getModifiedResource() {
+ return fSourceProject;
+ }
+
+
+ @Override
+ public String getName() {
+ return RefactoringCoreMessages.CopyProjectChange_Name + fSourceProject.getName();
+ }
+
+ @Override
+ public Change perform(IProgressMonitor pm) throws CoreException {
+ SubMonitor subMonitor= SubMonitor.convert(pm, RefactoringCoreMessages.CopyProjectChange_copying, 10);
+
+ if (fSourceProject == null || !fSourceProject.exists()) {
+ String message= Messages.format(RefactoringCoreMessages.CopyProjectChange_error_resource_not_exists,
+ BasicElementLabels.getPathLabel(fSourceProject.getFullPath().makeRelative(), false));
+ throw new CoreException(new Status(IStatus.ERROR, RefactoringCorePlugin.getPluginId(), message));
+ }
+
+ // make sure all files inside the resource are saved
+ if (fSourceProject.isAccessible()) {
+ fSourceProject.accept((IResourceVisitor) curr -> {
+ try {
+ if (curr instanceof IFile) {
+ // progress is covered outside.
+ saveFileIfNeeded((IFile) curr, new NullProgressMonitor());
+ }
+ } catch (CoreException e) {
+ // ignore
+ }
+ return true;
+ }, IResource.DEPTH_INFINITE, false);
+ }
+
+ IProjectDescription description= fSourceProject.getDescription();
+
+ if (fNewLocation != null && (fNewLocation.equals(Platform.getLocation()) || fNewLocation.isRoot())) {
+ fNewLocation= null;
+ }
+
+ description.setName(fNewName);
+ description.setLocation(fNewLocation);
+
+ fSourceProject.copy(description, IResource.FORCE | IResource.SHALLOW, subMonitor.newChild(10));
+
+ IProject targetProject= fSourceProject.getWorkspace().getRoot().getProject(fNewName);
+
+ return new DeleteResourceChange(targetProject.getFullPath(), true, true);
+
+ }
+
+ private static void saveFileIfNeeded(IFile file, IProgressMonitor pm) throws CoreException {
+ ITextFileBuffer buffer= FileBuffers.getTextFileBufferManager().getTextFileBuffer(file.getFullPath(), LocationKind.IFILE);
+ SubMonitor subMonitor= SubMonitor.convert(pm, 2);
+ if (buffer != null && buffer.isDirty() && buffer.isStateValidated() && buffer.isSynchronized()) {
+ buffer.commit(subMonitor.newChild(1), false);
+ file.refreshLocal(IResource.DEPTH_ONE, subMonitor.newChild(1));
+ buffer.commit(subMonitor.newChild(1), false);
+ file.refreshLocal(IResource.DEPTH_ONE, subMonitor.newChild(1));
+ } else {
+ subMonitor.worked(2);
+ }
+ }
+
+ @Override
+ public ChangeDescriptor getDescriptor() {
+ return fDescriptor;
+ }
+
+ /**
+ * Sets the change descriptor to be returned by {@link Change#getDescriptor()}.
+ *
+ * @param descriptor the change descriptor
+ */
+ public void setDescriptor(ChangeDescriptor descriptor) {
+ fDescriptor= descriptor;
+ }
+
+}
diff --git a/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/core/refactoring/resource/CopyProjectDescriptor.java b/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/core/refactoring/resource/CopyProjectDescriptor.java
new file mode 100644
index 00000000000..78fa7bc26d3
--- /dev/null
+++ b/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/core/refactoring/resource/CopyProjectDescriptor.java
@@ -0,0 +1,145 @@
+/*******************************************************************************
+ * Copyright (c) 2024 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
+ *******************************************************************************/
+package org.eclipse.ltk.core.refactoring.resource;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+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.Messages;
+import org.eclipse.ltk.internal.core.refactoring.RefactoringCoreMessages;
+import org.eclipse.ltk.internal.core.refactoring.resource.CopyProjectProcessor;
+
+/**
+ * Refactoring descriptor for the copy project 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.15
+ *
+ * @noinstantiate This class is not intended to be instantiated by clients.
+ * @noextend This class is not intended to be subclassed by clients.
+ */
+public class CopyProjectDescriptor extends RefactoringDescriptor {
+ /**
+ * Refactoring id of the 'Copy Project' refactoring (value:
+ * org.eclipse.ltk.core.refactoring.copyproject.resources
).
+ *
+ * Clients may safely cast the obtained refactoring descriptor to {@link CopyProjectDescriptor}.
+ *
+ */
+ public static final String ID= "org.eclipse.ltk.core.refactoring.copyproject.resource"; //$NON-NLS-1$
+
+ private IPath fSourcePath;
+
+ private String fNewName;
+
+ private IPath fNewLocation;
+
+ /**
+ * 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 CopyProjectDescriptor() {
+ super(ID, null, RefactoringCoreMessages.RenameResourceDescriptor_unnamed_descriptor, null, RefactoringDescriptor.STRUCTURAL_CHANGE | RefactoringDescriptor.MULTI_CHANGE);
+ }
+
+ /**
+ * The resource paths to delete.
+ *
+ * @return an array of IPaths.
+ */
+ public IPath getSourcePath() {
+ return fSourcePath;
+ }
+
+ public String getNewName() {
+ return fNewName;
+ }
+
+ public IPath getNewLocation() {
+ return fNewLocation;
+ }
+
+ /**
+ * The paths to the resources to be deleted. The resources can be {@link IProject} or a mixture
+ * of {@link IFile} and {@link IFolder}.
+ *
+ * @param resourcePath paths of the resources to be deleted
+ */
+ public void setResourcePath(IPath resourcePath) {
+ if (resourcePath == null)
+ throw new IllegalArgumentException();
+ fSourcePath= resourcePath;
+ }
+
+ /**
+ * The project to be copied.
+ *
+ * @param project {@link IProject} to be copied
+ */
+ public void setProjectToCopy(IProject project) {
+ if (project == null)
+ throw new IllegalArgumentException();
+ setResourcePath(project.getFullPath());
+ }
+
+ @Override
+ public Refactoring createRefactoring(RefactoringStatus status) throws CoreException {
+ IWorkspaceRoot wsRoot= ResourcesPlugin.getWorkspace().getRoot();
+ IResource resource= wsRoot.findMember(fSourcePath);
+ if (resource == null || !resource.exists()) {
+ status.addFatalError(Messages.format(RefactoringCoreMessages.CopyProjectDescriptor_project_copy_does_not_exist, BasicElementLabels.getPathLabel(fSourcePath, false)));
+ return null;
+ }
+ if (resource instanceof IProject project) {
+ return new CopyRefactoring(new CopyProjectProcessor(project, fNewName, fNewLocation));
+ }
+ return null;
+ }
+
+ public void setNewName(String newName) {
+ fNewName= newName;
+
+ }
+
+ public void setNewLocation(IPath newLocation) {
+ fNewLocation= newLocation;
+ }
+
+}
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 d0b9f263725..7af984b0cfc 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
@@ -33,6 +33,20 @@ public final class RefactoringCoreMessages extends NLS {
public static String CompositeChange_performingChangesTask_name;
+ public static String CopyProjectChange_copying;
+
+ public static String CopyProjectChange_error_resource_not_exists;
+
+ public static String CopyProjectChange_Name;
+
+ public static String CopyProjectDescriptor_project_copy_does_not_exist;
+
+ public static String CopyProjectProcessor_description;
+
+ public static String CopyProjectProcessor_error_project_exists;
+
+ public static String CopyProjectProcessor_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 3166e08f590..d44f72aa7a2 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
@@ -54,6 +54,13 @@ BufferValidationState_character_encoding_changed=The character encoding of ''{0}
CheckConditionContext_error_checker_exists= A checker of type ''{0}'' already exists.
CompositeChange_performingChangesTask_name=Performing changes...
+CopyProjectChange_copying=Copying...
+CopyProjectChange_error_resource_not_exists=Can not copy Project ''{0}''. Project does not exist.
+CopyProjectChange_Name=Copy Project
+CopyProjectDescriptor_project_copy_does_not_exist=The Project ''{0}'' to copy does not exist.
+CopyProjectProcessor_description=Copy Project ''{0}''
+CopyProjectProcessor_error_project_exists=There is already a Project with the name ''{0}''
+CopyProjectProcessor_name=Copy Project
ProcessorBasedRefactoring_initial_conditions=Checking preconditions...
ProcessorBasedRefactoring_check_condition_participant_failed=The participant ''{0}'' caused an internal error and has been disabled for this refactoring. See the error log for more details.
diff --git a/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/internal/core/refactoring/resource/CopyProjectProcessor.java b/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/internal/core/refactoring/resource/CopyProjectProcessor.java
new file mode 100644
index 00000000000..951dc868531
--- /dev/null
+++ b/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/internal/core/refactoring/resource/CopyProjectProcessor.java
@@ -0,0 +1,265 @@
+/*******************************************************************************
+ * Copyright (c) 2024 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
+ *******************************************************************************/
+package org.eclipse.ltk.internal.core.refactoring.resource;
+
+import java.net.URI;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.core.runtime.Platform;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceVisitor;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.resources.mapping.IResourceChangeDescriptionFactory;
+
+import org.eclipse.core.filebuffers.FileBuffers;
+import org.eclipse.core.filebuffers.ITextFileBuffer;
+import org.eclipse.core.filebuffers.LocationKind;
+
+import org.eclipse.ltk.core.refactoring.Change;
+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.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.ResourceChangeChecker;
+import org.eclipse.ltk.core.refactoring.participants.SharableParticipants;
+import org.eclipse.ltk.core.refactoring.resource.CopyProjectChange;
+import org.eclipse.ltk.core.refactoring.resource.CopyProjectDescriptor;
+import org.eclipse.ltk.core.refactoring.resource.Resources;
+import org.eclipse.ltk.internal.core.refactoring.BasicElementLabels;
+import org.eclipse.ltk.internal.core.refactoring.Messages;
+import org.eclipse.ltk.internal.core.refactoring.RefactoringCoreMessages;
+
+/**
+ * A copy processor for {@link IProject projects}. The processor will copy the project and load copy
+ * participants.
+ *
+ * @since 3.15
+ */
+public class CopyProjectProcessor extends CopyProcessor {
+ private IProject fProject;
+
+ private String fNewName;
+
+ private IPath fNewLocation;
+
+ /**
+ * Create a new copy project processor.
+ *
+ * @param project the {@link IProject} to copy.
+ * @param newLocation the new Location for the project.
+ * @param newName name of the new project.
+ */
+ public CopyProjectProcessor(IProject project, String newName, IPath newLocation) {
+ if (project == null || !project.exists()) {
+ throw new IllegalArgumentException("project must not be null and must exist"); //$NON-NLS-1$
+ }
+
+ fProject= project;
+ fNewName= newName;
+ fNewLocation= newLocation;
+ }
+
+ /**
+ * Returns the project to copy
+ *
+ * @return the project to copy
+ */
+ public IProject getProjectToCopy() {
+ return fProject;
+ }
+
+ @Override
+ public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException, OperationCanceledException {
+ IStatus status= Resources.checkInSync(fProject);
+ if (!status.isOK()) {
+ boolean autoRefresh= Platform.getPreferencesService().getBoolean(ResourcesPlugin.PI_RESOURCES, ResourcesPlugin.PREF_LIGHTWEIGHT_AUTO_REFRESH, false, null);
+ if (autoRefresh) {
+ fProject.refreshLocal(IResource.DEPTH_INFINITE, pm);
+ status= Resources.checkInSync(fProject);
+ }
+ }
+ return RefactoringStatus.create(status);
+ }
+
+ @Override
+ public RefactoringStatus checkFinalConditions(IProgressMonitor pm, CheckConditionsContext context) throws CoreException, OperationCanceledException {
+ pm.beginTask("", 1); //$NON-NLS-1$
+ try {
+ RefactoringStatus result= new RefactoringStatus();
+
+ if (!isSynchronizedExcludingLinkedResources(fProject)) {
+ String pathLabel= BasicElementLabels.getPathLabel(fProject.getFullPath(), false);
+
+ String locationLabel= null;
+ IPath location= fProject.getLocation();
+ if (location != null) {
+ locationLabel= BasicElementLabels.getPathLabel(location, true);
+ } else {
+ URI uri= fProject.getLocationURI();
+ if (uri != null) {
+ locationLabel= BasicElementLabels.getURLPart(uri.toString());
+ }
+ }
+
+ String warning;
+ if (locationLabel != null) {
+ warning= Messages.format(RefactoringCoreMessages.DeleteResourcesProcessor_warning_out_of_sync_container_loc, new Object[] { pathLabel, locationLabel });
+ } else {
+ warning= Messages.format(RefactoringCoreMessages.DeleteResourcesProcessor_warning_out_of_sync_container, pathLabel);
+ }
+ result.addWarning(warning);
+ }
+
+ checkDirtyResources(result);
+
+ if (ResourcesPlugin.getWorkspace().getRoot().getProject(fNewName).exists()) {
+ result.addError(Messages.format(RefactoringCoreMessages.CopyProjectProcessor_error_project_exists, fNewName));
+ }
+
+ ResourceChangeChecker checker= context.getChecker(ResourceChangeChecker.class);
+ IResourceChangeDescriptionFactory deltaFactory= checker.getDeltaFactory();
+ deltaFactory.copy(fProject, fNewLocation.append(fNewName));
+
+ return result;
+ } finally {
+ pm.done();
+ }
+ }
+
+ /**
+ * Checks whether this resource and its descendents are considered to be in sync with the local
+ * file system. The linked resources and their descendents are excluded from the check.
+ *
+ * @param resource the resource to check
+ * @return true
if this resource and its descendents except linked resources are
+ * synchronized, and false
in all other cases
+ * @throws CoreException if visiting the resource descendents fails for any reason
+ * @see IResource#isSynchronized(int)
+ */
+ public boolean isSynchronizedExcludingLinkedResources(IResource resource) throws CoreException {
+ boolean[] result= { true };
+ resource.accept((IResourceVisitor) visitedResource -> {
+ if (!result[0] || visitedResource.isLinked())
+ return false;
+ if (!visitedResource.isSynchronized(IResource.DEPTH_ZERO)) {
+ result[0]= false;
+ return false;
+ }
+ return true;
+ }, IResource.DEPTH_INFINITE, IContainer.DO_NOT_CHECK_EXISTENCE);
+ return result[0];
+ }
+
+ private void checkDirtyResources(final RefactoringStatus result) throws CoreException {
+ if (!fProject.isOpen()) {
+ return;
+ }
+ fProject.accept((IResourceVisitor) visitedResource -> {
+ if (visitedResource instanceof IFile) {
+ checkDirtyFile(result, (IFile) visitedResource);
+ }
+ return true;
+ }, IResource.DEPTH_INFINITE, false);
+ }
+
+ private void checkDirtyFile(RefactoringStatus result, IFile file) {
+ if (!file.exists())
+ return;
+ ITextFileBuffer buffer= FileBuffers.getTextFileBufferManager().getTextFileBuffer(file.getFullPath(), LocationKind.IFILE);
+ if (buffer != null && buffer.isDirty()) {
+ String message= RefactoringCoreMessages.DeleteResourcesProcessor_warning_unsaved_file;
+ if (buffer.isStateValidated() && buffer.isSynchronized()) {
+ result.addWarning(Messages.format(message, BasicElementLabels.getPathLabel(file.getFullPath(), false)));
+ } else {
+ result.addFatalError(Messages.format(message, BasicElementLabels.getPathLabel(file.getFullPath(), false)));
+ }
+ }
+ }
+
+ @Override
+ public Change createChange(IProgressMonitor pm) throws CoreException, OperationCanceledException {
+ pm.beginTask(RefactoringCoreMessages.DeleteResourcesProcessor_create_task, 1);
+ try {
+ CopyProjectChange change= new CopyProjectChange(fProject, fNewLocation, fNewName);
+ change.setDescriptor(new RefactoringChangeDescriptor(createDescriptor()));
+ return change;
+ } finally {
+ pm.done();
+ }
+ }
+
+ protected CopyProjectDescriptor createDescriptor() {
+ CopyProjectDescriptor descriptor= new CopyProjectDescriptor();
+ descriptor.setProject(null);
+ descriptor.setDescription(getDescription());
+ descriptor.setComment(descriptor.getDescription());
+ descriptor.setFlags(RefactoringDescriptor.STRUCTURAL_CHANGE | RefactoringDescriptor.MULTI_CHANGE | RefactoringDescriptor.BREAKING_CHANGE);
+
+ descriptor.setProjectToCopy(fProject);
+ descriptor.setNewName(fNewName);
+ descriptor.setNewLocation(fNewLocation);
+ return descriptor;
+ }
+
+ private String getDescription() {
+ return Messages.format(RefactoringCoreMessages.CopyProjectProcessor_description, BasicElementLabels.getPathLabel(fProject.getFullPath(), false));
+ }
+
+ @Override
+ public Object[] getElements() {
+ return new Object[] { fProject };
+ }
+
+ @Override
+ public String getIdentifier() {
+ return "org.eclipse.ltk.core.refactoring.copyProjectProcessor"; //$NON-NLS-1$
+ }
+
+ @Override
+ public String getProcessorName() {
+ return RefactoringCoreMessages.CopyProjectProcessor_name;
+ }
+
+ @Override
+ public boolean isApplicable() throws CoreException {
+ if (fProject == null)
+ return false;
+ if (!fProject.exists())
+ return false;
+ if (!fProject.isAccessible())
+ return false;
+ return true;
+ }
+
+ @Override
+ public RefactoringParticipant[] loadParticipants(RefactoringStatus status, SharableParticipants sharedParticipants) throws CoreException {
+ final String[] affectedNatures= ResourceProcessors.computeAffectedNatures(fProject);
+ final CopyArguments copyArguments= new CopyArguments(fNewLocation.append(fNewName), new ReorgExecutionLog());
+
+ return ParticipantManager.loadCopyParticipants(status, this, fProject, copyArguments, affectedNatures, sharedParticipants);
+ }
+}
\ No newline at end of file
diff --git a/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/internal/core/refactoring/resource/CopyProjectRefactoringContribution.java b/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/internal/core/refactoring/resource/CopyProjectRefactoringContribution.java
new file mode 100644
index 00000000000..f84bb3fa7b7
--- /dev/null
+++ b/bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/internal/core/refactoring/resource/CopyProjectRefactoringContribution.java
@@ -0,0 +1,95 @@
+/*******************************************************************************
+ * Copyright (c) 2024 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
+ *******************************************************************************/
+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.CopyProjectDescriptor;
+
+/**
+ * @since 3.15
+ */
+public class CopyProjectRefactoringContribution extends RefactoringContribution {
+
+ /**
+ * Key used for the new resource name
+ */
+ private static final String ATTRIBUTE_NAME= "name"; //$NON-NLS-1$
+
+ /**
+ * Key used for the new resource destination
+ */
+ private static final String ATTRIBUTE_DESTINATION= "destination"; //$NON-NLS-1$
+
+ /**
+ * Key prefix used for the path of the project to copy
+ */
+ private static final String ATTRIBUTE_ELEMENT= "element"; //$NON-NLS-1$
+
+ @Override
+ public Map retrieveArgumentMap(RefactoringDescriptor descriptor) {
+ if (descriptor instanceof CopyProjectDescriptor copyDesc) {
+ HashMap map= new HashMap<>();
+ IPath resources= copyDesc.getSourcePath();
+ String project= copyDesc.getProject();
+ map.put(ATTRIBUTE_ELEMENT, ResourceProcessors.resourcePathToHandle(project, resources));
+ map.put(ATTRIBUTE_NAME, copyDesc.getNewName());
+ IPath destinationPath= copyDesc.getNewLocation();
+ map.put(ATTRIBUTE_DESTINATION, ResourceProcessors.resourcePathToHandle(descriptor.getProject(), destinationPath));
+
+ return map;
+ }
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public RefactoringDescriptor createDescriptor() {
+ return new CopyProjectDescriptor();
+ }
+
+ @Override
+ public RefactoringDescriptor createDescriptor(String id, String project, String description, String comment, Map arguments, int flags) throws IllegalArgumentException {
+ String pathString= arguments.get(ATTRIBUTE_ELEMENT);
+ String newName= arguments.get(ATTRIBUTE_NAME);
+
+ String destination= arguments.get(ATTRIBUTE_DESTINATION);
+ if (destination == null) {
+ throw new IllegalArgumentException("Can not restore CopyProjectDescriptor from map, destination missing"); //$NON-NLS-1$
+ }
+
+ IPath resourcePath= ResourceProcessors.handleToResourcePath(project, pathString);
+ IPath destPath= ResourceProcessors.handleToResourcePath(project, destination);
+
+ if (resourcePath != null && newName != null) {
+ CopyProjectDescriptor descriptor= new CopyProjectDescriptor();
+ descriptor.setProject(project);
+ descriptor.setDescription(description);
+ descriptor.setComment(comment);
+ descriptor.setFlags(flags);
+ descriptor.setResourcePath(resourcePath);
+ descriptor.setNewName(newName);
+ descriptor.setNewLocation(destPath);
+ descriptor.setResourcePath(resourcePath);
+
+ return descriptor;
+ }
+ throw new IllegalArgumentException("Can not restore CopyProjectDescriptor from map"); //$NON-NLS-1$
+ }
+}
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 9ef5f57cac3..7b5249d26a2 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.13.400.qualifier
+Bundle-Version: 3.13.500.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.xml b/bundles/org.eclipse.ltk.ui.refactoring/plugin.xml
index 5223d470c3e..e4acec4f2a7 100644
--- a/bundles/org.eclipse.ltk.ui.refactoring/plugin.xml
+++ b/bundles/org.eclipse.ltk.ui.refactoring/plugin.xml
@@ -132,5 +132,21 @@
optional="true">
+
+
+
+
+
+
\ No newline at end of file
diff --git a/bundles/org.eclipse.ltk.ui.refactoring/src/org/eclipse/ltk/internal/ui/refactoring/actions/CopyProjectHandler.java b/bundles/org.eclipse.ltk.ui.refactoring/src/org/eclipse/ltk/internal/ui/refactoring/actions/CopyProjectHandler.java
new file mode 100644
index 00000000000..dcab4441a21
--- /dev/null
+++ b/bundles/org.eclipse.ltk.ui.refactoring/src/org/eclipse/ltk/internal/ui/refactoring/actions/CopyProjectHandler.java
@@ -0,0 +1,88 @@
+/*******************************************************************************
+ * Copyright (c) 2024 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
+ *******************************************************************************/
+package org.eclipse.ltk.internal.ui.refactoring.actions;
+
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.NullProgressMonitor;
+
+import org.eclipse.core.resources.IProject;
+
+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.CopyProjectProcessor;
+
+public class CopyProjectHandler extends AbstractResourcesHandler {
+
+ 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$
+
+ @Override
+ public Object execute(ExecutionEvent event) throws ExecutionException {
+
+
+ Object newNameValue= HandlerUtil.getVariable(event, LTK_COPY_PROJECT_COMMAND_NEWNAME_KEY);
+ Object newLocationValue= HandlerUtil.getVariable(event, LTK_COPY_PROJECT_COMMAND_NEWLOCATION_KEY);
+ ISelection sel= HandlerUtil.getCurrentSelection(event);
+
+ String newName= null;
+ if (newNameValue instanceof String) {
+ newName= (String) newNameValue;
+ }
+
+ IPath newLocation= null;
+ if (newLocationValue instanceof IPath) {
+ newLocation= (IPath) newLocationValue;
+ }
+
+ if (sel instanceof IStructuredSelection selection) {
+ List resources= Arrays.stream(getSelectedResources(selection))
+ .filter(IProject.class::isInstance)
+ .map(IProject.class::cast)
+ .toList();
+ if (resources.size() == 1) {
+
+ CopyRefactoring copyRefactoring= new CopyRefactoring(new CopyProjectProcessor(resources.get(0), newName, newLocation));
+ try {
+ CreateChangeOperation create= new CreateChangeOperation(
+ new CheckConditionsOperation(copyRefactoring, CheckConditionsOperation.FINAL_CONDITIONS),
+ RefactoringStatus.FATAL);
+
+ PerformChangeOperation perform= new PerformChangeOperation(create);
+ perform.setUndoManager(RefactoringCore.getUndoManager(), copyRefactoring.getName());
+
+ perform.run(new NullProgressMonitor());
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ return true;
+ }
+}
diff --git a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CopyProjectAction.java b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CopyProjectAction.java
index 4e65ce8c4a3..597be0afd34 100644
--- a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CopyProjectAction.java
+++ b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CopyProjectAction.java
@@ -41,6 +41,7 @@
import org.eclipse.ui.internal.ide.IDEWorkbenchMessages;
import org.eclipse.ui.internal.ide.IDEWorkbenchPlugin;
import org.eclipse.ui.internal.ide.IIDEHelpContextIds;
+import org.eclipse.ui.internal.ide.actions.LTKLauncher;
import org.eclipse.ui.internal.progress.ProgressMonitorJobsDialog;
/**
@@ -303,6 +304,10 @@ public void run() {
String newName = (String) destinationPaths[0];
URI newLocation = URIUtil.toURI((String) destinationPaths[1]);
+ if (LTKLauncher.copyProject(project, newName, URIUtil.toPath(newLocation))) {
+ return;
+ }
+
boolean completed = performCopy(project, newName, newLocation);
if (!completed) {
diff --git a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CopyProjectOperation.java b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CopyProjectOperation.java
index fe8f11cb10c..4750c14988e 100644
--- a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CopyProjectOperation.java
+++ b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CopyProjectOperation.java
@@ -36,6 +36,7 @@
import org.eclipse.ui.ide.IDE;
import org.eclipse.ui.ide.undo.WorkspaceUndoUtil;
import org.eclipse.ui.internal.ide.IDEWorkbenchMessages;
+import org.eclipse.ui.internal.ide.actions.LTKLauncher;
import org.eclipse.ui.internal.progress.ProgressMonitorJobsDialog;
/**
@@ -127,6 +128,10 @@ public void copyProject(IProject project) {
String newName = (String) destinationPaths[0];
URI newLocation = URIUtil.toURI((String)destinationPaths[1]);
+ if (LTKLauncher.copyProject(project, newName, URIUtil.toPath(newLocation))) {
+ return;
+ }
+
boolean completed = performProjectCopy(project, newName, newLocation);
if (!completed) {
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 f968602d68f..99f18dd0c94 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
@@ -27,7 +27,10 @@
import org.eclipse.core.commands.ParameterizedCommand;
import org.eclipse.core.commands.common.NotDefinedException;
import org.eclipse.core.expressions.EvaluationContext;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.IPath;
import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.ui.ISources;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.commands.ICommandService;
@@ -44,6 +47,10 @@ public class LTKLauncher {
private static final String LTK_RENAME_ID = "org.eclipse.ltk.ui.refactoring.commands.renameResource"; //$NON-NLS-1$
private static final String LTK_RENAME_COMMAND_NEWNAME_KEY = "org.eclipse.ltk.ui.refactoring.commands.renameResource.newName.parameter.key"; //$NON-NLS-1$
private static final String LTK_CHECK_COMPOSITE_RENAME_PARAMETER_KEY = "org.eclipse.ltk.ui.refactoring.commands.checkCompositeRename.parameter.key"; //$NON-NLS-1$
+ 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$
+
/**
* Open the LTK delete resources wizard if available.
*
@@ -113,6 +120,13 @@ public static boolean isCompositeRename(IStructuredSelection structuredSelection
return runCommand(LTK_RENAME_ID, structuredSelection, commandParameters);
}
+ public static boolean copyProject(IProject project, String newName, IPath newLocation) {
+ Map commandParameters = new HashMap<>();
+ commandParameters.put(LTK_COPY_PROJECT_COMMAND_NEWNAME_KEY, newName);
+ commandParameters.put(LTK_COPY_PROJECT_COMMAND_NEWLOCATION_KEY, newLocation);
+ return runCommand(LTK_COPY_PROJECT_ID, new StructuredSelection(project), 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/resource/ResourceRefactoringTests.java b/tests/org.eclipse.ltk.core.refactoring.tests/src/org/eclipse/ltk/core/refactoring/tests/resource/ResourceRefactoringTests.java
index 85bec642453..3e77001f537 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,6 +49,7 @@
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.resource.CopyProjectDescriptor;
import org.eclipse.ltk.core.refactoring.resource.DeleteResourcesDescriptor;
import org.eclipse.ltk.core.refactoring.resource.MoveRenameResourceDescriptor;
import org.eclipse.ltk.core.refactoring.resource.MoveResourceChange;
@@ -391,6 +392,33 @@ public void testDeleteRefactoring3_bug343584() throws Exception {
}
}
+ @Test
+ public void testCopyProjectRefactoring() throws Exception {
+ String content1= "hello";
+
+ IFolder testFolder= fProject.createFolder("test");
+ IFile file1= fProject.createFile(testFolder, "myFile.txt", content1);
+
+ RefactoringContribution contribution= RefactoringCore.getRefactoringContribution(CopyProjectDescriptor.ID);
+ CopyProjectDescriptor descriptor= (CopyProjectDescriptor) contribution.createDescriptor();
+
+ descriptor.setResourcePath(fProject.getProject().getFullPath());
+ descriptor.setNewName("project2");
+ descriptor.setNewLocation(fProject.getProject().getParent().getFullPath());
+
+ Change undoChange= perform(descriptor);
+
+ IProject targetProject= ResourcesPlugin.getWorkspace().getRoot().getProject("project2");
+
+ assertTrue(targetProject.exists());
+
+ assertMoveRename(file1, targetProject.getFolder("test"), "myFile.txt", content1);
+
+ perform(undoChange);
+
+ assertFalse(targetProject.exists());
+ }
+
private Change perform(Change change) throws CoreException {
PerformChangeOperation op= new PerformChangeOperation(change);
op.run(null);