Idea插件开发

前言

插件上传地址

https://plugins.jetbrains.com/

JDK要求必须11以上,我这里使用的是17。

下载JDK17

https://www.oracle.com/cn/java/technologies/downloads/#jdk17-windows

安装后确认下版本

1
java --version

环境变量中的JAVA_HOME也要设置为JDK17的路径。

image-20240803130640358

创建项目

创建项目

image-20240803121745932

项目打开后点击plugin.xml配置插件的基本信息

image-20240803122631873

如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<idea-plugin>
<id>cn.psvmc.VueComp</id>
<name>VueComp</name>
<vendor email="183518918@qq.com" url="https://www.psvmc.cn">码客说</vendor>
<description>
<![CDATA[
Convenient for generating Vue page templates and styles.<br><br>
After adding a folder, right-click on the menu to generate a file with the same name as the folder.<br>
Supports TS + Less + Composition API.<br>
Templates and styles are separated.<br>
TS is not separated because separation is not very useful with setup.<br>
Templates are not separated because after separation, clicking on attributes in IDEA does not navigate properly.
]]>
</description>
<depends>com.intellij.modules.platform</depends>
<extensions defaultExtensionNs="com.intellij">
</extensions>
<actions>
<action id="cn.psvmc.vuecomp.CreateVueCompAction"
class="cn.psvmc.vuecomp.CreateVueCompAction"
icon="/icons/vue.svg"
text="创建Vue组件/页面">
<add-to-group group-id="NewGroup" anchor="first"/>
<!-- 可通过 ctrl + H 快捷键触发 -->
<keyboard-shortcut keymap="$default" first-keystroke="ctrl H"/>
</action>
</actions>
</idea-plugin>

注意

插件如果要上传到IDEA上描述不能用中文,如果自己导入是可以中文的。

报错

Could not resolve org.jetbrains.intellij.plugins:gradle-intellij-plugin:1.8.0

环境变量中的JAVA_HOME也要设置为JDK17的路径。

我们安装这个插件方便插件的开发

image-20240805103451018

这里我就手动创建了。

示例

简单提示

这里我们只是简单的在右下角弹出通知显示项目根目录

CreateVueCompAction.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationType;
import com.intellij.notification.Notifications;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.vfs.VirtualFile;

public class CreateVueCompAction extends AnAction {

@Override
public void actionPerformed(AnActionEvent e) {
Notifications.Bus.notify(new Notification("Print", "", e.getProject().getBasePath(), NotificationType.INFORMATION), e.getProject());
}
}

当然也可以弹窗显示

1
Messages.showMessageDialog(e.getProject().getBasePath(), "Project BasePath", Messages.getInformationIcon());

在plugin.xml的根节点下添加

1
2
3
4
5
6
7
8
9
<actions>
<action id="cn.psvmc.vuecomp.CreateVueCompAction"
class="cn.psvmc.vuecomp.CreateVueCompAction"
text="创建Vue组件/页面">
<add-to-group group-id="NewGroup" anchor="first"/>
<!-- 可通过 ctrl + H 快捷键触发 -->
<keyboard-shortcut keymap="$default" first-keystroke="ctrl H"/>
</action>
</actions>

也可以在代码文件夹上点击鼠标右键,选择 New => Plugin DevKit => Action

如果没有的话,那么可能需要在先在IDEA中装个 Plugin DevKit插件。

获取选中的文件夹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.vfs.VirtualFile;

import java.util.ArrayList;
import java.util.List;

public class CreateVueCompAction extends AnAction {
@Override
public void actionPerformed(AnActionEvent e) {
Project project = e.getProject();
if (project == null) {
Messages.showMessageDialog("No project found", "Error", Messages.getErrorIcon());
return;
}

VirtualFile[] files = e.getData(com.intellij.openapi.actionSystem.CommonDataKeys.VIRTUAL_FILE_ARRAY);
if (files == null || files.length == 0) {
Messages.showMessageDialog("No file selected", "Error", Messages.getErrorIcon());
return;
}

List<String> folderPathList = new ArrayList<>();
for (VirtualFile file : files) {
if (file.isDirectory()) {
folderPathList.add(file.getPath());
}
}
String folderPaths= String.join("\n", folderPathList);
Messages.showMessageDialog(folderPaths, "选择的文件夹", Messages.getInformationIcon());
}
}

