| /*=============================================================================# |
| # 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.lang.reflect.InvocationTargetException; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| 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.GroupCategory; |
| import org.eclipse.ltk.core.refactoring.GroupCategorySet; |
| 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.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.IRFrame; |
| import org.eclipse.statet.r.core.model.IRFrameInSource; |
| 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.RElementAccess; |
| 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.GenericVisitor; |
| 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 RenameInRegionRefactoring extends Refactoring { |
| |
| |
| private class VariableSearcher extends GenericVisitor { |
| |
| private final int start= RenameInRegionRefactoring.this.selectionRegion.getStartOffset(); |
| private final int stop= RenameInRegionRefactoring.this.selectionRegion.getEndOffset(); |
| |
| @Override |
| public void visitNode(final RAstNode node) throws InvocationTargetException { |
| if (node.getStartOffset() >= this.stop || node.getEndOffset() < this.start) { |
| return; |
| } |
| final List<Object> attachments= node.getAttachments(); |
| for (final Object attachment : attachments) { |
| if (attachment instanceof RElementAccess) { |
| final RElementAccess access= (RElementAccess) attachment; |
| if (access.getType() != RElementName.MAIN_DEFAULT) { |
| continue; |
| } |
| final RAstNode nameNode= access.getNameNode(); |
| if (nameNode != null |
| && nameNode.getStartOffset() >= this.start && nameNode.getEndOffset() <= this.stop) { |
| add(access); |
| } |
| } |
| } |
| node.acceptInRChildren(this); |
| } |
| |
| private void add(final RElementAccess access) { |
| final IRFrame frame= access.getFrame(); |
| if (!(frame instanceof IRFrameInSource) |
| || frame.getFrameType() == IRFrame.PACKAGE) { |
| return; |
| } |
| Map<String, Variable> map= RenameInRegionRefactoring.this.variablesList.get(frame); |
| if (map == null) { |
| map= new HashMap<>(); |
| RenameInRegionRefactoring.this.variablesList.put(frame, map); |
| } |
| final String name= access.getSegmentName(); |
| Variable variable= map.get(name); |
| if (variable == null) { |
| variable= new Variable(frame, name); |
| map.put(name, variable); |
| } |
| variable.accessList.add(access); |
| } |
| |
| } |
| |
| public class Variable { |
| |
| |
| private final Object parent; |
| |
| private final String name; |
| private String newName; |
| |
| private final List<RElementAccess> accessList; |
| |
| private Map<String, Variable> subVariables= Collections.emptyMap(); |
| |
| public Variable(final Object parent, final String name) { |
| this.parent= parent; |
| this.name= name; |
| this.accessList= new ArrayList<>(); |
| } |
| |
| |
| public Object getParent() { |
| return this.parent; |
| } |
| |
| public String getName() { |
| return this.name; |
| } |
| |
| public String getNewName() { |
| return this.newName; |
| } |
| |
| public void setNewName(final String name) { |
| if (!this.name.equals(name)) { |
| this.newName= name; |
| } |
| else { |
| this.newName= null; |
| } |
| } |
| |
| public int getOccurrencesCount() { |
| return this.accessList.size(); |
| } |
| |
| |
| public Map<String, Variable> getSubVariables() { |
| return this.subVariables; |
| } |
| |
| } |
| |
| |
| private final RRefactoringAdapter adapter= new RRefactoringAdapter(); |
| private final ElementSet elementSet; |
| |
| private TextRegion selectionRegion; |
| |
| private final IRSourceUnit sourceUnit; |
| |
| private Map<IRFrame, Map<String, Variable>> variablesList; |
| |
| |
| /** |
| * Creates a new rename refactoring. |
| * @param su the source unit |
| * @param region (selected) region |
| */ |
| public RenameInRegionRefactoring(final IRSourceUnit 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.RenameInRegion_label; |
| } |
| |
| public String getIdentifier() { |
| return RRefactoring.RENAME_IN_REGION_REFACTORING_ID; |
| } |
| |
| public Map<IRFrame, Map<String, Variable>> getVariables() { |
| return this.variablesList; |
| } |
| |
| |
| @Override |
| public RefactoringStatus checkInitialConditions(final IProgressMonitor monitor) throws CoreException { |
| final SubMonitor m= SubMonitor.convert(monitor, 6); |
| RAstNode rootNode= null; |
| 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 ); |
| final AstInfo ast= modelInfo.getAst(); |
| if (ast != null) { |
| rootNode= (RAstNode) AstSelection.search(ast.getRoot(), |
| region.getStartOffset(), region.getEndOffset(), |
| AstSelection.MODE_COVERING_SAME_LAST ).getCovering(); |
| } |
| } |
| } |
| finally { |
| this.sourceUnit.disconnect(m.newChild(1)); |
| } |
| } |
| |
| if (rootNode == 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; |
| } |
| |
| searchVariables(rootNode, result); |
| m.worked(2); |
| return result; |
| } |
| finally { |
| m.done(); |
| } |
| } |
| |
| private void searchVariables(final RAstNode rootNode, final RefactoringStatus result) { |
| this.variablesList= new HashMap<>(); |
| final VariableSearcher searcher= new VariableSearcher(); |
| try { |
| rootNode.acceptInR(searcher); |
| } |
| catch (final InvocationTargetException e) {} |
| for (final Map<String, Variable> map : this.variablesList.values()) { |
| for (final Variable var : map.values()) { |
| checkVariables(var); |
| } |
| } |
| } |
| |
| private void checkVariables(final Variable var) { |
| Map<String, RenameInRegionRefactoring.Variable> map= null; |
| for (final RElementAccess access : var.accessList) { |
| final RElementAccess next= access.getNextSegment(); |
| if (next != null && next.getSegmentName() != null |
| && (next.getType() == RElementName.SUB_NAMEDPART |
| || next.getType() == RElementName.SUB_NAMEDSLOT )) { |
| if (map == null) { |
| map= new HashMap<>(); |
| } |
| Variable sub= map.get(next.getSegmentName()); |
| if (sub == null) { |
| sub= new Variable(var, next.getSegmentName()); |
| map.put(next.getSegmentName(), sub); |
| } |
| next.getSegmentName(); |
| next.getFrame(); |
| sub.accessList.add(next); |
| } |
| } |
| if (map != null) { |
| var.subVariables= map; |
| } |
| } |
| |
| |
| @Override |
| public RefactoringStatus checkFinalConditions(final IProgressMonitor monitor) throws CoreException { |
| final SubMonitor m= SubMonitor.convert(monitor, RefactoringMessages.Common_FinalCheck_label, 3); |
| try { |
| final RefactoringStatus status= new RefactoringStatus(); |
| 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); |
| } |
| final List<String> variableNames= createChanges(textFileChange, m.newChild(2)); |
| |
| final Map<String, String> arguments= new HashMap<>(); |
| final String description= NLS.bind(Messages.RenameInRegion_Descriptor_description, |
| RUtil.formatVarNames(variableNames)); |
| 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.RenameInRegion_label, |
| new Change[] { textFileChange }); |
| } |
| catch (final BadLocationException e) { |
| throw new CoreException(new Status(IStatus.ERROR, RCore.BUNDLE_ID, "Unexpected error (concurrent change?)", e)); |
| } |
| finally { |
| monitor.done(); |
| } |
| } |
| |
| private List<String> createChanges(final TextFileChange change, final SubMonitor m) throws BadLocationException { |
| m.setWorkRemaining(2 + 8); |
| |
| final List<String> names= new ArrayList<>(); |
| |
| this.sourceUnit.connect(m.newChild(1)); |
| try { |
| final SubMonitor m1= m.newChild(8).setWorkRemaining(this.variablesList.size()); |
| for (final Map<String, Variable> frameList : this.variablesList.values()) { |
| createMainChanges(frameList, change, names); |
| m1.worked(1); |
| } |
| return names; |
| } |
| finally { |
| this.sourceUnit.disconnect(m.newChild(1)); |
| } |
| } |
| |
| private void createMainChanges(final Map<String, Variable> frameList, |
| final TextFileChange change, final List<String> names) { |
| for (final Variable variable : frameList.values()) { |
| if (variable.newName != null) { |
| final String oldName= RRefactoringAdapter.getUnquotedIdentifier(variable.name); |
| final String oldMsgName= RUtil.formatVarName(oldName); |
| final boolean isQuoted= (variable.newName.charAt(0) == '`'); |
| final GroupCategorySet set= new GroupCategorySet(new GroupCategory( |
| ((IRFrameInSource) variable.getParent()).getFrameId() + '$' + variable.name, |
| NLS.bind(Messages.RenameInRegion_Changes_VariableGroup_name, oldMsgName), "")); //$NON-NLS-1$ |
| final String message= NLS.bind(Messages.RenameInRegion_Changes_ReplaceOccurrence_name, |
| oldMsgName); |
| |
| for (final RElementAccess access : variable.accessList) { |
| final RAstNode nameNode= access.getNameNode(); |
| if (nameNode == null) { |
| continue; |
| } |
| final String text= (isQuoted && nameNode.getNodeType() == NodeType.SYMBOL && nameNode.getOperator(0) == RTerminal.SYMBOL) ? |
| variable.newName : RRefactoringAdapter.getUnquotedIdentifier(variable.newName); |
| final TextRegion nameRegion= RAst.getElementNameRegion(nameNode); |
| TextChangeCompatibility.addTextEdit(change, message, |
| new ReplaceEdit(nameRegion.getStartOffset(), nameRegion.getLength(), text), |
| set ); |
| |
| } |
| names.add(oldName); |
| } |
| if (!variable.subVariables.isEmpty()) { |
| createSubChanges(variable, change, names); |
| } |
| } |
| } |
| |
| private void createSubChanges(final Variable parent, final TextFileChange change, final List<String> names) { |
| final String parentMsgName= RUtil.formatVarName( |
| RRefactoringAdapter.getUnquotedIdentifier(parent.name) ); |
| for (final Variable variable : parent.subVariables.values()) { |
| if (variable.newName != null) { |
| final String oldName= RRefactoringAdapter.getUnquotedIdentifier(variable.name); |
| final String oldMsgName= RUtil.formatVarName(oldName); |
| final boolean isQuoted= (variable.newName.charAt(0) == '`'); |
| final GroupCategorySet set= new GroupCategorySet(new GroupCategory( |
| ((IRFrameInSource) parent.getParent()).getFrameId() + '$' + parent.name, |
| NLS.bind(Messages.RenameInRegion_Changes_VariableGroup_name, parentMsgName), "")); //$NON-NLS-1$ |
| final String message= NLS.bind(Messages.RenameInRegion_Changes_ReplaceOccurrenceOf_name, |
| oldMsgName, parentMsgName ); |
| |
| for (final RElementAccess access : variable.accessList) { |
| final RAstNode nameNode= access.getNameNode(); |
| if (nameNode == null) { |
| continue; |
| } |
| final String text= (isQuoted && nameNode.getNodeType() == NodeType.SYMBOL && nameNode.getOperator(0) == RTerminal.SYMBOL) ? |
| variable.newName : RRefactoringAdapter.getUnquotedIdentifier(variable.newName); |
| final TextRegion nameRegion= RAst.getElementNameRegion(nameNode); |
| TextChangeCompatibility.addTextEdit(change, message, |
| new ReplaceEdit(nameRegion.getStartOffset(), nameRegion.getLength(), text), |
| set ); |
| } |
| names.add(oldName); |
| } |
| } |
| } |
| |
| } |