Меня зовут Александр Мамонов, и в KODE я занимаюсь разработкой на Flutter. Я столкнулся с бойлерплейтом композиции фич в наших проектах, поэтому решил написать универсальный плагин для создания файловой структуры фич в проекте.
В статье расскажу и покажу, как сделать базовый плагин для создания файловых структур и собрать его для локального использования или публикации.

Подготовка
В начале нужно установить плагин «Plugin Dev Kit» из магазина плагинов https://plugins.jetbrains.com/plugin/22851-plugin-devkit.

Шаг 1. Создание проекта
Выбираем «Создать новый проект». Затем тип проекта — IDE Plugin, и указываем его название.

Должен получиться такой шаблон:

Шаг 2. Конфигурация проекта
Открываем gradle.properties и вставляем параметры. Ниже в комментариях в коде указаны ссылки на ресурсы, где можно подробнее ознакомиться с параметрами.
# IntelliJ Platform Artifacts Repositories -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html
pluginGroup = com.your_feature_name
pluginName = your_feature_name
pluginRepositoryUrl = https://github.com/Name/your_feature_name
# SemVer format -> https://semver.org
pluginVersion = 1.0.0
# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
pluginSinceBuild = 232
#pluginUntilBuild = 251.*
# IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension
platformType = IC
platformVersion = 2023.2.6
# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
# Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22
platformPlugins =
# Gradle Releases -> https://github.com/gradle/gradle/releases
gradleVersion = 8.7
# Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib
kotlin.stdlib.default.dependency = false
# Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html
org.gradle.configuration-cache = true
# Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html
org.gradle.caching = true
Затем открываем plugin.xml

и заменяем содержимое на:
<!-- Plugin Configuration File. Read more: https://plugins.jetbrains.com/docs/intellij/plugin-configuration-file.html -->
<idea-plugin>
<!-- Unique identifier of the plugin. It should be FQN. It cannot be changed between the plugin versions. -->
<id>com.name.your_feature_name</id>
<!-- Public plugin name should be written in Title Case.
Guidelines: https://plugins.jetbrains.com/docs/marketplace/plugin-overview-page.html#plugin-name -->
<name>Your_feature_name</name>
<!-- A displayed Vendor name or Organization ID displayed on the Plugins Page. -->
<vendor email="support@yourcompany.com" url="https://www.yourcompany.com">YourCompany</vendor>
<!-- Description of the plugin displayed on the Plugin Page and IDE Plugin Manager.
Simple HTML elements (text formatting, paragraphs, and lists) can be added inside of <![CDATA[ ]]> tag.
Guidelines: https://plugins.jetbrains.com/docs/marketplace/plugin-overview-page.html#plugin-description -->
<description><![CDATA[
Enter short description for your plugin here.<br>
<em>most HTML tags may be used</em>
]]></description>
<!-- Product and plugin compatibility requirements.
Read more: https://plugins.jetbrains.com/docs/intellij/plugin-compatibility.html -->
<depends>com.intellij.modules.platform</depends>
<depends>org.jetbrains.kotlin</depends>
<!-- Extension points defined by the plugin.
Read more: https://plugins.jetbrains.com/docs/intellij/plugin-extension-points.html -->
<extensions defaultExtensionNs="com.intellij">
</extensions>
<!-- тут описываются команды или слушатели ide на которые должен реалигровать плагин
В нашем примере мы создаим одно действие для генерации шаблона
-->
<actions>
<action id="GenerateCleanCode.NewAction"
icon="/META-INF/pluginIcon.svg"
class="com.your_feature_name.GenerateFolderStructureAction"
text="Create New Feature">
<add-to-group group-id="NewGroup" anchor="last"/>
</action>
</actions>
</idea-plugin>
group-id="NewGroup" добавляет наше действие в группу “new” при взаимодействии с директориями, а anchor="last" добавляет наше действие последним в списке.
В параметр icon можно добавить свою svg-иконку, которая будет использоваться как в магазине приложений, так и в самой IDE при выборе действия.
В данный момент IDE будет ругаться на параметр class — мы его создадим в следующем шаге.
Шаг 3. Код плагина
Создадим класс InputDialog

