mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[jdk] Add code vision for external Java configuration tools
IDEA-364749 GitOrigin-RevId: cbd4a97359c41efe147177cac8b4f0e048b671b3
This commit is contained in:
committed by
intellij-monorepo-bot
parent
604fdbacda
commit
323e1ae105
@@ -480,6 +480,7 @@
|
||||
<postStartupActivity implementation="com.intellij.openapi.projectRoots.impl.ExistingJdkConfigurationActivity"/>
|
||||
<openapi.projectRoots.externalJavaConfigurationProvider implementation="com.intellij.openapi.projectRoots.impl.SdkmanrcConfigurationProvider"/>
|
||||
<openapi.projectRoots.externalJavaConfigurationProvider implementation="com.intellij.openapi.projectRoots.impl.ToolVersionsConfigurationProvider"/>
|
||||
<codeInsight.codeVisionProvider implementation="com.intellij.openapi.projectRoots.impl.ExternalJavaConfigurationCodeVision"/>
|
||||
<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."/>
|
||||
|
||||
+4
-32
@@ -1,15 +1,11 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
// Copyright 2000-2025 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
|
||||
|
||||
@@ -18,8 +14,6 @@ import kotlin.time.Duration.Companion.seconds
|
||||
*/
|
||||
public class ExternalJavaConfigurationActivity : ProjectActivity {
|
||||
|
||||
private val disposableMap = mutableMapOf<Class<*>, Disposable>()
|
||||
|
||||
init {
|
||||
if (ApplicationManager.getApplication().isUnitTestMode) {
|
||||
throw ExtensionNotApplicableException.create()
|
||||
@@ -30,33 +24,11 @@ public class ExternalJavaConfigurationActivity : ProjectActivity {
|
||||
// Delay a little bit not to overload project opening
|
||||
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)
|
||||
val service = project.service<ExternalJavaConfigurationService>()
|
||||
service.addExtensionPointListener()
|
||||
|
||||
for (configProvider in ExternalJavaConfigurationProvider.EP_NAME.extensionList) {
|
||||
val file = configProvider.getConfigurationFile(project)
|
||||
setupExtension(project, configProvider)
|
||||
if (file.exists()) configWatcherService.updateJdkFromConfig(configProvider)
|
||||
service.updateFromConfig(configProvider, true)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
// Copyright 2000-2025 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.codeInsight.codeVision.*
|
||||
import com.intellij.codeInsight.codeVision.ui.model.ClickableTextCodeVisionEntry
|
||||
import com.intellij.codeInsight.codeVision.ui.model.TextCodeVisionEntry
|
||||
import com.intellij.codeInsight.hints.InlayHintsUtils
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.java.JavaBundle
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.impl.ExternalJavaConfigurationService.JavaConfigurationStatus
|
||||
import com.intellij.openapi.roots.ProjectRootManager
|
||||
import com.intellij.openapi.roots.ui.configuration.ProjectSettingsService
|
||||
import com.intellij.openapi.util.TextRange
|
||||
import com.intellij.psi.PsiDocumentManager
|
||||
import com.intellij.psi.PsiFile
|
||||
import java.awt.event.MouseEvent
|
||||
|
||||
/**
|
||||
* Code Vision for external Java configuration files (e.g., .sdkmanrc, .tool-versions).
|
||||
*
|
||||
* It shows the [JavaConfigurationStatus] and makes it possible to set the project JDK
|
||||
* or download missing JDKs through an external configuration tool.
|
||||
*/
|
||||
public class ExternalJavaConfigurationCodeVision : CodeVisionProvider<Unit> {
|
||||
public companion object {
|
||||
internal const val ID: String = "java.external.configuration"
|
||||
}
|
||||
|
||||
override fun isAvailableFor(project: Project): Boolean = true
|
||||
|
||||
override fun precomputeOnUiThread(editor: Editor) {}
|
||||
|
||||
override fun preparePreview(editor: Editor, file: PsiFile) {
|
||||
|
||||
}
|
||||
|
||||
override val name: String
|
||||
get() = JavaBundle.message("external.java.configuration.inlay.provider.name")
|
||||
override val relativeOrderings: List<CodeVisionRelativeOrdering>
|
||||
get() = emptyList()
|
||||
override val defaultAnchor: CodeVisionAnchorKind
|
||||
get() = CodeVisionAnchorKind.Default
|
||||
override val id: String
|
||||
get() = ID
|
||||
|
||||
override fun computeCodeVision(editor: Editor, uiData: Unit): CodeVisionState {
|
||||
val project = editor.project ?: return CodeVisionState.READY_EMPTY
|
||||
|
||||
return InlayHintsUtils.computeCodeVisionUnderReadAction {
|
||||
val document = editor.document
|
||||
val psiFile = PsiDocumentManager.getInstance(project).getPsiFile(document) ?: return@computeCodeVisionUnderReadAction CodeVisionState.READY_EMPTY
|
||||
|
||||
val provider = ExternalJavaConfigurationProvider.EP_NAME.extensionList.find { it.isConfigurationFile(psiFile.name) }
|
||||
?: return@computeCodeVisionUnderReadAction CodeVisionState.READY_EMPTY
|
||||
|
||||
val text = FileDocumentManager.getInstance().getFile(document)?.let { FileDocumentManager.getInstance().getDocument(it)?.text }
|
||||
?: psiFile.text
|
||||
|
||||
val range = provider.getReleaseDataOffset(text) ?: return@computeCodeVisionUnderReadAction CodeVisionState.READY_EMPTY
|
||||
|
||||
val service = project.service<ExternalJavaConfigurationService>()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
service.updateFromConfig(provider as ExternalJavaConfigurationProvider<Any?>)
|
||||
val status = service.statuses[psiFile.virtualFile.path] ?: JavaConfigurationStatus.Unknown
|
||||
|
||||
val entry = buildEntry(project, provider, status)
|
||||
if (entry == null) return@computeCodeVisionUnderReadAction CodeVisionState.READY_EMPTY
|
||||
|
||||
CodeVisionState.Ready(listOf(TextRange(range.startOffset, range.endOffset) to entry))
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> buildEntry(project: Project,
|
||||
provider: ExternalJavaConfigurationProvider<T>,
|
||||
status: JavaConfigurationStatus): CodeVisionEntry? {
|
||||
return when (status) {
|
||||
is JavaConfigurationStatus.Unknown -> {
|
||||
val text = JavaBundle.message("external.java.configuration.inlay.unknown")
|
||||
TextCodeVisionEntry(text, ID, icon = AllIcons.Actions.Refresh).apply { showInMorePopup = false }
|
||||
}
|
||||
|
||||
is JavaConfigurationStatus.AlreadyConfigured -> {
|
||||
val jdkName = ProjectRootManager.getInstance(project).projectSdk?.name ?: return null
|
||||
val text = JavaBundle.message("external.java.configuration.inlay.already.configured", jdkName)
|
||||
val onClick: (MouseEvent?, Editor) -> Unit = { _, _ ->
|
||||
ProjectSettingsService.getInstance(project).openProjectSettings()
|
||||
}
|
||||
ClickableTextCodeVisionEntry(text, ID, onClick = onClick, icon = AllIcons.General.GreenCheckmark)
|
||||
}
|
||||
|
||||
is JavaConfigurationStatus.Found -> {
|
||||
val text = JavaBundle.message("external.java.configuration.inlay.found")
|
||||
val onClick: (MouseEvent?, Editor) -> Unit = { _, _ ->
|
||||
val service = project.service<ExternalJavaConfigurationService>()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
service.updateFromConfig(provider as ExternalJavaConfigurationProvider<Any?>, true)
|
||||
}
|
||||
ClickableTextCodeVisionEntry(text, ID, onClick = onClick, icon = AllIcons.General.Gear)
|
||||
}
|
||||
|
||||
is JavaConfigurationStatus.Missing<*> -> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val missing = status as JavaConfigurationStatus.Missing<T>
|
||||
val command = provider.getDownloadCommandFor(missing.releaseData)
|
||||
if (command != null) {
|
||||
val text = JavaBundle.message("external.java.configuration.inlay.download", command)
|
||||
val onClick: (MouseEvent?, Editor) -> Unit = { _, _ ->
|
||||
// TODO: Open terminal session with the [command]
|
||||
}
|
||||
ClickableTextCodeVisionEntry(text, ID, onClick = onClick, icon = AllIcons.Actions.Download)
|
||||
}
|
||||
else {
|
||||
val text = JavaBundle.message("external.java.configuration.inlay.missing")
|
||||
TextCodeVisionEntry(text, ID, icon = AllIcons.General.Warning).apply {
|
||||
showInMorePopup = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+59
-30
@@ -1,6 +1,8 @@
|
||||
// Copyright 2000-2025 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.codeInsight.codeVision.CodeVisionHost
|
||||
import com.intellij.codeInsight.codeVision.CodeVisionHost.LensInvalidateSignal
|
||||
import com.intellij.execution.wsl.WslPath
|
||||
import com.intellij.java.JavaBundle
|
||||
import com.intellij.notification.NotificationGroupManager
|
||||
@@ -9,9 +11,12 @@ import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.application.edtWriteAction
|
||||
import com.intellij.openapi.application.readAction
|
||||
import com.intellij.openapi.application.runInEdt
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.extensions.ExtensionPointListener
|
||||
import com.intellij.openapi.extensions.PluginDescriptor
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.guessProjectDir
|
||||
@@ -21,10 +26,6 @@ import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.roots.ProjectRootManager
|
||||
import com.intellij.openapi.util.SystemInfo
|
||||
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
|
||||
@@ -40,43 +41,50 @@ private val LOG = logger<ExternalJavaConfigurationService>()
|
||||
@Service(Service.Level.PROJECT)
|
||||
public class ExternalJavaConfigurationService(public val project: Project, private val scope: CoroutineScope) : Disposable {
|
||||
|
||||
internal sealed class JavaConfigurationStatus {
|
||||
object Unknown : JavaConfigurationStatus()
|
||||
object AlreadyConfigured : JavaConfigurationStatus()
|
||||
object Found : JavaConfigurationStatus()
|
||||
data class Missing<T>(val releaseData: T) : JavaConfigurationStatus()
|
||||
}
|
||||
|
||||
internal val statuses = HashMap<String, JavaConfigurationStatus>().withDefault { JavaConfigurationStatus.Unknown }
|
||||
|
||||
public sealed class JdkCandidate<T> {
|
||||
public data class Jdk<T>(val releaseData: T, val jdk: Sdk, val project: Boolean) : JdkCandidate<T>()
|
||||
public 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>) {
|
||||
//TODO: getConfigurationFile(project) create a File with a fixed name, only to extract this name back here
|
||||
// much better would be to provide .getConfigurationFileName() method, OR just cache the file created
|
||||
// inside getConfigurationFile()
|
||||
val configFileName = configProvider.getConfigurationFile(project).name
|
||||
for (event in events) {
|
||||
if (!event.path.endsWith(configFileName)) continue
|
||||
if (event !is VFileContentChangeEvent && event !is VFileCreateEvent) continue
|
||||
|
||||
updateJdkFromConfig(configProvider)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the project JDK according to the configuration file of [configProvider].
|
||||
* Searches for a matching JDK candidate if a Java configuration is defined in the config file.
|
||||
* If [configureJdk] is true, the project JDK will be updated if a candidate is found.
|
||||
*/
|
||||
public fun <T> updateJdkFromConfig(configProvider: ExternalJavaConfigurationProvider<T>) {
|
||||
public fun <T> updateFromConfig(configProvider: ExternalJavaConfigurationProvider<T>, configureJdk: Boolean = false) {
|
||||
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)
|
||||
when (val candidate = findCandidate(releaseData, configProvider)) {
|
||||
is JdkCandidate.Jdk -> {
|
||||
if (candidate.project) {
|
||||
setStatus(file.path, JavaConfigurationStatus.AlreadyConfigured)
|
||||
}
|
||||
else {
|
||||
setStatus(file.path, JavaConfigurationStatus.Found)
|
||||
if (configureJdk) configure(candidate.jdk, file.name, file.path, releaseData)
|
||||
}
|
||||
}
|
||||
is JdkCandidate.Path -> {
|
||||
setStatus(file.path, JavaConfigurationStatus.Found)
|
||||
if (configureJdk) {
|
||||
service<AddJdkService>().createJdkFromPath(candidate.path) {
|
||||
configure(it, file.name, file.path, releaseData)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
setStatus(file.path, JavaConfigurationStatus.Missing(releaseData))
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,12 +139,15 @@ public class ExternalJavaConfigurationService(public val project: Project, priva
|
||||
return null
|
||||
}
|
||||
|
||||
private fun <T> configure(jdk: Sdk, fileName: String, candidate: T) {
|
||||
private fun <T> configure(jdk: Sdk, fileName: String, filePath: String, candidate: T) {
|
||||
scope.launch(Dispatchers.EDT) {
|
||||
val rootManager = ProjectRootManager.getInstance(project)
|
||||
edtWriteAction { rootManager.projectSdk = jdk }
|
||||
LOG.info("[$fileName] $candidate - JDK registered: ${jdk.versionString}")
|
||||
|
||||
// Update status and refresh inlays after configuration
|
||||
setStatus(filePath, JavaConfigurationStatus.AlreadyConfigured)
|
||||
|
||||
NotificationGroupManager.getInstance()
|
||||
.getNotificationGroup("Setup JDK")
|
||||
.createNotification(
|
||||
@@ -148,5 +159,23 @@ public class ExternalJavaConfigurationService(public val project: Project, priva
|
||||
}
|
||||
}
|
||||
|
||||
private fun setStatus(filePath: String, newStatus: JavaConfigurationStatus) {
|
||||
if (statuses[filePath] == newStatus) return
|
||||
statuses[filePath] = newStatus
|
||||
runInEdt {
|
||||
project.service<CodeVisionHost>().invalidateProvider(
|
||||
LensInvalidateSignal(null, listOf(ExternalJavaConfigurationCodeVision.ID))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {}
|
||||
|
||||
public fun addExtensionPointListener() {
|
||||
ExternalJavaConfigurationProvider.EP_NAME.addExtensionPointListener(scope, object : ExtensionPointListener<ExternalJavaConfigurationProvider<*>> {
|
||||
override fun extensionAdded(extension: ExternalJavaConfigurationProvider<*>, pluginDescriptor: PluginDescriptor) {
|
||||
updateFromConfig(extension)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1777,6 +1777,12 @@ dialog.message.template.not.applicable=Template is not applicable
|
||||
dialog.message.class.not.found=Template class ''{0}'' not found
|
||||
notification.content.was.set.up=JDK ''{0}'' was set up for the project
|
||||
notification.content.change.jdk=Change JDK
|
||||
external.java.configuration.inlay.provider.name=External JDK Configuration
|
||||
external.java.configuration.inlay.unknown=Syncing\u2026
|
||||
external.java.configuration.inlay.already.configured=Project JDK ({0})
|
||||
external.java.configuration.inlay.found=Set as project JDK
|
||||
external.java.configuration.inlay.download=Run ''{0}''
|
||||
external.java.configuration.inlay.missing=JDK not found
|
||||
button.to.another.directory=To &Directory
|
||||
button.to.another.source.root=To &Source Root
|
||||
where.do.you.want.to.move.directory.prompt={0}\n\nWould you like to move the directory to another source root or another directory?
|
||||
|
||||
Reference in New Issue
Block a user