Ici vous pouvez lire le premier article avec la théorie de l'ingénierie des plug-ins.
Et dans cette partie, je vais vous dire quels problèmes nous avons rencontrés lors de la création du plugin et comment nous avons essayé de les résoudre.

De quoi vais-je parler?
- Partie pratique
- Interface multipage
- DI dans les plugins
- Génération de code
- Modification du code
- Que faire ensuite?
Interface multipage
La première chose que nous devions faire était de créer une interface utilisateur multi-pages. Nous avons créé le premier formulaire complexe avec un tas de coches, des champs de saisie. Un peu plus tard, nous avons décidé d'ajouter la possibilité de sélectionner une liste de modules que l'utilisateur peut connecter au nouveau module. Et nous voulons également choisir les modules d'application auxquels nous prévoyons de connecter le module créé.
Avoir autant de contrôles sur un seul formulaire n'est pas très pratique, ils ont donc fait trois pages distinctes, trois emporte-pièces séparés. En bref, la boîte de dialogue Assistant.

Mais comme créer une interface utilisateur multi-pages dans des plugins est très douloureux, je voulais trouver quelque chose de prêt. Et dans les entrailles d'IDEA, nous avons découvert une classe appelée WizardDialog .

Il s'agit d'une classe wrapper sur une boîte de dialogue standard, surveillant indépendamment la progression de l'utilisateur dans l'assistant et affichant les boutons nécessaires (Précédent, Suivant, Terminer, etc.). Un WizardModel spécial est attaché au WizardDialog , auquel des WizardSteps individuels sont ajoutés. Chaque WizardStep est un formulaire distinct.
Dans sa forme la plus simple, la mise en œuvre du dialogue est la suivante:
Wizarddialogclass MyWizardDialog( model: MyWizardModel, private val onFinishButtonClickedListener: (MyWizardModel) -> Unit ): WizardDialog<MyWizardModel>(true, true, model) { override fun onWizardGoalAchieved() { super.onWizardGoalAchieved() onFinishButtonClickedListener.invoke(myModel) } }
Nous allons hériter de la classe WizardDialog , paramétrer avec la classe de notre WizardModel . Cette classe a un rappel spécial ( onWizardGoalAchieved ), qui nous indique que l'utilisateur est passé par l'assistant jusqu'à la fin et a cliqué sur le bouton "Terminer".
Il est important de noter qu'à partir de cette classe, il est possible d'atteindre uniquement le WizardModel . Cela signifie que toutes les données que l'utilisateur collectera lors du passage de l'assistant, vous devez les ajouter dans le WizardModel .
Wizardmodel class MyWizardModel: WizardModel("Title for my wizard") { init { this.add(MyWizardStep1()) this.add(MyWizardStep2()) this.add(MyWizardStep3()) } }
Le modèle est le suivant: nous héritons de la classe WizardModel et en utilisant la méthode add intégrée, nous ajoutons des WizardSteps séparés à la boîte de dialogue.
Wizardstep class MyWizardStep1: WizardStep<MyWizardModel>() { private lateinit var contentPanel: JPanel override fun prepare(state: WizardNavigationState?): JComponent { return contentPanel } }
Les WizardSteps sont également simples: nous héritons de la classe WizardStep , la paramétrons avec notre classe de modèle et, surtout, redéfinissons la méthode prepare, qui renvoie le composant racine de votre futur formulaire.
En termes simples, cela ressemble vraiment à ceci. Mais dans le monde réel, votre formulaire ressemblera probablement à quelque chose comme ceci:

Ici, vous vous souvenez de ces moments où nous, dans le monde Android, ne savions pas encore ce qu'est l'architecture propre, MVP et avons écrit tout le code en une seule activité. Il y a un nouveau champ pour les batailles architecturales, et si vous voulez vous embrouiller, vous pouvez implémenter votre propre architecture pour les plugins.
Conclusion
Si vous avez besoin d'une interface utilisateur multi-pages, utilisez WizardDialog - ce sera plus facile.
Nous passons au sujet suivant - DI dans les plugins.
DI dans les plugins
Pourquoi une injection de dépendance dans un plug-in peut-elle être requise?
La première raison est l'organisation de l'architecture à l'intérieur du plugin.
Il semblerait, pourquoi généralement observer une sorte d'architecture à l'intérieur du plugin? Un plug-in est une chose utilitaire, une fois que je l'ai écrit, et c'est tout, j'ai oublié.
Oui, mais non.
Lorsque votre plugin grandit, lorsque vous écrivez beaucoup de code, la question du code structuré se pose d'elle-même. Ici, DI peut être utile.
La deuxième raison, plus importante - avec l'aide de DI, vous pouvez atteindre les composants écrits par les développeurs d'autres plugins. Il peut s'agir de bus d'événements, d'enregistreurs et bien plus encore.
Malgré le fait que vous êtes libre d'utiliser n'importe quel framework DI (Spring, Dagger, etc.), à l'intérieur d'IntelliJ IDEA il y a votre propre framework DI, qui est basé sur les trois premiers niveaux d'abstraction, dont j'ai déjà parlé: Application , Project et Module .

Chacun de ces niveaux a sa propre abstraction appelée Composant . Le composant du niveau requis est créé par instance de l'objet de ce niveau. Ainsi, ApplicationComponent est créé une fois pour chaque instance de la classe Application , de manière similaire à ProjectComponent pour les instances de Project , etc.
Que faut-il faire pour utiliser le cadre DI?
Tout d'abord, créez une classe qui implémente l'un des composants d'interface nécessaires - par exemple, une classe qui implémente ApplicationComponent , ou ProjectComponent , ou ModuleComponent . Dans le même temps, nous avons la possibilité d'injecter un objet du niveau dont nous mettons en œuvre l'interface. Autrement dit, dans ProjectComponent, vous pouvez injecter un objet de la classe Project .
Création de classes de composants 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
Deuxièmement, il est possible d'injecter d'autres composants du même niveau ou plus. C'est-à -dire, par exemple, dans ProjectComponent, vous pouvez injecter d'autres ProjectComponent ou ApplicationComponent . C'est là que vous pouvez accéder aux instances de composants "étrangers".
Dans le même temps, IDEA garantit que l'ensemble du graphe de dépendances sera correctement assemblé, tous les objets seront créés dans le bon ordre et correctement initialisés.
La prochaine chose à faire est d'enregistrer le composant dans le fichier plugin.xml . Dès que vous implémentez l'une des interfaces de composant (par exemple, ApplicationComponent ), IDEA vous propose immédiatement d'enregistrer votre composant dans plugin.xml.
Enregistrer le composant dans 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>
Comment cela se fait-il? Une balise <project-component> spéciale apparaît ( <application-component> , <module-component> - selon le niveau). Il y a une balise à l'intérieur , il en a deux autres: <interface-class> , où le nom d'interface de votre composant est indiqué, et <implementation-class> , où la classe d'implémentation est indiquée. Une même classe peut être une interface d'un composant ou son implémentation, vous pouvez donc le faire avec une seule balise <implementation-class> .
La dernière chose à faire est d'obtenir le composant à partir de l'objet correspondant, c'est-à -dire que nous obtenons ApplicationComponent à partir de l'instance Application , ProjectComponent à partir de Project , etc.
Obtenez le composant val myAppComponent = application.getComponent(MyAppComponent::class.java) val myProjectComponent = project.getComponent(MyProjectComponent::class.java) val myModuleComponent = module.getComponent(MyModuleComponent::class.java)
Conclusions
- Il y a un framework DI dans IDEA - pas besoin de faire glisser quoi que ce soit par vous-même: ni Dagger ni Spring. Bien sûr, vous le pouvez.
- Avec cette DI, vous pouvez atteindre les composants prĂŞts Ă l'emploi, et c'est le jus lui-mĂŞme.
Passons à la troisième tâche - la génération de code.
Génération de code
Rappelez-vous, dans la liste de contrôle, nous avions pour tâche de générer un grand nombre de fichiers? Chaque fois que nous créons un nouveau module, nous créons un tas de fichiers: interacteurs, présentateurs, fragments. Lors de la création d'un nouveau module, ces composants sont très similaires les uns aux autres, et j'aimerais apprendre à générer ce framework automatiquement.
Patterns
Quelle est la façon la plus simple de générer une tonne de code similaire? Utilisez des modèles. Vous devez d'abord regarder vos modèles et comprendre quelles exigences sont mises en avant pour le générateur de code.
Un morceau du modèle de fichier 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">') }> ... }
Premièrement: nous voulions pouvoir utiliser des conditions à l'intérieur de ces modèles. Je donne un exemple: si le plugin est en quelque sorte connecté avec l'interface utilisateur, nous voulons connecter les extensions spéciales kotlin-android-extensions Gradle-plugin.
Condition à l'intérieur du modèle <if (isKotlinProject) { apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' <if (isModuleWithUI) { apply plugin: 'kotlin-android-extensions' }> }>
La deuxième chose que nous voulons, c'est la possibilité d'utiliser une variable à l'intérieur de ce modèle. Par exemple, lorsque nous configurons kapt pour Moxy, nous voulons insérer le nom du package comme argument dans le processeur d'annotation.
Remplacez la valeur de la variable à l'intérieur du modèle kapt { arguments { arg("moxyReflectorPackage", '<include var="packageName">') } }
Une autre chose dont nous avons besoin est la possibilité de gérer les boucles à l'intérieur du modèle. Rappelez-vous le formulaire dans lequel nous avons sélectionné la liste des modules que nous voulons connecter au nouveau module en cours de création? Nous voulons les contourner en boucle et ajouter la même ligne.
Utilisez la boucle dans le modèle. <for (moduleName in enabledModules) { compileOnly project('<include var="moduleName">') }>
Ainsi, nous proposons trois conditions pour le générateur de code:
- Nous voulons utiliser les conditions
- Possibilité de substituer des valeurs variables
- Nous avons besoin de boucles dans les modèles
Générateurs de code
Quelles sont les options pour implémenter le générateur de code? Vous pouvez, par exemple, écrire votre propre générateur de code. Par exemple, les gars d'Uber l'ont fait: ils ont écrit leur propre plug-in pour générer des riblets (les soi-disant unités architecturales). Ils ont créé leur propre langage de modèle , dans lequel ils n'ont utilisé que la possibilité d'insérer des variables. Ils ont amené les conditions au niveau du générateur . Mais nous pensions que nous ne ferions pas cela.
La deuxième option consiste à utiliser la classe d'utilitaires FileTemplateManager intégrée à IDEA, mais je ne recommanderais pas de le faire. Parce qu'il a Velocity comme moteur, ce qui a quelques problèmes avec la transmission d'objets Java vers des modèles. De plus, FileTemplateManager ne peut pas générer de fichiers autres que Java ou XML à partir de la boîte. Et nous devions générer des fichiers Groovy, Kotlin, Proguard et d'autres types de fichiers.
La troisième option était ... FreeMarker . Si vous avez des modèles FreeMarker prêts à l'emploi, ne vous précipitez pas pour les jeter - ils peuvent vous être utiles à l'intérieur du plugin.
Que faut-il faire, quoi utiliser FreeMarker dans le plugin? Tout d'abord, ajoutez des modèles de fichiers. Vous pouvez créer le dossier / templates dans le dossier / resources et y ajouter tous nos modèles pour tous les fichiers - présentateurs, fragments, etc.

