AS 自定義插件 總結 [MD]


博文地址

我的GitHub 我的博客 我的微信 我的郵箱
baiqiantao baiqiantao bqt20094 baiqiantao@sina.com

目錄

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM