| /*=============================================================================# |
| # Copyright (c) 2008, 2021 Stephan Wahlbrink and others. |
| # |
| # This program and the accompanying materials are made available under the |
| # terms of the Eclipse Public License 2.0 which is available at |
| # https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 |
| # which is available at https://www.apache.org/licenses/LICENSE-2.0. |
| # |
| # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 |
| # |
| # Contributors: |
| # Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation |
| #=============================================================================*/ |
| |
| package org.eclipse.statet.ltk.ui.refactoring; |
| |
| import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert; |
| |
| import java.lang.reflect.InvocationTargetException; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import java.util.concurrent.atomic.AtomicReference; |
| |
| import org.eclipse.core.resources.ResourcesPlugin; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.OperationCanceledException; |
| import org.eclipse.core.runtime.SubMonitor; |
| import org.eclipse.core.runtime.jobs.IJobManager; |
| import org.eclipse.core.runtime.jobs.ISchedulingRule; |
| import org.eclipse.core.runtime.jobs.Job; |
| import org.eclipse.jface.dialogs.Dialog; |
| import org.eclipse.jface.dialogs.IDialogConstants; |
| import org.eclipse.jface.dialogs.MessageDialog; |
| import org.eclipse.jface.text.Position; |
| import org.eclipse.ltk.core.refactoring.Change; |
| import org.eclipse.ltk.core.refactoring.CompositeChange; |
| import org.eclipse.ltk.core.refactoring.PerformChangeOperation; |
| import org.eclipse.ltk.core.refactoring.Refactoring; |
| import org.eclipse.ltk.core.refactoring.RefactoringCore; |
| import org.eclipse.ltk.core.refactoring.RefactoringStatus; |
| import org.eclipse.ltk.internal.ui.refactoring.ChangeExceptionHandler; |
| import org.eclipse.ltk.ui.refactoring.RefactoringUI; |
| import org.eclipse.osgi.util.NLS; |
| import org.eclipse.swt.widgets.Display; |
| import org.eclipse.swt.widgets.Shell; |
| import org.eclipse.ui.progress.IProgressService; |
| |
| import org.eclipse.statet.jcommons.lang.NonNullByDefault; |
| import org.eclipse.statet.jcommons.lang.Nullable; |
| |
| import org.eclipse.statet.internal.ltk.ui.refactoring.Messages; |
| import org.eclipse.statet.ltk.model.core.element.SourceUnit; |
| import org.eclipse.statet.ltk.refactoring.core.ScheduledRefactoring; |
| import org.eclipse.statet.ltk.refactoring.core.SourceUnitChange; |
| |
| |
| /** |
| * Helper to execute a refactoring. The class takes care of pushing the |
| * undo change onto the undo stack and folding editor edits into one editor |
| * undo object. |
| */ |
| @NonNullByDefault |
| public class RefactoringExecutionHelper { |
| |
| |
| private final Refactoring refactoring; |
| |
| private final IProgressService execContext; |
| |
| private final Shell parentShell; |
| private final int stopSeverity; |
| |
| private @Nullable SourceUnit insertPositionSourceUnit; |
| private @Nullable Position insertPosition; |
| |
| |
| /** |
| * @param refactoring |
| * @param stopSeverity a refactoring status constant from {@link RefactoringStatus} |
| * @param parentShell |
| * @param context |
| */ |
| public RefactoringExecutionHelper(final Refactoring refactoring, final int stopSeverity, |
| final Shell parentShell, final IProgressService context) { |
| this.refactoring= nonNullAssert(refactoring); |
| this.stopSeverity= stopSeverity; |
| this.parentShell= nonNullAssert(parentShell); |
| this.execContext= nonNullAssert(context); |
| } |
| |
| |
| /** |
| * Must be called in the UI thread.<br> |
| * <strong>Use {@link #perform(boolean, boolean)} unless you know exactly what you are doing!</strong> |
| * |
| * @param forkChangeExecution if the change should not be executed in the UI thread: This may not work in any case |
| * @param cancelable if set, the operation will be cancelable |
| * @throws InterruptedException thrown when the operation is cancelled |
| * @throws InvocationTargetException thrown when the operation failed to execute |
| */ |
| public void perform(final boolean forkChangeExecution, final boolean cancelable) |
| throws InterruptedException, InvocationTargetException { |
| final AtomicReference<PerformChangeOperation> op= new AtomicReference<>(); |
| try { |
| this.execContext.busyCursorWhile((final IProgressMonitor monitor) -> { |
| final SubMonitor m= SubMonitor.convert(monitor, this.refactoring.getName(), 10); |
| |
| final IJobManager manager= Job.getJobManager(); |
| final Thread workingThread= Thread.currentThread(); |
| final ISchedulingRule rule= (this.refactoring instanceof ScheduledRefactoring) ? |
| ((ScheduledRefactoring)this.refactoring).getSchedulingRule() : |
| ResourcesPlugin.getWorkspace().getRoot(); |
| |
| manager.beginRule(rule, m.newChild(1)); |
| try { |
| if (cancelable && monitor.isCanceled()) { |
| throw new InterruptedException(); |
| } |
| |
| this.refactoring.setValidationContext(this.parentShell); |
| |
| final RefactoringStatus status= this.refactoring.checkAllConditions( |
| m.newChild(1, SubMonitor.SUPPRESS_NONE)); |
| if (status.getSeverity() >= this.stopSeverity) { |
| final AtomicBoolean canceled= new AtomicBoolean(); |
| this.parentShell.getDisplay().syncExec(() -> { |
| final Dialog dialog= RefactoringUI.createRefactoringStatusDialog( |
| status, this.parentShell, this.refactoring.getName(), false ); |
| final int selection= dialog.open(); |
| canceled.set(selection == IDialogConstants.CANCEL_ID); |
| }); |
| if (canceled.get()) { |
| throw new OperationCanceledException(); |
| } |
| } |
| |
| final Change change= this.refactoring.createChange( |
| m.newChild(2, SubMonitor.SUPPRESS_NONE) ); |
| change.initializeValidationData( |
| m.newChild(1, SubMonitor.SUPPRESS_NONE) ); |
| |
| final SourceUnitChange insertTracker= (this.insertPositionSourceUnit != null) ? |
| search(change) : null; |
| |
| final var operation= new PerformChangeOperation(change); |
| operation.setUndoManager(RefactoringCore.getUndoManager(), this.refactoring.getName()); |
| operation.setSchedulingRule(rule); |
| |
| if (cancelable && monitor.isCanceled()) { |
| throw new InterruptedException(); |
| } |
| |
| op.set(operation); |
| if (forkChangeExecution) { |
| operation.run(m.newChild(4, SubMonitor.SUPPRESS_NONE)); |
| } |
| else { |
| final AtomicReference<Exception> opException= new AtomicReference<>(); |
| final Display display= this.parentShell.getDisplay(); |
| manager.transferRule(rule, display.getThread()); |
| display.syncExec(() -> { |
| try { |
| operation.run(m.newChild(4, SubMonitor.SUPPRESS_NONE)); |
| } |
| catch (final CoreException | RuntimeException e) { |
| opException.set(e); |
| } |
| finally { |
| manager.transferRule(rule, workingThread); |
| } |
| }); |
| if (opException.get() != null) { |
| final Exception e= opException.get(); |
| if (e instanceof CoreException) { |
| throw (CoreException)e; |
| } |
| if (e instanceof RuntimeException) { |
| throw (RuntimeException)e; |
| } |
| } |
| } |
| |
| final RefactoringStatus validationStatus= operation.getValidationStatus(); |
| if (validationStatus != null && validationStatus.hasFatalError()) { |
| MessageDialog.openError(this.parentShell, this.refactoring.getName(), NLS.bind( |
| Messages.ExecutionHelper_CannotExecute_message, |
| validationStatus.getMessageMatchingSeverity(RefactoringStatus.FATAL) )); |
| return; |
| } |
| |
| if (insertTracker != null) { |
| this.insertPosition= insertTracker.getInsertPosition(); |
| } |
| } |
| catch (final OperationCanceledException e) { |
| throw new InterruptedException(e.getMessage()); |
| } |
| catch (final CoreException | RuntimeException e) { |
| throw new InvocationTargetException(e); |
| } |
| finally { |
| manager.endRule(rule); |
| this.refactoring.setValidationContext(null); |
| } |
| }); |
| } |
| catch (final InvocationTargetException e) { |
| final PerformChangeOperation operation= op.get(); |
| if (operation != null && operation.changeExecutionFailed()) { |
| final ChangeExceptionHandler handler= new ChangeExceptionHandler(this.parentShell, |
| this.refactoring ); |
| final Throwable inner= e.getTargetException(); |
| if (inner instanceof RuntimeException) { |
| handler.handle(operation.getChange(), (RuntimeException)inner); |
| } |
| else if (inner instanceof CoreException) { |
| handler.handle(operation.getChange(), (CoreException)inner); |
| } |
| else { |
| throw e; |
| } |
| } |
| else { |
| throw e; |
| } |
| } |
| } |
| |
| |
| |
| public void enableInsertPosition(final SourceUnit su) { |
| this.insertPositionSourceUnit= su; |
| } |
| |
| public @Nullable Position getInsertPosition() { |
| return this.insertPosition; |
| } |
| |
| private @Nullable SourceUnitChange search(final Change change) { |
| if (change instanceof SourceUnitChange) { |
| if (((SourceUnitChange)change).getSourceUnit() == this.insertPositionSourceUnit) { |
| return (SourceUnitChange)change; |
| } |
| } |
| if (change instanceof CompositeChange) { |
| final var children= ((CompositeChange)change).getChildren(); |
| for (int i= 0; i < children.length; i++) { |
| final SourceUnitChange child= search(children[i]); |
| if (child != null) { |
| return child; |
| } |
| } |
| } |
| return null; |
| } |
| |
| } |