Привет, ранее я написал статью о своем плагине и том, как переосмыслил подход к получению проектной модели 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. Также там есть примеры простого и мульти-модульного проекта, чтобы более детально разобрать как плагин работает. Надеюсь это будет кому-нибудь полезно.
Комментарии (4)
grisha9 Автор
11.09.2023 16:48+1Ковырял исходники idea) мне это также надо было для написания своего плагина из первой статьи. По этой причине я в конце каждой своей публикации про idea-плагины оставляю ссылку ну репоизиторий idea-community. Собственно тут я и решил задокументировать свой опыт, если кому то еще понадобится.
kavaynya
Спасибо за статью. Так как я и сам уже писал плагин для idea, мне интересно где вы черпали информацию? В оф. доках весьма мало информации, и в интернете найти что-то сложно.
grisha9 Автор
продублирую ссылкой. т.к. "промазал" с ответом, а редактировать уже поздно. https://habr.com/ru/articles/759984/comments/#comment_25953974