從0到1寫一款自動為Markdown標題添加序號的Jetbrains插件

1. markdown-index

最近做了一個Jetbrains的插件,叫markdown-index,它的作用是為Markdown文檔的標題自動添加序號,效果如下:

markdown-index使用效果

目前已經可以在Jetbrains全家桶的插件市場中搜索到。

插件已上架

2. 為什麼我要做這個插件

我習慣用Markdown寫完文章之後給文章標題添加上序號,這樣讀者閱讀起來會更清晰,像這樣:

序號讓閱讀更清晰

之前我都是用Typora寫完文章之後,把文章複製到VSCode中,然後使用VSCode中的markdown-index插件給文章標題自動添加序號,然後再複製文章內容進行分發。

本來可以一直沿用這個方式,可是在我最近使用VuePress搭建了個人部落格之後,在部落格寫作這個方向上我慢慢偏向了WebStorm,原因有3個:

  1. 在本地調試的時候我更喜歡一鍵啟動,而不需要每次打開Terminal輸入npm run docs:dev命令;

  2. 我配置了git push之後的網站自動部署流,由於平時開發用慣了IDEA,因此WebStorm的git用戶介面讓我感覺更親切;

  3. VSCode的markdown-index插件使用儘管已經很方便了,但是還是稍微有點繁瑣,因為必須先Command+Shift+p調出command palette,然後選擇markdown-index功能。我想直接滑鼠右鍵直接選擇markdown-index功能。

綜合上面3點原因,我參考了VSCode的markdown-index插件,查閱文檔,花了一晚上寫了Jetbrains全家桶的markdown-index插件。

下面給大家介紹一下插件從0到1的編寫流程以及在查閱官方文檔時的一些心得體會。

3. 插件開發前奏

一開始圖省事兒,想直接根據網友的插件開發經驗來做,但發現要麼資料過時,要麼是跟著做了不成功,最後索性直接找官方文檔了。

因此這個小插件90%的時間都花在了閱讀官方文檔上了。

3.1. 官方文檔

我們一開始肯定不知道官方文檔的地址,想直接從Jetbrains門戶網站找到插件開發的官方文檔也很浪費時間。我提供兩種方案:

  1. 使用百度搜索,搜索「Jetbrains插件開發」之類的關鍵詞,找到網友之前分享的開發部落格,一般寫的詳細的部落格(可能需要多找幾篇)會給出官方地址,然後,拋棄這篇文章,投入官方文檔的懷抱吧。

  2. 使用Google搜索,搜索英文關鍵詞,比如「jetbrains plugin development」,一般第一條就是我們要找的結果,這也是我採取的方法(不得不感嘆一句,Google搜索英文資料真的是好~)。

現在官方網站就到手了://plugins.jetbrains.com/docs/intellij/getting-started.html

官方文檔一般情況下寫得都非常詳細,尤其是摻雜著各種超鏈接。大家在讀官方文檔的時候如果不是十分清楚超鏈接的含義,盡量不要點,否則跳來跳去很容易把心態搞崩。

3.2. 開發插件的3種方式

官方說明了開發插件的三種方式,分別是:

  1. 使用官方發布在GitHub上的插件模板(Using GitHub Template)

  2. 使用Gradle(Using Gradle)

  3. 使用DevKit(Using DevKit)

我選擇的是第一種,原因是我之前從來沒有接觸過Jetbrains插件的開發,如果從白板開始寫起的話太麻煩了,使用官方提供的模板進行填空是最快的方式。

3.3. 使用IntelliJ Platform Plugin Template

官方的模板倉庫的地址://github.com/JetBrains/intellij-platform-plugin-template

官方解釋說這個倉庫預設了項目的腳手架和CI流程,乾淨又衛生!不管是新手還是老手,都能加快插件開發流程。

需要做的就是三個步驟:

  1. 登陸你的GitHub帳號
  2. 點擊倉庫的Use this template按鈕

use-this-template

  1. 用你的IDEA打開它

然後我們下來的參考文檔就是這個倉庫的README說明了。

3.4. 項目大致結構

項目結構

首先給大家介紹一下項目結構:

  • .github

裡面配置了GitHub Actions的工作流,具體來說就是我們自動將插件提交到GitHub之後,GitHub會根據這個工作流為我們自動做一些我們配置的事情,比如安裝依賴,比如發布到Jetbrain官方插件庫等,默認不需要更改。

  • .run

預設了一些Gradle的配置,使得我們可以在IDEA中直接滑鼠點擊執行指令,看下面這個圖就懂了.

沒用過Gradle也沒事兒,不影響我們寫核心邏輯

  • build

存放編譯之後的文件

  • src

我們的核心程式碼位置

  • 其他

其餘都是Gradle的配置文件和其他工具的配置文件,暫時不需要理會,需要的時候再說。

由於項目默認使用Kotlin,我不習慣,我換成了Java,方法很簡單,在src/main下面新建java目錄,把kotlin的所有目錄移動到java目錄即可,刪掉目錄下的Kotlin源文件,src/test同理。

