Java

IntelliJ IDEA

插件开发

项目中 Entity对象增加字段后 DTO,VO等都需要增加相应的字段, 这些重复性的代码太讨厌了, 遂写个插件解决一劳永逸, 自己动手, 丰衣足食!

plugins welcome

前置条件

preliminary-steps

Use the following checklist to ensure that you are ready to develop your custom plugins.

  • Plugin DevKit plugin must be enabled in IntelliJ IDEA.
  • IntelliJ Platform SDK must be configured for your IDEA project.

1. Plugin DevKit

Plugin DevKit is a bundled IntelliJ IDEA plugin for developing plugins for the IntelliJ Platform using IntelliJ IDEA’s build system. It provides its custom SDK type and a set of actions for building plugins within the IDE.

需安装启用IntelliJ IDEA 自带的Plugin DevKit插件

2. IntelliJ Platform SDK

启用 Plugin DevKit 后, 还需要配置 IntelliJ Platform SDK, 可以在 File -< Project Structure > SDKs + 指定到idea的安装目录即可

  • 关于 SandBox 选项 IntelliJ IDEA 插件以 Debug/Run 模式运行时是在 SandBox 中进行的, 不会影响当前的 IntelliJ IDEA; 但是同一台机器同时开发多个插件时默认使用的同一个 sandbox

  • 关于源码调试 需要下载对应分支的社区版源码active, 附加到 Sourcepath github idea 社区版源码

插件的主要类型 Main Types of Plugins

Products based on the IntelliJ Platform can be modified and adjusted for custom purposes by adding plugins. All downloadable plugins are available from the JetBrains Marketplace.

The most common types of plugins include:

  • UI Themes
  • Custom language support
  • Framework integration
  • Tool integration
  • User interface add-ons

types-of-plugins

插件的配置文件 Plugin Content

Plugin Content plugin-configuration-file

Plugin distribution will be built using Gradle or Plugin DevKit. The plugin jar file must contain:

  1. the configuration file (META-INF/plugin.xml) (Plugin Configuration File)
  2. the classes that implement the plugin functionality
  3. recommended: plugin logo file(s) (META-INF/pluginIcon*.svg) (Plugin Logo)

META-INF/plugin.xml插件的核心配置文件, 指定插件名称, 描述, 版本号, 支持的 IntelliJ IDEA 版本, 插件的 components 和 actions 以及软件商等信息

<idea-plugin>
  <id>org.yangfh.idea.iframetools</id>
  <name>iFrameTools</name>
  <version>1.0</version>
  <vendor email="718556228@qq.com" url="">yangfh</vendor>
  <description><![CDATA[
     深农院-框架工具插件:
     1. 实体新增字段自动补充其 DTO,ListDTO, Mapper代码(目前仅支持Java基本类型属性);
    ]]></description>
 
  <change-notes><![CDATA[
      build on 2022-03-21 ver 1.0: 第一个版本
    ]]>
  </change-notes>
  <!-- please see https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html for description -->
  <idea-version since-build="203.0"/>
  <!-- please see https://plugins.jetbrains.com/docs/intellij/plugin-compatibility.html
       on how to target different products -->
  <depends>com.intellij.modules.platform</depends>
  <!-- 同时需要引入,否则2020版本找不到 com.intellij.psi -->
  <depends>com.intellij.modules.lang</depends>
  <depends>com.intellij.modules.java</depends>
 
  <extensions defaultExtensionNs="com.intellij">
    <!-- Add your extensions here -->
  </extensions>
 
  <actions>
    <!-- Add your actions here -->
    <action id="iframetools.main" class="org.yangfh.idea.iframetools.actions.MainAction" text="iFrame-实体字段代码补充">
      <add-to-group group-id="CodeMenu" relative-to-action="Generate" anchor="before"/>
    </action>
 
  </actions>
 
</idea-plugin>

依赖可以选项 depends

https://plugins.jetbrains.com/docs/intellij/plugin-compatibility.html#modules-specific-to-functionality

Module or Plugin for <depends> ElementFunctionalityProduct Compatibility
com.intellij.modules.java or com.intellij.java

