I’ve been writing lots of quick fixes for bndtools lately. Since there seems to be very little introductory material in this area, I decided to share my experiences here so I can use it as a reference. The code used in here is currently in a coming PR but will soon end up in 7.1.0-SNAPSHOT.
I admit, I am not a fan of the Eclipse JDT APIs. This can partly be explained that they have a distinct vintage feel due to lack of generics and enums. There is a huge amount of slippery code where it is up to you to track the type safety and it there are an overwhelmingly large number of strings involved. Usage of interfaces is not consistent and in many places an adapter objects defines the API, which makes lambda’s hard, if not impossible, to use. Also, the Java language evolved tremendously since the beginning and the code clearly shows that an overhaul is long overdue.
However, there is also the problem that it is hard to get a good oversight what the intention was. I’ve been working with the AST API for some weeks and only now do I get some respect for what the code does.
Quick fixes show up when you type Control-1. Some time ago one of our committers, Father Krieg, made it trivial to add quick fix processors in an OSGi component. So now you only have to create your component class and you can add a quick fix. This has become so easy that organizations could consider writing a plugin for their own annotation types. The work I did was adding quick fix support for gogo commands, components, literals, and some minor frustrations with missing base quick fixes. These quick fixes can save developers significant time.
A quick fix must analyze the context of where the text cursor is placed. For example, if it is placed on a String Literal, it would be possible to propose converting the text to upper case if the literal had letters that could be changed to upper case. The proposal then consists of some text to explain the proposed change, an icon, and a callback. When the user selects the proposal, the callback is called and it must modify the program.
There is a lot going on in this definition. First. what is the cursor placed on? Clearly the cursor is in the text buffer but it would be quite hard to try to find out what that position really means. Anybody ever trying to figure out in a random text buffer where in a Java program you are will testify how hard this is.
For this reason, Eclipse JDT has parsed the text into an AST, an Abstract Syntax Tree. In an AST, every construct is represented as a node with a given type, and a set of children nodes of potentially mixed other types. That is, the nodes and the resulting tree structure are generic, the type of node provides the semantics.
At the top of the Eclipse AST we have a Compilation Unit, which represents the whole source file. For each structure in the Java Language there is a specific class that extends the ASTNode class. For example, the
PackageDeclaration class represents the
package foo.bar; part. Every semantic aspect of the source is represented as an ASTNode. Also types and names are nodes. There are no simple attributes or properties, everything, however small, is represented as a a node. For example, a fully qualified name has a Name node for each of its segments.
It is important to realize that the ASTNode represents the textual representation of the Java source code, not its semantics. Although it provides significant support for the Java semantics, it must faithfully represent the lexical structure of the Java source as well as being able to represent erroneous code. For example, also comments and Javadoc construct are represented as specifically typed nodes. The purpose of the AST is to structurally edit the content of an editor. The AST cannot have cycles and a node can only be part of one tree or not connected.
Navigating the AST can happen in different ways. It supports the visitor pattern. With this pattern you can quickly traverse the tree and with little effort filter out the interesting node. The disadvantage is that it that it requires a bit of work to keep the context because they’re calling you out of the blue.
The other ways is to traverse the tree manually. The children of an ASTNode are organized as properties. There are two types of properties, simple and list properties. Simple properties represent 0…1 node, list properties represent 0…n nodes. Ever property has a key. A key is unique for a specific ASTNode type and a property. There are simple keys and list keys. Navigating is cumbersome with this API, especially since it does not use any generics. The API sadly does not protect you from common mistakes.
When you can make a quick-fix, you get the context node in the AST. For example, a String Literal. You can then poke around to see what the content. If you find you can do something useful, you make a proposal that describes the change you can make, for example turning the text into uppercase, and provide a callback. When your proposal gets selected, the callback is executed.
Although the AST represents the current editor’s content, you cannot directly change the AST. Instead, you use the ASTRewriter class. This class takes the existing AST and records the changes you want to make to the AST without actually making them.
For example, you can create a new String Literal node and set the content to the upper cased string. This new String Literal is not connected to the original AST and never will. However, the ASTRewriter can make the changes to the text buffer to remove the old String Literal node and add the new String Literal node. These changes are recorded as text changes to the text buffer. This is a relative fragile process because you are supposed to make the changes in a careful order, I found it goes wrong easily. Once all the changes are done, you can get a TextEdit object that can be used to apply those changes to the editor’s Document.
The JDT is a very powerful library but it is far from easy to use. Like many Eclipse APIs, its ancient roots from the late nineties show. Java underwent a huge evolution to make the language a lot easier to use and more powerful that are sorely missed in this complex API. That said, it is still quite impressive what it does and maybe even more impressive that this code is already 25 years old.