目标:

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

设计:

开端构想,用户挑选要导入的文件夹,遍历文件夹下面文件的静态办法并存储,当用户输入时运用弹窗显现候选办法,选中后补全代码。 分化过程为:

  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: 声明插件依靠项

自定义IDEA代码补全插件

更多的特点能够查阅此链接

github.com/JetBrains/i…

github.com/JetBrains/g…

plugin.xml

文件位于srcmainresourcesMETA-INF下

  • id:gradle.properties里的pluginName_

  • name: gradle.properties里的pluginName_

  • vendor:开发者的名字

    自定义IDEA代码补全插件

    增加依靠

自定义IDEA代码补全插件

build.gradle.kts

在intellij节点下参加一句intellij

alternativeIdePath = "H:AndroidAndroid Studio"

自定义IDEA代码补全插件

途径设置为本地Android Studio方位,这样在运转时会直接运用本地的AS调试,避免从头下载Android Studio。

settings.gradle.kts

修改项目称号

rootProject.name = "Plugin Template Hint"

装备完结后,点击右边的gradle的runide即可运转插件,假如开发过程中想进行调试能够右键挑选debug形式。

自定义IDEA代码补全插件

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 = 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 ""
    }	
}

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

作用如下:

自定义IDEA代码补全插件

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) ?: 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

    <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里可选.
    自定义IDEA代码补全插件
  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.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"

    <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,在这儿也能够更改快捷键。

自定义IDEA代码补全插件
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 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元素的内部描述.。 作用如图,能够看到相关的类,然后就能够再去找到具体的完结办法。

自定义IDEA代码补全插件

最终作用如下: 在这儿刺进图片描述

相关材料

IntelliJ Platform SDK

运用PSI剖析Java代码

Intellij IDEA 插件开发秘籍