[jdk] Introduce ExternalJavaConfigurationService and migrate SdkmanrcWatcher

#IDEA-355295

GitOrigin-RevId: bbcefd977b5d88e3a8b0d137b1b5cfda6eb3050a
This commit is contained in:
Louis Vignier
2024-07-18 11:41:17 +02:00
committed by intellij-monorepo-bot
parent 4b6a424f1c
commit 7ccd3af88d
11 changed files with 419 additions and 309 deletions

View File

@@ -363,6 +363,9 @@
<extensionPoint qualifiedName="com.intellij.execution.applicationRunLineMarkerHider"
interface="com.intellij.execution.ApplicationRunLineMarkerHider" dynamic="true"/>
<extensionPoint qualifiedName="com.intellij.jvm.logging" interface="com.intellij.lang.logging.JvmLogger" dynamic="true"/>
<extensionPoint qualifiedName="com.intellij.openapi.projectRoots.externalJavaConfigurationProvider"
interface="com.intellij.openapi.projectRoots.impl.ExternalJavaConfigurationProvider" dynamic="true"/>
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">
@@ -537,7 +540,9 @@
<updateSettingsUIProvider implementation="com.intellij.openapi.projectRoots.impl.JdkUpdaterConfigurable"/>
<postStartupActivity implementation="com.intellij.openapi.projectRoots.impl.ExistingJdkConfigurationActivity"/>
<postStartupActivity implementation="com.intellij.openapi.projectRoots.impl.SdkmanrcWatcher"/>
<openapi.projectRoots.externalJavaConfigurationProvider implementation="com.intellij.openapi.projectRoots.impl.SdkmanrcConfigurationProvider"/>
<postStartupActivity implementation="com.intellij.openapi.projectRoots.impl.ExternalJavaConfigurationActivity"/>
<registryKey key="jdk.configure.existing" defaultValue="false" description="Attempt to add an existing SDK to the SDK table."/>
<applicationService serviceInterface="com.intellij.execution.runners.ProcessProxyFactory"
@@ -2751,7 +2756,7 @@
key="notification.group.preview.features"/>
<notificationGroup id="Remove redundant exports/opens" displayType="BALLOON" bundle="messages.JavaBundle"
key="notification.group.redundant.exports"/>
<notificationGroup id="Setup SDK" displayType="BALLOON" bundle="messages.JavaBundle" key="notification.group.setup.sdk"/>
<notificationGroup id="Setup JDK" displayType="BALLOON" bundle="messages.JavaBundle" key="notification.group.setup.jdk"/>
<notificationGroup id="External annotations" displayType="BALLOON" bundle="messages.JavaBundle"
key="notification.group.setup.external.annotations"/>
<notificationGroup id="Test integration" displayType="BALLOON" bundle="messages.JavaBundle" key="notification.group.testintegration"/>

View File

