Building an IntelliJ Plugin with Debugger Integration

Romain Bruyère - Sep 22 - - Dev Community

Building an IntelliJ plugin for debugging purpose can offer valuable insights into the data being handled, allowing you present it more effectively and upgrade the developer experience. However, the limited documentation around IntelliJ’s APIs often makes plugin development challenging. In this guide, we’ll walk through the steps to create a plugin that can execute methods on objects available in the stack when the debugger is paused.

1. Plugin creation

First, you will have to create an empty plugin. The easiest way to do it is directly inside IntelliJ:

  • Open the New Project wizard by selecting File | New | Project… and fill in the required details:
  • From the menu on the left, choose the IDE Plugin project type.

Sources:
https://plugins.jetbrains.com/docs/intellij/creating-plugin-project.html

2. Create an action

The goal of this demo plugin will be to give to the user the power to execute a predefined method on a variable when the debugger is paused.

Adding a new button in IntelliJ UI is the first step in designing the plugin behavior. It will create an entrypoint for the user to execute the plugin code.

Adding “My custom action” button

The system designed for that in IntelliJ APIs is called an Action.

package be.bruyere.romain.tutointellijplugindebug

import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAware

class CustomAction : AnAction(), DumbAware {

    override fun actionPerformed(event: AnActionEvent) {
        // TODO add action code
    }

    override fun update(event: AnActionEvent) {
        event.presentation.isEnabledAndVisible = true
    }

    override fun getActionUpdateThread(): ActionUpdateThread {
        return ActionUpdateThread.BGT
    }
}
Enter fullscreen mode Exit fullscreen mode

With a custom action like this one, you can:

  • Provide some code to be executed by the plugin, with actionPerformed method
  • Add logic if you want the action to be shown conditionnaly, with update method
  • Specify which thread will execute the update method

For the plugin button to be shown to the user, you will have to define its anchor in plugin.xml file, located in the /resources/META-INF folder.

<actions>
    <action id="be.bruyere.romain.tutointellijplugindebug.CustomAction"
            class="be.bruyere.romain.tutointellijplugindebug.CustomAction" text="My custom action" description="My custom action">
        <add-to-group group-id="XDebugger.ValueGroup" anchor="first"/>
    </action>
</actions>
Enter fullscreen mode Exit fullscreen mode

Here, the plugin button is placed as the first action in the right-click debugger menu. By running the Grable task intellij.runPlugin, you can now try to use it.

Sources:
https://plugins.jetbrains.com/docs/intellij/basic-action-system.html

3. Add a method invocation in debug mode

To execute, on the fly, a method on an object when the debugger is paused, multiple steps are required:

  1. Get the object reference
  2. Get the thread in which the method will be invoked
  3. Get the method reference
  4. Execute the method and get its return value

Prerequisites

Before these steps, you need to enable the debugger features in your plugin. To do this, you’ll need to add a dependency on the java plugin in the build.gradle.kts file.

intellij {
  version.set("2024.1")
  type.set("IC")

  plugins.set(listOf("java")) // To add
}
Enter fullscreen mode Exit fullscreen mode

3.1 Get the object reference

Jetbrains APIs provide a simple way to get the selected node from the right-click event context menu. The object reference value can then be accessed from there.

private fun getObjectReference(event: AnActionEvent): ObjectReferenceImpl? {
    val node = XDebuggerTreeActionBase.getSelectedNode(event.dataContext)
    val valueContainer = node?.valueContainer
    val value = valueContainer as? JavaValue
    return value?.descriptor?.value as ObjectReferenceImpl?
}
Enter fullscreen mode Exit fullscreen mode

3.2 Get the thread in which the method will be invoked

When the debugger is paused, invoking a method on the fly must be done in a specific thread, called the manager thread. The manager thread manages tasks and operations related to the debugger, ensuring that certain operations are run on the correct thread during debugging sessions

private fun getManagerThread(event: AnActionEvent): DebuggerManagerThreadImpl? {
    val node = XDebuggerTreeActionBase.getSelectedNode(event.dataContext)
    val valueContainer = node?.valueContainer
    val value = valueContainer as? JavaValue
    return value?.evaluationContext?.managerThread
}
Enter fullscreen mode Exit fullscreen mode

As for the object reference, the manager thread can be accessed from the selected node.

3.3 Get the method reference

With the object reference, any method can be accessed by its name.

// Example: getMethod(myObjectReference, "toString")
private fun getMethod(targetObjectRef: ObjectReferenceImpl, methodName: String): Method? =
        targetObjectRef.referenceType().methodsByName(methodName)[0]
Enter fullscreen mode Exit fullscreen mode

3.4 Execute the method and get its return value

Using the method and its associated object reference, you can construct a command that encapsulates the method call. You can then execute this command on the manager thread, wait for the response and return the result.

private fun executeMethod(
    event: AnActionEvent,
    targetObjectRef: ObjectReferenceImpl,
    managerThread: DebuggerManagerThreadImpl,
    method: Method?
): Value? {
    val session = DebuggerManagerEx.getInstanceEx(event.project).context.debuggerSession
    val context = session?.process?.suspendManager?.pausedContext
    val command = CustomDebuggerCommandImpl(context, targetObjectRef, method)
    managerThread.invokeAndWait(command)
    return command.result
}
Enter fullscreen mode Exit fullscreen mode

The command CustomDebuggerCommandImpl is not built in Jetbrains APIs. It’s a concrete implementation of the abstract class DebuggerCommandImpl, representing a specific command that executes within the context of IntelliJ’s debugging system. You’ll have to add that class by yourself in the your plugin.

class CustomDebuggerCommandImpl(
    private val context: SuspendContextImpl?,
    private val objectReference: ObjectReferenceImpl,
    private val method: Method?
) : DebuggerCommandImpl() {
    var result: Value? = null
        private set

    override fun action() {
        val threadReference = context?.frameProxy?.threadProxy()?.threadReference
        result = objectReference.invokeMethod(
            threadReference,
            method,
            listOf(),
            ObjectReferenceImpl.INVOKE_SINGLE_THREADED
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

Sources:
https://docs.oracle.com/en/java/javase/13/docs/api/jdk.jdi/module-summary.html
https://github.com/JetBrains/intellij-community/tree/master

4. Use of the result

The result obtained from the method invocation is not a basic object type like String, Integer, or any other standard object. Instead, it is a reference to an object created within the debugging context.

For example, if the invoked method was toString, you would receive a StringReferenceImpl. You can then retrieve the string value from that object.

val result = executeMethod(event, objectReference, managerThread, method)
val stringValue = result as? StringReferenceImpl

if(stringValue != null) {
  println(stringValue.value())
}
Enter fullscreen mode Exit fullscreen mode

There are various other implementations of type references, such as IntegerValueImpl or, more generally, ObjectReferenceImpl for custom classes.

Sources:
https://github.com/JetBrains/intellij-deps-jdi/tree/master/src/main/java/com/jetbrains/jdi

5. Full code example

Now that you understand the details of this IntelliJ plugin development process, here is a complete working example of the code: https://github.com/rombru/tuto-intellij-plugin-debug

If you'd like to see how this process was applied to create a published plugin, check out this plugin I developed for visualizing geometries on a map in debug mode:
https://plugins.jetbrains.com/plugin/25275-geometry-viewer
https://github.com/rombru/geometry-viewer

Conclusion image

. .