Always execute code when ESC is pressed

I’m currently using:

    val editorActionManager = EditorActionManager.getInstance()
    val escapeAction = IdeActions.ACTION_EDITOR_ESCAPE
    val originalEscapeHandler = editorActionManager.getActionHandler(escapeAction)
    editorActionManager.setActionHandler(escapeAction, MyAction(originalEscapeHandler).handler)

and

/**
 * We are overriding the ESC handler's behavior to remove our inlay component if it exists. Otherwise,
 * we will execute the original handler.
 */
class MyAction(originalHandler: EditorActionHandler) :
    EditorAction(MyActionHandler(originalHandler)), InlineChatAction {

  private class MyActionHandler(val originalHandler: EditorActionHandler) :
      EditorActionHandler() {
    override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext) {
      val sameEditorAsInlay = MyManager.getInlayComponent()?.editor == editor
      if (sameEditorAsInlay) {
        MyManager.dispose()
      } else {
        originalHandler.execute(editor, caret, dataContext)
      }
    }

    override fun isEnabledForCaret(
        editor: Editor,
        caret: Caret,
        dataContext: DataContext?
    ): Boolean {
      val sameEditorAsInlay = MyManager.getInlayComponent()?.editor == editor
      if (sameEditorAsInlay) return true
      return originalHandler.isEnabled(editor, caret, dataContext)
    }
  }
}

… but somehow this doesn’t always work.
I suspect it’s because other handlers in the chain intercept the execution and stop it.

Is there a way to simply always ensure this gets run?

Hi Jérémi,
Do I understand correctly, that it does work, but not always? Did you try to debug it?
I would start with invocations of isEnabledForCaret(), or in com.intellij.openapi.editor.actionSystem.EditorActionHandler#execute()

If it doesn’t work at all, did you try to register it with:

<editorActionHandler action="EditorEscape" implementationClass="com.example.MyActionHandler"/>

?

Hi @karol.lewandowski, sorry for the late response.

I had already tried to debug this issue, to no avail. The issue is somehow not always reproducible, so it’s pretty hard to really understanding what’s happening. Some times, the handler is called, some times not.

Anyhow, I did a little debugging session once again:

For whatever reason, I can reproduce the issue in the sandbox when I simply run it with no debugger attached. But when I run the sandbox with a debugger attached, then the issue goes away.
This is with absolutely no code changes whatsoever.
Any ideas on what might be happening?

Moreover, does my implementation of MyAction look correct? This should indeed allow it to always execute when ESC is pressed, right?

I wonder if it’s some sort of timing bug somewhere (given you said it works with debug, and fails without debug) :thinking:

Another debugging session

Is there a way to ensure my registered EditorAction are always executed first?
Without being entirely certain, I’m pretty sure this is related to the chain that is stored in the DynamicEditorActionHandler.

For example, I also have an action that is registered and associated with TAB.
Turns out if the Editor that is opened is in read-only mode, and I have code that is selected which is not exclusively whitespace (i.e. a new-line or a space), then my registered action never gets called when I press TAB because I get the “File is read-only” little pop-up.
(I added println in both isEnabledForCaret and doExecute to make make sure it never gets called.)

This highlights that there are indeed actions that are associated with shortcuts that do cut short the chain of registered actions.

Potential solutions

This leaves me with apparently only two potential solution:

  1. Figure out if the plugin SDK actually provides a way to always run something when a shortcut is pressed (my current understanding is that it does not)
  2. Figure out how to ensure that my injected actions are always the first to be run in the chain

Failed attempt

Note that I’ve also tried using an ActionPromoter:

/** Used to prioritize when multiple Actions are mapped to the same keyboard shortcut. */
class MyActionPromoter : ActionPromoter {

  /** Returning `null` means Actions will be executed in the original order. */
  override fun promote(
      actions: MutableList<out AnAction>,
      context: DataContext
  ): MutableList<AnAction>? {
    val project = context.getData(CommonDataKeys.PROJECT) ?: return null
    val isEnabled = MyPluginProjectSettings.isEnabled(project)
    if (!isEnabled) return null

    val notOurActions = actions.none { isActionToPrioritize(it) }
    if (notOurActions) return null

    return actions
        .sortedWith { a, b ->
          if (isActionToPrioritize(a)) return@sortedWith -1
          else if (isActionToPrioritize(b)) return@sortedWith 1 else return@sortedWith 0
        }
        .toMutableList()
  }

  private fun isActionToPrioritize(action: AnAction) = action is MyActionToPrioritize
}

Hi Jérémi,
I don’t see what could be the issue cause. Maybe another handler is registered after yours and it intercepts the processing?

Did you try to register it in XML with order="first"?

Regarding the debugging, is the problem gone, when you stop at breakpoints, or is it enough to just run in debug mode?
If it happens only when you suspend at breakpoint, then maybe logging breakpoints might provide you with more information. See:

Hi @karol.lewandowski ,

Maybe another handler is registered after yours and it intercepts the processing?

I find it odd that there doesn’t seem to be a mechanism for plugin developers to specify some code to reliably execute code whenever a given shortcut is pressed. :frowning:

Did you try to register it in XML with order="first" ?

Even when registering via XML with order="first", I still get the “File is read-only” interruption.

Am I somehow supposed to remove the Editor’s Listeners while my plugin is executing (before those shortcuts may be called), and apply them back when my plugin is done doing its work? That sounds quite hacky.

Regarding the debugging, is the problem gone, when you stop at breakpoints, or is it enough to just run in debug mode?

It was sufficient to simply be in debug mode, with breakpoints disabled.
However, now I can’t reproduce this problem anymore (not my problem in general, but specifically the reproducible instance of “this reliably works vs doesn’t work when comparing debug vs normal”).
This seems to point at some race condition existing somewhere in JetBrains’ code-base? :thinking:

maybe logging breakpoints might provide you with more information.

Unfortunately, it seems rather non-trivial to log the handlers-chain of the DynamicEditorActionHandler from within an EditorActionHandler.

Side-question

Furthermore: what would be the suggested way to register a callback for more complex shortcuts as well?
For example: SHIFT + TAB

Bump.

Does the Plugin SDK really not provide a way (even if non-trivial) to ensure that some code is always executed when a given shortcut is pressed ? :frowning:

Not to depend on the order of action handlers for the existing action, you can register a separate action in XML (using <action> tag). Basically, your MyAction implementation could work, you just don’t need to care about ‘original’ handler.
That combined with the action promoter you’ve used should do what you want.
(The way you tried to use the promoter previously didn’t work as it works upon actions, not action handlers, and you didn’t register your own separate action)

1 Like

Action tag to be specified like this:
<action id="YourActionId" class="fully.qualified.name.of.MyAction" use-shortcut-of="EditorEscape"/>

1 Like

It seems like using AnAction is the appropriate approach, thanks @batrdmi !

your MyAction implementation could work

Any reason to keep them as EditorAction at this point, or I should simply go for DumbAwareAction ?

For your case DumbAwareAction should be just as fine, I think. You’ll just need to get editor from data context explicitly.

1 Like