前言
前面都是在说模板生成的项目,现在开始搞真正的东西
Action System | IntelliJ Platform Plugin SDK
https://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上的源码
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相关的东西。