IntelliJ IDEA插件开发指南(二)

概要

上节中详细聊了下IDEA插件的创建以及plugin.xml的具体含义。本节来聊聊IDEA提供的插件开发关键类,IDEA中提供的用于进行插件开发的接口非常多,本章主要介绍几个开发时常见的接口和类。

AnAction

当我们想扩展IDEA提供的菜单栏,那么就可以通过创建Action类来实现相应的功能。

创建Action

创建Action有两种方式:

  1. 创建一个类,然后继承AnAction类,通过重写其actionPerformed()方法和update()方法;最后需要在Plugin.xml中进行配置
  2. 通过IDEA提供的界面进行创建,并重写其actionPerformed()方法和update()方法,如下图所示:
    这里写图片描述
    第二种方法会自动在Plugin.xml注册相应的Action信息。下面我会以Lombok为例,以此来说明下Action的使用~

Lombok中plugin.xml中的Action配置为:

<group id="LombokActionGroup" text="Lombok" description="Refactor code with lombok annotations" 
           icon="/icons/lombok.png" popup="true"> 
      <action id="defaultLombokData" class="de.plushnikov.intellij.plugin.action.lombok.LombokDataAction" 
              text="Default @Data" description="Action to replace getter/setter/equals/hashcode/toString methods with lombok @Data annotation"> 
      </action> 
      <separator/>
      <action id="defaultLombokGetter" class="de.plushnikov.intellij.plugin.action.lombok.LombokGetterAction" 
              text="Default @Getter" description="Action to replace all getter methods with lombok @Getter annotation"> 
      </action>
      <!-- 中间有action删减 -->
      <add-to-group group-id="RefactoringMenu" anchor="last"/> 
</group>

下面解释下具体的Action配置的关键信息:

  • group id:多个Action的组合,id需要唯一
  • group text:工具栏中group显示的信息
  • group icon:group图标
  • group popup:action是否为弹出
  • action id:action ID,需要唯一
  • action class:具体的Action实现类的全限定名
  • action text:当前action在工具栏中的展示信息
  • add-to-group:这个是比较关键的信息,group-id决定着当前group或者action显示在工具栏的具体地方,anchor决定着该action或者group显示在该工具栏的具体地方。

关于Action的关键信息可能解释的比较抽象,下面结合以上group定义来看看。

上述配置中定义了一个text为 Lombok 的group,其出现的位置为 add-to-group 中定义的 RefactoringMenu,即工具栏的refactor目录中,位置为 anchor定义的 last,即在最下面。在该group中又定义了多个action,出现方式为弹出(group中popup=”true”),最后的结果如下图所示:
这里写图片描述
对于add-to-group,其group-id可以有多种选择,如WindowMenu,HelpMenu, ToolsMenu等等,分别觉得则该group或者action在哪个工具栏中显示,而anchor的取值可以有first,last和before,first和last分别代表出现的位置为最前和最后,当取值为before的时候,还需要指定一个relative-to-action用于定位~

当然,如果不需要一个group来组合多个action功能,那么定义一个action即可,如下:

<action id="testAction" class="com.test.TestAction" text="testAction" 
            description="test"> 
      <add-to-group group-id="HelpMenu" anchor="last"/> 
</action> 

那么这时候就会在Help工具栏的最后一行中创建一个名为 testAction 的按钮啦~

重写AnAction的方法

重写actionPerformed函数

在菜单栏中点击我们定义的action时,就会执行具体Action类中的actionPerformed函数。当回调actionPerformed()方法时,就相当于当前的Action被点击了一次。

重写update函数

为了响应用户的点击事件,我们重写了actionPerformed方法。在actionPerformed方法中执行一些响应的逻辑,比如弹出一个对话框,在打开的class中自动生成相关的代码等操作。

但是有时候我们定义的插件只在某些场景中才可以使用,比如说我们编写自动生成代码的插件时,只有当文件打开且是相应的类型时才能正常执行;如果不符合条件,就应该将插件按钮置为不能点击。

当我们不希望用户点击我们定义的插件时,我们可以将插件隐藏,让用户无法看到插件,只有当符合插件执行的环境时,才让插件在菜单中显示~

这时为了能让用户在点击自定义插件对应的菜单栏之前动态判断该插件是否能够点击,只需要重写update函数。

update什么时候被回调呢?在IDEA中有很多的菜单栏,比如help,Window,Tools等等,当点击这些菜单栏使得Action菜单项显示出来时,就会回调update函数。

update函数被回调时,会传入AnActionEvent对象,通过AnActionEvent对象我们可以判断出当前编辑框是否打开等实时的IDEA环境状况。

以Lombok为例,只有当文件类型为.java以及文件打开时才能点击相应的action;当文件为xml时不能点击:
这里写图片描述

PsiAugmentProvider

对于这个类,IDEA中提供的说明是:有些代码不是它看起来的样子! 这种扩展允许插件增强现实,改变Java PSI元素的行为。

什么意思呢?我们先来看看PSI的定义:

The Program Structure Interface, commonly referred to as just PSI, is the layer in the IntelliJ Platform that is responsible for parsing files and creating the syntactic and semantic code model that powers so many of the platform’s features.

这里具体的和PSI相关的不会详细解释,只需要知道IDEA中所有文件以及文件中的内容都是用PSI树来表示的,比如类表示为PsiClass,方法表示为PsiMethod,字段表示为PsiField。我们可以通过更改PSI来做到动态的添加字段,方法等。具体的PSI相关的文章可以参考文献4和文献5~

上面说的比较抽象,我们来举一个简单的例子,还是以Lombok为例。了解Lombok原理的同学都知道,Lombok利用的是javac技术,就是在java文件编译的时候动态的将getter,setter等方法生成到class文件中。

这种方式就出带来一个问题,由于getter和setter方法在编译的时候才会动态生成,实际运行时不会有任务问题;但是在IDEA中,编译检查时会发现没有对应的setter或者getter方法,就会出现“标红”,提示没有该字段或者方法。

其实这时就应该清楚了,我们可以通过继承PsiAugmentProvider这个类,通过扩展PSI,动态的为该类中新增相应的“虚拟”的getter或者setter方法。以此来解决“标红”问题。

这时在编辑框中是没有通过PsiAugmentProvider扩展的这些方法,但是实际调用的时候却有,这也就是上述所说的:有些代码不是它看起来的样子!

动态扩展的PSI方法或者字段,在编译时不会生成。在进行扩展方法时,只需要生成具体的方法签名即可~不需要关注具体的实现细节。

重写getAugments方法

通过重写getAugments方法可以扩展每个文件的PSI,即实现动态的扩展相应的方法或者字段。

getAugments中会传入两个参数,分别为PsiElement psiElement和Class type;PsiElement是PSI系统下不同类型对象的一个统称,是基类;比如之前提到的PsiMethod、PsiClass等等都是一个个具体的PsiElement的实现。Class type,type类型可以用于判断具体是PsiMethod还是PsiClass,进行分开处理。

在getAugments中进行扩展相应方法时,需要借助IDEA中提供的LightMethodBuilder类。通过设置方法名,方法的修饰符,方法的返回值,方法的传入即可添加一个方法。

比如现在需要为mobile字段,添加set方法,那么具体的代码为:

PsiManager manager = psiField.getManager(); 
LightMethodBuilder method =  new LightMethodBuilder(manager, JavaLanguage.INSTANCE, methodName); 
method.addModifier(PsiModifier.PUBLIC); 
method.setContainingClass(psiClass); 
method.setNavigationElement(psiField);
method.addParameter(psiField.getName(), psiField.getType());
method.setMethodReturnType(PsiType.VOID);

