So you want to develop an IntelliJ plugin

Idan Koch
Wix Engineering
Published in
5 min readJan 10, 2019

WixEng is moving from Maven to Bazel, I joined the Bazel tools task force and I had the opportunity to work on IntelliJ Bazel plugin. Here is a bit of what I learned along the way. Hopefully, this post will help you kickstart future development efforts.

If I could give one piece of advice it would be this: “Before crafting a solution, it is best to try to understand the tools that are your disposal”. I found that doing some reverse engineering is best. Look at other known functionality in IntelliJ and try to find one that resembles what you need and try to understand it using documentation and examples from SDK project or other plugins (example: Scala Plugin).

Because every blog post needs a picture

Where do I begin?

Try out the JetBrains getting started example! Their “Hello World” plugin example will allow you to experiment and expand functionality, help you understand how to setup your plugin components, figure out the lifecycle of the components you intend to use and most importantly let you run and debug your code for the first time.

You will need to setup IntelliJ SDK to compile and use classes… well …from IntelliJ SDK,

In order to do so:

  1. Open project settings
  2. press +
  3. Choose “IntelliJ Platform Plugin SDK”
  4. Here you can either point to your current IntelliJ instance or download community edition. Either way point it to the Contents folder. Example: /Applications/IntelliJ IDEA CE.app/Contents, it will look something like this:

My project Example:

What did I set to accomplish?

1) When building/syncing projects using Bazel show errors on imports (Red squiggly lines over import) when their dependencies are not included in the relevant build files.

2) Add quick fix help if missing dependency is in the same repo

3) When pressing “Add dependency to build file” will add dependency to the relevant build file.

My solution:

  1. Create a service that will collect all import issues.
  2. Create an annotator that checks if a certain import line has an open issue highlights the problem.
  3. Add dependency to build file using PSI elements tools.

Part 1 — Define a service that will manage import issues

What is a service?

Simply put, it is a development unit which holds your logic. Like every component in IntelliJ plugin you need to configure your component in plugin.xml

<extensions defaultExtensionNs="com.intellij">
<!-- Add your extensions here -->
<applicationService serviceInterface="com.google.idea.blaze.base.ui.problems.ImportProblemContainerService" serviceImplementation="com.google.idea.blaze.base.ui.problems.ImportProblemContainerService">
</applicationService>
</extensions>

How do I get an instance of the service?

Accessing components is done using static method of relevant manager. For service retrieval use ServiceManager.

ServiceManager.getService(ImportProblemContainerService.class);

Part 2 — Highlight the import line

By using annotators I was able to go over a file’s PSI elements and single out import statements. For every import statement I check if there is an open issue exist for it and if so add error indication (Red squiggly line).

What are PSI elements?

Basically it is a tree structure of the files you are working on. The element holds metadata and operations needed to change the files. In order to look at PSI structure of a file you can use IntelliJ internal tools.

Internal Tools Setup

  1. Go to Help
  2. Edit Custom Properties
  3. If a file doesn’t already exist add it
  4. Insert the following line: idea.is.internal=true

You will see new tools options at your disposal. For my project “View PSI structure of current file” was the most helpful.

Example of PSI file structure:

Back to the annotator example, lets take a look at the configuration:

<extensions defaultExtensionNs=”com.intellij”><annotator language=”Scala” implementationClass=”com.google.idea.blaze.scala.sync.ImportIssueAnnotator”/></extensions>

Example of singling out java import statements:

package com.google.idea.blaze.java.sync;

import com.google.idea.blaze.base.ui.problems.ImportIssue;
import com.google.idea.blaze.base.ui.problems.ImportProblemContainerService;
import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.lang.annotation.Annotator;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiImportStatement;
import com.intellij.psi.PsiImportStaticStatement;
import org.jetbrains.annotations.NotNull;

import java.util.Optional;

public class ImportIssueAnnotator implements Annotator {

ImportProblemContainerService importProblemContainerService =
ServiceManager.getService(ImportProblemContainerService.class);

@Override
public void annotate(@NotNull final PsiElement element, @NotNull AnnotationHolder holder) {
if (element instanceof PsiImportStatement || element instanceof PsiImportStaticStatement) {
Optional<ImportIssue> issue = importProblemContainerService.findIssue(element);
if(issue.isPresent()){
importProblemContainerService.createImportErrorAnnotation(element, holder, issue.get());
}
}
}
}

You can customize the highlighted section. I used tooltip and generic error (for red squiggly lines). Example of adding annotation on import line:

public void createImportErrorAnnotation(@NotNull PsiElement element, @NotNull AnnotationHolder holder, ImportIssue importIssue) {
String tooltip = importIssue.getOriginalIssue().getMessage();
Annotation errorAnnotation = holder.createErrorAnnotation(element, tooltip);
errorAnnotation.setHighlightType(GENERIC_ERROR);
String originalLine = importIssue.getOriginalLine();
Project project = element.getProject();

List<Label> importClassTargets =
getImportClassTargets(element, originalLine, project).
stream().
distinct().
collect(Collectors.toList());
Optional<Label> currentClassTarget =
Optional.of(
findTarget(
project,
element, element.getContainingFile().getVirtualFile())
);

if(!importClassTargets.isEmpty() && currentClassTarget.isPresent()){
errorAnnotation.registerFix(
new ImportIssueQuickFix(
tooltip.substring(7),
importIssue,
importClassTargets,
currentClassTarget.get()
)
);
}
}

Part 3 — creating a quick fix action

You may have noticed that when creating the annotation I registered an IntentionAction. This action will be called once user presses on the option in quick fix menu (invoke method will be called). Override the getText to customize the quick fix menu item text.

public class ImportIssueQuickFix implements IntentionAction {
...

@Nls(capitalization = Nls.Capitalization.Sentence)
@NotNull
@Override
public String getText() {
return "Add dependency to build file";
}

...

@Override
public void invoke(@NotNull Project project, Editor editor, PsiFile psiFile) throws IncorrectOperationException {
//Place your quick fix code here
}

...
}

Bonus section

Some code samples for creating PSI elements I discovered along the way

Creating a comma element:

private PsiElement createCommaElement(Project project) {
PsiManager psiManager = PsiManager.getInstance(project);
final FileElement holderElement = DummyHolderFactory.createHolder(psiManager, null).getTreeElement();
final LeafElement newElement = ASTFactory.leaf(new IElementType("COMMA", Language.ANY), holderElement.getCharTable().intern(","));
holderElement.rawAddChildren(newElement);
GeneratedMarkerVisitor.markGenerated(newElement.getPsi());
return newElement.getPsi();
}

String Literal element:

PsiElementFactory factory = JavaPsiFacade.getInstance(project).getElementFactory();
PsiLiteralExpression stringLiteral = (PsiLiteralExpression)factory.createExpressionFromText("\""+value+"\"", null);

Enjoy!!!

--

--