自定义IDEA代码补全插件

目标:

对于项目中的静态方法(主要是各种工具类里的静态方法),可以在输入方法名时直接提示相关的静态方法,选中后自动补全代码,并导入静态类。

设计:

初步构想,用户选择要导入的文件夹,遍历文件夹下面文件的静态方法并存储,当用户输入时使用弹窗显示候选方法,选中后补全代码。
分解步骤为:

  1. 在设置页加入视图化操作,让用户选择文件夹路径;
  2. 通过持久化数据将选择的文件夹路径保存到本地;
  3. IDE打开时遍历本地保存的文件夹路径下的所有文件,得到所有静态方法;
  4. 用户输入时弹窗显示联想方法;
  5. 选中后自动补全;

开发:

1.搭建开发环境

JetBrains已经提供了纯样板模板,我们下载提供的插件模板 ,使用Android Studio (或IntelliJ IDEA )打开后,可以在gradle.properties中修改项目的属性,gradle.gradle.properties里各属性表示的意义如下

gradle.properties配置
  • pluginGrouppluginName_pluginVersion:插件名称与版本

  • pluginSinceBuildpluginUntilBuild:插件适用的IDE版本,从since到until,各种IDE的版本号可以在这个地方查阅内部编号范围

    Android Studio对应的IntelliJ 平台版本可以查阅Android Studio

  • pluginVerifierIdeVersions:用来检查IDE版本和插件之间兼容性

  • **platformType:**插件适用的IDE类型,IC指社区版,Android Studio基于社区版修改

  • platformPlugins: 声明插件依赖项

在这里插入图片描述

更多的属性可以查阅此链接

https://github.com/JetBrains/intellij-platform-plugin-template

https://github.com/JetBrains/gradle-intellij-plugin/blob/master/README.md#intellij-platform-properties

plugin.xml

文件位于src\main\resources\META-INF下

  • id:gradle.properties里的pluginName_

  • name: gradle.properties里的pluginName_

  • vendor:开发者的名字
    在这里插入图片描述

    添加依赖

在这里插入图片描述

build.gradle.kts

在intellij节点下加入一句intellij

alternativeIdePath = "H:\Android\Android Studio"

在这里插入图片描述

路径设置为本地Android Studio位置,这样在运行时会直接使用本地的AS调试,避免重新下载Android Studio。

settings.gradle.kts

修改项目名称

rootProject.name = "Plugin Template Hint"

配置完成后,点击右边的gradle的runide即可运行插件,如果开发过程中想进行调试可以右键选择debug模式。

在这里插入图片描述

2.设置页添加视图化操作

在IDE的设置页添加新UI,需要使用applicationConfigurable Extension Points。 先在plugin.xml里注册applicationConfigurable,并且新建类继承Configurable。插件的UI模块是在java的swing组件基础上直接包装了一层,可以直接使用。

    <extensions defaultExtensionNs="com.intellij">......<applicationConfigurable instance="com.plugin.hint.other.UtilsImportUI" /></extensions>
class UtilsImportUI : Configurable {private val persistentState: UtilsFolderSetting = ApplicationManager.getApplication().getComponent(UtilsFolderSetting::class.java)private var isModify = false//绘制界面,使用Swing组件override fun createComponent(): JComponent? {//......绘制代码此处省略}//控制按钮“Apply”是否可点击override fun isModified(): Boolean {return isModify}//"Apply"按钮点击事件override fun apply() {......persistentState!!.list = pathListpersistentState.loadState(persistentState)//持久化数据isModify = false                }//配置面板左边窗口的显示名称override fun getDisplayName(): String {return "Import Utils Files"}//调用IDE的文件管理器选择文件private fun dir(jPanel: JPanel): String {if (project == null) {project = guessCurrentProject(jPanel)}val fcDial = FileChooserFactory.getInstance().createFileChooser(fcDesc, project, null)val files = fcDial.choose(project)return if (files.isNotEmpty()) {files[0].path} else ""}	
}

上面省略了部分代码,主要是绘制界面、持久化数据、保存用户选中的文件位置,并进行相关的去重。

效果如下:

在这里插入图片描述

3.持久化数据

