End

AS 自定义插件 总结

本文地址


目录

IDEA 插件开发

IDEA 的插件几乎可以做任何事情,因为它把 IDE 本身的能力都封装好开放出来了。主要的插件功能包含以下四种::

  • Custom language support:自定义编程语言的支持。包括语法高亮、文件类型识别、代码格式化、代码查看和自动补全等等;参考 Custom Language Support Tutorial
  • Framework integration:框架集成。基于 IntelliJ 开发一个 IDE,比如 AndroidStudio 将 Android SDK 集成进 IntelliJ
  • Tool integration:工具集成。对 IntelliJ 定制一些个性化或者是实用的工具,这种插件是最多的,也是我们开发插件的主要目的
  • User interface add-ons:附加UI。对标准的UI界面进行修改,如在编辑框里加一个背景图片等;参考 BackgroundImage

第一个 IDEA 插件

在开发 IntelliJ 插件时,我们使用的是 IntelliJ IDEA(旗舰版、社区版都可以) 自身来开发。为什么不用 AS 呢?因为 AS 没有针对插件的各种环境,当然,你也可以自己下载插件然后在 AS 上去配置,但是过于麻烦。

开发插件的工具叫IntelliJ Platform Plugin SDKIntelliJ Plugin Develop kit,其实就是一个叫ideaIC的 SDK,可以类比为Android SDK

新建项目

File -> New -> Project -> 选择 Gradle -> 选择需要的库和框架 -> 填写项目信息 -> 确定

新建完工程之后,IDEA 会自动开始解析项目依赖,因为它要下载一个几百兆的 SDK 依赖包,所以可能会比较久。

配置 gradle

gradle 插件版本可以这样配置:

File -> Settings -> Build, Execution, Deployment -> Build Tools -> Gradle

为了方便,我将此目录拷到了根目录中

完整的配置

plugins {
    id 'java'
    id 'org.jetbrains.intellij' version '0.6.5'
}

group 'com.bqt.test.plugin'
version '0.3'

repositories {
    mavenCentral()
}

dependencies {
    testCompile 'junit:junit:4.12'
    implementation 'com.google.code.gson:gson:2.8.6'
}

intellij {
    version '201.8743.12'
    type 'IC'
    plugins['android']
}

patchPluginXml {
    version project.version
    sinceBuild '123.72'
    untilBuild ''
}

项目结构

依赖解析完成之后,项目结构如下图:

初始工程可能需要手动在 main 下创建 java 目录,再创建 package 目录

项目下的文件:

  • src:存放的是插件所需要的 Java 源码、插件配置、图片资源等内容
  • plugin.xml:插件的配置文件
  • build.gradle:和 Android 项目下的同名文件类似,构建项目的配置文件
  • settings.gradle:和 Android 项目下的同名文件类似,只有一行代码 rootProject.name = 'Bqtplugin'

plugin.xml

这里有一个官方提供的,包含所有可配置项的案例

比较重要的几个配置:

  • id:在插件市场中唯一确定你的插件。一旦定义好并启用后,后续不可再更改,否则会成为新的插件
  • name:插件名称,例如CodeGlance
  • vendor:作者主站网址和邮箱配置,便于用户有疑问时联系你
  • description:插件功能说明,支持大部分 HTML 标签
  • change-notes:插件更新日志描述

plugin.xml中的初始内容大概如下

<idea-plugin>
    <id>com.bqt.test.plugin.BqtPlugin</id>
    <name>白乾涛的插件</name>
    <vendor email="0909082401@163.com" url="https://www.cnblogs.com/baiqiantao/">白乾涛</vendor>
    <description> <![CDATA[这是description<br>支持大部分 HTML 标签]]></description>
    <change-notes><![CDATA[这是change-notes<br>支持大部分 HTML 标签]]></change-notes>

    <!-- 依赖的插件 -->
    <depends>com.intellij.modules.platform</depends>

    <extensions defaultExtensionNs="com.intellij">
        <!--依赖的其他插件能力,Add your extensions here-->
    </extensions>

    <actions>
        <!--在这里定义插件动作-->
    </actions>
</idea-plugin>

创建 Action

Action 是 IDEA 中对事件响应的处理器,主要用来接受用户的动作行为,类似 Android 中 Activity 或 View 的存在,是编写插件功能的最主要入口。

创建方式:在 package 上右键 -> New -> Plugin Devkit -> Action

需要填写的属性如下:

  • ActionID:代表该Action的唯一唯一标识
  • ClassName:对应的Java类的全路径
  • Name:就是最终插件在菜单上的名称
  • Description:对这个Action的描述信息
  • icon:插件图标,建议使用大小为16*16的png图片
  • Add to Group:指定我们自定义的插件应该放入到哪个菜单下面
    • Groups:这个Action所存在的组
    • Anchor:相对位置:first/last 最前/后面,before/after:放在 relative-to-action 属性指定的 ID 的前/后面
  • Keyboard Shortcut:调起此Action的快捷键

这些信息都会注册在plugin.xml中,后续可以手动修改。

其中,actionPerformed是其核心且必须实现的方法。

public abstract void actionPerformed(@NotNull AnActionEvent e);
public class MainMenuAction extends AnAction {

    @Override
    public void actionPerformed(AnActionEvent e) {
        //标准弹窗
        Messages.showMessageDialog("Hello World !", "Information", Messages.getInformationIcon());
    }
}

调试、构建

调试插件

代码写完之后,选择 Plugin 后点击 Run 按钮,或点击 gradle -> intellij -> runIde Task,就会启动一个安装了插件的 IDEA,点击Create New Project或者是导入一个现存的项目,让它正确进入到开发界面,然后就可以进行测试。

你还可以启动 Debug 模式,这样还能进行断点。

以上等价于通过运行runIde任务

生成插件

点击buildPlugin任务即可生成插件,插件生成后被存放在\build\distributions目录中,是一个zip文件。

文件名由settings.gradle中的rootProject.name的值 + build.gradle中的version的值构成,例如:BqtPlugin-0.1.zip

发布插件

  • 插件仓库地址
  • 需要注册账号并登录,也可以使用GitHub、Google等第三方账号登录。
  • 点击 Upload Plugin,准备好要上传的 jar 包,插件的大部分说明信息均配置在plugin.xml中,所以上传插件时只需简单的填写几个无关紧要的一些说明即可。
  • 上传以后还需要经过官方的审核,大约需要两个工作日:

Thank you! The plugin has been submitted for moderation. The request will be processed within two business days.

  • 自己可以在 My profile 中查看自己所有发布的插件的详细信息。
  • 审核通过后,就可以在 IntelliJ IDEA 和 AS 的插件市场搜索并下载了

一些可能遇到的问题

手动下载 ideaIC

因为这个组件非常大,大约 500MB 左右,通过 IDEA 很难下载成功,建议采取如下方式 手动下载 ideaIC

  • 找到你上述配置的 gradle 的如下子目录,例如D:\_dev\gradle\_GRADLE_USER_HOME\caches\modules-2\files-2.1\com.jetbrains.intellij.idea\ideaIC
  • 这个就是 ideaIC 组件配置信息目录,我们不需要关心里面具体什么内容,只需要看文件夹名字即可,例如为:2020.2.4(这其实就是你所安装的 IDEA 的版本)
  • 根据你的版本号,直接用迅雷下载以下文件,我这边瞬间就下载完成了:
  • 取消 IDEA 中的下载进程,将上面通过迅雷下载的ideaIC-2020.2.4.zip拷到2020.2.4目录中
  • 同步一下项目,就会跳过下载ideaIC-2020.2.4.zip这个步骤,后面很快就会提示:BUILD SUCCESSFUL
  • 然后就可以把上述ideaIC-2020.2.4.zip直接删掉了(因为这个文件会被复制到其他目录中),注意复制的ideaIC-2020.2.4.zip文件不能删掉(虽然他已经被解压了,zip文件也不能删掉)