See Java below.
Java language PSI Model, Inspections, Intentions, Completion, Refactoring, Test FrameworkIntelliJ IDEA, Android Studio
com.intellij.modules.androidstudioAndroid SDK Platform, Build Tools, Platform Tools, SDK ToolsAndroid Studio
com.intellij.modules.cidr.langC, C++, Objective-C/C++ language PSI Model, Swift/Objective-C Interaction, Inspections, Intentions, Completion, Refactoring, Test FrameworkAppCode, CLion
com.intellij.modules.cidr.debuggerDebugger Watches, Evaluations, Breakpoints, Inline DebuggingAppCode, CLion, RubyMine
com.intellij.modules.appcode or com.intellij.appcode

See AppCode/CLion below.
Xcode Project Model, CocoaPods, Core Data Objects, Device & Simulator SupportAppCode
com.intellij.modules.clion or com.intellij.clion

See AppCode/CLion below.
CMake, Profiler, Embedded Development, Remote Development, Remote Debug, DisassemblyCLion
com.intellij.cidr.baseNative Debugger Integration, Utility Classes, C/C++ Project Model/Workspace Support (OCWorkspace, CidrWorkspace, etc.), C/C++ Build and Run SupportAppCode, CLion
com.intellij.databaseDatabase Tools and SQL language PSI Model, Inspections, Completion, Refactoring, QueriesDataGrip, IntelliJ IDEA Ultimate, AppCode, PhpStorm, PyCharm Professional, RubyMine, CLion, GoLand, Rider, and WebStorm if the Database Tools and SQL plugin is installed.
org.jetbrains.plugins.goGo language PSI Model, Inspections, Intentions, Completion, Refactoring, Test FrameworkGoLand
com.intellij.modules.pythonPython language PSI Model, Inspections, Intentions, Completion, Refactoring, Test FrameworkPyCharm, and other products if the Python plugin is installed.
com.intellij.modules.riderConnection to ReSharper Process in BackgroundRider
com.intellij.modules.rubyRuby language PSI Model, Inspections, Intentions, Completion, Refactoring, Test FrameworkRubyMine, and IntelliJ IDEA Ultimate if the Ruby plugin is installed.
com.intellij.modules.ultimateLicensingAll commercial IDEs (IntelliJ IDEA Ultimate, PhpStorm, DataGrip, …)
com.intellij.swiftSwift language PSI Model, Inspections, Intentions, Completion, Refactoring, Test FrameworkAppCode, CLion
com.jetbrains.phpPHP language PSI Model, Inspections, Intentions, Completion, Refactoring, Test FrameworkPhpStorm, and other products if the PHP plugin is installed.
JavaScriptJavaScript language PSI Model, Inspections, Intentions, Completion, Refactoring, Test FrameworkWebStorm, and other products if the JavaScript plugin is installed.

项目结构

├─.idea
├─out  编译输出
  └─production
      └─iFrameTools
├─resources
  └─META-INF
	└─plugin.xml  插件配置文件
└─src  java 源码
    └─org
        └─yangfh
            └─idea

迁移至 2023 版本

新版本是默认是用 grdadle 构建, Kotlin 语言开发.

  • 删除Kotlin 相关
  1. 删除 ./src/main/kotlin 文件夹
  2. 修改 build.gradle.kts
plugins {  
id("java")  
id("org.jetbrains.kotlin.jvm") version "1.8.21"  //删除
id("org.jetbrains.intellij") version "1.13.3"   //删除
}  
  
group = "org.yangfh"  
version = "1.0-SNAPSHOT"  
  
repositories {  
mavenCentral()  
}  
 
intellij {  
version.set("2022.2.5")  
type.set("IC") // Target IDE Platform  
 
 
 
// 另外插件依赖 在这里需要添加, 同时 plugin.xml 也照旧定义
//例如
// plugins.set(listOf("com.intellij.java"))
plugins.set(listOf(/* Plugin Dependencies */))  
}  
  
tasks {  
withType<JavaCompile> {  
sourceCompatibility = "17"  
targetCompatibility = "17"  
}  
 
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {   //删除
kotlinOptions.jvmTarget = "17"   
}  
  
patchPluginXml {  
sinceBuild.set("222")  
untilBuild.set("232.*")  
}  
  
signPlugin {  
certificateChain.set(System.getenv("CERTIFICATE_CHAIN"))  
privateKey.set(System.getenv("PRIVATE_KEY"))  
password.set(System.getenv("PRIVATE_KEY_PASSWORD"))  
}  
  
publishPlugin {  
token.set(System.getenv("PUBLISH_TOKEN"))  
}  
}

代码归档

Transclude of iFrameTool.zip

PSI

IDEA中所有文件以及文件中的内容都是用 PSI 树来表示的, 比如类表示为PsiClass, 方法表示为PsiMethod, 字段表示为PsiField; 我们可以通过更改PSI来做到动态的添加字段, 方法等;

com.intellij.psi.PsiFileFactory: 文件相关操作, e.g.创建文件等;

com.intellij.psi.PsiElementFactory: 元素相关操作, e.g.创建java方法, 注解, 字段, 构造方法等;

com.intellij.psi.PsiManager: 项目访问PSI服务的主要入口点, e.g.查找文件, 查找文件夹等;

com.intellij.psi.PsiClass: 在java类查找元素, e.g.查找方法, 字段, 注解;

com.intellij.psi.JavaPsiFacade: java元素查找等操作, e.g.查找类等;

Intellij IDEA 插件开发秘籍

PSI操作 简单的遍历

Project project = e.getProject();//当前项目
PsiFile psiFile = e.getData(CommonDataKeys.PSI_FILE);//当前PSI文件 (类似一个.java文件) 
for (PsiElement psiElement: file.getChildren()) {//变量所有子 (.java 文件, 包括: 包名, 类名, 字段, 方法...)
    if(psiElement instanceof PsiClass) {
        PsiClass clazz = (PsiClass) psiElement;//找到类 PSI
        // 
        // clazz.findFieldByName()
        // clazz.findMethodsByName();
        return clazz;
    }
}
 

PSI操作 创建字段, 方法, 注释..

PsiElementFactory psiElementFactory = PsiElementFactory.getInstance(project);//psi 元素创建工厂
psiElementFactory.createCommentFromText("这是个注释" ,psiDTOClass);//创建注释
psiElementFactory.createConstructor//构造函数
psiElementFactory.createField()//字段
psiElementFactory.createCodeBlock()//代码块

PSI操作 给某个方法插入代码块

JvmMethod [] toDtoMethods = clazz.findMethodsByName("toDto");
PsiElement toDtosourceElement = toDtoMethods[0].getSourceElement();//Method psi
PsiElement sourceBlock= toDtosourceElement.getLastChild();//相当于 中括号psi
 

PSI操作 数组字段追加

//selectField = {"code", "xxx"} 
PsiElement[] children = selectField.getChildren();
PsiElement value= children[children.length-2];
value.add(
    psiElementFactory.createExpressionFromText("\"name\"",selectField)//字符串字面量 "name"
);
 

PSI操作 创建表达式

psiElementFactory.createExpressionFromText(" int i = 5; ",selectField.getLastChild())

PSI操作 空行? 代码格式

Whitespaces and Imports

When working with PSI modification functions,you should never create individual whitespace nodes (spaces or line breaks) from the text. Instead, all whitespace modifications are performed by the formatter, which follows the code style settings selected by the user. Formatting is automatically performed at the end of every command, andif you need, you can also perform it manually using the reformat(PsiElement) method in the CodeStyleManager class.

Modifying the PSI

实测, 毛用也没有, 格式化基本废了

Actions

Actions The IntelliJ Platform provides the concept of actions. An action is a class derived from AnAction, whose actionPerformed() method is called when its menu item or toolbar button is selected.

com.intellij.openapi.actionSystem.AnAction 是所有 Action的基类;

update 函数

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

//实例 如果当前编辑器是java文件则Action可用, 否则不可用

@Override
public void update(@NotNull AnActionEvent e) {
    Project project = e.getProject();
    Editor editor = e.getData(CommonDataKeys.EDITOR);//当前 PSI编辑器
    if (editor == null){
        e.getPresentation().setEnabled(false);// 设置当前 action 菜单的可用性
        //e.getPresentation().setVisible(false);// 设置当前 action 菜单的可见性
        //e.getPresentation().setEnabledAndVisible(false);// 同时设置当前 action 菜单的 可见性和可用性
        return ;
    }
    PsiFile psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());//当前编辑 PSI文件
    if("JAVA".equals(psiFile.getFileType().getName())){
        e.getPresentation().setEnabled(true);
    }else{
        e.getPresentation().setEnabled(false);
    }
}

