神奇的插件,卷。 2.练习

在这里,您可以阅读有关插件工程理论的第一篇文章。


在这一部分中,我将告诉您在创建插件期间遇到了哪些问题以及我们如何解决这些问题。
图片


我要说什么


  • 实践部分
    • 多页用户界面
    • 插件中的DI
    • 代码生成
    • 代码修改
  • 接下来要做什么?
    • 小费
    • 常见问题

多页用户界面


我们需要做的第一件事是创建一个多页面的UI。 我们使用了许多复选标记和输入字段来制作了第一个复杂表格。 过了一会儿,我们决定增加选择用户可以连接到新模块的模块列表的功能。 我们还希望选择计划将创建的模块连接到的应用程序模块。


在一种表单上拥有如此多的控件不是很方便,因此他们制作了三个单独的页面,三个单独的小甜饼切割器。 简而言之,向导对话框。


图片


但是由于在插件中制作多页UI非常痛苦,所以我想找一些准备好的东西。 在IDEA的肠道中,我们发现了一个名为WizardDialog的类。


图片


这是常规对话框上的包装器类,可独立监视向导中用户的进度,并显示必要的按钮(上一个,下一个,完成等)。 特殊的WizardModel附加到WizardDialog ,并向其中添加了各个WizardSteps。 每个WizardStep都是单独的表单。


对话的最简单形式如下:


巫师对话
class MyWizardDialog( model: MyWizardModel, private val onFinishButtonClickedListener: (MyWizardModel) -> Unit ): WizardDialog<MyWizardModel>(true, true, model) { override fun onWizardGoalAchieved() { super.onWizardGoalAchieved() onFinishButtonClickedListener.invoke(myModel) } } 

我们将从WizardDialog继承 ,并使用WizardModel的类进行参数 。 此类具有特殊的回调( onWizardGoalAchieved ),它告诉我们用户已通过向导结束并单击“完成”按钮。
重要的是要注意,在此类中,有机会仅访问WizardModel 。 这意味着在向导通过期间用户将收集的所有数据,必须添加到WizardModel中


向导模型
 class MyWizardModel: WizardModel("Title for my wizard") { init { this.add(MyWizardStep1()) this.add(MyWizardStep2()) this.add(MyWizardStep3()) } } 

该模型如下所示:我们从WizardModel类继承,并使用内置的add方法单独的WizardSteps 添加到对话框。


向导步骤
 class MyWizardStep1: WizardStep<MyWizardModel>() { private lateinit var contentPanel: JPanel override fun prepare(state: WizardNavigationState?): JComponent { return contentPanel } } 

WizardSteps也很简单:我们从WizardStep类继承,使用我们的模型类对其进行参数化,最重要的是,重新定义prepare方法,该方法返回您将来表单的根组件。


简单来说,它看起来确实像这样。 但是在现实世界中,您的表单很可能类似于以下内容:


图片


在这里,您可以回想起那些我们在Android世界中还不知道什么是Clean Architecture,MVP并在一个Activity中编写所有代码的时代。 架构之战有了新的领域,如果您想弄糊涂,可以为插件实现自己的架构。


结论


如果您需要多页UI,请使用WizardDialog-这样会更容易。


我们进入下一个主题-插件中的DI。


插件中的DI


为什么可能需要在插件内部进行依赖注入?
第一个原因是插件内部架构的组织。


看起来,为什么通常在插件内部观察某种架构? 插件是一种实用程序,一旦我写了,就是这样,我忘记了。
是的,但是没有。
当您的插件增长时,当您编写大量代码时,结构化代码的问题就自动出现了。 DI在这里可能会派上用场。


第二个更重要的原因-在DI的帮助下,您可以找到由其他插件的开发人员编写的组件。 它可以是事件总线,记录器等等。


尽管您可以自由使用任何DI框架(例如Spring,Dagger等),但在IntelliJ IDEA中还是有您自己的DI框架,它基于前三个抽象级别,我已经在上面提到过: ApplicationProjectModule


图片


每个级别都有其自己的抽象,称为Component 。 根据该级别的对象的实例创建所需级别的组件。 因此,为Application类的每个实例创建一次ApplicationComponent ,类似于Project实例的ProjectComponent ,依此类推。


