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)
        // 转换为 PsiJavaFile
        val 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)
        // 转换为 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}")
            }
        })
    }

}

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) ?: 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 的进阶知识,敬请期待~~

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MongoDB高可用架构涉及常用功能整理

MongoDB高可用架构涉及常用功能整理 1. mongo架构和相关组件1.1. Master-Slave主从模式1.2. Replica Set 副本集模式1.3. Sharding 分片模式 2. Sharding 分片模式2.1. Hashed Sharding方式2.2. Range Sharding方式 3. 事务性4. 疑问和思考4.1. 怎么保证数据的高可靠&#xff1…

常用中间件redis,kafka及其测试方法

常用消息中间件及其测试方法 一、中间件的使用场景引入中间件的目的一般有两个&#xff1a;1、提升性能常用的中间件&#xff1a;1) 高速缓存&#xff1a;redis2) 全文检索&#xff1a;ES3) 存日志&#xff1a;ELK架构4) 流量削峰&#xff1a;kafka 2、提升可用性产品架构中高可…

Web前端—浏览器渲染原理

浏览器渲染原理 浏览器渲染原理渲染时间点渲染流水线1. 解析HTML—Parse HTML2. 样式计算—Recalculate Style3. 布局—Layout4. 分层—Layer5. 绘制—Paint6. 分块—Tiling7. 光栅化—Raster8. 画—Draw完整过程 面试题1. 浏览器是如何渲染页面的&#xff1f;2. 什么是 reflow…

linux apt 速度慢 换源

Ubuntu 20.04.1 LTS已推出,一样的为期5年的服务,感觉不错,安装了一个,但是苦于使用默认源在国内下载太慢,就想着把apt源改为国内源,目前国内比较好的源,有阿里源,清华源,豆瓣源等,下面我以阿里源为例,说下如何修改。 也可以在中科大https://mirrors.ustc.edu.cn/查…

使用amd架构的计算机部署其他架构的虚拟机(如:arm)

1 下载quem模拟器 https://qemu.weilnetz.de/w64/2 QEMU UEFI固件文件下载(引导文件) 推荐使用&#xff1a;https://releases.linaro.org/components/kernel/uefi-linaro/latest/release/qemu64/QEMU_EFI.fd3 QEMU 安装 安装完成之后&#xff0c;需要将安装目录添加到环境变…

福昕阅读器 PDF 文档基本操作

福昕阅读器 PDF 文档基本操作 References 转至 PDF 顶部 快捷键&#xff1a;Home. 转至 PDF 顶部 快捷键&#xff1a;End. 打开超链接 文本选择工具 -> 手形工具 (Hand Tool) -> 点击超链接 福昕阅读器 同时在多个窗口中打开多个文件 文件 -> 偏好设置 -> 文…

数据库导入文件或者运行文件的时候报错误 #1046 - No database selected

如果我们在使用数据库导入文件的时候报错误 #1046 - No database selected该怎么解决 那么小编带我们可以从三个角度去观察 1、这种情况一般是因为你在数据库中没有这个数据库&#xff0c;你新建一个你要导入的数据库名字的数据库&#xff0c;然后选中该数据库&#xff0c;再进…

设计模式-初步认识

目录 &#x1f6fb;1.什么是设计模式 &#x1f69a;2.设计模式的优点 &#x1f68d;3.设计模式6大原则 &#x1f6f4;4.设计模式类型 1.什么是设计模式 设计模式代表了最佳的实践&#xff0c;通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开…

如何使用PHP和RabbitMQ实现消息队列?

前言 今天我们来做个小试验&#xff0c;用PHP和RabbitMQ实现消息队列功能。 前期准备&#xff0c;需要安装好docker、docker-compose的运行环境。 如何使用docker部署php服务_php如何使用docker发布-CSDN博客 一、安装RabbitMQ 1、创建相关目录&#xff0c;执行如下命令。…

数据分析与挖掘

