参考文章(不完全)
- Intellij 平台插件开发 API
- idea java文件显示_编写一个IDEA插件之:使用PSI分析Java代码
- idea插件开发(5)-Swing图形化设计
- idea插件开发(8)-Notification
- Qt Designer控件尺寸策略
- IntelliJ Platform UI Guidelines
- Settings Guide
运行效果
Gitee地址
https://gitee.com/shusy/Planetary-Engine-Idea
插件使用zip本地安装的方式(暂未发布到Jetbrains应用市场中),可在release中下载并安装
插件需求
开发新项目时,常常会使用Mybatis代码生成器生成对应的Controller、Service、Impl、Mapper,这很便捷也很好用,有的公司也会对代码生成器进一步封装,使生成器用起来更加的便捷。
代码生成器的痛点
如此厉害的代码生成器,依然有以下痛点
- 代码生成器使用的mybatis版本会和项目本身的mybatis版本冲突。
- 项目开发过程中对单个Class生成代码,需要修改main方法的配置,这时还得考虑整个项目是否能编译通过(笑。
- 当公司需要使用自定义的代码模板时,代码生成器的模板难以维护(使用代码生成器是为了降低工作量,难道要为了降低工作量而深入学习模板的维护吗?公司人来人往,每个同事都要学习代码模板的维护方式吗?)。
简化下来就是:
- 代码生成器对项目有入侵性。
- 代码生成器不能即时执行。
- 代码模板难以修改。
So~开发一个Idea插件,通过简单的配置以及点击,来生成模板代码的想法就出现了,这样做有以下好处:
- 项目无入侵:不会对项目的Pom.xml有任何污染。
- 即时操作:无需离开当前编辑页,通过邮件或多选文件的操作,即可对任意Java文件生成模板代码。
- 代码模板的维护难度可控:只做最小最核心功能,来减少编写模板的难度。
当然了,也有一些缺点:
- Idea的插件开发,对于国内来说,是一个朦胧的技术点,学习成本高、没有完整的教学文档(有英文版,可我英文不好😄)
- 开发周期可能很长,需要调试与适配的情况不少。
当然,对于我来说没有缺点,这么好的平台、这么酷炫的插件技术,为什么不玩玩呢?开发周期长?我又不是给公司做的,在乎这个干嘛😄
插件预期功能
- 可以指定生成文件时的包名
- 通过配置,指定含有某个注解时才可以生成模板代码,默认为识别Mybatis-plus的@TableName注解
- 可以为Controller、Service、Impl、Mapper都配置一个代码模板
- 可以选择文件或者文件夹,灵活的指定需要生成模板代码的文件
ok,需求大概了解了,开干!
初始化项目
想要开发Idea插件,需要先创建一个基础的项目结构,详细的描述在网上有不少,读者请参考引用文档。
大致有以下注意点:
- 开发语言:kotlin,Idea插件基于kotlin开发,但由于kotlin和Java都使用的JVM,所以写Java的开发者大可放心(能用Idea写Java项目,结果不能用Java写Idea的插件?笑话~)
- 依赖管理工具:gradle,对于只使用了pom的我来说,并不习惯,但是还好有GPT,问题不大。
- 重要文件:plugin.xml,插件、配置、应用等等的配置项,都在这个文件中,类似于spring.xml一般重要。文件路径
resources/META-INF/plugin.xml
- 记得安装
Plugin DevKit
插件,新版本Idea已默认安装 - Idea会下载对应版本的Gradle和Idea,必要时候,请搭上梯子并开启
全局代理
,梯子默认情况下是系统代理,而Idea默认不走系统代理(可以配置,但不在本文的讨论范围内)
Service注入
Idea也有类似IOC的概念,可以将一个自定义Service注入到IOC中,在需要的地方,通过静态方法即可取出使用。
注解方式注入
package com.asteroid.planetary_engine.idea.state;
// 项目级别的注入,每个项目拿到的Service都是新的
@Service(Service.Level.PROJECT)
public final class EntityGenerateStateManage{
}
// Idea应用级别的注入,无论哪个项目拿到的Service都是同一个
@Service(Service.Level.APP)
public final class EntityGenerateStateManage{
}
xml方式注入
<idea-plugin>
<extensions defaultExtensionNs="com.intellij">
<!-- 注册一个应用级别的 service (全局实例化一个)-->
<applicationService serviceImplementation="com.asteroid.planetary_engine.idea.state.EntityGenerateStateManage"/>
<!-- 注册一个项目级别的 service(每个窗口实例化一个) -->
<projectService serviceImplementation="com.asteroid.planetary_engine.idea.state.EntityGenerateStateManage"/>
</extensions>
</idea-plugin>
获取方式
public static EntityGenerateStateManage getInstance() {
// 应用级别
return ApplicationManager.getApplication().getService(MyApplicationService.class);
// 项目级别
Project project = ProjectManager.getInstance().getDefaultProject();
project.getService(MyProjectService.class);
return project;
}
实现步骤
- 在Setting中新增配置页,用来配置基础参数和代码模板
- 增加右键快捷项,触发生成模板代码逻辑
- 读取持久化的配置项,生成模板文件
绘制配置项UI界面
因为没接触过JSwing,导致这一步踩了不少坑,都是辛酸泪~
解释一下会用到的UI组件,在这一步,如果有点前端基础,会更容易理解一些。
- JPanel:类似div,画出一个框,最常用的组件。
- JTextField:类似span,仅为了显示一个固定文本。
- JBList:类似ul > li,是Jetbrains对JList的增强(具体增强了什么也没研究过,用新不用旧嘛)。
- JScrollPane:带滚动条的div,内部需要再放一个JPanel,用来限制长文本的宽高时非常好用。
- Base Layout Manage:类似前端的布局方式,通常直接用
GridLayoutManager
即可,详情可自行搜索。
新增界面文件
右键新增,输入EntityGenerateUI
,点击ok
此时会生成以下文件:
-
EntityGenerateUI:UI文件的目录
-
EntityGenerateUI.java:界面对应的Java对象,用来实现逻辑操作(后端)
-
EntityGenerateUI.form:界面的实际xml参数,用来渲染页面(前端)
在绘画EntityGenerateUI.form
时,Idea会将组件对应的Java字段自动写入EntityGenerateUI.java
细节点比较繁琐(就是前端活),说得再多,不如实际操作一下!
最终,我们获得了如下的配置界面
自动生成的代码如下
package com.asteroid.planetary_engine.idea.ui;
import com.asteroid.planetary_engine.idea.domain.EntityType;
import com.intellij.ui.components.JBList;
import lombok.Data;
import javax.swing.*;
@Data
public class EntityGenerateUI implements Configurable {
private JPanel rootPanel;
private JPanel mvcPanel;
private JPanel configPanel;
private JPanel commonPanel;
private JPanel itemPanel;
private JTextField packageName;
private JTextField annotationName;
private JBList<EntityType> entityTypeList;
private JPanel itemListPanel;
private JPanel templatePanel;
private JSplitPane jSplitPane;
private JScrollPane editorScrollPanel;
private JPanel editorPanel;
}
注册进Setting菜单中
在plugin.xml中,将EntityGenerateUI
注册
<idea-plugin>
<extensions defaultExtensionNs="com.intellij">
<!-- 属性 applicationConfigurable 和 projectConfigurable
parentId - 定义当前设置项在设置窗口中的位置,可选值为 https://plugins.jetbrains.com/docs/intellij/settings-guide.html#values-for-parent-id-attribute
Id - 唯一 ID,建议和类名一致
instance - Configurable 实现类的全名,和 provider 二选一
provider - ConfigurableProvider 实现类的全名,和 instance 二选一
nonDefaultProject - projectConfigurable 专属属性,是否允许用户配置默认配置 true - 该配置默认值写死的, false - 该配置默认值用户可以配置
nonDefaultProject = false 场景例子:编辑器字体,用户可以改变默认的字体,也可以专门为这个项目设置特定的配置
displayName - 展示名,不需要本地化场景
key 和 bundle - 需要本地化场景
groupWeight - 排序顺序,默认为 0 (权重最低)
dynamic - 设置项内容是否是动态的计算的,默认 false
childrenEPName - 如果配置项有多页,可以通过该字段组成树形结构??
-->
<!-- 注册一个Idea应用级别的配置页(每个窗口实例化一个) -->
<applicationConfigurable id="planetary-engine"
parentId="tools"
displayName="行星发动机"
instance="com.asteroid.planetary_engine.idea.ui.EntityGenerateUI" />
<!-- 注册一个项目级别的 配置页(每个窗口实例化一个) -->
<projectConfigurable id="planetary-engine"
parentId="tools"
displayName="行星发动机"
instance="com.asteroid.planetary_engine.idea.ui.EntityGenerateUI" />
</extensions>
</idea-plugin>
改造一下EntityGenerateUI
,实现Configurable
接口,标识为一个配置菜单项。
package com.asteroid.planetary_engine.idea.ui;
import com.asteroid.planetary_engine.idea.domain.EntityType;
import com.intellij.ui.components.JBList;
import com.intellij.openapi.options.Configurable;
import com.intellij.openapi.util.NlsContexts.ConfigurableName;
import lombok.Data;
import javax.swing.*;
@Data
public class EntityGenerateUI implements Configurable {
private JPanel rootPanel;
private JPanel mvcPanel;
private JPanel configPanel;
private JPanel commonPanel;
private JPanel itemPanel;
private JTextField packageName;
private JTextField annotationName;
private JBList<EntityType> entityTypeList;
private JPanel itemListPanel;
private JPanel templatePanel;
private JSplitPane jSplitPane;
private JScrollPane editorScrollPanel;
private JPanel editorPanel;
// 暂时没找到会在哪里显示
@Override
public @Nullable @NonNls String getHelpTopic() {
return "OH!!! Help me!!!";
}
// Setting页显示的配置名称
@Override
public @ConfigurableName String getDisplayName() {
return "PlanetaryEngine";
}
// 返回页面的根节点,这样才能显示出页面
@Override
public @Nullable JComponent createComponent() {
return rootPanel;
}
// 判断配置是否更改,如果更改了,Apply、Reset按钮会亮
@Override
public boolean isModified() {
return false;
}
// 用户点击Apply时的回调操作,通常会将新配置写入磁盘中
@Override
public void apply() throws ConfigurationException {
}
// 用户点击Reset按钮时的回调操作,通常会重新读取配置,覆盖临时配置
@Override
public void reset() {
}
}
运行预览
此时执行Run Plugin
运行项目,会弹出一个新的Idea窗口,使用该窗口打开任意一个新的项目
使用快捷键ctrl + shift + s
快速打开设置界面,找到Tools下的行星发动机
配置页面,即可看到我们画的页面了。
状态持久化
现在需要做两件事:
- 将默认的配置读出来,降低第一次使用的难度(resource下新建一个json文件)。
- 将修改后的配置,写入某个持久化文件中(这一步,Idea帮我们实现了)
Idea提供了如下方法,共同实现了持久化:
-
com.intellij.openapi.components.PersistentStateComponent
接口:重写数据的对比、加载逻辑。- 读取对应存储位置中,是否有历史配置,如果有,调用
loadState
方法,使开发者能拿到默认配置。 - 业务中调用
getState
方法,获取内存中的配置。
- 读取对应存储位置中,是否有历史配置,如果有,调用
-
com.intellij.openapi.components.State
注解:标识为需要持久化的对象,并自动将数据回写到磁盘中的文件。 -
com.intellij.openapi.components.Storage
注解:将数据的kv存储到指定的xml中。
存储位置
- Application级别:为该示例中的相对路径:
build/idea-sandbox/config/options/EntityGenerateSetting.xml
。- Project 级别状态,存储在
~/.idea
下
- 如果使用
StoragePathMacros.WORKSPACE_FILE
常量。则存储在
path/to/project/project.iws
- for file-based projectspath/to/project/.idea/workspace.xml
- for directory-based onesStoragePathMacros.WORKSPACE_FILE
是有特殊的含义,表示该状态,不会同步到代码仓库中,是该用户特化的配置而不是团队共享的,参见 .idea gitignore 说明StoragePathMacros.WORKSPACE_FILE
只能在 项目级别使用,如果在 Application 级别使用,将报错
实现基于Idea的持久化能力
我们只需要关注,提供给外部的getState
方法和加载历史配置的loadState
方法即可。
package com.asteroid.planetary_engine.idea.state;
import cn.hutool.json.JSONUtil;
import com.asteroid.planetary_engine.idea.domain.EntityType;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.Service;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import lombok.Data;
import org.apache.commons.io.IOUtils;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
@State(
name = "com.asteroid.planetary_engine.idea.setting.EntityGenerateSettingManage",
storages = @Storage("EntityGenerateSetting.xml")
)
// 将State存入IOC中,就能在UI界面中读取到值,更好操作
@Service(Service.Level.APP)
public final class EntityGenerateStateManage implements PersistentStateComponent<EntityGenerateStateManage.EntityGenerateState> {
// 第一步:读出默认配置
@Override
public EntityGenerateStateManage.@NotNull EntityGenerateState getState() {
if (DEFAULT_STATE == null) {
// EntityGenerate.json放在了resource下,记录基于插件的默认配置
InputStream inputStream = EntityGenerateStateManage.class.getClassLoader()
.getResourceAsStream("EntityGenerate.json");
try {
String string = IOUtils.toString(inputStream);
DEFAULT_STATE = JSONUtil.toBean(string, EntityGenerateState.class);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return DEFAULT_STATE;
}
// 第一步也可以是,从历史配置中读出配置
@Override
public void loadState(@NotNull EntityGenerateStateManage.EntityGenerateState state) {
DEFAULT_STATE = state;
}
public static EntityGenerateState DEFAULT_STATE = null;
// 对外提供静态方法,调用时会方便点
public static EntityGenerateStateManage getInstance() {
return ApplicationManager.getApplication().getService(EntityGenerateStateManage.class);
}
// 对UI暴露一个更新方法,使外部可以更新内存中的配置,Idea会自行将state写入Xml中
public static void update(EntityGenerateState state) {
DEFAULT_STATE.setPackageName(state.getPackageName());
DEFAULT_STATE.setAnnotationQualifiedName(state.getAnnotationQualifiedName());
DEFAULT_STATE.configMap.clear();
DEFAULT_STATE.configMap.putAll(state.configMap);
}
@Data
public static class EntityGenerateState {
/**
* 起始包名
*/
private String packageName;
/**
* 识别的注解的全限定名称
*/
private String annotationQualifiedName;
/**
* 配置项
*/
private HashMap<EntityType, String> configMap = new HashMap<>();
}
}
EntityGenerate.json
{
"packageName": "com.example.emptydemo",
"annotationQualifiedName": "com.example.emptydemo.at.Entity",
"configMap": {
"ServiceImpl": " import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;\n import ${sourcePackage}.${className};\n import ${sourcePackage}.${targetClassName};\n //import ${qualifiedName};\n \n @Service\npublic class ${targetClassName} extends ServiceImpl<${className}Mapper, ${className}>\n implements ${className}Service {\n \n }\n ",
"Service": "import com.baomidou.mybatisplus.extension.service.IService;\nimport ${sourcePackage}.${className};\n //import ${qualifiedName};\n\npublic interface ${targetClassName} extends IService<${className}> {\n\n }\n",
"Controller": "import ${sourcePackage}.${className};\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n@RestController\n@RequestMapping(\"/api/common/dict\")\npublic class ${targetClassName} {\n\n@Resource\nprivate ${className}Service ${className}Service;\n\n\n}\n",
"Mapper": "import com.baomidou.mybatisplus.core.mapper.BaseMapper;\nimport org.apache.ibatis.annotations.Mapper;\nimport ${sourcePackage}.${className};\n//import ${qualifiedName};\n\n@Mapper\npublic interface ${targetClassName} extends BaseMapper<${className}> {\n\n}\n"
}
}
该示例的xml序列化后的例子(不重要)
<application>
<component name="com.asteroid.planetary_engine.idea.setting.EntityGenerateSettingManage">
<option name="annotationQualifiedName" value="com.example.emptydemo.at.Entity" />
<option name="configMap">
<map>
<entry key="Controller" value="import ${sourcePackage}.${className}; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/common/dict") public class ${targetClassName} { @Resource private ${className}Service ${className}Service; } " />
<entry key="Service" value="import com.baomidou.mybatisplus.extension.service.IService; import ${sourcePackage}.${className}; //import ${qualifiedName}; public interface ${targetClassName} extends IService<${className}> { } " />
<entry key="ServiceImpl" value=" import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import ${sourcePackage}.${className}; import ${sourcePackage}.${targetClassName}; //import ${qualifiedName}; @Service public class ${targetClassName} extends ServiceImpl<${className}Mapper, ${className}> implements ${className}Service { } " />
<entry key="Mapper" value="import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; import ${sourcePackage}.${className}; //import ${qualifiedName}; @Mapper public interface ${targetClassName} extends BaseMapper<${className}> { } " />
</map>
</option>
<option name="packageName" value="com.example.emptydemo" />
</component>
</application>
Setting界面将配置持久化
在这一步,我们需要实现以下功能:
- UI从State中复制一个临时的配置,提供给用户操作。
- 用户在没有点击确认(apply)或者OK之后,才讲临时的配置写入持久化的配置中。
- 使用一个Map存储文件类型与代码模板的K-V,用户点击文件类型时,右侧的编辑框切换显示为对应的代码模板。
- 右侧的编辑框更改时,将代码模板的新值存入K-V中。
增加UI中的逻辑
核心修改点:
-
从State中读取并复制插件配置
private EntityGenerateStateManage.EntityGenerateState state = new EntityGenerateStateManage.EntityGenerateState(); { // 初始化UI中的临时界面 EntityGenerateStateManage stateManage = EntityGenerateStateManage.getInstance(); BeanUtil.copyProperties(stateManage.getState(), state); packageName.setText(state.getPackageName()); annotationName.setText(state.getAnnotationQualifiedName()); }
-
渲染代码模板,并添加对应的代码高亮:创建一个编辑器、为编辑器绑定高亮规则
// 为当前对象绑定一个编辑器 private void initEditor() { // 使用Live Template功能的创建Editor工具,初始不传入文本 myTemplateEditor = TemplateEditorUtil.createEditor(false, ""); // EditorEx 才有代码高亮功能,还是使用Idea自带的Java高亮规则 ((EditorEx) myTemplateEditor).setHighlighter(this.getJavaHighLight()); // 色彩范围,依然使用Idea默认的色彩 ((EditorEx) myTemplateEditor).setColorsScheme(EditorColorsManager.getInstance().getGlobalScheme()); // 当编辑器中的文本更改时,更新文件类型与代码模板的K-V myTemplateEditor.getDocument().addDocumentListener(new com.intellij.openapi.editor.event.DocumentListener() { @Override public void documentChanged(@NotNull com.intellij.openapi.editor.event.DocumentEvent e) { // 获取当前选中的子项 EntityType entityType = entityTypeList.getSelectedValue(); if (entityType != null) { // 更新子项对应的 value state.getConfigMap().put(entityType, myTemplateEditor.getDocument().getText()); } } }); editorPanel.add(myTemplateEditor.getComponent()); } // 从Live Template功能中copy来的代码,稳妥 private LayeredLexerEditorHighlighter getJavaHighLight() { SyntaxHighlighter originalHighlighter = SyntaxHighlighterFactory.getSyntaxHighlighter(JavaFileType.INSTANCE, null, null); if (originalHighlighter == null) { originalHighlighter = new JavaFileHighlighter(); } final EditorColorsScheme scheme = EditorColorsManager.getInstance().getGlobalScheme(); LayeredLexerEditorHighlighter highlighter; // 识别Java语言的高亮 highlighter = new LayeredLexerEditorHighlighter(new JavaFileHighlighter(), scheme); highlighter.registerLayer(new IElementType("java.FILE", JavaLanguage.INSTANCE), new LayerDescriptor(originalHighlighter, "")); return highlighter; }
-
可以修改不同文件类型的代码模板
public EntityGenerateUI() { // 为List绑定初始的可选值 DefaultListModel<EntityType> model = new DefaultListModel<>(); model.addAll(List.of(EntityType.values())); entityTypeList.setModel(model); entityTypeList.addListSelectionListener(e -> { // 该检查确保事件不是在值正在调整时触发 if (!e.getValueIsAdjusting()) { EntityType entityType = entityTypeList.getSelectedValue(); changeItemState(entityType); } }); // 设置 entityTypeList.setSelectedValue(model.firstElement(), false); } // 将右侧编辑器的显示内容更新 private synchronized void changeItemState(EntityType entityType) { String context = state.getConfigMap().get(entityType); // Editor中绑定了一个Document,这才是实际的文本。 // 为了避免对Document进行写操作时出现并发问题,需要用一个异步线程来单独操作 ApplicationManager.getApplication().runWriteAction(()->{ myTemplateEditor.getDocument().setText(context); // 更新文本后,将滚动框滚动到顶部 editorScrollPanel.getViewport().setViewPosition(new Point(0, 0)); }); }
代码示例
修改后的EntityGenerateUI
为以下内容
package com.asteroid.planetary_engine.idea.ui;
import cn.hutool.core.bean.BeanUtil;
import com.asteroid.planetary_engine.idea.domain.EntityType;
import com.asteroid.planetary_engine.idea.state.EntityGenerateStateManage;
import com.asteroid.planetary_engine.idea.utils.highlight.MyTemplateHighlighter;
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
import com.intellij.codeInsight.template.impl.TemplateEditorUtil;
import com.intellij.ide.fileTemplates.impl.FileTemplateHighlighter;
import com.intellij.ide.highlighter.JavaFileHighlighter;
import com.intellij.ide.highlighter.JavaFileType;
import com.intellij.lang.java.JavaLanguage;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.colors.EditorColorsScheme;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.editor.ex.util.LayerDescriptor;
import com.intellij.openapi.editor.ex.util.LayeredLexerEditorHighlighter;
import com.intellij.openapi.fileTypes.PlainSyntaxHighlighter;
import com.intellij.openapi.fileTypes.SyntaxHighlighter;
import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory;
import com.intellij.openapi.options.Configurable;
import com.intellij.openapi.options.ConfigurationException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.util.NlsContexts.ConfigurableName;
import com.intellij.psi.JavaCodeFragment;
import com.intellij.psi.JavaCodeFragmentFactory;
import com.intellij.psi.JavaPsiFacade;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.tree.IElementType;
import com.intellij.ui.components.JBList;
import lombok.Data;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.awt.*;
import java.util.List;
@Data
public class EntityGenerateUI implements Configurable {
private JPanel rootPanel;
private JPanel mvcPanel;
private JPanel configPanel;
private JPanel commonPanel;
private JPanel itemPanel;
private JTextField packageName;
private JTextField annotationName;
private JBList<EntityType> entityTypeList;
private JPanel itemListPanel;
private JPanel templatePanel;
private JSplitPane jSplitPane;
private JScrollPane editorScrollPanel;
private JPanel editorPanel;
private EntityGenerateStateManage.EntityGenerateState state = new EntityGenerateStateManage.EntityGenerateState();
private Editor myTemplateEditor;
{
// 初始化UI中的临时界面
EntityGenerateStateManage stateManage = EntityGenerateStateManage.getInstance();
BeanUtil.copyProperties(stateManage.getState(), state);
packageName.setText(state.getPackageName());
annotationName.setText(state.getAnnotationQualifiedName());
}
@Override
public @Nullable @NonNls String getHelpTopic() {
return "OH!!! Help me!!!";
}
@Override
public @ConfigurableName String getDisplayName() {
return "PlanetaryEngine";
}
@Override
public @Nullable JComponent createComponent() {
return rootPanel;
}
// 重写了equals方法,所以直接用临时配置和已加载配置equals就可以知道有没有更新
@Override
public boolean isModified() {
return !state.equals(EntityGenerateStateManage.DEFAULT_STATE);
}
// 将临时配置写入磁盘中
@Override
public void apply() throws ConfigurationException {
EntityGenerateStateManage.update(state);
}
// 为当前对象
private void initEditor() {
// 使用Live Template功能的创建Editor工具,初始不传入文本
myTemplateEditor = TemplateEditorUtil.createEditor(false, "");
// EditorEx 才有代码高亮功能,还是使用Idea自带的Java高亮规则
((EditorEx) myTemplateEditor).setHighlighter(this.getJavaHighLight());
// 色彩范围,依然使用Idea默认的色彩
((EditorEx) myTemplateEditor).setColorsScheme(EditorColorsManager.getInstance().getGlobalScheme());
// 当编辑器中的文本更改时,更新文件类型与代码模板的K-V
myTemplateEditor.getDocument().addDocumentListener(new com.intellij.openapi.editor.event.DocumentListener() {
@Override
public void documentChanged(@NotNull com.intellij.openapi.editor.event.DocumentEvent e) {
// 获取当前选中的子项
EntityType entityType = entityTypeList.getSelectedValue();
if (entityType != null) {
// 更新子项对应的 value
state.getConfigMap().put(entityType, myTemplateEditor.getDocument().getText());
}
}
});
editorPanel.add(myTemplateEditor.getComponent());
}
// 开发的大头,在构造器中,将各类需要的东西都准备好
public EntityGenerateUI() {
// JSwing默认的JTextArea等等组件,都难以配置代码的高亮,
// 所以在这里,我们使用Idea自带的Editor,来轻松实现Java的代码模板高亮(不高亮也能用,但是不好看)
this.initEditor();
// 更新、删除、更改起始包名时,将新值更新到临时配置中
packageName.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
state.setPackageName(packageName.getText());
}
@Override
public void removeUpdate(DocumentEvent e) {
state.setPackageName(packageName.getText());
}
@Override
public void changedUpdate(DocumentEvent e) {
state.setPackageName(packageName.getText());
}
});
// 更新、删除、更改识注解名时,将新值更新到临时配置中
annotationName.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
state.setAnnotationQualifiedName(annotationName.getText());
}
@Override
public void removeUpdate(DocumentEvent e) {
state.setAnnotationQualifiedName(annotationName.getText());
}
@Override
public void changedUpdate(DocumentEvent e) {
state.setAnnotationQualifiedName(annotationName.getText());
}
});
// 为List绑定初始的可选值
DefaultListModel<EntityType> model = new DefaultListModel<>();
model.addAll(List.of(EntityType.values()));
entityTypeList.setModel(model);
entityTypeList.addListSelectionListener(e -> {
// 该检查确保事件不是在值正在调整时触发
if (!e.getValueIsAdjusting()) {
EntityType entityType = entityTypeList.getSelectedValue();
changeItemState(entityType);
}
});
// 设置
entityTypeList.setSelectedValue(model.firstElement(), false);
}
// 将右侧编辑器的显示内容更新
private synchronized void changeItemState(EntityType entityType) {
String context = state.getConfigMap().get(entityType);
// Editor中绑定了一个Document,这才是实际的文本。
// 为了避免对Document进行写操作时出现并发问题,需要用一个异步线程来单独操作
ApplicationManager.getApplication().runWriteAction(()->{
myTemplateEditor.getDocument().setText(context);
// 更新文本后,将滚动框滚动到顶部
editorScrollPanel.getViewport().setViewPosition(new Point(0, 0));
});
}
@Override
public void reset() {
EntityGenerateStateManage stateManage = EntityGenerateStateManage.getInstance();
BeanUtil.copyProperties(stateManage.getState(), state);
packageName.setText(state.getPackageName());
annotationName.setText(state.getAnnotationQualifiedName());
}
// 从Live Template功能中copy来的代码,稳妥
private LayeredLexerEditorHighlighter getJavaHighLight() {
SyntaxHighlighter originalHighlighter = SyntaxHighlighterFactory.getSyntaxHighlighter(JavaFileType.INSTANCE, null, null);
if (originalHighlighter == null) {
originalHighlighter = new JavaFileHighlighter();
}
final EditorColorsScheme scheme = EditorColorsManager.getInstance().getGlobalScheme();
LayeredLexerEditorHighlighter highlighter;
// 识别Java语言的高亮
highlighter = new LayeredLexerEditorHighlighter(new JavaFileHighlighter(), scheme);
highlighter.registerLayer(new IElementType("java.FILE", JavaLanguage.INSTANCE), new LayerDescriptor(originalHighlighter, ""));
return highlighter;
}
}
自此,界面上的配置已经实现了持久化与更新
运行截图
生成Java文件
终于到了最后一步,根据代码模板生成对应的文件,先来分析一下需要做哪些操作:
-
右键出现自定义的操作栏
- 新建类
EntityGenerateAction
继承AnAction
并实现actionPerformed
方法。 - 在
plugin.xml
中注册EntityGenerateAction
为插件。
- 新建类
-
获取当前编辑器内的文件或者project栏选中的多个文件
@Override public void actionPerformed(@NotNull AnActionEvent anActionEvent) { // 方法一:可以直接拿到选中的虚拟文件(包含编辑器正在打开的文件、左侧Project选中的文件) VirtualFile[] virtualFiles = anActionEvent.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY); // 方法二:只能拿到当前编辑器打开的文件 // 先获取当前的编辑器对象,如果没有编辑器对象,代表没有打开文件 Editor editor = anActionEvent.getData(CommonDataKeys.EDITOR); if (null == editor) { return null; } // 获取当前编辑的文件,通过Document寻找结构化文件的方式 PsiFile psiFile = PsiDocumentManager.getInstance(anActionEvent.getProject()).getPsiFile(editor.getDocument()); }
-
解析Java文件,判断是否标注了指定的注解,没标注的不能生成模板代码
-
使用Idea的PSI机制,详情请查看参考文档
-
// 用法有点类似于Class的读取方式,API很简洁,很好用 private void generateByPsiJavaFile(Project project, PsiJavaFile psiJavaFile) { PsiClass[] classes = psiJavaFile.getClasses(); PsiClass psiClass = classes[0]; PsiAnnotation annotation = psiClass.getAnnotation(annotationQualifiedName); }
-
-
读取插件配置
// 直接调用之前准备的静态方式即可 private final EntityGenerateStateManage.EntityGenerateState generateState = EntityGenerateStateManage.getInstance().getState();
-
根据配置的代码模板,拼接真实的文件(太过简单,不解析了)
-
生成Java文件,注:此操作需要在异步的情况下执行
// project:当前打开的项目 // targetFileName:目标文件名 // JavaFileType.INSTANCE:文件类型 // content:文件的内容 public static void generateClassFile(Project project, String content, String targetFileName, String realPackageName, PsiFile psiFile) { // 使用Idea自带的写入命令处理器,还是很好用 WriteCommandAction.runWriteCommandAction(project, () -> { PsiFile targetFile = PsiFileFactory.getInstance(project).createFileFromText(targetFileName, JavaFileType.INSTANCE, content); PsiDirectory nestedDirectories = ...; // 将文件添加到某个目录下 nestedDirectories.add(targetFile); }); }
综上,创建EntityGenerateAction.java
和ClassCreateUtil.java
实例代码
package com.asteroid.planetary_engine.idea.action;
import cn.hutool.json.JSONUtil;
import com.asteroid.planetary_engine.idea.domain.EntityType;
import com.asteroid.planetary_engine.idea.state.EntityGenerateStateManage;
import com.asteroid.planetary_engine.idea.utils.ClassCreateUtil;
import com.asteroid.planetary_engine.idea.utils.NotifyUtil;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiJavaFile;
import com.intellij.psi.PsiManager;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class EntityGenerateAction extends AnAction {
private final EntityGenerateStateManage.EntityGenerateState generateState = EntityGenerateStateManage.getInstance().getState();
@Override
public void actionPerformed(@NotNull AnActionEvent anActionEvent) {
// 获取当前项目
Project project = anActionEvent.getProject();
if (null == project) {
return;
}
VirtualFile[] virtualFiles = anActionEvent.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY);
if (virtualFiles == null || virtualFiles.length == 0) {
NotifyUtil.notify("未找到.java文件", MessageType.INFO);
return;
}
Set<PsiJavaFile> psiJavaFiles = findPsiJavaFile(project, virtualFiles);
if (psiJavaFiles.isEmpty()) {
NotifyUtil.notify("未找到.java文件", MessageType.INFO);
return;
}
if (StringUtils.isEmpty(generateState.getPackageName())) {
NotifyUtil.notify("起始包名未配置", MessageType.WARNING);
return;
}
if (StringUtils.isEmpty(generateState.getAnnotationQualifiedName())) {
NotifyUtil.notify("注解的全限定名称未配置", MessageType.WARNING);
return;
}
for (PsiJavaFile psiJavaFile : psiJavaFiles) {
generateByPsiJavaFile(project, psiJavaFile);
}
}
private void generateByPsiJavaFile(Project project, PsiJavaFile psiJavaFile) {
PsiClass[] classes = psiJavaFile.getClasses();
if (classes.length == 0) {
NotifyUtil.notify("当前Java文件没有Class信息", MessageType.WARNING);
return;
}
PsiClass psiClass = classes[0];
if (psiClass == null) {
NotifyUtil.notify("当前文件不是.java文件", MessageType.WARNING);
return;
}
String packageName = generateState.getPackageName();
String annotationQualifiedName = generateState.getAnnotationQualifiedName();
HashMap<EntityType, String> configMap = generateState.getConfigMap();
PsiAnnotation annotation = psiClass.getAnnotation(annotationQualifiedName);
if (annotation == null) {
// Messages.showMessageDialog(psiJavaFile.getName() + "未标注@Entity注解", "未找到指定注解", Messages.getWarningIcon());
NotifyUtil.notify(psiJavaFile.getName() + "未标注@Entity注解", MessageType.WARNING);
// Messages.showMessageDialog("未标注@com.example.emptydemo.at.Entity注解", "未找到指定注解", UIUtil.getWarningIcon());
return;
}
if (StringUtils.isEmpty(packageName)) {
return;
}
for (Map.Entry<EntityType, String> entry : configMap.entrySet()) {
EntityType entityType = entry.getKey();
String content = entry.getValue();
HashMap<String, String> replaceKV = new HashMap<>();
replaceKV.put("className", psiClass.getName());
replaceKV.put("lowClassName", getLowClassName(psiClass.getName()));
replaceKV.put("qualifiedName", psiClass.getQualifiedName());
replaceKV.put("sourcePackage", psiJavaFile.getPackageName());
replaceKV.put("startPackage", packageName);
String targetClassName = psiClass.getName() + entityType.name();
replaceKV.put("targetClassName", targetClassName);
System.out.println(JSONUtil.toJsonPrettyStr(replaceKV));
// 将entityType.getCode()的第一个字符转为小写
String code = entityType.getCode();
String name = getLowClassName(code);
String realPackageName = packageName + "." + name;
replaceKV.put("targetPackageName", realPackageName);
content = "package " + realPackageName + ";\n" + content;
for (Map.Entry<String, String> subEntry : replaceKV.entrySet()) {
content = content.replace("${" + subEntry.getKey() + "}", subEntry.getValue() == null ? "" : subEntry.getValue());
}
System.out.println("content = " + content);
ClassCreateUtil.generateClassFile(project, content, targetClassName + ".java", realPackageName, psiJavaFile);
}
}
private Set<PsiJavaFile> findPsiJavaFile(Project project, VirtualFile[] virtualFiles) {
Set<PsiJavaFile> list = new HashSet<>();
for (VirtualFile virtualFile : virtualFiles) {
if (virtualFile.isDirectory()) {
VirtualFile[] children = virtualFile.getChildren();
if (children != null && children.length > 0) {
list.addAll(findPsiJavaFile(project, children));
}
} else {
if (virtualFile.getName().endsWith(".java")) {
System.out.println(virtualFile.getName() + "isJava");
list.add((PsiJavaFile) PsiManager.getInstance(project).findFile(virtualFile));
}
}
}
return list;
}
private String getLowClassName(@Nullable String name) {
if (StringUtils.isEmpty(name)) {
return "";
}
return name.substring(0, 1).toLowerCase() + name.substring(1);
}
private String getUpClassName(@Nullable String name) {
if (StringUtils.isEmpty(name)) {
return "";
}
return name.substring(0, 1).toUpperCase() + name.substring(1);
}
}
生成文件的工具类如下
package com.asteroid.planetary_engine.idea.utils;
import com.intellij.ide.highlighter.JavaFileType;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.ui.Messages;
import com.intellij.psi.*;
import com.intellij.util.ui.UIUtil;
import java.util.concurrent.atomic.AtomicReference;
public class ClassCreateUtil {
private static PsiDirectory createNestedDirectories(PsiDirectory baseDirectory, String[] packageParts) {
AtomicReference<PsiDirectory> currentDirectory = new AtomicReference<>(baseDirectory);
ApplicationManager.getApplication().runWriteAction(() -> {
currentDirectory.set(baseDirectory);
for (String packageNamePart : packageParts) {
PsiDirectory subdirectory = currentDirectory.get().findSubdirectory(packageNamePart);
if (subdirectory == null) {
subdirectory = currentDirectory.get().createSubdirectory(packageNamePart);
}
currentDirectory.set(subdirectory);
}
});
return currentDirectory.get(); // 返回最终创建或找到的目录
}
private static PsiDirectory findJavaDir(PsiDirectory directory) {
if (directory.getName().equals("java")) {
return directory;
}
PsiDirectory parentDirectory = directory.getParentDirectory();
if (parentDirectory == null) {
return null;
}
return findJavaDir(parentDirectory);
}
public static void generateClassFile(Project project, String content, String targetFileName, String realPackageName, PsiFile psiFile) {
WriteCommandAction.runWriteCommandAction(project, () -> {
PsiDirectory javaDir = findJavaDir(psiFile.getContainingDirectory());
if (javaDir == null) {
NotifyUtil.notify("未找到java目录", MessageType.WARNING);
Messages.showMessageDialog("未找到java根目录", "生成" + targetFileName + "失败", UIUtil.getWarningIcon());
return;
}
PsiDirectory nestedDirectories = createNestedDirectories(javaDir, realPackageName.split("\\."));
if (nestedDirectories.findFile(targetFileName) != null) {
NotifyUtil.notify("文件:" + targetFileName + "已存在", MessageType.WARNING);
return;
}
PsiFile targetFile = PsiFileFactory.getInstance(project).createFileFromText(targetFileName, JavaFileType.INSTANCE, content);
nestedDirectories.add(targetFile);
});
}
评论区