Пишем plugin для IDEA, используя External System API, на примере простой build системы

—

от автора

Привет, ранее я написал статью о своем плагине и том, как переосмыслил подход к получению проектной модели 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"/>

В итоге это будет выглядеть вот так:

Тут у нас есть глобальный переключатель для всех проектов - skip tests. И для каждого проекта (simple-project) в отдельности можно задать vm options. (Настройки просто для примера, и на результат они не влияют).

Тут у нас есть глобальный переключатель для всех проектов — skip tests. И для каждого проекта (simple-project) в отдельности можно задать vm options. (Настройки просто для примера, и на результат они не влияют).

Для настройки 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 — возвращает список всех конфигурационных файлов, которые относятся к проекту, чтобы в случае изменения одного из них показывать иконку для реимпорта проекта.

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

Финал. Собираем все вместе

Итого, сейчас мы реализовали следующие конечные точки:

Теперь осталось собрать все вместе и зарегистрировать нашу билд систему в 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"/>

Как можно видеть, менеджер нашей системы реализует:

Тут стоит выделить метод 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. Также там есть примеры простого и мульти-модульного проекта, чтобы более детально разобрать как плагин работает. Надеюсь это будет кому-нибудь полезно.


ссылка на оригинал статьи https://habr.com/ru/articles/759984/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *