ملحقات رائعة ، المجلد. 2. الممارسة

هنا يمكنك قراءة المقال الأول مع نظرية هندسة المكونات.


وفي هذا الجزء ، سوف أخبرك بالمشاكل التي واجهناها أثناء إنشاء المكوّن الإضافي وكيف حاولنا حلها.
صورة


ما الذي سأتحدث عنه؟


  • الجزء العملي
    • Multipage UI
    • DI في الإضافات
    • توليد الشفرة
    • تعديل التعليمات البرمجية
  • ماذا تفعل بعد ذلك؟
    • نصائح
    • التعليمات

Multipage UI


أول شيء يتعين علينا القيام به هو إنشاء واجهة مستخدم متعددة الصفحات. لقد صنعنا أول نموذج معقد مع مجموعة من العلامات ، وحقول الإدخال. بعد ذلك بقليل ، قررنا إضافة القدرة على تحديد قائمة الوحدات النمطية التي يمكن للمستخدم الاتصال بالوحدة النمطية الجديدة. ونريد أيضًا اختيار الوحدات النمطية للتطبيق التي نخطط لها لتوصيل الوحدة النمطية التي تم إنشاؤها.


وجود العديد من عناصر التحكم في نموذج واحد ليس ملائمًا للغاية ، لذلك قاموا بإنشاء ثلاث صفحات منفصلة ، وثلاثة قواطع منفصلة لملفات تعريف الارتباط. باختصار ، مربع الحوار معالج.


صورة


ولكن نظرًا لأن إنشاء واجهة مستخدم متعددة الصفحات في مكونات إضافية أمر مؤلم للغاية ، فقد أردت العثور على شيء جاهز. وفي أحشاء IDEA ، اكتشفنا فئة تسمى WizardDialog .


صورة


هذه فئة مجمعة على مربع حوار منتظم ، تراقب بشكل مستقل تقدم المستخدم في المعالج ، وتظهر الأزرار اللازمة (السابقة ، التالية ، النهاية ، إلخ). يتم إرفاق WizardModel خاص بـ WizardDialog ، حيث تتم إضافة WizardSteps الفردية. كل WizardStep هو نموذج منفصل.


في أبسط أشكاله ، يكون تنفيذ الحوار كما يلي:


WizardDialog
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 .


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

النموذج كالتالي: نرث من فئة WizardModel وباستخدام طريقة الإضافة المدمجة أضف WizardSteps منفصلة إلى مربع الحوار.


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

WizardSteps بسيطة أيضًا: نحن نرث من فئة WizardStep ، ونضعها في صف نموذجنا ، والأهم من ذلك ، نعيد تحديد طريقة الإعداد ، والتي تُرجع المكون الجذر للنموذج المستقبلي.


بعبارات بسيطة ، يبدو حقا مثل هذا. ولكن في العالم الواقعي ، على الأرجح سيكون النموذج الخاص بك يشبه شيء مثل هذا:


صورة


هنا يمكنك أن تتذكر تلك الأوقات التي لم نكن نعرف فيها في العالم الذي يعمل بنظام Android ما هي الهندسة النظيفة و MVP وكتبت كل الكود في نشاط واحد. هناك مجال جديد للمعارك المعمارية ، وإذا كنت ترغب في الحصول على الخلط ، يمكنك تنفيذ الهيكل الخاص بك للمكونات الإضافية.


استنتاج


إذا كنت بحاجة إلى واجهة مستخدم متعددة الصفحات ، فاستخدم WizardDialog - سيكون الأمر أسهل.


ننتقل إلى الموضوع التالي - DI في الإضافات.


DI في الإضافات


لماذا قد تكون هناك حاجة حقن التبعية داخل البرنامج المساعد؟
السبب الأول هو تنظيم الهيكل داخل البرنامج المساعد.


يبدو ، لماذا عموما لاحظ نوعا من الهندسة المعمارية داخل البرنامج المساعد؟ المكوّن الإضافي هو شيء مفيد ، بمجرد أن كتبت ، وهذا ما نسيته.
نعم لكن لا.
عندما ينمو البرنامج المساعد الخاص بك ، عندما تكتب الكثير من التعليمات البرمجية ، فإن مسألة التعليمات البرمجية المنظمة تنشأ من تلقاء نفسها. هنا قد يأتي في متناول اليدين.


السبب الثاني الأكثر أهمية - بمساعدة DI يمكنك الوصول إلى المكونات التي كتبها مطورو الإضافات الأخرى. يمكن أن يكون الحافلات الحدث ، وقطع الاشجار وأكثر من ذلك بكثير.


على الرغم من أنك حر في استخدام أي إطار DI (Spring ، Dagger ، إلخ) ، يوجد داخل IntelliJ IDEA إطار DI الخاص بك ، والذي يستند إلى المستويات الثلاثة الأولى من التجريد ، والتي تحدثت عنها بالفعل: التطبيق ، المشروع والوحدة النمطية .


صورة


كل مستوى من هذه المستويات له تجريد خاص به يسمى المكون . يتم إنشاء مكون المستوى المطلوب لكل مثيل لكائن هذا المستوى. لذلك يتم إنشاء ApplicationComponent مرة واحدة لكل مثيل لفئة التطبيق ، على غرار مثيلات ProjectComponent for Project ، وهكذا.


ما الذي يجب القيام به لاستخدام إطار عمل DI؟


أولاً ، قم بإنشاء فئة تقوم بتنفيذ أحد مكونات الواجهة التي نحتاجها - على سبيل المثال ، فئة تقوم بتطبيق ApplicationComponent أو ProjectComponent أو ModuleComponent . في الوقت نفسه ، لدينا فرصة لحق كائن من المستوى الذي ننفذ واجهة. وهذا ، على سبيل المثال ، في 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 يمكنك حقن ProjectComponent أو ApplicationComponent الأخرى. هذا هو المكان الذي يمكنك فيه الوصول إلى مثيلات المكونات "الغريبة".


في الوقت نفسه ، تضمن IDEA أن يتم تجميع الرسم البياني التبعية بالكامل بشكل صحيح ، سيتم إنشاء جميع الكائنات بالترتيب الصحيح وتهيئة بشكل صحيح.