解决乱码问题

除了以下位置均设置为 UTF-8 外,还需要一个特殊的设置:

  • 双击 Shift 搜索 vmoptions,打开搜索到的文件(或通过菜单:Help--Edit Custom VM Options打开)

  • 如果没有该文件,请按照提示自动创建即可
  • 在文件末尾添加-Dfile.encoding=UTF-8
  • 重启 AndroidStudio,问题解决

如何支持 AS

如果我们不做任何特殊配置,那么上面生成的插件在 AS 中安装时会提示:不兼容!

配置 build.gradle

intellij {
    version '201.8743.12' //基于哪个版本构建插件,对应 AS 4.1.1
    type 'IC' //IC指IDEA社区版(免费版本),IU指旗舰版(收费版本)
    plugins 'android'
}

配置 plugin.xml

<idea-plugin>
    <!-- 依赖的插件 -->
    <depends>com.intellij.modules.platform</depends>
    <depends>org.jetbrains.android</depends>
    <depends>com.intellij.modules.androidstudio</depends>
</idea-plugin>

如何设置兼容版本

patchPluginXml {
    version project.version
    sinceBuild '123.72' //最低支持的版本
    untilBuild '' //最高支持的版本,不能不设置,不设置是默认为 project.version
}
<idea-version since-build="93.13"/>
<idea-version since-build="162.539.11"/>
<idea-version until-build="162"/> <!-- 仅支持162(不包含)之前的版本-->
<idea-version since-build="162" until-build="162.*"/> <!-- 所有 162 系列版本,例如:162.94, 162.94.11 -->

注意:如果不明确设置,则since-builduntil-build的值默认都是intellij.version

常见交互效果

IntelliJ Platform UI Guidelines

Messages

Messages.showMessageDialog(project, "message", "title", Messages.getInformationIcon());
Messages.showMessageDialog("message", "title", Messages.getInformationIcon());

ListPopup

参考

Project project = e.getData(PlatformDataKeys.PROJECT);
Editor editor = e.getData(PlatformDataKeys.EDITOR);
Runnable runnable = () -> JBPopupFactory.getInstance().createMessage("消息内容").showInFocusCenter();
Runnable yesRunnable = () -> JBPopupFactory.getInstance()
        .createConfirmation("标题", runnable, 0)
        .showCenteredInCurrentWindow(project);
Runnable noRunnable = () -> JBPopupFactory.getInstance()
        .createListPopup(new BaseListPopupStep("标题", "第一个值", "第二个值", "可以有任意个值..."))
        .showInBestPositionFor(editor);
JBPopupFactory.getInstance()
        .createConfirmation("标题", "yes名称", "no名称", yesRunnable, noRunnable, 1)
        .showInBestPositionFor(e.getDataContext());

JBPopupFactory

JBPopupFactory factory = JBPopupFactory.getInstance();
factory.createHtmlTextBalloonBuilder(text, null, new JBColor(JBColor.RED, JBColor.GREEN), null)
    .setFadeoutTime(5000)
    .createBalloon()
    .show(factory.guessBestPopupLocation(editor), Balloon.Position.below);

Process

Progress indicators

两个常见的线程调度类:

  • ProgressManager.getInstance().run...
  • ApplicationManager.getApplication().invoke...
ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> {
    Thread.sleep(2000);//虽然在子线程执行,但是会卡界面
    Messages.showMessageDialog("message", "title", Messages.getInformationIcon()); //因为是在子线程,所以这个弹窗是弹不出来的
}, "title", true, project);
ProgressManager.getInstance().run(new Task.Backgroundable(project, "title") {
    @Override
    public void run(@NotNull ProgressIndicator indicator) {
        indicator.setText("text");
        indicator.setIndeterminate(true);
        Thread.sleep(2000);//在子线程执行,不会卡界面
    }
});

