Здесь можно почитать первую статью с теорией плагиностроения.
А в этой части я расскажу, с какими проблемами мы сталкивались во время создания плагина и как пытались их решать.

О чем буду говорить?
- Практическая часть
- Многостраничный UI
- DI в плагинах
- Генерация кода
- Модификация кода
- Что делать дальше?
- Советы
- FAQ
Многостраничный UI
Первым делом нам нужно было создать многостраничный UI. Мы сделали первую сложную форму с кучей галочек, полей ввода. Чуть позже решили добавить возможность выбора списка модулей, которые пользователь сможет подключить к новому модулю. А еще мы хотим выбирать те application-модули, к которым планируем подключить созданный модуль.
На одной форме располагать столько контролов не очень удобно, поэтому сделали три отдельные страницы, три отдельных формочки. Короче, Wizard-диалог.

Но поскольку делать многостраничный UI в плагинах очень больно, хотелось найти что-то готовое. И в недрах IDEA мы обнаружили класс, который называется WizardDialog.

Это класс-обертка над обычным диалогом, самостоятельно следящая за продвижением пользователя по wizard-у, и показывающая нужные кнопки (Previous, Next, Finish, etc). К WizardDialog-у прикрепляется специальная WizardModel, к которой добавляются отдельные WizardStep-ы. Каждый 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. У этого класса есть специальный callback (onWizardGoalAchieved), который говорит нам, что пользователь прошел wizard до конца и нажал на кнопку “Finish”.
Важно отметить, что изнутри этого класса есть возможность достучаться только до WizardModel. Это означает, что все данные, которые пользователь наберет по ходу прохождения wizard-а, вы должны складывать в WizardModel.
class MyWizardModel: WizardModel("Title for my wizard") {
init {
this.add(MyWizardStep1())
this.add(MyWizardStep2())
this.add(MyWizardStep3())
}
}Модель выглядит следующим образом: наследуемся от класса WizardModel и при помощи встроенного метода add добавляем в диалог отдельные WizardStep-ы.
class MyWizardStep1: WizardStep<MyWizardModel>() {
private lateinit var contentPanel: JPanel
override fun prepare(state: WizardNavigationState?): JComponent {
return contentPanel
}
}WizardStep-ы тоже устроены просто: наследуемся от класса WizardStep, параметризуем его нашим классом-моделью, и, главное, переопределяем метод prepare, который возвращает рутовый компонент вашей будущей формы.
В простом виде он действительно выглядит так. Но в реальном мире, скорее всего, ваша форма будет напоминать что-то подобное:

Тут можно вспомнить те времена, когда мы в мире Android еще не знали, что такое Clean Architecture, MVP и писали весь код в одной Activity. Здесь есть новое поле для архитектурных баталий, и если вы захотите заморочиться, то можете внедрить собственную архитектуру для плагинов.
Вывод
Если вам нужен многостраничный UI, используйте WizardDialog – будет проще.
Переходим к следующей теме – DI в плагинах.
DI в плагинах
Зачем вообще может потребоваться Dependency Injection внутри плагина?
Первая причина – организация архитектуры внутри плагина.
Казалось бы, зачем вообще соблюдать какую-то архитектуру внутри плагина? Плагин – это же утилитная вещь, раз написал – и все, забыл.
Да, но нет.
Когда ваш плагин разрастается, когда вы пишите много кода, вопрос о структурированности кода возникает сам собой. Тут DI может вам пригодиться.
Вторая, более важная причина, – при помощи DI можно достучаться до компонент, написанных разработчиками других плагинов. Это могут быть event bus-ы, logger-ы и многое другое.
Несмотря на то, что вы вольны использовать любой DI-фреймворк (Spring, Dagger, etc), внутри IntelliJ IDEA есть свой собственный DI-фреймворк, который основан на первых трех уровнях абстракции, о которых я уже говорил: Application, Project и Module.

С каждым из этих уровней связана своя абстракция, которая называется Component. Компонент нужного уровня создается на один инстанс объекта этого уровня. Так ApplicationComponent созд��ется единожды на каждый инстанс класса Application, аналогично ProjectComponent на инстансы 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. Как только вы реализуете один из Component-интерфейсов (например, ApplicationComponent), IDEA сразу предложит зарегистрировать ваш компонент в 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 получаем из инстанса Application, ProjectComponent – из Project и т.д.
val myAppComponent = application.getComponent(MyAppComponent::class.java)
val myProjectComponent = project.getComponent(MyProjectComponent::class.java)
val myModuleComponent = module.getComponent(MyModuleComponent::class.java) Выводы
- Внутри IDEA есть DI-фреймворк – не нужно тащить ничего самим: ни Dagger, ни Spring. Хотя, конечно, вы можете.
- При помощи этого DI можно достучаться до уже готовых компонентов, и это самый сок.
Переходим к третьей задаче – генерации кода.
Генерация кода
Помните, в чек-листе у нас была задача сгенерировать очень много файлов? Каждый раз, когда мы создаем новый модуль, мы создаем пачку файлов: интеракторы, презентеры, фрагменты. При создании нового модуля эти компоненты очень похожи друг на друга, и хотелось бы научиться генерировать этот каркас автоматически.
Шаблоны
Как проще всего нагенерировать тонну похожего кода? Использовать шаблоны. Для начала нужно посмотреть на свои шаблоны и понять, какие требования выдвигать к генератору кода.
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-плагин kotlin-android-extensions.
<if (isKotlinProject) {
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
<if (isModuleWithUI) {
apply plugin: 'kotlin-android-extensions'
}>
}>Вторая вещь, которую мы хотим, – возможность использовать переменную внутри этого шаблона. Например, когда мы настраиваем kapt для Moxy, мы хотим вставлять имя пакета в качестве аргумента annotation processor-а.
kapt {
arguments {
arg("moxyReflectorPackage", '<include var="packageName">')
}
}Еще одна вещь, которая нам нужна, – возможность обрабатывать циклы внутри шаблона. Помните форму, где мы выбирали список модулей, которые хотим подключить к создаваемому новому модулю? Мы хотим обходить их в цикле и добавлять одну и ту же строчку.
<for (moduleName in enabledModules) {
compileOnly project('<include var="moduleName">')
}>Таким образом, мы выдвинули три условия к генератору кода:
- Хотим использовать условия
- Возможность подставлять значения переменных
- Нам нужны циклы в шаблонах
Генераторы кода
Какие есть варианты реализации генератора кода? Можно, например, написать свой собственный генератор кода. Так сделали, например, ребята из Uber-а: написали свой плагин для генерации риблетов (так называются их архитектурные единицы). Они придумали свой язык шаблонов, в котором использовали только возможность вставки переменных. Условия они вынесли на уровень генератора. Но мы подумали, что так делать не будем.
Второй вариант – использовать встроенный в IDEA утилитный класс FileTemplateManager, но я бы не рекомендовал этого делать. Потому что в качестве движка у него – Velocity, у которого есть некоторые проблемы с пробросом Java-объектов в шаблоны. Кроме того, FileTemplateManager не умеет из коробки генерировать файлы, отличающиеся от Java или XML. А нам нужно было генерировать и Groovy-файлы, Kotlin, Proguard и другие типы файлов.
Третьим вариантом стал… FreeMarker. Если у вас есть готовые шаблоны FreeMarker, не спешите их выбрасывать – они могут вам пригодиться внутри плагина.
Что нужно сделать, что использовать FreeMarker внутри плагина? Во-первых, добавить шаблоны файлов. Вы можете создать папку /templates внутри папки /resources и добавлять туда все наши шаблоны для всех файлов – презентеров, фрагментов и т.п.

После этого нужно будет добавить зависимость от библиотеки FreeMarker. Поскольку плагин использует Gradle, добавить зависимость не составляет труда.
dependencies {
...
compile 'org.freemarker:freemarker:2.3.28'
}После этого конфигурируем 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 создаем файл с нужным текстом прямо на диске.
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)
}
}
}И вроде бы задача решена, но нет. В теоретической части я упоминал, что структурой PSI пронизана вся IDEA, и это нужно учитывать. Если вы будете создавать файлы в обход структуры PSI (например, через FileWriter), то IDEA просто не поймет, что вы что-то создали и не отобразит файлы в дереве проекта. Мы ждали около семи минут, прежде чем IDEA проиндексировала и увидела созданные файлы.
Вывод – делайте правильно, создавайте файлы, учитывая структуру PSI.
Создаем PSI-структуру для файлов
Для начала пилим структуру папок с помощью PsiDirectory. Стартовую директорию проекта можно получить с помощью extension-функций guessProjectDir и toPsiDirectory:
val projectPsiDirectory = project.guessProjectDir()?.toPsiDirectory(project)Последующие директории можно либо находить с помощью метода класса PsiDirectory findSubdirectory, либо создавать методом createSubdirectory.
val coreModuleDir = projectPsiDirectory.findSubdirectory("core")
val newModulePsiDir = coreModuleDir.createSubdirectory(config.mainParams.moduleName)Также я рекомендую вам создать Map-ку, из которой по строковому ключу можно получать все PsiDirectory структуры папок, чтобы потом добавлять созданные файлы в любую из этих папок.
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")
// ...}
Папки создали. Создавать PsiFile-ы будем при помощи PsiFileFactory. У этого класса есть специальный метод, который называется createFileFromText. Метод принимает на вход три параметра: имя (String fileName), текст (String text) и тип (FileType fileType) output-файла. Два из трех параметров понятно откуда брать: имя мы знаем сами, текст получим из FreeMarker-а. А откуда брать FileType? И что это вообще?
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. Это делается при помощи тега .
<idea-plugin>
...
<depends>org.jetbrains.android</depends>
<depends>org.jetbrains.kotlin</depends>
<depends>org.intellij.groovy</depends>
</idea-plugin>После того, как синхронизируем проект, у нас подтянутся зависимости с других плагинов, и мы сможем их использовать.
Но что, если мы не хотим зависеть от других плагинов? В таком случае мы можем создать заглушку для нужного нам типа файла. Для этого сначала создаем класс, который будет наследоваться от класса Language. В этот класс будет передаваться уникальный идентификатор нашего языка программирования (в нашем случае – "ru.hh.plugins.Ignore").
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.
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 – он будет содержать в себе все данные о шаблоне, из которого вы хотит�� сгенерировать код. В нем будут храниться название файла-шаблона, название output-файла, который вы получите после генерации кода, нужный FileType и, наконец, PsiDirectory, куда вы добавите созданный файл.
data class TemplateData(
val templateFileName: String,
val outputFileName: String,
val outputFileType: FileType,
val outputFilePsiDirectory: PsiDirectory?
)Затем возвращаемся к FreeMarker-у – достаем из него файл-шаблон, при помощи StringWriter получаем текст, в PsiFileFactory генерируем 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, и при помощи котлиновских extension-функций добавляем две строчки в конец этого файла. Так делать можно, IDEA увидит ваши изменения.
val projectBaseDirPath = project.basePath ?: return
val settingsPathFile = projectBaseDirPath + "/settings.gradle"
val settingsFile = File(settingsPathFile)
settingsFile.appendText("include ':$moduleName'")
settingsFile.appendText(
"project(':$moduleName').projectDir = new File(settingsDir, '$folderPath')"
)
Ну а в идеале, конечно, лучше через PSI-структуру – это надежнее.
Донастройка kapt-а для Toothpick
Вновь напомню задачу: в application-модуле есть build.gradle файл, а внутри него – настройки annotation-процессора. И мы хотим в конкретное место добавить пакет нашего созданного модуля.

Наша цель – найти определенный PsiElement, после которого мы планируем добавить нашу строку. Поиск элемента начинается с поиска PsiFile-а, который обозначает build.gradle файл application-модуля. А для этого нужно найти модуль, внутри которого будем искать файл.
val appModule = ModuleManager.getInstance(project)
.modules.toList()
.first { it.name == "headhunter-applicant" }Далее при помощи утилитного класса FilenameIndex можно найти PsiFile по его имени, указав в качестве области поиска найденный модуль.
val buildGradlePsiFile = FilenameIndex.getFilesByName(
appModule.project,
"build.gradle",
appModule.moduleContentScope
).first() После того как мы найдем PsiFile, можно приступать к поискам PsiElement-а. Чтобы его найти, я рекомендую установить специальный плагин – PSI Viewer. Этот плагин добавляет в IDEA специальную вкладку, там можно увидеть PSI-структуру открытого файла.

Если вы откроете какой-нибудь файл (например, build.gradle) и переместите курсор на интересующую вас строку кода, этот плагин перенесет вас в PSI-структуре к соответствующему элементу.

Это очень удобно – вы сможете понять, какой конкретно элемент вы ищете внутри вашего PsiFile-а.
Возвращаемся к нашей задаче. Мы нашли PsiFile. Внутри него при помощи вот такой простыни вы можете найти нужный элемент.
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 плагина, фабрики элементов этих языков у нас в кармане.
val factory = GroovyPsiElementFactory.getInstance(buildGradlePsiFile.project)
val packageName = config.mainParams.packageName
val newArgumentItem = factory.createStringLiteralForReference(packageName)Осталось только добавить созданный элемент в найденный ранее PsiElement.
targetPsiElement.add(newArgumentItem)Донастройка kapt-а для Moxy в application модуле
Последняя задача из чек-листа, связанная с модификацией кода, – донастройка kapt-а для Moxy в application модуле. Напоминаю задачу: мы хотим добавить название пакета нового модуля в аннотацию @RegisterMoxyReflectorPackages.

На самом деле эту задачу можно решить так же, как предыдущую: найти PsiFile, найти PsiElement, модифицировать его… Но я покажу немного другой способ, чтобы продемонстрировать еще несколько возможностей при работе с PsiElement-ами.
Можно было пойти следующим путем: отыскать класс, который помечен аннотацией @RegisterMoxyReflectorPackages, затем получить список значений в атрибуте value этой аннотации, модифицировать его и пересоздать аннотацию с нуля с новым списком.
Начнем с поиска модуля, в котором мы будем искать наш файл. Затем, при помощи утилитного класса PsiManager, мы найдем PsiClass нужной аннотации.
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.
CodeStyleManager.getInstance(module.project).reformat(replacedElement)Все задачи нашего чек-листа мы решили, резюмируем часть про модификацию кода.
Выводы
- Модифицировать код внутри плагина возможно, но придется делать это через PSI-структуру, а значит придется в ней разобраться.
- Помните, что PSI привязан к конкретному языку программирования, и чтобы все получилось, нужно использовать правильные PsiElement-ы.
Что делать дальше?
Давайте подведем итоги.
- Разрабатывать плагин не очень сложно – по крайней мере теперь, когда вы прочитали эту статью до конца.
- Плагины действительно помогают автоматизировать часть ваших задач. Например у нас получилось автоматизировать создание модулей. Тут свой профит: в нашем случае мы смогли ускорить создание модуля почти в два раза.
- Откуда брать информацию по плагинам? Я советую просто посмотреть чужие плагины. К сожалению, документация по плагинам IDEA не так хороша, как хотелось бы. В ней есть очень много теоретической информации, которая может не пригодиться с практической точки зрения. — Чтобы решить какую-то конкретную задачу, просто ищите на GitHub чужие плагины. Изучайте их, и вы найдете то, что нужно.
- Если вам нужен какой-то утилитный класс – скорее всего внутри IntelliJ IDEA он давно есть. Набираете необходимое, добавляете слово Util или Manager и, скорее всего, вы найдете утилитный класс, который решит вашу задачу.
- И последний совет: проводите отладку на маленьких проектах. К сожалению, задача runIde, которая запускает отдельный инстанс IDEA, поднимается очень долго. Она очень неповоротливая, и если вы попытаетесь отладить ваш плагин на проекте уровня hh.ru, проверка ваших изменений будет проходить довольно долго.
На этом все. Форкайте наш проект, смотрите изнутри, задавайте вопросы – ответим.
FAQ
- Много ли времени потратили на разработку плагина?
Этой задачей я занимался по остаточному принципу, поэтому по времени это все довольно растянулось. Если же попытаться подсчитать чистое время, то около 2 или 3 недель.
- Сталкивались ли с проблемами при обновлении IDEA на новую версию, ломалось ли что-нибудь в плагине?
Да, при обновлении версии IDEA некоторые части IDEA SDK меняются, методы становятся deprecated, некоторые вообще исчезают, приходится подстраиваться. Но у SDK-шных методов хорошая документация, там обычно пишут, на что нужно заменить вызов.
- Были ли проблемы интеграции плагина с уже установленными плагинами?
Только один раз – при работе с gitignore файлами. Как раз из-за неуникального идентификатора языка.
- Были ли проблемы с запуском плагина под разные операционные системы?
Мы запускали свой плагин на Android Studio в Mac OS, а также под Ubuntu, проблем не было. Слышал, что бывают проблемы с Windows, но мы не пробовали.
