前言

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

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代码。

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

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等之类的方法,笔者并没有展示。

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

    private static final Logger LOG = Logger.getInstance(AnAction.class);

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

import com.intellij.openapi.diagnostic.Logger    
private val logger: Logger = Logger.getInstance(HelloAction::class.java)

可以看看Logger的getInstance方法

    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方法,简单的写法如下

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

不需要搞这么复杂。

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

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,添加一些新的标签

    <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左侧项目树里 右键 弹出的菜单
ToolsMenuTools 下拉菜单
HelpMenuHelp 下拉菜单
MainToolbarLeft顶部工具栏 左侧
MainToolbarRight顶部工具栏 右侧
EditorTabPopupMenu编辑器标签页 右键 菜单
RunContextGroupRun 相关的上下文菜单
achor值含义
first插到该组最前面
last插到该组最后面
before插到某个 Action 前面(需配合 relative-to-action)
after插到某个 Action 后面(需配合 relative-to-action)

启动项目试试

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

里面有个Find Action,如下

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

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

可以发现出现了hello world。

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

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

笔者修改了一下代码

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下,放在最后

<add-to-group group-id="ToolsMenu" anchor="last"/>

结果如下图所示

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

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

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

后面尝试一下。

设置快捷键

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

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

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

            <keyboard-shortcut first-keystroke="ctrl 1" keymap="$default"/>

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

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

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

<keyboard-shortcut first-keystroke="meta 1" keymap="Mac OS X"/>

修改一点action里面的内容,

    private var a=1

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

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

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

每次触发a就会+1。

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

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


就这样,后面再说


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

actionPerformed

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

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

public class AnActionEvent implements PlaceProvider 

简单地说,这是一个类。

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

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

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

place

    override fun actionPerformed(p0: AnActionEvent) {
        val p=p0.place
        logger.warn(p)

    }

启动,结果如下

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

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

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

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

<add-to-group group-id="EditorPopupMenu" anchor="last"/>

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

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

看看相关代码

    public final @NotNull @NonNls String getPlace() {
        return this.myPlace;
    }

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

project

    override fun actionPerformed(p0: AnActionEvent) {
        val p=p0.project
        logger.warn("$p")
    }

结果如下

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

  public final @Nullable Project getProject() {
    return getData(CommonDataKeys.PROJECT);
  }

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

但是去看了看项目结构

发现没有toString方法。

笔者再一看

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

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

 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

笔者点进去,如下

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

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

  public final @Nullable <T> T getData(@NotNull DataKey<T> key) {
    return getDataContext().getData(key);
  }

getData是AnActionEvent的一个方法。

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

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,结果如下。

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,看看打印了什么

psiElement:FUN
navigatable:FUN

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

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

psiElement:PROPERTY
navigatable:PROPERTY

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

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

psiElement:PsiPackage:org.example
navigatable:PsiPackage:org.example

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

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

presentation

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

笔者突然发现如下东西

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

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

不对

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


继续。

    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方法,看看打印的是什么东西

 @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

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

直接看代码

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

    override fun update(e: AnActionEvent) {
        super.update(e)
    }

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

先看看actionPerformed的执行顺序

    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

 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文件

文件内容很简单,如下

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")
    }
}
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")
    }
}
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必须要实现。

  @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文件

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

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

运行,结果如下

没问题。

总结

看了看action相关的东西。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