自定义 DialogWrapper

MyDialogWrapper dialog = new MyDialogWrapper();
dialog.setTitle("标题");
dialog.setmOnSubmitListener(text -> Messages.showMessageDialog(text, "输入内容为:", Messages.getInformationIcon()));
dialog.show();
public class MyDialogWrapper extends DialogWrapper {
    
    private final JTextField mTextField = new JTextField();
    
    public MyDialogWrapper() {
        super(true);
        init();
    }
    
    @Override
    protected JComponent createNorthPanel() {
        JLabel title = new JLabel("表单标题");
        title.setFont(new Font("微软雅黑", Font.PLAIN, 26));
        JPanel north = new JPanel();
        north.add(title);
        return north;
    }
    
    @Override
    protected JComponent createCenterPanel() {
        JLabel jLabel = new JLabel("请输入:");
        jLabel.setForeground(new JBColor(JBColor.RED, JBColor.BLUE));
        
        JPanel center = new JPanel();
        center.setLayout(new GridLayout(3, 1));
        center.add(jLabel);
        center.add(mTextField);
        return center;
    }
    
    @Override
    protected JComponent createSouthPanel() {
        JButton submit = new JButton("提交");
        submit.setHorizontalAlignment(SwingConstants.CENTER);
        submit.setVerticalAlignment(SwingConstants.CENTER);
        submit.addActionListener(e -> {
            close(OK_EXIT_CODE);
            if (mOnSubmitListener != null) {
                mOnSubmitListener.onSubmit(mTextField.getText());
            }
        });
        
        JPanel south = new JPanel();
        south.add(submit);
        return south;
    }
    
    private OnSubmitListener mOnSubmitListener;
    
    public void setmOnSubmitListener(OnSubmitListener mOnSubmitListener) {
        this.mOnSubmitListener = mOnSubmitListener;
    }
    
    interface OnSubmitListener {
        void onSubmit(String text);
    }
}

拓展

自定义菜单

案例

<actions>
    <!--自定义菜单组-->
    <group id="om.bqt.test.plugin.menu1"
           text="我的插件"
           description="这是一个主菜单插件">
        <!--将此菜单组放到主菜单上-->
        <add-to-group group-id="MainMenu" anchor="last"/>
        <!--定义一个个的action-->
        <action id="com.bqt.test.plugin.action1"
                class="com.bqt.test.plugin.BqtPlugin.MainMenuAction"
                icon="/icons/icon.png"
                text="测试主菜单"
                description="测试主菜单--这是描述">
            <!--触发此action的快捷键-->
            <keyboard-shortcut keymap="$default" first-keystroke="shift ctrl alt L"/>
            <!--Tools菜单-->
            <add-to-group group-id="ToolsMenu" anchor="last"/>
            <!--Project面板上文件右键菜单-->
            <add-to-group group-id="ProjectViewPopupMenu" anchor="after" relative-to-action="AddToFavorites"/>
            <!--Editor区域右键菜单-->
            <add-to-group group-id="EditorPopupMenu" anchor="before" relative-to-action="$Paste"/>
            <!--Generate菜单-->
            <add-to-group group-id="GenerateGroup"/>
        </action>
    </group>
</actions>

popup 属性用于描述是否有子菜单弹出,如果取值为true,则<group>标签的内所有的<action>子标签作为<group>菜单的子选项,否则,<group>标签的内所有的<action>子标签将替换<group>菜单项所在的位置,即没有<group>这一层菜单。

以下为 popup 分别为 true 和 false 时的效果。

Action 的更多知识

Action 与 Application 同生命周期,所以不建议在 Action 的实例中保存短生命周期的对象,避免造成内存泄漏。

update 方法

