JetBrains IDE插件开发教程(四)——Action

前言

前面都是在说模板生成的项目,现在开始搞真正的东西

Action System | IntelliJ Platform Plugin SDKhttps://plugins.jetbrains.com/docs/intellij/action-system.html上面是官网关于Action的相关介绍

ACTION中文(简体)翻译:剑桥词典https://dictionary.cambridge.org/zhs/%E8%AF%8D%E5%85%B8/%E8%8B%B1%E8%AF%AD-%E6%B1%89%E8%AF%AD-%E7%AE%80%E4%BD%93/actionaction的翻译为中文,意思是行为、行动。

从概念上可以感悟到,定义Action就是定义了一个行为,比如说,定义一个Action,这个Action是出现一个对话框,或者是出现一行字等之类的。

或者直白点,一个Action类似于按钮Button,

直接写代码

写代码

HelloAction.kt

那么应该写什么代码,主要是没有什么需求,那就随便写写吧!

就定义一个action,触发action就打印出日志hello world

简单点,笔者依然使用fitst

首先,先删除src/main/kotlin下的默认文件,建立一个新的包,笔者在初始化项目设置的包org.plugin。

在包下面,新建一个HelloAction.kt文件

其中的代码暂时如下

复制代码
package org.plugin

import com.intellij.openapi.actionSystem.AnAction

class HelloAction: AnAction() {
    
}

那么应该这么使用,可以看看被IDEA反编译后的java代码。

当然,里面有非常多的内容,笔者复制关键的内容。

java 复制代码
public abstract class AnAction implements PossiblyDumbAware, ActionUpdateThreadAware {
    private static final Logger LOG = Logger.getInstance(AnAction.class);

    public AnAction() {
     ...
    }

    public AnAction(@Nullable Icon icon) {
        ...
    }

    public AnAction(@Nullable @ActionText String text) {
         ...
    }

    public AnAction(@NotNull Supplier<@ActionText String> dynamicText) {
            ...
    }

    public AnAction(@Nullable @ActionText String text, @Nullable @ActionDescription String description, @Nullable Icon icon) {
        ...
    }
    public void update(@NotNull AnActionEvent e) {
    }
    @OverrideOnly
    public abstract void actionPerformed(@NotNull AnActionEvent var1);

可以看到这是一个抽象类,多个constructor方法,子类必须实现actionPerformed方法。

还有其他方法,比如setDefaultIcon,setShortcutSet等之类的方法,笔者并没有展示。

笔者还专门复制了日志的初始化,那么就先初始化日志

java 复制代码
    private static final Logger LOG = Logger.getInstance(AnAction.class);

这个初始化是java,使用kotlin的关键代码如下

Kotlin 复制代码
import com.intellij.openapi.diagnostic.Logger    
private val logger: Logger = Logger.getInstance(HelloAction::class.java)

可以看看Logger的getInstance方法

java 复制代码
    public static @NotNull Logger getInstance(@NotNull Class<?> cl) {
        return ourFactory.getLoggerInstance("#" + cl.getName());
    }

从反编译的字节码可以看出需要传递Class,意思需要获取Java Class 对象。

相关的参考

Kotlin中的KClass:获取、用法与扩展功能详解-CSDN博客https://blog.csdn.net/Apple_wolf/article/details/135867799

AnAction::class Kotlin 的类引用,类型是 KClass<<AnAction>

AnAction::class.java 从 KClass 拿到对应的 Java Class<<AnAction>

::class 是 Kotlin 的类

::class.java 是 Java 的类

继续重写actionPerformed方法,简单的写法如下

Kotlin 复制代码
    override fun actionPerformed(p0: AnActionEvent) {
        logger.warn("Hello, World!")
    }

不需要搞这么复杂。

到目前为止,HelloAction.kt的代码如下

Kotlin 复制代码
package org.plugin

import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.diagnostic.Logger

class HelloAction: AnAction() {
    private val logger: Logger = Logger.getInstance(HelloAction::class.java)

