Moving from a refactoring to a quick assist
I'm back with a quick update. I said that the previous post was going to be the last one. Where I gave a step by step guide of how to build a refactoring for eclipse and gave my comments on the subject. But my original intend was to contribute a Quick assist instead of a refactoring.I will focus on the creation of the quick assist and specially how to build it using Changes.
As a side note, or should I say, a sad note. I'm about to present an implementation of a quick assist that has an equivalent in the JDT. When a field that can be final doesn't have the final modifier the JDT presents the quick assist that reads 'Change modifier to final'. I just want to clarify that I didn't base my code on that quick assist. It will be fun to compare though :).
As usual here's the code.
Contributing a quick assist for the JDT
To start with a quick assist, the first thing to find out is which is the extension point is needed to insert the new assist. This is it: org.eclipse.jdt.ui.quickAssistProcessorshere's how I contributed the quick assist:
The important thing to know right now is that the id is a unique indentifier for your contribution and class is the fully qualified name of the class that will implement the new functionality. By the way the class must implement the interface IQuickAssistProcessor.
Implementing the Quick Assist
When creating a refactoring, the way to build the transformations is by the use of Changes which were easy to create by the use of a AstRewrite instance. But building a quick assist is quite different. I'd say that the quick assist API expects you to handle things at a lower level of abstraction than the refactorings API. The main differences I found between implementing a refactoring and contributing quick assist are:- In the quick assist the full parsed AST is already there for you. Which is good
- In the refactoring you as the implementor are responsible of returning a potential change to the Ast while in the quick assist you must write the code to apply the change to the document. Here's where the abstraction goes a little south in comparison with the refactorings API. Because in the refactoring case the idea is to change a Data Structure, whereas in the quick assist the changes are performed in a plain text document
ToFinalQuickAssistProcessor
ToFinalQuickAssistProcessor has to implement two methods: hasAssists and getAssists. hasAssists evaluates if there are assists for a given context, and getAssists returns all the completion proposals for a given context as well. The detection code must be the same in both methods. The difference is that hasAssists must stop searching after finding the first assist and returns. It's and existence operation. getAssists must collect all the proposals whose detection code succeeds. getAssists returns an array of completion proposals which are implementations of the interface IJavaCompletionProposal. The implementers of this interface are responsible for applying the changes to the document when chosen by the user or when the quick assist is previewed.
Before getting started looking at the implementation please take into account the fact that the code is based on a refactoring. I reused as much code as possible but I didn't reuse the refactoring itself. I took the time to refactor my code and created two classes one called MakeFieldFinalDetector that contains the detection code that was present in the method checkInitialConditions and other class called FinalModifierAdder that takes care of transforming the AST which basically contains the code that belonged to the CreateChange method. I will not explain these two classes because is pretty much the same code explained in the previous post and because is not the main focus of this post.
As customary I'm going to dissect the methods implementation one by one in a top-down fashion.
hasAssists
The first thing to do here is verifying that the assist applies for the current current context. We must obtain the nodes covered by the context by invoking the method getCoveringNode. Taking advantage of the fact that a full parsed AST is available is easy to determine if the covering node is a SimpleName. Then we need to obtain the corresponding binding. With the binding and the AST then we can invoke hasToFinalQuickAssist which is going to perform the specifics of the detection.
hasToFinalQuickAssist
With the binding at hand we must check if it corresponds to a Field and then we can use the method MakeFieldFinalDetector.detect to find out if the field is assigned at declaration time and only there. If those conditions are met then the method returns true, false otherwise.
getAssists
As I mentioned before this method shares the detecting part in common with hasAssists.
Once the validation code has passed is time to create the proposal.
Here's where I got stuck for a while. Because I knew refactorings but now I had to do everything in a new place I knew nothing about. So what did I do?, I looked at the codebase an checked how it was done by other assists. For sure someone else had done what I was intending: reuse a refactoring's code to build a quick assist.
After a few hours of browsing and going back and forth there it was, what I needed. I felt I little guilty because the thing did everything for me. I just had to instantiate it and that was all. I run into CUCorrectionProposal. I created the class ToFinalQuickAssistCompletionProposal which inherits from CUCorrectionProposal.
To instantiate ToFinalQuickAssistCompletionProposal We need the context and the fragment that will be modified. To obtain the fragment there are a few steps we need to take in order to get to it from the binding instance:
- Obtain the SourceField instance using the getJavaElemtent method of the binding
- We invoke the SourceField's method findNode to obtain the corresponding FieldDeclaration instance
- We use the utility method getDeclarationFragmentByName to isolate the fragment we are interest in
ToFinalQuickAssistCompletionProposal
Here's where I used the not so public class CUCorrectionProposal. Basically what I do is use the context and the fragment to obtain the change instance and get the parameters needed by CUCorrectionProposal constructor parameters. Here's the summary of the purpose of CUCorrectionProposal class from the java file:
And this is the description of the parameters of the constructor being invoked:
Let's concentrate on how to obtain the change argument. Here's where I reuse the class FinalModifierAdder. To do its job this class needs the following arguments on its constructor:
- ast: We obtain it from the context calling context.getASTRoot().getAST()
- compilationUnit: This one is also available from the context by calling context.getCompilationUnit()
- fragment: This is the fragment that will be placed on another declaration if needed
getDeclarationFragmentByName
Becuase a FieldDeclaration can contain more then one fragment. We need to obtain the specific fragment that corresponds to a name. As you can see we are just iterating and looking for the fragment by name.
This is how I contributed a quick assists based on my refactoring code. I hope this can be useful in your eclipse projects.