update函数在 Action 状态发生更新时被回调,当 Action 状态刷新时,update 函数被 IDEA 回调,并且传递 AnActionEvent 对象,AnAction 对象中封装了当前 Action 对应的环境。

public void update(@NotNull AnActionEvent e)

我么可以在update() 方法中更新当前 Action 菜单的状态,比如可见性、可操作性等。

常见的观测状态有:项目是否被打开、是否有文件编辑器打开、选中的文本、当前打开文件的类型等。

@Override
public void update(@NotNull AnActionEvent e) {
    super.update(e);
    Editor editor = e.getData(PlatformDataKeys.EDITOR);
    e.getPresentation().setVisible(editor == null);// 设置当前 action 菜单的可见性,不可见是会被隐藏
    e.getPresentation().setEnabled(editor == null);// 设置当前 action 菜单的可用性,不可用时会被置灰
    e.getPresentation().setEnabledAndVisible(editor == null); // 同时设置可见性和可用性
}

AnActionEvent 对象

参数 AnActionEvent 中提供了上下文信息和与当前项目有关的众多数据,比如 Project、Editor、Navigatable 等,可以通过getData()方法获取,需要传递的参数为 PlatformDataKeys 类中的常量值。

PS: PlatformDataKeys类是CommonDataKeys的子类,也就是说,只要是CommonDataKeys有的,PlatformDataKeys类都有

Project project = e.getData(PlatformDataKeys.PROJECT); //获取当前操作的项目,进而可以获取当前项目的路径等数据
Editor editor = e.getData(PlatformDataKeys.EDITOR); //获取当前操作的编辑器,只有在打开了文件且处于编辑模式时才不为null
PsiFile psiFile = e.getData(PlatformDataKeys.PSI_FILE); //获取当前正在编辑的文件,进而可以获取到 VirtualFile
VirtualFile virtualFile = e.getData(CommonDataKeys.VIRTUAL_FILE); //获取编辑的文件,可以拿到绝对路径和名称

Presentation 对象

通过 AnActionEvent 对象的getPresentation()函数可以取得 Presentation 对象。

Presentation 对象表示一个 Action 在菜单中的外观,通过 Presentation 可以获取 Action 菜单项的各种属性,如显示的文本、描述、图标等。并且可以设置当前 Action 菜单项的状态、是否可见、显示的文本等。

e.getPresentation().setText(new SimpleDateFormat("yyyy.MM.dd HH:mm:ss SSS", Locale.getDefault()).format(new Date()));

修改选中的内容

private void changeSelectText(AnActionEvent e, String text) {
    Project project = e.getData(PlatformDataKeys.PROJECT);
    Editor editor = e.getData(PlatformDataKeys.EDITOR);
    
    Document document = editor.getDocument(); //代表整个文档,可以获取文档整个内容
    SelectionModel selectionModel = editor.getSelectionModel(); //代表选中的部分
    final int start = selectionModel.getSelectionStart();
    final int end = selectionModel.getSelectionEnd();
    
    WriteCommandAction.runWriteCommandAction(project, () -> document.replaceString(start, end, text));
    selectionModel.removeSelection();
}

保存及读取配置信息

String KEY_NAME = "key_bqt", KEY_SET_NAME = "key_set_bqt";
PropertiesComponent.getInstance().setValue(KEY_NAME, true, false);
PropertiesComponent.getInstance().setValue(KEY_NAME, 1, -1);
int value = PropertiesComponent.getInstance().getInt(KEY_NAME, -2);

boolean isValueSet = PropertiesComponent.getInstance().isValueSet(KEY_SET_NAME);
PropertiesComponent.getInstance().setValues(KEY_SET_NAME, new String[]{"1", "2", "3"});
String[] values = PropertiesComponent.getInstance().getValues(KEY_SET_NAME);

如何打开本地文件