    override fun actionPerformed(p0: AnActionEvent) {
        logger.warn("Hello, World!")
    }
}

那么接下来需要注册这个Action,到plugin.xml文件

plugin.xml注册action

去掉原有的extension,添加一些新的标签

XML 复制代码
    <actions>
        <action id="org.plugin.HelloAction"
                class="org.plugin.HelloAction" text="Hello World"
                description="Hello world">
            <add-to-group group-id="MainMenu" anchor="last"/>
        </action>
    </actions>

id:action的唯一标识符

class:Action 实现类的全限定类名(包名 + 类名)

group-id:是 IDE 菜单/工具栏的"容器"标识符。group-id 就是告诉 IDE:把按钮放进哪个现成的菜单里。

anchor:控制 Action 插入到菜单/工具栏的具体位置

group-id和anchor的可选项如下

group-id 对应位置
MainMenu 顶部菜单栏(File、Edit、View 旁边)
EditorPopupMenu 编辑器里 右键 弹出的菜单
ProjectViewPopupMenu 左侧项目树里 右键 弹出的菜单
ToolsMenu Tools 下拉菜单
HelpMenu Help 下拉菜单
MainToolbarLeft 顶部工具栏 左侧
MainToolbarRight 顶部工具栏 右侧
EditorTabPopupMenu 编辑器标签页 右键 菜单
RunContextGroup Run 相关的上下文菜单
achor值 含义
first 插到该组最前面
last 插到该组最后面
before 插到某个 Action 前面(需配合 relative-to-action)
after 插到某个 Action 后面(需配合 relative-to-action)

启动项目试试

启动后,笔者没有找到对应的Action,但是笔者在help里面发现一个东西

里面有个Find Action,如下

然后就出现一个搜索了,笔者搜索hello world,如下

点击了之后,看一下终端会不会出现什么东西。如下

可以发现出现了hello world。

笔者发现这个日志应该设置了等级的,笔者尝试过使用info,发现打印不出来

笔者多次探索,我以为是有个什么设置,但是笔者发现好像不需要那些

笔者修改了一下代码

Kotlin 复制代码
override fun actionPerformed(p0: AnActionEvent) {
        logger.warn("Hello, World!")
        logger.info("Hello!,666666666")
    }

启动并触发action。

在控制台上发现只有warn。

但是,笔者项目结构里面发现.intellijPlatform这个文件夹

展开看看

层层展开,可以发现,里面有一个关键的idea.log文件

打开可以发现里面有info信息,

触发action,结果如下

可以发现打印了info信息,说明文件日志的等级和控制台日志的等级应该是不一样的,

笔者尝试了其他等级的日志,debug和error,

TRACE < DEBUG < INFO < WARN < ERROR < FATAL

发现日志文件最低是info,而控制台最低是warn

debug不会显示的。

那么怎么修改等级,这里笔者没有解决,就交给读者了。

如何才能修改控制台或者日志文件的日志等级?

可以尝试修改位置,比如放在toolsMenu下,放在最后

Kotlin 复制代码
<add-to-group group-id="ToolsMenu" anchor="last"/>

结果如下图所示

但是,笔者发现会出现在其他位置

还是设置的last,但是不是在最后,可能是和其他action加载的顺序有关吗,笔者不理解。

总体来说,这就是使用的plugin.xml文件来注册的action,当然实际上可以完全通过代码注册

后面尝试一下。

设置快捷键

前面算是初步使用了action,那么继续来设置action的快捷键

设置快捷键显然也是使用plugin.xml最简单

在action标签里面写入如下内容

XML 复制代码
            <keyboard-shortcut first-keystroke="ctrl 1" keymap="$default"/>

意思是很显然的,设置快捷键ctrl+1,keymap的翻译一下表示键盘映射,default是默认的意思,

keymap="$default"表示在在 Windows/Linux 默认键位方案 下生效。

如果要设置mac os的快捷键 ,可以这样

XML 复制代码
<keyboard-shortcut first-keystroke="meta 1" keymap="Mac OS X"/>

修改一点action里面的内容,

Kotlin 复制代码
    private var a=1

