Intellij IDEA Plugin DEV (二)


5/23/2017 IDEA

相对于 Hello World 版的插件, 我们可以学习一个 API 的用法.

在写之前, 先补充一下上一篇博文中存在的问题.

使用 Gradle 开发 Intellij IDEA Plugin 的问题

当我们使用 Intellij 自带的 Intellij Platform Plugin 创建插件项目后, 我们可以通过 Intellij 以图形化界面创建 Action, Module Component 等, 就像这样:

OK 之后, 就会自动创建一个 GenerateActionByGui 继承 AnAction 的类, 并且在 plugin.xml 中自动写入插件的配置:

<actions>
    <action id="plugin.demo" class="com.code.demo.GenerateActionByGui" text="demo" description="this is a demo">
      <add-to-group group-id="WindowMenu" anchor="first"/>
    </action>
</actions>
1
2
3
4
5

简单快捷方便, 将程序猿偷懒的精神展现的淋漓尽致, 那么问题来了.......

当我们使用 Gradle 或者 Maven 这种项目管理软件创建 插件项目时, 你会发现这些功能没有了, 不信你看..

没有选择 Action 的按钮了, 而且还要一个问题

Use classpath of module 一直为 [none] , 这时候该怎么办呢?

解决方法

这里首先应该想到, 为什么创建 Intellij Platform Plugin 项目和创建 Gradle 项目存在区别? IDEA 应该会根据不同的项目类别显示不同的可用功能按钮. 对 Intellij IDEA 了解的朋友应该知道, 当创建或者打开一个IDEA工程时,会自动创建 .idea文件夹 和 .iml 文件, iml是 Intellij IDEA 的工程配置文件, 里面是当前projec的一些配置信息. 既然是工程配置文件, 应该会标识当前项目是属于哪种类型的工程, 顺着这种思路, 还真被我找到了.

使用 Gradle 创建的项目中, 在 .idea/moudle/ 下有3个 .iml 文件

我们需要改的就是 _main.iml

原来的文件内容:

<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id="Translation:main" external.linked.project.path="$MODULE_DIR$/../.." external.root.project.path="$MODULE_DIR$/../.." external.system.id="GRADLE" external.system.module.group="" external.system.module.type="sourceSet" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
  <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8" inherit-compiler-output="false">
    <output url="file://$MODULE_DIR$/../../build/classes/main" />
    <exclude-output />
    <content url="file://$MODULE_DIR$/../../src/main">
      <sourceFolder url="file://$MODULE_DIR$/../../src/main/java" isTestSource="false" />
      <sourceFolder url="file://$MODULE_DIR$/../../src/main/resources" type="java-resource" />
    </content>
    ......
1
2
3
4
5
6
7
8
9
10

改过之后的内容:

<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id="Translation:main" external.linked.project.path="$MODULE_DIR$/../.." external.root.project.path="$MODULE_DIR$/../.." external.system.id="GRADLE" external.system.module.group="" external.system.module.type="sourceSet" external.system.module.version="unspecified" type="PLUGIN_MODULE" version="4">
  <!-- 新加的一行 -->
  <component name="DevKit.ModuleBuildProperties" url="file://$MODULE_DIR$/../../src/main/resources/META-INF/plugin.xml" />
  <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8" inherit-compiler-output="false">
    <output url="file://$MODULE_DIR$/../../build/classes/main" />
    <exclude-output />
    <content url="file://$MODULE_DIR$/../../src/main">
      <sourceFolder url="file://$MODULE_DIR$/../../src/main/java" isTestSource="false" />
      <sourceFolder url="file://$MODULE_DIR$/../../src/main/resources" type="java-resource" />
    </content>
    ......
1
2
3
4
5
6
7
8
9
10
11
12

区别就是将 <module\> 标签中的 type 属性由原来的 JAVA_MODULE 改为 PLUGIN_MODULE. 修改之后整个项目其实已经是一个 Plugin 项目了

图标都变成 Plugin 特有的了,看到这个我就放心了.....

如果现在通过 GUI 创建一个 Action, 到最后你会发现会报一个找不到 plugin.xml 的错误, 所以还需要添加一个行

<component name="DevKit.ModuleBuildProperties" url="file://$MODULE_DIR$/../../src/main/resources/META-INF/plugin.xml" />
1

让 IDEA 找到 plugin.xml, 并在我们创建好 Action 后, 向 plugin.xml 中写入 Action 配置.

还有一个问题没有解决, 虽然我们已经能配置

但是当运行的时候,却会报错, 因为我们的依赖是通过 Gradle 管理的, 这样直接运行虽然能打开容器, 但却不能调试我们的插件, 我们还是通过 Gradle 运行

gradle runI
1

如果从 GitHub上 clone 下来一个优秀的插件学习的话, clone下来的包中可能没有 .idea 这个文件夹, 当导入到 IDEA 中时, 只有以下几个选项

如果不是上述项目, 我选择直接打开项目, 等 IDEA 自动创建好 .idea 后, 通过修改 .iml 文件, 将项目改为 plugin 项目, 然后就可以创建 Plugin 的运行环境了. 这种方法适合没有通过 Gradle 或者 Maven 项目管理的 Plugin 项目.

问题解决, 接下来是一个翻译插件的开发过程

翻译插件开发

愉快的用 GUI 创建一个 Action

有道 API

有道翻译 API HTTP 地址:

http://openapi.youdao.com/api

有道翻译 API HTTPS 地址:

https://openapi.youdao.com/api

接口调用参数

调用 API 需要向接口发送以下字段来访问服务.

字段名 类型 含义 必填 备注
q text 要翻译的文本 True 必须是 UTF-8 编码
from text 源语言 True 语言列表 (可设置为 auto)
to text 目标语言 True 语言列表 (可设置为 auto)
appKey text 应用 ID True 可在应用管理查看
salt text 随机数 True
sign text 签名, 通过 md5(appKey+q+salt + 密钥) 生成 True appKey+q+salt + 密钥的 MD5 值

签名生成方法如下:

  1. 将请求参数中的 appKey, 翻译文本 query(q, 注意为 UTF-8 编码), 随机数 (salt) 和密钥 (可在应用管理查看), 按照 appid+q+salt + 密钥 的顺序拼接得到字符串 str.
  2. 对字符串 str 做 md5, 得到 32 位小写的 sign(参考 Java 生成 MD5 示例)

注意:

  1. 请先将需要翻译的文本转换为 UTF-8 编码
  2. 在发送 HTTP 请求之前需要对各字段做 URL encode.
  3. 在生成签名拼接 appKey+q+salt 字符串时, q 不需要做 URL encode, 在生成签名之后, 发送 HTTP 请求之前才需要对要发送的待翻译文本字段 q 做 URL encode.

输出结果

返回的结果是 json 格式, 包含字段与 FROM 和 TO 的值有关, 具体说明如下:

字段名 类型 含义 备注
errorCode text 错误返回码 一定存在
query text 源语言 查询正确时, 一定存在
speakUrl text 输入发音地址 输入发音地址, 一定存在
tSpeakUrl text 翻译发音地址 翻译发音地址, 一定存在
translation text 翻译结果 查询正确时一定存在
basic text 词义 基本词典, 查词时才有
web text 词义 网络释义, 该结果不一定存在

总结

Action ID : 代表当前 Action 的唯一 id Class Name : 类名 Name : 插件按钮显示在菜单上的名称 Description : 鼠标悬浮在按钮上时,界面底部显得的描述 Add to Group : 功能按钮添加的位置 Groups : 所属的分组 Action : 设置在组中的位置 Keyboard shortcuts : 功能按钮的快捷键

插件开发一些 API

获取当前编辑的文件 PsiFile psiFile = event.getData(LangDataKeys.PSI_FILE); 可以通过下面两种方式对文件的进行操作:

new WriteCommandAction.Simple(event.getProject(), psiFile) {
    @Override
    protected void run() throws Throwable {
        //do something
    }
}.execute();
1
2
3
4
5
6
WriteCommandAction.runWriteCommandAction(event.getProject(), new Runnable() {
    @Override
    public void run() {
        //do something
    }
});
1
2
3
4
5
6

获取当前编辑的 class 对象

PsiElement element = psiFile.findElementAt(editor.getCaretModel().getOffset());
PsiClass psiClass = PsiTreeUtil.getParentOfType(element, PsiClass.class);
1
2

获取类名

String className = psiClass.getNameIdentifier().getText();
1

获得 PsiElementFactory 对象 可以通过这个工厂类创建成员变量 方法 类等等

PsiElementFactory elementFactory = JavaPsiFacade.getElementFactory(project);
1

添加一个方法

String methodText = buildMethodText(className);
PsiMethod psiMethod = elementFactory.createMethodFromText(methodText, psiClass);
psiClass.add(psiMethod);
1
2
3
private String buildMethodText (String className){
    return "public static " + className + " getInstance() {\n" +
            "        return " + buildFiledText() + ";\n" +
            "    }";
}
1
2
3
4
5

添加一个构造方法

PsiMethod constructor = elementFactory.createConstructor();
psiClass.add(constructor);
1
2

添加一个成员变量, PsiType 表示变量的类型, PsiModifierList 代表变量的修饰符, 可以通过 setInitializer 设置变量初始化方式

PsiType psiType = PsiType.getTypeByName(className, project
        , GlobalSearchScope.EMPTY_SCOPE);
PsiField psiField = elementFactory.createField("mInstance", psiType);
PsiExpression psiInitializer = elementFactory.createExpressionFromText("new " + className + "()", psiField);
psiField.setInitializer(psiInitializer);
PsiModifierList modifierList = psiField.getModifierList();
if (modifierList != null) {
    modifierList.setModifierProperty(PsiModifier.STATIC, true);
}
psiClass.add(psiField);
1
2
3
4
5
6
7
8
9
10

添加一个内部类

PsiClass innerClass = elementFactory.createClass(innerClassName);
PsiModifierList classModifierList = innerClass.getModifierList();
if (classModifierList != null) {
    classModifierList.setModifierProperty(PsiModifier.PRIVATE, true);
    classModifierList.setModifierProperty(PsiModifier.STATIC, true);
}
psiClass.add(innerClass);
1
2
3
4
5
6
7

其他

//创建枚举
PsiClass anEnum = elementFactory.createEnum("TestEnum");
//创建一个枚举常量
PsiEnumConstant enumConstant= elementFactory.createEnumConstantFromText("TEST",anEnum);
//创建接口
elementFactory.createInterface("TestInterface");
1
2
3
4
5
6

格式化代码

CodeStyleManager.getInstance(project).reformat(psiClass);
1

插件的 UI 都是使用 java Swing 来实现, 比如创建一个 Dialog:src>new>Dialog; 然后会生成一个 JDialog 的 java 类和一个 Dialog 的 from 文件. 然后在 Action 中使用:

TestDialog dialog = new TestDialog();
dialog.setBounds(new Rectangle(300, 200));
//让dialog居中
dialog.setLocationRelativeTo(null);
dialog.setVisible(true);
1
2
3
4
5
Last Updated: 7/3/2019, 6:22:52 PM