blob: 2a83513779c519bff4fbb3d788595559170e5040 [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.r.core.refactoring;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.jface.text.AbstractDocument;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.Refactoring;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.core.refactoring.TextFileChange;
import org.eclipse.osgi.util.NLS;
import org.eclipse.text.edits.InsertEdit;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.statet.jcommons.text.core.TextRegion;
import org.eclipse.statet.internal.r.core.refactoring.Messages;
import org.eclipse.statet.ltk.ast.core.AstInfo;
import org.eclipse.statet.ltk.ast.core.util.AstSelection;
import org.eclipse.statet.ltk.core.Ltk;
import org.eclipse.statet.ltk.model.core.ElementSet;
import org.eclipse.statet.ltk.refactoring.core.CommonRefactoringDescriptor;
import org.eclipse.statet.ltk.refactoring.core.RefactoringChange;
import org.eclipse.statet.ltk.refactoring.core.RefactoringMessages;
import org.eclipse.statet.ltk.refactoring.core.SourceUnitChange;
import org.eclipse.statet.ltk.refactoring.core.TextChangeCompatibility;
import org.eclipse.statet.r.core.RCore;
import org.eclipse.statet.r.core.RUtil;
import org.eclipse.statet.r.core.model.RModel;
import org.eclipse.statet.r.core.model.RModelManager;
import org.eclipse.statet.r.core.model.RSourceUnit;
import org.eclipse.statet.r.core.model.RSourceUnitModelInfo;
import org.eclipse.statet.r.core.rsource.ast.FDef;
import org.eclipse.statet.r.core.rsource.ast.GenericVisitor;
import org.eclipse.statet.r.core.rsource.ast.RAstNode;
import org.eclipse.statet.r.core.rsource.ast.RAsts;
import org.eclipse.statet.r.core.source.RHeuristicTokenScanner;
public class ExtractTempRefactoring extends Refactoring {
private class OccurrencesSearcher extends GenericVisitor {
private final int start= ExtractTempRefactoring.this.expression.getStartOffset();
@Override
public void visitNode(final RAstNode node) throws InvocationTargetException {
if (node.getEndOffset() < this.start) {
return;
}
if (ExtractTempRefactoring.this.expression.equalsValue(node)) {
ExtractTempRefactoring.this.occurrencesList.add(node);
}
else {
node.acceptInRChildren(this);
}
}
@Override
public void visit(final FDef node) throws InvocationTargetException {
}
}
private final RRefactoringAdapter adapter= new RRefactoringAdapter();
private final ElementSet elementSet;
private TextRegion selectionRegion;
private TextRegion operationRegion;
private final RSourceUnit sourceUnit;
private RAstNode expression;
private RAstNode container;
private List<RAstNode> occurrencesList;
private String tempName= "";
private boolean replaceAllOccurrences= true;
/**
* Creates a new extract temp refactoring.
* @param su the source unit
* @param region (selected) regino of the expression to extract
*/
public ExtractTempRefactoring(final RSourceUnit su, final TextRegion region) {
this.sourceUnit= su;
this.elementSet= new ElementSet(new Object[] { su });
if (region != null && region.getStartOffset() >= 0 && region.getLength() >= 0) {
this.selectionRegion= region;
}
}
@Override
public String getName() {
return Messages.ExtractTemp_label;
}
public String getIdentifier() {
return RRefactoring.EXTRACT_TEMP_REFACTORING_ID;
}
public void setTempName(final String newName) {
this.tempName= newName;
}
public String getTempName() {
return this.tempName;
}
public void setReplaceAllOccurrences(final boolean enable) {
this.replaceAllOccurrences= enable;
}
public int getAllOccurrencesCount() {
return (this.occurrencesList != null) ? this.occurrencesList.size() : null;
}
public boolean getReplaceAllOccurrences() {
return this.replaceAllOccurrences;
}
@Override
public RefactoringStatus checkInitialConditions(final IProgressMonitor monitor) throws CoreException {
final SubMonitor m= SubMonitor.convert(monitor, 6);
try {
if (this.selectionRegion != null) {
this.sourceUnit.connect(m.newChild(1));
try {
final AbstractDocument document= this.sourceUnit.getDocument(monitor);
final RHeuristicTokenScanner scanner= this.adapter.getScanner(this.sourceUnit);
final RSourceUnitModelInfo modelInfo= (RSourceUnitModelInfo)this.sourceUnit.getModelInfo(RModel.R_TYPE_ID, RModelManager.MODEL_FILE, m.newChild(1));
if (modelInfo != null) {
final TextRegion region= this.adapter.trimToAstRegion(document,
this.selectionRegion, scanner );
final AstInfo ast= modelInfo.getAst();
if (ast != null) {
this.expression= (RAstNode) AstSelection.search(ast.getRoot(),
region.getStartOffset(), region.getEndOffset(),
AstSelection.MODE_COVERING_SAME_LAST ).getCovering();
}
}
if (this.expression != null) {
final TextRegion region= this.expression;
this.operationRegion= this.adapter.expandSelectionRegion(document,
region, this.selectionRegion, scanner );
}
}
finally {
this.sourceUnit.disconnect(m.newChild(1));
}
}
if (this.expression == null) {
return RefactoringStatus.createFatalErrorStatus(Messages.ExtractTemp_error_InvalidSelection_message);
}
final RefactoringStatus result= new RefactoringStatus();
this.adapter.checkInitialToModify(result, this.elementSet);
m.worked(1);
if (result.hasFatalError()) {
return result;
}
checkExpression(result);
m.worked(2);
return result;
}
finally {
m.done();
}
}
private void checkExpression(final RefactoringStatus result) {
switch (this.expression.getNodeType()) {
case STRING_CONST:
case NUM_CONST:
case NULL_CONST:
case SYMBOL:
case BLOCK:
case GROUP:
case NS_GET:
case NS_GET_INT:
case SUB_INDEXED_S:
case SUB_INDEXED_D:
case SUB_NAMED_PART:
case SUB_NAMED_SLOT:
case POWER:
case SIGN:
case SEQ:
case SPECIAL:
case MULT:
case ADD:
case RELATIONAL:
case NOT:
case AND:
case OR:
case MODEL:
case A_LEFT:
case A_RIGHT:
case A_EQUALS:
case A_COLON:
case C_IF:
case C_FOR:
case C_WHILE:
case C_REPEAT:
case F_DEF:
case F_CALL:
break;
default:
result.merge(RefactoringStatus.createFatalErrorStatus(Messages.ExtractTemp_error_InvalidSelectionType_message));
return;
}
RAstNode parent= this.expression.getRParent();
SEARCH_BLOCK: while (parent != null) {
switch (parent.getNodeType()) {
case SOURCELINES:
case BLOCK:
break SEARCH_BLOCK;
case F_DEF_ARGS:
result.merge(RefactoringStatus.createFatalErrorStatus(Messages.ExtractTemp_error_InvalidSelectionFHeader_message));
return;
default:
parent= parent.getRParent();
continue SEARCH_BLOCK;
}
}
if (parent == null) {
throw new IllegalStateException();
}
if (this.selectionRegion != null
&& (this.selectionRegion.getStartOffset() != this.operationRegion.getStartOffset()
|| this.selectionRegion.getLength() != this.operationRegion.getLength() )) {
result.merge(RefactoringStatus.createWarningStatus(Messages.ExtractTemp_warning_ChangedRange_message));
}
this.occurrencesList= new ArrayList<>();
try {
parent.acceptInRChildren(new OccurrencesSearcher());
}
catch (final InvocationTargetException e) {}
for (final RAstNode node : this.occurrencesList) {
if (RAsts.hasErrors(node)) {
result.merge(RefactoringStatus.createWarningStatus(Messages.ExtractTemp_warning_OccurrencesSyntaxError_message));
break;
}
}
this.container= parent;
}
public RefactoringStatus checkTempName(final String name) {
final String message= this.adapter.validateIdentifier(name, "The local variable name");
if (message != null) {
return RefactoringStatus.createFatalErrorStatus(message);
}
return new RefactoringStatus();
}
@Override
public RefactoringStatus checkFinalConditions(final IProgressMonitor monitor) throws CoreException {
final SubMonitor m= SubMonitor.convert(monitor, RefactoringMessages.Common_FinalCheck_label, 3);
try {
final RefactoringStatus status= checkTempName(this.tempName);
this.adapter.checkFinalToModify(status, this.elementSet, m.newChild(2));
return status;
}
finally {
m.done();
}
}
@Override
public Change createChange(final IProgressMonitor monitor) throws CoreException {
final SubMonitor m= SubMonitor.convert(monitor, RefactoringMessages.Common_CreateChanges_label, 3);
try {
final TextFileChange textFileChange= new SourceUnitChange(this.sourceUnit);
if (this.sourceUnit.getWorkingContext() == Ltk.EDITOR_CONTEXT) {
textFileChange.setSaveMode(TextFileChange.LEAVE_DIRTY);
}
createChanges(textFileChange, m.newChild(2));
final Map<String, String> arguments= new HashMap<>();
final String varName= RRefactoringAdapter.getUnquotedIdentifier(this.tempName);
final String description= NLS.bind(Messages.ExtractTemp_Descriptor_description,
RUtil.formatVarName(varName) );
final IProject resource= this.elementSet.getSingleProject();
final String project= (resource != null) ? resource.getName() : null;
final String source= (project != null) ? NLS.bind(RefactoringMessages.Common_Source_Project_label, project) : RefactoringMessages.Common_Source_Workspace_label;
final int flags= 0;
final String comment= ""; //$NON-NLS-1$
final CommonRefactoringDescriptor descriptor= new CommonRefactoringDescriptor(
getIdentifier(), project, description, comment, arguments, flags);
m.worked(1);
return new RefactoringChange(descriptor,
Messages.ExtractTemp_label,
new Change[] { textFileChange });
}
catch (final BadLocationException e) {
throw new CoreException(new Status(IStatus.ERROR, RCore.BUNDLE_ID, "Unexpected error (concurrent change?)", e));
}
finally {
m.done();
}
}
private void createChanges(final TextFileChange change, final SubMonitor m) throws BadLocationException, CoreException {
m.setWorkRemaining(3 + 2 * 4);
this.sourceUnit.connect(m.newChild(1));
try {
final AbstractDocument doc= this.sourceUnit.getDocument(m.newChild(1));
final StringBuilder sb= new StringBuilder();
final String defAssign= " <- ";
final String text= doc.get(this.expression.getStartOffset(), this.expression.getLength());
final String variableName= this.tempName;
// define var
{ sb.setLength(0);
sb.append(variableName);
sb.append(defAssign);
sb.append(text);
RAstNode baseNode= this.expression;
while (baseNode.getRParent() != this.container) {
baseNode= baseNode.getRParent();
}
final int assignOffset= RRefactoringAdapter.prepareInsertBefore(sb, doc, baseNode.getStartOffset(), this.sourceUnit);
TextChangeCompatibility.addTextEdit(change, Messages.ExtractTemp_Changes_AddVariable_name,
new InsertEdit(assignOffset, sb.toString()) );
m.worked(4);
}
// replace occurrences
{ for (int i= 0; i < this.occurrencesList.size(); i++) {
final RAstNode node= this.occurrencesList.get(i);
TextChangeCompatibility.addTextEdit(change, Messages.ExtractTemp_Changes_ReplaceOccurrence_name,
new ReplaceEdit(node.getStartOffset(), node.getLength(), variableName),
(node == this.expression || this.replaceAllOccurrences));
}
m.worked(4);
}
}
finally {
this.sourceUnit.disconnect(m.newChild(1));
}
}
}