Monday, June 6, 2011

Rename Refactoring in Xtext 2.0

The upcoming Eclipse Indigo version of Xtext ships with experimental support for generic rename refactoring. It allows you to rename model elements and automatically fix all links to these elements. This blog entry describes the main components behind this new feature. The following screencast shows refactoring for a domainmodel language:



Enabling Refactoring


To enable refactoring support for your language, you have to add the RefactorElementNameFragment in the fragment section of the MWE workflow of your language, e.g.

// rename refactoring
fragment = refactoring.RefactorElementNameFragment {}

The fragment has an additional flag useJdtRefactoring which can be used to delegate to JDT's refactorings for languages using the JVM types compiling to Java (i.e. the domain model example or Xtend). Usually users are fine with the defaults.

After running the workflow, you will have working refactoring support - at least if you have stuck to the defaults with regard to naming, cross-referencing. and indexing. Give it a try. If it doesn't work yet you have to adapt the infrastructure a bit more.

Customizing


The most likely component you want to customize is the IRenameStrategy. This component defines how the declaration of the target element is performed. It has two major responsibilities:

  • Apply and revert the declaration change on the semantic model (methods applyDeclarationChange and revertDeclarationChange))

    The default is to look for an EAttribute 'name' on the target object and set its value using EMFs reflective API.

  • Create the LTK Change objects of the declaration change. These changes will be aggregated, checked for overlaps, presented to you in the preview and finally executed if you apply the refactoring.

    The default is to use the ILocationInFileProvider to locate the text range representing the name and create a ReplaceEdit for it.


As the IRenameStrategy is a stateful object, you have to bind a custom IRenameStrategy.Factory instead of the strategy itself to integrate your changes.

The other component you might want to customize is the IDependentElementsCalculator. Dependent elements are those elements whose qualified name changes when the target element is renamed. E.g. when you rename a Java class the qualified names of its inner classes change, too, thus references to these have to be updated. This calculation is performed by the IDependentElementsCalculator. By default, all elements contained in the target element are added. This matches Xtext's default strategy of qualified name computation.

The Internals


The following describe the steps taken in the refactoring and the responsible components

Refactoring UI


In terms of UI, the refactoring of Xtext looks pretty much the same as in JDT. When the user has triggered the refactoring - by a keystroke or using a context menu entry - the editor goes into a linked editing mode allowing to edit all occurrences of the element in the current document at once. Pressing the keys again or using the popup menu, the user switched to a dialog wizard or a preview wizard. Thanks a lot to Holger who did the major work on the UI.

The UI parts pass an element of type IRenameElementContext to the non-UI components, holding information on the target element, its EClass, the current editor etc.

Non-UI


The non-UI part and thereby the actual refactoring is mainly managed by the RenameElementProcessor.

As a first step, it loads the model declaring the element to be renamed into a new ResourceSet that is configured with respect to the declaring language. This allows to easily abandon all changes as well as avoiding concurrency issues. Some basic checks are performed like 'is the element available' or 'is the resource writeable' and the IRenameStrategy is initialized.

Then we calculate the dependent elements.

After that the URIs of the elements that are affected by the refactoring are calculated. Those URIs may change, e.g. if you use a name-based fragment strategy. So we calculate the original URIs, apply the change and recalculate the new URIs. The result is a Map<URI, URI>. This task is the responsibility of the IRenamedElementTracker.
In the end, the changes are reverted to leave the resource in the same state as before.

The final step is to handle the cross-references to the renamed elements. We have to consider resource internal cross-references as well as the ones from other resources. The latter can be retrieved from Xtext's index. Note that if your external cross-references are not indexed, they won't be found and consequently won't be updated by the refactoring.

As references can come from any other language, we use a ReferenceUpdaterDispatcher that searches for local and indexed references and dispatches to the IReferenceUpdater of the referring language. We have two implementations of this interface: The DefaultReferenceUpdater works with Xtext resources and uses the language's serializer to update the reference text. OTOH, the EmfResourceReferenceUpdater works for all EMF based languages and re-serializes the whole resource after applying the semantic change.

Rename Participants


Finally, one refactoring can trigger another. E.g. when renaming a rule in an Xtext grammar, the returned EClass should be renamed, too. For these cases, you can register a RenameParticipant by the common means of LTK. If the target of the participant is Xtext based, you can use a AbstractProcessorBasedRenameParticipant in order to reuse lots of the already described classes.

2 comments:

Sebastian said...

Thanks for these details from a developers point of view. The rename functionality actually works nice (I have stuck with the Xtext defaults for now). I already had the chance to take a closer look at the internals, and I was a bit surprised that current API is focused around rename only, and not refactoring in general. The more I am surprised that you call the new feature experimental. So I hope the API will gain more abstraction when it leaves the experimental state.

Unknown said...

We tried to do simple things first, and these turned out to be far more complicated than expected.

Even though our naming might be confusing, I guess a number of other refactorings can be done using this API, e.g. we also rename the files when renaming an Xtend class, we rename overridden rules and retrun types in the Xtext editor. But in general, refactorings are horribly complicated and require very deep statical analysis. Even in the JDT code it looks very complicated to add a new type of refactoring.

A general API for refactoring already exists: LTK :-) I fell that Xtext's current API will help you doing most of the Xtext/EMF specific update stuff. If you have any suggestions to do this in a better way, feel free to open an enhancmentr bugzilla.

The API is marked as experimental as it is new and we have to gather experiences with it. The JDT integration (Xtend refactoring) also showed we might need another interation to cut some components right. But the main hooks should stay the same.