25

编写一个IDEA插件之:事件监听

 3 years ago
source link: http://developer.51cto.com/art/202010/628266.htm
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

JvumAvr.jpg!mobile

事件监听,我们最熟悉不过的就是开发APP时,监听按钮点击事件、手指触摸及移动事件、网络状态事件等等。事件监听大多通过观察者模式实现,首先API调用者不需要知道后台是如何检测出网络状态不可用的,而只需要向系统注册一个监听器,当网络状态发生改变时,由系统回调给监听器。

本篇内容:

  • 项目或模块事件监听:在模块或者整个项目发生改变时,通过事件监听做出反应,如项目新增了一个模块或是删除了某个模块;
  • 文件编辑事件监听:在Java代码文件编辑时,通过事件监听能够知道哪个类的代码改变了,此时后台就可以刷新一些数据的缓存;

如何监听项目或模块改变事件

首先是项目级别的事件监听。添加一个项目管理事件监听器,我们需要实现ProjectManagerListener接口,该接口有四个方法,其源码如下。

public interface ProjectManagerListener extends EventListener { 
  default void projectOpened(@NotNull Project project) { 
  } 
  default void projectClosed(@NotNull Project project) { 
  } 
  default void projectClosing(@NotNull Project project) { 
  } 
  default void projectClosingBeforeSave(@NotNull Project project) { 
  } 
} 
  • projectOpened:该方法在项目打开时被回调;
  • projectClosingBeforeSave:在关闭项目时,开始保存项目之前被回调,或者说是在调用FileDocumentManager#saveAllDocuments方法保存所有文件之前被调用;
  • projectClosing:在projectClosingBeforeSave方法之后被回调;
  • projectClosed:与projectClosing的区别在于,projectClosed在项目已经关闭时被回调,在ProjectManagerImpl#closeProject方法执行到最后一行代码时被调用。

有了项目管理事件监听器之后,我们如何注册该监听器呢?

有两种方法,一种是代码方式注册,一种是在plugin.xml插件配置文件中注册。

代码方式注册可调用ProjectManager.getInstance().addProjectManagerListener();方法注册,但这种方式注册有一个弊端,就是无法监听到项目打开事件,projectOpened方法不会被调用,应该在我们能够调用该方法注册监听器时,项目实际已经打开了。

所以注册项目管理监听器我们只能通过修改plugin.xml配置文件方式注册,配置代码如下:

<applicationListeners> 
    <listener class="com.msyc.ycpay.plugin.listener.MyProjectManagerListener" 
              topic="com.intellij.openapi.project.ProjectManagerListener"/> 
</applicationListeners> 
  • topic:填写事件主题,类似于消息中间件中的Topic,只不过这里填写的是事件监听器的接口类名;
  • class:添加接口的实现类名;

当我们给IDEA注册自定义的项目管理事件监听器后,我们就可以通过项目管理事件监听器注册其它的事件监听器,例如注册模块监听事件,这是因为模块的事件触发在项目打开事件触发之后才会触发。因此,在projectOpened方法中可注册任何其它的事件监听器。

注册模块事件监听器代码如下:

project.getMessageBus().connect() 
.subscribe(ProjectTopics.MODULES, new ModuleListener(){}); 

subscribe方法需要两个参数:

  • topic:主题,可选值参见ProjectTopics类的源码,有PROJECT_ROOTS和MODULES;
  • handler:事件处理器、监听器,当topic为MODULES时,要求传递一个ModuleListener;

ModuleListener接口的定义如下:

public interface ModuleListener extends EventListener { 
  default void moduleAdded(@NotNull Project project, @NotNull Module module) { 
  } 
  default void beforeModuleRemoved(@NotNull Project project, @NotNull Module module) { 
  } 
  default void moduleRemoved(@NotNull Project project, @NotNull Module module) { 
  } 
  default void modulesRenamed(@NotNull Project project, @NotNull List<Module> modules, @NotNull Function<Module, String> oldNameProvider) { 
  } 
} 
  • moduleAdded:添加模块完成时被调用;
  • beforeModuleRemoved:模块被移除之前被调用;
  • moduleRemoved:模块被移除时被调用;
  • modulesRenamed:模块修改名字时被调用;

如何监听文件编辑事件

通过前面两篇的学习,我们已经了解什么是PSI,知道一个文件对应一个PsiFile,一个PsiFile本身也是一个PsiElement,由许多的PsiElement构成,每个PsiElement也都可以有子PsiElement。

因此,监听文件改变事件其实就是监听PSI树的结构改变事件,我们需要通过PsiManager注册PsiTreeChangeListener,代码如下。

PsiManager.getInstance(project).addPsiTreeChangeListener( 
                new PsiTreeChangeListener() { 
                    // ..... 
                }, FILES::clear); 

至于注册时机,视情况而定,可以在Service初始化时注册,可以在AnAction触发时注册,也可以在projectOpened事件方法中注册。

PsiTreeChangeListener接口定义的方法较多,可以分为两类事件,一类是before事件、一类是after事件,接口源码如下。

public interface PsiTreeChangeListener extends EventListener { 
  void beforeChildAddition(@NotNull PsiTreeChangeEvent event); 
  void beforeChildRemoval(@NotNull PsiTreeChangeEvent event); 
  void beforeChildReplacement(@NotNull PsiTreeChangeEvent event); 
  void beforeChildMovement(@NotNull PsiTreeChangeEvent event); 
  void beforeChildrenChange(@NotNull PsiTreeChangeEvent event); 
  void beforePropertyChange(@NotNull PsiTreeChangeEvent event); 
 
  void childAdded(@NotNull PsiTreeChangeEvent event); 
  void childRemoved(@NotNull PsiTreeChangeEvent event); 
  void childReplaced(@NotNull PsiTreeChangeEvent event); 
  void childrenChanged(@NotNull PsiTreeChangeEvent event); 
  void childMoved(@NotNull PsiTreeChangeEvent event); 
  void propertyChanged(@NotNull PsiTreeChangeEvent event); 
} 
  • childrenChanged:子元素内容改变时被调用;
  • childReplaced:子元素被替换时被调用,触发childReplaced事件也会伴随着childrenChanged事件;
  • childAdded:子元素添加时被调用,触发childAdded事件时也会伴随着childReplaced、childrenChanged或事件;
  • childRemoved:子元素移除时被调用,触发childRemoved事件也会伴随着childReplaced、childrenChanged事件;
  • propertyChanged:属性改变时被调用,例如修改文件名;

最后

“编写一个IDEA插件”系列暂时就写这些,因为对这方面感兴趣的读者可能比对汇编语言感兴趣的读者还少。其实这几篇分析的也是笔者写插件过程中用到的一些笔者认为非常重要的知识点,当然还有很多没分享,如果要继续写,估计还可以写几篇,但看到上篇的阅读量就没动力继续写下去了。

参考:

  • intellij-platform-plugin-template的项目管理监听器注册:https://sourcegraph.com/github.com/JetBrains/intellij-platform-plugin-template@main/-/blob/src/main/resources/META-INF/plugin.xml#L17:55
  • 接收有关项目结构变更的通知:https://jetbrains.org/intellij/sdk/docs/reference_guide/project_model/project.html?search=projectClosingBeforeSave

本文转载自微信公众号「Java艺术」,可以通过以下二维码关注。转载本文请联系Java艺术公众号。

QvQJnuy.png!mobile


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK