JEB Plugin Development Tutorial part 8/8

More on Interactivity

The source code for part 8 of this sample plugin is located on GitHub:

User Actions

CanExecute

JEB provides the ability to interact with the units. Those units are called interactive units.

The simplest way to interact with units is through well-known actions.

The actions have to be implemented by the plugin developer. An action has an ActionContext as a parameter, which allows the plugin to retrieve:

First, why are all these actions grayed out? Because canExecuteAction() does not return true - yet.

public boolean canExecuteAction(ActionContext actionContext) {
    logger.info("%s called with address %s and actionId %d",
            "canExecuteAction", actionContext.getAddress(), actionContext.getActionId());
    return false;
}

As you can see in the logs, this method is called each time you move the caret in the document. What if we try to return true instead of false?

As expected, all actions become clickable.

Note that you can also use the toolbar icons or, even better, the keyboard shortcuts.

Nothing happens when you click on any action: it is up to the plugin developer to implement the desired feature.

We will implement a simple action: renaming of a String. (Note: we could as well rename functions, methods, variables... but we need to check in the model where they are used to replace all occurrences, consistently. The added complexity is out-of-scope in this API introduction tutorial.)

First, we need to activate the rename feature when we are on a String:

Assignment 1: Save the string references and test if the caret in on a String.

PrepareExecution

When clicking on the "Rename" action button, the method IInteractiveUnit.prepareExecution() is called. Its goal is to prepare the execution of the code and, in the case of renaming, it also provides the initial value that we want to edit: it is called before displaying the following pop up:

You need to return true in the prepareExecution method to indicate that the processing should continue.

The prepareExecution method has one more parameter of type IActionData. To fill up the name field, we need to retrieve the correct Action Data type:

Use ActionRenameData to prefill the rename field with its current value.

public boolean prepareExecution(ActionContext actionContext, IActionData actionData) {
    if(actionContext.getActionId() == Actions.RENAME) {
        StringLiteral string = getElementAt(actionContext.getAddress(), strings);
        if(string != null) {
            ((ActionRenameData)actionData).setCurrentName(string.getValue());
            return true;
        }
    }
    return false;
}

Note that IActionData embeds a generic map to pass discretionary objects from prepareExecution to executeAction method.

ExecuteAction

The executeAction method is the last step: it performs the action and modifies the model.

What should we modify now?

IUnit is the central part of your plugin: it is responsible for updating the model and keep coherence regarding all its document. But there is something wrong with the initial design: we don't save a reference Document in the Unit.

Documents shall at least work with the same object as IUnit, and at least have a reference on their IUnit in order to retrieve the data (this is a good practice in JEB).

Therefore, we will now modify our unit to keep references of all lines (using a List). One problem that will remain is the mandatory conversion at the ITextDocumentPart level:

public List<? extends ILine> getLines

The getLines method is called each time you scroll/move around the document, so it would be too costly to recalculate the list of ILines each time it is called. It must be buffered and refreshed on changes: the unit must notify its Documents when the model changes.

To make notifications work, you must:

public class JavascriptDocument extends EventSource implements ITextDocument, IEventListener {
    public JavascriptDocument(JavascriptUnit unit) {
        this.unit = unit;
        unit.addListener(this);
        refreshPart();
    }

    // ...

    public void onEvent(IEvent e) {
        if(e.getType() == J.UnitChange) {
            refreshPart();
            this.notifyListeners(e);
        }
    }
}
notifyListeners(new JebEvent(J.UnitChange));

Refer to the technical draft "Staying informed of unit changes" for more details about unit changes tracking within documents.

Assignment 2: Finish the renaming implementation.

JEB natively manages navigation feature with four predefined actions selectable from menu or directly with shortcuts:

Assignment 3: Implement Jump To for function names

Now, let's look at Follow. We can see that it is active only when we set the caret on strings, var, function... this is because the Follow feature is bound to Items, more precisely, the IActionableItem.

When clicking on Follow, if the caret is positioned on an Item, the function IInteractiveUnit.getAddressOfItem() is called.

Assignment 4: Implement Follow for function name (it should work on the latest b();)

A solution to the assignments can be found by checking out the branch tutorial8 of the sample code.

Back to Part 1