From f73dc5839ce008212ce6130fdfe90e93fd5781f3 Mon Sep 17 00:00:00 2001 From: "Kellner, Niklas" Date: Thu, 12 Sep 2024 07:07:09 +0200 Subject: [PATCH] Use LTK Refactoring when copying (duplicating) a Project. The Idea is that currently it is not possible to modify a project when it is copied and renamed. This leads to usability issues for example in PDE, where the rename of a project modifies the MANIFEST.MF but a copy does not. This solves this by providing a copy project refactoring that can be extended using a copy participant. --- .../META-INF/MANIFEST.MF | 2 +- .../plugin.xml | 4 + .../resource/CopyProjectChange.java | 153 ++++++++++ .../resource/CopyProjectDescriptor.java | 145 ++++++++++ .../refactoring/RefactoringCoreMessages.java | 14 + .../RefactoringCoreMessages.properties | 7 + .../resource/CopyProjectProcessor.java | 265 ++++++++++++++++++ .../CopyProjectRefactoringContribution.java | 95 +++++++ .../META-INF/MANIFEST.MF | 2 +- .../org.eclipse.ltk.ui.refactoring/plugin.xml | 16 ++ .../actions/CopyProjectHandler.java | 88 ++++++ .../eclipse/ui/actions/CopyProjectAction.java | 5 + .../ui/actions/CopyProjectOperation.java | 5 + .../ui/internal/ide/actions/LTKLauncher.java | 14 + .../resource/ResourceRefactoringTests.java | 28 ++ 15 files changed, 841 insertions(+), 2 deletions(-) create mode 100644 bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/core/refactoring/resource/CopyProjectChange.java create mode 100644 bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/core/refactoring/resource/CopyProjectDescriptor.java create mode 100644 bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/internal/core/refactoring/resource/CopyProjectProcessor.java create mode 100644 bundles/org.eclipse.ltk.core.refactoring/src/org/eclipse/ltk/internal/core/refactoring/resource/CopyProjectRefactoringContribution.java create mode 100644 bundles/org.eclipse.ltk.ui.refactoring/src/org/eclipse/ltk/internal/ui/refactoring/actions/CopyProjectHandler.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 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);