Fantastische Plugins, vol. 2. Üben

Hier können Sie den ersten Artikel mit der Theorie des Plug-In-Engineerings lesen.


Und in diesem Teil werde ich Ihnen sagen, auf welche Probleme wir bei der Erstellung des Plugins gestoßen sind und wie wir versucht haben, sie zu lösen.
Bild


Worüber werde ich sprechen?


  • Praktischer Teil
    • Mehrseitige Benutzeroberfläche
    • DI in Plugins
    • Codegenerierung
    • Codeänderung
  • Was ist als nächstes zu tun?
    • Tipps
    • FAQ

Mehrseitige Benutzeroberfläche


Als erstes mussten wir eine mehrseitige Benutzeroberfläche erstellen. Wir haben das erste komplexe Formular mit einer Reihe von Häkchen und Eingabefeldern erstellt. Wenig später haben wir beschlossen, die Möglichkeit hinzuzufügen, eine Liste von Modulen auszuwählen, die der Benutzer mit dem neuen Modul verbinden kann. Außerdem möchten wir die Anwendungsmodule auswählen, mit denen das erstellte Modul verbunden werden soll.


Es ist nicht sehr praktisch, so viele Steuerelemente in einem Formular zu haben, dass drei separate Seiten und drei separate Ausstechformen erstellt wurden. Kurz gesagt, das Dialogfeld "Assistent".


Bild


Da das Erstellen einer mehrseitigen Benutzeroberfläche in Plugins sehr schmerzhaft ist, wollte ich etwas bereit finden. Und im Darm von IDEA haben wir eine Klasse namens WizardDialog entdeckt .


Bild


Dies ist eine Wrapper-Klasse über einen regulären Dialog, die den Fortschritt des Benutzers im Assistenten unabhängig überwacht und die erforderlichen Schaltflächen (Zurück, Weiter, Fertig stellen usw.) anzeigt. Ein spezielles WizardModel ist an den WizardDialog angehängt , zu dem einzelne WizardSteps hinzugefügt werden. Jeder WizardStep ist ein separates Formular.


In seiner einfachsten Form erfolgt die Umsetzung des Dialogs wie folgt:


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

Wir erben von der WizardDialog- Klasse und parametrisieren mit der Klasse unseres WizardModel . Diese Klasse hat einen speziellen Rückruf ( onWizardGoalAchieved ), der uns mitteilt, dass der Benutzer den Assistenten bis zum Ende durchlaufen und auf die Schaltfläche „Fertig stellen“ geklickt hat.
Es ist wichtig zu beachten, dass innerhalb dieser Klasse nur das WizardModel erreicht werden kann . Dies bedeutet, dass Sie alle Daten, die der Benutzer während des Durchlaufs des Assistenten sammelt , im WizardModel hinzufügen müssen .


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

Das Modell lautet wie folgt: Wir erben von der WizardModel- Klasse und fügen mithilfe der integrierten add- Methode separate WizardSteps zum Dialogfeld hinzu.


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

WizardSteps sind ebenfalls einfach: Wir erben von der WizardStep- Klasse, parametrisieren sie mit unserer Modellklasse und definieren vor allem die Prepare- Methode neu, die die Stammkomponente Ihres zukünftigen Formulars zurückgibt.


In einfachen Worten sieht es wirklich so aus. Aber in der realen Welt wird Ihre Form höchstwahrscheinlich ungefähr so ​​aussehen:


Bild


Hier können Sie sich an jene Zeiten erinnern, als wir in der Android-Welt noch nicht wussten, was Clean Architecture, MVP, und den gesamten Code in einer Aktivität geschrieben haben. Es gibt ein neues Feld für Architekturschlachten, und wenn Sie verwirrt werden möchten, können Sie Ihre eigene Architektur für Plugins implementieren.


Fazit


Wenn Sie eine mehrseitige Benutzeroberfläche benötigen, verwenden Sie WizardDialog - dies wird einfacher.


Wir fahren mit dem nächsten Thema fort - DI in Plugins.


DI in Plugins


Warum ist möglicherweise eine Abhängigkeitsinjektion in einem Plugin erforderlich?
Der erste Grund ist die Organisation der Architektur innerhalb des Plugins.


