33 how-to questions if you develop a JetBrains extension

Cédric Teyton
Packmind
Published in
9 min readFeb 13, 2023
how-to questions when developing a JetBrains extension

Are you about to start developing a JetBrains extension? Or maybe you’re currently working on that? As you may already know, once you develop a JetBrains extension, it’s available in the marketplace for all the IDEs maintained by the editor, such as JetBrains, PyCharm, Rider, WebStorm, and so on.

At Promyze, we’ve built a JetBrains extension that helps developers to share their best coding practices in their organization. We had to dive into the JetBrains SDK, and from that work, we wanted to build a ‘how-to’ list of use cases that might be helpful for you. We wish we had such one when we started working on that extension! Of course, feel free to ask any more questions! 🙂

NB: We also produced a similar post for VSCode if you consider a VSCode extension.

ℹ️ All the Java imports have not been included for readability purposes. Still, we added some of them to ensure you’re on the right way ;)

#1 How to access the visible lines in the current editor?

What if you’d be interested in computing stuff only for the content in the visible screen?

FileEditorManager fileEditorManager = FileEditorManager.getInstance(project);
Editor editor = fileEditorManager.getSelectedTextEditor();
// Instantiate the editor above as it suits the most for you

Document document = editor.getDocument();
Rectangle visibleArea = editor.getVisibleArea();

int startLine = document.getLineNumber(visibleArea.y);
int endLine = document.getLineNumber(visibleArea.y + visibleArea.height);

for (int line = startLine; line <= endLine; line++) {
int startOffset = document.getLineStartOffset(line);
int endOffset = document.getLineEndOffset(line);
String lineText = document.getText(new TextRange(startOffset, endOffset));
//Do smth with it, add to a list, or concatenate to a string.
}

#2 How to get the selected text?

Here're two examples of how to get the selected text in the editor:

Editor editor = FileEditorManager.getInstance(project).getSelectedTextEditor();;
SelectionModel selectionModel = editor.getSelectionModel();

if (selectionModel.hasSelection()) {
String selectedText = selectionModel.getSelectedText();
// do something
}

// Alternative
if (selectionModel.hasSelection()) {
int start = selectionModel.getSelectionStart();
int end = selectionModel.getSelectionEnd();
String selectedText = editor.getDocument().getText(new TextRange(start, end));
// do something
}

#3 How to add a marker to a line with a custom image?

You’ll need to extend the GutterIconRenderer class.

import com.intellij.openapi.editor.markup.GutterIconRenderer;
// ...
Editor editor = ...;
int lineNumber = ...; // the line number where you want to add a gutter

Icon icon = IconLoader.getIcon("/icons/youricon.png");
GutterIconRenderer renderer = new GutterIconRenderer() {
@Override
public Icon getIcon() {
return icon;
}

@Override
public boolean equals(Object obj) {
return obj instanceof GutterIconRenderer && ((GutterIconRenderer) obj).getIcon() == icon;
}
};

EditorGutter gutter = ((EditorEx) editor).getGutter();
gutter.registerTextAnnotation(lineNumber, renderer);

Override the equals method ensures that the marker won't be duplicated when the line is repainted.

#4 How to get the programming language of the current editor?

Easy one when you have access to the current Document:

import com.intellij.openapi.fileTypes.FileType;
//...
Editor editor = ...;
FileType fileType = ((EditorEx) editor).getDocument().getFileType();
String language = fileType.getName();

#5 How to add a menu on the right-click event?

To add a menu to the right-click event, you need to create a custom AnAction :

import com.intellij.openapi.actionSystem.AnAction;
//...
public class MyMenuAction extends AnAction {
public MyMenuAction() {
super("My Menu Action");
}

@Override
public void actionPerformed(AnActionEvent e) {
// Your implementation here
}
}

And register it as a popup menu action:

import com.intellij.openapi.actionSystem.ActionManager
//...
ActionManager actionManager = ActionManager.getInstance();
actionManager.registerAction("myMenuActionId", new MyMenuAction());

#6 How to set a custom shortcut to perform an action?

Following tip #5, declare first an AnAction class. Then we will use the Keymap class to add a shortcut to the action:

import com.intellij.openapi.keymap.Keymap;
//....
Keymap keymap = KeymapManager.getInstance().getActiveKeymap();
String actionId = "myActionId";
MyAction action = new MyAction();
keymap.addShortcut(actionId, KeyStroke.getKeyStroke(KeyEvent.VK_1, InputEvent.CTRL_DOWN_MASK)); // Ctrl+1

#7 How to list the JetBrains extensions pane opened in the IDE?

If your extension needs to know that information, here you are:

