使用Kotlin來開發IntelliJ plugin — Part 2

Freddie Wang
9 min readSep 16, 2018

--

在 part1 中已經介紹了基本 plugin的架構,接下來介紹一些實作細節。

在 part2裡面會以我自己寫的 plugin (https://github.com/wangyung/intellij-plugin-java-cfr)為範例。此 plugin 主要功能是使用 CFR (http://www.benf.org/other/cfr/) 來 decompile class 並且顯示在新的 tab 裡面。細分的話會需要以下幾個小功能

  • 設定 decompiler 的路徑
  • 尋找 class 檔案的位置
  • 啟動 decompiler
  • 獲取 decompile 的結果並且顯示到新的 tab 上

接下來就一一說明各部分的實作方式

設定 Decompiler 路徑

為了要設定 decompiler 的路徑,必須在 Setting 裡面加上簡單的 UI 來輸入檔案路徑,UI 大概會是長這樣

而實做這個 UI 有幾種方式,這裡推薦使用 Intellij自帶的 GUI Designer (https://www.jetbrains.com/help/idea/gui-designer-basics.html),具體使用方式可以參照官方說明。

UI 拉好之後 GUI Designer 就會產生出一個對應的 java 檔案,並且產生出一些 function,其中一個是 $$$getRootComponent$$$(),透過這個 function 就可以取得最上層的 JComponent了。如果有必要的話可以自行修改此 java 檔案,但是建議不要更改 GUI Designer 自動產生的程式碼。

接著把 UI 跟 Configurable 物件串起來,overwrite 這個 function就可以了。

override fun createComponent(): JComponent? {
return DecompilerConfigurationView.$$$getRootComponent$$$()
}

如此一來開啟 Settings 的時候,在 plugin 的選項裡就會有這 UI。

有了 UI 之後該怎麼儲存設定呢? 在 Intellij SDK 有提供 PropertiesComponent 來方便儲存設定 (讀取亦同)。所以只要override apply() 這個方法,就可以在使用者按下 Apply 按鈕的時候儲存設定了。

override fun apply() {
PropertiesComponent.getInstance().setValue(KEY, theValue)
}

這樣基本的 Setting 功能就完成了。

尋找 class 檔案的位置

對於 Decompiler 來說,input的資料是 class 檔案,但是該如何找到相對應的 class 檔案呢? 首先必須要先了解一下 IntelliJ 的幾個重要的 Components。

  • Project
  • Virtual File
  • Editor
  • Document

Project

顧名思義就是代表整個 project 的 component,包含了所有的原始碼、函式庫等等資訊

Virtual File

在 Intellij 有一個Virtual File System,這是對所有檔案操作的封裝,細節可以參考https://www.jetbrains.org/intellij/sdk/docs/basics/virtual_file_system.html

Editor

Intellij IDE 裡面的編輯器元件,透過 editor可以直接控制游標位置,編輯中檔案的內容等等

Document

表示一段可以編輯的 Unicode 文字

了解這幾個 Component 之後大概也就可以知道該如何找出 class 了。簡單的講就是拿到從現在編輯中的文件的檔案名稱,如果是 java 或者 kt 副檔名的檔案就從 project 底下找到同樣檔名的 class檔案,並把此檔案餵給 Decompiler。實作方法如下

fun getVirtualClassFile(currentDoc: Document, project: Project): VirtualFile? {
val currentSrc = FileDocumentManager.getInstance().getFile(currentDoc)
currentSrc?.let { srcFile ->
val currentClassFileName = sourceRegex.replace(srcFile.name, EXTENTION_NAME_CLASS)
if (!currentClassFileName.endsWith(EXTENTION_NAME_CLASS)) {
return@let
}

if (project.name != currentProjectName) {
possibleClassRoots = EMPTY_FILE_LIST
currentProjectName = project.name
}

if (possibleClassRoots.isEmpty()) {
possibleClassRoots = project.basePath?.let { basePath ->
File(basePath).walkTopDown()
.filter { file -> file.isDirectory && (file.name == "build" || file.name == "out") }
.toList()
} ?: EMPTY_FILE_LIST
}

possibleClassRoots.forEach { file ->
file.walkTopDown().find { classFile ->
compareClassFileName(classFile.name, currentClassFileName)
}?.run {
val targetFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(this)
if (targetFile == null) {
possibleClassRoots = EMPTY_FILE_LIST
}
return targetFile
}
}
}

return null
}

在這用到兩個 IntelliJ 提供的API,FileDocumentManagerLocalFileSystem 。用 FileDocumentManager 來找到編輯中的 VirtualFile,更換副檔名為 class,產生新的 Java File,最後再用這 Java File 來找到目標的 class 檔案。

啟動 Decompiler與顯示結果

找到了 class 檔案之後,接著就需要啟動 Decompiler 來反編譯這個 class 檔案。之前有提到怎麼儲存 Decompiler 的路徑,相反地也可以用同樣的方式來讀取 Decompiler 的路徑。

PropertiesComponent.getInstance().getValue(KEY)

有了路徑之後就可以透過 IntelliJ 提供的 API 來建立一個 GeneralCommandLine物件,簡單表示如下

GeneralCommandLine(listOf(javaExePath, "-jar", decompilerPath, targetPath))

有了這個 GeneralCommandLine 物件之後就可以利用ExecUtil.execAndGetOutput 得到 Subprocess的output了。如下所示

val generalCommandLine = GeneralCommandLine(listOf(javaExePath, "-jar", decompilerPath, targetPath))
val output = ExecUtil.execAndGetOutput(generalCommandLine)
if (output.exitCode == 0) {
writeDecompileResult(outputFilePath, output, project)
} else {
outputError(output.stderr, project)
}

最後把拿到的 output 顯示到新的 tab 上面。這需要使用 IntelliJ的 API FileEditorManager.openFile()。所以先要把 output 的資料寫到一個暫存檔中,再讓 FileEditorManager 來開啟這個暫存檔。

val tempFile = File(outputFilePath).also {
it.writeText(output.stdout, Charsets.UTF_8)
}
LocalFileSystem.getInstance().refreshAndFindFileByIoFile(tempFile)?.let { FileEditorManager.getInstance(project).openFile(it, true) }

這樣基本上就完成了一個簡單的 plugin了。

安裝 Plugin

最後一步就是安裝 plugin 了,這非常簡單只要執行 gradlew build 之後,plugin 就會出現在 build/distributions 底下,把裡面的 zip 檔拉到 Intellij 裡面就可以了。

這樣就完成了一個簡單的 plugin,至於其他的一些實作細節可以直接參考 github上的 source code (https://github.com/wangyung/intellij-plugin-java-cfr) 或是參考 Jetbrains 的官方文件了 (http://www.jetbrains.org/intellij/sdk/docs/welcome.html)。

--

--

Freddie Wang

I’m a software engineer from Taiwan and living in Japan now.