package com.redhat.ceylon.eclipse.code.refactor;
import static com.redhat.ceylon.compiler.java.codegen.CodegenUtil.getJavaNameOfDeclaration;
import static com.redhat.ceylon.eclipse.util.DocLinks.nameRegion;
import static com.redhat.ceylon.eclipse.util.JavaSearch.createSearchPattern;
import static com.redhat.ceylon.eclipse.util.JavaSearch.getProjectAndReferencingProjects;
import static com.redhat.ceylon.eclipse.util.JavaSearch.runSearch;
import static com.redhat.ceylon.eclipse.util.Nodes.getIdentifyingNode;
import static com.redhat.ceylon.eclipse.util.Nodes.getReferencedExplicitDeclaration;
import static java.util.Collections.emptyList;
import static org.eclipse.jdt.core.search.IJavaSearchConstants.CLASS_AND_INTERFACE;
import static org.eclipse.jdt.core.search.IJavaSearchConstants.REFERENCES;
import static org.eclipse.jdt.core.search.SearchPattern.R_EXACT_MATCH;
import static org.eclipse.jdt.core.search.SearchPattern.createPattern;
import static org.eclipse.ltk.core.refactoring.RefactoringStatus.createErrorStatus;
import static org.eclipse.ltk.core.refactoring.RefactoringStatus.createWarningStatus;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.antlr.runtime.CommonToken;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchMatch;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.SearchRequestor;
import org.eclipse.jface.text.Region;
import org.eclipse.ltk.core.refactoring.CompositeChange;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.core.refactoring.TextChange;
import org.eclipse.ltk.core.refactoring.TextFileChange;
import org.eclipse.ltk.core.refactoring.resource.RenameResourceChange;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.ui.IEditorPart;
import com.redhat.ceylon.compiler.typechecker.tree.Node;
import com.redhat.ceylon.compiler.typechecker.tree.Tree;
import com.redhat.ceylon.compiler.typechecker.tree.Tree.Identifier;
import com.redhat.ceylon.compiler.typechecker.tree.Visitor;
import com.redhat.ceylon.ide.common.util.escaping_;
import com.redhat.ceylon.model.typechecker.model.ClassOrInterface;
import com.redhat.ceylon.model.typechecker.model.Declaration;
import com.redhat.ceylon.model.typechecker.model.FunctionOrValue;
import com.redhat.ceylon.model.typechecker.model.Referenceable;
import com.redhat.ceylon.model.typechecker.model.Type;
import com.redhat.ceylon.model.typechecker.model.TypeDeclaration;
import com.redhat.ceylon.model.typechecker.model.TypeParameter;
import com.redhat.ceylon.model.typechecker.model.TypedDeclaration;
import com.redhat.ceylon.model.typechecker.model.Value;
public class RenameRefactoring extends AbstractRefactoring {
private static class FindRenamedReferencesVisitor
extends FindReferencesVisitor {
private FindRenamedReferencesVisitor(Declaration declaration) {
super(declaration);
}
@Override
protected boolean isReference(Declaration ref) {
return super.isReference(ref) ||
//include refinements of the selected
//declaration that we're renaming
ref!=null &&
ref.refines((Declaration) getDeclaration());
}
@Override
protected boolean isReference(Declaration ref, String id) {
return isReference(ref) &&
id!=null &&
//ignore references that use an alias
//since we don't need to rename them
getDeclaration().getNameAsString()
.equals(id);
}
@Override
public void visit(Tree.SpecifierStatement that) {
if (that.getRefinement()) {
Tree.Term lhs = that.getBaseMemberExpression();
if (lhs instanceof Tree.ParameterizedExpression) {
Tree.ParameterizedExpression pe =
(Tree.ParameterizedExpression) lhs;
for (Tree.ParameterList pl: pe.getParameterLists()) {
if (pl!=null) {
pl.visit(this);
}
}
Tree.TypeParameterList tpl =
pe.getTypeParameterList();
if (tpl!=null) {
tpl.visit(this);
}
}
//the LHS will be treated as a refinement by
//FindRefinementsVisitor so ignore it here
super.visit(that.getSpecifierExpression());
}
else {
super.visit(that);
}
}
}
private static class FindDocLinkReferencesVisitor
extends Visitor {
private Declaration declaration;
private int count;
public int getCount() {
return count;
}
FindDocLinkReferencesVisitor(Declaration declaration) {
this.declaration = declaration;
}
@Override
public void visit(Tree.DocLink that) {
Declaration base = that.getBase();
if (base!=null) {
if (base.equals(declaration)) {
count++;
}
else {
List<Declaration> qualified =
that.getQualified();
if (qualified!=null) {
if (qualified.contains(declaration)) {
count++;
}
}
}
}
}
}
private final class FindDocLinkVisitor extends Visitor {
private List<Region> links = new ArrayList<Region>();
List<Region> getLinks() {
return links;
}
private void visitIt(Region region, Declaration dec) {
if (dec!=null && dec.equals(declaration)) {
links.add(region);
}
}
@Override
public void visit(Tree.DocLink that) {
Declaration base = that.getBase();
if (base!=null) {
visitIt(nameRegion(that, 0), base);
List<Declaration> qualified =
that.getQualified();
if (qualified!=null) {
for (int i=0;
i<qualified.size();
i++) {
visitIt(nameRegion(that, i+1),
qualified.get(i));
}
}
}
}
}
private final class FindSimilarNameVisitor extends Visitor {
private ArrayList<Identifier> identifiers
= new ArrayList<Identifier>();
public ArrayList<Identifier> getIdentifiers() {
return identifiers;
}
@Override
public void visit(Tree.TypedDeclaration that) {
super.visit(that);
Tree.Identifier id =
that.getIdentifier();
if (id!=null) {
Type type =
that.getType()
.getTypeModel();
if (type!=null) {
TypeDeclaration td =
type.getDeclaration();
if ((td instanceof ClassOrInterface ||
td instanceof TypeParameter) &&
td.equals(declaration)) {
String text = id.getText();
String name = declaration.getName();
if (text.equalsIgnoreCase(name) ||
text.endsWith(name)) {
identifiers.add(id);
}
}
}
}
}
}
private String newName;
private final Declaration declaration;
private boolean renameFile;
private boolean renameValuesAndFunctions;
@Override
int getSaveMode() {
return isAffectingOtherFiles() ?
RefactoringSaveHelper.SAVE_REFACTORING :
RefactoringSaveHelper.SAVE_NOTHING;
}
public Node getNode() {
return node;
}
public RenameRefactoring(IEditorPart editor) {
super(editor);
boolean identifiesDeclaration =
node instanceof Tree.DocLink ||
getIdentifyingNode(node)
instanceof Tree.Identifier;
if (rootNode!=null && identifiesDeclaration) {
Referenceable refDec =
getReferencedExplicitDeclaration(node,
rootNode);
if (refDec instanceof Declaration) {
Declaration dec = (Declaration) refDec;
declaration = dec.getRefinedDeclaration();
newName = declaration.getName();
String filename =
declaration.getUnit()
.getFilename();
renameFile =
(declaration.getName() + ".ceylon")
.equals(filename);
}
else {
declaration = null;
}
}
else {
declaration = null;
}
}
@Override
public boolean getEnabled() {
return declaration instanceof Declaration &&
declaration.getName()!=null &&
project != null &&
(inSameProject(declaration) || inSameUnit());
}
private boolean inSameUnit() {
return getEditable() &&
declaration.getUnit()
.equals(rootNode.getUnit());
}
public int getCount() {
return declaration==null ? 0 :
countDeclarationOccurrences();
}
@Override
int countReferences(Tree.CompilationUnit cu) {
FindRenamedReferencesVisitor frv =
new FindRenamedReferencesVisitor(declaration);
Declaration dec =
(Declaration) frv.getDeclaration();
FindRefinementsVisitor fdv =
new FindRefinementsVisitor(dec);
FindDocLinkReferencesVisitor fdlrv =
new FindDocLinkReferencesVisitor(dec);
cu.visit(frv);
cu.visit(fdv);
cu.visit(fdlrv);
return frv.getNodes().size() +
fdv.getDeclarationNodes().size() +
fdlrv.getCount();
}
public String getName() {
return "Rename";
}
public RefactoringStatus checkInitialConditions(
IProgressMonitor pm)
throws CoreException,
OperationCanceledException {
// Check parameters retrieved from editor context
return new RefactoringStatus();
}
public RefactoringStatus checkFinalConditions(
IProgressMonitor pm)
throws CoreException,
OperationCanceledException {
if (!newName.matches("^[a-zA-Z_]\\w*$")) {
return createErrorStatus(
"Not a legal Ceylon identifier");
}
else if (escaping_.get_().isKeyword(newName)) {
return createErrorStatus(
"'" + newName + "' is a Ceylon keyword");
}
else {
int ch = newName.codePointAt(0);
if (declaration instanceof TypedDeclaration) {
if (!Character.isLowerCase(ch) && ch!='_') {
return createErrorStatus(
"Not an initial lowercase identifier");
}
}
else if (declaration instanceof TypeDeclaration) {
if (!Character.isUpperCase(ch)) {
return createErrorStatus(
"Not an initial uppercase identifier");
}
}
}
Declaration existing =
declaration.getContainer()
.getMemberOrParameter(
declaration.getUnit(),
newName, null, false);
if (null!=existing &&
!existing.equals(declaration)) {
return createWarningStatus(
"An existing declaration named '" +
newName +
"' already exists in the same scope");
}
return new RefactoringStatus();
}
public CompositeChange createChange(IProgressMonitor pm)
throws CoreException,
OperationCanceledException {
CompositeChange change =
(CompositeChange)
super.createChange(pm);
if (project!=null && renameFile) {
renameSourceFile(change);
}
refactorJavaReferences(pm, change);
return change;
}
private void renameSourceFile(CompositeChange change) {
String unitPath =
declaration.getUnit()
.getFullPath();
IPath oldPath =
project.getFullPath()
.append(unitPath);
String newFileName = getNewName() + ".ceylon";
IPath newPath =
oldPath.removeFirstSegments(1)
.removeLastSegments(1)
.append(newFileName);
if (!project.getFile(newPath).exists()) {
change.add(new RenameResourceChange(
oldPath, newFileName));
}
}
protected void refactorJavaReferences(IProgressMonitor pm,
final CompositeChange cc) {
final Map<IResource,TextChange> changes =
new HashMap<IResource, TextChange>();
SearchEngine searchEngine = new SearchEngine();
IProject[] projects =
getProjectAndReferencingProjects(project);
final String pattern;
try {
pattern = getJavaNameOfDeclaration(declaration);
}
catch (Exception e) {
return;
}
boolean anonymous = pattern.endsWith(".get_");
if (!anonymous) {
SearchPattern searchPattern =
createSearchPattern(declaration,
REFERENCES);
if (searchPattern==null) return;
SearchRequestor requestor =
new SearchRequestor() {
@Override
public void acceptSearchMatch(SearchMatch match) {
String filename =
match.getResource().getName();
boolean isJavaFile =
JavaCore.isJavaLikeFileName(
filename);
if (isJavaFile) {
TextChange change =
canonicalChange(cc, changes, match);
if (change!=null) {
int loc = pattern.lastIndexOf('.') + 1;
String oldName =
pattern.substring(loc);
if (declaration instanceof Value) {
change.addEdit(new ReplaceEdit(
match.getOffset() + 3,
oldName.length() - 3,
escaping_.get_().toInitialUppercase(newName)));
}
else {
change.addEdit(new ReplaceEdit(
match.getOffset(),
oldName.length(),
oldName.startsWith("$") ?
'$' + newName :
newName));
}
}
}
}
};
runSearch(pm, searchEngine, searchPattern,
projects, requestor);
}
if (anonymous ||
declaration instanceof FunctionOrValue &&
declaration.isToplevel()) {
int loc = pattern.lastIndexOf('.');
SearchPattern searchPattern =
createPattern(pattern.substring(0, loc),
CLASS_AND_INTERFACE,
REFERENCES,
R_EXACT_MATCH);
SearchRequestor requestor =
new SearchRequestor() {
@Override
public void acceptSearchMatch(SearchMatch match) {
TextChange change =
canonicalChange(cc, changes, match);
if (change!=null) {
int end =
pattern.lastIndexOf("_.");
int start =
pattern.substring(0, end)
.lastIndexOf('.') +1 ;
String oldName =
pattern.substring(start, end);
change.addEdit(new ReplaceEdit(
match.getOffset(),
oldName.length(),
newName));
}
}
};
runSearch(pm, searchEngine, searchPattern,
projects, requestor);
}
}
private TextChange canonicalChange(
CompositeChange composite,
Map<IResource, TextChange> changes,
SearchMatch match) {
IResource resource = match.getResource();
if (resource instanceof IFile) {
TextChange change = changes.get(resource);
if (change==null) {
IFile file = (IFile) resource;
change = new TextFileChange("Rename", file);
change.setEdit(new MultiTextEdit());
changes.put(resource, change);
composite.add(change);
}
return change;
}
else {
return null;
}
}
@Override
protected void refactorInFile(
TextChange tfc, CompositeChange cc,
Tree.CompilationUnit root,
List<CommonToken> tokens) {
tfc.setEdit(new MultiTextEdit());
if (declaration!=null) {
for (Node node: getNodesToRename(root)) {
renameNode(tfc, node, root);
}
for (Region region: getStringsToReplace(root)) {
renameRegion(tfc, region, root);
}
if (renameValuesAndFunctions) {
for (Tree.Identifier id:
getIdentifiersToRename(root)) {
renameIdentifier(tfc, id, root);
}
}
}
if (tfc.getEdit().hasChildren()) {
cc.add(tfc);
}
}
public List<Node> getNodesToRename(
Tree.CompilationUnit root) {
ArrayList<Node> list = new ArrayList<Node>();
FindRenamedReferencesVisitor frv =
new FindRenamedReferencesVisitor(
declaration);
root.visit(frv);
list.addAll(frv.getNodes());
FindRefinementsVisitor fdv =
new FindRefinementsVisitor(
(Declaration)
frv.getDeclaration());
root.visit(fdv);
list.addAll(fdv.getDeclarationNodes());
return list;
}
public List<Tree.Identifier> getIdentifiersToRename(
Tree.CompilationUnit root) {
if (declaration instanceof TypeDeclaration) {
FindSimilarNameVisitor fsnv =
new FindSimilarNameVisitor();
fsnv.visit(root);
return fsnv.getIdentifiers();
}
else {
return emptyList();
}
}
public List<Region> getStringsToReplace(
Tree.CompilationUnit root) {
FindDocLinkVisitor fdlv = new FindDocLinkVisitor();
fdlv.visit(root);
return fdlv.getLinks();
}
protected void renameIdentifier(TextChange tfc,
Tree.Identifier id, Tree.CompilationUnit root) {
String name = declaration.getName();
int loc = id.getText().indexOf(name);
int start = id.getStartIndex();
int len = id.getDistance();
if (loc>0) {
tfc.addEdit(new ReplaceEdit(
start + loc, name.length(),
newName));
}
else {
tfc.addEdit(new ReplaceEdit(
start, len,
escaping_.get_().toInitialLowercase(newName)));
}
}
protected void renameRegion(TextChange tfc, Region region,
Tree.CompilationUnit root) {
tfc.addEdit(new ReplaceEdit(
region.getOffset(),
region.getLength(),
newName));
}
protected void renameNode(TextChange tfc, Node node,
Tree.CompilationUnit root) {
Node identifyingNode = getIdentifier(node);
tfc.addEdit(new ReplaceEdit(
identifyingNode.getStartIndex(),
identifyingNode.getDistance(),
newName));
}
protected static Node getIdentifier(Node node) {
if (node instanceof Tree.SpecifierStatement) {
Tree.SpecifierStatement st =
(Tree.SpecifierStatement) node;
Tree.Term lhs = st.getBaseMemberExpression();
while (lhs instanceof Tree.ParameterizedExpression) {
Tree.ParameterizedExpression pe =
(Tree.ParameterizedExpression) lhs;
lhs = pe.getPrimary();
}
if (lhs instanceof Tree.StaticMemberOrTypeExpression) {
Tree.StaticMemberOrTypeExpression mte =
(Tree.StaticMemberOrTypeExpression) lhs;
return mte.getIdentifier();
}
else {
throw new RuntimeException("impossible");
}
}
else {
return getIdentifyingNode(node);
}
}
public boolean isRenameValuesAndFunctions() {
return renameValuesAndFunctions;
}
public void setRenameValuesAndFunctions(boolean renameLocals) {
this.renameValuesAndFunctions = renameLocals;
}
public void setNewName(String text) {
newName = text;
}
public Declaration getDeclaration() {
return declaration;
}
@Override
public boolean isAffectingOtherFiles() {
if (declaration==null) {
return false;
}
if (declaration.isToplevel() ||
declaration.isShared()) {
return true;
}
if (declaration.isParameter()) {
FunctionOrValue fov =
(FunctionOrValue)
declaration;
Declaration container =
fov.getInitializerParameter()
.getDeclaration();
if (container.isToplevel() ||
container.isShared()) {
return true;
}
}
return false;
}
public String getNewName() {
return newName;
}
public boolean isRenameFile() {
return renameFile;
}
public void setRenameFile(boolean renameFile) {
this.renameFile = renameFile;
}
}