blob: 0bf527ed360cbe9189b601afc1d01974e84ff0fc [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2008, 2020 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.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.DeleteEdit;
import org.eclipse.text.edits.InsertEdit;
import org.eclipse.statet.jcommons.text.core.BasicTextRegion;
import org.eclipse.statet.jcommons.text.core.TextRegion;
import org.eclipse.statet.internal.r.core.refactoring.Messages;
import org.eclipse.statet.ltk.ast.core.AstNode;
import org.eclipse.statet.ltk.core.LTK;
import org.eclipse.statet.ltk.core.LTKUtils;
import org.eclipse.statet.ltk.model.core.ElementSet;
import org.eclipse.statet.ltk.model.core.elements.ISourceStructElement;
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.RCodeStyleSettings;
import org.eclipse.statet.r.core.RCore;
import org.eclipse.statet.r.core.RUtil;
import org.eclipse.statet.r.core.model.ArgsDefinition;
import org.eclipse.statet.r.core.model.ArgsDefinition.Arg;
import org.eclipse.statet.r.core.model.IRElement;
import org.eclipse.statet.r.core.model.IRMethod;
import org.eclipse.statet.r.core.model.IRModelInfo;
import org.eclipse.statet.r.core.model.IRModelManager;
import org.eclipse.statet.r.core.model.IRSourceUnit;
import org.eclipse.statet.r.core.model.RElementName;
import org.eclipse.statet.r.core.model.RModel;
import org.eclipse.statet.r.core.rlang.RTerminal;
import org.eclipse.statet.r.core.rsource.ast.FDef;
import org.eclipse.statet.r.core.rsource.ast.NodeType;
import org.eclipse.statet.r.core.rsource.ast.RAst;
import org.eclipse.statet.r.core.rsource.ast.RAstNode;
import org.eclipse.statet.r.core.source.RHeuristicTokenScanner;
public class FunctionToS4MethodRefactoring extends Refactoring {
public class Variable {
private final Arg arg;
private boolean asGenericArgument;
private boolean asGenericArgumentDefault;
private String argumentType;
public Variable(final Arg arg) {
this.arg= arg;
}
void init(final boolean enable) {
this.asGenericArgumentDefault= this.asGenericArgument= enable;
}
public String getName() {
return (this.arg.name != null) ? this.arg.name : ""; //$NON-NLS-1$
}
public boolean getUseAsGenericArgumentDefault() {
return this.asGenericArgumentDefault;
}
public boolean getUseAsGenericArgument() {
return this.asGenericArgument;
}
public void setUseAsGenericArgument(final boolean enable) {
this.asGenericArgument= enable;
}
public String getArgumentType() {
return this.argumentType;
}
public void setArgumentType(final String typeName) {
if (typeName != null && typeName.trim().length() > 0) {
this.argumentType= typeName;
}
else {
this.argumentType= null;
}
}
}
private final RRefactoringAdapter adapter= new RRefactoringAdapter();
private final ElementSet elementSet;
private TextRegion selectionRegion;
private TextRegion operationRegion;
private final IRSourceUnit sourceUnit;
private IRMethod function;
// private RAstNode container;
private List<Variable> variablesList;
private String functionName= ""; //$NON-NLS-1$
private boolean generateGeneric= true;
/**
* Creates a new converting refactoring.
* @param su the source unit
* @param region (selected) region of the function to convert
*/
public FunctionToS4MethodRefactoring(final IRSourceUnit su, final TextRegion selection) {
this.sourceUnit= su;
this.elementSet= new ElementSet(new Object[] { su });
if (selection != null && selection.getStartOffset() >= 0 && selection.getLength() >= 0) {
this.selectionRegion= selection;
}
}
@Override
public String getName() {
return Messages.FunctionToS4Method_label;
}
public String getIdentifier() {
return RRefactoring.EXTRACT_FUNCTION_REFACTORING_ID;
}
public void setFunctionName(final String newName) {
this.functionName= newName;
}
public String getFunctionName() {
return this.functionName;
}
public List<Variable> getVariables() {
return this.variablesList;
}
public void setGenerateGeneric(final boolean enable) {
this.generateGeneric= enable;
}
public boolean getGenerateGeneric() {
return this.generateGeneric;
}
@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 IRModelInfo modelInfo= (IRModelInfo) this.sourceUnit.getModelInfo(RModel.R_TYPE_ID, IRModelManager.MODEL_FILE, m.newChild(1));
if (modelInfo != null) {
final TextRegion region= this.adapter.trimToAstRegion(document,
this.selectionRegion, scanner );
ISourceStructElement element= LTKUtils.getCoveringSourceElement(
modelInfo.getSourceElement(), region );
while (element != null) {
if (element instanceof IRMethod) {
this.function= (IRMethod) element;
break;
}
element= element.getSourceParent();
}
}
if (this.function != null) {
final ISourceStructElement source= (ISourceStructElement) this.function;
this.operationRegion= this.adapter.expandSelectionRegion(document,
source.getSourceRange(), this.selectionRegion, scanner );
}
}
finally {
this.sourceUnit.disconnect(m.newChild(1));
}
}
if (this.function == null) {
return RefactoringStatus.createFatalErrorStatus(Messages.FunctionToS4Method_error_InvalidSelection_message);
}
final RefactoringStatus result= new RefactoringStatus();
this.adapter.checkInitialToModify(result, this.elementSet);
m.worked(1);
if (result.hasFatalError()) {
return result;
}
checkFunction(result);
m.worked(2);
return result;
}
finally {
m.done();
}
}
private void checkFunction(final RefactoringStatus result) {
if ((this.function.getElementType() & IRElement.MASK_C2) != IRElement.R_COMMON_FUNCTION
&& (this.function.getElementType() & IRElement.MASK_C2) != IRElement.R_COMMON_FUNCTION) {
result.merge(RefactoringStatus.createFatalErrorStatus(Messages.FunctionToS4Method_error_SelectionAlreadyS4_message));
return;
}
final RAstNode node= (RAstNode) this.function.getAdapter(AstNode.class);
if (RAst.hasErrors(node)) {
result.merge(RefactoringStatus.createWarningStatus(Messages.FunctionToS4Method_warning_SelectionSyntaxError_message));
}
// if (fSelectionRegion != null
// && (fSelectionRegion.getOffset() != this.operationRegion.getOffset()
// || this.selectionRegion.getLength() != this.operationRegion.getLength() )) {
// result.merge(RefactoringStatus.createWarningStatus("The selected code does not equal exactly the found expression(s)."));
// }
final RElementName elementName= this.function.getElementName().getLastSegment();
this.functionName= elementName.getDisplayName();
final ArgsDefinition argsDef= this.function.getArgsDefinition();
final int count= (argsDef != null) ? argsDef.size() : 0;
this.variablesList= new ArrayList<>(count);
boolean dots= false;
for (int i= 0; i < count; i++) {
final Arg arg= argsDef.get(i);
final Variable variable= new Variable(arg);
if (variable.getName().equals(RTerminal.S_ELLIPSIS)) {
dots= true;
variable.init(true);
}
else {
variable.init(!dots);
}
this.variablesList.add(variable);
}
}
public RefactoringStatus checkFunctionName(final String newName) {
if (newName == null || newName.isEmpty()) {
return RefactoringStatus.createFatalErrorStatus(
NLS.bind(Messages.RIdentifiers_error_EmptyFor_message, "The function name"));
}
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= checkFunctionName(this.functionName);
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.functionName);
final String description= NLS.bind(Messages.FunctionToS4Method_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);
return new RefactoringChange(descriptor,
Messages.FunctionToS4Method_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 + 3 * 4);
this.sourceUnit.connect(m.newChild(1));
try {
final AbstractDocument document= this.sourceUnit.getDocument(m.newChild(1));
final RHeuristicTokenScanner scanner= this.adapter.getScanner(this.sourceUnit);
final RCodeStyleSettings codeStyle= RRefactoringAdapter.getCodeStyle(this.sourceUnit);
final StringBuilder sb= new StringBuilder();
final String nl= document.getDefaultLineDelimiter();
final String argAssign= codeStyle.getArgAssignString();
RAstNode firstParentChild= (RAstNode) this.function.getAdapter(AstNode.class);
while (true) {
final RAstNode parent= firstParentChild.getRParent();
if (parent == null
|| parent.getNodeType() == NodeType.SOURCELINES || parent.getNodeType() == NodeType.BLOCK) {
break;
}
firstParentChild= parent;
}
final TextRegion region= this.adapter.expandWhitespaceBlock(document, this.operationRegion, scanner);
final int insertOffset= this.adapter.expandWhitespaceBlock(document,
this.adapter.expandSelectionRegion(document,
new BasicTextRegion(firstParentChild.getStartOffset()), this.operationRegion, scanner ),
scanner ).getStartOffset();
final FDef fdefNode= this.function.getAdapter(FDef.class);
final TextRegion fbodyRegion= this.adapter.expandWhitespaceBlock(document,
this.adapter.expandSelectionRegion(document,
fdefNode.getContChild(), this.operationRegion, scanner ),
scanner );
{ TextChangeCompatibility.addTextEdit(change, Messages.FunctionToS4Method_Changes_DeleteOld_name,
new DeleteEdit(region.getStartOffset(), region.getLength()));
m.worked(4);
}
{ sb.setLength(0);
sb.append("setGeneric(\""); //$NON-NLS-1$
sb.append(this.functionName);
sb.append("\","); //$NON-NLS-1$
sb.append(nl);
sb.append("function("); //$NON-NLS-1$
boolean dots= false;
for (final Variable variable : this.variablesList) {
if (variable.getName().equals(RTerminal.S_ELLIPSIS)) {
dots= true;
}
if (variable.getUseAsGenericArgument()) {
sb.append(RElementName.create(RElementName.MAIN_DEFAULT, variable.getName()).getDisplayName());
sb.append(", "); //$NON-NLS-1$
}
}
if (!dots) {
sb.append("..., "); //$NON-NLS-1$
}
sb.delete(sb.length() - 2, sb.length());
sb.append(')');
if (codeStyle.getNewlineFDefBodyBlockBefore()) {
sb.append(nl);
}
else {
sb.append(' ');
}
sb.append('{');
sb.append(nl);
sb.append("standardGeneric(\""); //$NON-NLS-1$
sb.append(this.functionName);
sb.append("\")"); //$NON-NLS-1$
sb.append(nl);
sb.append("})"); //$NON-NLS-1$
sb.append(nl);
sb.append(nl);
final String genericDef= RRefactoringAdapter.indent(sb, document,
firstParentChild.getStartOffset(),
this.sourceUnit, scanner );
TextChangeCompatibility.addTextEdit(change, Messages.FunctionToS4Method_Changes_AddGenericDef_name,
new InsertEdit(insertOffset, genericDef));
m.worked(4);
}
{ sb.setLength(0);
sb.append("setMethod(\""); //$NON-NLS-1$
sb.append(this.functionName);
sb.append("\","); //$NON-NLS-1$
sb.append(nl);
sb.append("signature("); //$NON-NLS-1$
boolean hasType= false;
for (final Variable variable : this.variablesList) {
if (variable.getUseAsGenericArgument() && variable.getArgumentType() != null) {
hasType= true;
sb.append(RElementName.create(RElementName.MAIN_DEFAULT, variable.getName()).getDisplayName());
sb.append(argAssign);
sb.append("\""); //$NON-NLS-1$
sb.append(variable.getArgumentType());
sb.append("\", "); //$NON-NLS-1$
}
}
if (hasType) {
sb.delete(sb.length() - 2, sb.length());
}
sb.append("),"); //$NON-NLS-1$
sb.append(nl);
sb.append("function("); //$NON-NLS-1$
final FDef.Args argsNode= fdefNode.getArgsChild();
for (final Variable variable : this.variablesList) {
sb.append(RElementName.create(RElementName.MAIN_DEFAULT, variable.getName()).getDisplayName());
final FDef.Arg argNode= argsNode.getChild(variable.arg.index);
if (argNode.hasDefault()) {
sb.append(argAssign);
sb.append(document.get(argNode.getDefaultChild().getStartOffset(), argNode.getDefaultChild().getLength()));
}
sb.append(", "); //$NON-NLS-1$
}
if (!this.variablesList.isEmpty()) {
sb.delete(sb.length() - 2, sb.length());
}
sb.append(')');
if (codeStyle.getNewlineFDefBodyBlockBefore()
|| fdefNode.getContChild().getNodeType() != NodeType.BLOCK) {
sb.append(nl);
}
else {
sb.append(' ');
}
sb.append(document.get(fbodyRegion.getStartOffset(), fbodyRegion.getLength()).trim());
sb.append(")"); //$NON-NLS-1$
sb.append(nl);
final String methodDef= RRefactoringAdapter.indent(sb, document,
firstParentChild.getStartOffset(),
this.sourceUnit, scanner );
TextChangeCompatibility.addTextEdit(change, Messages.FunctionToS4Method_Changes_AddMethodDef_name,
new InsertEdit(insertOffset, methodDef));
m.worked(4);
}
}
finally {
this.sourceUnit.disconnect(m.newChild(1));
}
}
}