private void openFile(Project project, File copyToFile) {
    VirtualFile virtualFile = LocalFileSystem.getInstance().findFileByIoFile(copyToFile);
    if (virtualFile != null) {
        new OpenFileDescriptor(project, virtualFile).navigate(true);
    }
}

刷新目录

//刷新目录结构,参数:是否异步,是否递归,完成后的回调。不建议获取项目的baseDir,建议针对性的刷新指定目录
getEventProject(e).getBaseDir().refresh(true, true, () ->
    Messages.showMessageDialog("目录刷新成功", "创建成功", Messages.getInformationIcon()));

如何访问资源文件

插件有两个核心的目录:

  • main/java:存放Java源代码,编译为插件后都被编译成立class文件
  • main/resources:存放资源,编译为插件后所有文件原封不动的保留了下来

所以,读取资源文件的含义其实就是读取jar包中的文件。jar包中的文件无法通过包名的方式读取,只能先通过流的方式将其拷贝到本地目录后,然后读取本地文件。

读取 jar 包中的文件

参考

核心代码:

InputStream fontStream = getClass().getResourceAsStream(copyFrom);

完整代码:

Project project = e.getData(PlatformDataKeys.PROJECT);
String res = "/icons/icon.png";
File copyToFile = new File(project.getBasePath(), res);
if (!copyToFile.getParentFile().exists()) {
    copyToFile.getParentFile().mkdirs(); //创建本地目录
}
copyFileToDisk(res, copyToFile.getAbsolutePath()); //将jar包中的文件复制到本地目录中
private void copyFileToDisk(String copyFrom, String copyTo) {
    try {
        InputStream fontStream = getClass().getResourceAsStream(copyFrom);
        FileOutputStream fileOutputStream = new FileOutputStream(copyTo);
        byte[] buffer = new byte[1024 * 10];
        int length;
        while ((length = fontStream.read(buffer)) > 0) {
            fileOutputStream.write(buffer, 0, length);
        }
        fileOutputStream.close();
        fontStream.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

执行 cmd 命令

参考

不只是cmd命令可以执行,bat脚本、git命令、shell命令都可以执行,但前提是要在PATH中配置环境变量。

除了用 Process 类外,还可以使用 ProcessBuilder 类,单个人感觉 ProcessBuilder 类并不能完全 cover 住 Process 类的功能。

方式一

通过 start 会启动一个命令行界面,能看到执行时打印的日志;没有 start 时命令也是正常执行了的,只不过没有任何日志提示。

命令的基本格式:

//其中【cd/d】用于切换目录,【start】用于启动一个命令行界面
"cmd /c cd/d " + path + " & start " + batFilePath/command + " " + params
String cmd = "cmd /c cd/d D:\\ & del 1.txt & del 2.txt";
String cmd2 = "cmd /c cd/d D:\\ & start del 3.txt";
Process process = Runtime.getRuntime().exec(cmd);
System.out.println(process.getInputStream());

例如:

  • cmd /c start dircmd /c start dir .:打印当前目录,例如D:\_dev\_code\idea\Test
  • cmd /c start dir D:\cmd /c cd/d D:\\ & start dir:打印D:\目录

注意:运行 bat 时的当前位置和 bat 文件存放的位置是不一样的!如果需要两者一致,需要使用cd/d命令切换目录。

方式二

这种情况下,如果没有 start,执行时的日志可以被我们收集起来。

private static String exec(String cmd) {
    StringBuilder sb = new StringBuilder();
    try {
        Process process = Runtime.getRuntime().exec(cmd);
        BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream(), "GBK"));
        String line = null;
        while ((line = br.readLine()) != null) {
            sb.append(line).append("\n");
        }
    } catch (Exception e) {
        sb.append(e.getMessage());
    }
    return sb.toString().trim();
}

2020-11-30

posted @ 2020-11-30 00:08  白乾涛  阅读(2324)  评论(1编辑  收藏  举报