AS 自定义插件 总结
目录
IDEA 插件开发
IDEA 的插件几乎可以做任何事情,因为它把 IDE 本身的能力都封装好开放出来了。主要的插件功能包含以下四种::
Custom language support
:自定义编程语言的支持。包括语法高亮、文件类型识别、代码格式化、代码查看和自动补全等等;参考 Custom Language Support TutorialFramework integration
:框架集成。基于 IntelliJ 开发一个 IDE,比如 AndroidStudio 将 Android SDK 集成进 IntelliJTool integration
:工具集成。对 IntelliJ 定制一些个性化或者是实用的工具,这种插件是最多的,也是我们开发插件的主要目的User interface add-ons
:附加UI。对标准的UI界面进行修改,如在编辑框里加一个背景图片等;参考 BackgroundImage
第一个 IDEA 插件
在开发 IntelliJ 插件时,我们使用的是 IntelliJ IDEA
(旗舰版、社区版都可以) 自身来开发。为什么不用 AS 呢?因为 AS 没有针对插件的各种环境,当然,你也可以自己下载插件然后在 AS 上去配置,但是过于麻烦。
开发插件的工具叫
IntelliJ Platform Plugin SDK
或IntelliJ 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
:插件名称,例如CodeGlancevendor
:作者主站网址和邮箱配置,便于用户有疑问时联系你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
的版本) - 根据你的版本号,直接用迅雷下载以下文件,我这边瞬间就下载完成了:
- ideaIC-2020.2.4.zip:对应 IDEA
2020.3
版本 - ideaIC-201.8743.12.zip:对应 AS
4.1.1
版本
- ideaIC-2020.2.4.zip:对应 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
- Android Studio Plugin Development
- Modules Specific to Functionality
- Plugin Dependencies
- Plugin Compatibility with IntelliJ Platform Products
- Write an Android Studio Plugin
如果我们不做任何特殊配置,那么上面生成的插件在 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-build
和until-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
两个常见的线程调度类:
- 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);
}
}
拓展
自定义菜单
- 内置的 Action ID
- 图标规范:必须为
.png
格式,大小为16x16
.
案例
<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 属性
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 dir
或cmd /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
本文来自博客园,作者:白乾涛,转载请注明原文链接:https://www.cnblogs.com/baiqiantao/p/14059062.html