上两篇文章主要描述了idea插件工程创建和idea插件的配置,本文继承上述两篇文章,详细描述下插件开发基础的第三块内容。也是开发具体插件功能前必须要了解的内容,否则开发过程中有可能会经常卡壳。
因大部分配置相关的内容在前两篇文章中都已描述过了,所以本文中会着重说明下与配置相关的程序代码,配置相关内容可查看前两篇文章。
术语
- AST:Abstract Syntax Tree
 - UAST:Unified Abstract Syntax Tree,An abstraction layer on the different AST
 - DOM:Document Object Model
 - EDT:Event Dispatch Thread,handles all Swing events
 - EP:Extension Point
 - ES:External System,allows integrating external project management systems.
 - FBI:File Based Index,allows storing key-value information based on the project's files.
 - LVCS:Local History
 - LaF:Look and Feel,Defines the visual appearance and behavior of the user interface by use Swing
 - JPS:JetBrains Project System
 - JBR:JetBrains Runtime
 - PSI:Program Structure Interface
 - RA:Read Action
 - WA:Write Action
 - RC:Run Configuration
 - SSR:Structural Search and Replace,Allows searching and replacing code
 - VCS:Version Control System
 - VF:Virtual File
 - VFS:Virtual File System
 
一、插件打包
正式的插件包会有三种形式如下:
1、无依赖的jar包
插件.jar文件与所有必需的捆绑库一起位于插件“根”文件夹下的/lib文件夹中。/lib文件夹中的所有 jar都会自动添加到类路径中:
.IntelliJIDEAx0/
└── plugins
    └── sample.jar
        ├── com/company/sample/SamplePluginService.class
        │   ...
        │   ...
        └── META-INF
            ├── plugin.xml
            ├── pluginIcon.svg
            └── pluginIcon_dark.svg
2、有依赖的jar包
插件.jar文件与所有必需的捆绑库一起位于插件“根”文件夹下的/lib文件夹中。/lib文件夹中的所有 jar都会自动添加到类路径中:
.IntelliJIDEAx0/
└── plugins
    └── sample
        └── lib
            ├── lib_foo.jar
            ├── lib_bar.jar
            │   ...
            │   ...
            └── sample.jar
                ├── com/company/sample/SamplePluginService.class
                │   ...
                │   ...
                └── META-INF
                    ├── plugin.xml
                    ├── pluginIcon.svg
                    └── pluginIcon_dark.svg
3、包含源码的jar包
这种形式主要是为了其它人扩展用,比如定义两个模块API和Implement,需要把API公开时,这样就可以打包在一起供其它人来引用,配置在build.gradle中进行配置:
tasks {
  val createOpenApiSourceJar by registering(Jar::class) {
    // Java sources
    from(sourceSets.main.get().java) {
      include("**/com/example/plugin/openapi/**/*.java")
    }
    destinationDirectory.set(layout.buildDirectory.dir("libs"))
    archiveClassifier.set("src")
  }
  buildPlugin {
    dependsOn(createOpenApiSourceJar)
    from(createOpenApiSourceJar) { into("lib/src") }
  }
}
上述配置将创建一个源JAR,其中包含com.example.plugin.openapi包中的Java文件,并将其添加到所需 example-plugin.zip 中的最终插件 ZIP 分发中!/example-plugin /lib /src目录。
二、依赖管理
1、插件依赖
一个插件可能依赖于其他插件的类,这些插件可能是捆绑的、第三方的或同一作者的。设置其他插件或模块的类的依赖,需要三个必需步骤:1、找到插件 ID;2、项目设置;3、plugin.xml中的声明
找到插件id
- JetBrains 市场上提供的可扩展插件
 
