| /*=============================================================================# |
| # 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)); |
| } |
| } |
| |
| } |