الشيء التالي الذي يجب عمله هو تسجيل المكون في ملف plugin.xml . بمجرد تنفيذ إحدى واجهات المكونات (على سبيل المثال ، 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> واحدة .

آخر ما يجب فعله هو الحصول على المكون من الكائن المقابل ، أي أننا نحصل على ApplicationComponent من مثيل التطبيق ، ProjectComponent من Project ، إلخ.


الحصول على المكون
 val myAppComponent = application.getComponent(MyAppComponent::class.java) val myProjectComponent = project.getComponent(MyProjectComponent::class.java) val myModuleComponent = module.getComponent(MyModuleComponent::class.java) 

النتائج


  1. هناك إطار DI داخل IDEA - لا حاجة لسحب أي شيء بمفردك: لا خنجر ولا 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 الخاصة .


حالة داخل القالب
 <if (isKotlinProject) { apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' <if (isModuleWithUI) { apply plugin: 'kotlin-android-extensions' }> }> 

الشيء الثاني الذي نريده هو القدرة على استخدام متغير داخل هذا القالب. على سبيل المثال ، عندما نقوم بتكوين kapt لـ Moxy ، نريد إدراج اسم الحزمة كوسيطة لمعالج التعليقات التوضيحية.


استبدل قيمة المتغير داخل القالب
 kapt { arguments { arg("moxyReflectorPackage", '<include var="packageName">') } } 

شيء آخر نحتاجه هو القدرة على التعامل مع الحلقات داخل القالب. تذكر النموذج الذي حددنا فيه قائمة الوحدات التي نريد توصيلها بالوحدة الجديدة التي يتم إنشاؤها؟ نريد الالتفاف حولهم في حلقة وإضافة نفس السطر.


استخدم الحلقة في القالب.
 <for (moduleName in enabledModules) { compileOnly project('<include var="moduleName">') }> 

وبالتالي ، طرحنا ثلاثة شروط لمولد الشفرة:


  • نحن نريد استخدام الشروط
  • القدرة على استبدال القيم المتغيرة
  • نحن بحاجة إلى حلقات في الأنماط

مولدات الشفرة


ما هي الخيارات لتطبيق مولد الشفرة؟ يمكنك ، على سبيل المثال ، كتابة مولد الشفرة الخاص بك. على سبيل المثال ، قام الرجال من أوبر بهذا: لقد كتبوا مكونًا إضافيًا لتوليد قصاصات (ما يسمى بالوحدات المعمارية). لقد توصلوا إلى لغة النموذج الخاصة بهم ، والتي استخدموا فيها فقط القدرة على إدراج المتغيرات. أحضروا الظروف إلى مستوى المولد . لكننا اعتقدنا أننا لن نفعل ذلك.


الخيار الثاني هو استخدام فئة الأداة المساعدة FileTemplateManager المضمنة في IDEA ، لكنني لا أوصي بذلك. لأنه لديه سرعة كمحرك ، والذي لديه بعض المشاكل في إعادة توجيه كائنات Java إلى القوالب. بالإضافة إلى ذلك ، لا يمكن لـ FileTemplateManager إنشاء ملفات غير Java أو XML من المربع. ونحتاج إلى إنشاء ملفات Groovy و Kotlin و Proguard وأنواع أخرى من الملفات.


كان الخيار الثالث ... FreeMarker . إذا كان لديك قوالب FreeMarker جاهزة ، فلا تتعجل في التخلص منها - فقد تكون مفيدة لك داخل البرنامج المساعد.


ما الذي يجب القيام به ، وماذا يجب استخدام FreeMarker داخل البرنامج المساعد؟ أولاً ، قم بإضافة قوالب الملفات. يمكنك إنشاء مجلد / templates داخل المجلد / resources وإضافة جميع القوالب الخاصة بنا إلى هناك لجميع الملفات - العروض التقديمية ، الأجزاء ، إلخ.


صورة


بعد ذلك ، ستحتاج إلى إضافة تبعية إلى مكتبة 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 أنك قد أنشأت شيئًا ولن تعرض الملفات في شجرة المشروع. انتظرنا حوالي سبع دقائق قبل فهرسة IDEA ورأينا الملفات التي تم إنشاؤها.


الخلاصة - افعلها بشكل صحيح ، أنشئ ملفات ، مع مراعاة هيكل PSI.


إنشاء هيكل PSI للملفات


للبدء ، دعونا نرى بنية المجلد باستخدام PsiDirectory . يمكن الحصول على دليل البدء للمشروع باستخدام وظائف التمديد guessProjectDir و toPsiDirectory :


الحصول على مشروع PsiDirectory
 val projectPsiDirectory = project.guessProjectDir()?.toPsiDirectory(project) 

يمكن العثور على الدلائل اللاحقة باستخدام طريقة فئة PsiDirectory findSubdirectory ، أو يتم إنشاؤها باستخدام طريقة createSubdirectory .


البحث وإنشاء PsiDirectory
 val coreModuleDir = projectPsiDirectory.findSubdirectory("core") val newModulePsiDir = coreModuleDir.createSubdirectory(config.mainParams.moduleName) 

أوصي أيضًا بإنشاء خريطة يمكنك من خلالها الحصول على جميع هياكل مجلد PsiDirectory باستخدام مفتاح سلسلة ، ثم إضافة الملفات التي تم إنشاؤها إلى أي من هذه المجلدات.


إنشاء خريطة بنية المجلد

إرجاع mutableMapOf <String، PsiDirectory؟> (). Apply {
هذا ["الجذر"] = modulePsiDir
هذا ["src"] = modulePsiDir.createSubdirectory ("src")
هذا ["main"] = هذا ["src"]؟. createSubdirectory ("main")
هذا ["java"] = هذا ["main"]؟. createSubdirectory ("java")
هذا ["res"] = هذا ["main"]؟. createSubdirectory ("res")


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

}


المجلدات التي تم إنشاؤها. سنقوم بإنشاء PsiFiles باستخدام PsiFileFactory . هذه الفئة لها طريقة خاصة تسمى createFileFromText . تقبل الطريقة ثلاثة معلمات كمدخلات: الاسم (String fileName) والنص (String text) واكتب (FileType fileType) لملف الإخراج. اثنين من المعلمات الثلاثة واضحة حيث يمكن الحصول عليها: نحن نعرف الاسم بأنفسنا ، نحصل على النص من FreeMarker. وأين يمكن الحصول على FileType ؟ وما هو كل شيء؟


نوع الملف


FileType هي فئة خاصة تدل على نوع الملف. يتوفر ملفان فقط لنا من "المربع": JavaFileType و XmlFileType ، على التوالي لملفات Java وملفات XML. ولكن السؤال الذي يطرح نفسه هو: أين يمكن الحصول على أنواع ملف build.gradle ، وملفات Kotlin ، و Proguard ، و .gitignore ، أخيرًا ؟!


أولاً ، يمكن أخذ معظم أنواع FileType هذه من مكونات إضافية أخرى مكتوبة بالفعل من قبل شخص ما. يمكن الحصول على GroovyFileType من البرنامج المساعد Groovy ، KotlinFileType من البرنامج المساعد Kotlin ، Proguard من البرنامج المساعد Android .


كيف نضيف تبعية البرنامج المساعد لآخر لدينا؟ نحن نستخدم gradle-intellij-plugin . ويضيف كتلة intellij خاصة إلى ملف build.gradle الخاص بالبرنامج المساعد ، يوجد بداخله خاصية إضافية - الإضافات . في هذه الخاصية ، يمكنك سرد قائمة معرفات المكونات الإضافية التي نريد الاعتماد عليها.


إضافة التبعيات على الإضافات الأخرى
 // build.gradle  intellij { … plugins = ['android', 'Groovy', 'kotlin'] } 

نأخذ المفاتيح من مستودع البرنامج المساعد الرسمي JetBrains . بالنسبة إلى المكونات الإضافية المضمّنة في IDEA (وهي Groovy و Kotlin و Android) ، يكون اسم مجلد المكون الإضافي داخل IDEA كافٍ. بالنسبة للباقي ، تحتاج إلى الانتقال إلى صفحة مكون إضافي معين في مستودع المكونات الإضافية الرسمي لـ JetBrains ، وسيتم الإشارة إلى خاصية معرف البرنامج المساعد XML هناك ، بالإضافة إلى الإصدار (على سبيل المثال ، هنا صفحة مكون الإضافة 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> 

بعد مزامنة المشروع ، سنقوم بتشديد التبعيات من المكونات الإضافية الأخرى ، وسنكون قادرين على استخدامها.


ولكن ماذا لو كنا لا نريد الاعتماد على الإضافات الأخرى؟ في هذه الحالة ، يمكننا إنشاء كعب قصير لنوع الملف الذي نحتاجه. للقيام بذلك ، قم أولاً بإنشاء فصل سيرث من فصل اللغة . سيتم تمرير المعرف الفريد للغة البرمجة الخاصة بنا إلى هذه الفئة (في حالتنا - "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 الذي تم إنشاؤه حديثًا.


قم بإنشاء FileType الخاص بك لـ .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 } 

الانتهاء من إنشاء الملف


بعد العثور على جميع أنواع FileType الضرورية ، أوصي بإنشاء حاوية خاصة تسمى TemplateData - وسوف تحتوي على جميع البيانات المتعلقة بالقالب الذي تريد إنشاء تعليمة برمجية منه. سوف يحتوي على اسم ملف القالب ، واسم ملف الإخراج الذي تحصل عليه بعد إنشاء الرمز ، و FileType المطلوب ، وأخيراً PsiDirectory ، حيث تضيف الملف الذي تم إنشاؤه.


TemplateData
 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 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 ؟: return
val settingsPathFile = projectBaseDirPath + "/settings.gradle"


val settingsFile = ملف (settingsPathFile)


settingsFile.appendText ("include ': $ moduleName'")
settingsFile.appendText (
"project (': $ 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/ar463611/


All Articles