blob: d5c2781e27655959e0371d5246718e8784e9056c [file] [log] [blame]
/*=============================================================================#
# 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;
}
}