赞
踩
所谓 PSI(Program Structure Interface),直译过来是程序结构接口,其实就是 IntelliJ 平台给我们提供用来解析代码文件,简化对各类编程语言(Java、Kotlin、XML)操作的接口。大部分针对编程语言或者框架的便利插件其实就与此相关,本文则会先介绍关于 PSI 的一些基础知识,然后再以一些 Mybatis 插件提供的 Java 方法 和 Mapper XML 文件互相跳转的例子来说明 PSI 的实际应用,最终实现效果如下图,本文涉及的到的完整代码文件也已上传到GitHub。
在本系列的第五篇文章中介绍了Virtual Files 和 Documents 用于处理文件的 API,而 PSI file 也是用于处理文件的 API,不过也有一些不同,具体如下:
类别 | 层面 | 范围 |
---|---|---|
VF、Document | 文本文件 | 应用级 |
PSI | 编程语言语法树 | 项目级 |
获取文件的 PSI file 对象的方式主要有以下几种(来自官网):
最后一种方式
FilenameIndex.getVirtualFilesByName()
得到的结果是Virtual File 对象,需要再通过倒数第二行的方式再获取到对应的 PIS file 对象。
不过通过以上方式获取到的 PsiFile 只是顶层的接口,针对不同的编程语言,我们会使用相应的实现类。例如 Java 是 PsiJavaFile,XML 是 XMLFile。
下面通过实际使用来进行介绍,首先是 PsiJavaFile,当然在使用之前需要确保build.gradle
文件中将 Java 添加到插件配置中(XML 内置无需添加):
intellij {
// 用到的插件
plugins.set(listOf("com.intellij.java"))
}
然后在plugin.xml
中加入以下配置:
<depends>com.intellij.modules.java</depends>
如果是 XML 则需要添加如下配置:
<depends>com.intellij.modules.xml</depends>
经过以上配置后,我们就可以使用 PsiJavaFile 和 XMLFile 的相关 API 了。
例如以下代码可以得到 java 文件的所属表名和类中 所有的方法名,然后展示出来:
class PsiJavaAction: AnAction() { override fun actionPerformed(e: AnActionEvent) { // 获取 PsiFile 对象 val psiFile = e.getData(CommonDataKeys.PSI_FILE) // 转换为 PsiJavaFile val psiJavaFile = psiFile as PsiJavaFile // 获取类所属包 Utils.info("当前类所属包:${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 文件中的各类元素,可以看到上面我们在传参时传递的是JavaRecursiveElementVisitor
,这是用于遍历 Java 中各类元素(字段、方法、注解等)的一个实现类,只需要重写对应的方法即可,在上面我们重写了visitMethod
方法,其实内部提供了很多方法,大家可以自行尝试,通过方法名也可以看到这里还支持遍历 break 语句,断言语句等等:
下面再说明如何遍历 XML 文件中的元素:
class PsiXMLAction: AnAction() { override fun actionPerformed(e: AnActionEvent) { // 获取 PsiFile 对象 val psiFile = e.getData(CommonDataKeys.PSI_FILE) // 转换为 XmlFile val xmlFile = psiFile as XmlFile // 获取根标签名称 Utils.info("根标签名称:${xmlFile.rootTag?.name}") // 遍历获取所有的元素信息 xmlFile.accept(object: XmlRecursiveElementVisitor() { override fun visitXmlAttribute(attribute: XmlAttribute) { Utils.info("属性名称:${attribute.name}, 属性值:${attribute.value}") } }) } }
可以看到这里遍历使用的是XmlRecursiveElementVisitor
,是 XML 对于PsiElementVisitor
的一个实现类,用于遍历 XML 文件中的各种元素:
在上面介绍 PSI 文件的时候多次提到元素的概念,PSI 文件则正是由一系列的 PSI Element 所组成。和 PSI file 类似,PSI Element 也属于一个顶层接口,针对不同的编程语言,会有多种 PSI 元素。以 Java 为例,有 PsiClass、PSIMethod、PsiField 等对应 Java 语法的各类元素。而 XML 中也有 XmlTag、XmlAttribute 等概念。那我们如何快速知道一个文件中有哪些 PSI 元素?如何快速知道一个我们不熟悉的编程语言中的 PSI 元素?别慌,IntelliJ平台给我提供了工具:
通过 IntelliJ 平台的工具,我们可以很方便地查看当前或者任意一种文件的 PSI 结构,下面分别以 Java 和 XML 文件为例,首先是 Java 文件:
同时点击左下的元素节点,上方还会自动对应到元素位置:
然后是 XML 文件:
当然,除了 Java 和 XML,IntelliJ 支持的编程语言远不止这些,这里展示一部分,剩下的大家可以自行探索:
上面介绍了如何快速查看 PSI 文件中的元素,下面再介绍如何去获取 PSI 元素,以下来自官网:
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() |
可以看到总共有三种方式:第一种是直接获取当前光标位置的 PSI 元素;第二种是可以自己指定偏移量(如果不熟悉偏移量的概念,可以看本系列第五篇文章中讲解 CaretModel 的部分),获取指定文件指定位置的 PSI 元素;最后一种引用则使用的较少,这里不再展开介绍,大家可以查看官方文档进行了解。
除了获取某个位置的 PSI 元素,我们还可以获取其所属父元素或者子元素,下面以 Java 文件为例讲解如何使用:
class PsiJavaAction: AnAction() { override fun actionPerformed(e: AnActionEvent) { val psiFile = e.getData(CommonDataKeys.PSI_FILE) // 获取光标处 PSI 元素,假定该元素在方法内部 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
可以获取到一个元素的父元素,同时支持跨层级获取,既可以获取元素所属的方法,也可以获取元素所属的类。
效果如下:
相应地我们也可以通过PsiTreeUtil::getChildrenOfTypeAsList
去获取某个元素的所有子元素:
class PsiJavaAction: AnAction() {
override fun actionPerformed(e: AnActionEvent) {
val psiFile = e.getData(CommonDataKeys.PSI_FILE)
// 先获取光标所在处元素所属的类
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 }}")
}
}
在正式实现前,先介绍一下整体的实现思路,这里只说明如何从 Java 方法跳转到 Mapper XML 文件中的节点,反向参考代码也很好理解,思路如下:
RelatedItemLineMarkerProvider
并重写collectNavigationMarkers
方法设置。设置行标记符号,平台给我们提供了 RelatedItemLineMarkerProvider 类进行设置,只需要自定义了自己的行标记类,然后在 plugin.xml 中添加 如下配置即可:
<codeInsight.lineMarkerProvider language="JAVA" implementationClass="cn.butterfly.psi.provider.JavaMapperLineMarkerProvider"/>
- 1
- 2
代码实现如下:
class JavaMapperLineMarkerProvider: RelatedItemLineMarkerProvider() { override fun collectNavigationMarkers( element: PsiElement, result: MutableCollection<in RelatedItemLineMarkerInfo<*>> ) { // 查找类名后缀为 Mapper 内的所有方法 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 文件对应的 PSI 文件对象 val virtualFile = FileTypeIndex.getFiles(XmlFileType.INSTANCE, GlobalSearchScope.allScope(element.project)) .first { it.name.startsWith(className) } val psiFile = PsiManager.getInstance(element.project).findFile(virtualFile) // 遍历 XML 文件中标签 id 节点值等于 Java 方法名的元素, 然后添加可跳转的行标记符 psiFile?.accept(object : XmlRecursiveElementVisitor() { override fun visitXmlAttribute(attribute: XmlAttribute) { if (attribute.name == "id" && attribute.value == element.name) { // NavigationGutterIconBuilder 用于创建标识符 result.add( NavigationGutterIconBuilder.create(PluginIcons.MAPPER_ICON) .setTargets(setOf(attribute.navigationElement)) .setTooltipText("Navigation to target in mapper xml").createLineMarkerInfo(element) ) } } }) } }
本文简单介绍了关于 PSI 文件和元素的基础知识,最后以一个 Mybatis 文件跳转的例子去演示了如何去实际运用 PSI,在下一篇文章则会介绍关于 PSI 的进阶知识,敬请期待~~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。