IntelliJ IDE 插件开发 | (七)PSI 入门及实战(实现 MyBatis 插件的跳转功能)

2024-03-25 01:20

本文主要是介绍IntelliJ IDE 插件开发 | (七)PSI 入门及实战(实现 MyBatis 插件的跳转功能),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

系列文章

  • IntelliJ IDE 插件开发 |(一)快速入门
  • IntelliJ IDE 插件开发 |(二)UI 界面与数据持久化
  • IntelliJ IDE 插件开发 |(三)消息通知与事件监听
  • IntelliJ IDE 插件开发 |(四)来查收你的 IDEA 使用报告吧
  • IntelliJ IDE 插件开发 |(五)VFS 与编辑器
  • IntelliJ IDE 插件开发 |(六)内部模式的使用
  • IntelliJ IDE 插件开发 |(七)PSI 入门及实战(实现 MyBatis 插件的跳转功能)

前言

所谓 PSI(Program Structure Interface),直译过来是程序结构接口,其实就是 IntelliJ 平台给我们提供用来解析代码文件,简化对各类编程语言(Java、Kotlin、XML)操作的接口。大部分针对编程语言或者框架的便利插件其实就与此相关,本文则会先介绍关于 PSI 的一些基础知识,然后再以一些 Mybatis 插件提供的 Java 方法 和 Mapper XML 文件互相跳转的例子来说明 PSI 的实际应用,最终实现效果如下图,本文涉及的到的完整代码文件也已上传到GitHub。

动画

PSI file(PSI 文件)

在本系列的第五篇文章中介绍了Virtual Files 和 Documents 用于处理文件的 API,而 PSI file 也是用于处理文件的 API,不过也有一些不同,具体如下:

类别层面范围
VF、Document文本文件应用级
PSI编程语言语法树项目级

获取文件的 PSI file 对象的方式主要有以下几种(来自官网):

ContextAPI
ActionAnActionEvent.getData(CommonDataKeys.PSI_FILE)
DocumentPsiDocumentManager.getPsiFile()
PSI ElementPsiElement.getContainingFile()
Virtual FilePsiManager.findFile(), PsiUtilCore.toPsiFiles()
File NameFilenameIndex.getVirtualFilesByName()

最后一种方式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)// 转换为 PsiJavaFileval psiJavaFile = psiFile as PsiJavaFile// 获取类所属包Utils.info("当前类所属包:${psiJavaFile.packageName}")// 遍历获取所有的方法名psiJavaFile.accept(object: JavaRecursiveElementVisitor() {override fun visitMethod(method: PsiMethod) {Utils.info("查找到方法:${method.name}")}})}}

image-20240324153748554

在以上代码中需要注意psiJavaFile.accept()方法,其中 accept 方法是 PsiFile 所提供的方法,方法签名为void accept(@NotNull PsiElementVisitor visitor),用于遍历 PSI 文件中的各类元素,可以看到上面我们在传参时传递的是JavaRecursiveElementVisitor,这是用于遍历 Java 中各类元素(字段、方法、注解等)的一个实现类,只需要重写对应的方法即可,在上面我们重写了visitMethod方法,其实内部提供了很多方法,大家可以自行尝试,通过方法名也可以看到这里还支持遍历 break 语句,断言语句等等:

image-20240324155416824

下面再说明如何遍历 XML 文件中的元素:

class PsiXMLAction: AnAction() {override fun actionPerformed(e: AnActionEvent) {// 获取 PsiFile 对象val psiFile = e.getData(CommonDataKeys.PSI_FILE)// 转换为 XmlFileval xmlFile = psiFile as XmlFile// 获取根标签名称Utils.info("根标签名称:${xmlFile.rootTag?.name}")// 遍历获取所有的元素信息xmlFile.accept(object: XmlRecursiveElementVisitor() {override fun visitXmlAttribute(attribute: XmlAttribute) {Utils.info("属性名称:${attribute.name}, 属性值:${attribute.value}")}})}}

image-20240324160702331

可以看到这里遍历使用的是XmlRecursiveElementVisitor,是 XML 对于PsiElementVisitor 的一个实现类,用于遍历 XML 文件中的各种元素:

image-20240324161835384

PSI Element(PSI 元素)

在上面介绍 PSI 文件的时候多次提到元素的概念,PSI 文件则正是由一系列的 PSI Element 所组成。和 PSI file 类似,PSI Element 也属于一个顶层接口,针对不同的编程语言,会有多种 PSI 元素。以 Java 为例,有 PsiClass、PSIMethod、PsiField 等对应 Java 语法的各类元素。而 XML 中也有 XmlTag、XmlAttribute 等概念。那我们如何快速知道一个文件中有哪些 PSI 元素?如何快速知道一个我们不熟悉的编程语言中的 PSI 元素?别慌,IntelliJ平台给我提供了工具:

image-20240324163733415

通过 IntelliJ 平台的工具,我们可以很方便地查看当前或者任意一种文件的 PSI 结构,下面分别以 Java 和 XML 文件为例,首先是 Java 文件:

image-20240324164120824

同时点击左下的元素节点,上方还会自动对应到元素位置:

image-20240324164235622

然后是 XML 文件:

image-20240324164626925

当然,除了 Java 和 XML,IntelliJ 支持的编程语言远不止这些,这里展示一部分,剩下的大家可以自行探索:

image-20240324164741314

上面介绍了如何快速查看 PSI 文件中的元素,下面再介绍如何去获取 PSI 元素,以下来自官网:

ContextAPI
ActionAnActionEvent.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 FilePsiFile.findElementAt(offset)
ReferencePsiReference.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可以获取到一个元素的父元素,同时支持跨层级获取,既可以获取元素所属的方法,也可以获取元素所属的类。

效果如下:

image-20240324172523728

相应地我们也可以通过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 }}")}}

