从零到一打造一款提升效率的IDEA插件-根据java doc自动生成枚举代码

大家好,这里是小奏 ,觉得文章不错可以关注公众号小奏技术

背景

最近写枚举代码感觉写的有点烦,每次都要重复复制代码

枚举的代码大致都是这种模式

java 复制代码
import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

@Getter
@AllArgsConstructor
public enum XiaoZouStatusEnum {

    A(0, "待处理"),

    B(1, "处理中"),

    C(2, "已完成"),
    
    D(3, "失败");

    private final int code;

    private final String description;

    public static final Map<Integer, XiaoZouStatusEnum> ENUM_MAP = Arrays.stream(XiaoZouStatusEnum.values())
            .collect(Collectors.toMap(XiaoZouStatusEnum::getCode, e -> e));

    public static XiaoZouStatusEnum parse(int type) {
        return ENUM_MAP.get(type);
    }

    public static Optional<XiaoZouStatusEnum> parseOptional(int type) {
        return Optional.ofNullable(ENUM_MAP.get(type));
    }
}

所以想实现一个idea的插件,右键直接自动生成枚举代码,提升下开发效率

现在用AI去实现也很快,但是感觉还是没有idea右键点一下快

因为AI还要去打字描述半天需求,可能不一定符合预期

实现

环境准备和项目创建

目前2025版本的idea插件开发需要JDK17+,然后需要安装一个idea插件开发的插件

插件安装完后我们就用idea创建创建一个枚举自动生成插件的项目

项目名我们就叫enum-generate

创建完成后的项目结构大致如下

css 复制代码
.
├── gradle
├── build.gradle.kts
├── gradle.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
└── src
    ├── main
    │   ├── java
    │   └── resources
    │       └── META-INF
    │           └── plugin.xml  // 插件的核心配置文件
    └── test

定义插件的Action

我们需要创建一个 Action,当用户在编辑器中右键点击一个字段时,这个 Action 会出现在菜单中。

  1. src/main/java 目录下创建一个包名为io.github.weihubeats.enumgenerate

  2. 新建一个GenerateEnumAction

java 复制代码
public class GenerateEnumAction extends AnAction {

    @Override
    public void actionPerformed(@NotNull AnActionEvent e) {
        // 插件的主要逻辑将在这里实现
    }
}

注册Action

创建了Action class后,我们需要在plugin.xml 文件中注册它,这样IDEA才能知道它的存在以及何时显示它

打开 src/main/resources/META-INF/plugin.xml 文件,在 标签内添加如下代码:

java 复制代码
    <actions>
        <action id="io.github.weihubeats.enumgenerate.GenerateEnumAction"
                class="io.github.weihubeats.enumgenerate.GenerateEnumAction"
                text="Generate Enum from Field"
                description="Generates a Java enum class based on the selected field's Javadoc.">
            <add-to-group group-id="EditorPopupMenu" anchor="last"/>
        </action>
    </actions>

这些配置我们详细说明一下作用

  • <add-to-group>: 将这个菜单组添加到了编辑器的右键弹出菜单 (EditorPopupMenu) 的最后。

  • <action>: 注册了我们的GenerateEnumAction

    • id: Action 的唯一标识

    • class: 指向我们创建的 Action 类的完全限定名

    • text: 显示在菜单项上的文本

    • description: 鼠标悬停时显示的描述信息

实现Action的核心逻辑

我们上面的代码只继承了AnAction,没有实现我们的核心逻辑

核心逻辑也很简单

java 复制代码
package io.github.weihubeats.enumgenerate;

import com.intellij.ide.highlighter.JavaFileType;
import com.intellij.openapi.actionSystem.ActionUpdateThread;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiFileFactory;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.jetbrains.annotations.NotNull;

public class GenerateEnumAction extends AnAction {

    @Override
    public void update(@NotNull AnActionEvent e) {
        // 控制 Action 的可见性
        PsiElement psiElement = e.getData(CommonDataKeys.PSI_ELEMENT);
        // 只有当选中的元素是 PsiField (类属性)时,才显示此 Action
        e.getPresentation().setEnabledAndVisible(psiElement instanceof PsiField);
    }