import com.intellij.openapi.wm.ToolWindowManager;
//...
ToolWindowManager toolWindowManager = ToolWindowManager.getInstance(project);
for (String id: toolWindowManager.getToolWindowIds()) {
ToolWindow toolWindow = toolWindowManager.getToolWindow(id);
if (toolWindow != null && toolWindow.isVisible()) {
System.out.println("Opened tool window: " + id);
}
}

The isVisible method determines if the tool window is currently open.

#8 How to retrieve all the JetBrains extensions installed?

This information can be complementary to the previous tip #7:

import com.intellij.openapi.extensions.ExtensionPoint;
// ...
ExtensionPoint<PluginDescriptor> extensionPoint = Extensions.getRootArea().getExtensionPoint(Extensions.getExtensionPointName("com.intellij.pluginsList"));
for (PluginDescriptor pluginDescriptor : extensionPoint.getExtensionList()) {
System.out.println("Installed plugin: " + pluginDescriptor.getName());
}

#9 How to underline a line of code?

You can use the EditorGutterComponent class and the GutterIconRenderer class:

import com.intellij.openapi.editor.markup.GutterIconRenderer;
import com.intellij.codeInsight.daemon.LineMarkerInfo;
//...
public class MyLineMarkerProvider extends LineMarkerProvider {
@Nullable
@Override
public LineMarkerInfo getLineMarkerInfo(@NotNull PsiElement element) {
if (element instanceof PsiMethodCallExpression) {
int startOffset = element.getTextRange().getStartOffset();
int endOffset = element.getTextRange().getEndOffset();
TextRange textRange = new TextRange(startOffset, endOffset);
GutterIconRenderer gutterIconRenderer = new MyGutterIconRenderer();
return new LineMarkerInfo<>(element, textRange, null, Pass.UPDATE_ALL, null, gutterIconRenderer, GutterIconRenderer.Alignment.RIGHT);
}
return null;
}
}

And this is the GutterIconRenderer implementation, where we took as an example the notification icon, but you’ll be welcome to add your icon:

private static class MyGutterIconRenderer extends GutterIconRenderer {
@NotNull
@Override
public Icon getIcon() {
return AllIcons.Ide.Notification.Info;
}
}

#10 How to retrieve the file name of the current tab?

Here you are:

import com.intellij.openapi.vfs.VirtualFile;
//...
FileEditorManager fileEditorManager = FileEditorManager.getInstance(project);
Editor editor = fileEditorManager.getSelectedTextEditor();
if (editor != null) {
VirtualFile virtualFile = FileEditorManagerEx.getInstanceEx(project).getFile(editor);
if (virtualFile != null) {
System.out.println("Current file name: " + virtualFile.getName());
}
}

Note that the FileEditorManagerEx class provides additional functionality for managing editor tabs.

#11 How to listen when a tab has been closed?

You can implement a listener for when a tab is closed:

FileEditorManager.getInstance(project).addFileEditorManagerListener(new FileEditorManagerListener() {
@Override
public void fileClosed(@NotNull FileEditorManager source, @NotNull VirtualFile file) {
System.out.println("File closed: " + file.getName());
}
});

#12 How to listen when a tab has been opened?

In the same spirit as tip #11:

FileEditorManager.getInstance(project).addFileEditorManagerListener(new FileEditorManagerListener() {
@Override
public void fileOpened(@NotNull FileEditorManager source, @NotNull VirtualFile file) {
System.out.println("File opened: " + file.getName());
}
});

#13 How to listen when the current tab has changed?

Again, same approach:

FileEditorManager.getInstance(project).addFileEditorManagerListener(new FileEditorManagerListener() {
@Override
public void selectionChanged(@NotNull FileEditorManagerEvent event) {
VirtualFile newFile = event.getNewFile();
if (newFile != null) {
System.out.println("Current tab changed to: " + newFile.getName());
}
}
});

#14 How to add and access a JetBrains extension setting?

The Settings class and the State class will help you. In our case in Promyze, we had to use the current user API Key:

public class PromyzeSettings {
private static final String SETTING_KEY = "promyzeApiKey";

@State(name = SETTING_KEY, storages = {@Storage(value = "promyzeSettings.xml")})
public static PromyzeSettingState settingState = new PromyzeSettingState();

public static PromyzeSettingState getInstance() {
return ServiceManager.getService(MySettings.class).settingState;
}
}

public class PromyzeSettingState {
public String promyzeApiKey = "";
}

To access the setting, you can use the following code:

String userApiKey = PromyzeSettings.getInstance().promyzeApiKey;

#15 How to implement a Caret Listener?

This is useful if your extension needs to run an analysis when the caret moves. The CaretListener interface will be your alley:

import com.intellij.openapi.editor.CaretListener;
//...
Editor editor = FileEditorManager.getInstance(project).getSelectedTextEditor();
CaretModel caretModel = editor.getCaretModel();
caretModel.addCaretListener(new CaretListener() {
@Override
public void caretPositionChanged(CaretEvent e) {
System.out.println("Caret position changed: " + e.getNewPosition());
}
});

The getNewPosition method of the CaretEvent class returns the new position of the caret.

#16 How to get the current line of the caret?

Here you are:

Editor editor = FileEditorManager.getInstance(project).getSelectedTextEditor();
CaretModel caretModel = editor.getCaretModel();
int lineNumber = editor.getDocument().getLineNumber(caretModel.getOffset());.

#17 How to listen when the current editor is saved?

You’ll need to add a listener:

Editor editor = FileEditorManager.getInstance(project).getSelectedTextEditor();
Document document = editor.getDocument();
document.addDocumentListener(new DocumentListener() {
@Override
public void beforeDocumentChange(DocumentEvent event) {
// Called before the text of the document is changed.
}

@Override
public void documentChanged(DocumentEvent event) {
// Called after the text of the document has been changed.
}
});

#18 How to listen when a specific shortcut is called?

This is how you can listen to the keyboard shortcut CTRL + X.

import com.intellij.openapi.actionSystem.KeyboardShortcut;
//...
KeyboardShortcut keyboardShortcut = new KeyboardShortcut(KeyStroke.getKeyStroke(KeyEvent.VK_X, InputEvent.CTRL_DOWN_MASK), null);
AnAction anAction = new AnAction() {
@Override
public void actionPerformed(AnActionEvent event) {
// handle shortcut event
}
};
anAction.registerCustomShortcutSet(new CustomShortcutSet(keyboardShortcut), event.getData(CommonDataKeys.EDITOR_COMPONENT));

#19 How to change the logo of my JetBrains extension?

Add your icon to the resources directory of the project (in one of the following formats: .png, .jpeg, or .gif). Next, in the plugin.xml file, you need to specify the path to the image file in the icon attribute of the idea-plugin tag like this:

<idea-plugin>
...
<icon path="icon.png"/>
...
</idea-plugin>

#20 How can I display a modal form after a command is sent from a right-click menu?

Assuming you’ve implemented an action in the contextual menu, you can then display a modal form thanks to the*DialogWrapper* class:

public class MyAction extends AnAction {
@Override
public void actionPerformed(AnActionEvent e) {
MyDialog dialog = new MyDialog(e.getProject());
dialog.show();
}
}

public class MyDialog extends DialogWrapper {
public MyDialog(Project project) {
super(project);
init();
setTitle("My Dialog");
}

@Nullable
@Override
protected JComponent createCenterPanel() {
JPanel panel = new JPanel();
// Add components to the panel here
return panel;
}
}

#21 How to prompt a warning notification?

The Notification class will help you with that purpose:

import com.intellij.notification.Notification;
//...
Notification notification = new Notification("MyGroup", "My Title", "My Content", NotificationType.WARNING);
Notifications.Bus.notify(notification, project);

On the Notification object, you can also set the setListener property to provide a listener that will be notified when the notification is clicked.

You can use as well NotificationType.INFO and NotificationType.ERROR for informative and error messages.

#22 How to get the IDE name?

As said earlier, this value can be "IntelliJ IDEA", "PyCharm", "WebStorm", etc., depending on the product being used:

import com.intellij.openapi.application.ApplicationInfo;
**//...**
String ideName = ApplicationInfo.getInstance().getFullProductName();

#23 How to get the current version of the IDE?

Related to tip #22:

String ideVersion = ApplicationInfo.getInstance().getFullVersion();

The full version string typically has the format "major.minor.build".

#24 How to get the current compilation issues in the opened file?

In case your extension needs them:

import com.intellij.codeInspection.ex.HighlightManager**;
//...**
FileEditorManager fileEditorManager = FileEditorManager.getInstance(project);
Editor editor = fileEditorManager.getSelectedTextEditor();
if (editor != null) {
Document document = editor.getDocument();
PsiFile psiFile = PsiDocumentManager.getInstance(project).getPsiFile(document);
if (psiFile != null) {
List<ProblemDescriptor> problems = new ArrayList<>();
GlobalInspectionContext globalContext = InspectionManager.getInstance(project).createNewGlobalContext(false);
for (InspectionToolWrapper toolWrapper: globalContext.getInspectionTools(psiFile)) {
ProblemsHolder problemsHolder = new ProblemsHolder(InspectionManager.getInstance(project), document, false);
toolWrapper.processFile(psiFile, problemsHolder, globalContext);
problems.addAll(problemsHolder.getResults().getResultItems());
}
// Do something with the problems list
}
}

#25 How to get the content of the Output Tab?

