AndroidStudio 插件開發記

開發環境

  • 系統: Windows 10
  • 工具: IntelliJ IDEA 2019.2.1 Community Edition
  • SDK: Java 8, Kotlin 1.3.41
  • AndroidStudio: Android Studio 3.5

blog 和 github 的鏈接, 查看完整文章

Github

dengzii's blog

官方文檔

http://www.jetbrains.org/intellij/sdk/docs/reference_guide

我開發的插件

插件的大致分類

語言支持

For example, Gradle, Scala, Groovy, 這些插件 IDEA, AndroidStudio 都是自帶有的, 所以我們才能在編寫這些語言代碼的時候有語法高亮檢測.

語言插件主要的一些功能是 文件類型識別, 語法高亮檢測, 格式化, 語法提示等等.

框架集成

AndroidStudio 就是一個例子, 他集成了 AndroidSDK 的一系列功能, 比如, 資源文件識別組織與提示, 集成 Gradle, Debug, ADB, 打包APK, 讓我們可以更好的開發 Android 應用. 類似的插件還有 Gradle, Maven, Spring Plugin等.

框架集成插件的主要功能是某種語言特定代碼的識別, 直接訪問特定框架的功能

工具集成

例如, 翻譯工具, Markdown View, WiFi ADB等.

UI增強

對 IDE 的主題顏色樣式做一些更改, 比如 MaterialTheme.

創建一個項目

兩種工具開發插件項目

Gradle

本著緊隨時代潮流的想法, 剛開始我是用 Gradle 構建項目的, 但是, 我在 IDEA Community 2019.2.1 版本中, 無論如何都無法成功, Gradle 提示找不到 PsiJavaFile 這些類, 但項目中是可以引用的. 我嘗試了換 Gradle版本, 5.4, 4.5.1 兩個版本, 把 jar 包移到 libs 中依賴, 均以失敗告終, 如果有人可以編譯運行, 請千萬要告訴我.

DevKit

創建項目:

New Project => IntelliJ Platform Plugin => Input Project Name => Finish

配置項目:

File => Project Structure
            Project => Project SDK => IntelliJ IDEA Community Edition IC-xxx
            Module => Select your module => Tab Dependencies => Module SDK => Project

創建完畢后, 你的目錄結構應如下

resource/
    META-INF/
        plugin.xml  // plugin config file
src/    // source code directory

plugin.xml

<idea-plugin url="https://www.your_plugin_home_page.com">   

    <name>Your plugin name</name>

    <id>com.your_domain.plugin_name</id>

    <depends>com.intellij.modules.all</depends>
    <!-- kotlin support -->     
    <depends>org.jetbrains.kotlin</depends>
    
    <description>Your will see it at plugin download page</description>

    <change-notes>What's update</change-notes>

    <version>1.0.0</version>
</idea-plugin>

如果你的插件需要支持 kotlin, 則必須添加這個依賴

<depends>org.jetbrains.kotlin</depends>

準備工作

線程規則

在 IntelliJ IDEA 平臺中, 分為 UI 線程和后臺線程, 這點和 Android 開發類似, 不同的是,

取操作可以在任何線程進行, 但在其他線程中讀取需要使用 ApplicationManager.getApplication().runReadAction() 或者 ReadAction.run/compute 方法

操作只允許在 UI 線程進行, 必須使用 ApplicationManager.getApplication().runWriteAction()WriteAction.run/compute 進行寫操作

為了保證線程安全, 我們必須這樣做

什么是 PSI

PSI 是 Program Structure Interface 的縮寫, 它定義了如何描述一種語言. 通過 AnActionEvent#getData(LangDataKeys.PSI_FILE) 獲取當前文件的 PsiFile 對象.

每一種語言都有對應的 PsiFile 接口, 在插件開發模式下, 我們可以通過 Tools => View PSI Structure 查看一個文件的 PSI 結構, 他可以幫我們快速了解一種語言的 PSI 接口定義, 如果想開發解析某種語言的插件, 需要在項目中引入相應的 SDK.

Kotlin 類對應的 PSI 接口是 KtClass, 文件對應的是 KtFile

Java 類對應的 PSI 接口是 PsiClass, 文件對應的是 PsiJavaFile

一個源碼文件的所有的元素都是 PsiElement 的子類, 包括 PsiFile, 比如在 Java 源碼文件 PsiJavaFile 中 , 關鍵詞 private, public 對應的 PsiElement 是 PsiKeyword. 通過PsiElement#acceptChild 方法可以遍歷一個 element的所有子元素. 通過 PsiElement 的 add, delete, replace 等方法, 可以輕松的操作 PsiElement