对于在JetBrains Marketplace上发布的插件:可以打开插件的详细信息页面,然后向下滚动到底部的附加信息部分,最后复制插件 ID;
2、Jetllij IDE捆绑的可扩展插件
常见的插件如下,也可以在build.gradle文件中配置一个listBundledPlugins功能task列出插件列表。
Plugin Name  | Plugin ID  | Related Documentation  | 
Copyright  | com.intellij.copyright  | |
CSS  | com.intellij.css  | WebStorm Plugin Development  | 
Database Tools and SQL  | com.intellij.database  | DataGrip Plugin Development  | 
IntelliLang  | org.intellij.intelliLang  | Language Injection  | 
Java  | com.intellij.java  | Java  | 
JavaScript and TypeScript  | JavaScript  | WebStorm Plugin Development  | 
Kotlin  | org.jetbrains.kotlin  | Configuring Kotlin Support  | 
Markdown  | org.intellij.plugins.markdown  | |
Maven  | org.jetbrains.idea.maven  | |
Spring  | com.intellij.spring  | Spring API  | 
Spring Boot  | com.intellij.spring.boot  | Spring Boot  | 
项目设置
需要分别在build和plugin两个文件中设置要扩展的插件,先在build.gradle中引入插件依赖,然后在plugin.xml中配置具体的扩展。
intellij { plugins.set(listOf("com.example.another-plugin:1.0")) }
plugin.xml中的依赖声明
<depends>com.example.another-plugin</depends>
依赖插件设置
如下代表表示,指定要依赖的kotlin插件optional="true"为可选的插件依赖项。在这种情况下,即使依赖的插件没有安装或启用,插件也会加载,但插件的部分功能将不可用。
plugin.xml:主plugin.xml定义了对 Java 插件(插件 ID com.intellij.java)的必需依赖并注册了相应的com.intellij.annotator扩展。此外,它还指定了对 Kotlin 插件的可选依赖项(插件 ID org.jetbrains.kotlin):
<idea-plugin>
   <depends>com.intellij.java</depends>
   <depends
       optional="true"
       config-file="myPluginId-withKotlin.xml">org.jetbrains.kotlin</depends>
   <extensions defaultExtensionNs="com.intellij">
      <annotator
          language="JAVA"
          implementationClass="com.example.MyJavaAnnotator"/>
   </extensions>
</idea-plugin>
myPluginId-withKotlin.xml:配置文件myPluginId-withKotlin.xml与主plugin.xml文件位于同一目录中。它注册了相应的com.intellij.annotator扩展:
<idea-plugin>
   <extensions defaultExtensionNs="com.intellij">
      <annotator
          language="kotlin"
          implementationClass="com.example.MyKotlinAnnotator"/>
   </extensions>
</idea-plugin>
2、依赖冲突解决
因为idea本身集成了一些捆绑的插件,如果自己开发的idea插件扩展了特定插件的特定版本,有可能出现版本冲突等问题。所以此时依赖管理就派上用场了,可以全局配置也可以用类加载器细致管理。
build.gradle全局配置
configurations.all { 
   resolutionStrategy.sortArtifacts(ResolutionStrategy.SortOrder.DEPENDENCY_FIRST)
}
自定义类加载器
Thread currentThread = Thread.currentThread();
ClassLoader originalClassLoader = currentThread.getContextClassLoader();
ClassLoader pluginClassLoader = this.getClass().getClassLoader();
try {
  currentThread.setContextClassLoader(pluginClassLoader);
  // code working with ServiceLoader here
} finally {
  currentThread.setContextClassLoader(originalClassLoader);
}
三、插件扩展
监听器和扩展点
1、声明扩展
本质上来说就是基于一个已有的插件进行开发,需要注意在使用时需要在plugin和build文件中添加依赖才会生效,插件一般有三种扩展类型:
- com.intellij.toolWindow:可向IDE两加的工具栏添加按钮
 - com.intellij.applicationConfigurable和com.intellij.projectConfigurable:可在idea的settings页面添加一个自定义的插件设置页面;
 - 自定义语言插件使用许多扩展点来扩展 IDE 中的各种语言支持功能。
 
官方提供的扩展点:Extension Point and Listener List | IntelliJ Platform Plugin SDK
开源提供的扩展点:IntelliJ Platform Explorer | JetBrains Marketplace
plugin.xml声明扩展配置如下:
<extensions defaultExtensionNs="com.intellij">
    <!--appStarter 还有id, order, os属性,有必要设置id的唯一值避免冲突-->
    <appStarter 
            implementation="com.zd.MyAppStarter"/>
    <projectTemplatesFactory
            implementation="com.zd.MyProjectTemplatesFactory"/>