使用DI框架需要做什么?


首先,创建一个实现我们需要的接口组件之一的类-例如,一个实现ApplicationComponentProjectComponentModuleComponent的类 。 同时,我们有机会注入要实现其接口的级别的对象。 也就是说,例如,您可以在ProjectComponent中注入Project类的对象。


创建组件类
 class MyAppComponent( val application: Application, val anotherApplicationComponent: AnotherAppComponent ): ApplicationComponent class MyProjectComponent( val project: Project, val anotherProjectComponent: AnotherProjectComponent, val myAppComponent: MyAppComponent ): ProjectComponent class MyModuleComponent( val module: Module, val anotherModuleComponent: AnotherModuleComponent, val myProjectComponent: MyProjectComponent, val myAppComponent: MyAppComponent ): ModuleComponent 

其次,可以注入相同或更高级别的其他组件。 也就是说,例如,您可以在ProjectComponent中注入其他ProjectComponentApplicationComponent 。 在这里您可以访问“外来”组件的实例。


同时,IDEA保证将正确地组装整个依赖图,所有对象都将以正确的顺序创建并正确初始化。


下一步是在plugin.xml文件中注册组件。 一旦实现了Component接口之一(例如ApplicationComponent ),IDEA将立即提供将您的组件注册在plugin.xml中的功能。


在plugin.xml中注册组件
 <idea-plugin> ... <project-components> <component> <interface-class> com.experiment.MyProjectComponent </interface-class> <implementation-class> com.experiments.MyProjectComponentImpl </implementation-class> </component> </project-components> </idea-plugin> 

怎么做? 出现一个特殊的<project-component>标记( <application-component><module-component> -取决于级别)。 它内部有一个标签,其中还有两个标签: <interface-class> (表示组件的接口名称)和<implementation-class> (表示实现类)。 一个类可以是组件的接口,也可以是其实现,因此可以使用单个<implementation-class>标签

最后要做的是从相应的对象中获取组件,即,我们从Application实例中获取ApplicationComponent ,从Project中获取ProjectComponent ,等等。


获取组件
 val myAppComponent = application.getComponent(MyAppComponent::class.java) val myProjectComponent = project.getComponent(MyProjectComponent::class.java) val myModuleComponent = module.getComponent(MyModuleComponent::class.java) 

结论


  1. IDEA中有一个DI框架-无需自己拖任何东西:Dagger和Spring都没有。 当然可以。
  2. 使用此DI,您可以获取完成的组件,这就是果汁本身。

让我们继续第三个任务-代码生成。


代码生成


还记得吗,在清单中,我们有生成大量文件的任务? 每次创建新模块时,我们都会创建一堆文件:交互器,演示者,片段。 创建新模块时,这些组件彼此非常相似,我想学习如何自动生成此框架。


模式


生成大量相似代码的最简单方法是什么? 使用模式。 首先,您需要查看模板并了解对代码生成器提出了哪些要求。