3.5. plugin配置文件

還有一個文件需要單獨拿出來說一下,位於src/main/resources/META-INF目錄下的plugin.xml文件。

插件的extensionsactions以及listeners都在該文件中進行配置。

這些東西都是個啥先不用管,就是個配置而已,能難到哪去。之後敲程式碼的時候就知道了,先混個眼熟吧。

<idea-plugin>
  <id>org.jetbrains.plugins.template</id>
  <name>Template</name>
  <vendor>JetBrains</vendor>
  <depends>com.intellij.modules.platform</depends>

  <extensions defaultExtensionNs="com.intellij">
    <applicationService serviceImplementation="..."/>
    <projectService serviceImplementation="..."/>
  </extensions>

  <projectListeners>
    <listener class="..." topic="..."/>
  </projectListeners>
</idea-plugin>

而且,README文檔里也說了,更多的詳細配置可以查看配置文檔,鏈接為:

//plugins.jetbrains.com/docs/intellij/plugin-configuration-file.html?from=IJPluginTemplate。

4. 插件開發過程

4.1. 參考示例程式碼

現在我們對這個模板項目已經有了直觀的感覺了,下面開始寫程式碼了,是不是腦子裡還是空空如也,因為有幾件事情我們目前壓根不知道。

我應該在哪個目錄下寫Java程式碼?寫完之後怎麼調用?調用完了之後怎麼和IDEA聯動?聯動肯定需要知道IDEA提供的api,去哪兒找?

我當時想的就是這幾個問題,所以我的第一反應是:作為一個成熟的軟體開發商,應該會提供實例程式碼給我們,我們就能參考了。

於是接著讀README,還真就給出了一個示例程式碼倉庫,地址為:

//github.com/JetBrains/intellij-sdk-code-samples

進入一看,示例太多了。。。。於是根據我的需求,我就找了一個名字最相關的,看起來也最簡單的項目——editor_basics

這其實是一個試錯的過程,建議一開始看個簡單的示例,不需要看懂實際程式碼,我們的目的是要從例子中找到我們下一步需要了解的概念。

研究了一小會兒之後,我發現我需要了解2個概念。

4.2. Actions

Actions中文的意思是「動作」,舉幾個例子:

  1. 菜單中的File | Open File…按鈕,點擊後觸發打開本文文件資源管理器的動作;
  2. 滑鼠右鍵菜單中Paste按鈕點擊之後觸發粘貼的動作;
  3. Command + C快捷操作之後,觸發複製的動作;

這3個例子說明了幾點重要細節,首先Action可以出現在IDE的不同地方,至於出現在哪裡,取決於你的註冊過程;Action可以有不同的行為,具體的行為是什麼取決於你的實現;最後不管是滑鼠點擊還是快捷鍵組合都能觸發Action

4.2.1. 註冊

src目錄下,創建一個actions目錄(其實創不創建不重要,但是我喜歡這種清晰的組織方式),目錄上滑鼠右鍵,選擇右鍵菜單中的New | Plugin Devkit | Action(如果你滑鼠右鍵沒有這個按鈕,那就安裝一個Plugin DevKit這個插件),進入New Action介面。

New Action操作步驟

New Action介面

需要注意的是

  • Name:就是在菜單中實際顯示的名稱
  • Anchor:菜單中顯示的次序,First指排在第一位,Last指排在最後一位

4.2.2. Actions Groups

Action默認是按照Group進行組織的,選擇某個Group就意味著要把你的Action放在XX菜單中或者XX工具欄上,這裡我選擇的是EditorPopupMenu,意思就是編輯器上的右鍵彈出菜單

我是怎麼找到的呢?

因為我的功能需求比較簡單,我看了一下Group的大致命名方式,我就嘗試性的搜索了一下PopupMenu,由於針對的是編輯器,於是最後找到了EditorPopupMenu,多少有點運氣成分,如果各位讀者的需求更獨特的話就需要多試幾次或者閱讀官方文檔嘍。

4.2.3. 實現Action

填寫完New Action表單之後,再看一下plugin.xml文件,會發現多了一個配置:

並且actions目錄下多了一個PopAction的源文件,在actionPerformed中需要我們寫的就是Action的實現。

package com.github.chanmufeng.tesplugin.actions;

import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;

public class PopAction extends AnAction {

    @Override
    public void actionPerformed(AnActionEvent e) {
        // TODO: insert action logic here
    }
}

4.2.4. 測試一下插件

先不著急實現,我們先試一下Action註冊的效果。選擇Run Plugin命令,點擊運行

8

此時你會看到又彈出了一個IDEA!

沒錯,這個就是插件的測試環境,使用方法和正常的IDEA沒有任何區別,只不過這個環境下默認安裝了我們剛才編寫的插件。

接下來新建或者打開一個已有項目,點擊一下滑鼠右鍵看一下「markdown-index」這個Action是否註冊成功。

4.2.5. 實現Action