actionPerformed 函数

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

实例 如果当前编辑器, 以当前为Entity类, 查找其DTO类添加相同的字段

@Override
public void actionPerformed(AnActionEvent e) {
    Project project = e.getProject();
    Editor editor = e.getData(CommonDataKeys.EDITOR);//当前编辑PSI
    PsiFile psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());//当前编辑文件
    String fileName = psiFile.getName();
    String className = fileName.substring(0, fileName.length() - 5);
    //当做实体类
    PsiClass entityPsi = getPsiClassByFile(psiFile);
    //查找其DTO
    PsiClass entityDTOPsi = getPsiClassByName(className+"DTO", project);
    if(entityDTOPsi==null)return;
    final PsiElementFactory psiElementFactory = PsiElementFactory.getInstance(project);//psi 元素创建工厂
    final PsiField[] enityFields = entityPsi.getAllFields();
    WriteCommandAction.runWriteCommandAction(project, () -> {
        for (int i = 0; i < enityFields.length; i++) {
            PsiField enityField = enityFields[i];
            PsiField enityFieldCopy = psiElementFactory.createField(enityField.getName(), enityField.getType());
            //给DTO 添加字段
            entityDTOPsi.add( enityFieldCopy);
            System.out.println(" entityDTOPsi add "+ enityFieldCopy);
        }
    });
}
public static final PsiClass getPsiClassByName(String name, Project project){
    PsiShortNamesCache psiShortNamesCache = PsiShortNamesCache.getInstance(project);//缓存查找实例
    PsiClass[] psiClasss = psiShortNamesCache.getClassesByName(name, GlobalSearchScope.projectScope(project));
    if(psiClasss.length > 0){
        return psiClasss[0];
    }else{
        return null;
    }
}
public static final PsiClass getPsiClassByFile(PsiFile file){
    for (PsiElement psiElement: file.getChildren()) {
        if(psiElement instanceof PsiClass) {
            PsiClass clazz = (PsiClass) psiElement;
            return clazz;
        }
    }
    log.warn(" PsiFile "+file.getName() +", not found PsiClass!");
    return null;
}

Determining the Action Context

The AnActionEvent object passed to update() carries information about the current context for the action. Context information is available from the methods of AnActionEvent, providing information such as the Presentation and whether the action is triggered by a Toolbar.

Additional context information is available using the method AnActionEvent.getData(). Keys defined in CommonDataKeys arepassed to the getData() method to retrieve objects such as Project, Editor, PsiFile, and other information.

Accessing this information is relatively light-weight and is suited for AnAction.update().

附加上下文的信息可以通过 AnActionEvent.getData() 获取到

//例如 获取 PsiFile 
 PsiFile data = e.getData(CommonDataKeys.PSI_FILE);
 
 

插件入口: 扩展idea 自带的 Generate…

扩展 idea 自带的 Code Generate… 选项 Action;

看代码, 整体是分开 Action 和 Handler 两个对象来处理

Action

Action 的核心代码类 com.intellij.codeInsight.generation.actions.GenerateGetterSetterBaseAction

对话框显示, 在 GenerateMembersHandlerBase::invoke 函数里面, 调用 chooseOriginalMembers 显示选择对话框并返回选择的成员字段

final ClassMember[] members = chooseOriginalMembers(aClass, project, editor);

Handler

Handler 的核心代码类 com.intellij.codeInsight.generation.GenerateGetterSetterHandlerBase

对话框显示的字段会根据Handler的 GenerateMembersHandlerBase::generateMemberPrototypes 方法返回的信息过滤显示显示;

实例代码

so 根据其套路: 创建 Action 继承com.intellij.codeInsight.generation.actions.GenerateGetterSetterBaseAction

public class MainAction extends GenerateGetterSetterBaseAction {
    private static final Logger log = Logger.getInstance(AnAction.class);
    public MainAction() {
        super(new MainActionHandle("标题!!!"));
    }
 
    ...
}
 

创建一个 Handler 继承 com.intellij.codeInsight.generation.GenerateGetterSetterHandlerBase

复制 GenerateGetterHandler 的 generateMemberPrototypes 函数代码

public class MainActionHandle extends GenerateGetterSetterHandlerBase {
 
