Making intentions more accessible for domain experts

ยท

6 min read

The intention menu plays a key role in most DSLs developed with JetBrains MPS. The popup menu usually provides quick access to operations such as context-sensitive refactorings.

The only possibility to open the menu is by pressing alt enter. I would like to argue that having only a shortcut as an option is not the best solution for domain experts who are usually not used to JetBrains IDEs or controlling an application by the keyboard. Wouldn't it be better to also open the intention menu with the mouse?

In this blog, I will suggest a small MPS plugin to support domain experts by letting them open the intentions via the editor popup menu (the one which opens on right-clicking on a node). The image shows the outcome.

Implementing the "Intentions..."-action

Every action must be placed inside an MPS plugin. As a preparation step add a new plugin or choose an existing one from your MPS project (a tutorial can be found in the MPS user guide).

If the plugin is selected, add an MPS action to the plugin model and fill in common fields such as name and caption as you prefer.

action ShowIntentionPopupMenu {
  required access: editor command
  also available in: << ... >>

  caption: Intentions...
  mnemonic: <no mnemonic>
  description: Opens the Intention menu

required access

To open the menu, access to the current EditorComponent is needed as this owns an instance of IntentionsSupport. Having an action context parameter of type EditorComponent forces the action to be an editor command (required access: editor command).

execute block

The intentions menu can be opened programmatically by invoking IntentionsSupports method checkAndShowMenu(). Sadly, the EditorComponent field myIntentionsSupport is private as well as the method checkAndShowMenu(). Thus we have to use Java reflection. To simplify our lives, we use the language de.slisson.mps.reflection which is part of the MPS-extensions platform. The language is marked as deprecated as it's generally not a good solution to use MPS or IDEA internal implementations (I created a ticket and asked JetBrains to provide a public API).

execute(event)->void { 
  this.editorComponent.[myIntentionsSupport].[checkAndShowMenu()];  
}

(The code pieces surrounded by square brackets must be invoked using Java reflections.)

If you are using the MPS-extensions platform, there is another solution for the execute block below.

Action Group

The next step is to add an action group to make the action accessible in the editor context actions. I placed the action under the placeholder refactoring so that the intention menu and refactoring submenu are located side by side.

group IntentionPopupMenu_Group               
is popup: false                              

contents                                     
   ShowIntentionPopupMenu                    

modifications                                
   add to EditorPopup at position refactoring

So far, all required parts are implemented and after building the plugin the action should already be visible in the editor context menu and working as expected, but there are further improvements we could apply to the intention.

Keymap Changes

A good usability plus is telling the user the action's shortcut so that he or she can learn and use it next time. To achieve that a KeymapChangesDeclaration is added to our plugin.

keymap changes IntentionPopupMenu_KeyMap for Default

  ShowIntentionPopupMenu <alt>+<VK_ENTER>

The key is now displayed to actions right in the menu.

Side notes: There are now multiple alt enter shortcuts to different actions registered. It's not guaranteed that the IDEA platform will choose our new action by pressing alt enter (as we only indent to supply a new menu item, we don't bother with IDEA's heuristic for now; both actions are similar). Further, the original action to open the intention menu is unfortunately not registered in the IDEA action manager. Hence, you cannot change or remove the key binding to avoid the duplicated shortcut. However, if the new action is triggered, the Presentation Assistant plugin can detect that, which is not the case for the original action as it is not registered in the action manager.

Action Icon

Action icons enable the user to spot actions faster once they are used to the icons. It would be handy to reuse the intention bulb icon, which is also shown in the editor when an intention is available for the selected node.

The following workaround is required to reuse the intention bulb icon:

  1. Add a file resource (concept FileIcon) at icon:

  2. Execute the action Show reflective editor on the created FileIcon node

  3. Set child iconExpression to AllIcons.Actions.IntentionBulb

The iconExpression is deprecated but there is no better alternative so far (see MPS ticket). You can of course, also add an icon image to your solution instead.

is applicable block

Another improvement could be to enable the action only if intentions are available for the selected node. I haven't implemented it yet, but if you like, take a look at OriginalIntentionMenu.getIntentionsGroup() of com.mbeddr.mpsutil.intentions.runtime.plugin (which is also part of the MPS-extensions platform)

execute block if the MPS-extensions platform is used

If all steps above are followed and intention grouping is used, you will discover that the action is opening the original MPS intention menu and not IntentionsMenuWithGroups from the MPS-extensions platform. If you would like to understand the cause, take a look at the constructor of OriginalIntentionMenu (which is not the original MPS core implementation of the popup menu as the name may imply).

The OriginalIntentionMenu overrides the original action stored in myShowIntentionsAction of IntentionsSupport but the field is not used in the method checkAndShowMenu(). Hence, we have to invoke the action directly instead of the method. (If you don't use MPS-extensions, you can also use this solution as the original action is stored in myShowIntentionsAction and invokes checkAndShowMenu().

execute(event)->void { 
    this.editorComponent.[myIntentionsSupport].[myShowIntentionsAction].actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, null)

Complete action

Last but not least a full example of the action:

action ShowIntentionPopupMenu {
  required access: editor command
  also available in: << ... >>

  caption: Intentions...
  mnemonic: <no mnemonic>
  description: Opens the Intention menu
  icon: <no icon>  AllIcons.Actions.IntentionBulb

  construction parameters
    << ... >>

  action context parameters ( always visible = false )
    EditorComponent editorComponent key: EDITOR_COMPONENT required

  <update block>

  execute(event)->void { 
    this.editorComponent.[myIntentionsSupport].[checkAndShowMenu()]; 

    // OR if MPS-extensions platform is integrated
    this.editorComponent.[myIntentionsSupport].[myShowIntentionsAction].actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, null)) 
  }
}

And your plugin should contain at least these three nodes:

Do you like this extension? Leave a comment or send me a message ๐Ÿ™‚


Update

I was pointed to the intention light bulb which appears on the leftmost editor cell if an intention is available for the selected node. So there are two possibilities in MPS to open the intention menu and not just one as I wrote above. It is, however, easy to miss the icon as it can be quite far from the cursor and it forces you to move the mouse and your intention away from the current node. It also shows up after some milliseconds which is not ideal if you know that you are going to invoke an intention when clicking on a node.

ย