    @Override
    public void actionPerformed(@NotNull AnActionEvent e) {
        // 获取当前上下文信息
        Project project = e.getProject();
        // 获取当前光标下的 PSI (Program Structure Interface) 元素
        PsiElement psiElement = e.getData(CommonDataKeys.PSI_ELEMENT);

        // 判断该元素是否为一个类的字段
        if (project == null || !(psiElement instanceof PsiField)) {
            return;
        }

        PsiField selectedField = (PsiField) psiElement;
        PsiClass containingClass = selectedField.getContainingClass();
        if (containingClass == null) {
            return;
        }

        // 1. 解析 Javadoc
        String javadocComment = Objects.requireNonNull(selectedField.getDocComment()).getText();
        String enumConstantsString = parseJavadoc(javadocComment);
        if (enumConstantsString.isEmpty()) {
            // 可以添加一个提示,告知用户 Javadoc 格式不正确
            return;
        }

        // 2. 生成枚举类名
        String baseClassName = containingClass.getName().replaceAll("(DO|DTO|VO)$", "");
        String fieldName = selectedField.getName();
        String capitalizedFieldName = fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
        String enumName = baseClassName + capitalizedFieldName + "Enum";

        // 3. 生成枚举类代码
        String enumCode = generateEnumCode(enumName, enumConstantsString);

        // 4. 在同一个目录下创建文件并写入代码
        PsiDirectory containingDirectory = containingClass.getContainingFile().getContainingDirectory();
        createEnumFile(project, containingDirectory, enumName, enumCode);
    }

    /**
     * 解析 Javadoc 注释,提取枚举常量信息
     * 格式一: 0-待处理
     * 格式二: 1:处理中
     * 格式三: 2:已完成
     * 格式四: 3 = 失败
     * 格式五(带空格): 4 - 已归档
     * 也可以混用,用逗号或换行分隔
     * 5:已取消, 6-已删除
     * @param javadoc Javadoc 字符串
     * @return 格式化后的枚举常量字符串
     */
    private String parseJavadoc(String javadoc) {
        // 新的正则表达式,更强大、更灵活
        // \s* 表示匹配零个或多个空格
        // [-::=] 表示匹配 - 或 : 或 :(中文冒号) 或 =
        Pattern pattern = Pattern.compile("(\\d+)\\s*[-::=]\\s*([^,\\n]*)");
        Matcher matcher = pattern.matcher(javadoc);
        StringBuilder constantsBuilder = new StringBuilder();
        int count = 0;
        char enumConstName = 'A';

        while (matcher.find()) {
            if (count > 0) {
                constantsBuilder.append(",\n\n");
            }
            String code = matcher.group(1).trim();
            String description = matcher.group(2).trim();
            // 使用 A, B, C... 作为枚举名
            constantsBuilder.append("    ").append(enumConstName++).append("(").append(code).append(", \"").append(description).append("\")");
            count++;
        }
        if(count > 0) {
            constantsBuilder.append(";");
        }

        return constantsBuilder.toString();
    }

    /**
     * 生成完整的枚举类代码
     *
     * @param enumName          枚举类名
     * @param enumConstants     枚举常量字符串
     * @return 完整的 Java 代码
     */
    private String generateEnumCode(String enumName, String enumConstants) {
        return "import lombok.RequiredArgsConstructor;\n" +
            "import lombok.Getter;\n\n" +
            "import java.util.Arrays;\n" +
            "import java.util.Map;\n" +
            "import java.util.Optional;\n" + 
            "import java.util.stream.Collectors;\n\n" +
            "@Getter\n" +
            "@RequiredArgsConstructor\n" +
            "public enum " + enumName + " {\n\n" +
            enumConstants + "\n\n" +
            "    private final int code;\n" +
            "    private final String description;\n\n" +
            "    public static final Map<Integer, " + enumName + "> ENUM_MAP = Arrays.stream(" + enumName + ".values())\n" +
            "            .collect(Collectors.toMap(" + enumName + "::getCode, e -> e));\n\n" +
            "    public static " + enumName + " parse(int type) {\n" +
            "        return ENUM_MAP.get(type);\n" +
            "    }\n\n" +

            "    public static Optional<" + enumName + "> parseOptional(int type) {\n" +
            "        return Optional.ofNullable(ENUM_MAP.get(type));\n" +
            "    }\n" +

            "}\n";
    }