在getAugments中进行扩展相应字段时,需要借助IDEA中提供的LightFieldBuilder类。同样通过设置字段名,字段的修饰符即可添加一个字段。

比如现在需要添加 private static final Logger logger字段,那么具体的代码为:

PsiType psiLoggerType = psiElementFactory.createTypeFromText(LOGGER_TYPE, psiClass);
LightFieldBuilder loggerField = new LightFieldBuilder(manager, LOGGER_NAME, psiLoggerType); 
LightModifierList modifierList = (LightModifierList) loggerField.getModifierList(); 
modifierList.addModifier(PsiModifier.PRIVATE); 
modifierList.addModifier(PsiModifier.STATIC); 
modifierList.addModifier(PsiModifier.FINAL); 
loggerField.setContainingClass(psiClass); 
loggerField.setNavigationElement(psiAnnotation);

在PsiAugmentProvider中,对于每个生成的method和field,都需要加入到Cache当中,以此来保证每次获取时候的性能~在IDEA插件开发当中可以选择其提供的CachedValuesManager~

StructureViewExtension

顾名思义,IDEA提供的这个扩展接口就是为了对其提供的Structure功能进行扩展,同样以lombok项目为例,Structcture中能够清晰明了的显示当前类的结构,如下图所示:
这里写图片描述
Structcture中详细显示当前类中的字段和方法,如上节提到的如果我们通过PsiAugmentProvider提供了相关的字段和方法,如果没有继承StructureViewExtension并重写其相关的方法,那么在如图所示的structure结构中就不会显示相关的字段和方法。所以一般使用了PsiAugmentProvider都需要再继承StructureViewExtension并重写相应的方法。

StructureViewExtension接口中字段和方法如下:

ExtensionPointName<StructureViewExtension> EXTENSION_POINT_NAME = ExtensionPointName.create("com.intellij.lang.structureViewExtension");

  Class<? extends PsiElement> getType();

  StructureViewTreeElement[] getChildren(PsiElement parent);
  @Nullable
  Object getCurrentEditorElement(Editor editor, PsiElement parent);
  • EXTENSION_POINT_NAME字段是创建一个扩展点,用于在plugin.xml中配置使用
  • getType():获得具体的类型,一般为PsiClass.class
  • getCurrentEditorElement():由于只查看相应的结构,不需要获得编辑对象,返回nulll即可
  • getChildren():获得子元素,还记得在PsiAugmentProvider中我们通过LightFieldBuilder或者LightMethodBuilder来创建字段或者对象么;如果需要扩展StructureViewExtension,这里可以继承LightFieldBuilder或者LightMethodBuilder来创建相应的方法,getChildren()中遍历字段和方法,通过instanceof判断是否是为具体的对象,如果是则加入到result列表中即可~
@Override
  public StructureViewTreeElement[] getChildren(PsiElement parent) {
    Collection<StructureViewTreeElement> result = new ArrayList<StructureViewTreeElement>();
    final PsiClass psiClass = (PsiClass) parent;

    for (PsiField psiField : psiClass.getFields()) {
      if (psiField instanceof LombokLightFieldBuilder) {
        result.add(new PsiFieldTreeElement(psiField, false));
      }
    }

    for (PsiMethod psiMethod : psiClass.getMethods()) {
      if (psiMethod instanceof LombokLightMethodBuilder) {
        result.add(new PsiMethodTreeElement(psiMethod, false));
      }
    }

    for (PsiClass psiInnerClass : psiClass.getInnerClasses()) {
      if (psiInnerClass instanceof LombokLightClassBuilder) {
        result.add(new JavaClassTreeElement(psiInnerClass, false, new HashSet<PsiClass>() {{
          add(psiClass);
        }}));
      }
    }

    if (!result.isEmpty()) {
      return result.toArray(new StructureViewTreeElement[result.size()]);
    } else {
      return StructureViewTreeElement.EMPTY_ARRAY;
    }
  }
  • 8
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值