創建一個用于 Java 的 PsiElement

PsiElementFactory factory = JavaPsiFacade.getElementFactory(project);
PsiField = factory.createFieldFromText("private String str = \"Hello\";", null);

創建一個用于 Kotlin 的 KtElement

KtPsiFactory ktPsiFactory = KtPsiFactoryKt.KtPsiFactory(project);
KtProperty ktProperty = ktPsiFactory.createProperty("private var str = \"Hello\"");

通過這兩個工廠類可以創建所有的 PSI 元素, 當然我們也可以通過 new 實例化各種元素, 然后通過 add 關聯在一起, 但這樣相對比較麻煩.

什么是 VFS

VFS 是 Virtual File System 的縮寫, 它封裝了大部分對活動文件的操作, 它提供了一個處理文件通用 API, 可以追蹤文件變化

WriteCommandAction 操作 PSI

當我們使用 PsiElement#add(PsiElement e) 方法操作文件的時候需要用到這個類, WriteCommandAction#writeCommandAction(ThrowableRunnable t) 方法傳入一個 Runnable.

如何編寫用戶界面

我們可以選擇 UI Designer, 或者自己手動敲. UI Designer 可以可視化編寫界面, 直觀, 在包目錄上右鍵菜單 new 即可看到. 這個和 Swing 編程一毛一樣. JetBrains 提供了它自己封裝的一系列控件, 一般以 JB 開頭, 比如 JBLabel, JBPanel, 有些特定的功能和統一的風格.

技巧和注意事項

  1. 插件開發, 我們需要使用 Community 版本的 IDEA, 否則無法調試源碼

  2. 如果沒有發現 DevKit, 可能是該插件沒有啟用, 在 File > Settings > Plugins 中啟用即可

  3. 為了便于開發, 我們可以配置 IDEA 的源碼, 在 https://github.com/JetBrains/intellij-community/ 倉庫中下載與你 IDEA build 版本一支的源碼, 然后添加到 ProjectStructure > SDKs > IntelliJ IDEA Community Edition IC xxx > Sourcepath

  4. 多個插件開發配置不同環境, 配置 SandBox ProjectStructure > SDKs > IntelliJ IDEA Community Edition IC xxx > Sandbox Home

  5. 導入插件項目不能直接 File> Open, 而應該 File > New > Project From Existing Soruces...

  6. Help > Edit Custom Properties 中 添加 idea.is.internal=true 并重啟, 可以啟用 Tools > Internal Actions, 這里有許多好用的插件開發調試工具.

  7. 工具 PSI Viewer Tools > View PSI Structure... 可以讓我們快速了解到一個文件的 PSI 結構

Action 的使用

Action 顧名思義就是動作, 用戶可以通過按下一個快捷鍵或點擊菜單選項觸發.

定義

Action 定義了用戶的一個動作, 快捷鍵, 我們創建一個 Action 需要一個類繼承 AnAction, 并重寫 actionPerformed(AnActionEvent anActionEvent) 方法, 之后在 plugin.xml 中注冊該 Action.

基本上我們常用的數據上下文信息都可以在 anActionEvent 中獲取, 例如光標: PlatformDataKeys.Carte, 獲取當前語言 LangDataKeys.LANGUAGE.

例子, 定義一個 Action, 打印項目名, 路徑, 及正在編輯的文件名

public class MainAction extends AnAction {
    @Override
    public void actionPerformed(@NotNull AnActionEvent anActionEvent) {
        Project project = anActionEvent.getProject();
        PsiFile psiFile = anActionEvent.getData(LangDataKeys.PSI_FILE);
        System.out.println("Project Name:" + project.getName());
        System.out.println("Project Path" + project.getProjectFilePath());
        System.out.println("Editor File Name:" + psiFile.getName());
    }
}

注冊

在 plugin.xml 中注冊該 action, 所有的 Action 都定義在 <actions></actions> 中

<actions>
    <action id="your_id_usually_is_doaim_and_action_name" class="com.your_domain.MainAction"
            text="This is action name"
            description="This is description" keymap="$default">
        <add-to-group group-id="ToolsMenu" anchor="first"/> 
        <keyboard-shortcut first-keystroke="alt G" keymap="$default"/>
    </action>
</actions>

group-id 定義了該 Action 出現的位置, 這里是在菜單 Tools 的第一個位置, first-keystroke 為快捷鍵, 組合鍵用空格分開, 比如 "ctrl shift alt G".

我們在 Tools 第一個選項即可看到 "This is action name" 這個選項, 點擊或按快捷鍵即可出發該 Action.

控制Action的隱藏顯示

