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.
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.
}
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
}
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.
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();
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());
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
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.
This information can be complementary to the previous tip #7:
import com.intellij.openapi.extensions.ExtensionPoint;
// ...
ExtensionPoint extensionPoint = Extensions.getRootArea().getExtensionPoint(Extensions.getExtensionPointName("com.intellij.pluginsList"));
for (PluginDescriptor pluginDescriptor : extensionPoint.getExtensionList()) {
System.out.println("Installed plugin: " + pluginDescriptor.getName());
}
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;
}
}
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.
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());
}
});
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());
}
});
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());
}
}
});
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;
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.
Here you are:
Editor editor = FileEditorManager.getInstance(project).getSelectedTextEditor();
CaretModel caretModel = editor.getCaretModel();
int lineNumber = editor.getDocument().getLineNumber(caretModel.getOffset());.
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.
}
});
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));
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:
...
...
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;
}
}
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.
As said earlier, this value can be “IntelliJ IDEA”, “PyCharm”, “WebStorm”, etc., depending on the product being used:
String ideVersion = ApplicationInfo.getInstance().getFullVersion();
The full version string typically has the format “major.minor.build”.
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 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
}
}
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.
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();
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:
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 your plugin.xml file:
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);
Here you are:
Locale locale = Locale.getDefault();
The language code of the locale by using the getLanguage() method:
String language = locale.getLanguage();
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
}
}
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
}
}
}
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");
}
}
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.
Promyze, the collaborative platform dedicated to improve developers’ skills through best practices sharing and definition.
Crafted from Bordeaux, France.
©2023 Promyze – Legal notice
NEW: Introducing AI for generating coding practices and discussions for your Promyze workshops |
Social media