Es scheint, warum im Allgemeinen eine Art Architektur innerhalb des Plugins zu beobachten? Ein Plug-In ist eine nützliche Sache, nachdem ich es geschrieben habe, und das war's, ich habe es vergessen.
Ja, aber nein.
Wenn Ihr Plugin wächst und Sie viel Code schreiben, stellt sich die Frage nach strukturiertem Code von selbst. Hier kann DI nützlich sein.


Der zweite, wichtigere Grund: Mit Hilfe von DI können Sie die Komponenten erreichen, die von den Entwicklern anderer Plugins geschrieben wurden. Es können Eventbusse, Logger und vieles mehr sein.


Trotz der Tatsache, dass Sie jedes DI-Framework (Spring, Dagger usw.) verwenden können, gibt es in IntelliJ IDEA Ihr eigenes DI-Framework, das auf den ersten drei Abstraktionsebenen basiert, über die ich bereits gesprochen habe: Anwendung , Projekt und Modul .


Bild


Jede dieser Ebenen hat ihre eigene Abstraktion namens Komponente . Die Komponente der erforderlichen Ebene wird pro Instanz des Objekts dieser Ebene erstellt. Daher wird ApplicationComponent einmal für jede Instanz der Application- Klasse erstellt, ähnlich wie ProjectComponent für Project- Instanzen usw.


Was muss getan werden, um das DI-Framework zu verwenden?


Erstellen Sie zunächst eine Klasse, die eine der benötigten Schnittstellenkomponenten implementiert, z. B. eine Klasse, die ApplicationComponent , ProjectComponent oder ModuleComponent implementiert . Gleichzeitig haben wir die Möglichkeit, ein Objekt der Ebene einzufügen, deren Schnittstelle wir implementieren. Das heißt, Sie können beispielsweise in ProjectComponent ein Objekt der Project- Klasse einfügen .


Komponentenklassen erstellen
 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 

Zweitens ist es möglich, andere Komponenten des gleichen Niveaus oder höher zu injizieren. Das heißt, Sie können beispielsweise in ProjectComponent andere ProjectComponent oder ApplicationComponent einfügen . Hier können Sie auf Instanzen von "fremden" Komponenten zugreifen.


Gleichzeitig garantiert IDEA, dass der gesamte Abhängigkeitsgraph korrekt zusammengestellt wird, alle Objekte in der richtigen Reihenfolge erstellt und korrekt initialisiert werden.


Als Nächstes registrieren Sie die Komponente in der Datei plugin.xml . Sobald Sie eine der Komponentenschnittstellen (z. B. ApplicationComponent ) implementieren, bietet IDEA sofort an, Ihre Komponente in plugin.xml zu registrieren.


Registrieren Sie die Komponente in 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> 

Wie wird das gemacht? Ein spezielles <Projekt-Komponenten> -Tag wird angezeigt ( <Anwendungskomponente> , <Modulkomponente> - abhängig von der Ebene). Darin befindet sich ein Tag mit zwei weiteren Tags: <Schnittstellenklasse> , in der der Schnittstellenname Ihrer Komponente angegeben ist, und <Implementierungsklasse> , in der die Implementierungsklasse angegeben ist. Ein und dieselbe Klasse kann entweder eine Schnittstelle einer Komponente oder deren Implementierung sein, sodass Sie mit einem einzelnen <implementation-class> -Tag arbeiten können .

Als letztes müssen Sie die Komponente vom entsprechenden Objekt abrufen , dh wir erhalten die ApplicationComponent von der Application- Instanz, ProjectComponent von Project usw.


Holen Sie sich die Komponente
 val myAppComponent = application.getComponent(MyAppComponent::class.java) val myProjectComponent = project.getComponent(MyProjectComponent::class.java) val myModuleComponent = module.getComponent(MyModuleComponent::class.java) 

Schlussfolgerungen


  1. In IDEA gibt es ein DI-Framework - Sie müssen nichts alleine ziehen: weder Dolch noch Frühling. Obwohl Sie natürlich können.
  2. Mit diesem DI können Sie die vorgefertigten Komponenten erreichen, und dies ist der Saft selbst.

Fahren wir mit der dritten Aufgabe fort - der Codegenerierung.


Codegenerierung