数据起源&#xff1a; 规模庞大&#xff0c;结构复杂&#xff0c;难以通过现有商业工具和技术在可容忍的时间内获取、管理和处理的数据集。具有5V特性&#xff1a;数量&#xff08;Volume&#xff09;&#xff1a;数据量大、多样性&#xff08;Variety&#xff09;&#xff1a…

neo4j所有关系只显示RELATION,而不显示具体的关系

当看r时&#xff0c;真正的关系在properties中的type里&#xff0c;而type为“RELATION” 造成这个的原因是&#xff1a; 在创建关系时&#xff0c;需要指定关系的类型&#xff0c;这是固定的&#xff0c;不能像属性那样从CSV文件的一个字段动态赋值。标准的Cypher查询语言不支…

Verilog刷题笔记42

题目&#xff1a;Create 16 D flip-flops. It’s sometimes useful to only modify parts of a group of flip-flops. The byte-enable inputs control whether each byte of the 16 registers should be written to on that cycle. byteena[1] controls the upper byte d[15:8…

轻量级 C++ UI 库:快速、可移植、自包含 | 开源日报 No.168

ocornut/imgui Stars: 53.4k License: MIT imgui 是 C 的无臃肿图形用户界面&#xff0c;具有最小的依赖关系。 该项目的主要功能、关键特性、核心优势包括&#xff1a; 为 C 提供了一个轻量级的图形用户界面库输出优化的顶点缓冲区&#xff0c;可在 3D 渲染应用程序中随时呈…

Django下载使用、文件介绍

【一】下载并使用 【1】下载框架 &#xff08;1&#xff09;注意事项 计算机名称不要出现中文python解释器版本不同可能会出现启动报错项目中所有的文件名称不要出现中文多个项目文件尽量不要嵌套,做到一项一夹 &#xff08;2&#xff09;下载 Django属于第三方模块&#…

网络套接字-TCP服务器

一 前言 前面已经写过udp服务器的实现了&#xff0c;那里说了很多编写服务器的所需知识&#xff0c;在tcp服务器实现中就不再赘述了。 二 服务端编写 大致接口如下。 ./server port端口号 启动时指明端口号 void usage(const std::string proc) {std::cout<<"Usa…

jetcache 2级缓存模式实现批量清除

需求 希望能够实现清理指定对象缓存的方法&#xff0c;例如缓存了User表&#xff0c;当User表巨大时&#xff0c;通过id全量去清理不现实&#xff0c;耗费资源也巨大。因此需要能够支持清理指定本地和远程缓存的批量方法。 分析 查看jetcache生成的cache接口&#xff0c;并没…

银行监管报送系统介绍(五):金融统计数据大集中自动化报送系统——PBOC Report

人民银行金融统计数据大集中自动化报送系统&#xff08;简称PBOC Report&#xff09;&#xff0c;是基于现代计算机网络技术应用基础上&#xff0c;由人行总行设置金融统计数据服务器&#xff0c;建立的一个全国统一的金融统计数据库。 人行针对各银行存贷款、中间业务、网点人…

上位机图像处理和嵌入式模块部署(qmacvisual之ROI设定)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 ROI&#xff0c;全称是region of interest&#xff0c;也就是感兴趣区域。这里面一般分成两种情况&#xff0c;一种是所有的算法都依赖于这个ROI&a…

基于SpringBoot+Vue+Mybatis的408刷题小程序管理端

简介 原始数据&#xff1a;书目信息、章节信息、题目信息、系统菜单、系统角色、系统用户。 主要任务&#xff1a;系统主要采用spring boot作为后端框架&#xff0c;前端使用vueelementUI&#xff0c;为408刷题小程序提供一个方面的管理和维护的任务&#xff0c;主要功能包括…

【ArcGIS】统计土地利用数据每类面积及占比

1.栅格转面 转换工具/从栅格/栅格转面&#xff0c;得到面.shp 不要勾选简化面&#xff0c;否则会影响面积 2.添加面积字段&#xff0c;计算面积 面.shp添加双精度字段area 查看面.shp属性表中有没有gridcode为0的&#xff0c;如有的话编辑矢量&#xff0c;删除该要素。 计…
最新文章