Disposer in the IntelliJ Platform

Wuzi

September 2023, 16

4 min read

Disposer in the IntelliJ Platform

This article is a case study to simplify the IntelliJ Platform to introduce the IntelliJ Platform in the disposer.

In fact, the SDK documentation already has a more detailed description of this: Disposer and Disposable, before we get started, it might be a good idea to take a look here.

Scene Description

I have modelled a scenario here. There is a ToolWindow layout as follows,Here we use the UI Inspector to analyse the layout:

A rootPanel with BorderLayout, top (North) is a Toolbar, center (Center) is a JPanel. Click ➕ will add a random code snippet to Center’s JPanel.

The scenario, as above, is a very simple ToolWindow with very simple functionality.

Memory leak

Here is the key part of the code snippet:

MyToolWindowFactory.java
1
import com.intellij.openapi.project.Project;
2
import com.intellij.openapi.wm.ToolWindow;
3
import com.intellij.openapi.wm.ToolWindowFactory;
4
import com.intellij.ui.content.Content;
5
import com.intellij.ui.content.ContentFactory;
6
import com.obiscr.template.dispose.MainPanel;
7
import org.jetbrains.annotations.NotNull;
8
9
public class MyToolWindowFactory implements ToolWindowFactory {
10
@Override
11
public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
12
ContentFactory contentFactory = ContentFactory.getInstance();
13
MainPanel panel = new MainPanel(project);
14
Content content = contentFactory.createContent(panel.getComponent(), "Dispose", false);
15
toolWindow.getContentManager().addContent(content);
16
}
17
}

and

MainPanel.java
1
import com.intellij.icons.AllIcons;
2
import com.intellij.openapi.actionSystem.AnAction;
3
import com.intellij.openapi.actionSystem.AnActionEvent;
4
import com.intellij.openapi.actionSystem.DefaultActionGroup;
5
import com.intellij.openapi.actionSystem.impl.ActionToolbarImpl;
6
import com.intellij.openapi.editor.Editor;
7
import com.intellij.openapi.editor.EditorFactory;
8
import com.intellij.openapi.editor.EditorSettings;
9
import com.intellij.openapi.editor.colors.EditorColorsManager;
10
import com.intellij.openapi.editor.colors.EditorColorsScheme;
11
import com.intellij.openapi.editor.ex.EditorEx;
12
import com.intellij.openapi.editor.highlighter.EditorHighlighter;
13
import com.intellij.openapi.editor.highlighter.EditorHighlighterFactory;
14
import com.intellij.openapi.project.Project;
15
import com.intellij.testFramework.LightVirtualFile;
16
import com.intellij.ui.components.panels.VerticalLayout;
17
import com.intellij.util.ui.JBUI;
18
import com.intellij.util.ui.UIUtil;
19
import org.jetbrains.annotations.NotNull;
20
21
import javax.swing.*;
22
import java.awt.*;
23
import java.io.File;
24
import java.time.LocalDateTime;
25
import java.time.format.DateTimeFormatter;
26
import java.util.UUID;
27
28
public class MainPanel {
29
30
private final JPanel rootPanel;
31
private final JPanel centerPanel;
32
private final Project myProject;
33
34
35
public MainPanel(Project project) {
36
myProject = project;
37
rootPanel = new JPanel(new BorderLayout());
38
centerPanel = new JPanel(new VerticalLayout(JBUI.scale(10)));
39
centerPanel.setBorder(JBUI.Borders.empty(10));
40
41
DefaultActionGroup actionGroup = new DefaultActionGroup();
42
AnAction addAction = new AnAction(() -> "Add Item", AllIcons.General.Add) {
43
@Override
44
public void actionPerformed(@NotNull AnActionEvent e) {
45
String text = "// Code snippet at " + LocalDateTime.now().
46
format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) + "\n";
47
text += HelloWorld.getRandomHelloWorldCode();
48
Editor editor = createEditor(myProject, text);
49
centerPanel.add(editor.getComponent());
50
centerPanel.revalidate();
51
centerPanel.repaint();
52
}
53
};
54
actionGroup.add(addAction);
55
ActionToolbarImpl actionToolbar = new ActionToolbarImpl("MyPanelToolbar", actionGroup, true);
56
actionToolbar.setTargetComponent(rootPanel);
57
actionToolbar.setBorder(JBUI.Borders.customLine(UIUtil.getBoundsColor(), 1,0,1,0));
58
rootPanel.add(actionToolbar, BorderLayout.NORTH);
59
rootPanel.add(centerPanel, BorderLayout.CENTER);
60
}
61
62
public JPanel getComponent() {
63
return rootPanel;
64
}
65
66
private Editor createEditor(Project project, String text) {
67
EditorFactory editorFactory = EditorFactory.getInstance();
68
EditorEx editor = (EditorEx) editorFactory.createViewer(
69
editorFactory.createDocument(text),
70
project);
71
// Others settings
72
// ...
73
return editor;
74
}
75
}

Here we just create the Editor object, but we don’t clean up the memory it occupies when the program exits.

When closing the IDE, relevant exception messages are displayed (Of course, this information is also displayed in the idea.log):

According to the error log, we can know that it is because the created Editor object is not released when the application is closed and there is a risk of memory overflow.

Solving

Ok, now we already know where the problem is, the following we solve the problem, the solution is also simple, just need to close the IDE, release in this object can be.

First, make MainPanel implement the Disposable interface and override the dispose() method. Then after creating editor, register Disposer with parent this to indicate that the editor will be destroyed when this is disposed.

1
public class MainPanel implements Disposable {
2
@Override
3
public void dispose() {
4
centerPanel.removeAll();
5
rootPanel.removeAll();
6
}
7
8
private Editor createEditor(Project project, String text) {
9
EditorEx editor = ...;
10
Disposer.register(this, () -> EditorFactory.getInstance().releaseEditor(editor));
11
return editor;
12
}
13
}

In addition, since in this case the components are all based on Content, we register the disposer for content as MainPanel

1
public class MyToolWindowFactory implements ToolWindowFactory {
2
3
@Override
4
public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
5
MainPanel panel = new MainPanel(project);
6
Content content = ...;
7
content.setDisposer(panel);
8
...
9
}
10
}

The source code for ContentImpl.java looks like this, and you can see that when content is destroyed, it calls Disposer.dispose(myDisposer); to destroy the set myDisposer, which is the MainPanel above.

1
@Override
2
public void dispose() {
3
if (myShouldDisposeContent && myComponent instanceof Disposable) {
4
Disposer.dispose((Disposable)myComponent);
5
}
6
7
if (myDisposer != null) {
8
Disposer.dispose(myDisposer);
9
myDisposer = null;
10
}
11
12
myFocusRequest = null;
13
clearUserData();
14
}

So now the whole process is as follows:

Terminal window
1
content.dispose() --(call)--> panel.dispose() --(call)--> EditorFactory.getInstance().releaseEditor(editor);

So, when content is closed, the editor object is also destroyed.

Tweet this article