接下來做的就是實現actionPerformed(AnActionEvent e)方法,毫無疑問,我們所需的一切數據都是從e這個對象中獲取了。

目標非常的清晰:

  1. e中獲取到當前文件的所有行數據
  2. 根據行前的#數量遞歸添加標號
  3. 用添加標號之後的文本替換掉原來的文本

那調用的API不知道啊,怎麼辦?我的辦法就是利用IDEA出色的提示功能以及源碼的注釋。

比如我想獲取當前所在的文件,那我肯定會先敲e.get,然後等著提示:

image-20220827161401837

我發現第一個就很像,我就選了,可是讓我傳DataKey類型的參數,我不知道該怎麼傳,我就點進去,看看注釋,發現了新大陸:

繼續往下推,就獲得了所有我想獲得的對象,如果這招對你行不通,那就去看官方文檔或者上文提到的示例程式碼,肯定有一個適合你。

插件的核心功能到此為止其實已經結束了,但是我當時又稍微折騰了一下。

4.3. Services

有程式碼潔癖的人肯定受不了把所有程式碼寫在一個方法里,至少封裝一下方法吧。還記得一開始項目模板為我們提供了一個services目錄嗎,我當時就猜測這個目錄就是專門放我們編寫的服務的,對於大型插件來說這是必須的。於是我又簡單翻了一下官方文檔。

發現我真是個小天才!Services確實是干這個的,而且跟Spring Bean的使用方法非常類似。

4.3.1. 分類

Services分類如下:

  • 重量級Service
    • application-level services(Application級別的Service)
    • project-level services(Project級別的Service)
    • module-level services(Module級別的Service,在多模組項目下不建議使用)
  • 輕量級Service

先說說重量級Service,分成了三個級別,目的是為了控制不同粒度下的數據許可權。

Application級別的Service全局只有一個訪問點,也就是說IDEA不管打開幾個項目,Service的實例對象只有一個。

Project級別的Service在每個項目下只有一個訪問點,如果IDEA打開了3個項目,就會生成3個實例。

Module級別的Service在每個模組下都會有一個訪問點。

4.3.2. 重量級Service的使用場景

重量級Service適合比較規整的項目,比如嚴格定義XXServiceInterface並且有一個或多個實現類XXXServiceImplementation

重量級Service必須在plugin.xml中進行註冊,在xml標籤中直接定義Service的作用範圍,如下:

<extensions defaultExtensionNs="com.intellij">
  <!-- Declare the application-level service -->
  <applicationService
      serviceInterface="mypackage.MyApplicationService"
      serviceImplementation="mypackage.MyApplicationServiceImpl"/>

  <!-- Declare the project-level service -->
  <projectService
      serviceInterface="mypackage.MyProjectService"
      serviceImplementation="mypackage.MyProjectServiceImpl"/>
</extensions>

4.3.3. 輕量級Service的使用場景

沒那麼多苛刻條件,不需要繼承關係,就比如我這個插件,我只是想讓某些方法抽離出來而已,沒必要搞的繼承這麼複雜。因此我選用的也是該類Service。

輕量級Service不需要在plugin.xml文件中註冊,但是該類Service必須被final修飾,並在類頭部添加@Service註解。舉個例子:

@Service
public final class ProjectService {

  private final Project myProject;

  public ProjectService(Project project) {
    myProject = project;
  }

  public void someServiceMethod(String parameter) {
    AnotherService anotherService = myProject.getService(AnotherService.class);
    String result = anotherService.anotherServiceMethod(parameter, false);
    // do some more stuff
  }
}

4.3.4. 如何獲取Service實例

重量級Service就不說了。有需要的朋友直接看文檔,非常清晰。

//plugins.jetbrains.com/docs/intellij/plugin-services.html#retrieving-a-service

輕量級Service直接用本插件的程式碼來做演示:

 // 獲取自己編寫的MarkdownIndexService
 MarkdownIndexService markdownIndexService =
   ApplicationManager.getApplication().getService(MarkdownIndexService.class);

輕量級Service實例的生命周期範圍和調用者保持一致,以上面為例,我用的getApplication().getService,那麼MarkdownIndexService的作用範圍就是Application

5. Listeners

簡單提一句Linsteners,在這個插件里沒有使用到,從名字上很好理解,就是監聽器,想想就知道肯定有個回調函數,你可以在其中捕獲到某些IDEA的操作行為,然後添加自己的邏輯。

是不是很簡單?

6. 插件發布

插件寫完了,接下來我們發布到plugin repository,讓更多的人看到我們的插件。

6.1. 修改插件圖標

使用你鐘意的圖標替換掉src/main/resources/META-INF目錄下的pluginIcon.svg文件即可。

6.2. 發布插件

首先你需要登陸Jetbrains帳號,如果沒有的話就註冊一個吧,註冊地址給上。

//plugins.jetbrains.com/author/me

然後在右上角點擊帳號名稱,選擇Upload plugin,最後上傳你的插件jar包,並填寫表單即可。

7. 源碼分享


完~