一块build.gradle文件模板
 apply plugin: 'com.android.library' <if (isKotlinProject) { apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' <if (isModuleWithUI) { apply plugin: 'kotlin-android-extensions' }> }> ... android { ... <if (isMoxyEnabled) { kapt { arguments { arg("moxyReflectorPackage", '<include var="packageName">') } } }> ... } ... dependencies { compileOnly project(':common') compileOnly project(':core-utils') <for (moduleName in enabledModules) { compileOnly project('<include var="moduleName">') }> ... } 

首先:我们希望能够在这些模式中使用条件。 我举一个例子:如果插件以某种方式与UI连接,我们想连接特殊的Gradle-plugin kotlin-android-extensions


模板中的条件
 <if (isKotlinProject) { apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' <if (isModuleWithUI) { apply plugin: 'kotlin-android-extensions' }> }> 

我们想要的第二件事是能够在此模板内使用变量。 例如,当我们为Moxy配置kapt时,我们想将包名称作为注释处理器的参数插入。


在模板中替换变量的值
 kapt { arguments { arg("moxyReflectorPackage", '<include var="packageName">') } } 

我们需要的另一件事是能够处理模板内的循环。 还记得我们在表格中选择了要连接到正在创建的新模块的模块列表吗? 我们想循环处理它们并添加同一行。


使用模板中的循环。
 <for (moduleName in enabledModules) { compileOnly project('<include var="moduleName">') }> 

因此,我们为代码生成器提出了三个条件:


  • 我们要使用条件
  • 替代变量值的能力
  • 我们需要模式循环

代码生成器


有哪些实现代码生成器的选项? 例如,您可以编写自己的代码生成器。 例如,Uber的人就是这样做的:他们编写了自己的插件来生成Riblet (所谓的建筑单元)。 他们提出了自己的模板语言 ,他们只使用了插入变量的能力。 他们使条件达到了发电机水平 。 但是我们认为我们不会那样做。


第二种选择是使用IDEA中内置的FileTemplateManager实用工具类,但我不建议这样做。 因为他具有Velocity作为引擎,所以在将Java对象转发到模板方面存在一些问题。 此外, FileTemplateManager无法从框中生成Java或XML以外的文件。 我们需要生成Groovy文件,Kotlin,Proguard和其他类型的文件。


第三个选项是... FreeMarker 。 如果您有现成的FreeMarker模板,请不要着急丢弃它们-它们在插件内对您很有用。


需要做什么,如何在插件中使用FreeMarker ? 首先,添加文件模板。 您可以在/ resources文件夹内创建/ templates文件夹,并在其中添加所有文件的所有模板-演示者,片段等。


图片


之后,您将需要在FreeMarker库上添加一个依赖项。 由于该插件使用Gradle,因此添加依赖项非常简单。


在FreeMarker库上添加依赖项
 dependencies { ... compile 'org.freemarker:freemarker:2.3.28' } 

之后,在我们的插件中配置FreeMarker。 我建议您在此简单复制此配置-受其折磨,受苦,复制它,一切正常。


FreeMarker配置
 class TemplatesFactory(val project: Project) : ProjectComponent { private val freeMarkerConfig by lazy { Configuration(Configuration.VERSION_2_3_28).apply { setClassForTemplateLoading( TemplatesFactory::class.java, "/templates" ) defaultEncoding = Charsets.UTF_8.name() templateExceptionHandler = TemplateExceptionHandler.RETHROW_HANDLER logTemplateExceptions = false wrapUncheckedExceptions = true } } ... 

现在该使用FreeMarker创建文件了。 为此,我们从配置中按名称获取模板,并使用常规FileWriter在磁盘上直接创建具有所需文本的文件。


通过FileWriter创建文件
 class TemplatesFactory(val project: Project) : ProjectComponent { ... fun generate( pathToFile: String, templateFileName: String, data: Map<String, Any> ) { val template = freeMarkerConfig.getTemplate(templateFileName) FileWriter(pathToFile, false).use { writer -> template.process(data, writer) } } } 

任务似乎已经解决,但是没有。 在理论部分,我提到整个IDEA都渗透了PSI结构,因此必须予以考虑。 如果您绕过PSI结构创建文件(例如,通过FileWriter),则IDEA根本不会理解您已创建了文件,因此不会在项目树中显示文件。 我们等了大约7分钟,然后IDEA索引了,然后看到了创建的文件。


结论-考虑PSI的结构,正确执行操作,创建文件。


为文件创建PSI结构


首先, 让我们看一下使用PsiDirectory的文件夹结构。 可以使用扩展功能guessProjectDirtoPsiDirectory获得项目的开始目录:


获取PsiDirectory项目
 val projectPsiDirectory = project.guessProjectDir()?.toPsiDirectory(project) 

后续目录可以使用PsiDirectory findSubdirectory类方法找到,也可以使用createSubdirectory方法创建。


查找并创建PsiDirectory
 val coreModuleDir = projectPsiDirectory.findSubdirectory("core") val newModulePsiDir = coreModuleDir.createSubdirectory(config.mainParams.moduleName) 

我还建议您创建一个Map ,使用字符串键从中获取所有PsiDirectory文件夹结构,然后将创建的文件添加到这些文件夹中的任何一个。


创建文件夹结构图

返回mutableMapOf <String,PsiDirectory?>()。套用{
这个[“ root”] = modulePsiDir
这个[“ src”] = modulePsiDir.createSubdirectory(“ src”)
this [“ main”] = this [“ src”]?。createSubdirectory(“ main”)
this [“ java”] = this [“ main”]?。createSubdirectory(“ java”)
this [“ res”] = this [“ main”]?。createSubdirectory(“ res”)


 //   PsiDirectory   package name: // ru.hh.feature_worknear → ru / hh / feature_worknear createPackageNameFolder(config) // data this["data"] = this["package"]?.createSubdirectory("data") // ... 

}


文件夹已创建。 我们将使用PsiFileFactory创建PsiFiles 。 此类具有称为createFileFromText的特殊方法。 该方法接受三个参数作为输入:输出文件的名称(字符串fileName),文本(字符串text)和类型(FileType fileType)。 三个参数中的两个参数清楚地说明了从何处获取它:我们自己知道名称,我们从FreeMarker获得了文本。 在哪里获取FileType ? 到底是什么呢?


档案类型


FileType是一个特殊的类,表示文件的类型。 “框”中只有两个FileType可供我们使用:JavaFileType和XmlFileType,分别用于Java文件和XML文件。 但是问题来了:从哪里获得build.gradle文件, Kotlin文件, Proguard.gitignore的类型 ,最后呢?


首先,大多数这些FileType可以从某人已经编写的其他插件中获取。 GroovyFileType可以从Groovy插件获取 ,KotlinFileType可以从Kotlin插件获取 ,Proguard可以从Android插件获取


我们如何将另一个插件的依赖关系添加到我们的插件中? 我们使用gradle-intellij-plugin 。 它在插件的build.gradle文件中添加了一个特殊的intellij块,该文件中有一个特殊的属性-plugins。 在此属性中,您可以列出我们要依赖的插件标识符列表。


添加对其他插件的依赖
 // build.gradle  intellij { … plugins = ['android', 'Groovy', 'kotlin'] } 

我们从官方JetBrains插件存储库中获取密钥。 对于内置在IDEA中的插件(Groovy,Kotlin和Android),IDEA中的插件文件夹名称就足够了。 其余的,您需要转到官方JetBrains插件存储库中特定插件的页面,该位置将显示Plugin XML ID属性以及版本(例如,这是Docker插件页面 )。 阅读有关在GitHub上连接其他插件的更多信息。


其次,您需要在plugin.xml文件中添加一个依赖项描述。 这是使用标记完成的

我们在plugin.xml中连接插件
 <idea-plugin> ... <depends>org.jetbrains.android</depends> <depends>org.jetbrains.kotlin</depends> <depends>org.intellij.groovy</depends> </idea-plugin> 

同步项目后,我们将加强来自其他插件的依赖关系,并能够使用它们。


但是,如果我们不想依赖其他插件怎么办? 在这种情况下,我们可以为所需的文件类型创建一个存根。 为此,首先创建一个将从Language类继承的类。 我们的编程语言的唯一标识符将传递给此类(在我们的示例中为“ ru.hh.plugins.Ignore” )。


为GitIgnore文件创建语言
 class IgnoreLanguage private constructor() : Language("ru.hh.plugins.Ignore", "ignore", null), InjectableLanguage { companion object { val INSTANCE = IgnoreLanguage() } override fun getDisplayName(): String { return "Ignore() ($id)" } } 

这里有一个功能:一些开发人员添加一个非唯一的行作为标识符。 因此,您的插件与其他插件的集成可能会中断。 我们很棒,我们有一条独特的路线。


创建Language之后,接下来要做的就是创建FileType 。 我们从LanguageFileType类继承,使用我们定义的语言实例进行初始化,覆盖一些非常简单的方法。 做完了 现在我们可以使用新创建的FileType


为.gitignore创建自己的FileType
 class IgnoreFileType(language: Language) : LanguageFileType(language) { companion object { val INSTANCE = IgnoreFileType(IgnoreLanguage.INSTANCE) } override fun getName(): String = "gitignore file" override fun getDescription(): String = "gitignore files" override fun getDefaultExtension(): String = "gitignore" override fun getIcon(): Icon? = null } 

完成创建文件


找到所有必需的FileType之后 ,我建议创建一个名为TemplateData的特殊容器-该容器将包含有关要从其生成代码的模板的所有数据。 它将包含模板文件的名称,生成代码后获得的输出文件的名称,所需的FileType以及最后的PsiDirectory ,在其中添加创建的文件。


模板数据
 data class TemplateData( val templateFileName: String, val outputFileName: String, val outputFileType: FileType, val outputFilePsiDirectory: PsiDirectory? ) 

然后返回FreeMarker-我们从其中获取模板文件,使用StringWriter获取文本,在PsiFileFactory中生成具有所需文本和类型的PsiFile 。 创建的文件将添加到所需目录。


在所需的文件夹中创建PsiFile
 fun createFromTemplate(data: FileTemplateData, properties: Map<String, Any>): PsiFile { val template = freeMarkerConfig.getTemplate(data.templateFileName) val text = StringWriter().use { writer -> template.process(properties, writer) writer.buffer.toString() } return psiFileFactory.createFileFromText(data.outputFileName, data.outputFileType, text) } 

因此,考虑了PSI结构,IDEA和其他插件将看到我们所做的事情。 这样做有好处:例如,如果Git插件发现您已添加了新文件,它将自动显示一个对话框,询问您是否要将这些文件添加到Git?


代码生成结论


  • 文本文件可以由FreeMarker生成。 很舒服
  • 生成文件时,您需要考虑PSI结构,否则一切都会出错。
  • 如果要使用PsiFileFactory生成文件,则必须在某处找到FileType。

好了,现在我们来看最后一个最实用的部分-这是对代码的修改。


代码修改


实际上,仅创建用于代码生成的插件是胡说八道,因为您可以使用其他工具和相同的FreeMarker来生成代码。 但是FreeMarker不能做的就是修改代码。


我们的清单有几项与修改代码有关的任务,让我们从最简单的一项开始-修改settings.gradle文件。


修改settings.gradle


让我提醒您我们想要做什么:我们需要在此文件中添加几行内容,以描述新创建的模块的路径:


模块路径说明
 // settings.gradle include ':analytics project(':analytics').projectDir = new File(settingsDir, 'core/framework-metrics/analytics) ... include ':feature-worknear' project(':feature-worknear').projectDir = new File(settingsDir, 'feature/feature-worknear') 

我稍早吓到您,在处理文件时必须始终考虑PSI结构,否则 一切都会燃烧 将无法正常工作。 实际上,在简单的任务中(例如在文件末尾添加几行),可以将其省略。 您可以使用通常的java.io.File将一些行添加到文件中。 为此,我们找到文件的路径,创建java.io.File实例,并在Kotlin 扩展功能的帮助下,在此文件的末尾添加两行。 您可以执行此操作,IDEA将看到您的更改。


在settings.gradle文件中添加行

val projectBaseDirPath = project.basePath ?:返回
val settingsPathFile = projectBaseDirPath +“ /settings.gradle”


val settingsFile =文件(settingsPathFile)


settingsFile.appendText(“ include':$ moduleName'”)
settingsFile.appendText(
“项目(':$ moduleName')。projectDir =新文件(settingsDir,'$ folderPath')”


好吧,理想情况下,当然,通过PSI结构会更好-它更可靠。


Kapt调整牙签


我再次提醒您这个问题:在应用程序模块中有一个build.gradle文件,并且在其中有注释处理器的设置。 我们想将我们创建的模块的软件包添加到特定位置。


在哪里?

图片


我们的目标是找到一个特定的PsiElement ,然后计划添加我们的线。 对元素的搜索始于对PsiFile的搜索,该文件表示应用程序模块的build.gradle文件。 为此,您需要找到我们将在其中寻找文件的模块。


我们正在按名称查找模块
 val appModule = ModuleManager.getInstance(project) .modules.toList() .first { it.name == "headhunter-applicant" } 

接下来,使用实用程序类FilenameIndex,可以通过其名称查找PsiFile ,并将找到的模块指定为搜索区域。


按名称查找PsiFile
 val buildGradlePsiFile = FilenameIndex.getFilesByName( appModule.project, "build.gradle", appModule.moduleContentScope ).first() 

找到PsiFile之后,我们可以开始搜索PsiElement。 , – PSI Viewer . IDEA , PSI- .


图片


- (, build.gradle) , PSI- .


图片


– , PsiFile -.


. PsiFile . .


PsiElement
 val toothpickRegistryPsiElement = buildGradlePsiFile.originalFile .collectDescendantsOfType<GrAssignmentExpression>() .firstOrNull { it.text.startsWith("arguments") } ?.lastChild ?.children?.firstOrNull { it.text.startsWith("toothpick_registry_children_package_names") } ?.collectDescendantsOfType<GrListOrMap>() ?.first() ?: return 

?.. ? PSI-. GrAssignmentExpression , , arguments = [ … ] . , toothpick_registry_children_package_names = [...] , Groovy-.


PsiElement , . . .


PSI- , PsiElementFactory , . Java-? Java-. Groovy? GroovyPsiElementFactory . 依此类推。


PsiElementFactory . Groovy Kotlin , .


PsiElement package name
 val factory = GroovyPsiElementFactory.getInstance(buildGradlePsiFile.project) val packageName = config.mainParams.packageName val newArgumentItem = factory.createStringLiteralForReference(packageName) 

PsiElement .


Map-
 targetPsiElement.add(newArgumentItem) 

kapt- Moxy application


-, , – kapt- Moxy application . : @RegisterMoxyReflectorPackages .


-?

图片


, : PsiFile , PsiElement , … , PsiElement -.


: , @RegisterMoxyReflectorPackages , value , .


, . , PsiManager , PsiClass .


PsiClass @RegisterMoxyReflectorPackages
 val appModule = ModuleManager.getInstance(project) .modules.toList() .first { it.name == "headhunter-applicant" } val psiManager = PsiManager.getInstance(appModule.project) val annotationPsiClass = ClassUtil.findPsiClass( psiManager, "com.arellomobile.mvp.RegisterMoxyReflectorPackages" ) ?: return 

AnnotatedMembersSearch , .


,
 val annotatedPsiClass = AnnotatedMembersSearch.search( annotationPsiClass, appModule.moduleContentScope ).findAll() ?.firstOrNull() ?: return 

, PsiElement , value. , .


 val annotationPsiElement = (annotatedPsiClass .annotations .first() as KtLightAnnotationForSourceEntry ).kotlinOrigin val packagesPsiElements = annotationPsiElement .collectDescendantsOfType<KtValueArgumentList>() .first() .collectDescendantsOfType<KtValueArgument>() val updatedPackagesList = packagesPsiElements .mapTo(mutableListOf()) { it.text } .apply { this += "\"${config.packageName}\"" } val newAnnotationValue = updatedPackagesList.joinToString(separator = ",\n") 

KtPsiFactory PsiElement – .


 val kotlinPsiFactory = KtPsiFactory(project) val newAnnotationPsiElement = kotlinPsiFactory.createAnnotationEntry( "@RegisterMoxyReflectorPackages(\n$newAnnotationValue\n)" ) val replaced = annotationPsiElement.replace(newAnnotationPsiElement) 

.


? code style. , IDEA : CodeStyleManager.


code style
 CodeStyleManager.getInstance(module.project).reformat(replacedElement) 

- , .



  • , PSI-, .
  • , PSI , , PsiElement-.

?


.


  • – , .
  • . . : .
  • ? . , IDEA , . , . — - , GitHub . , , .
  • - – IntelliJ IDEA . , Util Manager , , , .
  • : . , runIde , IDEA, . , hh.ru, .

仅此而已。 , , – .


常见问题


  • ?

, . , 2 3 .


  • IDEA , - ?

, IDEA IDEA SDK , deprecated, , . SDK- , , .


  • ?

– gitignore . - .


  • ?

Android Studio Mac OS, Ubuntu, . , Windows, .

Source: https://habr.com/ru/post/zh-CN463611/


All Articles