自定义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: 声明插件依赖项

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

github.com/JetBrains/i...

github.com/JetBrains/g...

plugin.xml

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

  • id:gradle.properties里的pluginName_

  • name: gradle.properties里的pluginName_

  • vendor :开发者的名字

    添加依赖

build.gradle.kts

在intellij节点下加入一句intellij

kotlin 复制代码
alternativeIdePath = "H:\Android\Android Studio"

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

settings.gradle.kts

修改项目名称

kotlin 复制代码
rootProject.name = "Plugin Template Hint"

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

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

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

xml 复制代码
    <extensions defaultExtensionNs="com.intellij">
   		......
       <applicationConfigurable instance="com.plugin.hint.other.UtilsImportUI" />
   </extensions>
kotlin 复制代码
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 = pathList
        persistentState.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保存,读取时

xml 复制代码
    <application-components>
        <component>
            <implementation-class>com.plugin.hint.other.UtilsFolderSetting</implementation-class>
        </component>
    </application-components>
kotlin 复制代码
@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里遍历文件夹路径,对所有文件进行解析,并保存静态方法。

kotlin 复制代码
 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方法如下,逻辑可以看注释

kotlin 复制代码
    var globalPsiMethods = HashMap<String, List<PsiMethod>>()

    //遍历文件夹,解析文件,存储方法
    fun addPsiMethodByPath(path: String, project: Project) {
        val virtualFile = project.workspaceFile!!.fileSystem.findFileByPath(path) ?: return
        if (virtualFile.isDirectory) {//如果是文件夹,递归遍历
            val virtualFiles = virtualFile.children
            for (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.methods
                    val 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

xml 复制代码
    <extensions defaultExtensionNs="com.intellij">
		......
        <completion.contributor
            implementationClass="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:内容提供者,我们在这里返回待选择的
kotlin 复制代码
class UtilsCompletionContributor : CompletionContributor() {

    //查找可以自动补全的代码
    init {
       extend(CompletionType.BASIC, PlatformPatterns.psiElement(), UtilsCompletionProvider())
    }
}

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

kotlin 复制代码
class UtilsCompletionProvider : CompletionProvider<CompletionParameters>() {

    //添加自动补全代码
    override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext
                                , result: CompletionResultSet) {
        val prefix = result.prefixMatcher.prefix
        if (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"

xml 复制代码
    <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为为代码补全的弹窗。选中逻辑与第一种方案一样。

kotlin 复制代码
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 LookupElement
                    if (lookupElement.psiElement is PsiMethod) {//如果选中的element是方法
                        val psiMethod = lookupElement.psiElement as PsiMethod
                        //得到上下文InsertionContext
                        val 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.length
                        document.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.FOCUSED
	        return 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 插件开发秘籍

相关推荐
无极程序员10 小时前
PHP常量
android·ide·android studio
小黄人软件13 小时前
android浏览器源码 可输入地址或关键词搜索 android studio 2024 可开发可改地址
android·ide·android studio
帅得不敢出门20 小时前
Gradle命令编译Android Studio工程项目并签名
android·ide·android studio·gradlew
Rverdoser1 天前
Android Studio 多工程公用module引用
android·ide·android studio
hello world smile2 天前
Flutter常用命令整理
android·flutter·移动开发·android studio·安卓
大耳猫2 天前
Android Studio 多工程公用module引用
android·java·kotlin·android studio
xiaoerbuyu12333 天前
单选按钮 带角标
android studio
----云烟----3 天前
如何更改Android studio的项目存储路径
android·ide·android studio
YunFeiDong3 天前
Android Studio打包时不显示“Generate Signed APK”提示信息
android·ide·android studio
----云烟----4 天前
Android Studio各种历史版本
android·ide·android studio