Having worked recently on a plugin for
Intellij IDEA 7 I used a few things not necessarily so obvious for IDEA plugin writers as documentation around writing plugins is far from perfect :)
Here they are. Maybe this stuff will be useful also in incoming IDEA 8 release.
1.Rule one - don't use singletons in your pluginSingletons are
evil. In IDEA they are evil even more as you must support multiframe environment (where each project is opened in a separate frame). If you, foolishly, start using singletons for your windows, models, panels, configuration you will end up with nasty interference between all IDEA frames. IDEA provides you all the tools to avoid using of singletons: application, project and module components (they have to implement special interfaces though), hierarchical Pico container (you may put there
any object there without special requirements for the interface) and easy access to it, e.g.:
// getting component registered in Pico container
public static T getProjectComponent(final Project project, final Class clazz) {
return clazz.cast(project.getPicoContainer().getComponentInstanceOfType(clazz));
}
The only reasonable excuse for using singletons is establishing really global components for the whole IDEA platform (which must be shareable between IDEA frames), but still you should do is via interfaces and dependency injection everywhere (or pulling your global component from Pico as a last resort) than creating anti-pattern MySingleton.getInstance().
Sometimes you may be tempted to use singletons due to IDEA Action System (IDEA actions cannot normally injected dependencies).
Do NOT do it. Instead inject your domain objects to data context (see below) or use helper methods to fetch your current project or module and then retrieve desired component from it or its Pico container. Normally it's enough to use AnActionEvent to find out which project given action should refer to, e.g.:
@Nullable
public static Project getCurrentProject(DataContext dataContext) {
return DataKeys.PROJECT.getData(dataContext);
}
@Nullable
public static Project getCurrentProject(AnActionEvent e) {
return getCurrentProject(e.getDataContext());
}
AFAIK if you want to use actions defined via your plugin.xml file, there is no way to inject anything to them (in DI sense - action objects are after all single instance objects serving in various contexts - e.g. for various buttons, menus or even projects). Thus you have to resign from “Hollywood call” mode (as it would make your object stateful) and starting pulling your dependencies using techniques (without storing them as instance variable - as it would make actions stateful) described above, e.g.:
public class MyAction extends AnAction {
// ...
@Override
public void actionPerformed(final AnActionEvent e) {
// fetch a project compoment defined in plugin.xml
MyComponent myProjectComponent = project.getComponent(MyComponent.class);
// fetch custom project component registered manually
// (outside plugin.xml) on project level - see helper method above
MyCustomComponent myCustomComponent = getProjectComponent(
final Project project, MyCustomComponent.class)
// ... do something now with your components
}
}
2.Finding current project from your ComboBoxActionGetting Project reference here may be more challenging as we don't have access to AnActionEvent while deciding what options should be available in the combo box. However IDEA internally seems to use the following trick:
public class ComboBoxAction extends ComboBoxAction {
@NotNull
@Override
protected DefaultActionGroup createPopupActionGroup(JComponent jComponent) {
final Project project = DataKeys.PROJECT.getData(
DataManager.getInstance().getDataContext(jComponent));
final DefaultActionGroup g = new DefaultActionGroup();
// knowing your project populate g
// ...
return g;
}
3.“Injecting” your own domain objects into Action systemIt turns out that Action system is very powerful and it's easy to make actions aware of your own objects if they are “in scope” (e.g. to enable/disable an icon or respond to user click when an element of your domain object model is selected in some editor).
We want to achieve the following:
public class MyAction extends AnAction {
@Override
public void actionPerformed(AnActionEvent event) {
MyObject myObject = event.getData(MY_OBJECT_KEY);
if (myObject != null) {
// do something with your domain object
}
}
@Override
public void update(final AnActionEvent event) {
MyObject myObject = event.getData(MY_OBJECT_KEY);
// e.g. action must be enabled if MyObject is really available in the context
event.getPresentation().setEnabled(myObject != null);
}
To achieve it, it's enough to:
- define MY_OBJECT_KEY, e.g.:
public static final DataKey<MyObject> MY_OBJECT_KEY = DataKey.create(“some-unique-key”);
- implement com.intellij.openapi.actionSystem.DataProvider interface in your UI components which are responsible for displaying your model objects (e.g. some tree or table)
public final class MyTreePanel extends JPanel implements TreeSelectionListener, DataProvider {
private JTree myTree;
private MyModel model;
// your normal UI logic goes here
@Nullable
public Object getData(@NonNls final String dataId) {
if (dataId.equals(MY_OBJECT_KEY.getName())) {
// return appropriate domain object model of MyObject type
// (e.g. the one which is currently selected in the tree
// or return null if there is no reasonable candidate
// (e.g. nothing is selected)
}
return null;
}
}
You can easily stack your DataProviders and getData() implementation (e.g. composite may delegate the search one or several to its children).
4.Fetching revision X of selected virtual file ...
... without resorting to fetch whole file history (extremely painful in IDEA with remote SVN repository)
@Nullable
private static VirtualFile getVirtualFileRevision(
@NotNull final Project project, @NotNull final VirtualFile virtualFile,
@NotNull String revision) throws VcsException {
AbstractVcs vcs = VcsUtil.getVcsFor(project, virtualFile);
if (vcs == null) {
return null;
}
VcsRevisionNumber vcsRevisionNumber = vcs.parseRevisionNumber(revision);
DiffProvider diffProvider = vcs.getDiffProvider();
if (diffProvider == null) {
return null;
}
ContentRevision contentRevision = diffProvider.createFileContent(vcsRevisionNumber, virtualFile);
if (contentRevision == null) {
return null;
}
// this operation is typically quite costly (unless revision points to checked out file)
final String content = contentRevision.getContent();
if (content == null) {
return null;
}
return new VcsVirtualFile(contentRevision.getFile().getPath(), content.getBytes(),
vcsRevisionNumber.asString(), virtualFile.getFileSystem());
}
Thank you
Dmitry Jemerov for your help while solving our issues with OpenAPI and helping us where IDEA documentation did not.