在一些情況, Action 在當前情況可能不可用, Action 是需要隱藏的, 比如, Generate=>toString 這個 Action 在編輯 xml 文件時就不適用, 需要隱藏, 重寫 AnAction#update即可達到這個目的.

public class ToStringAction extends AnAction {
    private static final LANG_XML = Language.findLanguageByID("XML");
    @Override
    public void update(@NotNull final AnActionEvent e) {
        Project project = e.getProject();
        PsiFile psiFile = e.getData(CommonDataKeys.PSI_FILE);
        
        e.getPresentation().setEnabledAndVisible(true);
        
        if(project == null || psiFile == null || !psiFile.getLanguage().is(LANG_XML)){
            e.getPresentation().setEnabledAndVisible(false);
        }
    }
}

以上代碼可以實現沒有打開 project, 沒有打開文件或 語言不是 xml 時隱藏 ToStringAction.

Editor

Editor 接口定義了對當前編輯器的一系列讀寫操作接口.

獲取 Editor

@Override
public void actionPerformed(@NotNull AnActionEvent anActionEvent) {
    Editor editor = anActionEvent.getData(PlatformDataKeys.EDITOR);
}

獲取當前選擇的文本

SelectionModel selection = editor.getSelectionModel()
if(selection != null){
    String text = selection.getSelectedText(true);
}

Editor 可以獲取一下8種 Model

  • CaretModel 光標相關的 Model
  • FoldingModel 折疊段落 Model
  • IndentsModel 縮進 Model
  • ScrollingModel 滾動 Model
  • SoftWrapModel 自動換行 Model
  • MarkupModel 標記,高亮 Model
  • InlayModel 嵌套 Model
  • SelectionModel 選擇 Model

在獲取相關 Model 時需要檢查是否為空, 比如沒有光標的時候, getCarteModel 將返回空. 針對我們要進行的不同操作獲取不同的 Model.

組件

ToolWindow

ToolWindow 就是底部 Logcat, Event Log 依附在左右兩側或底部的窗口, 可以最小化成一個按鈕, 或展開, 改變大小和位置關閉.
在菜單欄中 View => Tool Window 列表中可以看到當前所有的 ToolWindow.

定義一個 ToolWindow, 顯示當前項目名, 包上點擊右鍵 new => Swing Ui Designer => GUI Form => TestToolWindow

點擊 TestToolWindow.form 編輯界面, 添加一個 JLabel, 然后編輯 TestToolWindow, 讓他實現 ToolWindowFactory 接口.

public class TestToolWindow implements ToolWindowFactory {

    private JPanel rootPanel;
    private JLabel label1;

    public JPanel getContent() {
        return rootPanel;
    }

    @Override
    public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
        ContentFactory contentFactory = ContentFactory.SERVICE.getInstance();
        Content content = contentFactory.createContent(getContent(), "TestToolWindow", false);
        toolWindow.getContentManager().addContent(content);
    }
}

在 plugin.xml 中注冊, ToolWindow 需要放在 extensions 標簽中.

<extensions defaultExtensionNs="com.intellij">
    <toolWindow id="TestToolWindow"
                canCloseContents="false"
                factoryClass="com.your_domain.TestToolWindow"
                anchor="bottom"/>
</extensions>

其中, id 是 ToolWindow 的標題, canCloseContents 設置是否可以關閉, factoryClass 就是實現了 ToolWindowFactory 的該 ToolWindow 的工廠類. anchor 為顯示位置

在 Action 中添加以下代碼, 觸發該 Action, ToolWindow 就彈出了并顯示了項目的名稱.

public void actionPerformed(@NotNull AnActionEvent anActionEvent) {

    ToolWindow toolWindow = ToolWindowManager.getInstance(project).getToolWindow("TestToolWindow");
    toolWindow.show(new Runnable() {
        @Override
        public void run() {}
    });
    JTextField field = (JLabel) toolWindow.getContentManager()
            .getContent(0).getComponent().getComponent(0);
    if (field!=null){
        field.setText(project.getName());
    }
}

Dialog

IntelliJ SDK 中有一個 DialogWrap, 用這個可以與 IDEA 保持一致風格, 但是用這個就無法使用 GUI Designer 了. 它的使用方法與 Swing 中的 Dialog 差別不大.

一般情況, 我們開發的 plugin 都需要一個或若干個 Dialog.

持久化

PropertiesComponent 提供了數據持久化的接口, 他是一個單例, 通過 getInstance() 方法我們可以獲取一個 Application 級的持久化實例, 他在所有的 Project 中都生效, 而 使用 PropertiesComponent.getInstance(Porject) 則只針對當前 Project 生效.

(完)

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容