Denken Sie daran, in der Checkliste hatten wir die Aufgabe, viele Dateien zu generieren? Jedes Mal, wenn wir ein neues Modul erstellen, erstellen wir eine Reihe von Dateien: Interaktoren, Präsentatoren, Fragmente. Beim Erstellen eines neuen Moduls sind diese Komponenten einander sehr ähnlich, und ich möchte lernen, wie dieses Framework automatisch generiert wird.


Muster


Was ist der einfachste Weg, um eine Menge ähnlichen Codes zu generieren? Verwenden Sie Muster. Zuerst müssen Sie sich Ihre Vorlagen ansehen und verstehen, welche Anforderungen an den Codegenerator gestellt werden.


Ein Teil der build.gradle-Dateivorlage
 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">') }> ... } 

Erstens: Wir wollten in der Lage sein, Bedingungen innerhalb dieser Muster zu verwenden. Ich gebe ein Beispiel: Wenn das Plugin irgendwie mit der Benutzeroberfläche verbunden ist, möchten wir die speziellen Gradle-Plugin- Kotlin-Android-Erweiterungen verbinden .


Bedingung innerhalb der Vorlage
 <if (isKotlinProject) { apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' <if (isModuleWithUI) { apply plugin: 'kotlin-android-extensions' }> }> 

Das zweite, was wir wollen, ist die Möglichkeit, eine Variable in dieser Vorlage zu verwenden. Wenn wir beispielsweise kapt für Moxy konfigurieren, möchten wir den Paketnamen als Argument für den Anmerkungsprozessor einfügen.


Ersetzen Sie den Wert der Variablen in der Vorlage
 kapt { arguments { arg("moxyReflectorPackage", '<include var="packageName">') } } 

Eine andere Sache, die wir brauchen, ist die Fähigkeit, Schleifen innerhalb der Vorlage zu verarbeiten. Erinnern Sie sich an das Formular, in dem wir die Liste der Module ausgewählt haben, die wir mit dem neu erstellten Modul verbinden möchten? Wir wollen sie in einer Schleife umgehen und dieselbe Zeile hinzufügen.


Verwenden Sie die Schleife in der Vorlage.
 <for (moduleName in enabledModules) { compileOnly project('<include var="moduleName">') }> 

Daher stellen wir drei Bedingungen für den Codegenerator vor:


  • Wir wollen die Bedingungen nutzen
  • Möglichkeit, variable Werte zu ersetzen
  • Wir brauchen Schleifen in Mustern

Codegeneratoren


Welche Möglichkeiten gibt es, um den Codegenerator zu implementieren? Sie können beispielsweise Ihren eigenen Codegenerator schreiben. Zum Beispiel haben die Jungs von Uber dies getan: Sie haben ihr eigenes Plug-In für die Erzeugung von Riblets geschrieben (die sogenannten Architektureinheiten). Sie entwickelten eine eigene Vorlagensprache , in der sie nur die Möglichkeit zum Einfügen von Variablen verwendeten. Sie brachten die Bedingungen auf das Niveau des Generators . Aber wir dachten, dass wir das nicht tun würden.


Die zweite Option ist die Verwendung der in IDEA integrierten Dienstprogrammklasse FileTemplateManager. Ich würde dies jedoch nicht empfehlen. Weil er Velocity als Engine hat, die einige Probleme beim Weiterleiten von Java-Objekten an Vorlagen hat. Darüber hinaus kann der FileTemplateManager keine anderen Dateien als Java oder XML aus der Box generieren. Und wir mussten Groovy-Dateien, Kotlin, Proguard und andere Dateitypen generieren.


Die dritte Option war ... FreeMarker . Wenn Sie vorgefertigte FreeMarker- Vorlagen haben, werfen Sie sie nicht weg - sie können Ihnen im Plugin nützlich sein.


Was muss getan werden, was muss FreeMarker im Plugin verwenden? Fügen Sie zunächst Dateivorlagen hinzu. Sie können den Ordner / templates im Ordner / resources erstellen und dort alle unsere Vorlagen für alle Dateien hinzufügen - Präsentatoren, Fragmente usw.


Bild


Danach müssen Sie eine Abhängigkeit von der FreeMarker-Bibliothek hinzufügen. Da das Plugin Gradle verwendet, ist das Hinzufügen einer Abhängigkeit unkompliziert.


Fügen Sie eine Abhängigkeit von der FreeMarker-Bibliothek hinzu
 dependencies { ... compile 'org.freemarker:freemarker:2.3.28' } 

Konfigurieren Sie danach FreeMarker in unserem Plugin. Ich rate Ihnen, diese Konfiguration einfach hier zu kopieren - sie wird gefoltert, gelitten, kopiert und alles funktioniert einfach.


FreeMarker-Konfiguration
 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 } } ... 

Es ist Zeit, Dateien mit FreeMarker zu erstellen. Dazu erhalten wir eine Vorlage aus der Konfiguration anhand ihres Namens und erstellen mit einem normalen FileWriter eine Datei mit dem gewünschten Text direkt auf der Festplatte.


Erstellen einer Datei mit 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) } } } 

Und die Aufgabe scheint gelöst zu sein, aber nein. Im theoretischen Teil habe ich erwähnt, dass die gesamte IDEE von der PSI-Struktur durchdrungen ist, und dies muss berücksichtigt werden. Wenn Sie Dateien erstellen, die die PSI-Struktur umgehen (z. B. über FileWriter), versteht IDEA einfach nicht, dass Sie etwas erstellt haben, und zeigt die Dateien nicht im Projektbaum an. Wir haben ungefähr sieben Minuten gewartet, bevor IDEA indiziert und die erstellten Dateien gesehen hat.


Fazit - Machen Sie es richtig, erstellen Sie Dateien unter Berücksichtigung der Struktur des PSI.


Erstellen Sie eine PSI-Struktur für Dateien


Lassen Sie uns zunächst die Ordnerstruktur mit PsiDirectory anzeigen . Das Startverzeichnis des Projekts kann mit den Erweiterungsfunktionen rateProjectDir und toPsiDirectory abgerufen werden:


Holen Sie sich das PsiDirectory-Projekt
 val projectPsiDirectory = project.guessProjectDir()?.toPsiDirectory(project) 

Nachfolgende Verzeichnisse können entweder mit der Klassenmethode psiDirectory findSubdirectory gefunden oder mit der Methode createSubdirectory erstellt werden.


Suchen und erstellen Sie PsiDirectory
 val coreModuleDir = projectPsiDirectory.findSubdirectory("core") val newModulePsiDir = coreModuleDir.createSubdirectory(config.mainParams.moduleName) 

Ich empfehle außerdem, dass Sie eine Map erstellen, aus der Sie alle PsiDirectory- Ordnerstrukturen mithilfe eines Zeichenfolgenschlüssels abrufen und die erstellten Dateien dann zu einem dieser Ordner hinzufügen können.


Erstellen Sie eine Ordnerstrukturzuordnung

return 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") // ... 

}}


Ordner erstellt. Wir werden PsiFiles mit PsiFileFactory erstellen. Diese Klasse verfügt über eine spezielle Methode namens createFileFromText . Die Methode akzeptiert drei Parameter als Eingabe: Name (String fileName), Text (String text) und Typ (FileType fileType) der Ausgabedatei. Zwei der drei Parameter sind klar, woher sie stammen: Wir kennen den Namen selbst und den Text von FreeMarker. Und wo bekomme ich FileType ? Und worum geht es?


Dateityp


FileType ist eine spezielle Klasse, die den Dateityp angibt. Aus der "Box" stehen uns nur zwei FileType zur Verfügung: JavaFileType und XmlFileType für Java-Dateien bzw. XML-Dateien. Aber es stellt sich die Frage: Woher bekommen Sie die Typen für die build.gradle- Datei, für die Kotlin- Dateien, für Proguard , für .gitignore ?!


Erstens können die meisten dieser Dateitypen von anderen Plugins übernommen werden, die bereits von jemandem geschrieben wurden. GroovyFileType kann aus dem Groovy-Plugin , KotlinFileType aus dem Kotlin-Plugin und Proguard aus dem Android-Plugin entnommen werden.


Wie fügen wir die Abhängigkeit eines anderen Plugins zu unserem hinzu? Wir verwenden das Gradle-Intellij-Plugin . Es fügt der build.gradle-Datei des Plugins einen speziellen Intellij-Block hinzu, in dem sich eine spezielle Eigenschaft befindet - Plugins . In dieser Eigenschaft können Sie die Liste der Plugin-IDs auflisten, von denen wir abhängen möchten.


Fügen Sie Abhängigkeiten zu anderen Plugins hinzu
 // build.gradle  intellij { … plugins = ['android', 'Groovy', 'kotlin'] } 

Wir nehmen die Schlüssel aus dem offiziellen JetBrains-Plugin-Repository . Für in IDEA integrierte Plugins (Groovy, Kotlin und Android) reicht der Name des Plugin-Ordners in IDEA aus. Im Übrigen müssen Sie zur Seite eines bestimmten Plugins im offiziellen JetBrains-Plugin-Repository gehen. Dort wird die XML-ID- Eigenschaft des Plugins sowie die Version angegeben (hier ist beispielsweise die Seite des Docker-Plugins ). Lesen Sie mehr über das Verbinden anderer Plugins auf GitHub .


Zweitens müssen Sie der Datei plugin.xml eine Abhängigkeitsbeschreibung hinzufügen . Dies erfolgt über das Tag .

Wir verbinden Plugins in plugin.xml
 <idea-plugin> ... <depends>org.jetbrains.android</depends> <depends>org.jetbrains.kotlin</depends> <depends>org.intellij.groovy</depends> </idea-plugin> 

Nachdem wir das Projekt synchronisiert haben, werden wir die Abhängigkeiten von anderen Plugins verschärfen und sie verwenden können.


Aber was ist, wenn wir uns nicht auf andere Plugins verlassen wollen? In diesem Fall können wir einen Stub für den Dateityp erstellen, den wir benötigen. Erstellen Sie dazu zunächst eine Klasse, die von der Sprachklasse erbt. Die eindeutige Kennung unserer Programmiersprache wird an diese Klasse übergeben (in unserem Fall "ru.hh.plugins.Ignore" ).


Erstellen Sie eine Sprache für GitIgnore-Dateien
 class IgnoreLanguage private constructor() : Language("ru.hh.plugins.Ignore", "ignore", null), InjectableLanguage { companion object { val INSTANCE = IgnoreLanguage() } override fun getDisplayName(): String { return "Ignore() ($id)" } } 

Hier gibt es eine Funktion: Einige Entwickler fügen eine nicht eindeutige Zeile als Kennung hinzu. Aus diesem Grund kann die Integration Ihres Plugins in andere Plugins unterbrochen werden. Wir sind großartig, wir haben eine einzigartige Linie.


Nachdem Sie Language erstellt haben, müssen Sie als Nächstes einen Dateityp erstellen. Wir erben von der LanguageFileType- Klasse, verwenden die von uns definierte Sprachinstanz zum Initialisieren und überschreiben einige sehr einfache Methoden. Fertig. Jetzt können wir den neu erstellten FileType verwenden .


Erstellen Sie Ihren eigenen Dateityp für .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 } 

Beenden Sie die Erstellung der Datei


Nachdem Sie alle erforderlichen Dateitypen gefunden haben , empfehle ich, einen speziellen Container namens TemplateData zu erstellen. Dieser enthält alle Daten zu der Vorlage, aus der Sie Code generieren möchten. Es enthält den Namen der Vorlagendatei, den Namen der Ausgabedatei , die Sie nach dem Generieren des Codes erhalten, den gewünschten Dateityp und schließlich PsiDirectory , in dem Sie die erstellte Datei hinzufügen.


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

Dann kehren wir zu FreeMarker zurück - wir erhalten die Vorlagendatei daraus, mit StringWriter erhalten wir den Text, in der PsiFileFactory generieren wir die PsiFile mit dem gewünschten Text und Typ. Die erstellte Datei wird dem gewünschten Verzeichnis hinzugefügt.


Erstellen Sie PsiFile im gewünschten Ordner
 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) } 

Somit wird die PSI-Struktur berücksichtigt und IDEA sowie andere Plugins werden sehen, was wir getan haben. Dies kann einen Gewinn bringen: Wenn das Plugin für Git beispielsweise feststellt, dass Sie eine neue Datei hinzugefügt haben, wird automatisch ein Dialogfeld angezeigt, in dem Sie gefragt werden, ob Sie diese Dateien zu Git hinzufügen möchten.


Schlussfolgerungen zur Codegenerierung


  • Textdateien können von FreeMarker generiert werden. Sehr bequem.
  • Beim Generieren von Dateien müssen Sie die PSI-Struktur berücksichtigen, da sonst alles schief geht.
  • Wenn Sie Dateien mit PsiFileFactory generieren möchten, müssen Sie FileType irgendwo finden.

Nun wenden wir uns dem letzten, köstlichsten praktischen Teil zu - dies ist eine Modifikation des Codes.


Codeänderung


Tatsächlich ist das Erstellen eines Plugins nur zum Generieren von Code Unsinn, da Sie Code mit anderen Tools und demselben FreeMarker generieren können. Was FreeMarker jedoch nicht tun kann, ist den Code zu ändern.


Unsere Checkliste enthält mehrere Aufgaben im Zusammenhang mit dem Ändern des Codes. Beginnen wir mit der einfachsten - dem Ändern der Datei settings.gradle .


Modification settings.gradle


Ich möchte Sie daran erinnern, was wir tun möchten: Wir müssen dieser Datei einige Zeilen hinzufügen, die den Pfad zum neu erstellten Modul beschreiben:


Beschreibung des Pfades zum Modul
 // 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') 

Ich habe Sie etwas früher erschreckt, dass Sie bei der Arbeit mit Dateien sonst immer die PSI-Struktur berücksichtigen müssen alles wird brennen wird nicht funktionieren. Tatsächlich kann dies bei einfachen Aufgaben wie dem Hinzufügen einiger Zeilen am Ende einer Datei weggelassen werden. Sie können der Datei mit der üblichen Datei java.io.File einige Zeilen hinzufügen . Dazu finden wir den Pfad zur Datei, erstellen die Instanz java.io.File und fügen mit Hilfe der Kotlin- Erweiterungsfunktionen zwei Zeilen am Ende dieser Datei hinzu. Sie können dies tun, IDEA wird Ihre Änderungen sehen.


Hinzufügen von Zeilen zur Datei settings.gradle

val projectBaseDirPath = project.basePath?: return
val settingsPathFile = projectBaseDirPath + "/settings.gradle"


val settingsFile = Datei (settingsPathFile)


settingsFile.appendText ("include ': $ moduleName'")
settingsFile.appendText (
"project (': $ moduleName'). projectDir = neue Datei (settingsDir, '$ folderPath')"
)


Im Idealfall ist es natürlich besser durch die PSI-Struktur - es ist zuverlässiger.


Kapt Tuning für Zahnstocher


Ich erinnere Sie noch einmal an das Problem: Im Anwendungsmodul befindet sich eine build.gradle- Datei und darin befinden sich Einstellungen für den Anmerkungsprozessor. Und wir möchten ein Paket unseres erstellten Moduls an einer bestimmten Stelle hinzufügen.


Wohin wohin? ..

Bild


Unser Ziel ist es, ein bestimmtes PsiElement zu finden. Danach planen wir, unsere Linie hinzuzufügen. Die Suche nach dem Element beginnt mit der Suche nach der PsiFile , die die Datei build.gradle des Anwendungsmoduls bezeichnet. Und dafür müssen Sie das Modul finden, in dem wir nach der Datei suchen.


Wir suchen ein Modul mit Namen
 val appModule = ModuleManager.getInstance(project) .modules.toList() .first { it.name == "headhunter-applicant" } 

Als Nächstes können Sie mithilfe der Dienstprogrammklasse FilenameIndex PsiFile anhand seines Namens finden und das als Suchbereich gefundene Modul angeben .


Suche nach PsiFile mit Namen
 val buildGradlePsiFile = FilenameIndex.getFilesByName( appModule.project, "build.gradle", appModule.moduleContentScope ).first() 

Nachdem wir die PsiFile gefunden haben, können wir nach dem PsiElement suchen. Um es zu finden, empfehle ich die Installation eines speziellen Plug-Ins - PSI Viewer . IDEA , PSI- .


Bild


- (, build.gradle) , PSI- .


Bild


– , 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 . Usw.


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 .


-?

Bild


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

- , .


Schlussfolgerungen


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

Was ist als nächstes zu tun?


.


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

Das ist alles. , , – .


FAQ


  • ?

, . , 2 3 .


  • IDEA , - ?

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


  • ?

– gitignore . - .


  • ?

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

Source: https://habr.com/ru/post/de463611/


All Articles