    /**
     * 创建并写入枚举文件
     *
     * @param project     当前项目
     * @param directory   目标目录
     * @param enumName    枚举类名
     * @param enumCode    枚举代码
     */
    private void createEnumFile(Project project, PsiDirectory directory, String enumName, String enumCode) {
        // 使用 WriteCommandAction 来执行文件写入操作,确保操作是可撤销的
        WriteCommandAction.runWriteCommandAction(project, () -> {
            try {
                String fileName = enumName + ".java";
                // 检查文件是否已存在
                if (directory.findFile(fileName) != null) {
                    // 文件已存在,可以提示用户或直接覆盖
                    System.out.println("File " + fileName + " already exists.");
                    return;
                }
                PsiFileFactory fileFactory = PsiFileFactory.getInstance(project);
                PsiFile javaFile = fileFactory.createFileFromText(fileName, JavaFileType.INSTANCE, enumCode);
                directory.add(javaFile);
                System.out.println("Successfully generated " + fileName);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        });
    }

    @Override
    public @NotNull ActionUpdateThread getActionUpdateThread() {
        return ActionUpdateThread.BGT;
    }
}

依赖管理

由于我们代码生成使用了lombok相关的注解,所以我们添加一下lombok相关的依赖

我们在build.gradle.kts添加如下信息

kts 复制代码
dependencies {
    intellijPlatform {
        create("IC", "2024.2.5")
        testFramework(org.jetbrains.intellij.platform.gradle.TestFrameworkType.Platform)

         bundledPlugin("com.intellij.java")
    }

    compileOnly("org.projectlombok:lombok:1.18.30") 
    annotationProcessor("org.projectlombok:lombok:1.18.30")
}

运行测试

要运行很简单

直接点击GradleRun Plugin

这个时候会重新启动一个idea 然后我们就可以在新打开的idea中体验测试我们的插件

实际效果

插件安装

如果我们正常的idea要安装插件有两种方式

  1. 从本地磁盘安装
  2. 通过插件市场下载

从本地磁盘安装

本地首先打包出一个jar

运行jar后我们的项目build->libs下面就有一个jar了

然后通过本地导入安装即可

插件市场安装

插件市场安装我们就要先上传到插件市场

上传地址: plugins.jetbrains.com/developers/...

进入上传页面按table进行信息填写即可

上传完成后在1-2个工作日会审核

通过或者不通过都会发邮件进行通知

enum-generate插件市场地址:plugins.jetbrains.com/plugin/2811...

项目源码

总结

总的来说idea简单插件开发起来不是特别难。

但是不同idea的版本兼容还是很麻烦的

相关推荐
uzong12 分钟前
认知破局:在信息茧房时代重构后端工程师的思维思维
后端
Lisonseekpan1 小时前
MVCC的底层实现原理是什么?
java·数据库·后端·mysql
中东大鹅2 小时前
SpringBoot实现文件上传
java·spring boot·后端
David爱编程2 小时前
Java中main 方法为何必须是static?
java·后端
追梦人物3 小时前
Uniswap 手续费和协议费机制剖析
前端·后端·区块链
程序员Forlan3 小时前
SpringBoot查询方式全解析
java·spring boot·后端
PetterHillWater4 小时前
Kimi-K2模型真实项目OOP重构实践
后端·aigc
Moonbit5 小时前
月报 Vol.02:新增条件编译属性 cfg、#alias属性、defer表达式,增加 tuple struct 支持
后端·程序员·编程语言
Ray665 小时前
AviatorScript 表达式引擎
后端