Donnerstag, 25. Februar 2010

Rename Refactoring for Xtext DSLs

Today, i kind of finished hacking together a rename refactoring for an Xtext DSL i'm working on. It may be kind of messy in some places, but anyway, here it is. In a few places, it is DSL specific and so far it only processes the open file. But it should be easy to adapt (and clean up). Please feel free to copy, adapt, generalize or whatever.

First, there is the rename processor "RenameProcessor.java":

package mysomething.refactoring.rename;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
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.emf.common.util.URI;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.CompositeChange;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.core.refactoring.TextFileChange;
import org.eclipse.ltk.core.refactoring.participants.CheckConditionsContext;
import org.eclipse.ltk.core.refactoring.participants.ProcessorBasedRefactoring;
import org.eclipse.ltk.core.refactoring.participants.RefactoringParticipant;
import org.eclipse.ltk.core.refactoring.participants.SharableParticipants;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEditGroup;
import org.eclipse.xtext.parsetree.CompositeNode;
import org.eclipse.xtext.parsetree.NodeAdapter;
import org.eclipse.xtext.parsetree.NodeUtil;
import org.eclipse.xtext.resource.EObjectAtOffsetHelper;
import org.eclipse.xtext.resource.IEObjectDescription;
import org.eclipse.xtext.resource.IReferenceDescription;
import org.eclipse.xtext.resource.IResourceDescription;
import org.eclipse.xtext.resource.IResourceDescriptions;
import org.eclipse.xtext.resource.XtextResource;
import org.eclipse.xtext.ui.core.editor.XtextEditor;
import org.eclipse.xtext.util.concurrent.IUnitOfWork;
import mysomething.Rule;
import mysomething.RuleReference;
import mysomething.utils.EObjectResolver;
import mysomething.utils.URIFragmentResolver;

import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;

public class RenameProcessor extends
org.eclipse.ltk.core.refactoring.participants.RenameProcessor {

private XtextEditor editor;
private IResourceDescriptions resourceDescriptions;
private List<CompositeNode> references = null;
private String currentName;
private CompositeNode declaration;
private IFile file;

public RenameProcessor(XtextEditor editor,
IResourceDescriptions resourceDescriptions) {
this.editor = editor;
this.resourceDescriptions = resourceDescriptions;

final ITextSelection selection = (ITextSelection) editor
.getSelectionProvider().getSelection();
final IEObjectDescription eObjectDescription = editor.getDocument()
.readOnly(new EObjectResolver(selection, resourceDescriptions));

declaration = editor.getDocument().readOnly(
new URIFragmentResolver(eObjectDescription.getEObjectURI()
.fragment()));

if (declaration.getElement() instanceof Rule) {
Rule r = (Rule) declaration.getElement();
currentName = r.getName();
}

file = (IFile) editor.getEditorInput().getAdapter(IFile.class);

if (eObjectDescription != null) {
references = findReferenceDescriptions(eObjectDescription);
}

}

@Override
public Object[] getElements() {
return null;
}

@Override
public String getIdentifier() {
return "Rename Processor Identifier";
}

@Override
public String getProcessorName() {
return "Rename Processor";
}

@Override
public boolean isApplicable() throws CoreException {
return (references != null);
}

@Override
public RefactoringStatus checkInitialConditions(IProgressMonitor pm)
throws CoreException, OperationCanceledException {
RefactoringStatus status = new RefactoringStatus();

if (references == null)
status.addFatalError("Could not obtain references!");

return status;
}

@Override
public RefactoringStatus checkFinalConditions(IProgressMonitor pm,
CheckConditionsContext context) throws CoreException,
OperationCanceledException {
RefactoringStatus status = new RefactoringStatus();
RenameRefactoring refactoring = (RenameRefactoring) getRefactoring();
if (currentName.equals(refactoring.getRenameText())) {
status.addFatalError("Name unchanged!");
} else if (refactoring.getRenameText().length() <= 0) {
status.addFatalError("name must not be empty!");
}

return status;
}

@Override
public Change createChange(IProgressMonitor pm) throws CoreException,
OperationCanceledException {
CompositeChange compositeChange = new CompositeChange("Rename");
pm.beginTask("Rename Refactoring", references.size());

MultiTextEdit multiEdit = new MultiTextEdit();
TextFileChange fileChange = new TextFileChange("Declaraction Renaming", file);

fileChange.setEdit(multiEdit);
fileChange.setTextType("bnf");
compositeChange.add(fileChange);

RenameRefactoring refactoring = (RenameRefactoring) getRefactoring();
String replaceText = refactoring.getRenameText();

ReplaceEdit replaceEdit = new ReplaceEdit(declaration.getOffset(), ((Rule)declaration.getElement()).getName().length(), replaceText);
multiEdit.addChild(replaceEdit);

TextEditGroup editGroup = new TextEditGroup("declaration update", replaceEdit);
fileChange.addTextEditGroup(editGroup);
pm.worked(1);


for (int i=0; i < references.size(); i++) {
CompositeNode reference = references.get(i);

replaceEdit = new ReplaceEdit(reference.getOffset(), ((Rule)declaration.getElement()).getName().length(), replaceText);
multiEdit.addChild(replaceEdit);
editGroup = new TextEditGroup("reference update", replaceEdit);
fileChange.addTextEditGroup(editGroup);

pm.worked(1);
}

return compositeChange;
}

@Override
public RefactoringParticipant[] loadParticipants(RefactoringStatus status,
SharableParticipants sharedParticipants) throws CoreException {
return null;
}

private List<CompositeNode> findReferenceDescriptions(
final IEObjectDescription eObjectDescription) {
List<CompositeNode> references = new ArrayList<CompositeNode>();

for (IResourceDescription resourceDescription : resourceDescriptions
.getAllResourceDescriptions()) {
Iterable<IReferenceDescription> matchingReferenceDescriptors = Iterables
.filter(resourceDescription.getReferenceDescriptions(),
new Predicate<IReferenceDescription>() {
public boolean apply(IReferenceDescription input) {
return eObjectDescription
.getEObjectURI()
.equals(input.getTargetEObjectUri());
}
});
for (IReferenceDescription matchingReferenceDescription : matchingReferenceDescriptors) {
CompositeNode node = editor.getDocument().readOnly(
new URIFragmentResolver(matchingReferenceDescription
.getSourceEObjectUri().fragment()));
references.add(node);
}
}
return references;
}

private IFile getProjectFile(String filename) {
IWorkspace ws = ResourcesPlugin.getWorkspace();
IWorkspaceRoot root = ws.getRoot();
IProject project = root.getProject();

IFile file = null;
if (project == null) {
IPath path = ws.getRoot().findMember(filename).getFullPath();
file = ws.getRoot().getFile(path);
} else {
file = project.getFile(filename);
}

return file;
}

}


