IJ-MR-219988

IJPL-253731 add IntelliJ Platform dependency version completion

Import product release metadata before Gradle task execution and use it to suggest product-specific versions in Kotlin DSL builds. Publish fetched models after successful and failed syncs so invalid versions can be corrected during initial import.

Signed-off-by: Jakub Chrzanowski <jakub.chrzanowski@jetbrains.com>

(cherry picked from commit 047e54ca52d6e0435cc06d90664ae259a30598fd)
Signed-off-by: Jakub Chrzanowski <jakub.chrzanowski@jetbrains.com>

GitOrigin-RevId: f87119fb6d747b8d5d55c83e96865601d861dc7c
This commit is contained in:
Jakub Chrzanowski
2026-09-01 21:48:44 +00:00
committed by intellij-monorepo-bot
parent bf5d227b92
commit 7aeed064b8
17 changed files with 840 additions and 1 deletions
@@ -21,7 +21,9 @@ jvm_library(
visibility = ["//visibility:public"],
deps = [
# do not sort,
"@lib//:kotlin-stdlib",
"@lib//:jetbrains-annotations",
"//plugins/gradle/tooling-extension-api:toolingExtension",
"//plugins/gradle/tooling-extension-impl:toolingExtension-impl",
"@lib//:gradle-provided",
],
@@ -40,6 +42,7 @@ jvm_library(
runtime_deps = [
# do not sort,
":gradle-tooling",
"//plugins/gradle/tooling-extension-api:toolingExtension_test_lib",
"//plugins/gradle/tooling-extension-impl:toolingExtension-impl_test_lib",
"@lib//:gradle-provided",
],
@@ -24,7 +24,9 @@
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="library" name="jetbrains-annotations" level="project" />
<orderEntry type="module" module-name="intellij.gradle.toolingExtension" />
<orderEntry type="module" module-name="intellij.gradle.toolingExtension.impl" />
<orderEntry type="library" scope="PROVIDED" name="Gradle" level="project" />
</component>
@@ -0,0 +1,2 @@
# Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
com.intellij.devkit.gradle.tooling.IntelliJPlatformModelBuilder
@@ -1,5 +1,7 @@
<idea-plugin>
<dependencies>
<!-- region Generated dependencies - run `Generate Product Layouts` to regenerate -->
<module name="intellij.gradle.toolingExtension"/>
<module name="intellij.gradle.toolingExtension.impl"/>
</dependencies>
</idea-plugin>
@@ -0,0 +1,95 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.devkit.gradle.tooling
import org.gradle.api.Project
import org.gradle.api.plugins.ExtensionAware
import org.gradle.tooling.model.Model
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.plugins.gradle.tooling.AbstractModelBuilderService
import org.jetbrains.plugins.gradle.tooling.Message
import org.jetbrains.plugins.gradle.tooling.ModelBuilderContext
import java.io.Serializable
import java.lang.reflect.ParameterizedType
private const val INTELLIJ_PLATFORM_EXTENSION_NAME = "intellijPlatform"
private const val INTELLIJ_PLATFORM_DEPENDENCY_CONFIGURATION_CLASS =
"org.jetbrains.intellij.platform.gradle.extensions.IntelliJPlatformDependencyConfiguration"
private const val INTELLIJ_PLATFORM_TYPE_CLASS = "org.jetbrains.intellij.platform.gradle.IntelliJPlatformType"
private const val DUMP_PRODUCTS_RELEASES_TASK_NAME = "dumpProductsReleases"
private const val GRADLE_ACTION_CLASS = "org.gradle.api.Action"
private const val PRODUCT_CODE_GETTER = "getCode"
/**
* Transfers IntelliJ Platform dependency helpers and the generated product-release catalog from Gradle to the IDE.
*/
@ApiStatus.Internal
interface IntelliJPlatformGradleModel : Model, Serializable {
val dependencyHelperProductCodes: Map<String, String>
val productReleasesFile: String?
}
internal class IntelliJPlatformGradleModelImpl(
override val dependencyHelperProductCodes: Map<String, String>,
override val productReleasesFile: String?,
) : IntelliJPlatformGradleModel
/**
* Reflects over the IntelliJ Platform Gradle Plugin DSL and schedules its release-catalog dump for IDE import.
*/
@ApiStatus.Internal
class IntelliJPlatformModelBuilder : AbstractModelBuilderService() {
override fun canBuild(modelName: String?): Boolean {
return modelName == IntelliJPlatformGradleModel::class.java.name
}
override fun buildAll(modelName: String, project: Project, context: ModelBuilderContext): Any? {
val extension = (project.dependencies as? ExtensionAware)
?.extensions
?.findByName(INTELLIJ_PLATFORM_EXTENSION_NAME)
?: return null
val dumpTask = project.tasks.findByName(DUMP_PRODUCTS_RELEASES_TASK_NAME)
if (dumpTask != null) {
project.gradle.startParameter.setTaskNames(
project.gradle.startParameter.taskNames.toSet() + dumpTask.path,
)
}
return IntelliJPlatformGradleModelImpl(
dependencyHelperProductCodes = extension.javaClass.loadDependencyHelperProductCodes(),
productReleasesFile = dumpTask?.outputs?.files?.singleFile?.absolutePath,
)
}
override fun reportErrorMessage(modelName: String, project: Project, context: ModelBuilderContext, exception: Exception) {
context.messageReporter.createMessage()
.withGroup(this)
.withKind(Message.Kind.WARNING)
.withTitle("Gradle import errors")
.withText("Unable to build IntelliJ Platform project configuration")
.withException(exception)
.reportMessage(project)
}
private fun Class<*>.loadDependencyHelperProductCodes(): Map<String, String> {
val platformTypeClass = classLoader.loadClass(INTELLIJ_PLATFORM_TYPE_CLASS)
val productCodeGetter = platformTypeClass.getMethod(PRODUCT_CODE_GETTER)
val productCodeByTypeName = platformTypeClass.enumConstants.associate { platformType ->
(platformType as Enum<*>).name.lowercase() to productCodeGetter.invoke(platformType) as String
}
return methods.asSequence()
.filter { method ->
method.genericParameterTypes.any { parameterType ->
parameterType is ParameterizedType &&
parameterType.rawType.typeName == GRADLE_ACTION_CLASS &&
parameterType.actualTypeArguments.any { it.typeName == INTELLIJ_PLATFORM_DEPENDENCY_CONFIGURATION_CLASS }
}
}
.mapNotNull { method ->
productCodeByTypeName[method.name.lowercase()]?.let { method.name to it }
}
.toMap(sortedMapOf())
}
}
@@ -19,8 +19,10 @@ jvm_library(
["resources/**/*"],
),
visibility = ["//visibility:public"],
runtime_deps = ["//plugins/kotlin/plugin"],
deps = [
# do not sort,
"@lib//:kotlin-stdlib",
"//java/idea-ui:ui",
"//platform/core-api:core",
"//platform/core-impl",
@@ -35,9 +37,11 @@ jvm_library(
"//platform/analysis-api:analysis",
"//platform/execution",
"//platform/lang-api:lang",
"//platform/util",
"//plugins/gradle",
"//plugins/gradle:gradle-common",
"//plugins/gradle/java",
"//plugins/gradle/tooling-extension-api:toolingExtension",
"//plugins/devkit/intellij.devkit.gradle.tooling:gradle-tooling",
"//platform/smRunner",
"//plugins/devkit/devkit-core:core",
@@ -51,6 +55,8 @@ jvm_library(
"//platform/backend/workspace",
"//platform/projectModel-impl",
"//jps/model-api:model",
"//plugins/kotlin/base/psi",
"@lib//:kotlinc-kotlin-compiler-common-provided",
],
)
@@ -80,9 +86,11 @@ jvm_library(
"//platform/analysis-api:analysis_test_lib",
"//platform/execution:execution_test_lib",
"//platform/lang-api:lang_test_lib",
"//platform/util:util_test_lib",
"//plugins/gradle:gradle_test_lib",
"//plugins/gradle:gradle-common_test_lib",
"//plugins/gradle/java:java_test_lib",
"//plugins/gradle/tooling-extension-api:toolingExtension_test_lib",
"//plugins/devkit/intellij.devkit.gradle.tooling:gradle-tooling_test_lib",
"//platform/smRunner:smRunner_test_lib",
"//plugins/devkit/devkit-core:core_test_lib",
@@ -100,6 +108,9 @@ jvm_library(
"//platform/backend/workspace:workspace_test_lib",
"//platform/projectModel-impl:projectModel-impl_test_lib",
"//jps/model-api:model_test_lib",
"//plugins/kotlin/plugin:plugin_test_lib",
"//plugins/kotlin/base/psi:psi_test_lib",
"@lib//:kotlinc-kotlin-compiler-common-provided",
],
)
### auto-generated section `build intellij.devkit.gradle` end
@@ -207,6 +218,8 @@ jvm_library(
"//platform/projectModel-impl:projectModel-impl_test_lib",
"//jps/model-api:model",
"//jps/model-api:model_test_lib",
"//plugins/devkit/intellij.devkit.gradle.tooling:gradle-tooling",
"//plugins/devkit/intellij.devkit.gradle.tooling:gradle-tooling_test_lib",
"//libraries/junit4",
"//libraries/junit4:junit4_test_lib",
],
@@ -8,6 +8,7 @@
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="module" module-name="intellij.java.ui" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="intellij.platform.core.impl" />
@@ -22,9 +23,11 @@
<orderEntry type="module" module-name="intellij.platform.analysis" />
<orderEntry type="module" module-name="intellij.platform.execution" />
<orderEntry type="module" module-name="intellij.platform.lang" />
<orderEntry type="module" module-name="intellij.platform.util" />
<orderEntry type="module" module-name="intellij.gradle" />
<orderEntry type="module" module-name="intellij.gradle.common" />
<orderEntry type="module" module-name="intellij.gradle.java" />
<orderEntry type="module" module-name="intellij.gradle.toolingExtension" />
<orderEntry type="module" module-name="intellij.devkit.gradle.tooling" />
<orderEntry type="module" module-name="intellij.platform.smRunner" />
<orderEntry type="module" module-name="intellij.devkit.core" />
@@ -40,5 +43,8 @@
<orderEntry type="module" module-name="intellij.platform.backend.workspace" />
<orderEntry type="module" module-name="intellij.platform.projectModel.impl" />
<orderEntry type="module" module-name="intellij.platform.jps.model" />
<orderEntry type="module" module-name="kotlin.plugin" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.kotlin.base.psi" />
<orderEntry type="library" scope="PROVIDED" name="kotlinc.kotlin-compiler-common" level="project" />
</component>
</module>
@@ -38,6 +38,7 @@
<orderEntry type="module" module-name="intellij.platform.projectModel.impl" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.jps.model" scope="TEST" />
<orderEntry type="module" module-name="intellij.devkit.gradle" scope="TEST" />
<orderEntry type="module" module-name="intellij.devkit.gradle.tooling" scope="TEST" />
<orderEntry type="module" module-name="intellij.libraries.junit4" scope="TEST" />
</component>
<component name="TestModuleProperties" production-module="intellij.devkit.gradle" />
@@ -1,10 +1,12 @@
<idea-plugin>
<!-- region Generated dependencies - run `Generate Product Layouts` to regenerate -->
<dependencies>
<plugin id="org.jetbrains.kotlin"/>
<module name="intellij.devkit.core"/>
<module name="intellij.devkit.gradle.tooling"/>
<module name="intellij.gradle"/>
<module name="intellij.gradle.java"/>
<module name="intellij.gradle.toolingExtension"/>
<module name="intellij.java.psi"/>
<module name="intellij.java.ui"/>
<module name="intellij.platform.smRunner"/>
@@ -19,5 +21,14 @@
<extensions defaultExtensionNs="com.intellij">
<attachSourcesProvider implementation="org.jetbrains.idea.devkit.gradle.IntelliJPlatformAttachSourcesProvider"/>
<consoleFilterProvider implementation="org.jetbrains.idea.devkit.gradle.GradlePluginConsoleFilterProvider"/>
<completion.contributor language="kotlin"
implementationClass="org.jetbrains.idea.devkit.gradle.IntelliJPlatformVersionsCompletionContributor"/>
<projectService serviceInterface="org.jetbrains.idea.devkit.gradle.IntelliJPlatformGradleModelProvider"
serviceImplementation="org.jetbrains.idea.devkit.gradle.IntelliJPlatformGradleModelProviderImpl"/>
</extensions>
</idea-plugin>
<applicationListeners>
<listener class="org.jetbrains.idea.devkit.gradle.IntelliJPlatformGradleSyncListener"
topic="org.jetbrains.plugins.gradle.service.syncAction.GradleSyncListener"/>
</applicationListeners>
</idea-plugin>
@@ -2,9 +2,22 @@
package org.jetbrains.idea.devkit.gradle
import com.intellij.devkit.gradle.tooling.IntelliJPlatformAuxiliaryArtifactProvider
import com.intellij.devkit.gradle.tooling.IntelliJPlatformGradleModel
import com.intellij.gradle.toolingExtension.modelAction.GradleModelFetchPhase
import com.intellij.gradle.toolingExtension.modelProvider.GradleClassProjectModelProvider
import org.jetbrains.plugins.gradle.service.project.AbstractProjectResolverExtension
/**
* Fetches IntelliJ Platform metadata before task execution, allowing the IDE to retain it when later sync stages fail.
*/
internal class DevKitGradleProjectResolverExtension : AbstractProjectResolverExtension() {
override fun getToolingExtensionsClasses(): Set<Class<*>> =
setOf(IntelliJPlatformAuxiliaryArtifactProvider::class.java)
override fun getModelProviders() = listOf(
GradleClassProjectModelProvider(
IntelliJPlatformGradleModel::class.java,
GradleModelFetchPhase.PROJECT_LOADED_PHASE,
)
)
}
@@ -0,0 +1,47 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.idea.devkit.gradle
import com.intellij.devkit.gradle.tooling.IntelliJPlatformGradleModel
import java.nio.file.Path
import kotlin.io.path.isRegularFile
import kotlin.io.path.readLines
/** IDE-side completion metadata imported from [IntelliJPlatformGradleModel]. */
internal data class IntelliJPlatformGradleData(
val dependencyHelperProductCodes: Map<String, String>,
val productReleases: Map<String, List<IntelliJPlatformProductRelease>>,
)
internal data class IntelliJPlatformProductRelease(
val version: String,
val channel: String,
)
internal fun IntelliJPlatformGradleModel.toIntelliJPlatformGradleData() = IntelliJPlatformGradleData(
dependencyHelperProductCodes = dependencyHelperProductCodes,
productReleases = productReleasesFile.readProductReleases(),
)
/** Reads `product-code<TAB>version<TAB>channel` records written by the Gradle task. */
internal fun String?.readProductReleases(): Map<String, List<IntelliJPlatformProductRelease>> {
val file = this?.let { Path.of(it) }?.takeIf { it.isRegularFile() } ?: return emptyMap()
return runCatching {
file.readLines()
.mapNotNull(::parseProductRelease)
.groupBy(
keySelector = { it.first },
valueTransform = { it.second },
)
}.getOrDefault(emptyMap())
}
private fun parseProductRelease(value: String): Pair<String, IntelliJPlatformProductRelease>? {
val fields = value.split('\t')
if (fields.size != 3 || fields.any(String::isBlank)) return null
return fields[0] to IntelliJPlatformProductRelease(
version = fields[1],
channel = fields[2],
)
}
@@ -0,0 +1,58 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.idea.devkit.gradle
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.io.FileUtil
import com.intellij.psi.PsiFile
import java.util.concurrent.atomic.AtomicReference
/** Resolves imported metadata for the closest Gradle module containing a physical PSI file. */
internal fun interface IntelliJPlatformGradleModelProvider {
fun getModel(file: PsiFile): IntelliJPlatformGradleData?
companion object {
fun getInstance(project: Project): IntelliJPlatformGradleModelProvider =
project.getService(IntelliJPlatformGradleModelProvider::class.java)
}
}
/** Publishes per-sync model snapshots without blocking concurrent completion lookups. */
internal class IntelliJPlatformGradleModelProviderImpl : IntelliJPlatformGradleModelProvider {
private val dataByLinkedProjectPath = AtomicReference<Map<String, Map<String, IntelliJPlatformGradleData>>>(emptyMap())
override fun getModel(file: PsiFile): IntelliJPlatformGradleData? {
val filePath = file.virtualFile?.path
if (filePath == null) {
LOG.debug("Cannot find IntelliJ Platform Gradle model for a non-physical file: ${file.name}")
return null
}
val match = dataByLinkedProjectPath.get()
.values
.asSequence()
.flatMap { it.asSequence() }
.filter { (modulePath) -> FileUtil.isAncestor(modulePath, filePath, false) }
.maxByOrNull { (modulePath) -> modulePath.length }
if (LOG.isDebugEnabled) {
LOG.debug("IntelliJ Platform Gradle model lookup for '$filePath': ${match?.key ?: "no matching Gradle module"}")
}
return match?.value
}
internal fun replaceProjectData(
linkedProjectPath: String,
dataByModulePath: Map<String, IntelliJPlatformGradleData>,
) {
dataByLinkedProjectPath.updateAndGet { currentData ->
if (dataByModulePath.isEmpty()) currentData - linkedProjectPath
else currentData + (linkedProjectPath to dataByModulePath.toMap())
}
}
companion object {
private val LOG = Logger.getInstance(IntelliJPlatformGradleModelProviderImpl::class.java)
}
}
@@ -0,0 +1,57 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.idea.devkit.gradle
import com.intellij.devkit.gradle.tooling.IntelliJPlatformGradleModel
import com.intellij.openapi.diagnostic.Logger
import org.jetbrains.plugins.gradle.service.project.ProjectResolverContext
import org.jetbrains.plugins.gradle.service.syncAction.GradleSyncListener
/** Publishes fetched IntelliJ Platform models after both successful and failed Gradle syncs. */
internal class IntelliJPlatformGradleSyncListener : GradleSyncListener {
override fun onModelFetchCompleted(context: ProjectResolverContext) {
importProjectModels(context)
}
override fun onModelFetchFailed(context: ProjectResolverContext, exception: Throwable) {
importProjectModels(context)
}
@Suppress("SSBasedInspection") // Gradle's tooling model exposes the project directory as File.
private fun importProjectModels(context: ProjectResolverContext) {
if (!context.hasModulesWithModel(IntelliJPlatformGradleModel::class.java)) return
val provider = IntelliJPlatformGradleModelProvider.getInstance(context.project) as? IntelliJPlatformGradleModelProviderImpl ?: return
val modelsByModulePath = context.allBuilds
.asSequence()
.flatMap { it.projects.asSequence() }
.mapNotNull { gradleProject ->
context.getProjectModel(gradleProject, IntelliJPlatformGradleModel::class.java)?.let {
gradleProject.projectDirectory.toPath().toString() to it
}
}
.toMap()
val importedModels = provider.importProjectModels(context.externalProjectPath, modelsByModulePath)
if (importedModels > 0) {
LOG.debug("Imported IntelliJ Platform Gradle data for $importedModels module(s)")
}
}
companion object {
private val LOG = Logger.getInstance(IntelliJPlatformGradleSyncListener::class.java)
}
}
/** Converts fetched tooling models and publishes modules with a usable product-release catalog. */
internal fun IntelliJPlatformGradleModelProviderImpl.importProjectModels(
linkedProjectPath: String,
modelsByModulePath: Map<String, IntelliJPlatformGradleModel>,
): Int {
val dataByModulePath = modelsByModulePath
.mapValues { (_, model) -> model.toIntelliJPlatformGradleData() }
.filterValues { it.productReleases.isNotEmpty() }
if (dataByModulePath.isNotEmpty()) {
replaceProjectData(linkedProjectPath, dataByModulePath)
}
return dataByModulePath.size
}
@@ -0,0 +1,135 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.idea.devkit.gradle
import com.intellij.codeInsight.completion.CompletionContributor
import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionProvider
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.completion.CompletionType
import com.intellij.codeInsight.completion.PrioritizedLookupElement
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.devkit.core.icons.DevkitCoreIcons
import com.intellij.openapi.util.text.StringUtil
import com.intellij.patterns.PatternCondition
import com.intellij.patterns.PlatformPatterns
import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.util.ProcessingContext
import org.jetbrains.idea.devkit.projectRoots.IntelliJPlatformProduct
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtBinaryExpression
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.KtStringTemplateExpression
import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
import org.jetbrains.plugins.gradle.util.GradleConstants
private const val DEPENDENCIES_BLOCK = "dependencies"
private const val INTELLIJ_PLATFORM_BLOCK = "intellijPlatform"
private const val CREATE_HELPER = "create"
private const val TYPE_PARAMETER = "type"
private const val VERSION_PARAMETER = "version"
private const val COMPLETION_PRIORITY = 100.0
/** Completes IntelliJ Platform dependency versions from metadata imported during Gradle sync. */
internal class IntelliJPlatformVersionsCompletionContributor : CompletionContributor() {
init {
val place = PlatformPatterns.psiElement()
.withParent(KtLiteralStringTemplateEntry::class.java)
.with(object : PatternCondition<PsiElement>("intellijPlatformVersionLiteral") {
override fun accepts(element: PsiElement, context: ProcessingContext?): Boolean {
if (!element.containingFile.name.endsWith(GradleConstants.KOTLIN_DSL_SCRIPT_EXTENSION)) return false
val callExpression = element.getParentOfType<KtCallExpression>(true) ?: return false
val intellijPlatformClosure = callExpression.getParentOfType<KtCallExpression>(true)
?.takeIf { it.calleeExpression?.text == INTELLIJ_PLATFORM_BLOCK }
?: return false
intellijPlatformClosure.getParentOfType<KtCallExpression>(true)
?.takeIf { it.calleeExpression?.text == DEPENDENCIES_BLOCK }
?: return false
return callExpression.isVersionArgument(element) ||
(callExpression.containsInLambda(element) && element.isVersionAssignmentValue())
}
})
extend(CompletionType.BASIC, place, IntelliJPlatformVersionsCompletionProvider())
}
private fun KtCallExpression.isVersionArgument(element: PsiElement): Boolean {
val argument = valueArguments.firstOrNull { PsiTreeUtil.isAncestor(it, element, false) } ?: return false
val argumentName = argument.getArgumentName()?.asName?.asString()
if (argumentName != null) return argumentName == VERSION_PARAMETER
val versionArgumentIndex = if (calleeExpression?.text == CREATE_HELPER) 1 else 0
return valueArguments.indexOf(argument) == versionArgumentIndex
}
private fun KtCallExpression.containsInLambda(element: PsiElement): Boolean {
return lambdaArguments.any { PsiTreeUtil.isAncestor(it, element, false) }
}
private fun PsiElement.isVersionAssignmentValue(): Boolean {
val assignment = getParentOfType<KtBinaryExpression>(strict = false) ?: return false
val property = assignment.left as? KtNameReferenceExpression ?: return false
val value = assignment.right ?: return false
return assignment.operationToken == KtTokens.EQ &&
property.getReferencedName() == VERSION_PARAMETER &&
PsiTreeUtil.isAncestor(value, this, false)
}
private class IntelliJPlatformVersionsCompletionProvider : CompletionProvider<CompletionParameters>() {
override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
val callExpression = parameters.position.getParentOfType<KtCallExpression>(true) ?: return
val dependencyHelper = callExpression.calleeExpression?.text ?: return
val gradleModel = IntelliJPlatformGradleModelProvider.getInstance(parameters.position.project).getModel(parameters.originalFile) ?: return
val productCode = when (dependencyHelper) {
CREATE_HELPER -> callExpression.getCreateProductCode()
else -> gradleModel.dependencyHelperProductCodes[dependencyHelper]
} ?: return
val productName = IntelliJPlatformProduct.fromProductCode(productCode)?.getName() ?: "IntelliJ Platform"
val releases = gradleModel.productReleases[productCode]
.orEmpty()
.asSequence()
.distinct()
.sortedWith { first, second ->
StringUtil.compareVersionNumbers(second.version, first.version)
.takeIf { it != 0 }
?: compareValues(first.channel, second.channel)
}
.toList()
releases.forEachIndexed { index, release ->
val channel = release.channel.toPresentableChannel()
val lookupElement = LookupElementBuilder.create(release.version)
.withIcon(DevkitCoreIcons.Sdk_closed)
.withTailText(" $channel", true)
.withTypeText("$productName ($productCode)", true)
result.addElement(PrioritizedLookupElement.withPriority(lookupElement, COMPLETION_PRIORITY + releases.size - index))
}
}
private fun KtCallExpression.getCreateProductCode(): String? {
val typeArgument = valueArguments.firstOrNull {
it.getArgumentName()?.asName?.asString() == TYPE_PARAMETER
} ?: valueArguments.firstOrNull { it.getArgumentName() == null }
val typeExpression = typeArgument?.getArgumentExpression() as? KtStringTemplateExpression ?: return null
return (typeExpression.entries.singleOrNull() as? KtLiteralStringTemplateEntry)?.text
}
private fun String.toPresentableChannel(): String {
return when (this) {
"EAP", "RC" -> this
else -> lowercase().replaceFirstChar(Char::titlecase)
}
}
}
}
@@ -0,0 +1,64 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.idea.devkit.gradle
import com.intellij.devkit.gradle.tooling.IntelliJPlatformGradleModel
import com.intellij.gradle.toolingExtension.modelAction.GradleModelFetchPhase
import junit.framework.TestCase
import org.jetbrains.plugins.gradle.service.project.ProjectResolverContext
import java.lang.reflect.Proxy
import java.nio.file.Files
internal class DevKitGradleProjectResolverExtensionTest : TestCase() {
fun testModelIsFetchedBeforeTaskExecution() {
val modelProvider = DevKitGradleProjectResolverExtension().modelProviders.single()
assertEquals(GradleModelFetchPhase.PROJECT_LOADED_PHASE, modelProvider.phase)
}
fun testFailedFetchWithoutIntelliJPlatformModelIsIgnored() {
val context = Proxy.newProxyInstance(
javaClass.classLoader,
arrayOf(ProjectResolverContext::class.java),
) { _, method, arguments ->
when (method.name) {
"hasModulesWithModel" -> {
assertSame(IntelliJPlatformGradleModel::class.java, arguments.single())
false
}
else -> error("Unexpected ProjectResolverContext call: ${method.name}")
}
} as ProjectResolverContext
IntelliJPlatformGradleSyncListener().onModelFetchFailed(context, RuntimeException("Expected sync failure"))
}
fun testReadsProductReleaseCatalog() {
val file = Files.createTempFile("product-releases", ".txt")
try {
Files.writeString(
file,
"""
IU${'\t'}2025.2.6${'\t'}RELEASE
IU${'\t'}253.123${'\t'}EAP
IC-2024.3
IC${'\t'}2024.3
IC${'\t'}2024.3${'\t'}RELEASE${'\t'}unexpected
""".trimIndent(),
)
assertEquals(
mapOf(
"IU" to listOf(
IntelliJPlatformProductRelease("2025.2.6", "RELEASE"),
IntelliJPlatformProductRelease("253.123", "EAP"),
),
),
file.toString().readProductReleases(),
)
}
finally {
Files.deleteIfExists(file)
}
}
}
@@ -0,0 +1,79 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.idea.devkit.gradle
import com.intellij.devkit.gradle.tooling.IntelliJPlatformGradleModel
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase
import com.intellij.testFramework.replaceService
import java.nio.file.Files
internal class IntelliJPlatformGradleModelProviderTest : LightJavaCodeInsightFixtureTestCase() {
private lateinit var provider: IntelliJPlatformGradleModelProviderImpl
override fun setUp() {
super.setUp()
provider = IntelliJPlatformGradleModelProviderImpl()
project.replaceService(IntelliJPlatformGradleModelProvider::class.java, provider, testRootDisposable)
}
fun testImportsDataForModule() {
val file = myFixture.addFileToProject("project/build.gradle.kts", "")
val projectPath = file.virtualFile.parent.path
val data = gradleData("2026.1")
updateProjectData(projectPath, projectPath to data)
assertSame(data, provider.getModel(file))
}
fun testUsesClosestModulePath() {
val file = myFixture.addFileToProject("project/nested/build.gradle.kts", "")
val nestedProjectPath = file.virtualFile.parent.path
val projectPath = file.virtualFile.parent.parent.path
val rootData = gradleData("2025.3")
val nestedData = gradleData("2026.1")
updateProjectData(projectPath, projectPath to rootData, nestedProjectPath to nestedData)
assertSame(nestedData, provider.getModel(file))
}
fun testEmptyImportClearsLinkedProjectData() {
val file = myFixture.addFileToProject("project/build.gradle.kts", "")
val projectPath = file.virtualFile.parent.path
updateProjectData(projectPath, projectPath to gradleData("2026.1"))
updateProjectData(projectPath)
assertNull(provider.getModel(file))
}
fun testImportsDataProducedByModelFetch() {
val file = myFixture.addFileToProject("project/build.gradle.kts", "")
val projectPath = file.virtualFile.parent.path
val releasesFile = Files.createTempFile("product-releases", ".txt")
try {
Files.writeString(releasesFile, "IU\t2026.1\tRELEASE\n")
val model = object : IntelliJPlatformGradleModel {
override val dependencyHelperProductCodes = mapOf("intellijIdea" to "IU")
override val productReleasesFile = releasesFile.toString()
}
assertEquals(1, provider.importProjectModels(projectPath, mapOf(projectPath to model)))
assertEquals(gradleData("2026.1"), provider.getModel(file))
}
finally {
Files.deleteIfExists(releasesFile)
}
}
private fun updateProjectData(projectPath: String, vararg moduleData: Pair<String, IntelliJPlatformGradleData>) {
provider.replaceProjectData(projectPath, moduleData.toMap())
}
private fun gradleData(version: String) = IntelliJPlatformGradleData(
dependencyHelperProductCodes = mapOf("intellijIdea" to "IU"),
productReleases = mapOf("IU" to listOf(IntelliJPlatformProductRelease(version, "RELEASE"))),
)
}
@@ -0,0 +1,251 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.idea.devkit.gradle
import com.intellij.codeInsight.CodeInsightSettings
import com.intellij.codeInsight.completion.CompletionContributor
import com.intellij.codeInsight.completion.CompletionContributorEP
import com.intellij.codeInsight.completion.PrioritizedLookupElement
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.lookup.LookupElementPresentation
import com.intellij.devkit.core.icons.DevkitCoreIcons
import com.intellij.openapi.extensions.DefaultPluginDescriptor
import com.intellij.psi.PsiFile
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase
import com.intellij.testFramework.registerExtension
import com.intellij.testFramework.replaceService
import com.intellij.util.application
internal class IntelliJPlatformVersionsCompletionContributorTest : LightJavaCodeInsightFixtureTestCase() {
private var gradleData = IntelliJPlatformGradleData(
dependencyHelperProductCodes = mapOf(
"intellijIdea" to "IU",
"intellijIdeaCommunity" to "IC",
),
productReleases = mapOf(
"IC" to listOf(release("2023.1"), release("2023.2")),
"IU" to listOf(release("2023.2"), release("2024.1")),
),
)
private var modelRequestFile: PsiFile? = null
override fun setUp() {
super.setUp()
project.replaceService(
IntelliJPlatformGradleModelProvider::class.java,
IntelliJPlatformGradleModelProvider { file ->
modelRequestFile = file
gradleData
},
testRootDisposable,
)
val contributor = CompletionContributorEP(
"kotlin",
IntelliJPlatformVersionsCompletionContributor::class.java.name,
DefaultPluginDescriptor("testIntelliJPlatformVersionsCompletion"),
)
application.registerExtension(CompletionContributor.EP, contributor, testRootDisposable)
}
fun testSuggestsPlatformVersionInKotlinStringLiteral() {
myFixture.configureByText(
"build.gradle.kts",
"""
dependencies {
intellijPlatform {
intellijIdea("<caret>")
}
}
""".trimIndent(),
)
assertPlatformVersions("2023.2", "2024.1")
assertSame(myFixture.file, modelRequestFile)
}
fun testSuggestsOnlyVersionsForDependencyHelperProductType() {
myFixture.configureByText(
"build.gradle.kts",
"""
dependencies {
intellijPlatform {
intellijIdeaCommunity("<caret>")
}
}
""".trimIndent(),
)
assertPlatformVersions("2023.1", "2023.2")
}
fun testRendersProductDetailsAndPrioritizesDottedVersionsNewestFirst() {
val versions = listOf("2025.2.6", "2025.2.6.3", "2025.2.6.2", "2025.2.6.1")
gradleData = gradleData.copy(
productReleases = mapOf(
"IU" to versions.map { version ->
release(version, channel = if (version == "2025.2.6.2") "EAP" else "RELEASE")
},
),
)
myFixture.configureByText(
"build.gradle.kts",
"""
dependencies {
intellijPlatform {
intellijIdea("<caret>")
}
}
""".trimIndent(),
)
val elements = completeBasicElements()
assertEquals(
mapOf(
"2025.2.6.3" to 104.0,
"2025.2.6.2" to 103.0,
"2025.2.6.1" to 102.0,
"2025.2.6" to 101.0,
),
elements.filter { it.lookupString in versions }.associate {
it.lookupString to it.`as`(PrioritizedLookupElement.CLASS_CONDITION_KEY)?.priority
},
)
val presentation = LookupElementPresentation()
elements.first { it.lookupString == "2025.2.6.3" }.renderElement(presentation)
assertSame(DevkitCoreIcons.Sdk_closed, presentation.icon)
assertEquals(" Release", presentation.tailText)
assertEquals("IntelliJ IDEA (IU)", presentation.typeText)
val eapPresentation = LookupElementPresentation()
elements.first { it.lookupString == "2025.2.6.2" }.renderElement(eapPresentation)
assertEquals(" EAP", eapPresentation.tailText)
}
fun testSuggestsPlatformVersionForCreateVersionArgument() {
myFixture.configureByText(
"build.gradle.kts",
"""
dependencies {
intellijPlatform {
create("IU", "<caret>")
}
}
""".trimIndent(),
)
assertPlatformVersions("2023.2", "2024.1")
}
fun testSuggestsPlatformVersionForCreateNamedTypeArgument() {
myFixture.configureByText(
"build.gradle.kts",
"""
dependencies {
intellijPlatform {
create(version = "<caret>", type = "IC")
}
}
""".trimIndent(),
)
assertPlatformVersions("2023.1", "2023.2")
}
fun testDoesNotSuggestPlatformVersionForCreateTypeArgument() {
myFixture.configureByText(
"build.gradle.kts",
"""
dependencies {
intellijPlatform {
create("<caret>", "2026.1")
}
}
""".trimIndent(),
)
assertNoPlatformVersions()
}
fun testDoesNotSuggestPlatformVersionForUnsupportedDependencyHelper() {
myFixture.configureByText(
"build.gradle.kts",
"""
dependencies {
intellijPlatform {
plugin("<caret>")
}
}
""".trimIndent(),
)
assertNoPlatformVersions()
}
fun testDoesNotSuggestPlatformVersionWithoutImportedProductReleases() {
gradleData = gradleData.copy(productReleases = emptyMap())
myFixture.configureByText(
"build.gradle.kts",
"""
dependencies {
intellijPlatform {
intellijIdea("<caret>")
}
}
""".trimIndent(),
)
assertNoPlatformVersions()
}
fun testDoesNotSuggestPlatformVersionOutsideKotlinStringLiteral() {
myFixture.configureByText("build.gradle.kts", "val <caret>")
assertNoPlatformVersions()
}
fun testDoesNotSuggestPlatformVersionOutsideGradleKotlinScript() {
myFixture.configureByText(
"Example.kt",
"""
dependencies {
intellijPlatform {
intellijIdea("<caret>")
}
}
""".trimIndent(),
)
assertNoPlatformVersions()
}
private fun completeBasic(): List<String> {
return completeBasicElements().map { it.lookupString }
}
private fun completeBasicElements(): List<LookupElement> {
val settings = CodeInsightSettings.getInstance()
val autoComplete = settings.AUTOCOMPLETE_ON_CODE_COMPLETION
settings.AUTOCOMPLETE_ON_CODE_COMPLETION = false
return try {
myFixture.completeBasic().orEmpty().toList()
}
finally {
settings.AUTOCOMPLETE_ON_CODE_COMPLETION = autoComplete
}
}
private fun assertNoPlatformVersions() {
assertDoesntContain(completeBasic(), *PLATFORM_VERSIONS.toTypedArray())
}
private fun assertPlatformVersions(vararg expected: String) {
assertEquals(expected.toSet(), completeBasic().filter { it in PLATFORM_VERSIONS }.toSet())
}
companion object {
private val PLATFORM_VERSIONS = listOf("2023.1", "2023.2", "2024.1")
private fun release(version: String, channel: String = "RELEASE") = IntelliJPlatformProductRelease(version, channel)
}
}