为了保存用户选择的文件夹路径,我们需要对数据进行持久化。
在plugin.xml里注册implementation-class,并且新建类继承PersistentStateComponent,其中,name为XML中根标记的名称,storages 为保存的文件的名称,默认位置是配置文件地址的options目录下(默认位置可以点击File -> Mange IDE Settings -> Export Settings 查看)。

我们将路径通过list保存,读取时

    <application-components><component><implementation-class>com.plugin.hint.other.UtilsFolderSetting</implementation-class></component></application-components>
@State(name = "searchUtilsPath", storages = [Storage(value = "searchUtilsPath.xml")])
class UtilsFolderSetting : PersistentStateComponent<UtilsFolderSetting?> {var list: MutableList<String> = ArrayList()override fun getState(): UtilsFolderSetting {return this}override fun loadState(state: UtilsFolderSetting) {XmlSerializerUtil.copyBean(state, this)}
}
4.启动时遍历文件,保存静态方法

工程模板service下有两个类MyApplicationServiceMyProjectService,分别是 application 级别的service和 project 级别的service,其实还有一个module 级别的service,但是并不推荐(性能原因)。其中MyApplicationService为全局单例,而MyProjectService会在对应范围的每个实例创建一个单独的服务实例。这里我们在MyProjectService里遍历文件夹路径,对所有文件进行解析,并保存静态方法。

 class MyProjectService(project: Project) {init {if (project.workspaceFile != null) {val persistentState = ApplicationManager.getApplication().getComponent(UtilsFolderSetting::class.java)val pathList = persistentState.list//得到持久化数据for (s in pathList) {//遍历文件夹路径UtilMethodsHandle.addPsiMethodByPath(s, project)}}}
}

persistentState 为得到的持久化数据,然后再对文件路径进行解析。
addPsiMethodByPath方法如下,逻辑可以看注释

    var globalPsiMethods = HashMap<String, List<PsiMethod>>()//遍历文件夹,解析文件,存储方法fun addPsiMethodByPath(path: String, project: Project) {val virtualFile = project.workspaceFile!!.fileSystem.findFileByPath(path) ?: returnif (virtualFile.isDirectory) {//如果是文件夹,递归遍历val virtualFiles = virtualFile.childrenfor (file in virtualFiles) {addPsiMethodByPath(file.path, project)}} else {//如果是文件,解析val psiFile = PsiManager.getInstance(project).findFile(virtualFile)//判断是否是java文件,后面看是否支持kotlin文件if (psiFile is PsiJavaFile) {val classes = psiFile.classes //遍历文件里的类,因为可能会有内部类for (aClass in classes) {val tempMethods = aClass.methodsval list: MutableList<PsiMethod> = ArrayList()//遍历类里面的方法for (method in tempMethods) {//判断是静态并且不是私有的方法if (method.hasModifierProperty(PsiModifier.STATIC)&& !method.hasModifierProperty(PsiModifier.PRIVATE)) {list.add(method)}}globalPsiMethods[path] = list}}}}

解释上面的代码,需要先了解IntelliJ平台的一些名称概念。

PSI 程序结构接口(Program Structure Interface),是IntelliJ平台中的一个层,负责解析文件并创建支持平台许多功能的语法和语义代码模型。

PSI File ,IDEA将文件结构抽象为接口,叫程序结构接口文件(PSI File),不同类型的文件解析后生成不同的PsiFile接口的实现类实例,这也是IDEA能够扩展支持多语言的基础。PsiFile类是所有PSI文件的公共基类,而在特定的语言文件通常是由它的子类来表示。例如,PsiJavaFile类表示Java文件,XmlFile类表示XML文件。

VirtualFileSystem 虚拟文件系统(VFS)是IntelliJ平台的组件,该组件封装了用于处理以Virtual File表示的文件的大部分活动。
它具有以下主要目的:
提供一个通用API来处理文件,而不管文件的实际位置如何(在磁盘上,在归档中,在HTTP服务器等上)
当检测到更改时,跟踪文件修改并提供文件内容的旧版本和新版本。
提供了将其他持久性数据与VFS中的文件相关联的可能性。
Virtual File System

上面的代码通过project得到VirtualFile,判断如果是文件夹,递归调用方法,否则返回相对应的PsiFile,接着判断如果是PsiJavaFile(因为项目有可能包含kotlin文件),则遍历PsiClass(有可能包含内部类)得到所有PsiMethod,最后判断method是否是静态的(method.hasModifierProperty(PsiModifier.STATIC))并且不是私有的(!method.hasModifierProperty(PsiModifier.PRIVATE)),最后加入列表。

5.用户输入时自动弹窗显示联想方法

这里的两种方案,其实最开始使用的是第一种方法,在IDE自带的代码补全弹窗里插入我们保存的方法,但是这种方案没有解决方法显示排序的问题,提供的 order="first"属性并没有生效,最后使用了第二种方案。这里记录一下,可能以后在写其他插件时会用到。

第一种方案:

我们在plugin.xml里注册CompletionContributor languageJAVA

    <extensions defaultExtensionNs="com.intellij">......<completion.contributorimplementationClass="com.plugin.hint.other.UtilsCompletionContributor" language="JAVA"order="first" /></extensions>

CompletionContributor,实现extend函数,有三个参数

  1. CompletionType:代码完成的类型,基本完成(BASIC)、智能类型(SMART)匹配完成,Settings/Preferences | Editor | General | Code Completion里可选.
    在这里插入图片描述
  2. ElementPattern:匹配类型,可以对返回的元素进行过滤
  3. CompletionProvider:内容提供者,我们在这里返回待选择的
class UtilsCompletionContributor : CompletionContributor() {//查找可以自动补全的代码init {extend(CompletionType.BASIC, PlatformPatterns.psiElement(), UtilsCompletionProvider())}
}

UtilsCompletionProvider类,继承CompletionProvider,重写addCompletions方法,将元素加入到CompletionResultSet

class UtilsCompletionProvider : CompletionProvider<CompletionParameters>() {//添加自动补全代码override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {val prefix = result.prefixMatcher.prefixif (prefix.isEmpty()) {return}for (methodList in UtilMethodsHandle.globalPsiMethods.values) {for (method in methodList) {var s: String? = ""if (method.containingClass != null) {s = method.containingClass!!.qualifiedName//类名称}val element: LookupElement = LookupElementBuilder.create(method).withTypeText(s)//右边文字//.withIcon(MethodIcon).withIcon(AllIcons.Nodes.MethodReference)//左边图标.withBoldness(true)//是否加粗//选中后的处理事件.withInsertHandler { context1: InsertionContext, lookupElement: LookupElement? ->context1.document.insertString(context1.startOffset, ".")context1.document.insertString(context1.tailOffset, "();")//导入所引用的类JavaCompletionUtil.insertClassReference(method.containingClass!!, context1.file, context1.startOffset)//移动光标到代码尾部               context1.editor.caretModel.moveToOffset(context1.tailOffset - 2)}//添加element到代码补全弹窗result.addElement(PrioritizedLookupElement.withPriority(element, Int.MAX_VALUE.toDouble()))}}

上面代码,先检测是否有匹配的,否则返回。然后循环创建LookupElement。InsertHandler为选中后的操作,在这里补全代码,引入当前方法所在类。

如上图,在自带的代码补全弹窗里添加了2条我们的方法。

第二种方案:
在用户输入后使用快捷键呼出代码补全弹窗,使用Action完成。IntelliJ 平台中的Action需要代码实现并且必须注册。注册决定了Action在 IDE UI 中出现的位置。实现并注册后,Action会接收来自 IntelliJ 平台的回调以响应用户。
1.创建UtilsAction类,继承 Action类。当使用键盘快捷键或从菜单、工具栏操作时,就会回调 Action 类的 actionPerformed 方法。
先在plugin.xml里注册Action,这里默认的快捷键是"control shift X"

    <actions><action class="com.plugin.hint.other.UtilsAction" description="方法提示" id="plugin.hint" text="hint"><add-to-group anchor="first" group-id="CodeCompletionGroup" /><keyboard-shortcut first-keystroke="control shift X" keymap="$default" /></action></actions>

效果如图,Code Completion组下添加了我们新建的Action,在这里也可以更改快捷键。
在这里插入图片描述
UtilsAction类里,我们在actionPerformed 方法里弹出代码补全弹窗。searchText为用户输入的需要补全的代码。LookupImpl为为代码补全的弹窗。选中逻辑与第一种方案一样。

class UtilsAction : AnAction() {override fun actionPerformed(e: AnActionEvent) {......//需要查找的字符val searchText = StringBuilder()//selectedText表示光标选中的文本,如果不为空,则查找选中的,没有就从光标位置向前拼接字符,一直到空格为止if (editor.selectionModel.selectedText == null|| editor.selectionModel.selectedText == "") {var indexText = document.text.subSequence(startOffset - 1, startOffset).toString()while (startOffset > 0 && nameMatch(indexText)) {searchText.insert(0, indexText)startOffset--indexText = document.text.subSequence(startOffset - 1, startOffset).toString()}} else {searchText.append(editor.selectionModel.selectedText)}if (project != null) {val lookup = obtainLookup(editor, project)for (methodList in UtilMethodsHandle.globalPsiMethods.values) {for (method in methodList) {var qualifiedName: String? = ""if (method.containingClass != null) {qualifiedName = method.containingClass!!.qualifiedName}LOG.info("actionPerformed: $method+$qualifiedName")if (!method.isValid) continue//检查元素是否有效,比如切换分支后就会失效//创建一个element,与第一种方案一样val element: LookupElement = LookupElementBuilder.create(method).withTypeText(qualifiedName).withIcon(MethodIcon)//.withIcon(AllIcons.Nodes.MethodReference).withBoldness(true)val item = CompletionResult.wrap(element, PlainPrefixMatcher(searchText.toString()), CompletionSorter.emptySorter())if (item != null) {//将element添加进去lookup.addItem(item.lookupElement, item.prefixMatcher)}}}lookup.addLookupListener(object : LookupListener {override fun itemSelected(event: LookupEvent) {//item选中事件,与val lookupElement = event.item as LookupElementif (lookupElement.psiElement is PsiMethod) {//如果选中的element是方法val psiMethod = lookupElement.psiElement as PsiMethod//得到上下文InsertionContextval insertionContext = InsertionContext(OffsetMap(document), Lookup.AUTO_INSERT_SELECT_CHAR, arrayOf(lookupElement), psiFile!!, editor, false)//val tailOffset = OffsetMap(document).getOffset(InsertionContext.TAIL_OFFSET)//如果是选中状态,计算开始位置需要减去字符长度if (startOffset == start) startOffset -= searchText.lengthdocument.insertString(startOffset, ".")document.insertString(insertionContext.tailOffset, "();")//导入所引用的类JavaCompletionUtil.insertClassReference(psiMethod.containingClass!!, psiFile, startOffset)//移动光标到代码尾部editor.caretModel.moveToOffset(insertionContext.tailOffset - 2)}}})lookup.showLookup()//显示弹窗}private fun obtainLookup(editor: Editor, project: Project): LookupImpl {val lookup = LookupManager.getInstance(project).createLookup(editor, LookupElement.EMPTY_ARRAY, "",DefaultArranger()) as LookupImpl/*        if (editor.isOneLineMode) {lookup.setCancelOnClickOutside(true)lookup.setCancelOnOtherWindowOpen(true)}*///lookup.lookupFocusDegree = if (autopopup) LookupFocusDegree.UNFOCUSED else LookupFocusDegree.FOCUSEDreturn lookup}}

这里使用的代码补全弹窗是系统自带的弹窗,在这里说一下怎么找到各种UI相对应的类。
我们需要启用内部模式。在idea.properties里添加idea.is.internal=true,保存并重启IDE。会看到Tool中多了一个选项Internal Actions,然后选择 UI -> UI Inspector,打开 UI 检查器,启用之后就可以以交互方式测试UI元素。查看时,将光标居中于UI元素上,使用Ctrl+Alt+鼠标左键即可显示UI元素的内部描述.。
效果如图,可以看到相关的类,然后就可以再去找到具体的实现方法。
在这里插入图片描述

最终效果如下:
在这里插入图片描述

相关资料

IntelliJ Platform SDK

使用PSI分析Java代码

Intellij IDEA 插件开发秘籍

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SearchWP WordPress高级网站内容搜索插件

点击阅读SearchWP WordPress高级网站内容搜索插件原文 SearchWP WordPress高级网站内容搜索插件是一个非常强大的工具&#xff0c;可以显着增强您网站的搜索功能。通过向网站访问者提供高度相关和精确的搜索结果&#xff0c;它可以有效地简化他们的搜索过程&#xff0c;促进发…

CentOS 8离线安装telnet

下载telnet rpm安装包&#xff0c;可从https://www.rpmfind.net/linux/rpm2html/search.php?querytelnet&submitSearch…&systemcentos&arch 根据自己的操作系统下载对应的包&#xff0c;这里以CentOS8为例,分别下载如下的rtp包 xinetd-2.3.15-24.el8.x86_64.rpm…

设计师必备的Figma可视化组件库资产已更新至 7.0版本

在当今数字化时代&#xff0c;数据量呈爆炸式增长&#xff0c;大屏可视化的主要程度越来越高&#xff0c;而大屏背后的设计师们面对的挑战也越来越多&#xff0c;其中之一就是大屏可视化设计项目中的重复性元素设计。这一过程不仅耗费时间&#xff0c;还明显降低了设计团队的生…

Qt6.5类库详解:QLineEdit

哈喽大家好&#xff0c;我是20YC小二&#xff01;欢迎关注(20YC编程)&#xff0c;现在有免费《C程序员》视频教程下载哦&#xff01; ~下面开始今天的分享内容~ 1. QLineEdit介绍 QLineEdit是一个单行文本编辑器&#xff0c;允许用户输入和编辑纯文本。它提供了许多有用的编辑…

周记 从现在开始

每周笔记 2023&#xff1a;12.11 早上&#xff1a; ​ 全是课 下午&#xff1a; ​ 全是课 晚上&#xff1a; ​ 全是课 什么也没学 2023&#xff1a;12.12 早上&#xff1a; ​ 全是课 下午&#xff1a; ​ 全是课 晚上&#xff1a; ​ 全是课 什么也没学 20…

SSH的交互原理(wireshark的分析)

SSH的交换原理&#xff08;wireshark篇&#xff09; 首先要想了解ssh的交换原理&#xff0c;必须要先了解他的加密方式&#xff0c;他的加密方式是对称加密&#xff0c;和公钥加密。什么意思呢&#xff1f; 首先我们向服务器发送一个请求&#xff0c;然后服务器会发给我们他的…

GitBook安装及使用——使用 Markdown 创建你自己的博客网站和电子书

目录 前言一、依赖环境二、gitbook安装使用1.安装 gitbook-cli2.安装 gitbook3.Gitbook初始化4.创建你的文章5.修改 SUMMARY.md 和 README.md6.编译生成静态网页7.运行以便在浏览器预览8.运行效果 前言 GitBook是一个命令行工具&#xff0c;用于使用 Markdown 构建漂亮的博客网…

SSE(服务器推送事件)规范

SSE 是指 "Server-Sent Events"&#xff0c;即服务器推送事件。它是一种基于 HTTP 的服务器推送技术&#xff0c;允许服务器实时向客户端推送数据。SSE 规范定义了一种在客户端和服务器之间单向实时通信的方式&#xff0c;通常用于实现服务器向客户端推送更新、通知或…

【matlab】MATLAB常用内置函数示例

目录: 数值计算函数 1.1 abs函数 1.2 sin、cos、tan函数 1.3 exp、log函数 1.4 sqrt函数 1.5 sum、prod函数 矩阵和向量操作函数 2.1 size函数 2.2 reshape函数 2.3 repmat函数 2.4 eye函数 2.5 rand、randn函数 绘图函数 3.1 plot函数 3.2 scatter函数 3.3 bar函数 3.4 hist函…

面试题总结(十四)【系统移植】【华清远见西安中心】

你用过哪些Bootloader&#xff1f;有什么区别&#xff1f; 作为一个AI助手&#xff0c;我没有直接使用过Bootloader。但我可以为您提供一些关于常见Bootloader的信息和他们之间的区别。 1. GRUB (GRand Unified Bootloader)&#xff1a;GRUB是一个功能强大且广泛使用的Bootload…

【鉴权】JWT加密

目录 定义 官网 定义 JWT是JSON Web Token的缩写&#xff0c;是RFC7519规范。该规范目的是为了让客户端和服务端可靠的传递信息。 官网 JSON Web Tokens - jwt.io JWT是由三个部分组成&#xff0c;HMACSHA256( base64UrlEncode(header) "." base64UrlEncode(pa…

rk3568 MDIO总线

rk3568 MDIO总线 MDIO(Management Data Input/Output)是一种管理数据输入/输出协议,用于在以太网交换机和PHY(物理层收发器)之间进行通信。在网络设备中,MDIO总线用于控制网络接口的PHY芯片,例如通过MDIO总线访问PHY芯片的寄存器。这些寄存器包含了一些关于网络连接状态…

AWS 知识二:AWS同一个VPC下的ubuntu实例通过ldapsearch命令查询目录用户信息

前言&#xff1a; 前提&#xff1a;需要完成我的AWS 知识一创建一个成功运行的目录。 主要两个重要&#xff1a;1.本地windows如何通过SSH的方式连接到Ubuntu实例 2.ldapsearch命令的构成 一 &#xff0c;启动一个新的Ubuntu实例 1.创建一个ubuntu实例 具体创建实例步骤我就不…

vue el-date-picker中datetime类型对今天之后的日期包含时分禁用

vue el-date-picker中datetime类型对今天之后的日期包含时分禁用 目前对选择秒那一列未禁用 <template><div><el-date-pickerv-model"deactivateTime"type"datetime"format"yyyy-MM-dd HH:mm:ss"value-format"yyyy-MM-dd HH…

抖音直播间websocket礼物和弹幕消息推送可能出现重复的情况,解决办法

在抖音直播间里&#xff0c;通过websocket收到的礼物消息数据格式如下&#xff1a; {common: {method: WebcastGiftMessage,msgId: 7283420150152942632,roomId: 7283413007005207308,createTime: 1695803662805,isShowMsg: True,describe: 莎***:送给主播 1个入团卡,priority…

HarmonyOS4.0从零开始的开发教程17给您的应用添加通知

HarmonyOS&#xff08;十五&#xff09;给您的应用添加通知 通知介绍 通知旨在让用户以合适的方式及时获得有用的新消息&#xff0c;帮助用户高效地处理任务。应用可以通过通知接口发送通知消息&#xff0c;用户可以通过通知栏查看通知内容&#xff0c;也可以点击通知来打开应…

管理类联考——数学——真题篇——按题型分类——充分性判断题——蒙猜A/B

老规矩&#xff0c;看目录&#xff0c;平均3-5题 文章目录 A/B2023真题&#xff08;2023-19&#xff09;-A-选项特点&#xff1a;两个等号&#xff1b;-纯蒙猜-哪个长选哪个【不要用这招&#xff0c;因为两个选项&#xff0c;总会有一个长的&#xff0c;那不就大多都是A/B&…

git log实用指令

gi查看提交历史 在提交了若干更新&#xff0c;又或者克隆了某个项目之后&#xff0c;你也许想回顾下提交历史。 完成这个任务最简单而又有效的工具是 git log 命令 当你在此项目中运行 git log 命令时&#xff0c;可以看到下面的输出&#xff1a; $ git log commit ca82a6df…

透视数据:数据可视化工具的多重场景应用

数据可视化工具已经成为了许多领域中的重要利器&#xff0c;它们在各种场景下发挥着重要作用。下面我就以可视化从业者的角度简单谈谈数据可视化工具在不同场景下的应用&#xff1a; 企业数据分析与决策支持 在企业层面&#xff0c;数据可视化工具被广泛应用于数据分析和决策…

16 v-model绑定多选框

概述 使用v-model绑定多选框也是一种比较常见的需求&#xff0c;比如一个用户可以绑定多个角色&#xff0c;可以有多个兴趣爱好。 在本节课中&#xff0c;我们来学习一下这两种用法。 基本用法 我们创建src/components/Demo16.vue&#xff0c;在这个组件中&#xff0c;我们…