從0到1寫一款自動為Markdown標題新增序號的Jetbrains外掛

2022-08-29 12:01:02

1. markdown-index

最近做了一個Jetbrains的外掛,叫markdown-index,它的作用是為Markdown檔案的標題自動新增序號,效果如下:

目前已經可以在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搜尋英文資料真的是好~)。

現在官方網站就到手了:https://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

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

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

需要做的就是三個步驟:

  1. 登陸你的GitHub賬號
  2. 點選倉庫的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檔案裡也說了,更多的詳細設定可以檢視設定檔案,連結為:

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

4. 外掛開發過程

4.1. 參考範例程式碼

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

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

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

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

https://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介面。

需要注意的是

  • 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命令,點選執行

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

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

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

4.2.5. 實現Action

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

目標非常的清晰:

  1. e中獲取到當前檔案的所有行資料
  2. 根據行前的#數量遞迴新增標號
  3. 用新增標號之後的文字替換掉原來的文字

那呼叫的API不知道啊,怎麼辦?我的辦法就是利用IDEA出色的提示功能以及原始碼的註釋。

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

我發現第一個就很像,我就選了,可是讓我傳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就不說了。有需要的朋友直接看檔案,非常清晰。

https://plugins.jetbrains.com/docs/intellij/plugin-services.html

輕量級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賬號,如果沒有的話就註冊一個吧,註冊地址給上。

https://plugins.jetbrains.com/author/me

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

7. 原始碼分享


完~