The handler called from the command "RenameRefactoringHandler.java":


package mysomething.refactoring.rename;

import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.handlers.HandlerUtil;
import org.eclipse.xtext.parsetree.CompositeNode;
import org.eclipse.xtext.resource.IEObjectDescription;
import org.eclipse.xtext.resource.IResourceDescriptions;
import org.eclipse.xtext.ui.core.editor.XtextEditor;
import mysomething.Rule;
import mysomething.utils.EObjectResolver;
import mysomething.utils.URIFragmentResolver;

import com.google.inject.Inject;

public class RenameRefactoringHandler extends AbstractHandler {

@Inject
private IResourceDescriptions resourceDescriptions;

@Override
public Object execute(ExecutionEvent event) throws ExecutionException {
try {
String currentName = "";

XtextEditor editor = (XtextEditor) HandlerUtil
.getActiveEditor(event);

final ITextSelection selection = (ITextSelection) editor
.getSelectionProvider().getSelection();

final IEObjectDescription eObjectDescription = editor
.getDocument()
.readOnly(
new EObjectResolver(selection, resourceDescriptions));

CompositeNode o = editor.getDocument().readOnly(
new URIFragmentResolver(eObjectDescription.getEObjectURI()
.fragment()));

if (o.getElement() instanceof Rule) {
Rule r = (Rule) o.getElement();
currentName = r.getName();
}

RenameProcessor processor = new RenameProcessor(editor,
resourceDescriptions);
RenameRefactoring refactoring = new RenameRefactoring(processor);

RenameRefactoringWizard wizard = new RenameRefactoringWizard(
refactoring, RefactoringWizard.WIZARD_BASED_USER_INTERFACE);
wizard.setRenameText(currentName);

RefactoringWizardOpenOperation openOperation = new RefactoringWizardOpenOperation(
wizard);

openOperation.run(Display.getCurrent().getActiveShell(),
"Refactoring not possible!");

} catch (Exception e) {
MessageDialog.openInformation(
Display.getDefault().getActiveShell(),
"Rename Refactoring",
"Error while applying refactoring to workbench/wizard: "
+ e.getMessage());
e.printStackTrace();
}
return null;
}

}


and we have the two gui classes "RenameRefactoringWizard.java" and "RenamePageComposite.java":

package mysomething.refactoring.rename;

import org.eclipse.jface.wizard.IWizardPage;
import org.eclipse.ltk.core.refactoring.Refactoring;
import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
import org.eclipse.ltk.ui.refactoring.UserInputWizardPage;
import org.eclipse.swt.widgets.Composite;

