Иногда простые вещи очень утомляют, особенно когда их необходимо делать постоянно. Одна из таких вещей при работе с фреймворком Moxy — это добавление стратегий к функциям. Для ускорения этого процесса был написан плагин, который по "alt+enter" предоставляет выбор стратегии если ее нет или диалог с заменой на другую стратегию. Те, кто хочет узнать как это работает, добро пожаловать под кат.

Когда я только начинал изучать Moxy, одной из сложностей было запомнить названия стратегий и не забывать об их существовании. После практики с ними эта проблема ушла, но все равно было желание каким то образом это оптимизировать.
В этот момент я заинтересовался созданием плагинов, и adev_one подсказал мне, что плагин как раз и может решить такую проблему.
Inspection
Итак, мы написали функцию и хотим добавить стратегию. В этот момент нам на помощь приходит code inspections. Они анализируют код текущего файла при помощи PSI (Program Structure Interface), и если что то не так, подсвечивают код красным или серым, и по alt+enter предлагают возможные исправления.
Официальная документация по разработке code inspections.
PSI — это структура, в которой у каждого выражения, ключевого слова и т.д. есть свой аналог, который содержит информацию о нем, его parent и child. Для того, чтобы понять какой класс соответствует необходимой конструкции языка, есть хороший плагин Psi viewer plugin.
Рассмотрим для примера с помощью него структуру интерфейса с функцией.
Видим, что интересующий нас класс это KtNamedFunction.
Рассмотрим создание Inpection.
Как создавать проект и не только хорошо расписано в цикле статей. Дополнительно понадобятся зависимости в файле plugins.xml
<depends>org.jetbrains.kotlin</depends>
<depends>com.intellij.modules.lang</depends>и в файле build.gradle
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
implementation "org.jetbrains.kotlin:kotlin-reflect"
implementation "com.github.moxy-community:moxy:1.0.13"
}
intellij {
...
plugins = ["Kotlin"]
...
}Перейдем к созданию inpection.
Для начала необходимо наследовать AbstractKotlinInspection
class MvpViewStrategyInspection : AbstractKotlinInspection() {}В нем нам понадобится переопределить следующую функцию:
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor В ней мы создаем Visitor, который будет анализировать текущий код файла и добавлять ошибки и предупреждения.
Хорошей документации по Visitor я не нашел, но когда смотрел плагины kotlin наткнулся на очень удобные функции-фабрики для создания Visitor. Для анализа KtNamedFunction есть следующая фабрика:
org.jetbrains.kotlin.psi VisitorWrappersKt.class
public fun namedFunctionVisitor(block: (KtNamedFunction) → Unit): KtVisitorVoidВ лямбде нужно проанализировать PSI: выбрана ли функция без аннотации и является ли она частью интерфейса, который реализует интерфейс MvpView. Когда все условия выполнены, регистрируем ошибку с помощью ProblemsHolder.
В итоге выглядит это вот так:
private val fixes = MoxyStrategy.values().map { AddStrategyFix(it) }.toTypedArray()
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
return namedFunctionVisitor { ktNamedFunction ->
if (!ktNamedFunction.isClassInheritMvpView() ||
ktNamedFunction.isHasMoxyAnnotation()
) return@namedFunctionVisitor
holder.registerProblem(
ktNamedFunction, // psiElement
StrategyIntentionType.MissingStrategy.title, // description
*fixes // LocalQuickFix
)
}
}Что такое AddStrategyFix? Это объект, который наследует LocalQuickFix. Он отвечает за исправления кода и выполнится, когда будет выбран из предложенных пунктов:
class AddStrategyFix(
private val strategy: MoxyStrategy
) : LocalQuickFix {
override fun getFamilyName(): String = "add ${strategy.className}"
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val ktFunction = (descriptor.psiElement as KtNamedFunction)
val editor = ktFunction.getProjectEditor()
ktFunction.addStrategyAnnotation(strategy, project, editor)
}
}getFamilyName — определяет название пункта, который будет в диалоге.
В applyFix очевидно нужно выполнить исправление. В нашем случае добавить аннотацию.
Осталось зарегистрировать Inspection в plguin.xml, (похоже Activity в Manifest)
<extensions defaultExtensionNs="com.intellij">
<localInspection language="kotlin"
displayName="missing strategy for function"
groupPath="Moxy"
groupBundle="messages.InspectionsBundle"
groupKey="group.names.probable.bugs"
enabledByDefault="true"
level="ERROR"
implementationClass="com.maksimnovikov.inspection.MvpViewStrategyInspection"/>
</extensions>В итоге получаем:

Так же автоматически добавляются импорты аннотации StateStrategyType и выбранной стратегии.
Для стратегии AddToEndSingleTagStrategy дополнительно сделал добавление второго аргумента
tag и перенос курсора на него.

Intention
Функцию написали, стратегию быстро добавили. Передумали и решили заменить ее на другую.
Теперь наш помощник это Intention.
Принцип работы аналогичен с Inspection, но без какого либо выделения кода и анализируется только текущий выбранный элемент под кареткой. Как использовать уже имеющиеся intention. Официальная документация по разработке.
Рассмотрим создание Intention.
Необходимо наследовать PsiElementBaseIntentionAction :
class MvpViewStrategyIntention : PsiElementBaseIntentionAction(){
override fun getText(): String
override fun getFamilyName(): String
override fun isAvailable(project: Project, editor: Editor?, element: PsiElement): Boolean
override fun invoke(project: Project, editor: Editor?, element: PsiElement)
}getText — определяет отображаемое название
getFamilyName — нужен для иерархии intention и отображения в настройках
isAvailable — в этой функции исходя из текущего элемента под курсором, необходимо определить будет ли добавлен intention в текущий список или нет.
invoke — вызывается, когда мы выбрали данный intention из списка
Рассмотрим подробнее isAvailable и invoke:
isAvailable
В этой функции нужно как можно раньше определить, что intention не нужно добавлять и не анализировать дальше. Это увеличит производительность.
Так же текущий выбранный элемент может быть child текущий функции, потому необходимо получить саму функцию. Остальные проверки аналогичны.
override fun isAvailable(project: Project, editor: Editor?, element: PsiElement): Boolean {
if ((element.containingFile ?: return false) !is KtFile) return false
if (!element.isClassInheritMvpView()) return false
val ktFunction = element.getParentOfType<KtNamedFunction>() ?: return false
return ktFunction.isHasMoxyAnnotation()
}invoke
Поскольку добавление отдельного Intention для каждой стратегии не выгодно по производительности, необходимо п��казать еще один диалог по нажатию на один пункт.
Под капотом наша любимая IDE использует swing. Соответственно, кто хорошо с ним знаком, может создавать разнообразный интерфейс. Я пошел другим путем и использовал простой билдер для диалога выбора — JBPopupFactory
fun <T> Editor.showSelectPopup(
items: List<T>,
onSelected: (T) -> Unit
) {
JBPopupFactory.getInstance()
.createPopupChooserBuilder(items)
.setRequestFocus(true)
.setCancelOnClickOutside(true)
.setItemChosenCallback { onSelected(it) }
.createPopup()
.showInBestPositionFor(this)
}Однако, после создания диалога контекст сменяется на другой, который уже не может изменять код и при попытки сделать это выдает ошибку. Чтобы это исправить дополнительно оборачиваем действие по замене стратегии в функцию WriteCommandAction.runWriteCommandAction(project) {}
override fun invoke(project: Project, editor: Editor, element: PsiElement) {
editor.showSelectPopup(MoxyStrategy.values().toList()) { selectedStrategy ->
WriteCommandAction.runWriteCommandAction(project) {
val ktFunction = element.getParentOfType<KtNamedFunction>() ?: return@runWriteCommandAction
ktFunction.replaceAnnotation(selectedStrategy, project, editor)
}
}
}Так же регистрируем intention в plugin.xml
<extensions defaultExtensionNs="com.intellij">
<intentionAction>
<className>com.maksimnovikov.intention.MvpViewStrategyIntention</className>
<category>Moxy intentions</category>
</intentionAction>
</extensions>В итоге получаем следующий функционал:


Итого
С этим плагином решается проблема запоминания названий стандартных стратегий. Их список всегда виден. Если принцип работы стратегии поначалу может быть не понятен, возможно вставить её и перейти к исходному коду, где есть документация. Также в общем улучшается удобство работы со стратегиями Moxy.
Создание плагинов раньше для меня казалось очень сложным и непонятным. В основном из-за отсутствия примеров и недостаточности документации на официальном сайте. Это и подтолкнуло меня написать эту статью. Надеюсь создание плагинов стало для вас чуть более понятным. Буду рад конструктивной критике и обратной связи.
Полный код проекта можно посмотреть здесь