Après cela, vous devrez ajouter une dépendance à la bibliothèque FreeMarker. Étant donné que le plugin utilise Gradle, ajouter une dépendance est facile.
Ajouter une dépendance à la bibliothèque FreeMarker dependencies { ... compile 'org.freemarker:freemarker:2.3.28' }
Après cela, configurez FreeMarker dans notre plugin. Je vous conseille de simplement copier cette configuration ici - elle est torturée, subie, copiez-la et tout fonctionne.
Configuration de 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 } } ...
Il est temps de créer des fichiers à l'aide de FreeMarker . Pour ce faire, nous obtenons un modèle de la configuration par son nom et en utilisant un FileWriter normal, créez un fichier avec le texte souhaité directement sur le disque.
Création d'un fichier via 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) } } }
Et la tâche semble être résolue, mais non. Dans la partie théorique, j'ai mentionné que l'ensemble de l'IDEA est imprégné de la structure PSI, et cela doit être pris en compte. Si vous créez des fichiers en contournant la structure PSI (par exemple, via FileWriter), IDEA ne comprendra tout simplement pas que vous avez créé quelque chose et n'affichera pas les fichiers dans l'arborescence du projet. Nous avons attendu environ sept minutes avant que IDEA indexe et ne voie les fichiers créés.
Conclusion - faites-le bien, créez des fichiers, en tenant compte de la structure du PSI.
Créer une structure PSI pour les fichiers
Pour commencer, voyons la structure des dossiers à l'aide de PsiDirectory . Le répertoire de démarrage du projet peut être obtenu en utilisant les fonctions d'extension guessProjectDir et toPsiDirectory :
Obtenez le projet PsiDirectory val projectPsiDirectory = project.guessProjectDir()?.toPsiDirectory(project)
Les répertoires suivants peuvent être trouvés à l'aide de la méthode de classe PsiDirectory findSubdirectory , ou créés à l'aide de la méthode createSubdirectory .
Rechercher et créer PsiDirectory val coreModuleDir = projectPsiDirectory.findSubdirectory("core") val newModulePsiDir = coreModuleDir.createSubdirectory(config.mainParams.moduleName)
Je vous recommande également de créer une carte à partir de laquelle vous pouvez obtenir toutes les structures de dossiers PsiDirectory à l' aide d'une clé de chaîne, puis d'ajouter les fichiers créés à l'un de ces dossiers.
Créer une carte de structure de dossiersreturn mutableMapOf <String, PsiDirectory?> (). apply {
this ["root"] = modulePsiDir
this ["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") // ...
}
Dossiers créés. Nous allons créer des PsiFiles à l' aide de PsiFileFactory . Cette classe a une méthode spéciale appelée createFileFromText . La méthode accepte trois paramètres en entrée: nom (String fileName), texte (String text) et type (FileType fileType) du fichier de sortie. Deux des trois paramètres indiquent clairement où l'obtenir: nous connaissons le nom nous-mêmes, nous obtenons le texte de FreeMarker. Et où trouver FileType ? Et de quoi s'agit-il?
Type de fichier
FileType est une classe spéciale qui désigne le type de fichier. Seuls deux FileType sont à notre disposition dans la «boîte»: JavaFileType et XmlFileType, respectivement pour les fichiers Java et les fichiers XML. Mais la question se pose: où trouver les types pour le fichier build.gradle , pour les fichiers Kotlin , pour Proguard , pour .gitignore , enfin?!
Premièrement, la plupart de ces FileType peuvent être extraits d'autres plugins déjà écrits par quelqu'un. GroovyFileType peut être extrait du plugin Groovy , KotlinFileType du plugin Kotlin , Proguard du plugin Android .
Comment ajouter la dépendance d'un autre plugin au nôtre? Nous utilisons le plugin gradle-intellij . Il ajoute un bloc intellij spécial au fichier build.gradle du plugin, à l'intérieur duquel se trouve une propriété spéciale - plugins . Dans cette propriété, vous pouvez lister la liste des identifiants de plugin dont nous voulons dépendre.
Ajouter des dépendances à d'autres plugins Nous récupérons les clés du dépôt officiel du plugin JetBrains . Pour les plugins intégrés à IDEA (qui sont Groovy, Kotlin et Android), le nom du dossier du plugin dans IDEA est suffisant. Pour le reste, vous devez aller sur la page d'un plugin spécifique dans le référentiel officiel des plugins JetBrains, la propriété ID XML du plugin y sera indiquée, ainsi que la version (par exemple, voici la page du plugin Docker ). En savoir plus sur la connexion d'autres plugins sur GitHub .
Deuxièmement, vous devez ajouter une description de dépendance au fichier plugin.xml . Cela se fait à l'aide de la balise .
Nous connectons les plugins dans plugin.xml <idea-plugin> ... <depends>org.jetbrains.android</depends> <depends>org.jetbrains.kotlin</depends> <depends>org.intellij.groovy</depends> </idea-plugin>
Après avoir synchronisé le projet, nous resserrerons les dépendances des autres plugins et nous pourrons les utiliser.
Mais que se passe-t-il si nous ne voulons pas dépendre d'autres plugins? Dans ce cas, nous pouvons créer un stub pour le type de fichier dont nous avons besoin. Pour ce faire, créez d'abord une classe qui héritera de la classe Language . L'identifiant unique de notre langage de programmation sera transmis à cette classe (dans notre cas - "ru.hh.plugins.Ignore" ).
Créer une langue pour les fichiers 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)" } }
Il y a une fonctionnalité ici: certains développeurs ajoutent une ligne non unique comme identifiant. Pour cette raison, l'intégration de votre plugin avec d'autres plugins peut se casser. Nous sommes super, nous avons une ligne unique.
La prochaine chose à faire après avoir créé Language est de créer un FileType . Nous héritons de la classe LanguageFileType , utilisons l'instance de langue que nous avons définie pour initialiser, remplaçons certaines méthodes très simples. C'est fait. Nous pouvons maintenant utiliser le FileType nouvellement créé.
Créez votre propre FileType pour .gitignore 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 }
Terminez la création du fichier
Après avoir trouvé tous les FileType nécessaires, je recommande de créer un conteneur spécial appelé TemplateData - il contiendra toutes les données sur le modèle à partir duquel vous souhaitez générer du code. Il contiendra le nom du fichier modèle, le nom du fichier de sortie que vous obtenez après avoir généré le code, le FileType souhaité et enfin PsiDirectory , où vous ajoutez le fichier créé.
TemplateData data class TemplateData( val templateFileName: String, val outputFileName: String, val outputFileType: FileType, val outputFilePsiDirectory: PsiDirectory? )
Ensuite, nous revenons à FreeMarker - nous en obtenons le fichier modèle, en utilisant StringWriter nous obtenons le texte, dans PsiFileFactory nous générons le PsiFile avec le texte et le type souhaités. Le fichier créé est ajouté au répertoire souhaité.
Créez PsiFile dans le dossier souhaité 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) }
Ainsi, la structure PSI est prise en compte, et IDEA, ainsi que d'autres plugins, verront ce que nous avons fait. Cela peut être avantageux: par exemple, si le plugin pour Git voit que vous avez ajouté un nouveau fichier, il affichera automatiquement une boîte de dialogue vous demandant si vous souhaitez ajouter ces fichiers à Git?
Conclusions sur la génération de code
- Les fichiers texte peuvent être générés par FreeMarker. Très confortable.
- Lors de la génération de fichiers, vous devez tenir compte de la structure PSI, sinon tout ira mal.
- Si vous souhaitez générer des fichiers à l'aide de PsiFileFactory, vous devrez trouver FileType quelque part.
Eh bien, nous passons maintenant à la dernière, la partie pratique la plus délicieuse - il s'agit d'une modification du code.
Modification du code
En fait, créer un plugin uniquement pour générer du code est un non-sens, car vous pouvez générer du code avec d'autres outils et le même FreeMarker . Mais ce que FreeMarker ne peut pas faire, c'est modifier le code.
Notre liste de contrôle a plusieurs tâches liées à la modification du code, commençons par la plus simple - la modification du fichier settings.gradle .
Modification settings.gradle
Permettez-moi de vous rappeler ce que nous voulons faire: nous devons ajouter quelques lignes à ce fichier qui décriront le chemin d'accès au module nouvellement créé:
Description du chemin d'accès au module Je vous ai fait peur un peu plus tôt que vous devez toujours prendre en compte la structure PSI lorsque vous travaillez avec des fichiers, sinon tout va brûler ne fonctionnera pas. En fait, dans les tâches simples, telles que l'ajout de quelques lignes à la fin d'un fichier, cela peut être omis. Vous pouvez ajouter quelques lignes au fichier en utilisant le fichier java.io.File habituel. Pour ce faire, nous trouvons le chemin d'accès au fichier, créons l'instance java.io.File et à l'aide des fonctions d' extension Kotlin, ajoutez deux lignes à la fin de ce fichier. Vous pouvez le faire, IDEA verra vos modifications.
Ajout de lignes au fichier settings.gradleval projectBaseDirPath = project.basePath?: retour
val settingsPathFile = projectBaseDirPath + "/settings.gradle"
val settingsFile = File (settingsPathFile)
settingsFile.appendText ("include ': $ moduleName'")
settingsFile.appendText (
"project (': $ moduleName'). projectDir = new File (settingsDir, '$ folderPath')"
)
Eh bien, idéalement, bien sûr, c'est mieux grâce à la structure PSI - c'est plus fiable.
Réglage Kapt pour cure-dents
Encore une fois, je vous rappelle le problème: dans le module d'application, il y a un fichier build.gradle , et à l'intérieur il y a des paramètres pour le processeur d'annotation. Et nous voulons ajouter un package de notre module créé à un endroit spécifique.
Notre objectif est de trouver un PsiElement spécifique, après quoi nous prévoyons d'ajouter notre ligne. La recherche de l'élément commence par la recherche du PsiFile , qui désigne le fichier build.gradle du module d'application. Et pour cela, vous devez trouver le module dans lequel nous rechercherons le fichier.
Nous recherchons un module par nom val appModule = ModuleManager.getInstance(project) .modules.toList() .first { it.name == "headhunter-applicant" }
Ensuite, en utilisant la classe utilitaire FilenameIndex, vous pouvez trouver PsiFile par son nom, en spécifiant le module trouvé comme zone de recherche.
Recherche de PsiFile par nom val buildGradlePsiFile = FilenameIndex.getFilesByName( appModule.project, "build.gradle", appModule.moduleContentScope ).first()
Après avoir trouvé le PsiFile, nous pouvons commencer à rechercher le 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 . Et ainsi de suite.
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, .
C’est tout. , , – .
FAQ
, . , 2 3 .
, IDEA IDEA SDK , deprecated, , . SDK- , , .
– gitignore . - .
Android Studio Mac OS, Ubuntu, . , Windows, .