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