Warning

This tutorial is deprecated, and will be rewritten to account for JEB 4 API changes.

JEB Plugin Development Tutorial part 8/8

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

  • Clone this repo: git clone https://github.com/pnfsoftware/jeb2-plugindemo-js.git
  • Switch to the tutorial8 branch: git checkout tutorial8

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:

  • the action id: one defined in the Actions class. It indicates which action is involved (Comment, Rename...)
  • the address: the current position in the document
  • the item id: we will discuss it later.

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

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?

  • IInput: this is a pointer to original file, it makes no sense.
  • AstRoot: this is our kind-of model, but there is no way to retrieve the input data (the toString() method has a specific implementation and the toSource() auto-formats and remove comments.

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:

  • Register the notified element:
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);
        }
    }
}
  • Call the notify method in the executeAction method (on success):
notifyListeners(new JebEvent(J.UnitChange));

Note

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

Assignment

Finish the renaming implementation.

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

  • Jump To: move caret to a specific address. It uses the ITextDocument.addressToCoordinates() that we already implemented for Notifications.
  • Navigate Forward / Navigate Backward: move caret to the previous/next position (caret position history is saved each time you jump)
  • Follow: jump to an address bound to the current element. The best example is on a function call: you can jump to its definition

Assignment

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

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.