5分钟 IDEA 插件开发快速入门 Demo

基于 Gradle 开发 IntelliJ 插件是官方推荐的方式。本 Demo 使用 IntelliJ Platform Plugin Template 快速构建一个插件项目 。 利用 IntelliJ Platform 平台开发自己的第一个插件!

5分钟 IDEA 插件开发快速入门 Demo

基于 Gradle 开发 IntelliJ 插件是官方推荐的方式。本 Demo 使用 IntelliJ Platform Plugin Template 快速构建一个插件项目 。 利用 IntelliJ Platform 平台开发自己的第一个插件!

Requirement

Demo简介

本Demo主要演示如何快速构建一个插件项目,实现一个 Action,对 IDEA 插件开发有个最基本的了解。

本Demo目标:

  • 快速搭建一个插件项目
  • 完成一个Menu Action:选中Java代码并替换成固定字符串
  • 了解PSI概念: 编写一个 Action,弹出窗口,显示光标所在位置上的 Java 方法相关的 PSI 信息

项目结构预览

  1~5-Minutes-Demo~\QUICK-PLUGIN-DEMO
  2|   .gitignore
  3|   build.gradle.kts
  4|   CHANGELOG.md
  5|   CODE_OF_CONDUCT.md
  6|   gradle.properties
  7|   gradlew
  8|   gradlew.bat
  9|   LICENSE
 10|   qodana.yml
 11|   README.md
 12|   settings.gradle.kts
 13|   
 14+---.github
 15|   |   dependabot.yml
 16|   |   
 17|   +---ISSUE_TEMPLATE
 18|   |       bug_report.md
 19|   |       
 20|   +---readme
 21|   |       draft-release.png
 22|   |       intellij-platform-plugin-template.png
 23|   |       qodana.png
 24|   |       run-debug-configurations.png
 25|   |       run-logs.png
 26|   |       settings-secrets.png
 27|   |       ui-testing.png
 28|   |       use-this-template.png
 29|   |       
 30|   +---template-cleanup
 31|   |   |   CHANGELOG.md
 32|   |   |   gradle.properties
 33|   |   |   README.md
 34|   |   |   settings.gradle.kts
 35|   |   |   
 36|   |   \---.github
 37|   |           dependabot.yml
 38|   |           
 39|   \---workflows
 40|           build.yml
 41|           release.yml
 42|           run-ui-tests.yml
 43|           template-cleanup.yml
 44|           
 45+---.idea
 46|       icon.png
 47|       
 48+---.run
 49|       Run IDE for UI Tests.run.xml
 50|       Run IDE with Plugin.run.xml
 51|       Run Plugin Tests.run.xml
 52|       Run Plugin Verification.run.xml
 53|       Run Qodana.run.xml
 54|       
 55+---gradle
 56|   \---wrapper
 57|           gradle-wrapper.jar
 58|           gradle-wrapper.properties
 59|           
 60\---src
 61    +---main
 62    |   +---java
 63    |   |   \---jiux
 64    |   |       \---net
 65    |   |           \---idea
 66    |   |               \---plugin
 67    |   |                   \---demo
 68    |   |                       \---action
 69    |   |                               EditorReplaceAction.java
 70    |   |                               PsiDemoAction.java
 71    |   |                               
 72    |   +---kotlin
 73    |   |   \---org
 74    |   |       \---jetbrains
 75    |   |           \---plugins
 76    |   |               \---template
 77    |   |                   |   MyBundle.kt
 78    |   |                   |   
 79    |   |                   +---listeners
 80    |   |                   |       MyProjectManagerListener.kt
 81    |   |                   |       
 82    |   |                   \---services
 83    |   |                           MyApplicationService.kt
 84    |   |                           MyProjectService.kt
 85    |   |                           
 86    |   \---resources
 87    |       +---messages
 88    |       |       MyBundle.properties
 89    |       |       
 90    |       \---META-INF
 91    |               plugin.xml
 92    |               pluginIcon.svg
 93    |               
 94    \---test
 95        +---kotlin
 96        |   \---org
 97        |       \---jetbrains
 98        |           \---plugins
 99        |               \---template
100        |                       MyPluginTest.kt
101        |                       
102        \---testData
103            \---rename
104                    foo.xml
105                    foo_after.xml

第一步:利用 IntelliJ Platform Plugin Template 创建项目

1# clone 项目到本地 
2
3git clone git@github.com:JetBrains/intellij-platform-plugin-template.git quick-plugin-demo
4
5# 删除 github 的远程仓库地址, 切换成自己的
6
7git remote rm origin
8
9git remote add origin 自己的远程仓库地址

该步骤有可能因为墙的原因网络被中断,多试几次。

第二步:导入项目,进行相关配置

修改配置支持Java8

  • gradle.properties
 1
 2# 插件支持的最小版本改为 202
 3pluginSinceBuild=202
 4#增加 2020.2版本
 5pluginVerifierIdeVersions=2020.2, 2020.3.4, 2021.1.3, 2021.2.1
 6# IC是社区版,这里用IU企业版
 7platformType=IU
 8#该版本为要求Java8的最高版本,在此之后的版本最低要求Java11
 9platformVersion=2020.2
10#JDK1.8
11javaVersion=1.8

IDEA 更多版本号,参见 最新IDEA版本

  • build.gradle.kts
1// 将 Gradle IntelliJ Plugin 的版本修改为 1.0
2id("org.jetbrains.intellij") version "1.0"

完成以上修改后, 重新加载项目,点击运行 Run Plugin 启动插件。

first-run.png

支持Java平台

默认只引入了基础平台相关的Jar包,要支持 Java 语言,需要自己添加。

  • gradle.properties
1
2# 引入Java支持
3platformPlugins=com.intellij.java
  • src/main/resources/META-INF/plugin.xml
1
2<idea-plugin>
3
4    <!-- 引入Java依赖 -->
5    <depends>com.intellij.java</depends>
6    <depends>com.intellij.modules.lang</depends>
7
8</idea-plugin>
  • 创建Java源代码目录

IDEA 插件既支持 Kotlin,Java 语言独立开发,也支持两者混合开发,写的类可以互相调用。 默认只有 kotlin 源代码目录。 Java 源代码目录需要手动创建。 创建 src/main/java 即可开始写 Java 代码了。

默认的 kotlin 目录可以删除,但个人建议保留。 因为 kotlin 下的代码可以直接拿来做国际化,有些开源库是 kotlin 写的,可以直接 拿来用,混合开发还是比较有优势的。

完成以上步骤后,刷新下项目,相关的依赖就加入到工程中了, 可以正式开始写代码了。

第三步 创建一个 Action

Action 是一个具有状态,展示和行为的实体。 通过继承 AnAction 重写 actionPerformed 方法实现 Action 的行为控制。 通过可选择地重写 update 方法实现 Action 的展示控制。

  • com.dmall.rdp.plugin.demo.action.EditorReplaceAction
 1/**
 2 * Menu Action 将代码中选中的字符替换成固定的字符
 3 */
 4public class EditorReplaceAction extends AnAction {
 5    /**
 6     * Action 事件处理
 7     *
 8     * @param e 事件对象
 9     */
10    @Override
11    public void actionPerformed(@NotNull AnActionEvent e) {
12        // 获取当前工程,当前编辑器,当前文档
13        final Editor editor = e.getRequiredData(CommonDataKeys.EDITOR);
14        final Project project = e.getRequiredData(CommonDataKeys.PROJECT);
15        final Document document = editor.getDocument();
16
17        // 获取编辑器当前光标
18        Caret primaryCaret = editor.getCaretModel().getPrimaryCaret();
19        // 光标选中的开始位置和结束位置
20        int start = primaryCaret.getSelectionStart();
21        int end = primaryCaret.getSelectionEnd();
22        // 将选中的字符替换成固定字符
23        // 注意不能直接使用  document.replaceString() 方法
24        // document.replace相关操作文档的方法,不能在直接在事件处理上下文线程中执行。
25        // 而是 必须 在 写操作的上下文线程中执行,即使用 WriteCommandAction.runWriteCommandAction 方法
26        // 因为 这类文档操作 被认为是耗时的操作,不能阻塞UI事件主线程。
27        WriteCommandAction.runWriteCommandAction(project, () ->
28                document.replaceString(start, end, "~这是替换的~")
29        );
30        // 刚才替换的字符串取消选中
31        primaryCaret.removeSelection();
32    }
33
34    /**
35     * 控制在菜单中的展示,满足以下条件时可见且可用:
36     * <ul>
37     *   <li>工程打开</li>
38     *   <li>编辑器打开</li>
39     *   <li>字符串被选中</li>
40     * </ul>
41     *
42     * @param e Event related to this action
43     */
44    @Override
45    public void update(@NotNull AnActionEvent e) {
46        // 获取当前工程
47        final Project project = e.getProject();
48        // 获取编辑器
49        final Editor editor = e.getData(CommonDataKeys.EDITOR);
50        // 仅在 当前工程和编辑器不为空时(它们都处于打开的状态),且存在选中的字符时,设置该 Menu Action 可见且可用
51        e.getPresentation().setEnabledAndVisible(
52                project != null && editor != null && editor.getSelectionModel().hasSelection());
53    }
54}
  • src/main/resources/META-INF/plugin.xml
 1
 2<actions>
 3    <!-- 将此 Action 放到弹出菜单的第一个位置; 如果项目和编辑器是打开的,有字符被选中时,它总是可用的 -->
 4    <action id="com.dmall.rdp.plugin.demo.action.EditorReplaceAction"
 5            class="com.dmall.rdp.plugin.demo.action.EditorReplaceAction"
 6            text="选中替换"
 7            description="选中替换">
 8        <!-- 设置快捷键 Ctrl+Alt+G -->
 9        <keyboard-shortcut keymap="$default" first-keystroke="control alt G"/>
10        <!-- 放到编辑器弹出菜单第一个位置 -->
11        <add-to-group group-id="EditorPopupMenu" anchor="first"/>
12    </action>
13</actions>

第四步 创建一个 Action 展示 PSI 信息

PSI 简介

程序结构接口(Program Structure Interface),通常简称为PSI。 是IntelliJ平台中负责解析文件和创建语法和语义代码模型的层。

  • PSI 文件

PSI 文件是一个程序逻辑结构的根,该结构将文件的内容表示为一种特定编程语言中的元素的层次结构。 PsiFile 类是所有 PSI 文件的通用基类,而特定语言的文件通常由其子类表示。 例如,PsiJavaFile 类表示一个Java文件,而 XmlFile 类表示一个XML文件

  • PSI 元素

一个 PSI 文件代表了 PSI 元素(也叫 PSI 树)的层次结构。 PSI 元素表示源代码的内部结构,是由 IntelliJ 平台解析的。 在 PSI 元素上的操作一般用于代码分析,代码检查等。

PsiElement 类是 PSI 元素的通用基类。

有关 PSI 的更多介绍,参见 PSI

展示 PSI 信息

  • com.dmall.rdp.plugin.demo.action.PsiDemoAction
 1/**
 2 * PSI 演示 Action,展示光标所处位置上的 PSI 元素信息。
 3 *
 4 */
 5public class PsiDemoAction extends AnAction {
 6    @Override
 7    public void actionPerformed(AnActionEvent anActionEvent) {
 8        //获取当前编辑器和 PSIFile 对象
 9        Editor editor = anActionEvent.getData(CommonDataKeys.EDITOR);
10        PsiFile psiFile = anActionEvent.getData(CommonDataKeys.PSI_FILE);
11        if (editor == null || psiFile == null) {
12            return;
13        }
14        //获取光标在文档中的偏移量
15        int offset = editor.getCaretModel().getOffset();
16        final StringBuilder infoBuilder = new StringBuilder();
17        //获取光标所在位置的 PSI 树中的元素
18        PsiElement element = psiFile.findElementAt(offset);
19        infoBuilder.append("当前光标所指元素: ").append(element).append("\n");
20        if (element != null) {
21            //查找 方法
22            PsiMethod containingMethod = PsiTreeUtil.getParentOfType(element, PsiMethod.class);
23            infoBuilder
24                    .append("方法: ")
25                    .append(containingMethod != null ? containingMethod.getName() : "none")
26                    .append("\n");
27            if (containingMethod != null) {
28                //查找方法所属的类
29                PsiClass containingClass = containingMethod.getContainingClass();
30                infoBuilder
31                        .append("类: ")
32                        .append(containingClass != null ? containingClass.getName() : "none")
33                        .append("\n");
34                //查找方法中的本地变量
35                infoBuilder.append("本地变量:\n");
36                containingMethod.accept(new JavaRecursiveElementVisitor() {
37                    @Override
38                    public void visitLocalVariable(PsiLocalVariable variable) {
39                        super.visitLocalVariable(variable);
40                        infoBuilder.append(variable.getName()).append("\n");
41                    }
42                });
43            }
44        }
45        Messages.showMessageDialog(anActionEvent.getProject(), infoBuilder.toString(), "PSI信息", null);
46    }
47
48    @Override
49    public void update(AnActionEvent e) {
50        Editor editor = e.getData(CommonDataKeys.EDITOR);
51        PsiFile psiFile = e.getData(CommonDataKeys.PSI_FILE);
52        e.getPresentation().setEnabled(editor != null && psiFile != null);
53    }
54}
  • src/main/resources/META-INF/plugin.xml
1
2    <!-- 将此 Action 放在工具菜单的最后一个位置; 如果项目和编辑器是打开的,它总是可用的 -->
3    <action
4            id="com.dmall.rdp.plugin.demo.action.PsiDemoAction"
5            class="com.dmall.rdp.plugin.demo.action.PsiDemoAction"
6            text="查看PSI信息 ">
7        <!-- 放到工具菜单第后一个位置 -->
8        <add-to-group group-id="ToolsMenu" anchor="last"/>
9    </action>

第五步运行插件,查看效果

点击 Run Plugin 或 Debug Plugin 运行插件,试着操作,查看效果。

img.png

psi_review.png

一些特别有用的参考

下面收集的文档对开发插件是非常有帮助的,建议深入熟悉。