    override fun actionPerformed(p0: AnActionEvent) {
        logger.warn("a=${++a}")
    }

如果在字符串里面可以直接使用$a,不需要加大括号

如果要在字符串里做**运算或表达式,**需要加大括号,如上面代码所示。

每次触发a就会+1。

启动,进入测试的IDEA,多次点击ctrl+1,然后返回写插件的IDEA的控制台,结果如下

笔者是没有问题的,如果因为某种原因快捷键ctrl+1不行,笔者建议换一个快捷键。


就这样,后面再说


笔者本来想使用代码设置快捷键,但搞了一会儿,没有成功,算了,多次一举。

actionPerformed

继续使用这个函数,前面只是打印了日志,这个函数就相当于按钮事件,点击之后就会执行,

其中有一个参数AnActionEvent,在字节码里面看看这是什么

java 复制代码
public class AnActionEvent implements PlaceProvider 

简单地说,这是一个类。

那么必然有其对应的属性和方法。

可以点击左边侧边栏,看看类的结构。

这里面有许多方法和属性,随便玩一玩。

place

Kotlin 复制代码
    override fun actionPerformed(p0: AnActionEvent) {
        val p=p0.place
        logger.warn(p)

    }

启动,结果如下

place是位置的意思,这个place属性,那么意思很显然,就是触发这个action所在的位置。

现在就设置两个进入方式,一个快捷键,一个在tool里面,但是显示是MainMenu,小事情。

可以添加一个进入方式,比如右键菜单菜单进入

在plugin.xml里面设置如下内容

XML 复制代码
<add-to-group group-id="EditorPopupMenu" anchor="last"/>

启动,右键,在弹出的菜单栏最下面可以发现hello world

点击即可触发,结果如下。

看看相关代码

java 复制代码
    public final @NotNull @NonNls String getPlace() {
        return this.myPlace;
    }

可以发现返回的是字符串,怪不得可以直接日志打印。

project

Kotlin 复制代码
    override fun actionPerformed(p0: AnActionEvent) {
        val p=p0.project
        logger.warn("$p")
    }

结果如下

java 复制代码
2026-05-30 22:27:43,322 [   8369]   WARN - #org.plugin.HelloAction 
Project(name=ides, 
containerState=COMPONENT_CREATED, 
componentStore=C:\Users\26644\IdeaProjects\ides)

可以发现,输出了项目的名字叫ides,这是笔者自己取的;

容器状态(containerState)------完成创建(COMPONENT_CREATED);

项目的配置文件的路径------C:\Users\26644\IdeaProjects\ides。

原来输出是这三个。


笔者有新的想法,先去研究一下


过了不知道多久,一言难尽,慢慢说

首先,前面project是一个Project

java 复制代码
  public final @Nullable Project getProject() {
    return getData(CommonDataKeys.PROJECT);
  }

能够被日志打印出来,说明必然实现了toString方法,不然日志打印会报错。

但是去看了看项目结构

发现没有toString方法。

笔者再一看

原来这个Project是一个接口。原来如此。

那么来看看到底是那个类实现了toString。

Kotlin 复制代码
 override fun actionPerformed(p0: AnActionEvent) {
        val p=p0.project?:return
        logger.warn("实际类: ${p.javaClass.name}")
        logger.warn("$p")
    }

获取p是那个类。

从上面接口,因为Project是可以为空的,后面?判断是否为空,为空直接return。

运行,结果如下

原来是com.intellij.openapi.project.impl.ProjectImpl

笔者点进去,如下

笔者点进去之后,发现是这样的

java 复制代码
public final fun toString(): kotlin.String { /* compiled code */ }

没有具体实现,确实是有toString。

这个时候,笔者突然就有一个想法,笔者就想看看到底源代码怎么写的。


笔者本来想写一写其中的经历,但是难以言明,有点怪

本来笔者使用的版本是2025.2.1,但是下载的源代码有问题。

因此,笔者使用的是2025.3.5。

总之,笔者最后成功了。


代码如下

其中含义就不展示了,总之,就是获取相关的信息,最近拼接字符串。

或者可以看github上的源码

intellij-community/platform/platform-impl/src/com/intellij/openapi/project/impl/ProjectImpl.kt at master · JetBrains/intellij-communityhttps://github.com/JetBrains/intellij-community/blob/master/platform/platform-impl/src/com/intellij/openapi/project/impl/ProjectImpl.kt#L377

getData

java 复制代码
  public final @Nullable <T> T getData(@NotNull DataKey<T> key) {
    return getDataContext().getData(key);
  }

getData是AnActionEvent的一个方法。

需要传入一个DataKey,前面获取Project,就是使用的getData这个方法,玩一玩。

java 复制代码
import com.intellij.openapi.actionSystem.CommonDataKeys 
override fun actionPerformed(p0: AnActionEvent) {
        logger.warn("________________________________")
        // 获取当前聚焦的编辑器
        val editor = p0.getData(CommonDataKeys.EDITOR)
        logger.warn("editor:$editor")

        // 获取当前选中的虚拟文件
        val virtualFile = p0.getData(CommonDataKeys.VIRTUAL_FILE)
        logger.warn("virtualFile:$virtualFile")

        // 获取当前 PSI 文件(已解析的 AST)
        val psiFile = p0.getData(CommonDataKeys.PSI_FILE)
        logger.warn("psiFile:$psiFile")

        // 获取当前选中的 PSI 元素(如光标下的类、方法)
        val psiElement = p0.getData(CommonDataKeys.PSI_ELEMENT)
        logger.warn("psiElement:$psiElement")

        // 获取导航目标(如在 Project View 中选中的文件/目录)
        val navigatable = p0.getData(CommonDataKeys.NAVIGATABLE)
        logger.warn("navigatable:$navigatable")

    }

当笔者打开一个文件

光标在最前面,此时,触发action,结果如下。

Kotlin 复制代码
editor:EditorImpl[file://C:/Users/26644/IdeaProjects/ides/src/main/kotlin/Main.kt]
virtualFile:file://C:/Users/26644/IdeaProjects/ides/src/main/kotlin/Main.kt
psiFile:KtFile: Main.kt
psiElement:null
navigatable:null

可以发现editor里面包含显示打开的文件,virtualFile就是打开文件,psiFile也是一个文件,但是没有完整的路径,其他都是null。

把光标移动到其他地方

比如,移动到main上面,触发action,看看打印了什么

Kotlin 复制代码
psiElement:FUN
navigatable:FUN

居然打印了函数,笔者已经感觉到了,这里面有许多好玩的东西,以后再说这样,慢慢来,不急。

如果移动到name上面会打印什么。

Kotlin 复制代码
psiElement:PROPERTY
navigatable:PROPERTY

打印的是PROPERTY,属性的意思。

移动到包的上面,看看会打印什么。

Kotlin 复制代码
psiElement:PsiPackage:org.example
navigatable:PsiPackage:org.example

打印了包,有点意思,以后在来玩玩。

看来这个getData就是获取其他关键东西的方法。

presentation

笔者突然发现一个事情,当我修改代码的时候,笔者每次都是重启的,没有热重载,

笔者突然发现如下东西

点击代码已更改右侧的按钮,就相当于热重载了,哦,原来如此。

但是不是一直都有,有时候没有了,有点怪。

不对

需要点击调试模式,就是那个虫子的按钮,才会出现,明白了。


继续。

Kotlin 复制代码
    override fun actionPerformed(p0: AnActionEvent) {
        val p=p0.presentation
        logger.warn("$p")
    }

打印结果如下

Hello World (Hello world), flags=enabled, visible, disable_group_if_empty

presentation的类型是Presentation

可以查看toString方法,看看打印的是什么东西

java 复制代码
 @Override
  public @NonNls String toString() {
    StringBuilder sb = new StringBuilder();
    sb.append(getText()).append(" (").append(descriptionSupplier.get()).append(")");
    sb.append(", flags=[");
    int start = sb.length();
    appendFlag(myFlags, IS_TEMPLATE, sb, start, "template");
    appendFlag(myFlags, IS_ENABLED, sb, start, "enabled");
    appendFlag(myFlags, IS_VISIBLE, sb, start, "visible");
    if (BitUtil.isSet(myFlags, IS_KEEP_POPUP_IF_REQUESTED) &&
        BitUtil.isSet(myFlags, IS_KEEP_POPUP_IF_PREFERRED)) {
      appendFlag(1, 1, sb, start, "keep_popup_always");
    }
    else {
      appendFlag(myFlags, IS_KEEP_POPUP_IF_REQUESTED, sb, start, "keep_popup_if_requested");
      appendFlag(myFlags, IS_KEEP_POPUP_IF_PREFERRED, sb, start, "keep_popup_if_preferred");
    }
    appendFlag(myFlags, IS_POPUP_GROUP, sb, start, "popup_group");
    appendFlag(myFlags, IS_PERFORM_GROUP, sb, start, "perform_group");
    appendFlag(myFlags, IS_HIDE_GROUP_IF_EMPTY, sb, start, "hide_group_if_empty");
    appendFlag(myFlags, IS_DISABLE_GROUP_IF_EMPTY, sb, start, "disable_group_if_empty");
    appendFlag(myFlags, IS_APPLICATION_SCOPE, sb, start, "application_scope");
    appendFlag(myFlags, IS_PREFER_INJECTED_PSI, sb, start, "prefer_injected_psi");
    appendFlag(myFlags, IS_ENABLED_IN_MODAL_CONTEXT, sb, start, "enabled_in_modal_context");
    sb.append("]");
    return sb.toString();
  }

可以看出关键的东西。

第一个Hello World是getText返回的;

括号里面的Hello World是descriptionSupplier.get返回的;

IS_ENABLED判断是否能使用,返回enabled;

IS_VISIBLE判断是否可见,返回visible;

disable_group_if_empty 的意思是:对于一个ActionGroup,如果下面的所有子 Action 当前都不可见时,那么这个ActionGroup也置灰(禁用)。

但是目前只是看的单个Action,没有设置group,这个标志,不管它。

看来这个presentation是关于Action的信息。

inputEvent

这个其实是关于键盘的事情,笔者就不废话了

直接看代码

Kotlin 复制代码
package org.plugin

import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.diagnostic.Logger
import java.awt.event.InputEvent
import java.awt.event.KeyEvent
import java.awt.event.MouseEvent

class HelloAction : AnAction() {
    private val logger: Logger = Logger.getInstance(HelloAction::class.java)

    override fun actionPerformed(p0: AnActionEvent) {
        val p: InputEvent =p0.inputEvent?:return
        when(p){
            is KeyEvent -> {
                val isCtrl=p.isControlDown
                logger.warn("keyChar=${p.keyChar}, Ctrl=$isCtrl")
            }
            is MouseEvent -> {
                logger.warn("鼠标 button=${p.button}, 点击次数=${p.clickCount}")
            }
        }

    }


}

结果如下

is 是进行类型检查。


行,就这样,慢慢来

笔者感觉自己同时在使用java和kotlin

update

前面都是使用的actionPerformed方法,或者说,使用的是AnActionEvent相关的东西。

继续使用另一个方法update

Kotlin 复制代码
    override fun update(e: AnActionEvent) {
        super.update(e)
    }

有AnActionEvent,那也可以使用上面那些东西的。

先看看actionPerformed的执行顺序

Kotlin 复制代码
    override fun actionPerformed(p0: AnActionEvent) {
        logger.warn("777777")
    }

    override fun update(e: AnActionEvent) {
        logger.warn("12333")
    }

笔者使用ctrl+1触发,根据日志可以推测是先触发update然后是actionPerformed

当我点击在tools这个

发现只是触发了update

需要点击这个action才能触发actionPerformed

看来这个update是在actionPerformed之前。

update实际上是专门用于在 Action 被展示之前更新其状态(可用性、可见性、文本等)。

比如

判断鼠标是否在kt文件中,如果不住,就不可以使用这个Action

Kotlin 复制代码
 override fun update(e: AnActionEvent) {
        // 获取当前文件
        val virtualFile=e.getData(CommonDataKeys.VIRTUAL_FILE)
        // 判断null 和设置默认值
        val ext=virtualFile?.extension?:"java"
        logger.warn("ext=$ext")
        if(ext!="kt"){
            e.presentation.isEnabled=false
        }

    }

代码意思是很显然的,先获取当前的文件,然后判断null,不是null,获取文件的后缀,

最后判断是不是kt文件,结果如下。

刚开始不是kt,默认设置为java,打开kt文件,触发action,打印了日志,没问题。


就这样吧,不急,后面再说


这个update方法,是很频繁的,笔者前面打开tool就调用了,因此,必须做轻量的操作。

比如读取文件,发生请求之类的,就不用写在update,这些显然写在actionPerformed里面。

ActionGroup

意思是很显然的,就是有多个action,笔者尽力简单点。

说起来,笔者搞了这么久,还没有建立org.plugin包。

建立包,结果如下

笔者在org.plubin包的下面新建MyGroup.kt和WorldAction.kt文件

文件内容很简单,如下

Kotlin 复制代码
package org.plugin
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.diagnostic.Logger

class HelloAction : AnAction("Hello") {
    private val logger: Logger = Logger.getInstance(HelloAction::class.java)

    override fun actionPerformed(p0: AnActionEvent) {
        logger.warn("777777")
    }
}
Kotlin 复制代码
package org.plugin

import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.diagnostic.Logger

class WorldAction : AnAction("World") {
    private val logger: Logger = Logger.getInstance(WorldAction::class.java)

    override fun actionPerformed(p0: AnActionEvent) {
        logger.warn("8888")
    }
}
Kotlin 复制代码
package org.plugin

import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent

class MyGroup: ActionGroup("MyGroup") {
    override fun getChildren(e: AnActionEvent?): Array<out AnAction> {
        return arrayOf(HelloAction(), WorldAction())
    }

}

主要是没什么需求,随便写写,不重要。

这个ActionGroup啊,

还是继承AnAction。

其中只有一个getChildren必须要实现。

Kotlin 复制代码
  @ApiStatus.OverrideOnly
  public abstract @NotNull AnAction @NotNull [] getChildren(@Nullable AnActionEvent e);

返回一个数组,数组中元素的类型是AnAction

其中有个out关键字。参考如下。

泛型:in、out、where · Kotlin 官方文档 中文版https://book.kotlincn.net/text/generics.html笔者看了看

out是什么意思_out的翻译_音标_读音_用法_例句_爱词霸在线词典https://www.iciba.com/word?w=outout表示输出的,在外部,说白了,out的意思就是只能读,不能写。

还需要修改plugin.xml文件

XML 复制代码
   <actions>
        <group id="org.plugin.MyGroup"
               class="org.plugin.MyGroup"
               text="动态分组"
               popup="true">
            <add-to-group group-id="ToolsMenu" anchor="last"/>
        </group>
    </actions>

意思是很显然的。不必多说。

运行,结果如下

没问题。

总结

看了看action相关的东西。

相关推荐
laufing1 小时前
java web 基础 ---- servlet
java·servlet·web开发
程序猿乐锅1 小时前
【苍穹外卖|Day01】项目初识:从多模块结构到 OpenAPI 接口文档踩坑
java·spring·maven·mybatis
李白的天不白1 小时前
针对你遇到的 Client.Timeout exceeded 问题,我判断是防火墙拦截了 HTTPS 流量
java
linweidong1 小时前
Java 后端开发面试 50 个高频易混淆知识点详解
java·spring boot·spring·spring cloud·面试·mybatis·spring事务
码语智行1 小时前
应用启动和关闭监听器功能分析
java·spring boot
Resky08181 小时前
什么是 Spring IOC:倒过来让容器帮你 new,而不是你到处 new
java·spring
AutumnWind04201 小时前
【JDK动态代理源码梳理】
java·后端·spring
暗夜猎手-大魔王2 小时前
转载--Hermes Agent 10 | 7 层安全防线:从用户授权到输入净化
java·数据库·安全
idolao3 小时前
Oligo 7.60 安装教程:引物设计+Java 环境配置
java·开发语言