Привет, ранее я написал статью о своем плагине и том, как переосмыс��ил подход к получению проектной модели Maven. И обещал более подробно рассказать о технических деталях реализации плагина и точках расширения IDEA, которые использовал. Кстати, если у вас есть Maven-проекты и вы еще не пробовали мой плагин, то был бы очень признателен, если бы вы попробовали и оставили обратную связь. Это помогает мне сделать проект лучше, находить и устранять проблемы. А теперь к делу.
Большинство так называемых внешних систем, по отношению к IDE, соответствуют общему шаблону (не обязательно все пункты):
имеют конфигурационные файлы;
на основании конфигурационных файлов можно построить проект;
предоставляют возможность выполнения каких-либо задач;
Всем этим условиям удовлетворяют популярные билд-системы: Gradle, Maven, Sbt и не только, например Docker-compose. IDEA плагины для них построены на External System API (специальный набор точек расширения и готовых сервисов для интеграции с внешними системами) и используют его в той или иной степени. Поэтому для написания своего плагина, я также использовал данное API и хочу рассказать о нем более подробно т.к. официальная документация дает только верхнеуровневый обзор.
Задача
Чтобы не просто перечислять классы и интерфейсы, я решил рассмотреть их на примере создания своего плагина для некой абстрактной билд-системы. С конфигурационным билд файлом в формате JSON:
{ "groupId": "ru.rzn.build.json.system", "artifactId": "simple-project", "version": "1.0-SNAPSHOT", "sources": ["src/main/java"], "resources": ["src/main/resources"], "sourcesTest": ["src/test/java"], "resourcesTest": ["src/test/resources"], "compilerJdkVersion": "11", "compilerArgs": ["-verbose", "-Xlint:all"], "dependencies": [ { "groupId": "org.hamcrest", "artifactId": "hamcrest-core", "version": "4.12", "relativePath": "lib/hamcrest-core-2.1.jar" } ], "modules": [] }
Для простоты задачи, чтобы сократить объем кода, сделаем допущения, что artifactId равен имени директории, где находится модуль/под-модуль, имя билд файла строго равно build.json и билд-файл не может содержать null значений.
Желательно также, чтобы читатель уже был знаком с базовыми понятиями "плагинописания" для IDEA, пробовал создавать простые проекты, имел представление о Project Structure (ctrl+alt+shift + s) и знал, что такое plugin.xml и зачем он нужен.
Project Data Domain
Первое, что надо сделать - это обработать конфигурационные файлы и создать на их основе модель данных. Модель данных в External System API представляет из себя древовидную структуру. Основными ее классами являются:
DataNode - элемент вершины дерева, который хранит непосредственно данные и имеет ссылку на родителя и дочерние элементы;
Key - ключ с которым ассоциированы данные конкретного типа;
ExternalNodeData - непосредственно сами данные.
Визуально это выглядит вот так:

Для большинства случаев уже созданы готовые классы данных:
ProjectData - данные о проекте;
ModuleData - данные о модуле внутри проекта;
ContentRootData - данные о каталогах внутри модуля(main/test/resources);
TaskData - данные о задаче, которые может выполнять внешняя система;
LibraryDependencyData - данные о зависимостях проекта и/или модуля и т.д.
Для этих элементов также есть готовые обработчики, которые отвечают за создание элементов в внутренней структуре IDEA - Project Structure, но об этом чуть позже.
Обработка конфигурационных файлов
Когда мы уже познакомились с моделью данных, перейдем непосредственно к обработке конфигурационных файлов. Нужно представить их содержимое в виде дерева - DataNode<Key, ExternalNodeData>.
У каждой внешней системы должен быть свой уникальный id, чтобы отличать их друг от друга и понимать, какие данные какой системой созданы. Для этого нам нужно создать константу ProjectSystemId:
object Constants { const val JSON_BUILD_SYSTEM = "JsonBuildSystem" val SYSTEM_ID = ProjectSystemId(JSON_BUILD_SYSTEM.uppercase(Locale.getDefault()), JSON_BUILD_SYSTEM) }
Для получения проектной модели нужно реализовать интерфейс ExternalSystemProjectResolver, основной метод которого отвечает за то чтобы создать и вернуть модель данных - resolveProjectInfo. Ниже частично приведена реализация для нашей абстрактной билд-системы.
class JsonBuildSystemProjectResolver : ExternalSystemProjectResolver<ExecutionSettings> { override fun resolveProjectInfo( id: ExternalSystemTaskId, projectPath: String, isPreviewMode: Boolean, settings: ExecutionSettings?, listener: ExternalSystemTaskNotificationListener ): DataNode<ProjectData> { settings ?: throw ExternalSystemException("settings is empty") val configPath = settings.configPath ?: throw ExternalSystemException("config paths is empty") val buildModel = JsonBuildSystemUtils.fromJson(configPath) val languageLevel = getLanguageLevel(buildModel) val mainModulePath = Path.of(configPath).parent val projectDataNode = createProjectNode(buildModel, mainModulePath) projectDataNode.createChild(ProjectKeys.TASK, TaskData(SYSTEM_ID, "clean", mainModulePath.toString(), null)) setupJdkNodes(settings, projectDataNode, mainModulePath, languageLevel) setupModulesData(buildModel, mainModulePath.parent, projectDataNode) listener.onTaskOutput(id, "import finished", true) return projectDataNode } }
На вход к нам поступает внутренний id задачи, корневая директория проекта, настройки исполнения (о настройках более подробно будет далее), listener для событий процесса. Далее мы парсим наш JSON-файл, создаем вершину с проектом DataNode<ProjectData>, к которой уже добавляем остальные узлы - информацию о задачах, модулях их зависимостях и директориях, SDK и прочее.
По умолчанию данный сервис поднимается в отдельном процессе, но нам этого не надо, поэтому отключаем это поведение в plugin.xml.
<registryKey key="JSONBUILDSYSTEM.system.in.process" defaultValue="true"/>
В моем GMaven и также в Gradle-��лагине, сделано аналогично. Потому-что получение проектной модели как правило делегируется билд-системе, а она уже стартует отдельный процесс.
Код project resolver'а по сути является просто мэппингом данных из проектной структуры билд-системы в модель данных External System, где для подавляющего числа элементов уже есть готовые классы модели данных и обработчики для них, которые позволяют на основе этих данных создавать элементы проектной структуры IDEA: проекты, модули, зависимости, таски, а также создавать для них визуальное представление.
Для того чтобы программно получить созданную резолвером модель данных внешней системы, нужно вызвать:
ProjectDataManager.getInstance() .getExternalProjectData(project, SYSTEM_ID, externalProjectPath) ?.getExternalProjectStructure();
Уже такая простая реализация, если её сконфигурировать до конца, годится для того чтобы создавать проект внутри IDE и визуально отображать данные в билд-окне.