文件创建及写入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;

import java.io.IOException;

public class CreateVueCompAction extends AnAction {
@Override
public void actionPerformed(AnActionEvent e) {
ApplicationManager.getApplication().runWriteAction(() -> {
Project project = e.getProject();
if (project == null) {
return;
}
VirtualFile[] files = e.getData(com.intellij.openapi.actionSystem.CommonDataKeys.VIRTUAL_FILE_ARRAY);
if (files == null || files.length == 0) {
Messages.showMessageDialog("未选择文件夹!", "错误", Messages.getErrorIcon());
return;
}
for (VirtualFile file : files) {
if (file.isDirectory()) {
try {
String fileName = "main.txt";
VirtualFile newFile = file.findChild(fileName);
if (newFile == null) {
newFile = file.createChildData(this, fileName);
String content = "Hello, World!";
VirtualFile finalNewFile = newFile;
WriteAction.run(() -> {
finalNewFile.setBinaryContent(content.getBytes());
});
}
} catch (IOException ignored) {
}
}
}
VirtualFileManager.getInstance().refreshWithoutFileWatcher(true);
});
}
}

生成Vue文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import cn.psvmc.vuecomp.utils.ZFreeMarkerUtils;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationType;
import com.intellij.notification.Notifications;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class CreateVueCompAction extends AnAction {
@Override
public void actionPerformed(AnActionEvent e) {
ApplicationManager.getApplication().runWriteAction(() -> {
Project project = e.getProject();
if (project == null) {
return;
}
VirtualFile[] files = e.getData(com.intellij.openapi.actionSystem.CommonDataKeys.VIRTUAL_FILE_ARRAY);
if (files == null || files.length == 0) {
Messages.showMessageDialog("未选择文件夹!", "错误", Messages.getErrorIcon());
return;
}
for (VirtualFile file : files) {
if (file.isDirectory()) {
String fileName = file.getName();
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("fileName", fileName);
List<Map<String, String>> templateList = new ArrayList<>();
Map<String, String> templateMap = new HashMap<>();
templateMap.put("templateName", "ts_vue.ftl");
templateMap.put("fileNameAll", fileName + ".vue");
templateList.add(templateMap);

Map<String, String> styleMap = new HashMap<>();
styleMap.put("templateName", "style.ftl");
styleMap.put("fileNameAll", fileName + ".less");
templateList.add(styleMap);

for (Map<String, String> item : templateList) {
try {
String fileNameAll = item.get("fileNameAll");
String templateName = item.get("templateName");
VirtualFile newFile = file.findChild(fileNameAll);
if (newFile == null) {
newFile = file.createChildData(this, fileNameAll);
String content = ZFreeMarkerUtils.getStrByFtl(templateName, dataModel);
VirtualFile finalNewFile = newFile;
WriteAction.run(() -> {
finalNewFile.setBinaryContent(content.getBytes());
});
} else {
Notification notice = new Notification("Print", "", "文件已存在", NotificationType.INFORMATION);
Notifications.Bus.notify(notice, e.getProject());
}
} catch (IOException ignored) {
}
}
}
}
VirtualFileManager.getInstance().refreshWithoutFileWatcher(true);
});
}
}

对应的模板文件

ts_vue.ftl

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div class="${fileName}">
{{ fileName }}
</div>
</template>

<script lang="ts" setup>
import {ref} from "vue";

const fileName = ref("${fileName}")
</script>

<style src="./${fileName}.less" lang="less" scoped></style>

style.ftl

1
2
.${fileName} {
}

使用的时候,先创建我们的组件文件夹,比如MainView,右键点击创建Vue组件/页面,就会在这个文件夹中创建跟文件夹同名的两个文件。

设置图标

在 IDEA 插件开发中,设置 action 的图标主要涉及到以下几个步骤:

准备图标文件

确保你有一个图标文件,通常是 PNG 格式,SVG也可以。尺寸为 16x16 像素,不要太大,会把菜单撑变形。

将图标文件放入资源目录

将图标文件放在插件项目的 resources 目录下,通常路径为 src/main/resources/icons/

修改 plugin.xml 文件

打开 plugin.xml 文件,找到或添加 actions 节点,并在其中定义你的 action。要指定图标,使用 icon 属性。下面是一个示例配置:

1
2
3
4
5
6
7
8
9
<actions>
<action
id="com.example.MyAction"
class="com.example.MyAction"
text="My Action"
description="Description of my action"
icon="/icons/my_icon.png">
</action>
</actions>

在这个例子中,icon 属性的值是图标文件的路径,相对于 resources 目录。

插件本身的图标可以替换resources/META-INF/pluginIcon.svg文件,大小为40x40

配置存储和读取

工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.Service;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

@State(name = "PluginSettings", storages = @Storage("PluginSettings.xml"))
@Service(Service.Level.APP)
public final class PluginSettings implements PersistentStateComponent<PluginSettings.State> {
private State state = new State();

public static class State {
public String vueVersion = "vue3";
public String cssVersion = "less";
}

@Nullable
@Override
public State getState() {
return state;
}

@Override
public void loadState(@NotNull State state) {
this.state = state;
}

// 提供 getter 和 setter 方法
public String getVueVersion() {
return state.vueVersion;
}

public void setVueVersion(String str) {
state.vueVersion = str;
}

public String getCssVersion() {
return state.cssVersion;
}

public void setCssVersion(String str) {
state.cssVersion = str;
}

// 工具类方法
public static PluginSettings getInstance() {
return ApplicationManager.getApplication().getService(PluginSettings.class);
}
}

plugin.xml中根节点内添加

1
2
3
<extensions defaultExtensionNs="com.intellij">
<applicationService serviceImplementation="cn.psvmc.vuecomp.settings.PluginSettings"/>
</extensions>

你可以这样使用 PluginSettings 工具类来读取或保存配置:

读取配置

1
2
PluginSettings settings = PluginSettings.getInstance();
String vueVersion = settings.getVueVersion();

修改配置

1
2
PluginSettings settings = PluginSettings.getInstance();
settings.setVueVersion("vue3");

说明

  1. @State 注解:指定了保存配置的文件名称(PluginSettings.xml),可以根据需要调整。
  2. PersistentStateComponent 接口:提供了存储和加载状态的方法 getStateloadState,这些方法会由 IntelliJ IDEA 自动调用。
  3. 工具类方法 getInstance:使用 ServiceManager.getService 获取 PluginSettings 实例,这是获取配置信息的一种标准方法。

环境配置

构建配置

gradle\wrapper\gradle-wrapper.properties

1
2
3
4
5
6
#Wed Mar 20 15:45:42 HKT 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-7.6-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

settings.gradle.kts

1
2
3
4
5
6
7
8
9
10
11
12
rootProject.name = "VueComp"

pluginManagement {
repositories {
maven("https://maven.aliyun.com/repository/central")
maven("https://maven.aliyun.com/repository/public")
maven ("https://maven.aliyun.com/repository/gradle-plugin")
maven ("https://maven.aliyun.com/repository/apache-snapshots")
mavenCentral()
gradlePluginPortal()
}
}

build.gradle.kts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
plugins {
id("java")
id("org.jetbrains.intellij") version "1.8.0"
}

group = "cn.psvmc"
version = "1.0.3"

repositories {
maven("https://maven.aliyun.com/repository/central")
maven("https://maven.aliyun.com/repository/public")
maven ("https://maven.aliyun.com/repository/gradle-plugin")
maven ("https://maven.aliyun.com/repository/apache-snapshots")
mavenCentral()
gradlePluginPortal()
}

dependencies {
implementation("org.freemarker:freemarker:2.3.31")
}

intellij {
version.set("2021.3.3")
type.set("IC") // Target IDE Platform

plugins.set(listOf(/* Plugin Dependencies */))
}

tasks {
// Set the JVM compatibility versions
withType<JavaCompile> {
options.encoding = "UTF-8"
sourceCompatibility = "11"
targetCompatibility = "11"
}

patchPluginXml {
sinceBuild.set("213")
untilBuild.set("223.*")
}
}

注意相比于自动生成项目的配置,这里更改了如下几点:

镜像库