To retrieve the output content of the app, you can use this trick:

import com.intellij.openapi.wm.ToolWindow**;
//...**
String outputContent;
ToolWindow toolWindow = ToolWindowManager.getInstance(project).getToolWindow("Output");
if (toolWindow != null) {
Content content = toolWindow.getContentManager().getContent(0);
JComponent component = content.getComponent();
if (component instanceof JTextArea) {
JTextArea textArea = (JTextArea) component;
outputContent = textArea.getText();
}
}

You can see the tab name is dynamic so you can retrieve the content for another tab.

#26 How to get the current editor theme?

It might impact the way you want to render your components:

import com.intellij.openapi.editor.colors.EditorColorsManager**;**
//...
EditorColorsManager editorColorsManager = EditorColorsManager.getInstance();
EditorColorsScheme currentScheme = editorColorsManager.getGlobalScheme();
String themeName = currentScheme.getName();

#27 How to listen when the IDE is opened?

You must create a class that implements StartupActivity and add it as an extension in your plugin.xml file:

import com.intellij.openapi.startup.StartupActivity;
// ...
public class MyStartupActivity implements StartupActivity {
@Override
public void runActivity(@NotNull Project project) {
// your code here, it will be executed when the IDE is opened
}
}

And in your plugin.xml file:

<extensions defaultExtensionNs="com.intellij">
<startupActivity implementation="your.package.MyStartupActivity"/>
</extensions>

#28 How to listen when the IDE is closed?

Similar to tip #27:

public class MyApplicationListener implements ApplicationListener {
@Override
public void beforeApplicationQuit(@NotNull boolean isRestart) {
// your code here, it will be executed before the IDE is closed
}
}

And in the plugin.xml file:

<extensions defaultExtensionNs="com.intellij">
<applicationListener implementation="your.package.MyApplicationListener"/>
</extensions>

#29 How to run an asynchronous task in background?

This is recommended to avoid blocking operations for the users. You can be sure they won’t hesitate to uninstall your extension if it’s too annoying ;)

You can create a class that extends Task.Backgroundable and override the run method to perform the task in the background. Here is an example:

import com.intellij.openapi.task.Task.Backgroundable;
//...
public class MyBackgroundTask extends Task.Backgroundable {
public MyBackgroundTask(@Nullable Project project, @NotNull String title) {
super(project, title);
}

@Override
public void run(@NotNull ProgressIndicator progressIndicator) {
// your code here, it will be executed in the background
}
}

To run it:

MyBackgroundTask task = new MyBackgroundTask(project, "My Background Task");
ProgressManager.getInstance().run(task);

#30 How to get the current language of the IDE?

Here you are:

Locale locale = Locale.getDefault();

The language code of the locale by using the getLanguage() method:

String language = locale.getLanguage();

#31 How to listen when an application is run?

The Run feature in your IDE:

import com.intellij.openapi.application.ApplicationListener;
import com.intellij.openapi.application.ApplicationManager;

public class MyApplicationListener implements ApplicationListener {

public void init() {
ApplicationManager.getApplication().addApplicationListener(this);
}

@Override
public void applicationStarted() {
// your code here, executed when the application starts
}
}

#32 How to listen when an Extract Method operation is called?

You can also listen for specific code operations; here, we take the Extract Method operation:

import com.intellij.refactoring.RefactoringEventListener;
import com.intellij.refactoring.RefactoringEventData;

public class MyRefactoringEventListener implements RefactoringEventListener {

public void init() {
RefactoringEventListener.DEFAULT.addListener(this);
}

@Override
public void refactoringStarted(String refactoringId, RefactoringEventData data) {
if (refactoringId.equals("Extract Method")) {
// your code here, executed when the Extract Method operation is called
}
}
}

#33 How to know if the current editor has defined the "LF" or "CRLF" mode for line breaks?

For the record, LF is more on Unix/Mac, while CRLF is used on Windows. Trust me, if you need to parse the source code, it can spare you some trouble ;)

Editor editor = EditorHelper.getCurrentEditor();
if (editor != null) {
Document document = editor.getDocument();
String lineSeparator = document.getUserData(Document.LINE_SEPARATOR_KEY);
if ("\n".equals(lineSeparator)) {
System.out.println("Lf mode");
} else if ("\r\n".equals(lineSeparator)) {
System.out.println("crlf mode");
}
}

Wrapping up

That’s all, folks! We hope it can help you start working on your JetBrains extension. In case you work with your team, it can make sense to define best practices when developing a JetBrains extension. Promyze can help you with that; feel free to try it.

--

--

Cédric Teyton
Packmind

CEO & co-founder @Promyze.com, solution to define and share your best coding practices. Interested in knowledge sharing in engineering teams and code quality.