| 我的GitHub | 我的博客 | 我的微信 | 我的郵箱 |
|---|---|---|---|
| baiqiantao | baiqiantao | bqt20094 | baiqiantao@sina.com |
目錄
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\Testcmd /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