public class RenameRefactoringWizard extends RefactoringWizard {

private RenameInputWizardPage wizardPage;
private String renameText = "";

public RenameRefactoringWizard(Refactoring refactoring, int flags) {
super(refactoring, flags);
}

@Override
protected void addUserInputPages() {
wizardPage = new RenameInputWizardPage("Refactoring");
wizardPage.setRenameText(renameText);
addPage(wizardPage);
}

public void setRenameText(String s) {
renameText = s;
if (wizardPage != null)
wizardPage.setRenameText(s);
}

static class RenameInputWizardPage extends UserInputWizardPage {

private RenamePageComposite composite;
private String renameText;

public RenameInputWizardPage(String name) {
super(name);
setTitle("Rename Refactoring");
setDescription("Automated Refactoring to rename rule names, rule references etc."
+ " with the possibility to automatically update references");
}

public void createControl(Composite parent) {
composite = new RenamePageComposite(parent, 0);
composite.getRenameText().setText(renameText);
composite.getRenameText().selectAll();
setControl(composite);
}

public String getRenameText() {
return composite.getRenameText().getText();
}

public void setRenameText(String s) {
renameText = s;
if (composite != null)
composite.getRenameText().setText(renameText);
}

@Override
public IWizardPage getNextPage() {
RenameRefactoring refactoring = (RenameRefactoring) getRefactoring();
if (refactoring != null) {
refactoring.setRenameText(getRenameText());
}
return super.getNextPage();
}

@Override
protected boolean performFinish() {
RenameRefactoring refactoring = (RenameRefactoring) getRefactoring();
if (refactoring != null) {
refactoring.setRenameText(getRenameText());
}

return super.performFinish();
}
}

}



package mysomething.refactoring.rename;

import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Text;

public class RenamePageComposite extends Composite {
private Text renameText;

public RenamePageComposite(Composite parent, int style) {
super(parent, style);
final GridLayout gridLayout = new GridLayout();
setLayout(gridLayout);

final Composite composite = new Composite(this, SWT.NONE);
composite.setLayoutData(new GridData(GridData.FILL, GridData.BEGINNING,
true, false));
final GridLayout gridLayout_1 = new GridLayout();
gridLayout_1.numColumns = 2;
composite.setLayout(gridLayout_1);

final Label renameLabel = new Label(composite, SWT.NONE);
renameLabel.setLayoutData(new GridData(GridData.BEGINNING,
GridData.CENTER, false, true));
renameLabel.setText("New Name:");

renameText = new Text(composite, SWT.BORDER);
renameText.setLayoutData(new GridData(GridData.FILL, GridData.CENTER,
true, true));
}

@Override
public void dispose() {
super.dispose();
}

public Text getRenameText() {
return renameText;
}
}


Finally, two helper classes that deal with the EObject stuff:


package mysomething.utils;

import java.util.Iterator;

import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.xtext.resource.EObjectAtOffsetHelper;
import org.eclipse.xtext.resource.IEObjectDescription;
import org.eclipse.xtext.resource.IResourceDescription;
import org.eclipse.xtext.resource.IResourceDescriptions;
import org.eclipse.xtext.resource.XtextResource;
import org.eclipse.xtext.util.concurrent.IUnitOfWork;

import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;

public class EObjectResolver implements
IUnitOfWork<IEObjectDescription, XtextResource> {
private final ITextSelection selection;
private IResourceDescriptions resourceDescriptions;

public EObjectResolver(ITextSelection selection,
IResourceDescriptions resourceDescriptions) {
this.selection = selection;
this.resourceDescriptions = resourceDescriptions;
}

public IEObjectDescription exec(XtextResource state) throws Exception {
EObject element = EObjectAtOffsetHelper.resolveElementAt(state,
selection.getOffset(), null);
if (element != null) {
final URI eObjectURI = EcoreUtil.getURI(element);
IResourceDescription resourceDescription = resourceDescriptions
.getResourceDescription(eObjectURI.trimFragment());
if (resourceDescription != null) {
Iterator<IEObjectDescription> eObjectDescriptions = Iterables
.filter(resourceDescription.getExportedObjects(),
new Predicate<IEObjectDescription>() {
public boolean apply(
IEObjectDescription input) {
return input.getEObjectURI().equals(
eObjectURI);
}
}).iterator();
if (eObjectDescriptions.hasNext()) {
return eObjectDescriptions.next();
}
}
}
return null;
}
}



package mysomething.utils;

import org.eclipse.emf.ecore.EObject;
import org.eclipse.xtext.parsetree.CompositeNode;
import org.eclipse.xtext.parsetree.NodeAdapter;
import org.eclipse.xtext.parsetree.NodeUtil;
import org.eclipse.xtext.resource.XtextResource;
import org.eclipse.xtext.util.concurrent.IUnitOfWork;

public class URIFragmentResolver implements
IUnitOfWork<CompositeNode, XtextResource> {
private String uriFragment;

public URIFragmentResolver(String uriFragment) {
this.uriFragment = uriFragment;
}

@Override
public CompositeNode exec(XtextResource state) throws Exception {
EObject o = state.getEObject(uriFragment);
NodeAdapter node = NodeUtil.getNodeAdapter(o);

return node.getParserNode();

}
}
blog comments powered by Disqus