Writing a Simple Intellij Plugin
source link: https://engblog.yext.com/post/writing-a-simple-intellij-plugin
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
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.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK