系列文章
本系列文章已收錄到專欄,交流群號(hào):689220994,也可點(diǎn)擊鏈接加入。
前言
所謂 PSI(Program Structure Interface),直譯過(guò)來(lái)是程序結(jié)構(gòu)接口,其實(shí)就是 IntelliJ 平臺(tái)給我們提供用來(lái)解析代碼文件,簡(jiǎn)化對(duì)各類編程語(yǔ)言(Java、Kotlin、XML)操作的接口。大部分針對(duì)編程語(yǔ)言或者框架的便利插件其實(shí)就與此相關(guān),本文則會(huì)先介紹關(guān)于 PSI 的一些基礎(chǔ)知識(shí),然后再以一些 Mybatis 插件提供的 Java 方法 和 Mapper XML 文件互相跳轉(zhuǎn)的例子來(lái)說(shuō)明 PSI 的實(shí)際應(yīng)用,最終實(shí)現(xiàn)效果如下圖,本文涉及的到的完整代碼文件也已上傳到GitHub。
PSI file(PSI 文件)
在本系列的第五篇文章中介紹了Virtual Files 和 Documents 用于處理文件的 API,而 PSI file 也是用于處理文件的 API,不過(guò)也有一些不同,具體如下:
類別 | 層面 | 范圍 |
---|---|---|
VF、Document | 文本文件 | 應(yīng)用級(jí) |
PSI | 編程語(yǔ)言語(yǔ)法樹 | 項(xiàng)目級(jí) |
獲取文件的 PSI file 對(duì)象的方式主要有以下幾種(來(lái)自官網(wǎng)):
Context | API |
---|---|
Action | AnActionEvent.getData(CommonDataKeys.PSI_FILE) |
Document | PsiDocumentManager.getPsiFile() |
PSI Element | PsiElement.getContainingFile() |
Virtual File |
PsiManager.findFile() , PsiUtilCore.toPsiFiles()
|
File Name | FilenameIndex.getVirtualFilesByName() |
最后一種方式
FilenameIndex.getVirtualFilesByName()
得到的結(jié)果是Virtual File 對(duì)象,需要再通過(guò)倒數(shù)第二行的方式再獲取到對(duì)應(yīng)的 PIS file 對(duì)象。
不過(guò)通過(guò)以上方式獲取到的 PsiFile 只是頂層的接口,針對(duì)不同的編程語(yǔ)言,我們會(huì)使用相應(yīng)的實(shí)現(xiàn)類。例如 Java 是 PsiJavaFile,XML 是 XMLFile。
下面通過(guò)實(shí)際使用來(lái)進(jìn)行介紹,首先是 PsiJavaFile,當(dāng)然在使用之前需要確保build.gradle
文件中將 Java 添加到插件配置中(XML 內(nèi)置無(wú)需添加):
intellij {
// 用到的插件
plugins.set(listOf("com.intellij.java"))
}
然后在plugin.xml
中加入以下配置:
<depends>com.intellij.modules.java</depends>
如果是 XML 則需要添加如下配置:
<depends>com.intellij.modules.xml</depends>
經(jīng)過(guò)以上配置后,我們就可以使用 PsiJavaFile 和 XMLFile 的相關(guān) API 了。
例如以下代碼可以得到 java 文件的所屬表名和類中 所有的方法名,然后展示出來(lái):
class PsiJavaAction: AnAction() {
override fun actionPerformed(e: AnActionEvent) {
// 獲取 PsiFile 對(duì)象
val psiFile = e.getData(CommonDataKeys.PSI_FILE)
// 轉(zhuǎn)換為 PsiJavaFile
val psiJavaFile = psiFile as PsiJavaFile
// 獲取類所屬包
Utils.info("當(dāng)前類所屬包:${psiJavaFile.packageName}")
// 遍歷獲取所有的方法名
psiJavaFile.accept(object: JavaRecursiveElementVisitor() {
override fun visitMethod(method: PsiMethod) {
Utils.info("查找到方法:${method.name}")
}
})
}
}
在以上代碼中需要注意psiJavaFile.accept()
方法,其中 accept 方法是 PsiFile 所提供的方法,方法簽名為void accept(@NotNull PsiElementVisitor visitor)
,用于遍歷 PSI 文件中的各類元素,可以看到上面我們?cè)趥鲄r(shí)傳遞的是JavaRecursiveElementVisitor
,這是用于遍歷 Java 中各類元素(字段、方法、注解等)的一個(gè)實(shí)現(xiàn)類,只需要重寫對(duì)應(yīng)的方法即可,在上面我們重寫了visitMethod
方法,其實(shí)內(nèi)部提供了很多方法,大家可以自行嘗試,通過(guò)方法名也可以看到這里還支持遍歷 break 語(yǔ)句,斷言語(yǔ)句等等:
下面再說(shuō)明如何遍歷 XML 文件中的元素:
class PsiXMLAction: AnAction() {
override fun actionPerformed(e: AnActionEvent) {
// 獲取 PsiFile 對(duì)象
val psiFile = e.getData(CommonDataKeys.PSI_FILE)
// 轉(zhuǎn)換為 XmlFile
val xmlFile = psiFile as XmlFile
// 獲取根標(biāo)簽名稱
Utils.info("根標(biāo)簽名稱:${xmlFile.rootTag?.name}")
// 遍歷獲取所有的元素信息
xmlFile.accept(object: XmlRecursiveElementVisitor() {
override fun visitXmlAttribute(attribute: XmlAttribute) {
Utils.info("屬性名稱:${attribute.name}, 屬性值:${attribute.value}")
}
})
}
}
可以看到這里遍歷使用的是XmlRecursiveElementVisitor
,是 XML 對(duì)于PsiElementVisitor
的一個(gè)實(shí)現(xiàn)類,用于遍歷 XML 文件中的各種元素:
PSI Element(PSI 元素)
在上面介紹 PSI 文件的時(shí)候多次提到元素的概念,PSI 文件則正是由一系列的 PSI Element 所組成。和 PSI file 類似,PSI Element 也屬于一個(gè)頂層接口,針對(duì)不同的編程語(yǔ)言,會(huì)有多種 PSI 元素。以 Java 為例,有 PsiClass、PSIMethod、PsiField 等對(duì)應(yīng) Java 語(yǔ)法的各類元素。而 XML 中也有 XmlTag、XmlAttribute 等概念。那我們?nèi)绾慰焖僦酪粋€(gè)文件中有哪些 PSI 元素?如何快速知道一個(gè)我們不熟悉的編程語(yǔ)言中的 PSI 元素?別慌,IntelliJ平臺(tái)給我提供了工具:
通過(guò) IntelliJ 平臺(tái)的工具,我們可以很方便地查看當(dāng)前或者任意一種文件的 PSI 結(jié)構(gòu),下面分別以 Java 和 XML 文件為例,首先是 Java 文件:
同時(shí)點(diǎn)擊左下的元素節(jié)點(diǎn),上方還會(huì)自動(dòng)對(duì)應(yīng)到元素位置:
然后是 XML 文件:
當(dāng)然,除了 Java 和 XML,IntelliJ 支持的編程語(yǔ)言遠(yuǎn)不止這些,這里展示一部分,剩下的大家可以自行探索:
上面介紹了如何快速查看 PSI 文件中的元素,下面再介紹如何去獲取 PSI 元素,以下來(lái)自官網(wǎng):
Context | API |
---|---|
Action |
AnActionEvent.getData(CommonDataKeys.PSI_ELEMENT) Note: If an editor is currently open and the element under caret is a reference, this will return the result of resolving the reference. |
PSI File | PsiFile.findElementAt(offset) |
Reference | PsiReference.resolve() |
可以看到總共有三種方式:第一種是直接獲取當(dāng)前光標(biāo)位置的 PSI 元素;第二種是可以自己指定偏移量(如果不熟悉偏移量的概念,可以看本系列第五篇文章中講解 CaretModel 的部分),獲取指定文件指定位置的 PSI 元素;最后一種引用則使用的較少,這里不再展開介紹,大家可以查看官方文檔進(jìn)行了解。
除了獲取某個(gè)位置的 PSI 元素,我們還可以獲取其所屬父元素或者子元素,下面以 Java 文件為例講解如何使用:
class PsiJavaAction: AnAction() {
override fun actionPerformed(e: AnActionEvent) {
val psiFile = e.getData(CommonDataKeys.PSI_FILE)
// 獲取光標(biāo)處 PSI 元素,假定該元素在方法內(nèi)部
val psiElement = e.getData(PlatformDataKeys.EDITOR)
?.caretModel?.let { psiFile?.findElementAt(it.offset) }
// 獲取該元素所屬的方法名
val psiMethod = PsiTreeUtil.getParentOfType(psiElement, PsiMethod::class.java)
// 獲取該元素所屬的類名
val psiClass = PsiTreeUtil.getParentOfType(psiElement, PsiClass::class.java)
Utils.info("所屬方法名:${psiMethod?.name}")
Utils.info("所屬類名:${psiClass?.name}")
}
}
可以看到上面我們使用PsiTreeUtil::getParentOfType
可以獲取到一個(gè)元素的父元素,同時(shí)支持跨層級(jí)獲取,既可以獲取元素所屬的方法,也可以獲取元素所屬的類。
效果如下:
相應(yīng)地我們也可以通過(guò)PsiTreeUtil::getChildrenOfTypeAsList
去獲取某個(gè)元素的所有子元素:
class PsiJavaAction: AnAction() {
override fun actionPerformed(e: AnActionEvent) {
val psiFile = e.getData(CommonDataKeys.PSI_FILE)
// 先獲取光標(biāo)所在處元素所屬的類
val psiElement = e.getData(PlatformDataKeys.EDITOR)
?.caretModel?.let { psiFile?.findElementAt(it.offset) }
val psiClass = PsiTreeUtil.getParentOfType(psiElement, PsiClass::class.java)
// 獲取類中所有的方法
val psiMethods = PsiTreeUtil.getChildrenOfTypeAsList(psiClass, PsiMethod::class.java)
Utils.info("包含的方法:${psiMethods.joinToString(",") { it.name }}")
}
}
實(shí)戰(zhàn)
在正式實(shí)現(xiàn)前,先介紹一下整體的實(shí)現(xiàn)思路,這里只說(shuō)明如何從 Java 方法跳轉(zhuǎn)到 Mapper XML 文件中的節(jié)點(diǎn),反向參考代碼也很好理解,思路如下:
- 左側(cè)圖標(biāo)行標(biāo)記符通過(guò)實(shí)現(xiàn)
RelatedItemLineMarkerProvider
并重寫collectNavigationMarkers
方法設(shè)置。 - 判斷代碼行的元素類型為 PsiMethod 才進(jìn)行設(shè)置,同時(shí)文件類名以 Mapper 結(jié)尾。
- 根據(jù)類名在項(xiàng)目查找同名的 Mapper XML 文件。
- 通過(guò) accept 方法遍歷 XML 文件所有的屬性,將 id 值為對(duì)應(yīng)方法名的標(biāo)簽所對(duì)應(yīng)的元素保存到可跳轉(zhuǎn)的目標(biāo)。
設(shè)置行標(biāo)記符號(hào),平臺(tái)給我們提供了 RelatedItemLineMarkerProvider 類進(jìn)行設(shè)置,只需要自定義了自己的行標(biāo)記類,然后在 plugin.xml 中添加 如下配置即可:
<codeInsight.lineMarkerProvider language="JAVA" implementationClass="cn.butterfly.psi.provider.JavaMapperLineMarkerProvider"/>
代碼實(shí)現(xiàn)如下:文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-861625.html
class JavaMapperLineMarkerProvider: RelatedItemLineMarkerProvider() {
override fun collectNavigationMarkers(
element: PsiElement,
result: MutableCollection<in RelatedItemLineMarkerInfo<*>>
) {
// 查找類名后綴為 Mapper 內(nèi)的所有方法
if (element !is PsiMethod) {
return
}
val psiClass = PsiTreeUtil.getParentOfType(element, PsiClass::class.java) ?: return
val className = psiClass.name ?: return
if (!className.endsWith("Mapper")) {
return
}
// 查找同名 XML 文件對(duì)應(yīng)的 PSI 文件對(duì)象
val virtualFile = FileTypeIndex.getFiles(XmlFileType.INSTANCE, GlobalSearchScope.allScope(element.project))
.first { it.name.startsWith(className) }
val psiFile = PsiManager.getInstance(element.project).findFile(virtualFile)
// 遍歷 XML 文件中標(biāo)簽 id 節(jié)點(diǎn)值等于 Java 方法名的元素, 然后添加可跳轉(zhuǎn)的行標(biāo)記符
psiFile?.accept(object : XmlRecursiveElementVisitor() {
override fun visitXmlAttribute(attribute: XmlAttribute) {
if (attribute.name == "id" && attribute.value == element.name) {
// NavigationGutterIconBuilder 用于創(chuàng)建標(biāo)識(shí)符
result.add(
NavigationGutterIconBuilder.create(PluginIcons.MAPPER_ICON)
.setTargets(setOf(attribute.navigationElement))
.setTooltipText("Navigation to target in mapper xml").createLineMarkerInfo(element)
)
}
}
})
}
}
總結(jié)
本文簡(jiǎn)單介紹了關(guān)于 PSI 文件和元素的基礎(chǔ)知識(shí),最后以一個(gè) Mybatis 文件跳轉(zhuǎn)的例子去演示了如何去實(shí)際運(yùn)用 PSI,在下一篇文章則會(huì)介紹關(guān)于 PSI 的進(jìn)階知識(shí),敬請(qǐng)期待~~文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-861625.html
到了這里,關(guān)于IntelliJ IDE 插件開發(fā) | (七)PSI 入門及實(shí)戰(zhàn)(實(shí)現(xiàn) MyBatis 插件的跳轉(zhuǎn)功能)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!