1
2
3
4
5
6
7
8
repositories {
maven("https://maven.aliyun.com/repository/central")
maven("https://maven.aliyun.com/repository/public")
maven ("https://maven.aliyun.com/repository/gradle-plugin")
maven ("https://maven.aliyun.com/repository/apache-snapshots")
mavenCentral()
gradlePluginPortal()
}

编码

要设置编码,否则中文会乱码。

1
2
3
4
5
withType<JavaCompile> {
options.encoding = "UTF-8"
sourceCompatibility = "11"
targetCompatibility = "11"
}

项目本身也设置为UTF-8

image-20240804004959572

插件版本设置

调试IDEA版本

intellij中版本设置的时调试时使用的IDEA版本,他会自动下载

1
2
3
4
5
6
intellij {
version.set("2021.3.3")
type.set("IC") // Target IDE Platform

plugins.set(listOf(/* Plugin Dependencies */))
}

运行的时候会自动下载该版本的IDEA来运行我们的插件。

我们平常使用的是IU版本。

IC版本是社区版本,是不用激活的,方便我们测试。

过程中有三个文件都不小,比较费时:

注意

如果下载太慢可以本地下载

https://download.jetbrains.com.cn/idea/ideaIC-2021.3.3.exe

下载后修改配置

1
2
3
4
intellij {
plugins.set(listOf(/* Plugin Dependencies */))
localPath.set("D:\\Program Files\\JetBrains\\IntelliJ IDEA Community Edition 2021.3.3")
}

注意

调试的版本要和依赖版本匹配,不能随意更改。

插件支持IDEA版本

这个版本是我们插件支持的最低和最高版本

1
2
3
4
patchPluginXml {
sinceBuild.set("213")
untilBuild.set("241.*")
}

这个版本可以在IDEA的 Help => About 查看

image-20240803232013171

Java版本设置

还有这个版本要和对应IDEA依赖的Java版本一致,可以和我们插件项目依赖的Java版本不一致,我就是用的JDK17,而这里配置的11,可以正常编译运行。

1
2
3
4
withType<JavaCompile> {
sourceCompatibility = "11"
targetCompatibility = "11"
}

更换gradle版本

默认的7.5我在构建的时候报错,所以就更换为7.6版本了。

gradle-wrapper.properties

1
2
3
4
5
6
#Wed Mar 20 15:45:42 HKT 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-7.6-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

镜像地址

https://mirrors.cloud.tencent.com/gradle/

构建报错

Caused by: java.lang.NullPointerException: getHeaderField(“Location”) must not be null

这个错误不影响。

FreeMarker使用

build.gradle.kts中添加

1
2
3
dependencies {
implementation("org.freemarker:freemarker:2.3.31")
}

resources/templates/template.ftl

1
2
# This is an example template
Hello, ${name}!

工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;

import java.io.*;
import java.util.HashMap;
import java.util.Map;
public class ZFreeMarkerUtils {
public static String getStrByFtl(String templateName,Map<String, Object> dataModel) {
try {
// 配置 Freemarker
Configuration cfg = new Configuration(Configuration.VERSION_2_3_31);
cfg.setClassForTemplateLoading(ZFreeMarkerUtils.class, "/templates");
Template template = cfg.getTemplate(templateName);
try (StringWriter out = new StringWriter()) {
template.process(dataModel, out);
String result = out.toString();
System.out.println(result);
return result;
} catch (TemplateException e) {
return "";
}

} catch (IOException ex) {
return "";
}
}

public static void test(){
// 创建数据模型
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("name", "World");
getStrByFtl("template.ftl",dataModel);
}
}

打包

Gradle中通过Tasks/build/build来打包我们的插件。

构建好后我们可以在build/distributions目录下面找到我们的zip包,拿到后直接在idea上面进行离线安装即可。

image-20240804035040334

注意

网上有说用intellij/buildPlugin打包,这是不对的,这样打的是Jar包,我们代码中引用的模板文件不会打进去,就不能正常使用。

插件上传

插件上传地址

https://plugins.jetbrains.com/

上传后需要两日进行审核。

我的插件的地址:

https://plugins.jetbrains.com/plugin/25038-vuecomp?noRedirect=true

下载地址