</extensions>
defaultExtensionNs值,如果扩展了idea核心的需要输入com.intellij。如果是其它的非捆绑的扩展输入plugin的具体值,自定义的扩展实现类如下:
public class MyAppStarter implements ApplicationStarter {
    @Override
    public String getCommandName() {
        return null;
    }
}
public class MyProjectTemplatesFactory extends ProjectTemplatesFactory {
    @Override
    public String @NotNull [] getGroups() {
        return new String[0];
    }
    @Override
    public ProjectTemplate @NotNull [] createTemplates(@Nullable String group, @NotNull WizardContext context) {
        return new ProjectTemplate[0];
    }
}
以上是用接口声明的,如果扩展点是使用beanClass属性声明的,则在指定的 bean 类中设置@Attribute注解来设置相应的属性。
2、定义扩展点
通过在您的插件中定义扩展点,可以允许其他插件扩展您的插件的功能。在plugin.xml文件中可把扩展点<extensionPoints>定义在<extension>中,也可以单独定义。扩展点支持动态属性,但由于限制比较多,暂时不建议使用。扩展点的两种定义方式:
Interface扩展点
允许其他插件使用代码扩展您的插件,其他插件将提供实现该接口的类。然后就可以能够调用这些接口上的方法。name属性为此扩展点分配一个唯一的名称,插件内全局唯一,所以建议用类的全限定名。
<extensionPoints>
    <extensionPoint
            name="myExtensionPoint1"
            beanClass="com.zd.MyBeanClass">
        <with attribute="implementationClass" implements="com.intellij.serviceContainer.LazyExtensionInstance"/>
    </extensionPoint>
</extensionPoints>
public class MyBeanClass extends LazyExtensionInstance {
    @Attribute("key")
    public String key;
    @Attribute("implementationClass")
    public String implementationClass;
    public String getKey() {
        return key;
    }
    @Override
    protected @Nullable String getImplementationClassName() {
        return null;
    }
}
Bean扩展点
允许其他插件使用数据扩展您的插件。需要指定扩展类的完全限定名称,其他插件将提供将转换为该类实例的数据。
<extensionPoints>
    <extensionPoint
            name="myExtensionPoint2"
            interface="com.zd.MyInterface"/>
</extensionPoints>
public class MyInterface {
}
使用扩展点
在另一个插件中使用自定义的扩展点。这里有一个注意点其实是有双层意义,扩展别人的插件或是为了别人扩展插件。但扩展点可以定义但不一定会公布出去,可以查看打包一节。
下图代码中depends值为上面扩展点定义中的插件的id值,也时也可以了解下defaultExtensionNs的使用方法。
<idea-plugin>
  <id>another.plugin</id>
  <!-- Declare dependency on plugin defining extension point: -->
  <depends>my.plugin</depends>
  <!-- Use "my.plugin" namespace: -->
  <extensions defaultExtensionNs="my.plugin">
    <myExtensionPoint1
            key="someKey"
            implementationClass="another.some.implementation.class"/>
    <myExtensionPoint2
            implementation="another.MyInterfaceImpl"/>
  </extension>
</idea-plugin>
在代码中拿到扩展点的实例进行编程:
public class MyExtensionUsingService {
  private static final ExtensionPointName<MyBeanClass> EP_NAME =
          ExtensionPointName.create("my.plugin.myExtensionPoint1");
  public void useExtensions() {
    for (MyBeanClass extension : EP_NAME.getExtensionList()) {
      String key = extension.getKey();
      String clazz = extension.getImplementationClassName();
      // ...
    }
  }
}
四、定义服务
可以简单理解为spring中的service,用于处理复杂的逻辑,类似于bean的概念。目的是为了当您的插件调用相应实例的方法时按需加载的插件组件。IntelliJ 平台确保仅加载服务的一个实例,即使它被多次调用也是如此。
服务必须具有用于服务实例化的实现类。服务也可能有一个接口类,用于获取服务实例并提供服务的 API。需要关闭挂钩/清理例程的服务可以实现Disposable并执行必要的工作dispose()。
IntelliJ 平台提供三种类型的服务:应用程序级服务(全局单例)、项目级服务和模块级服务。对于后两者,会为其对应作用域的每个实例创建一个单独的服务实例。
避免或谨慎使用模块级服务,因为它会增加包含许多模块的项目的内存使用量
定义服务有配置和注解两种实现方式:
1、配置服务
配置方式
应用型
public interface MyAppService {
  void doSomething(String param);
}
public class MyAppServiceImpl implements MyAppService {
  @Override
  public void doSomething(String param) {
    // ...
  }
}
项目型
public interface MyProjectService {
  void doSomething(String param);
}
public class MyProjectServiceImpl {
  private final Project myProject;
  public MyProjectServiceImpl(Project project) {
    myProject = project;
  }
  public void doSomething(String param) {
    String projectName = myProject.getName();
    // ...
  }
}
在plugin.xml中配置上述服务
<extensions defaultExtensionNs="com.intellij">
  <!-- Declare the application-level service -->
  <applicationService
      serviceInterface="com.example.MyAppService"
      serviceImplementation="com.example.MyAppServiceImpl"/>
  <!-- Declare the project-level service -->
  <projectService
      serviceInterface="com.example.MyProjectService"
      serviceImplementation="com.example.MyProjectServiceImpl"/>
