Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[submodule "templates"]
path = templates
branch = v3
branch = v4
url = https://github.com/minecraft-dev/templates
209 changes: 123 additions & 86 deletions src/main/kotlin/creator/custom/CreatorTemplateProcessor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* https://mcdev.io/
*
* Copyright (C) 2025 minecraft-dev
* Copyright (C) 2026 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
Expand Down Expand Up @@ -44,6 +44,7 @@ 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.util.Key
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.refreshAndFindVirtualFile
Expand Down Expand Up @@ -119,6 +120,11 @@ class CreatorTemplateProcessor(
}
}
}

if (!reporter.hasErrors) {
// Always shown, even without properties, as these can include warnings about what the template does
CreatorFinalizer.buildUiAll(this, context, template.descriptor.finalizers.orEmpty())
}
}
}

Expand Down Expand Up @@ -230,72 +236,20 @@ class CreatorTemplateProcessor(
val templateProperties = collectTemplateProperties()
thisLogger().debug("Template properties: $templateProperties")

val generatedFiles = mutableListOf<Pair<TemplateFile, Path>>()
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)

generatedFiles.add(file to destPath)
} catch (t: Throwable) {
if (t is ControlFlowException) {
throw t
}

thisLogger().error("Failed to process template file $file", t)
}
}
val generatedFiles = writeFiles(template, template.descriptor.files.orEmpty(), templateProperties, projectPath)

val finalizeAction = suspend {
WriteAction.runAndWait<Throwable> {
LocalFileSystem.getInstance().refresh(false)
// Apparently a module root is required for the reformat to work
setupTempRootModule(project, projectPath)

val genVirtualFiles = generatedFiles
.mapNotNullTo(mutableListOf()) {
it.first to (it.second.refreshAndFindVirtualFile() ?: return@mapNotNullTo null)
}
reformatFiles(project, genVirtualFiles)
openFilesInEditor(project, genVirtualFiles)
reformatAndOpenFiles(project, generatedFiles)
}

val finalizers = template.descriptor.finalizers
if (!finalizers.isNullOrEmpty()) {
context.wizardContext.putUserData(LOADED_TEMPLATE_KEY, template)
CreatorFinalizer.executeAll(context.wizardContext, project, finalizers, templateProperties)
}
}
Expand Down Expand Up @@ -325,38 +279,121 @@ class CreatorTemplateProcessor(
return properties.mapValuesTo(into) { (_, prop) -> prop.get() }
}

private fun reformatFiles(
project: Project,
files: MutableList<Pair<TemplateFile, VirtualFile>>
) {
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
companion object {
/**
* The template whose files are being generated, so finalizers can read files from it.
*/
val LOADED_TEMPLATE_KEY = Key.create<LoadedTemplate>("mcdev.creator.loadedTemplate")

/**
* Evaluates the given template files and writes them to the project directory.
*
* @return the files that were written, and where they were written to
*/
fun writeFiles(
template: LoadedTemplate,
files: List<TemplateFile>,
templateProperties: Map<String, Any?>,
projectPath: Path,
): List<Pair<TemplateFile, Path>> {
val generatedFiles = mutableListOf<Pair<TemplateFile, Path>>()
for (file in files) {
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)

generatedFiles.add(file to destPath)
} catch (t: Throwable) {
if (t is ControlFlowException) {
throw t
}

thisLogger().error("Failed to process template file $file", t)
}
}
return generatedFiles
}

/**
* Reformats and opens the given generated files, as requested by their [TemplateFile]. Must be called in a
* write action.
*/
fun reformatAndOpenFiles(project: Project, generatedFiles: List<Pair<TemplateFile, Path>>) {
val genVirtualFiles = generatedFiles
.mapNotNullTo(mutableListOf()) {
it.first to (it.second.refreshAndFindVirtualFile() ?: return@mapNotNullTo null)
}
reformatFiles(project, genVirtualFiles)
openFilesInEditor(project, genVirtualFiles)
}

private fun reformatFiles(
project: Project,
files: MutableList<Pair<TemplateFile, VirtualFile>>
) {
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<Pair<TemplateFile, VirtualFile>>
) {
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)
private fun openFilesInEditor(
project: Project,
files: MutableList<Pair<TemplateFile, VirtualFile>>
) {
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)
}
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/main/kotlin/creator/custom/TemplateDescriptor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* https://mcdev.io/
*
* Copyright (C) 2025 minecraft-dev
* Copyright (C) 2026 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
Expand Down Expand Up @@ -40,7 +40,7 @@ data class TemplateDescriptor(

companion object {

val SUPPORTED_FORMAT_VERSIONS = intArrayOf(3, 2, 1)
val SUPPORTED_FORMAT_VERSIONS = intArrayOf(4, 3, 2, 1)
val LATEST_FORMAT_VERSION = SUPPORTED_FORMAT_VERSIONS[0];
}
}
Expand Down
32 changes: 32 additions & 0 deletions src/main/kotlin/creator/custom/finalizers/CreatorFinalizer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

package com.demonwav.mcdev.creator.custom.finalizers

import com.demonwav.mcdev.creator.custom.CreatorContext
import com.demonwav.mcdev.creator.custom.TemplateEvaluator
import com.demonwav.mcdev.creator.custom.TemplateValidationReporter
import com.demonwav.mcdev.creator.custom.TemplateValidationReporterImpl
Expand All @@ -31,13 +32,21 @@ import com.intellij.openapi.extensions.RequiredElement
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.KeyedExtensionCollector
import com.intellij.serviceContainer.BaseKeyedLazyInstance
import com.intellij.ui.dsl.builder.Panel
import com.intellij.util.KeyedLazyInstance
import com.intellij.util.xmlb.annotations.Attribute

interface CreatorFinalizer {

fun validate(reporter: TemplateValidationReporter, properties: Map<String, Any>) = Unit

/**
* Creates the UI elements to show in the creator when this finalizer is part of the template, such as settings
* the finalizer needs or warnings about what it will do. Identical (as in [equals]) elements created by multiple
* finalizers are only shown once.
*/
fun createUi(properties: Map<String, Any>): List<FinalizerUi> = emptyList()

suspend fun execute(
context: WizardContext,
project: Project,
Expand Down Expand Up @@ -83,6 +92,22 @@ interface CreatorFinalizer {
}
}

fun buildUiAll(
panel: Panel,
context: CreatorContext,
finalizers: List<Map<String, Any>>,
) {
val uis = finalizers.flatMap { properties ->
val finalizer = (properties["type"] as? String)?.let(COLLECTOR::findSingle)
?: return@flatMap emptyList()
finalizer.createUi(properties)
}

for (ui in uis.distinct()) {
ui.buildUi(panel, context)
}
}

suspend fun executeAll(
context: WizardContext,
project: Project,
Expand Down Expand Up @@ -112,6 +137,13 @@ interface CreatorFinalizer {
}
}

/**
* A part of the creator UI contributed by a [CreatorFinalizer].
*/
interface FinalizerUi {
fun buildUi(panel: Panel, context: CreatorContext)
}

class CreatorFinalizerBean : BaseKeyedLazyInstance<CreatorFinalizer>(), KeyedLazyInstance<CreatorFinalizer> {

@Attribute("type")
Expand Down
Loading
Loading