@@ -75,7 +75,7 @@ public abstract class JavaCreateTemplateInPackageAction<T extends PsiElement> ex
public void run(@NotNull ProgressIndicator indicator) {
SdkLookupUtil.lookupAndSetupSdkBlocking(project, indicator, JavaSdk.getInstance(), sdk -> {
JavaSdkUtil.applyJdkToProject(project, sdk);
Notifications.Bus.notify(new Notification("Setup SDK", JavaBundle.message("notification.content.was.set.up", sdk.getVersionString()), NotificationType.INFORMATION).addAction(
Notifications.Bus.notify(new Notification("Setup JDK", JavaBundle.message("notification.content.was.set.up", sdk.getVersionString()), NotificationType.INFORMATION).addAction(
new NotificationAction(JavaBundle.message("notification.content.change.jdk")) {
@Override
public void actionPerformed(@NotNull AnActionEvent e,

View File

@@ -0,0 +1,61 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.projectRoots.impl
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.service
import com.intellij.openapi.extensions.ExtensionNotApplicableException
import com.intellij.openapi.extensions.ExtensionPointListener
import com.intellij.openapi.extensions.PluginDescriptor
import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.ProjectActivity
import com.intellij.openapi.util.Disposer
import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.seconds
/**
* Configures the project JDK according to configuration files from external config tools.
*/
class ExternalJavaConfigurationActivity : ProjectActivity {
private val disposableMap = mutableMapOf<Class<*>, Disposable>()
init {
if (ApplicationManager.getApplication().isUnitTestMode) {
throw ExtensionNotApplicableException.create()
}
}
override suspend fun execute(project: Project) {
delay(5.seconds)
val configWatcherService = project.service<ExternalJavaConfigurationService>()
ExternalJavaConfigurationProvider.EP_NAME.addExtensionPointListener(object : ExtensionPointListener<ExternalJavaConfigurationProvider<*>> {
override fun extensionAdded(extension: ExternalJavaConfigurationProvider<*>, pluginDescriptor: PluginDescriptor) {
setupExtension(project, extension)
}
override fun extensionRemoved(extension: ExternalJavaConfigurationProvider<*>, pluginDescriptor: PluginDescriptor) {
val clazz = extension::class.java
disposableMap[clazz]?.let { Disposer.dispose(it) }
disposableMap.remove(extension::class.java)
}
}, configWatcherService)
for (configProvider in ExternalJavaConfigurationProvider.EP_NAME.extensionList) {
val file = configProvider.getConfigurationFile(project)
setupExtension(project, configProvider)
if (file.exists()) configWatcherService.updateJdkFromConfig(configProvider)
}
}
private fun setupExtension(project: Project, extension: ExternalJavaConfigurationProvider<*>) {
val key = extension::class.java
if (disposableMap.containsKey(key)) return
val configWatcherService = project.service<ExternalJavaConfigurationService>()
val disposable = Disposer.newDisposable(configWatcherService)
configWatcherService.registerListener(disposable, extension)
disposableMap[extension::class.java] = disposable
}
}

View File

@@ -0,0 +1,41 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.projectRoots.impl
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import java.io.File
/**
* This extension point makes it possible to select which JDK should be configured for the project,
* based on the configuration file of an external SDK manager.
*
* @param T Release data representation for the configuration provider.
*/
interface ExternalJavaConfigurationProvider<T> {
companion object {
val EP_NAME: ExtensionPointName<ExternalJavaConfigurationProvider<*>> = ExtensionPointName.create("com.intellij.openapi.projectRoots.externalJavaConfigurationProvider")
}
/**
* Returns the configuration file supported by this provider.
*/
fun getConfigurationFile(project: Project): File
/**
* Returns the release data [T] corresponding to the [text] content of the configuration file.
*/
fun getReleaseData(text: String): T?
/**
* Returns true if the release data matches the given SDK.
*/
fun matchAgainstSdk(releaseData: T, sdk: Sdk): Boolean
/**
* Returns true if the release data matches the given path.
*/
fun matchAgainstPath(releaseData: T, path: String): Boolean
}

View File

@@ -0,0 +1,141 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.projectRoots.impl
import com.intellij.java.JavaBundle
import com.intellij.notification.NotificationGroupManager
import com.intellij.notification.NotificationType
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.readAction
import com.intellij.openapi.application.writeAction
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.JdkFinder
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.vfs.newvfs.BulkFileListener
import com.intellij.openapi.vfs.newvfs.events.VFileContentChangeEvent
import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
private val LOG = logger<ExternalJavaConfigurationService>()
/**
* The JDK needed for a project can be described in a configuration file written by an external tool.
* This service is used to find a matching JDK candidate for a given release data.
*
* @see ExternalJavaConfigurationProvider
*/
@Service(Service.Level.PROJECT)
class ExternalJavaConfigurationService(val project: Project, private val scope: CoroutineScope) : Disposable {
sealed class JdkCandidate<T> {
data class Jdk<T>(val releaseData: T, val jdk: Sdk, val project: Boolean) : JdkCandidate<T>()
data class Path<T>(val releaseData: T, val path: String) : JdkCandidate<T>()
}
internal fun <T> registerListener(disposable: Disposable, configProvider: ExternalJavaConfigurationProvider<T>) {
project.messageBus.connect(disposable).subscribe(VirtualFileManager.VFS_CHANGES, object : BulkFileListener {
override fun after(events: MutableList<out VFileEvent>) {
for (event in events) {
if (!event.path.endsWith(configProvider.getConfigurationFile(project).name)) continue
if (event !is VFileContentChangeEvent && event !is VFileCreateEvent) continue
updateJdkFromConfig(configProvider)
}
}
})
}
/**
* Updates the project JDK according to the configuration file of [configProvider].
*/
fun <T> updateJdkFromConfig(configProvider: ExternalJavaConfigurationProvider<T>) {
scope.launch {
val releaseData: T = getReleaseData(configProvider) ?: return@launch
val file = configProvider.getConfigurationFile(project)
val suggestion = findCandidate(releaseData, configProvider)
when (suggestion) {
is JdkCandidate.Jdk -> if (!suggestion.project) configure(suggestion.jdk, file.name, releaseData)
is JdkCandidate.Path -> service<AddJdkService>().createJdkFromPath(suggestion.path) {
configure(it, file.name, releaseData)
}
else -> null
}
}
}
/**
* Parses the configuration file and returns a candidate.
*/
suspend fun <T> getReleaseData(configProvider: ExternalJavaConfigurationProvider<T>): T? {
val text = readAction {
val virtualFile = VirtualFileManager.getInstance().findFileByNioPath(configProvider.getConfigurationFile(project).toPath().toAbsolutePath())
virtualFile?.let { FileDocumentManager.getInstance().getDocument(it)?.text }
} ?: return null
return configProvider.getReleaseData(text)
}
/**
* Finds a matching JDK candidate for the release data among registered and detected JDKs.
*/
fun <T> findCandidate(releaseData: T, configProvider: ExternalJavaConfigurationProvider<T>): JdkCandidate<T>? {
val fileName = configProvider.getConfigurationFile(project).name
// Match against the project SDK
val projectSdk = ProjectRootManager.getInstance(project).projectSdk
if (projectSdk != null && configProvider.matchAgainstSdk(releaseData, projectSdk)) {
return JdkCandidate.Jdk(releaseData, projectSdk, true)
} else {
LOG.info("[$fileName] $releaseData - Project JDK doesn't match (${projectSdk?.versionString})")
}
// Match against the project JDK table
val jdks = ProjectJdkTable.getInstance().allJdks
for (jdk in jdks) {
if (configProvider.matchAgainstSdk(releaseData, jdk)) {
LOG.info("[$fileName] $releaseData - Candidate found: ${jdk.versionString}")
return JdkCandidate.Jdk(releaseData, jdk, false)
}
}
// Match against JdkFinder
JdkFinder.getInstance().suggestHomePaths().forEach { path ->
if (configProvider.matchAgainstPath(releaseData, path)) {
LOG.info("[$fileName] $releaseData - Candidate found to register")
return JdkCandidate.Path(releaseData, path)
}
}
return null
}
private fun <T> configure(jdk: Sdk, fileName: String, candidate: T) {
scope.launch(Dispatchers.EDT) {
val rootManager = ProjectRootManager.getInstance(project)
writeAction { rootManager.projectSdk = jdk }
LOG.info("[$fileName] $candidate - JDK registered: ${jdk.versionString}")
NotificationGroupManager.getInstance()
.getNotificationGroup("Setup JDK")
.createNotification(
JavaBundle.message("sdk.configured.external.config.title", fileName),
JavaBundle.message("sdk.configured", jdk.versionString),
NotificationType.INFORMATION
)
.notify(project)
}
}
override fun dispose() {}
}

View File

@@ -18,14 +18,21 @@ internal class ParseSdkmanrcAction: AnAction() {
return
}
val watcher = project.service<SdkmanrcWatcherService>()
val sdkmanrcConfigProvider = ExternalJavaConfigurationProvider.EP_NAME.extensionList.find { it is SdkmanrcConfigurationProvider }
if (sdkmanrcConfigProvider == null) {
e.presentation.isEnabledAndVisible = false
return
}
val file: VirtualFile? = CommonDataKeys.VIRTUAL_FILE.getData(e.dataContext)
e.presentation.isEnabledAndVisible = file != null && file.path == watcher.file.absolutePath
e.presentation.isEnabledAndVisible = file != null && file.path == sdkmanrcConfigProvider.getConfigurationFile(project).absolutePath
}
override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
project.service<SdkmanrcWatcherService>().configureSdkFromSdkmanrc()
val sdkmanrcConfigProvider = ExternalJavaConfigurationProvider.EP_NAME.extensionList.find { it is SdkmanrcConfigurationProvider } ?: return
project.service<ExternalJavaConfigurationService>().updateJdkFromConfig(sdkmanrcConfigProvider)
}
}

View File

@@ -0,0 +1,79 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.projectRoots.impl
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.NlsSafe
import org.jetbrains.jps.model.java.JdkVersionDetector
import java.io.File
import java.util.Properties
private val LOG = logger<SdkmanrcConfigurationProvider>()
data class SdkmanReleaseData(val target: String,
val version: String,
val flavour: String? = null,
val vendor: String? = null) {
companion object {
private val regex: Regex = Regex("(\\d+(?:\\.\\d+)*)(?:\\.([^-]+))?-?(.*)?")
fun parse(text: String): SdkmanReleaseData? {
val matchResult = regex.find(text) ?: return null
return SdkmanReleaseData(
text,
matchResult.groups[1]?.value ?: return null,
matchResult.groups[2]?.value,
matchResult.groups[3]?.value
)
}
}
fun matchVersionString(versionString: @NlsSafe String): Boolean {
LOG.info("Matching '$versionString'")
if (version !in versionString) return false
val variant = when {
vendor == "adpt" && flavour == "hs" -> JdkVersionDetector.Variant.AdoptOpenJdk_HS
vendor == "adpt" && flavour == "j9" -> JdkVersionDetector.Variant.AdoptOpenJdk_J9
vendor == "amzn" -> JdkVersionDetector.Variant.Corretto
vendor == "graal" -> JdkVersionDetector.Variant.GraalVM
vendor == "graalce" -> JdkVersionDetector.Variant.GraalVMCE
vendor == "jbr" -> JdkVersionDetector.Variant.JBR
vendor == "librca" -> JdkVersionDetector.Variant.Liberica
vendor == "oracle" -> JdkVersionDetector.Variant.Oracle
vendor == "open" -> JdkVersionDetector.Variant.Oracle
vendor == "sapmchn" -> JdkVersionDetector.Variant.SapMachine
vendor == "sem" -> JdkVersionDetector.Variant.Semeru
vendor == "tem" -> JdkVersionDetector.Variant.Temurin
vendor == "zulu" -> JdkVersionDetector.Variant.Zulu
else -> JdkVersionDetector.Variant.Unknown
}
// Check vendor
val variantName = variant.displayName
return variantName != null && versionString.contains(variantName)
}
}
class SdkmanrcConfigurationProvider: ExternalJavaConfigurationProvider<SdkmanReleaseData> {
override fun getConfigurationFile(project: Project): File = File(project.basePath, ".sdkmanrc")
override fun getReleaseData(text: String): SdkmanReleaseData? {
val properties = Properties().apply {
load(text.byteInputStream())
}
return SdkmanReleaseData.parse(properties.getProperty("java"))
}
override fun matchAgainstSdk(releaseData: SdkmanReleaseData, sdk: Sdk): Boolean {
val versionString = sdk.versionString ?: return false
return releaseData.matchVersionString(versionString)
}
override fun matchAgainstPath(releaseData: SdkmanReleaseData, path: String): Boolean {
val info = SdkVersionUtil.getJdkVersionInfo(path) ?: return false
return releaseData.matchVersionString(info.displayVersionString())
}
}

View File

@@ -1,240 +0,0 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.projectRoots.impl
import com.intellij.java.JavaBundle
import com.intellij.notification.NotificationGroupManager
import com.intellij.notification.NotificationType
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.readAction
import com.intellij.openapi.application.writeAction
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.serviceAsync
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.extensions.ExtensionNotApplicableException
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.options.advanced.AdvancedSettings
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.JavaSdk
import com.intellij.openapi.projectRoots.JdkFinder
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.startup.ProjectActivity
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.vfs.newvfs.BulkFileListener
import com.intellij.openapi.vfs.newvfs.events.VFileContentChangeEvent
import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.intellij.util.concurrency.annotations.RequiresEdt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.jps.model.java.JdkVersionDetector
import java.io.File
import java.util.*
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
private val LOG = logger<SdkmanrcWatcher>()
data class SdkmanCandidate(val target: String,
val version: String,
val flavour: String? = null,
val vendor: String? = null) {
companion object {
private val regex: Regex = Regex("(\\d+(?:\\.\\d+)*)(?:\\.([^-]+))?-?(.*)?")
fun parse(text: String): SdkmanCandidate? {
val matchResult = regex.find(text) ?: return null
return SdkmanCandidate(
text,
matchResult.groups[1]?.value ?: return null,
matchResult.groups[2]?.value,
matchResult.groups[3]?.value
)
}
}
fun matchVersionString(versionString: @NlsSafe String): Boolean {
LOG.info("Matching '$versionString'")
if (version !in versionString) return false
val variant = when {
vendor == "adpt" && flavour == "hs" -> JdkVersionDetector.Variant.AdoptOpenJdk_HS
vendor == "adpt" && flavour == "j9" -> JdkVersionDetector.Variant.AdoptOpenJdk_J9
vendor == "amzn" -> JdkVersionDetector.Variant.Corretto
vendor == "graal" -> JdkVersionDetector.Variant.GraalVM
vendor == "graalce" -> JdkVersionDetector.Variant.GraalVMCE
vendor == "jbr" -> JdkVersionDetector.Variant.JBR
vendor == "librca" -> JdkVersionDetector.Variant.Liberica
vendor == "oracle" -> JdkVersionDetector.Variant.Oracle
vendor == "open" -> JdkVersionDetector.Variant.Oracle
vendor == "sapmchn" -> JdkVersionDetector.Variant.SapMachine
vendor == "sem" -> JdkVersionDetector.Variant.Semeru
vendor == "tem" -> JdkVersionDetector.Variant.Temurin
vendor == "zulu" -> JdkVersionDetector.Variant.Zulu
else -> JdkVersionDetector.Variant.Unknown
}
// Check vendor
val variantName = variant.displayName
return variantName != null && versionString.contains(variantName)
}
}
@Service(Service.Level.PROJECT)
class SdkmanrcWatcherService(private val project: Project, private val scope: CoroutineScope): Disposable {
val file: File = File(project.basePath, ".sdkmanrc")
fun registerListener(project: Project) {
project.messageBus.connect(this).subscribe(VirtualFileManager.VFS_CHANGES, object : BulkFileListener {
override fun after(events: MutableList<out VFileEvent>) {
for (event in events) {
when {
!event.path.endsWith(".sdkmanrc") -> {}
event is VFileContentChangeEvent || event is VFileCreateEvent -> configureSdkFromSdkmanrc()
}
}
}
})
}
sealed class SdkSuggestion {
data class Jdk(val target: SdkmanCandidate, val jdk: Sdk, val project: Boolean) : SdkSuggestion()
data class Path(val target: SdkmanCandidate, val path: String) : SdkSuggestion()
}
fun configureSdkFromSdkmanrc() {
scope.launch {
val result = suggestSdkFromSdkmanrc()
withContext(Dispatchers.EDT) {
when {
result is SdkSuggestion.Jdk && !result.project -> configure(result.jdk, result.target)
result is SdkSuggestion.Path -> configure(result.path, result.target)
else -> Unit
}
}
}
}
@RequiresBackgroundThread
suspend fun suggestSdkFromSdkmanrc(): SdkSuggestion? {
if (!file.exists()) return null
// Parse .sdkmanrc
val properties = Properties().apply {
val vfText = readAction {
findVirtualFile()?.let { FileDocumentManager.getInstance().getDocument(it)?.text }
}
load(vfText?.byteInputStream() ?: file.inputStream())
}
val java = properties.getProperty("java") ?: return null
val target = SdkmanCandidate.parse(java) ?: return null
LOG.info(".sdkmanrc found: $target")
val testedPaths = mutableListOf<String>()
// Match against the project SDK
val projectSdk = ProjectRootManager.getInstance(project).projectSdk
if (target.match(projectSdk)) {
return SdkSuggestion.Jdk(target, projectSdk, true)
} else {
projectSdk?.homePath?.let { testedPaths.add(it) }
LOG.info("$java - Project JDK doesn't match .sdkmanrc (${projectSdk?.versionString})")
}
// Match against the project JDK table
val jdks = ProjectJdkTable.getInstance().allJdks
for (jdk in jdks) {
if (jdk.homePath !in testedPaths && target.match(jdk)) {
jdk.homePath?.let { testedPaths.add(it) }
LOG.info("$java - Candidate found: ${jdk.versionString}")
return SdkSuggestion.Jdk(target, jdk, false)
}
}
// Match against JdkFinder
JdkFinder.getInstance().suggestHomePaths().forEach { path ->
if (path !in testedPaths && target.match(path)) {
testedPaths.add(path)
return SdkSuggestion.Path(target, path)
}
}
return null
}
private fun findVirtualFile() = VirtualFileManager.getInstance().findFileByNioPath(file.toPath().toAbsolutePath())
@RequiresEdt
private suspend fun configure(path: String, target: SdkmanCandidate) {
LOG.info("${target.target} - Candidate found: ${SdkVersionUtil.getJdkVersionInfo(path)?.displayVersionString()}")
val jdk = SdkConfigurationUtil.createAndAddSDK(path, JavaSdk.getInstance())
if (jdk != null) {
LOG.info("${target.target} - Registered ${jdk.name}")
configure(jdk, target)
}
}
@RequiresEdt
private suspend fun configure(jdk: Sdk, target: SdkmanCandidate) {
val rootManager = ProjectRootManager.getInstance(project)
writeAction {
rootManager.projectSdk = jdk
}
LOG.info("${target.target} - Configured ${jdk.name}")
NotificationGroupManager.getInstance()
.getNotificationGroup("Setup SDK")
.createNotification(
JavaBundle.message("sdk.configured.sdkmanrc.title"),
JavaBundle.message("sdk.configured.sdkmanrc", jdk.versionString),
NotificationType.INFORMATION
)
.notify(project)
}
@OptIn(ExperimentalContracts::class)
private fun SdkmanCandidate.match(sdk: Sdk?): Boolean {
contract { returns(true) implies (sdk != null) }
if (sdk == null) return false
val versionString = sdk.versionString ?: return false
return matchVersionString(versionString)
}
private fun SdkmanCandidate.match(path: String): Boolean {
val info = SdkVersionUtil.getJdkVersionInfo(path) ?: return false
return matchVersionString(info.displayVersionString())
}
override fun dispose() {}
}
private class SdkmanrcWatcher : ProjectActivity {
init {
if (ApplicationManager.getApplication().isUnitTestMode) {
throw ExtensionNotApplicableException.create()
}
}
override suspend fun execute(project: Project) {
if (!AdvancedSettings.getBoolean("java.sdkmanrc.watcher")) {
return
}
val watcherService = project.serviceAsync<SdkmanrcWatcherService>()
watcherService.registerListener(project)
watcherService.configureSdkFromSdkmanrc()
}
}

View File

@@ -0,0 +1,52 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.projectRoots.impl
import com.intellij.openapi.application.runWriteActionAndWait
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.testFramework.HeavyPlatformTestCase
import com.intellij.testFramework.IdeaTestUtil
import com.intellij.testFramework.VfsTestUtil.createFile
import kotlinx.coroutines.runBlocking
abstract class ExternalJavaConfigurationTest: HeavyPlatformTestCase() {
override fun setUp() {
super.setUp()
for (versionString in mockJdkVersions) {
val jdk = IdeaTestUtil.createMockJdk(versionString, "")
SdkConfigurationUtil.addSdk(jdk)
}
}
abstract val mockJdkVersions: List<String>
fun <T> checkSuggestion(
watcher: ExternalJavaConfigurationService,
configProvider: ExternalJavaConfigurationProvider<T>,
configFileContent: String,
expectedVersionString: String,
) {
val file = configProvider.getConfigurationFile(project)
createFile(getOrCreateProjectBaseDir(), file.name, configFileContent)
val suggestion = runBlocking { watcher.getReleaseData(configProvider) }
assert(suggestion != null)
val jdk = watcher.findCandidate<T>(suggestion!!, configProvider)
assert(jdk is ExternalJavaConfigurationService.JdkCandidate.Jdk)
assertEquals((jdk as ExternalJavaConfigurationService.JdkCandidate.Jdk).jdk.versionString, expectedVersionString)
}
override fun tearDown() {
try {
runWriteActionAndWait {
ProjectJdkTable.getInstance().apply {
allJdks.forEach { removeJdk(it) }
}
}
} catch (e: Exception) {
addSuppressedException(e)
} finally {
super.tearDown()
}
}
}

View File

@@ -1,52 +1,30 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.projectRoots.impl
import com.intellij.openapi.application.runWriteActionAndWait
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.platform.util.coroutines.childScope
import com.intellij.testFramework.HeavyPlatformTestCase
import com.intellij.testFramework.IdeaTestUtil
import com.intellij.testFramework.VfsTestUtil.createFile
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import kotlinx.coroutines.cancel
import kotlinx.coroutines.runBlocking
class SdkmanrcWatcherHeavyTests : HeavyPlatformTestCase() {
override fun setUp() {
super.setUp()
val graal21 = IdeaTestUtil.createMockJdk("GraalVM CE 23.1.2 - Java 21.0.2", "")
SdkConfigurationUtil.addSdk(graal21)
val jbr17 = IdeaTestUtil.createMockJdk("JetBrains Runtime 17.0.7", "")
SdkConfigurationUtil.addSdk(jbr17)
val open11 = IdeaTestUtil.createMockJdk("Oracle OpenJDK 11.0.2", "")
SdkConfigurationUtil.addSdk(open11)
}
class SdkmanrcWatcherHeavyTests : ExternalJavaConfigurationTest() {
override val mockJdkVersions: List<String> = listOf(
"GraalVM CE 23.1.2 - Java 21.0.2",
"Oracle OpenJDK 11.0.2",
"JetBrains Runtime 17.0.7",
)
fun `test jdk suggestion after sdkmanrc change`() {
runBlocking {
val scope = childScope()
val scope = childScope("")
try {
val watcher = SdkmanrcWatcherService(project, scope)
val watcher = ExternalJavaConfigurationService(project, scope)
val configProvider = SdkmanrcConfigurationProvider()
assert(watcher.suggestSdkFromSdkmanrc() == null)
assert(watcher.getReleaseData(configProvider) == null)
createFile(getOrCreateProjectBaseDir(), ".sdkmanrc", "java=21.0.2-graalce")
val suggestion1 = watcher.suggestSdkFromSdkmanrc()
assert(suggestion1 is SdkmanrcWatcherService.SdkSuggestion.Jdk)
assert((suggestion1 as SdkmanrcWatcherService.SdkSuggestion.Jdk).jdk.versionString == "GraalVM CE 23.1.2 - Java 21.0.2")
createFile(getOrCreateProjectBaseDir(), ".sdkmanrc", "java=11.0.2-open")
val suggestion2 = watcher.suggestSdkFromSdkmanrc()
assert(suggestion2 is SdkmanrcWatcherService.SdkSuggestion.Jdk)
assert((suggestion2 as SdkmanrcWatcherService.SdkSuggestion.Jdk).jdk.versionString == "Oracle OpenJDK 11.0.2")
createFile(getOrCreateProjectBaseDir(), ".sdkmanrc", "java=17.0.7-jbr")
val suggestion3 = watcher.suggestSdkFromSdkmanrc()
assert(suggestion3 is SdkmanrcWatcherService.SdkSuggestion.Jdk)
assert((suggestion3 as SdkmanrcWatcherService.SdkSuggestion.Jdk).jdk.versionString == "JetBrains Runtime 17.0.7")
checkSuggestion(watcher, configProvider, "java=21.0.2-graalce", "GraalVM CE 23.1.2 - Java 21.0.2")
checkSuggestion(watcher, configProvider, "java=11.0.2-open", "Oracle OpenJDK 11.0.2")
checkSuggestion(watcher, configProvider, "java=17.0.7-jbr", "JetBrains Runtime 17.0.7")
}
catch (_: Exception) {}
finally {
@@ -54,39 +32,25 @@ class SdkmanrcWatcherHeavyTests : HeavyPlatformTestCase() {
}
}
}
override fun tearDown() {
try {
runWriteActionAndWait {
ProjectJdkTable.getInstance().apply {
allJdks.forEach { removeJdk(it) }
}
}
} catch (e: Exception) {
addSuppressedException(e)
} finally {
super.tearDown()
}
}
}
class SdkmanrcWatcherLightTests : BasePlatformTestCase() {
fun `test candidates parsing`() {
assertEquals(SdkmanCandidate.parse("8"),
SdkmanCandidate("8", "8", null, ""))
assertEquals(SdkmanReleaseData.parse("8"),
SdkmanReleaseData("8", "8", null, ""))
assertEquals(SdkmanCandidate.parse("19-zulu"),
SdkmanCandidate("19-zulu", "19", null, "zulu"))
assertEquals(SdkmanReleaseData.parse("19-zulu"),
SdkmanReleaseData("19-zulu", "19", null, "zulu"))
assertEquals(SdkmanCandidate.parse("11.0.17-tem"),
SdkmanCandidate("11.0.17-tem", "11.0.17", null, "tem"))
assertEquals(SdkmanReleaseData.parse("11.0.17-tem"),
SdkmanReleaseData("11.0.17-tem", "11.0.17", null, "tem"))
assertEquals(SdkmanCandidate.parse("11.0.9.fx-librca"),
SdkmanCandidate("11.0.9.fx-librca", "11.0.9", "fx", "librca"))
assertEquals(SdkmanReleaseData.parse("11.0.9.fx-librca"),
SdkmanReleaseData("11.0.9.fx-librca", "11.0.9", "fx", "librca"))
assertEquals(SdkmanCandidate.parse("16.0.1.hs-adpt"),
SdkmanCandidate("16.0.1.hs-adpt", "16.0.1", "hs", "adpt"))
assertEquals(SdkmanReleaseData.parse("16.0.1.hs-adpt"),
SdkmanReleaseData("16.0.1.hs-adpt", "16.0.1", "hs", "adpt"))
}
fun `test candidates matching`() {
@@ -108,7 +72,7 @@ class SdkmanrcWatcherLightTests : BasePlatformTestCase() {
"22.ea.2-open" to "Oracle OpenJDK 22",
)) {
assert(
SdkmanCandidate.parse(candidate)?.matchVersionString(version) == true
SdkmanReleaseData.parse(candidate)?.matchVersionString(version) == true
) { "$candidate doesn't match $version" }
}
}

View File

@@ -1244,8 +1244,8 @@ scheduled.thread.pool.executor.with.zero.core.threads.description='ScheduledThre
scope.hierarchy=Hierarchy of {0}
sdk.cannot.create=Cannot Create SDK
sdk.java.no.classes=Cannot find JDK classes in ''{0}''
sdk.configured.sdkmanrc.title=JDK is configured (.sdkmanrc)
sdk.configured.sdkmanrc=''{0}'' is set as project JDK.
sdk.configured.external.config.title=JDK is configured ({0})
sdk.configured=''{0}'' is set as project JDK.
section.title.inspection.suspicious.names.ignore.methods=Ignore methods:
set.language.level=Set language level
set.language.level.to.0=Set language level to {0}
@@ -1823,7 +1823,7 @@ notification.group.source.searcher=Failed to find sources for JAR file
notification.group.language.level=Preview Java language level requires accepting license
notification.group.preview.features=Preview Java language level may be discontinued
notification.group.redundant.exports=Redundant exports/opens can be removed
notification.group.setup.sdk=JDK configured
notification.group.setup.jdk=JDK configured
notification.group.setup.external.annotations=Failed to load external annotations
notification.group.testintegration=Failed to generate tests for @TestDataPath
notification.group.legacy.library=Legacy library depends on IDE installation