Writing a Simple Intellij Plugin

When talking about our work, it’s often convenient to have others review the code that you’re looking at. Sharing files is a convenient way to establish a shared context. However, the process of actually getting coworkers to open those files can be inconvenient.

Paths are long and inconvenient to copy, and within the Yext monorepo, filenames are not necessarily unique or even quickly identifiable amongst a sea of similar looking files. Our code review tool has a browser built into it, but getting a link still requires you to either navigate to the file (which is slow) or type the path (which is both slow and hard). On the other hand, I can navigate to a file in my integrated development environment (IDE) almost instantly.

After struggling with this for the nth time, I thought “Why can’t my IDE just generate the link for me?” As a Vim hipster, I implemented this for myself pretty quickly, but I felt bad for my Intellij-bound coworkers who couldn’t experience my joy. Thus, the idea for a simple Intellij plugin was born. All the plugin would do is provide a right click action when you click a file’s tab to copy the repository link.

Starting Out

I started out at this page on the JetBrains website, which offered the various ways to create a plugin. We use Bazel at Yext and (beyond some small forays into Android) I’ve never used Gradle, so I went with the Using Github Template solution. As it turns out, this also involves Gradle, but they’ve done all the hard work for you. That leads you to this github repo which is very smooth. You simply click Use this template and it creates an Intellij project for you, specifically for developing your plugin.

Investigation

With repo in hand, it was time to figure out how to write my plugin. I popped open the project in Intellij and after an exhaustive five minutes of documentation searching, went all in on the Run ‘Run Plugin’ command. I learn by doing, so I figured this might be more instructive. It turns out this launches a second instance of Intellij with your plugin installed. A quick check of the console in the first instance confirmed the hello world logging was working as expected. This setup works really well and is much more intuitive than Chrome or Firefox plugin development, in my experience — especially viewing the logs.

Intellij plugins use Kotlin. You can use Java, but all the documentation and resources on the internet I found were for Kotlin. Given the reputation Kotlin has for being easy to learn and fun to program in, I figured I’d stay in Kotlin.

Actual Plugin Stuff

I started by deleting all the existing code, because it didn’t look useful. This turned out to be fine, so no regrets. My googling for “right click context menu intellij plugin” eventually led me to a Medium post which was very similar to what I was trying to do. While some parts didn’t exactly line up with what I wanted to do, it detailed how to add an action to a menu, and it was only a hop, skip, and a jump to find out how to add to the right click file tab context menu. With a simple, basically empty class and some xml tweaks, I got the Copy Repository Link action to show up in the right spot. At this point, my actual code looked like this.

class GitClipCopyAction : AnAction() {
    override fun actionPerformed(e: AnActionEvent) {
    }
}

Next was adding functionality. Again, the Intellij plugin documentation looks very thorough but finding anything useful in there is like finding a needle in a haystack. A smattering of stack overflow, random blog posts, and exploring autocomplete in Intellij finally got me the correct code to determine the relative path of the file to our git root. This isn’t the finest code ever produced, but given that all our developers have the same environment, it shouldn’t throw any exceptions for them anytime soon.

var path = e.getData(LangDataKeys.VIRTUAL_FILE)?.canonicalPath.orEmpty()
var vcsRoot = VcsUtil.getVcsRootFor(e.project!!, e.getData(LangDataKeys.VIRTUAL_FILE))
if (vcsRoot != null) {
    var relativePath = VcsFileUtil.getRelativeFilePath(path, vcsRoot)
}

I’m not entirely sure how you’re supposed to figure this out without just following around random code examples on the internet and noting useful looking classes along the way. Then again, sometimes that feels like an apt description of an average day’s work :)

The next step was figuring out how to manipulate the system clipboard in Kotlin. Googling it didn’t give any useful results except for Android. Unfortunately the large Kotlin user base there means most searches are biased towards Android results. Like many JVM-based languages though, Kotlin also has access to standard java imports. There are many java solutions to this problem on the internet, and they only need a small modification to account for Kotlin syntax.

Toolkit.getDefaultToolkit().systemClipboard.setContents(
    StringSelection("https://yext.internal.repo.url/+/refs/heads/master/" + relativePath),
    null)

Boring Plugin Stuff

The Intellij index is incredibly powerful and a good chunk of why users love it. However, that means the default is for actions to be unavailable until indexing is complete. To ignore this restriction, you have to make your action DumbAware. I guess my plugin is pretty simple but I can’t help but feel a little insulted. I also struggled a surprising amount with getting the icon in the right place. Both the Medium post from before and the Intellij documentation specified separate locations, neither of which worked for me.

Also, after it was shared, a new version of Intellij came out and those who upgraded reported it didn’t work. That’s because I had foolishly supplied an upper bound for compatible versions. To fix this, in gradle.properties, I put no value for pluginUntilBuild. Then I set updateSinceUntilBuild = false in build.gradle.kts, and removed untilBuild(pluginUntilBuild) from patchXml in the same file. There were several suggestions on the internet on how to accomplish this, but this is what ended up working for me.

The Final Implementation

This class implements the plugin functionality

package com.yext.intellij.jsharps

import com.intellij.ide.plugins.PluginManager
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.LangDataKeys
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.vcsUtil.VcsFileUtil
import com.intellij.vcsUtil.VcsUtil

import java.awt.Toolkit
import java.awt.datatransfer.StringSelection

class GitClipCopyAction : DumbAwareAction() {

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

    override fun actionPerformed(e: AnActionEvent) {
        var path = e.getData(LangDataKeys.VIRTUAL_FILE)?.canonicalPath.orEmpty()
        var vcsRoot = VcsUtil.getVcsRootFor(e.project!!, e.getData(LangDataKeys.VIRTUAL_FILE))
        if (vcsRoot != null) {
            var relativePath = VcsFileUtil.getRelativeFilePath(path, vcsRoot)
            Toolkit.getDefaultToolkit().systemClipboard.setContents(
                StringSelection("https://yext.internal.repo.url/+/refs/heads/master/" + relativePath),
                null
            )
        }
    }
}

And this handles declaring the action

<idea-plugin>
    <id>com.yext.intellij.jsharps.GitClipCopy</id>
    <name>Git Clip Copy</name>
    <vendor>JSharps</vendor>

    <!-- Product and plugin compatibility requirements -->
    <!-- https://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/plugin_compatibility.html -->
    <depends>com.intellij.modules.platform</depends>

    <actions>
        <group id="my-group">
          <separator/>
          <action id="com.yext.intellij.jsharps.GitClipCopyAction"
                class="com.yext.intellij.jsharps.GitClipCopyAction" text="Copy Gerrit Link"
                description="Copy Link to git url"
                icon="/icons/clip_icon.png"></action>
          <add-to-group group-id="EditorTabPopupMenu" anchor="last"/>
        </group>
    </actions>
</idea-plugin>

As expected, this plugin has a very small footprint, and the majority of it is telling Intellij how and where to render the action. After running ./gradlew buildPlugin the plugin zip was ready to go! Here, the Intellij documentation was straightforward and installation was no problem.

Future Enhancements

This is a great proof of concept, and I’ve already gotten some feedback and generated a few ideas myself:

  • Customizable repo format — for distribution or just to remove magic strings.
  • Line numbers if you click in the gutter or something
  • Allowing it to link to specific refs.
  • Hot keys

Final Thoughts

Several of my coworkers have let me know that this plugin has improved their workflow, which is all I ever wanted out of it. Developing an Intellij plugin was straightforward and an unexpected avenue for improving quality-of-life. Now I’m eyeing Firefox extensions to make similar small but impactful improvements in the web portion of my life.