</extensions>
注解方式
这种方式不需要在plugin.xml中注册,但服务的实现类必须是final类型的。
应用型
@Service
public final class MyAppService {
  public void doSomething(String param) {
    // ...
  }
}
项目型
@Service(Service.Level.PROJECT) 
public final class MyProjectService {
  private final Project myProject;
  public MyProjectService(Project project) {
    myProject = project;
  }
  public void doSomething(String param) {
    String projectName = myProject.getName();
    // ...
  }
}
2、获取服务
MyAppService applicationService =
    ApplicationManager.getApplication().getService(MyAppService.class);
MyProjectService projectService =
    project.getService(MyProjectService.class);
或
MyAppService applicationService = MyAppService.getInstance();
MyProjectService projectService = MyProjectService.getInstance(project);
五、侦听器
消息传递基础结构)。侦听器实现必须是无状态的,并且不能实现生命周期(例如,Disposable),可以定义应用程序级和项目级侦听器。
类似于mq的概念,订阅topic然后再实现一个handle处理类。在Idea中侦听器的设计大概如下所示:

编辑
1、定义应用程序级侦听器
<applicationListeners>
    <listener
            class="com.zd.MyVfsListener"
            topic="com.intellij.openapi.vfs.newvfs.BulkFileListener"/>
</applicationListeners>
public class MyVfsListener implements BulkFileListener {
    @Override
    public void after(@NotNull List<? extends VFileEvent> events) {
        // handle the events
//        events.connect().subscribe(VirtualFileManager.VFS_CHANGES, new BulkFileListener() {
//            @Override
//            public void after(@NotNull List<? extends VFileEvent> events) {
//                // handle the events
//            }
//        });
    }
}
- topic属性:指定与要接收的事件类型对应的侦听器接口的完全限定名
 - class属性:指定插件中实现侦听器接口并接收事件的实现逻辑类
 
2、定义项目级监听器
<projectListeners>
    <listener
            class="com.zd.MyToolWindowListener"
            topic="com.intellij.openapi.wm.ex.ToolWindowManagerListener"/>
</projectListeners>
public class MyToolWindowListener implements ToolWindowManagerListener {
    private final Project project;
    public MyToolWindowListener(Project project) {
        this.project = project;
    }
    @Override
    public void stateChanged(@NotNull ToolWindowManager toolWindowManager) {
        // handle the state change
    }
}
3、侦听器设置
activeInTestMode和activeInHeadlessMode属性用于设置是否禁用此侦听器,但前提是需要知道Application.isHeadlessEnvironment()和Application.isUnitTestMode()的返回值为ture时相应的属性才会生效。
4、自定义侦听器
定义Topic
public class Notifier {
 
  @Topic.AppLevel
  public static final Topic<FileDocumentManagerListener> FILE_DOCUMENT_SYNC 
= new Topic<>(FileDocumentManagerListener.class, 
              Topic.BroadcastDirection.TO_DIRECT_CHILDREN, 
              true);
   void beforeAction(Context context);
   void afterAction(Context context);
}
实现Subscriber
public class NotifierListener {
    public void init(MessageBus bus) {
        bus.connect().subscribe(Notifier.CHANGE_ACTION_TOPIC,
                new ChangeActionNotifier() {
                    @Override
                    public void beforeAction(Context context) {
                        // Process 'before action' event.
                    }
                    @Override
                    public void afterAction(Context context) {
                        // Process 'after action' event.
                    }
                });
    }
}
实现publisher
public class NotifierPublisher {
    public void doChange(Context context) {
        Notifier publisher =
                ComponentManager.getMessageBus().syncPublisher(Notifier.CHANGE_ACTION_TOPIC);
        publisher.beforeAction(context);
        try {
            // do action
        } finally {
            publisher.afterAction(context);
        }
    }
}
六、设置插件图标
要求:1、尺寸:40 x 40 或80 x 80像素;2、形状:插件Logo周边至少要留2px的透明边距;3、所有插件徽标图像必须为 SVG 格式;3、放在resources/META-INT文件夹下:
- pluginIcon.svg:是默认的插件标志,仅用于浅色IDE 主题,
 - pluginIcon_dark.svg:可选的替代插件徽标,仅用于深色 IDE 主题
 
七、开发插件常见问题
https://plugins.jetbrains.com/docs/intellij/faq.html