Но мы не будем на этом останавливаться, а пойдем дальше.
ProjectDataService & ExternalSystemViewContributor
Теперь более детально поговорим о создании своих элементов модели данных. Если внимательно пройтись по реализации нашего резолвера, то можно увидеть, что мы добавили два кастомных элемента.
Первый - это CompilerArgData для хранения аргументов компилятора. Чтобы на основании этих данных иметь возможность делать какую-то полезную работу, а именно добавлять параметры компилятора к модулю, нужно реализовать интерфейс ProjectDataService.
Основные методы:
getTargetDataKey - ключ данных, которые будет обрабатывать сервис;
importData - создание/обновление элементов IDE; на вход поступают данные модели, что соответсвуют ключу;
computeOrphanData - вычисление удаленных данных, которые не представлены в текущей версии модели данных;
removeData непосредственно удаление данных.
Пример реализации для нашей билд-системы:
class CompilerArgDataService : AbstractProjectDataService<CompilerArgData, Void>() { override fun getTargetDataKey() = CompilerArgData.KEY override fun importData( toImport: Collection<DataNode<CompilerArgData>>, projectData: ProjectData?, project: Project, modifiableModelsProvider: IdeModifiableModelsProvider ) { val config = CompilerConfiguration.getInstance(project) as CompilerConfigurationImpl for (node in toImport) { val moduleData = node.parent?.data as? ModuleData ?: continue val ideModule = modifiableModelsProvider.findIdeModule(moduleData) ?: continue config.setAdditionalOptions(ideModule, ArrayList(node.data.arguments)) } } }
Для каждого элемента CompilerArgData, мы получаем его родителя - данные о модуле к которому относятся данные настройки и добавляем эти настройки к нему. Благодаря входным параметрам данного метода, можно получить доступ практически любому функционалу IDEA. Удалять в данном случае ничего не нужно, т.к. настройки привязаны строго к модулю, и если он удаляется, то удаляются и настройки.
Для лучшего понимания, можно посмотреть более сложный пример готового обработчика, представленного в External System, который создает/обновляет/удаляет модули проектной структуры IDEA ModuleDataService.
Также нужно не забыть зарегистрировать данный сервис в plugin.xml.
<externalProjectDataService implementation="ru.rzn.gmyasoedov.jsonbuildsystem.project.service.CompilerArgDataService"/>
Второй наш элемент модели данных - это BuildActionData, т.к. наша би��д система ничего не умеет и это просто JSON-файл, то нам нужен способ собирать проект. Для этого мы и создаем данный узел в дереве модели данных, чтобы отображать его в билд-окне, на одном уровне с "тасками" (можно посмотреть как это выглядит на картинке выше) и по клику запускать процесс сборки проекта, используя уже готовый обработчик, который делает это через меню IDEA Build | Build Project.
Сначала нужно создать наследника ExternalSystemNode.
@Order(ExternalSystemNode.BUILTIN_TASKS_DATA_NODE_ORDER) class BuildActionViewNode(externalProjectsView: ExternalProjectsView, dataNode: DataNode<BuildActionData>) : ExternalSystemNode<BuildActionData>(externalProjectsView, null, dataNode) { override fun update(presentation: PresentationData) { super.update(presentation) presentation.setIcon(AllIcons.Nodes.ConfigFolder) } override fun getName() = "Build" override fun getActionId() = "CompileProject" }
Это элемент модели данных UI, который будет непосредственно отображаться в билд-окне. Для него мы задаем иконку, имя и actionId. В данном случае мы берем уже готовый actionId который отвечает за сборку проекта. Также через аннотацию @Order указываем на каком уровне будут отображен элемент в дереве - на одном уровне с тасками. Теперь нужно реализовать ExternalSystemViewContributor, чтобы смэппить данные из модели проекта в модель для UI:
class JsonBuildSystemExternalViewContributor : ExternalSystemViewContributor() { override fun getSystemId() = SYSTEM_ID override fun getKeys(): List<Key<*>> = listOf(BuildActionData.KEY) override fun createNodes( externalProjectsView: ExternalProjectsView, dataNodes: MultiMap<Key<*>?, DataNode<*>?> ): List<ExternalSystemNode<*>> { val buildActionNodes = dataNodes[BuildActionData.KEY] return buildActionNodes.map { BuildActionViewNode(externalProjectsView, it as DataNode<BuildActionData>) } } }
Здесь мы указываем на какие ключи будет реагировать обработчик и далее для элементов BuildActionData, которые у нас находятся в дереве данных по ключу BuildActionData.KEY, мы создаем и возвращаем уже UI элементы дерева для отображении в билд-окне. Регистрируем сервис в plugin.xml.
<externalSystemViewContributor id="JsonBuildSystem" implementation="ru.rzn.gmyasoedov.jsonbuildsystem.view.JsonBuildSystemExternalViewContributor"/>
В заключении по данному разделу можно сказать, что элемент данных не обязан иметь какие либо обработчики, но тогда он и не будет ничего уметь делать. Он может иметь один из двух типов обработчиков или все сразу. Например, для ModuleData в External System API уже есть обработчики чтобы отображать данный узел визуально в дереве и на его основании создавать модуль в проектной структуре IDE. И да, на один элемент данных может быть "повешено" несколько обработчиков - ProjectDataService.
Выполнение задач
Для выполнения задач билд-системы нужно реализовать интерфейс ExternalSystemTaskManager, но т.к. наша абстрактная билд-система это просто файлик, который ничего не умеет, то для примера реализуем только clean-таск для очистки билд-директорий. Никто не мешает нам пройтись рекурсивно по всем модулям и очистить их target-папки.
class JsonBuildSystemTaskManager : ExternalSystemTaskManager<ExecutionSettings> { override fun executeTasks( id: ExternalSystemTaskId, taskNames: MutableList<String>, projectPath: String, settings: ExecutionSettings?, jvmParametersSetup: String?, listener: ExternalSystemTaskNotificationListener ) { if (taskNames != listOf("clean")) { throw ExternalSystemException("only clean implemented") } settings ?: throw ExternalSystemException("settings is empty") val configPath = settings.configPath ?: throw ExternalSystemException("config paths is empty") JsonBuildSystemUtils.getAllModulesWithPath(configPath) .map { it.modelPath.resolve("target") } .forEach { FileUtil.deleteRecursively(it) } } }
Также необходимо создать классы JsonBuildSystemRuntimeConfigurationProducer, JsonBuildSystemRunConfigurationExtension и JsonBuildSystemExternalTaskConfigurationType для интеграции с Run Configurations. Разбирать их содержимое тут я не буду, они достаточно простые. Их нужно зарегистрировать в plugin.xml.
<runConfigurationProducer implementation="ru.rzn.gmyasoedov.jsonbuildsystem.project.execution.JsonBuildSystemRuntimeConfigurationProducer"/> <configurationType implementation="ru.rzn.gmyasoedov.jsonbuildsystem.project.execution.JsonBuildSystemExternalTaskConfigurationType"/> <externalSystem.runConfigurationEx implementation="ru.rzn.gmyasoedov.jsonbuildsystem.project.execution.JsonBuildSystemRunConfigurationExtension"/>
Настройки
Для внешней системы нужно создать четыре вида настроек.
Первое - это настройки, непосредственно относящиеся к проекту ExternalProjectSettings:
class ProjectSettings : ExternalProjectSettings() { var configPath: String? = null var vmOptions: String? = null var jdkName: String? = null }
Настройки нашей билд-системы содержат три дополнительных поля:
configPath - это полный путь к конфигурационному JSON файлу проекта;
vmOptions - просто для примера, чтобы дальше показать как редактировать настройки через UI;
jdkName - имя JDK, нужно для установки projectJDK на которой будет, работать наш проект.
Второй тип настроек - это глобальные настройки для всех проектов нашей билд-системы: AbstractExternalSystemSettings:
@State(name = "JsonBuildSystemSettings", storages = [Storage("jsonBuildSystem.xml")]) class SystemSettings(project: Project) : AbstractExternalSystemSettings<SystemSettings, ProjectSettings, SettingsListener>( SettingsListener.TOPIC, project ), PersistentStateComponent<SystemSettingsState> { var skipTests = false override fun copyExtraSettingsFrom(settings: SystemSettings) {} override fun checkSettings(old: ProjectSettings, current: ProjectSettings) {} override fun loadState(state: SystemSettingsState) { super.loadState(state) skipTests = state.skipTests } override fun getState(): SystemSettingsState { val state = SystemSettingsState() fillState(state) state.skipTests = skipTests return state } override fun getLinkedProjectSettings(projectPath: String): ProjectSettings? { val projectAbsolutePath = Path.of(projectPath).toAbsolutePath() val projectSettings: ProjectSettings? = super.getLinkedProjectSettings(projectPath) if (projectSettings == null) { for (setting in linkedProjectsSettings) { val settingPath = Path.of(setting.externalProjectPath).toAbsolutePath() if (FileUtil.isAncestor(settingPath.toFile(), projectAbsolutePath.toFile(), false)) { return setting } } } return projectSettings } override fun subscribe(listener: ExternalSystemSettingsListener<ProjectSettings?>, parentDisposable: Disposable) { } }
Глобальные настройки содержат одно дополнительное поле skipTests - также для примера, чтобы показать редактирование настроек через UI.
Данный тип настроек агрегирует в себе, настройки для каждого из проектов созданных на основе нашей билд системы (AbstractExternalSystemSettings#myLinkedProjectsSettings). Чтобы получить настройки конкретного проекта, нужно реализовать метод getLinkedProjectSettings, где мы, по указанному пути, находим нужный проект и возвращаем его ProjectSettings. Настройки проекта регистрируются в myLinkedProjectsSettings по родительской директории проекта. Тут нужно задать @State чтобы настройки не сбрасывались каждый раз при выходе из IDE и сохранялись на диске. Также нужно обязательно реализовать методы для загрузки и получения состояния: loadState, getState и метод subscribe для реагирования на изменение настроек. Но subscribe, для простоты, мы оставим пустым. Но в целом тут можно программно запустить процесс обновления проектной модели в случае изменения настроек:
ExternalSystemUtil.refreshProject(externalProjectPath, ImportSpecBuilder(project, SYSTEM_ID))
Следующий тип настроек, это так называемые локальные настройки - AbstractExternalSystemLocalSettings.
@State(name = "JsonBuildSystemLocalSettings", storages = [Storage(StoragePathMacros.CACHE_FILE)]) class LocalSettings(project: Project) : AbstractExternalSystemLocalSettings<LocalSettings.JsonLocalState>( Constants.SYSTEM_ID, project, JsonLocalState() ) { class JsonLocalState : State() }
Содержат состояние локального workspace'a, включая модель данных External System для текущих проектов. Чтобы не перечитывать модель данных при каждом открытии IDE, а брать данные сразу из локальных настроек. В общем тут ничего своего добавлять не надо, просто создаем наследника под нашу билд систему с ее SYSTEM_ID и также указываем, что их надо сохранять на диск.
И последний тип настроек, это настройки исполнения - ExternalSystemExecutionSettings, которые далее передаются во все сервисы, где идет обращения к внешней системе: JsonBuildSystemTaskManager, JsonBuildSystemProjectResolver.
class ExecutionSettings : ExternalSystemExecutionSettings() { var configPath: String? = null var jdkName: String? = null }
Обычно они содержат параметры необходимые для среды исполнения: путь к jdk, maven, gradle и прочее в зависимости от типа внешней системы. В остальном эти настройки близки по структуре к ProjectSettings. Основное отличие в том что это "короткоживущие" настройки, которые не хранятся на диске и получаются непосредственно из ProjectSettings в процессе обращения к внешней системе (подробнее будут ниже).
Редактирование настроек
Для редактирования настроек, необходимо реализовать AbstractExternalProjectSettingsControl. Вот так для примера, выглядит редактирование наших ProjectSettings:
class ProjectSettingsControl(initialSettings: ProjectSettings) : AbstractExternalProjectSettingsControl<ProjectSettings>(initialSettings) { private var vmOptionsField: JTextField? = null override fun validate(settings: ProjectSettings) = true override fun resetExtraSettings(isDefaultModuleCreation: Boolean) { if (vmOptionsField != null) { vmOptionsField!!.text = initialSettings.vmOptions } } override fun fillExtraControls(content: PaintAwarePanel, indentLevel: Int) { val vmOptionsLabel = JBLabel("Vm options") vmOptionsField = JTextField() content.add(vmOptionsLabel, ExternalSystemUiUtil.getLabelConstraints(indentLevel)) content.add(vmOptionsField, ExternalSystemUiUtil.getLabelConstraints(0)) content.add(Box.createGlue(), ExternalSystemUiUtil.getFillLineConstraints(indentLevel)) vmOptionsLabel.setLabelFor(vmOptionsField) } override fun isExtraSettingModified(): Boolean { if (vmOptionsField?.text != (initialSettings.vmOptions ?: "")) { return true } return false } override fun applyExtraSettings(settings: ProjectSettings) { if (vmOptionsField != null) { settings.vmOptions = vmOptionsField!!.getText() } } }
Тут необходимо реализовать методы:
validate - проверка валидности новых настроек;
resetExtraSettings - сброс настроек в исходное состояние, если например хотим отменить ещё не сохраненные изменения;
fillExtraControls - создание UI компонентов;
isExtraSettingsModified - были изменения или нет;
applyExtraSettings - сохранение новых настроек.
Для SystemSettings все аналогично, поэтому код приводить тут не буду.
Далее все наши реализации редактирования настроек, необходимо обернуть в AbstractExternalSystemConfigurable, чтобы связать редактирование настроек с нашей билд системой по ее SYSTEM_ID:
class JsonBuildSystemSettingsConfigurable(project: Project) : AbstractExternalSystemConfigurable<ProjectSettings, SettingsListener, SystemSettings>(project, SYSTEM_ID) { override fun getId() = "reference.settingsdialog.project.jsonBuildSystem" override fun newProjectSettings() = ProjectSettings() override fun createSystemSettingsControl(settings: SystemSettings) = SystemSettingsControl(settings) override fun createProjectSettingsControl(settings: ProjectSettings) = ProjectSettingsControl(settings) }
Также нужно реализовать метод getId() для программной навигации к настройкам по их идентификатору и методы по созданию UI для каждого типа настроек. И зарегистрировать в plugin.xml.
<projectConfigurable groupId="build.tools" groupWeight="200" id="reference.settingsdialog.project.jsonBuildSystem" instance="ru.rzn.gmyasoedov.jsonbuildsystem.settings.JsonBuildSystemSettingsConfigurable" displayName="JsonBuildSystem"/>
В итоге это будет выглядеть вот так:

Для настройки skipTest можно реализовать отдельный Action и добавить его вызов на билд-окно.
class SkipTestsAction : ToggleAction() { override fun isSelected(e: AnActionEvent): Boolean { val project = e.project ?: return false return project.getService(SystemSettings::class.java).skipTests } override fun setSelected(e: AnActionEvent, state: Boolean) { val project = e.project ?: return val settings = project.getService(SystemSettings::class.java) settings.skipTests = !settings.skipTests } init { templatePresentation.icon = AllIcons.RunConfigurations.ShowIgnored templatePresentation.text = "Skip Tests" } }
И в plugin.xml:
<actions> <action id="JsonBuildSystem.Toolbar.SkipTests" class="ru.rzn.gmyasoedov.jsonbuildsystem.action.SkipTestsAction"/> <group id="JsonBuildSystem.View.ActionsToolbar.CenterPanel"> <separator/> <reference id="JsonBuildSystem.Toolbar.SkipTests"/> <separator/> <add-to-group group-id="ExternalSystemView.ActionsToolbar.CenterPanel" anchor="last"/> </group> </actions>
Будет выглядеть вот так:

Авто импорт проекта
Позволяет настроить интеграцию для автоматического обновления структуры проекта при изменении конфигурационных файлов. Для этого необходимо реализовать ExternalSystemAutoImportAware.
class JsonBuildSystemAutoImportAware : ExternalSystemAutoImportAware { override fun getAffectedExternalProjectPath(changedFileOrDirPath: String, project: Project): String? { val changedPath = Path.of(changedFileOrDirPath) if (changedPath.isDirectory()) return null val fileSimpleName = changedPath.fileName.toString() if (!JsonBuildSystemUtils.isBuildSystemFileName(fileSimpleName)) return null val systemSettings = project.getService(SystemSettings::class.java) return systemSettings.getLinkedProjectSettings(changedPath.parent.toString())?.externalProjectPath } override fun getAffectedExternalProjectFiles(projectPath: String?, project: Project): List<File> { projectPath ?: return listOf() val systemSettings = project.getService(SystemSettings::class.java) val projectSettings = systemSettings.getLinkedProjectSettings(projectPath) ?: return listOf() return projectSettings.configPath?.let { listOf(File(it)) } ?: listOf() } }
getAffectedExternalProjectPath - служит для проверки того, должно ли конкретное изменение файла/каталога вызывать обновление внешнего проекта.
getAffectedExternalProjectFiles - возвращает список всех конфигурационных файлов, которые относятся к проекту, чтобы в случае изменения одного из них показывать иконку для реимпорта проекта.

По умолчанию процесс реимпорта проекта запускается автоматически, при внешних изменениях конфигурационных файлов - например, если изменения пришли из системы контроля версий. В случае ручного изменения билд скриптов, отображается иконка, для вызова реимпорта проекта вручную. Также можно настроить, чтобы процесс автоимпорта был всегда автоматическим при любых изменениях:

Финал. Собираем все вместе
Итого, сейчас мы реализовали следующие конечные точки:
JsonBuildSystemProjectResolver - резолвер для получения проектной модели;
JsonBuildSystemTaskManager - менеджер управления тасками;
JsonBuildSystemSettingsConfigurable - управление глобальными и локальными настройками проектов;
JsonBuildSystemAutoImportAware управление авто-импортом проекта.
Теперь осталось собрать все вместе и зарегистрировать нашу билд систему в plugin.xml. Для этого нам надо реализовать основную точку расширения для внешних систем - ExternalSystemManager.
class JsonBuildSystemManager : ExternalSystemConfigurableAware, ExternalSystemUiAware, ExternalSystemAutoImportAware, ExternalSystemManager<ProjectSettings, SettingsListener, SystemSettings, LocalSettings, ExecutionSettings> { private val autoImportAwareDelegate: ExternalSystemAutoImportAware = CachingExternalSystemAutoImportAware( JsonBuildSystemAutoImportAware() ) override fun getConfigurable(project: Project) = JsonBuildSystemSettingsConfigurable(project) override fun getProjectRepresentationName(targetProjectPath: String, rootProjectPath: String?): String { return ExternalSystemApiUtil.getProjectRepresentationName(targetProjectPath, rootProjectPath) } override fun getExternalProjectConfigDescriptor(): FileChooserDescriptor { return FileChooserDescriptorFactory.createSingleFolderDescriptor() } override fun getProjectIcon() = AllIcons.FileTypes.Json override fun getTaskIcon() = AllIcons.Nodes.ConfigFolder override fun enhanceRemoteProcessing(parameters: SimpleJavaParameters) = throw java.lang.UnsupportedOperationException() override fun getSystemId() = SYSTEM_ID override fun getSettingsProvider(): Function<Project, SystemSettings> { return Function<Project, SystemSettings> { project: Project -> project.getService(SystemSettings::class.java) } } override fun getLocalSettingsProvider(): Function<Project, LocalSettings> { return Function<Project, LocalSettings> { project: Project -> project.getService(LocalSettings::class.java) } } override fun getExecutionSettingsProvider(): Function<Pair<Project, String>, ExecutionSettings> { return Function<Pair<Project, String>, ExecutionSettings> { val project = it.first val projectPath = it.second val systemSettings = project.getService(SystemSettings::class.java) val projectSettings = systemSettings.getLinkedProjectSettings(projectPath) val executionSettings = ExecutionSettings() executionSettings.configPath = projectSettings?.configPath executionSettings.jdkName = projectSettings?.jdkName projectSettings?.vmOptions ?.also { executionSettings.withVmOptions(ParametersListUtil.parse(it, true, true)) } executionSettings } } override fun getProjectResolverClass() = JsonBuildSystemProjectResolver::class.java override fun getTaskManagerClass() = JsonBuildSystemTaskManager::class.java override fun getExternalProjectDescriptor() = BuildFileChooserDescriptor() override fun getAffectedExternalProjectPath(changedFileOrDirPath: String, project: Project): String? { return autoImportAwareDelegate.getAffectedExternalProjectPath(changedFileOrDirPath, project) } override fun getAffectedExternalProjectFiles(projectPath: String?, project: Project): List<File> { return autoImportAwareDelegate.getAffectedExternalProjectFiles(projectPath, project) } }
<externalSystemManager implementation="ru.rzn.gmyasoedov.jsonbuildsystem.JsonBuildSystemManager"/>
Как можно видеть, менеджер нашей системы реализует:
ExternalSystemManager - самый главный интерфейс, который содержит методы для регистрация нашего резволвера проектов JsonBuildSystemProjectResolver - getProjectResolverClass, менеджера задач JsonBuildSystemTaskManager - getTaskManagerClass, методы для сервисов настроек - getSettingsProvider, getLocalSettingsProvider, getExecutionSettingsProvider и getSystemId идентификатор внешней системы.
ExternalSystemConfigurableAware - возвращает созданный нами JsonBuildSystemSettingsConfigurable в методе getConfigurable;
ExternalSystemUiAware - реализуем методы getProjectRepresentationName, getExternalProjectConfigDescriptor, getProjectIcon, getTaskIcon для UI настроек build tool окна;
ExternalSystemAutoImportAware для регистрации нашего сервиса автоимпорта JsonBuildSystemAutoImportAware, завернутого в кеш, как рекомендовано в документации;
Тут стоит выделить метод getExecutionSettingsProvider, который принимает на вход проект и его базовый путь, по которому мы находим ProjectSettings и далее конвертируем их в настройки исполнения ExecutionSettings. Которые далее передаются во все сервисы где идет обращения к внешней системе: JsonBuildSystemTaskManager, JsonBuildSystemProjectResolver.
Далее создаем factory-класс для нашего билд-окна:
class JsonBuildSystemToolWindowFactory : AbstractExternalSystemToolWindowFactory(SYSTEM_ID) { override fun getSettings(project: Project): AbstractExternalSystemSettings<*, *, *> = project.getService(SystemSettings::class.java) }
<toolWindow id="JsonBuildSystem" anchor="right" icon="AllIcons.FileTypes.Json" factoryClass="ru.rzn.gmyasoedov.jsonbuildsystem.view.JsonBuildSystemToolWindowFactory"/>
Теперь все практически готово.
Открытие готовых проектов и создание новых
Остался последний штришок. Нужно научится открывать проекты. Для этого нам понадобится реализация AbstractOpenProjectProvider:
class JsonBuildSystemOpenProjectProvider : AbstractOpenProjectProvider() { override val systemId: ProjectSystemId = SYSTEM_ID override fun isProjectFile(file: VirtualFile) = JsonBuildSystemUtils.isBuildSystemFile(file) override fun linkToExistingProject(projectFile: VirtualFile, project: Project) { ExternalProjectsManagerImpl.getInstance(project).setStoreExternally(true) val projectSettings = JsonBuildSystemUtils.createProjectSettings(projectFile, project) val externalProjectPath = projectSettings.externalProjectPath ExternalSystemApiUtil.getSettings(project, SYSTEM_ID).linkProject(projectSettings) ExternalProjectsManagerImpl.getInstance(project).runWhenInitialized { ExternalSystemUtil.refreshProject( externalProjectPath, ImportSpecBuilder(project, SYSTEM_ID) ) } } }
Которая отвечает за проверку - принадлежит ли файл нашей билд системе. Далее в методе linkToExistingProject создаем дефолтные настройки нового проекта, где устанваливаем полный путь к конфигурационному файлу проекта (createProjectSettings) и добавляем их к SystemSettings нашей билд системы, вызывая метод linkSettings. И запускаем процесс импорта, который прочитает конфигурационные файлы и создаст проектную модель.
Далее реализуем точку расширения для открытия проектов. Где используем только что созданный ProjectProvider.
class JsonBuildSystemProjectOpenProcessor : ProjectOpenProcessor() { private val importProvider = JsonBuildSystemOpenProjectProvider() override fun canOpenProject(file: VirtualFile): Boolean = importProvider.canOpenProject(file) override fun doOpenProject( virtualFile: VirtualFile, projectToClose: Project?, forceOpenInNewFrame: Boolean ): Project? { return runBlockingModal(ModalTaskOwner.guess(), "") { importProvider.openProject(virtualFile, projectToClose, forceOpenInNewFrame) } } override val name = Constants.SYSTEM_ID.readableName override val icon = AllIcons.FileTypes.Json override fun canImportProjectAfterwards(): Boolean = true override fun importProjectAfterwards(project: Project, file: VirtualFile) { importProvider.linkToExistingProject(file, project) } }
<projectOpenProcessor implementation="ru.rzn.gmyasoedov.jsonbuildsystem.wizard.JsonBuildSystemProjectOpenProcessor"/>
Wizard для создания новых проектов и модулей для нашей билд системы можно посмотреть тут. Приводить его здесь я не буду и оставлю на самостоятельный разбор, чтобы не перегружать статью кодом и справочной информаций. Ничего сложного там нет, и создается он по образу и подобию wizard’ов для других билд систем как Maven и Gradle.
Итог
Вот теперь вроде и все. Исходный код плагина выложен на Github. Также там есть примеры простого и мульти-модульного проекта, чтобы более детально разобрать как плагин работает. Надеюсь это будет кому-нибудь полезно.