    public MainActionHandle(@NlsContexts.DialogTitle String chooserTitle) {
        super(chooserTitle);
    }
    @Override
    protected @NlsContexts.HintText String getNothingFoundMessage() {
        return " NothingFound ?";
    }
    @Override
    protected GenerationInfo[] generateMemberPrototypes(PsiClass aClass, ClassMember original) throws IncorrectOperationException {
        if (original instanceof PropertyClassMember) {
            final PropertyClassMember propertyClassMember = (PropertyClassMember)original;
            final GenerationInfo[] getters = propertyClassMember.generateGetters(aClass);
            if (getters != null) {
                return getters;
            }
        } else if (original instanceof EncapsulatableClassMember) {
            final EncapsulatableClassMember encapsulatableClassMember = (EncapsulatableClassMember)original;
            final GenerationInfo getter = encapsulatableClassMember.generateGetter();
            if (getter != null) {
                return new GenerationInfo[]{getter};
            }
        }
        return GenerationInfo.EMPTY_ARRAY;
    }
    @Override
    protected String getHelpId() {
        return null;//去掉 help 图标
    }
    ....

插件开发 实例

实体添加新字段后, 查找DTO 添加字段, 查找ListDTO插入字段和, 查找Mapper toDto方法插入Get, Set代码

  1. 选择对话框
public void showDialog(@NotNull final Project project, @NotNull final Editor editor, @NotNull PsiFile file){
        if (!EditorModificationUtil.checkModificationAllowed(editor)) return;
        if (!FileDocumentManager.getInstance().requestWriting(editor.getDocument(), project)) {
            return;
        }
        final PsiClass aClass = OverrideImplementUtil.getContextClass(project, editor, file, false);
        if (aClass == null || aClass.isInterface()) return;
        log.assertTrue(aClass.isValid());
        log.assertTrue(aClass.getContainingFile() != null);
        try {
            //弹窗&返回选择的字段
            final ClassMember[] members = chooseOriginalMembers(aClass, project, editor);
            if (members == null) return;
            List<PsiFieldMember> chooses = Arrays.asList(members).stream().map(e -> (PsiFieldMember) e)
                    .collect(Collectors.toList());
            process(project, aClass, chooses);
 
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    
    protected void process(final Project project,
                        final PsiClass entity, final List<PsiFieldMember> members){
    WriteCommandAction.runWriteCommandAction(project, () -> {
        LocalDateTime localTime = LocalDateTime.now();
        processDTO(project,  entity, members, localTime, "");//处理 DTO
        processListDTO(project,  entity, members, localTime, "");//处理 ListDTO
        processMapper(project,  entity, members, localTime, "");//处理 Mapper
    });
}
  1. 处理 DTO
    String className = entityPsi.getName();
    PsiClass entityDTOPsi = PsiElementUtils.getPsiClassByName(className+"DTO", project);
    if(entityDTOPsi==null)return;
    PsiElement clsElement = entityDTOPsi.getSourceElement();
    final PsiElementFactory psiElementFactory = PsiElementFactory.getInstance(project);//psi 元素创建工厂
    for (Iterator<PsiFieldMember> iterator = members.iterator(); iterator.hasNext(); ) {
        PsiFieldMember next =  iterator.next();
        PsiField element = next.getElement();
        PsiField enityFieldCopy = psiElementFactory.createField(element.getName(), element.getType());
        //字段注释
        PsiComment commentForFidld = psiElementFactory.createCommentFromText("//追加于: "+lTime,entityDTOPsi);
        enityFieldCopy.add( commentForFidld);
        PsiElement anchor= PsiElementUtils.getLastPsiField(clsElement);
        //添加字段
        clsElement.addAfter(enityFieldCopy, anchor);
    }
  1. 处理 ListDTO
String className = entityPsi.getName();
PsiClass entityListDTOPsi = PsiElementUtils.getPsiClassByName(className+"ListDTO", project);
PsiField selectField = entityListDTOPsi.findFieldByName("SELECT", false);//String[] SELECT 字段
if(entityListDTOPsi==null)return;
if(selectField==null)return;
 
PsiElement clsElement = entityListDTOPsi.getSourceElement();
final PsiElementFactory psiElementFactory = PsiElementFactory.getInstance(project);//psi 元素创建工厂
//PsiType stringType = psiElementFactory.createTypeFromText("java.lang.String", null);
for (Iterator<PsiFieldMember> iterator = members.iterator(); iterator.hasNext(); ) {
    PsiFieldMember next =  iterator.next();
    PsiField element = next.getElement();
    String fieldName = element.getName();
    PsiField enityFieldCopy = psiElementFactory.createField(fieldName, element.getType());
    //字段注释
    PsiComment commentFromText = psiElementFactory.createCommentFromText("//追加于: "+lTime,
            enityFieldCopy);
    enityFieldCopy.add(commentFromText);
    //添加字段
    PsiElement anchor= PsiElementUtils.getLastPsiField(clsElement);
    entityListDTOPsi.addAfter(enityFieldCopy, anchor);
    //给select 添加
    PsiElement[] children = selectField.getChildren();
    PsiElement value= children[children.length-2];
    value.add(
            psiElementFactory.createExpressionFromText("\""+fieldName+"\"",selectField)//字符串字面量
    );
}
  1. 处理 Mapper
String className = entityPsi.getName();
PsiClass entityMappPsi = PsiElementUtils.getPsiClassByName(className+"Mapper", project);
JvmMethod[] toDtoPropsFound = entityMappPsi.findMethodsByName("toDtoProps");
JvmMethod[] toEntityPropsFound = entityMappPsi.findMethodsByName("toEntityProps");
JvmMethod toDtoProps = toDtoPropsFound[0];
JvmMethod toEntityProps = toEntityPropsFound[0];
final PsiElementFactory psiElementFactory = PsiElementFactory.getInstance(project);//psi 元素创建工厂
PsiElement toDtosourceElement = toDtoProps.getSourceElement();
PsiElement toEntitysourceElement = toEntityProps.getSourceElement();
//分号
PsiStatement statementSemicolon = psiElementFactory.createStatementFromText(";", null);
String lTime = localTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
boolean isAddComment = false;
//注释
PsiComment comment = psiElementFactory.createCommentFromText("//追加于: "+lTime,null);
for (Iterator<PsiFieldMember> iterator = members.iterator(); iterator.hasNext(); ) {
    PsiFieldMember next = iterator.next();
    PsiField element = next.getElement();
    String fieldName = element.getName();
    String capFieldName = PsiElementUtils.captureName(fieldName);
    // to dto
    PsiElement sourceBlock= toDtosourceElement.getLastChild();//中括号
    PsiElement anchor =  PsiElementUtils.getReturnPsiElement(toDtoProps);// RETURN
    String expToDtoString = String.format("giveDto.set%s( giveEntity.get%s() )", capFieldName, capFieldName);
    PsiExpression expToDto = psiElementFactory.createExpressionFromText(expToDtoString, toDtosourceElement);
    expToDto.add(statementSemicolon);// ;
    if (!isAddComment) {
        sourceBlock.addBefore( comment, anchor);
    }
    sourceBlock.addBefore( expToDto, anchor);
 
    // to ent
    ...
}

代码归档

iFrameTools

踩坑指南

Run Plugin 错误 accessible: module java.desktop does not “opens javax.swing.text.html”

又是JDK9模块化后的反射问题, 添加--illegal-access, --add-opens 之类的参数也没用.. 正解是使用在 project structure 添加并使用IDEA自带的那个JDK: ./IntelliJ IDEA Community Edition 2021.2.3/jbr

idea 插件的各种类定义错误 ClassDefFoundError

这个一般是由于idea版本不兼容出现的, 在高版本的idea中需要手动加载依赖; 一般解决的方法是直接在 plugin.xml 中添加 <depends>

<!-- 依赖模块 -->
<depends>com.intellij.modules.platform</depends>
<!-- 同时需要引入,否则2020版本找不到 com.intellij.psi -->
<depends>com.intellij.modules.lang</depends>
<depends>com.intellij.modules.java</depends>

idea插件开发,报错 NoClassDefFoundError

版本更新 破坏API 更改列表

https://plugins.jetbrains.com/docs/intellij/api-changes-list.html

插件开发的模板项目

官方还提供 插件的项目模版

IntelliJ Platform Plugin Template SDK 的代示例 IntelliJ Platform Plugin Template is a repository that provides a pure boilerplate template to make it easier to create a new plugin project using the recommended Gradle setup.