Skip to content
Open
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
8 changes: 6 additions & 2 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
root = true

[*.{kt,kts}]
max_line_length=120
ij_kotlin_imports_layout=*
indent_size = 4
indent_style = space
max_line_length = 120
ij_kotlin_imports_layout = *
ij_kotlin_name_count_to_use_star_import = 2147483647
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
insert_final_newline = true
2 changes: 1 addition & 1 deletion src/main/kotlin/creator/custom/TemplateDescriptor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ data class TemplateDescriptor(

companion object {

const val FORMAT_VERSION = 1
const val FORMAT_VERSION = 2
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Minecraft Development for IntelliJ
*
* https://mcdev.io/
*
* Copyright (C) 2025 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 <https://www.gnu.org/licenses/>.
*/

package com.demonwav.mcdev.creator.custom.derivation

import com.demonwav.mcdev.creator.custom.PropertyDerivation
import com.demonwav.mcdev.creator.custom.TemplateValidationReporter
import com.demonwav.mcdev.creator.custom.types.CreatorProperty
import com.demonwav.mcdev.util.MinecraftVersions
import com.demonwav.mcdev.util.SemanticVersion

class ExtractPaperApiVersionPropertyDerivation : ExtractVersionMajorMinorPropertyDerivation() {

override fun derive(parentValues: List<Any?>): Any? {
val from = parentValues[0] as SemanticVersion
if (from >= MinecraftVersions.MC1_20_5) {
return from
}

return super.derive(parentValues);
}

companion object : PropertyDerivationFactory {

override fun create(
reporter: TemplateValidationReporter,
parents: List<CreatorProperty<*>?>?,
derivation: PropertyDerivation
): PreparedDerivation? {
if (parents.isNullOrEmpty()) {
reporter.error("Expected a parent")
return null
}

if (!parents[0]!!.acceptsType(SemanticVersion::class.java)) {
reporter.error("First parent must produce a semantic version")
return null
}

return ExtractPaperApiVersionPropertyDerivation()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import com.demonwav.mcdev.creator.custom.TemplateValidationReporter
import com.demonwav.mcdev.creator.custom.types.CreatorProperty
import com.demonwav.mcdev.util.SemanticVersion

class ExtractVersionMajorMinorPropertyDerivation : PreparedDerivation {
open class ExtractVersionMajorMinorPropertyDerivation : PreparedDerivation {

override fun derive(parentValues: List<Any?>): Any? {
val from = parentValues[0] as SemanticVersion
Expand Down
3 changes: 3 additions & 0 deletions src/main/kotlin/creator/custom/model/StringList.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,7 @@ data class StringList(val values: List<String>) : List<String> by values {
@JvmOverloads
fun toString(separator: String, prefix: String = "", postfix: String = ""): String =
values.joinToString(separator, prefix, postfix)

fun toStringQuoted(): String =
values.joinToString(", ", transform = { '"' + it + '"' })
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
/*
* Minecraft Development for IntelliJ
*
* https://mcdev.io/
*
* Copyright (C) 2025 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 <https://www.gnu.org/licenses/>.
*/

package com.demonwav.mcdev.creator.custom.types

import com.demonwav.mcdev.creator.collectMavenVersions
import com.demonwav.mcdev.creator.custom.CreatorContext
import com.demonwav.mcdev.creator.custom.CreatorCredentials
import com.demonwav.mcdev.creator.custom.TemplateEvaluator
import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor
import com.demonwav.mcdev.creator.custom.TemplateValidationReporter
import com.demonwav.mcdev.creator.custom.model.TemplateApi
import com.demonwav.mcdev.creator.custom.types.GradlePluginSelectorCreatorProperty.Holder
import com.demonwav.mcdev.util.SemanticVersion
import com.demonwav.mcdev.util.getOrLogException
import com.github.kittinunf.fuel.core.Request
import com.github.kittinunf.fuel.core.extensions.authentication
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.observable.properties.GraphProperty
import com.intellij.openapi.observable.util.transform
import com.intellij.ui.ComboboxSpeedSearch
import com.intellij.ui.JBColor
import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.RightGap
import com.intellij.ui.dsl.builder.bindItem
import com.intellij.ui.dsl.builder.bindSelected
import com.intellij.ui.dsl.builder.bindText
import com.intellij.util.ui.AsyncProcessIcon
import fleet.multiplatform.shims.ConcurrentHashMap
import java.util.function.Function
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.kotlin.cli.common.toBooleanLenient

class GradlePluginSelectorCreatorProperty(
descriptor: TemplatePropertyDescriptor,
context: CreatorContext
) : CreatorProperty<Holder>(descriptor, context, Holder::class.java) {
private val loadingVersionsProperty = graph.property(true)
private val loadingVersionsStatusProperty = graph.property("")

private val defaultValue = createDefaultValue(descriptor.default)

override val graphProperty: GraphProperty<Holder> = graph.property(defaultValue)
var property: Holder by graphProperty

private val versionsModel = graph.property<Set<SemanticVersion>>(emptySet())

private val versionProperty = graphProperty.transform({ it.version }, { property.copy(version = it) })
private val enabledProperty = graphProperty.transform({ it.enabled }, { property.copy(enabled = it) })

override fun createDefaultValue(raw: Any?): Holder {
if (raw is Boolean) {
return Holder(SemanticVersion(emptyList()), raw)
}
if (raw is String) {
return deserialize(raw)
}

return Holder(SemanticVersion(emptyList()), false)
}

override fun serialize(value: Holder): String = value.toString()

override fun deserialize(string: String): Holder =
Holder.tryParse(string) ?: Holder(SemanticVersion(emptyList()), false)

override fun buildUi(panel: Panel) {
val label = descriptor.translatedLabel
panel.row(label) {
checkBox("")
.bindSelected(enabledProperty)
.enabled(descriptor.editable != false)

label("Version:").gap(RightGap.SMALL)
val combobox = comboBox(versionsModel.get())
.bindItem(versionProperty)
.enabled(descriptor.editable != false)
.also { ComboboxSpeedSearch.installOn(it.component) }

val warning = descriptor.translatedWarning
if (warning != null) {
combobox.comment(descriptor.translate(warning))
}

cell(AsyncProcessIcon(makeStorageKey("progress")))
.visibleIf(loadingVersionsProperty)
label("").applyToComponent { foreground = JBColor.RED }
.bindText(loadingVersionsStatusProperty)
.visibleIf(loadingVersionsProperty)

versionsModel.afterChange { versions ->
combobox.component.removeAllItems()
for (version in versions) {
combobox.component.addItem(version)
}
}
}.propertyVisibility()
}

override fun setupProperty(reporter: TemplateValidationReporter) {
super.setupProperty(reporter)

var rawVersionFilter: (String) -> Boolean = { true }
var versionFilter: (SemanticVersion) -> Boolean = { true }

val url = descriptor.parameters?.get("sourceUrl") as? String
if (url == null) {
reporter.error("Expected string parameter 'sourceUrl'")
return
}

val rawVersionFilterCondition = descriptor.parameters["rawVersionFilter"]
if (rawVersionFilterCondition != null) {
if (rawVersionFilterCondition !is String) {
reporter.error("'rawVersionFilter' must be a string")
} else {
rawVersionFilter = { version ->
val props = mapOf("version" to version)
TemplateEvaluator.condition(props, rawVersionFilterCondition)
.getOrLogException(thisLogger()) == true
}
}
}

val versionFilterCondition = descriptor.parameters["versionFilter"]
if (versionFilterCondition != null) {
if (versionFilterCondition !is String) {
reporter.error("'versionFilter' must be a string")
} else {
versionFilter = { version ->
val props = mapOf("version" to version)
TemplateEvaluator.condition(props, versionFilterCondition)
.getOrLogException(thisLogger()) == true
}
}
}

downloadVersions(
context,
// The key might be a bit too unique, but that'll do the job
descriptor.name + "@" + descriptor.hashCode(),
url,
rawVersionFilter,
versionFilter,
descriptor.limit ?: 50
) { result ->
result.onSuccess { versions ->
val set = versions.toSet()
versionsModel.set(set)
loadingVersionsProperty.set(false)
}.onFailure { exception ->
loadingVersionsStatusProperty.set(exception.message ?: exception.javaClass.simpleName)
}
}
}

companion object {

private var versionsCache = ConcurrentHashMap<String, List<SemanticVersion>>()

fun downloadVersions(
context: CreatorContext,
key: String,
url: String,
rawVersionFilter: (String) -> Boolean,
versionFilter: (SemanticVersion) -> Boolean,
limit: Int,
uiCallback: (Result<List<SemanticVersion>>) -> Unit
) {
// Let's not mix up cached versions if different properties
// point to the same URL, but have different filters or limits
val cacheKey = "$key-$url"
val cachedVersions = versionsCache[cacheKey]
if (cachedVersions != null) {
uiCallback(Result.success(cachedVersions))
return
}

val scope = context.childScope("GradlePluginSelectorCreatorProperty")
scope.launch(Dispatchers.Default) {
val result = withContext(Dispatchers.IO) {
val requestCustomizer = CreatorCredentials.findMavenRepoCredentials(url)?.let { (user, pass) ->
Function<Request, Request> { request -> request.authentication().basic(user, pass) }
}

runCatching { collectMavenVersions(url, requestCustomizer) }
}.map { result ->
val versions = result.asSequence()
.filter(rawVersionFilter)
.mapNotNull(SemanticVersion::tryParse)
.filter(versionFilter)
.sortedDescending()
.take(limit)
.toList()

versionsCache[cacheKey] = versions
versions
}

withContext(context.uiContext) {
uiCallback(result)
}
}
}
}

class Factory : CreatorPropertyFactory {
override fun create(
descriptor: TemplatePropertyDescriptor,
context: CreatorContext
): CreatorProperty<*> = GradlePluginSelectorCreatorProperty(descriptor, context)
}

@TemplateApi
data class Holder(
val version: SemanticVersion,
val enabled: Boolean
) {
override fun toString(): String {
return "$enabled $version"
}

companion object {
fun tryParse(raw: String): Holder? {
val split = raw.split(" ", limit = 2)
return when (split.size) {
1 -> raw.toBooleanLenient()?.let { Holder(SemanticVersion(emptyList()), it) }
2 -> split[0].toBooleanLenient()?.let {
Holder(
SemanticVersion.tryParse(split[1]) ?: SemanticVersion(emptyList()),
it
)
}

else -> null
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class MavenArtifactVersionCreatorProperty(
var versionFilter: (SemanticVersion) -> Boolean = { true }

override val graphProperty: GraphProperty<SemanticVersion> = graph.property(SemanticVersion(emptyList()))
private val versionsProperty = graph.property<Collection<SemanticVersion>>(emptyList())
private val versionsProperty = graph.property<Set<SemanticVersion>>(emptySet())
private val loadingVersionsProperty = graph.property(true)
private val loadingVersionsStatusProperty = graph.property("")

Expand Down Expand Up @@ -127,7 +127,7 @@ class MavenArtifactVersionCreatorProperty(
descriptor.limit ?: 50
) { result ->
result.onSuccess { versions ->
versionsProperty.set(versions)
versionsProperty.set(versions.toSet())
loadingVersionsProperty.set(false)
}.onFailure { exception ->
loadingVersionsStatusProperty.set(exception.message ?: exception.javaClass.simpleName)
Expand Down
Loading