image-20240324174920319

实战

在正式实现前,先介绍一下整体的实现思路,这里只说明如何从 Java 方法跳转到 Mapper XML 文件中的节点,反向参考代码也很好理解,思路如下:

  1. 左侧图标行标记符通过实现RelatedItemLineMarkerProvider并重写collectNavigationMarkers方法设置。
  2. 判断代码行的元素类型为 PsiMethod 才进行设置,同时文件类名以 Mapper 结尾。
  3. 根据类名在项目查找同名的 Mapper XML 文件。
  4. 通过 accept 方法遍历 XML 文件所有的属性,将 id 值为对应方法名的标签所对应的元素保存到可跳转的目标。

设置行标记符号,平台给我们提供了 RelatedItemLineMarkerProvider 类进行设置,只需要自定义了自己的行标记类,然后在 plugin.xml 中添加 如下配置即可:

<codeInsight.lineMarkerProvider language="JAVA"implementationClass="cn.butterfly.psi.provider.JavaMapperLineMarkerProvider"/>

代码实现如下:

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) ?: returnval className = psiClass.name ?: returnif (!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 的进阶知识,敬请期待~~

这篇关于IntelliJ IDE 插件开发 | (七)PSI 入门及实战(实现 MyBatis 插件的跳转功能)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


原文地址:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.chinasem.cn/article/843423

相关文章

利用Python实现时间序列动量策略

《利用Python实现时间序列动量策略》时间序列动量策略作为量化交易领域中最为持久且被深入研究的策略类型之一,其核心理念相对简明:对于显示上升趋势的资产建立多头头寸,对于呈现下降趋势的资产建立空头头寸... 目录引言传统策略面临的风险管理挑战波动率调整机制:实现风险标准化策略实施的技术细节波动率调整的战略价

使用Python和Tkinter实现html标签去除工具

《使用Python和Tkinter实现html标签去除工具》本文介绍用Python和Tkinter开发的HTML标签去除工具,支持去除HTML标签、转义实体并输出纯文本,提供图形界面操作及复制功能,需... 目录html 标签去除工具功能介绍创作过程1. 技术选型2. 核心实现逻辑3. 用户体验增强如何运行

SpringBoot实现Kafka动态反序列化的完整代码

《SpringBoot实现Kafka动态反序列化的完整代码》在分布式系统中,Kafka作为高吞吐量的消息队列,常常需要处理来自不同主题(Topic)的异构数据,不同的业务场景可能要求对同一消费者组内的... 目录引言一、问题背景1.1 动态反序列化的需求1.2 常见问题二、动态反序列化的核心方案2.1 ht

Python实现文件批量重命名器

《Python实现文件批量重命名器》在日常工作和学习中,我们经常需要对大量文件进行重命名操作,本文将介绍一个使用Python开发的文件批量重命名工具,提供了多种重命名模式,有需要的小伙伴可以了解下... 目录前言功能特点模块化设计1.目录路径获取模块2.文件列表获取模块3.重命名模式选择模块4.序列号参数配

golang实现延迟队列(delay queue)的两种实现

《golang实现延迟队列(delayqueue)的两种实现》本文主要介绍了golang实现延迟队列(delayqueue)的两种实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的... 目录1 延迟队列:邮件提醒、订单自动取消2 实现2.1 simplChina编程e简单版:go自带的time

Python使用python-docx实现自动化处理Word文档

《Python使用python-docx实现自动化处理Word文档》这篇文章主要为大家展示了Python如何通过代码实现段落样式复制,HTML表格转Word表格以及动态生成可定制化模板的功能,感兴趣的... 目录一、引言二、核心功能模块解析1. 段落样式与图片复制2. html表格转Word表格3. 模板生

SpringBoot实现多环境配置文件切换

《SpringBoot实现多环境配置文件切换》这篇文章主要为大家详细介绍了如何使用SpringBoot实现多环境配置文件切换功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1. 示例代码结构2. pom文件3. application文件4. application-dev文

Python FastAPI实现JWT校验的完整指南

《PythonFastAPI实现JWT校验的完整指南》在现代Web开发中,构建安全的API接口是开发者必须面对的核心挑战之一,本文将深入探讨如何基于FastAPI实现JWT(JSONWebToken... 目录一、JWT认证的核心原理二、项目初始化与环境配置三、安全密码处理机制四、JWT令牌的生成与验证五、

JavaScript实战:智能密码生成器开发指南

本文通过JavaScript实战开发智能密码生成器,详解如何运用crypto.getRandomValues实现加密级随机密码生成,包含多字符组合、安全强度可视化、易混淆字符排除等企业级功能。学习密码强度检测算法与信息熵计算原理,获取可直接嵌入项目的完整代码,提升Web应用的安全开发能力 目录

Python使用Turtle实现精确计时工具

《Python使用Turtle实现精确计时工具》这篇文章主要为大家详细介绍了Python如何使用Turtle实现精确计时工具,文中的示例代码讲解详细,具有一定的借鉴价值,有需要的小伙伴可以参考一下... 目录功能特点使用方法程序架构设计代码详解窗口和画笔创建时间和状态显示更新计时器控制逻辑计时器重置功能事件