blob: 8267bcd2d232a927cfe8fec45960a5d8cab371ce [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.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);
}
}
}
}