User: rednesto Date: 06 Aug 24 19:05 Revision: 896f3b5228c24f6f2412b5084d0cdf8679da3fb2 Summary: Extract creator template processing to a separate class TeamCity URL: https://ci.mcdev.io/viewModification.html?tab=vcsModificationFiles&modId=9572&personal=false Index: src/main/kotlin/creator/custom/CreatorTemplateProcessor.kt =================================================================== --- src/main/kotlin/creator/custom/CreatorTemplateProcessor.kt (revision 896f3b5228c24f6f2412b5084d0cdf8679da3fb2) +++ src/main/kotlin/creator/custom/CreatorTemplateProcessor.kt (revision 896f3b5228c24f6f2412b5084d0cdf8679da3fb2) @@ -0,0 +1,355 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.asset.MCDevBundle.invoke +import com.demonwav.mcdev.creator.custom.finalizers.CreatorFinalizer +import com.demonwav.mcdev.creator.custom.providers.EmptyLoadedTemplate +import com.demonwav.mcdev.creator.custom.providers.LoadedTemplate +import com.demonwav.mcdev.creator.custom.types.CreatorProperty +import com.demonwav.mcdev.creator.custom.types.CreatorPropertyFactory +import com.demonwav.mcdev.creator.custom.types.ExternalCreatorProperty +import com.demonwav.mcdev.util.toTypedArray +import com.demonwav.mcdev.util.virtualFileOrError +import com.intellij.codeInsight.CodeInsightSettings +import com.intellij.codeInsight.actions.ReformatCodeProcessor +import com.intellij.ide.projectView.ProjectView +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.application.WriteAction +import com.intellij.openapi.diagnostic.Attachment +import com.intellij.openapi.diagnostic.ControlFlowException +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.module.ModuleManager +import com.intellij.openapi.module.ModuleTypeId +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ModuleRootManager +import com.intellij.openapi.ui.DialogPanel +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.refreshAndFindVirtualFile +import com.intellij.psi.PsiManager +import com.intellij.ui.JBColor +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.panel +import com.intellij.util.application +import java.nio.file.Path +import java.util.function.Consumer +import kotlin.collections.mapNotNull +import kotlin.collections.orEmpty +import kotlin.collections.set +import kotlin.io.path.createDirectories +import kotlin.io.path.writeText + +interface ExternalTemplatePropertyProvider { + + val projectNameProperty: GraphProperty + + val useGit: Boolean +} + +/** + * Handles all the logic involved in making the creator UI and generating the project files. + */ +class CreatorTemplateProcessor( + propertyGraph: PropertyGraph, + wizardContext: WizardContext, + private val externalPropertyProvider: ExternalTemplatePropertyProvider +) { + + var hasTemplateErrors: Boolean = true + private set + + private var properties: MutableMap> = mutableMapOf() + private var context: CreatorContext = CreatorContext(propertyGraph, properties, wizardContext) + + fun createOptionsPanel(template: LoadedTemplate): DialogPanel? { + properties = mutableMapOf() + context = context.copy(properties = properties) + + if (!template.isValid) { + return null + } + + val projectNameProperty = externalPropertyProvider.projectNameProperty + properties["PROJECT_NAME"] = ExternalCreatorProperty( + context = context, + graphProperty = projectNameProperty, + valueType = String::class.java + ) + + return panel { + val reporter = TemplateValidationReporterImpl() + val uiFactories = setupTemplate(template, reporter) + if (uiFactories.isEmpty() && !reporter.hasErrors) { + row { + label(MCDevBundle("creator.ui.warn.no_properties")) + .component.foreground = JBColor.YELLOW + } + } else { + hasTemplateErrors = reporter.hasErrors + reporter.display(this) + + if (!reporter.hasErrors) { + for (uiFactory in uiFactories) { + uiFactory.accept(this) + } + } + } + } + } + + private fun setupTemplate( + template: LoadedTemplate, + reporter: TemplateValidationReporterImpl + ): List> { + return try { + val properties = template.descriptor.properties.orEmpty() + .mapNotNull { + reporter.subject = it.name + setupProperty(it, reporter) + } + .sortedBy { (_, order) -> order } + .map { it.first } + + val finalizers = template.descriptor.finalizers + if (finalizers != null) { + CreatorFinalizer.validateAll(reporter, finalizers) + } + + properties + } catch (t: Throwable) { + if (t is ControlFlowException) { + throw t + } + + thisLogger().error( + "Unexpected error during template setup", + t, + template.label, + template.descriptor.toString() + ) + + emptyList() + } finally { + reporter.subject = null + } + } + + private fun setupProperty( + descriptor: TemplatePropertyDescriptor, + reporter: TemplateValidationReporter + ): Pair, Int>? { + if (!descriptor.groupProperties.isNullOrEmpty()) { + val childrenUiFactories = descriptor.groupProperties + .mapNotNull { setupProperty(it, reporter) } + .sortedBy { (_, order) -> order } + .map { it.first } + + val factory = Consumer { panel -> + val label = descriptor.translatedLabel + if (descriptor.collapsible == false) { + panel.group(label) { + for (childFactory in childrenUiFactories) { + childFactory.accept(this@group) + } + } + } else { + val group = panel.collapsibleGroup(label) { + for (childFactory in childrenUiFactories) { + childFactory.accept(this@collapsibleGroup) + } + } + + group.expanded = descriptor.default as? Boolean ?: false + } + } + + val order = descriptor.order ?: 0 + return factory to order + } + + if (descriptor.name in properties.keys) { + reporter.fatal("Duplicate property name ${descriptor.name}") + } + + val prop = CreatorPropertyFactory.createFromType(descriptor.type, descriptor, context) + if (prop == null) { + reporter.fatal("Unknown template property type ${descriptor.type}") + } + + prop.setupProperty(reporter) + + properties[descriptor.name] = prop + + if (descriptor.visible == false) { + return null + } + + val factory = Consumer { panel -> prop.buildUi(panel) } + val order = descriptor.order ?: 0 + return factory to order + } + + fun generateFiles(project: Project, template: LoadedTemplate) { + if (template is EmptyLoadedTemplate) { + return + } + + val projectPath = context.wizardContext.projectDirectory + val templateProperties = collectTemplateProperties() + thisLogger().debug("Template properties: $templateProperties") + + val generatedFiles = mutableListOf>() + for (file in template.descriptor.files.orEmpty()) { + if (file.condition != null && + !TemplateEvaluator.condition(templateProperties, file.condition).getOrElse { false } + ) { + continue + } + + val relativeTemplate = TemplateEvaluator.template(templateProperties, file.template).getOrNull() + ?: continue + val relativeDest = TemplateEvaluator.template(templateProperties, file.destination).getOrNull() + ?: continue + + try { + val templateContents = template.loadTemplateContents(relativeTemplate) + ?: continue + + val destPath = projectPath.resolve(relativeDest).toAbsolutePath() + if (!destPath.startsWith(projectPath)) { + // We want to make sure template files aren't 'escaping' the project directory + continue + } + + var fileTemplateProperties = templateProperties + if (file.properties != null) { + fileTemplateProperties = templateProperties.toMutableMap() + fileTemplateProperties.putAll(file.properties) + } + + val processedContent = TemplateEvaluator.template(fileTemplateProperties, templateContents) + .onFailure { t -> + val attachment = Attachment(relativeTemplate, templateContents) + thisLogger().error("Failed evaluate template '$relativeTemplate'", t, attachment) + } + .getOrNull() + ?: continue + + destPath.parent.createDirectories() + destPath.writeText(processedContent) + + val virtualFile = destPath.refreshAndFindVirtualFile() + if (virtualFile != null) { + generatedFiles.add(file to virtualFile) + } else { + thisLogger().warn("Could not find VirtualFile for file generated at $destPath (descriptor: $file)") + } + } catch (t: Throwable) { + if (t is ControlFlowException) { + throw t + } + + thisLogger().error("Failed to process template file $file", t) + } + } + + val finalizeAction = { + WriteAction.runAndWait { + LocalFileSystem.getInstance().refresh(false) + // Apparently a module root is required for the reformat to work + setupTempRootModule(project, projectPath) + + reformatFiles(project, generatedFiles) + openFilesInEditor(project, generatedFiles) + } + + val finalizers = template.descriptor.finalizers + if (!finalizers.isNullOrEmpty()) { + CreatorFinalizer.executeAll(context.wizardContext, project, finalizers, templateProperties) + } + } + if (context.wizardContext.isCreatingNewProject) { + TemplateService.instance.registerFinalizerAction(project, finalizeAction) + } else { + application.executeOnPooledThread { finalizeAction() } + } + } + + private fun setupTempRootModule(project: Project, projectPath: Path) { + val modifiableModel = ModuleManager.getInstance(project).getModifiableModel() + val module = modifiableModel.newNonPersistentModule("mcdev-temp-root", ModuleTypeId.JAVA_MODULE) + val rootsModel = ModuleRootManager.getInstance(module).modifiableModel + rootsModel.addContentEntry(projectPath.virtualFileOrError) + rootsModel.commit() + modifiableModel.commit() + } + + private fun collectTemplateProperties(): MutableMap { + val into = mutableMapOf() + + into.putAll(TemplateEvaluator.baseProperties) + + into["USE_GIT"] = externalPropertyProvider.useGit + + return properties.mapValuesTo(into) { (_, prop) -> prop.get() } + } + + private fun reformatFiles( + project: Project, + files: MutableList> + ) { + val psiManager = PsiManager.getInstance(project) + val psiFiles = files.asSequence() + .filter { (desc, _) -> desc.reformat != false } + .mapNotNull { (_, file) -> psiManager.findFile(file) } + + val processor = ReformatCodeProcessor(project, psiFiles.toTypedArray(), null, false) + psiFiles.forEach(processor::setDoNotKeepLineBreaks) + + val insightSettings = CodeInsightSettings.getInstance() + val oldSecondReformat = insightSettings.ENABLE_SECOND_REFORMAT + insightSettings.ENABLE_SECOND_REFORMAT = true + try { + processor.run() + } finally { + insightSettings.ENABLE_SECOND_REFORMAT = oldSecondReformat + } + } + + private fun openFilesInEditor( + project: Project, + files: MutableList> + ) { + val fileEditorManager = FileEditorManager.getInstance(project) + val projectView = ProjectView.getInstance(project) + for ((desc, file) in files) { + if (desc.openInEditor == true) { + fileEditorManager.openFile(file, true) + projectView.select(null, file, false) + } + } + } +} Index: src/main/kotlin/creator/custom/CustomPlatformStep.kt =================================================================== --- src/main/kotlin/creator/custom/CustomPlatformStep.kt (revision fc65be86388514694b95348642ae00493b402eb6) +++ src/main/kotlin/creator/custom/CustomPlatformStep.kt (revision 896f3b5228c24f6f2412b5084d0cdf8679da3fb2) @@ -22,43 +22,23 @@ import com.demonwav.mcdev.MinecraftSettings import com.demonwav.mcdev.asset.MCDevBundle -import com.demonwav.mcdev.creator.custom.finalizers.CreatorFinalizer import com.demonwav.mcdev.creator.custom.providers.EmptyLoadedTemplate import com.demonwav.mcdev.creator.custom.providers.LoadedTemplate import com.demonwav.mcdev.creator.custom.providers.TemplateProvider -import com.demonwav.mcdev.creator.custom.types.CreatorProperty -import com.demonwav.mcdev.creator.custom.types.CreatorPropertyFactory -import com.demonwav.mcdev.creator.custom.types.ExternalCreatorProperty import com.demonwav.mcdev.creator.modalityState -import com.demonwav.mcdev.util.toTypedArray -import com.demonwav.mcdev.util.virtualFileOrError -import com.intellij.codeInsight.CodeInsightSettings -import com.intellij.codeInsight.actions.ReformatCodeProcessor -import com.intellij.ide.projectView.ProjectView import com.intellij.ide.wizard.AbstractNewProjectWizardStep import com.intellij.ide.wizard.GitNewProjectWizardData import com.intellij.ide.wizard.NewProjectWizardBaseData import com.intellij.ide.wizard.NewProjectWizardStep -import com.intellij.openapi.application.WriteAction -import com.intellij.openapi.diagnostic.Attachment -import com.intellij.openapi.diagnostic.ControlFlowException import com.intellij.openapi.diagnostic.getOrLogException import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.diagnostic.thisLogger -import com.intellij.openapi.fileEditor.FileEditorManager -import com.intellij.openapi.module.ModuleManager -import com.intellij.openapi.module.ModuleTypeId +import com.intellij.openapi.observable.properties.GraphProperty import com.intellij.openapi.observable.util.transform import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.progress.Task import com.intellij.openapi.project.Project -import com.intellij.openapi.roots.ModuleRootManager -import com.intellij.openapi.vfs.LocalFileSystem -import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFileManager -import com.intellij.openapi.vfs.refreshAndFindVirtualFile -import com.intellij.psi.PsiManager import com.intellij.ui.JBColor import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.Cell @@ -67,17 +47,11 @@ import com.intellij.ui.dsl.builder.SegmentedButton import com.intellij.ui.dsl.builder.TopGap import com.intellij.ui.dsl.builder.bindText -import com.intellij.ui.dsl.builder.panel import com.intellij.util.application import com.intellij.util.ui.AsyncProcessIcon -import java.nio.file.Path -import java.util.function.Consumer import javax.swing.JLabel import kotlin.collections.component1 import kotlin.collections.component2 -import kotlin.collections.set -import kotlin.io.path.createDirectories -import kotlin.io.path.writeText /** * The step to select a custom template repo. @@ -117,10 +91,15 @@ lateinit var noTemplatesAvailable: Cell var templateLoadingIndicator: ProgressIndicator? = null - private var hasTemplateErrors: Boolean = true + private val externalPropertyProvider = object : ExternalTemplatePropertyProvider { + override val projectNameProperty: GraphProperty + get() = data.getUserData(NewProjectWizardBaseData.KEY)?.nameProperty + ?: throw RuntimeException("Could not find wizard base data") - private var properties = mutableMapOf>() - private var creatorContext = CreatorContext(propertyGraph, properties, context) + override val useGit: Boolean + get() = data.getUserData(GitNewProjectWizardData.KEY)?.git == true + } + private val templateProcessor = CreatorTemplateProcessor(propertyGraph, context, externalPropertyProvider) override fun setupUI(builder: Panel) { lateinit var templatePropertyPlaceholder: Placeholder @@ -166,7 +145,7 @@ segmentedButton(emptyList(), LoadedTemplate::label, LoadedTemplate::tooltip) .bind(selectedTemplateProperty) .validation { - addApplyRule("", condition = ::hasTemplateErrors) + addApplyRule("", condition = templateProcessor::hasTemplateErrors) } }.visibleIf( availableTemplatesProperty.transform { it.size > 1 } @@ -191,7 +170,7 @@ } selectedTemplateProperty.afterChange { template -> - createOptionsPanelInBackground(template, templatePropertyPlaceholder) + templatePropertyPlaceholder.component = templateProcessor.createOptionsPanel(template) } builder.row { @@ -310,276 +289,7 @@ ProgressManager.getInstance().runProcessWithProgressAsynchronously(task, indicator) } - private fun createOptionsPanelInBackground(template: LoadedTemplate, placeholder: Placeholder) { - properties = mutableMapOf() - creatorContext = creatorContext.copy(properties = properties) - - if (!template.isValid) { - return - } - - val baseData = data.getUserData(NewProjectWizardBaseData.KEY) - ?: return thisLogger().error("Could not find wizard base data") - - properties["PROJECT_NAME"] = ExternalCreatorProperty( - context = creatorContext, - graphProperty = baseData.nameProperty, - valueType = String::class.java - ) - - placeholder.component = panel { - val reporter = TemplateValidationReporterImpl() - val uiFactories = setupTemplate(template, reporter) - if (uiFactories.isEmpty() && !reporter.hasErrors) { - row { - label(MCDevBundle("creator.ui.warn.no_properties")) - .component.foreground = JBColor.YELLOW - } - } else { - hasTemplateErrors = reporter.hasErrors - reporter.display(this) - - if (!reporter.hasErrors) { - for (uiFactory in uiFactories) { - uiFactory.accept(this) - } - } - } - } - } - - private fun setupTemplate( - template: LoadedTemplate, - reporter: TemplateValidationReporterImpl - ): List> { - return try { - val properties = template.descriptor.properties.orEmpty() - .mapNotNull { - reporter.subject = it.name - setupProperty(it, reporter) - } - .sortedBy { (_, order) -> order } - .map { it.first } - - val finalizers = template.descriptor.finalizers - if (finalizers != null) { - CreatorFinalizer.validateAll(reporter, finalizers) - } - - properties - } catch (t: Throwable) { - if (t is ControlFlowException) { - throw t - } - - thisLogger().error( - "Unexpected error during template setup", - t, - template.label, - template.descriptor.toString() - ) - - emptyList() - } finally { - reporter.subject = null - } - } - - private fun setupProperty( - descriptor: TemplatePropertyDescriptor, - reporter: TemplateValidationReporter - ): Pair, Int>? { - if (!descriptor.groupProperties.isNullOrEmpty()) { - val childrenUiFactories = descriptor.groupProperties - .mapNotNull { setupProperty(it, reporter) } - .sortedBy { (_, order) -> order } - .map { it.first } - - val factory = Consumer { panel -> - val label = descriptor.translatedLabel - if (descriptor.collapsible == false) { - panel.group(label) { - for (childFactory in childrenUiFactories) { - childFactory.accept(this@group) - } - } - } else { - val group = panel.collapsibleGroup(label) { - for (childFactory in childrenUiFactories) { - childFactory.accept(this@collapsibleGroup) - } - } - - group.expanded = descriptor.default as? Boolean ?: false - } - } - - val order = descriptor.order ?: 0 - return factory to order - } - - if (descriptor.name in properties.keys) { - reporter.fatal("Duplicate property name ${descriptor.name}") - } - - val prop = CreatorPropertyFactory.createFromType(descriptor.type, descriptor, creatorContext) - if (prop == null) { - reporter.fatal("Unknown template property type ${descriptor.type}") - } - - prop.setupProperty(reporter) - - properties[descriptor.name] = prop - - if (descriptor.visible == false) { - return null - } - - val factory = Consumer { panel -> prop.buildUi(panel) } - val order = descriptor.order ?: 0 - return factory to order - } - override fun setupProject(project: Project) { - val template = selectedTemplate - if (template is EmptyLoadedTemplate) { - return + templateProcessor.generateFiles(project, selectedTemplate) - } + } - - val projectPath = context.projectDirectory - val templateProperties = collectTemplateProperties() - thisLogger().debug("Template properties: $templateProperties") - - val generatedFiles = mutableListOf>() - for (file in template.descriptor.files.orEmpty()) { - if (file.condition != null && - !TemplateEvaluator.condition(templateProperties, file.condition).getOrElse { false } - ) { - continue - } +} - - val relativeTemplate = TemplateEvaluator.template(templateProperties, file.template).getOrNull() - ?: continue - val relativeDest = TemplateEvaluator.template(templateProperties, file.destination).getOrNull() - ?: continue - - try { - val templateContents = template.loadTemplateContents(relativeTemplate) - ?: continue - - val destPath = projectPath.resolve(relativeDest).toAbsolutePath() - if (!destPath.startsWith(projectPath)) { - // We want to make sure template files aren't 'escaping' the project directory - continue - } - - var fileTemplateProperties = templateProperties - if (file.properties != null) { - fileTemplateProperties = templateProperties.toMutableMap() - fileTemplateProperties.putAll(file.properties) - } - - val processedContent = TemplateEvaluator.template(fileTemplateProperties, templateContents) - .onFailure { t -> - val attachment = Attachment(relativeTemplate, templateContents) - thisLogger().error("Failed evaluate template '$relativeTemplate'", t, attachment) - } - .getOrNull() - ?: continue - - destPath.parent.createDirectories() - destPath.writeText(processedContent) - - val virtualFile = destPath.refreshAndFindVirtualFile() - if (virtualFile != null) { - generatedFiles.add(file to virtualFile) - } else { - thisLogger().warn("Could not find VirtualFile for file generated at $destPath (descriptor: $file)") - } - } catch (t: Throwable) { - if (t is ControlFlowException) { - throw t - } - - thisLogger().error("Failed to process template file $file", t) - } - } - - val finalizeAction = { - WriteAction.runAndWait { - LocalFileSystem.getInstance().refresh(false) - // Apparently a module root is required for the reformat to work - setupTempRootModule(project, projectPath) - - reformatFiles(project, generatedFiles) - openFilesInEditor(project, generatedFiles) - } - - val finalizers = selectedTemplate.descriptor.finalizers - if (!finalizers.isNullOrEmpty()) { - CreatorFinalizer.executeAll(context, project, finalizers, templateProperties) - } - } - if (context.isCreatingNewProject) { - TemplateService.instance.registerFinalizerAction(project, finalizeAction) - } else { - application.executeOnPooledThread { finalizeAction() } - } - } - - private fun setupTempRootModule(project: Project, projectPath: Path) { - val modifiableModel = ModuleManager.getInstance(project).getModifiableModel() - val module = modifiableModel.newNonPersistentModule("mcdev-temp-root", ModuleTypeId.JAVA_MODULE) - val rootsModel = ModuleRootManager.getInstance(module).modifiableModel - rootsModel.addContentEntry(projectPath.virtualFileOrError) - rootsModel.commit() - modifiableModel.commit() - } - - private fun collectTemplateProperties(): MutableMap { - val into = mutableMapOf() - - into.putAll(TemplateEvaluator.baseProperties) - - val gitData = data.getUserData(GitNewProjectWizardData.KEY) - into["USE_GIT"] = gitData?.git == true - - return properties.mapValuesTo(into) { (_, prop) -> prop.get() } - } - - private fun reformatFiles( - project: Project, - files: MutableList> - ) { - val psiManager = PsiManager.getInstance(project) - val psiFiles = files.asSequence() - .filter { (desc, _) -> desc.reformat != false } - .mapNotNull { (_, file) -> psiManager.findFile(file) } - - val processor = ReformatCodeProcessor(project, psiFiles.toTypedArray(), null, false) - psiFiles.forEach(processor::setDoNotKeepLineBreaks) - - val insightSettings = CodeInsightSettings.getInstance() - val oldSecondReformat = insightSettings.ENABLE_SECOND_REFORMAT - insightSettings.ENABLE_SECOND_REFORMAT = true - try { - processor.run() - } finally { - insightSettings.ENABLE_SECOND_REFORMAT = oldSecondReformat - } - } - - private fun openFilesInEditor( - project: Project, - files: MutableList> - ) { - val fileEditorManager = FileEditorManager.getInstance(project) - val projectView = ProjectView.getInstance(project) - for ((desc, file) in files) { - if (desc.openInEditor == true) { - fileEditorManager.openFile(file, true) - projectView.select(null, file, false) - } - } - } -}