и наполним его:
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.Messages
import java.awt.*
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.JTextField
class InputDialog : DialogWrapper(null) {
private val textField = JTextField(20)
var featureName = ""
init {
init()
title = "Enter Feature Name"
}
override fun createCenterPanel(): JComponent {
val panel = JPanel(BorderLayout())
panel.add(JLabel("Feature Name:"), BorderLayout.WEST)
panel.add(textField, BorderLayout.CENTER)
return panel
}
override fun getPreferredSize(): Dimension {
return Dimension(300, 100)
}
override fun doOKAction() {
featureName = textField.text.trim()
if (featureName.isEmpty()) {
Messages.showErrorDialog("Feature name cannot be empty.", "Error")
return
}
super.doOKAction()
}
}
Этот диалог будет вызываться при нажатии нашего действия и будет выглядеть вот так:

Следом создаем GenerateFolderStructureAction

и наполняем его:
package com.your_feature_name
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.vfs.VirtualFile
import java.util.*
class GenerateFolderStructureAction : AnAction() {
override fun actionPerformed(event: AnActionEvent) {
// тут мы используем созданный нами ранее диалог
val dialog = InputDialog()
// показываем его
dialog.show()
ApplicationManager.getApplication().runWriteAction {
if (dialog.isOK) {
// достаем введенное пользователем название
val featureName = dialog.featureName
// получаем системный путь где начать создание шаблона
val libDir = event.getData(PlatformDataKeys.VIRTUAL_FILE)
generateFolderStructure(libDir, featureName)
}
}
}
private fun generateFolderStructure(libDir: VirtualFile?, featureName: String) {
if (libDir != null) {
// создаем подпапку с введеным пользователем названием
val featureDir = libDir.createChildDirectory(null, featureName)
// data
val dataDir = featureDir.createChildDirectory(null, "data")
// в созданной директории создаем файл "${featureName}_data_source.dart" - расширение файла и его содержимое могут быть любыми
// .createChildData создает файл с указаным именем и расширениием
// .setBinaryContent записывает нужный нам текст внутрь созданного файла
dataDir.createChildDirectory(null, "data_source").createChildData(null, "${featureName}_data_source.dart")
.setBinaryContent(getDataSourceContent(featureName).toByteArray())
// ... тут можно дальше создавать новые директории и файлы
// показываем диалог с успешным завершением генерации шаблона
showToastMessage("Generated Successfully!")
}
}
private fun String.toCamelCase(): String {
return this.split("_")
.joinToString("") { it.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } }
}
private fun showToastMessage(message: String) {
ApplicationManager.getApplication().invokeLater {
Messages.showMessageDialog(message, "Success", Messages.getInformationIcon())
}
}
private fun getDataSourceContent(featureName: String): String {
// при записи в файл | - указывает начало строки, чтобы у содержимого сохранилось форматирование
return """
|final class ${featureName.toCamelCase()}DataSource {
| const ${featureName.toCamelCase()}DataSource({required NetworkService service}) : _service = service;
|
| final NetworkService _service;
|
| }
|
""".trimMargin()
}
}
Шаг 4. Запуск и сборка
Для того, чтобы проверить плагин, в верхнем правом углу будет команда run plugin.

Она запустит новое окно IDE с включенным в него вашим плагином. Если плагин отработал корректно, пришло время сборки и публикации.
Создаем новое действие Gradle, а в команде выбираем runPluginVerifier:

Запускаем новый скрипт:

И исправляем ошибки — в том случае, если они будут.
Если скрипт прошел успешно, то в папке build/distributions будет лежать .zip архив с вашим плагином.

Если вы хотите поделиться плагином локально, данный архив можно установить без публикации, через команду install Plugin from Disk в самой студии.

Шаг 5. Публикация
Создаем аккаунт в https://plugins.jetbrains.com/ и выбираем upload plugin:

Заполняем поля, делаем скриншоты работы нашего плагина и следуем дальнейшим подсказкам от jetbrains.

Ревью и публикация плагина занимает два-три рабочих дня, так что наберитесь терпения.
В данном туториале мы создали базовый плагин для создания файловых структур и собрали его для локального использования или публикации.
Ознакомиться с реализацией можно на GitHub
Надеюсь, дальнейшее развитие вашего плагина пойдет проще!