diff --git a/java/java-impl/resources/META-INF/JavaPlugin.xml b/java/java-impl/resources/META-INF/JavaPlugin.xml
index 98c92f4e3502..4f8e5ad280c7 100644
--- a/java/java-impl/resources/META-INF/JavaPlugin.xml
+++ b/java/java-impl/resources/META-INF/JavaPlugin.xml
@@ -280,6 +280,9 @@
+
+
@@ -481,6 +484,9 @@
+
+
+
diff --git a/java/java-impl/resources/codeVisionProviders/java.configuration/preview.java b/java/java-impl/resources/codeVisionProviders/java.configuration/preview.java
new file mode 100644
index 000000000000..c3813cc2e723
--- /dev/null
+++ b/java/java-impl/resources/codeVisionProviders/java.configuration/preview.java
@@ -0,0 +1,3 @@
+// .sdkmanrc
+java=tem-21.0.11
+gradle=7.5.1
\ No newline at end of file
diff --git a/java/java-impl/src/com/intellij/openapi/projectRoots/impl/ExternalJavaConfigurationCodeVision.kt b/java/java-impl/src/com/intellij/openapi/projectRoots/impl/ExternalJavaConfigurationCodeVision.kt
index be0de6956daa..d4dfce1ac7e7 100644
--- a/java/java-impl/src/com/intellij/openapi/projectRoots/impl/ExternalJavaConfigurationCodeVision.kt
+++ b/java/java-impl/src/com/intellij/openapi/projectRoots/impl/ExternalJavaConfigurationCodeVision.kt
@@ -6,7 +6,9 @@ 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.ide.DataManager
import com.intellij.java.JavaBundle
+import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.fileEditor.FileDocumentManager
@@ -14,11 +16,16 @@ 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.ui.popup.JBPopupFactory
+import com.intellij.openapi.util.Key
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
+import com.intellij.ui.awt.RelativePoint
import java.awt.event.MouseEvent
+private val FAKE_DATA_KEY: Key = Key.create("external.java.configuration.fake.key")
+
/**
* Code Vision for external Java configuration files (e.g., .sdkmanrc, .tool-versions).
*
@@ -35,7 +42,7 @@ public class ExternalJavaConfigurationCodeVision : CodeVisionProvider {
override fun precomputeOnUiThread(editor: Editor) {}
override fun preparePreview(editor: Editor, file: PsiFile) {
-
+ editor.putUserData(FAKE_DATA_KEY, true)
}
override val name: String
@@ -46,10 +53,14 @@ public class ExternalJavaConfigurationCodeVision : CodeVisionProvider {
get() = CodeVisionAnchorKind.Default
override val id: String
get() = ID
+ override val groupId: String
+ get() = ExternalJavaConfigurationGroupSettingProvider.GROUP_ID
override fun computeCodeVision(editor: Editor, uiData: Unit): CodeVisionState {
val project = editor.project ?: return CodeVisionState.READY_EMPTY
+ if (editor.getUserData(FAKE_DATA_KEY) == true) return fakeUserData()
+
return InlayHintsUtils.computeCodeVisionUnderReadAction {
val document = editor.document
val psiFile = PsiDocumentManager.getInstance(project).getPsiFile(document) ?: return@computeCodeVisionUnderReadAction CodeVisionState.READY_EMPTY
@@ -74,6 +85,13 @@ public class ExternalJavaConfigurationCodeVision : CodeVisionProvider {
}
}
+ private fun fakeUserData(): CodeVisionState {
+ val text = JavaBundle.message("external.java.configuration.inlay.already.configured", "temurin-11")
+ return CodeVisionState.Ready(listOf(
+ TextRange(13, 28) to ClickableTextCodeVisionEntry(text, ID, onClick = { _, _ -> }, icon = AllIcons.General.GreenCheckmark)
+ ))
+ }
+
private fun buildEntry(project: Project,
provider: ExternalJavaConfigurationProvider,
status: JavaConfigurationStatus): CodeVisionEntry? {
@@ -105,19 +123,35 @@ public class ExternalJavaConfigurationCodeVision : CodeVisionProvider {
is JavaConfigurationStatus.Missing<*> -> {
@Suppress("UNCHECKED_CAST")
val missing = status as JavaConfigurationStatus.Missing
- 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]
+
+ val actions = ExternalJavaConfigurationMissingAction.EP_NAME.extensionList.mapNotNull { it.createAction(project, provider, missing.releaseData) }
+
+ val onClick: (MouseEvent?, Editor) -> Unit = { event, editor ->
+ if (actions.isNotEmpty()) {
+ val group = DefaultActionGroup(actions)
+ val dataContext = DataManager.getInstance().getDataContext(event?.component ?: editor.component)
+ val popup = JBPopupFactory.getInstance().createActionGroupPopup(
+ null,
+ group,
+ dataContext,
+ JBPopupFactory.ActionSelectionAid.SPEEDSEARCH,
+ true
+ )
+ if (event != null) {
+ popup.show(RelativePoint(event))
+ } else {
+ popup.showInBestPositionFor(editor)
+ }
}
- 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
- }
+
+ val text = when {
+ actions.isNotEmpty() -> JavaBundle.message("external.java.configuration.inlay.missing.actions")
+ else -> JavaBundle.message("external.java.configuration.inlay.missing")
+ }
+
+ ClickableTextCodeVisionEntry(text, ID, onClick = onClick).apply {
+ showInMorePopup = actions.isNotEmpty()
}
}
}
diff --git a/java/java-impl/src/com/intellij/openapi/projectRoots/impl/ExternalJavaConfigurationCopyCommandAction.kt b/java/java-impl/src/com/intellij/openapi/projectRoots/impl/ExternalJavaConfigurationCopyCommandAction.kt
new file mode 100644
index 000000000000..18af3d6af8ad
--- /dev/null
+++ b/java/java-impl/src/com/intellij/openapi/projectRoots/impl/ExternalJavaConfigurationCopyCommandAction.kt
@@ -0,0 +1,24 @@
+// 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.icons.AllIcons
+import com.intellij.java.JavaBundle
+import com.intellij.openapi.actionSystem.AnAction
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.ide.CopyPasteManager
+import com.intellij.openapi.project.Project
+import java.awt.datatransfer.StringSelection
+
+/**
+ * Provides an action to copy the external tool download command to the clipboard.
+ */
+public class ExternalJavaConfigurationCopyCommandAction : ExternalJavaConfigurationMissingAction {
+ override fun createAction(project: Project, provider: ExternalJavaConfigurationProvider, releaseData: T): AnAction? {
+ val command = provider.getDownloadCommandFor(releaseData) ?: return null
+ return object : AnAction(JavaBundle.message("external.java.configuration.copy.command") , null, AllIcons.Actions.Copy) {
+ override fun actionPerformed(e: AnActionEvent) {
+ CopyPasteManager.getInstance().setContents(StringSelection(command))
+ }
+ }
+ }
+}
diff --git a/java/java-impl/src/com/intellij/openapi/projectRoots/impl/ExternalJavaConfigurationGroupSettingProvider.kt b/java/java-impl/src/com/intellij/openapi/projectRoots/impl/ExternalJavaConfigurationGroupSettingProvider.kt
new file mode 100644
index 000000000000..f4423b3cbd32
--- /dev/null
+++ b/java/java-impl/src/com/intellij/openapi/projectRoots/impl/ExternalJavaConfigurationGroupSettingProvider.kt
@@ -0,0 +1,20 @@
+// 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.settings.CodeVisionGroupSettingProvider
+import com.intellij.java.JavaBundle
+
+public class ExternalJavaConfigurationGroupSettingProvider : CodeVisionGroupSettingProvider {
+ public companion object {
+ public const val GROUP_ID: String = "java.configuration"
+ }
+
+ override val groupId: String
+ get() = GROUP_ID
+
+ override val groupName: String
+ get() = JavaBundle.message("code.vision.java.external.configuration.name")
+
+ override val description: String
+ get() = JavaBundle.message("code.vision.java.external.configuration.description")
+}
\ No newline at end of file
diff --git a/java/java-impl/src/com/intellij/openapi/projectRoots/impl/ExternalJavaConfigurationMissingAction.kt b/java/java-impl/src/com/intellij/openapi/projectRoots/impl/ExternalJavaConfigurationMissingAction.kt
new file mode 100644
index 000000000000..c18d4a47051b
--- /dev/null
+++ b/java/java-impl/src/com/intellij/openapi/projectRoots/impl/ExternalJavaConfigurationMissingAction.kt
@@ -0,0 +1,18 @@
+// 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.actionSystem.AnAction
+import com.intellij.openapi.extensions.ExtensionPointName
+import com.intellij.openapi.project.Project
+
+/**
+ * Extension point to provide actions when external Java configuration declares a JDK that is missing.
+ */
+public interface ExternalJavaConfigurationMissingAction {
+ public companion object {
+ public val EP_NAME: ExtensionPointName =
+ ExtensionPointName.create("com.intellij.openapi.projectRoots.externalJavaConfigurationMissingAction")
+ }
+
+ public fun createAction(project: Project, provider: ExternalJavaConfigurationProvider, releaseData: T): AnAction?
+}
diff --git a/java/java-impl/src/com/intellij/openapi/projectRoots/impl/ExternalJavaConfigurationRefreshAction.kt b/java/java-impl/src/com/intellij/openapi/projectRoots/impl/ExternalJavaConfigurationRefreshAction.kt
new file mode 100644
index 000000000000..4747d96088c2
--- /dev/null
+++ b/java/java-impl/src/com/intellij/openapi/projectRoots/impl/ExternalJavaConfigurationRefreshAction.kt
@@ -0,0 +1,22 @@
+// 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.icons.AllIcons
+import com.intellij.java.JavaBundle
+import com.intellij.openapi.actionSystem.AnAction
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.Project
+
+/**
+ * Provides an action to copy the external tool download command to the clipboard.
+ */
+public class ExternalJavaConfigurationRefreshAction : ExternalJavaConfigurationMissingAction {
+ override fun createAction(project: Project, provider: ExternalJavaConfigurationProvider, releaseData: T): AnAction {
+ return object : AnAction(JavaBundle.message("external.java.configuration.refresh"), null, AllIcons.Actions.Refresh) {
+ override fun actionPerformed(e: AnActionEvent) {
+ project.service().updateFromConfig(provider, true)
+ }
+ }
+ }
+}
diff --git a/java/java-impl/src/com/intellij/openapi/projectRoots/impl/ExternalJavaConfigurationService.kt b/java/java-impl/src/com/intellij/openapi/projectRoots/impl/ExternalJavaConfigurationService.kt
index 9b9ba930d6b9..4d2a9d34b1b3 100644
--- a/java/java-impl/src/com/intellij/openapi/projectRoots/impl/ExternalJavaConfigurationService.kt
+++ b/java/java-impl/src/com/intellij/openapi/projectRoots/impl/ExternalJavaConfigurationService.kt
@@ -26,6 +26,8 @@ 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.terminal.ui.TerminalWidget
+import com.intellij.util.asDisposable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -178,4 +180,8 @@ public class ExternalJavaConfigurationService(public val project: Project, priva
}
})
}
+
+ public fun addTerminationCallback(session: TerminalWidget, configProvider: ExternalJavaConfigurationProvider) {
+ session.addTerminationCallback({ updateFromConfig(configProvider) }, scope.asDisposable())
+ }
}
\ No newline at end of file
diff --git a/java/java-terminal/BUILD.bazel b/java/java-terminal/BUILD.bazel
index 94516808c899..bf3282335bd8 100644
--- a/java/java-terminal/BUILD.bazel
+++ b/java/java-terminal/BUILD.bazel
@@ -15,10 +15,14 @@ jvm_library(
deps = [
"@lib//:kotlin-stdlib-provided",
"//plugins/terminal",
- "//platform/core-api:core",
- "//java/execution/impl",
"//plugins/terminal/completion",
+ "//java/execution/impl",
+ "//java/java-impl:impl",
+ "//platform/core-api:core",
+ "//platform/editor-ui-api:editor-ui",
+ "//platform/execution-impl",
"//platform/lang-impl",
+ "//platform/util:util-ui",
],
runtime_deps = [":terminal_resources"]
)
diff --git a/java/java-terminal/intellij.java.terminal.iml b/java/java-terminal/intellij.java.terminal.iml
index 52bc197d09b3..2ce78db80c8a 100644
--- a/java/java-terminal/intellij.java.terminal.iml
+++ b/java/java-terminal/intellij.java.terminal.iml
@@ -10,9 +10,13 @@
-
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/java/java-terminal/resources/intellij.java.terminal.xml b/java/java-terminal/resources/intellij.java.terminal.xml
index 17cfa214c779..a0e1fbc4d474 100644
--- a/java/java-terminal/resources/intellij.java.terminal.xml
+++ b/java/java-terminal/resources/intellij.java.terminal.xml
@@ -6,4 +6,8 @@
+
+
+
+
diff --git a/java/java-terminal/resources/messages/JavaTerminalBundle.properties b/java/java-terminal/resources/messages/JavaTerminalBundle.properties
index b33edbacb656..738879f07877 100644
--- a/java/java-terminal/resources/messages/JavaTerminalBundle.properties
+++ b/java/java-terminal/resources/messages/JavaTerminalBundle.properties
@@ -7,5 +7,6 @@ java.command.terminal.show.version.option.description=Prints the product version
java.command.terminal.dry.run.option.description=Creates VM and load the main class but do not execute the main method.
java.command.terminal.enable.preview.option.description=Allows classes to depend on preview features of the release.
java.command.terminal.verbose.option.description=Enables verbose output for the given subsystem.
+external.java.configuration.run.command=Run ''{0}''\u2026
error.stream.name=error
output.stream.name=output
\ No newline at end of file
diff --git a/java/java-terminal/src/com/intellij/java/terminal/ExternalJavaConfigurationDownloadInTerminalAction.kt b/java/java-terminal/src/com/intellij/java/terminal/ExternalJavaConfigurationDownloadInTerminalAction.kt
new file mode 100644
index 000000000000..4cc3bfa2b87a
--- /dev/null
+++ b/java/java-terminal/src/com/intellij/java/terminal/ExternalJavaConfigurationDownloadInTerminalAction.kt
@@ -0,0 +1,29 @@
+// 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.java.terminal
+
+import com.intellij.icons.AllIcons
+import com.intellij.openapi.actionSystem.AnAction
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.projectRoots.impl.ExternalJavaConfigurationMissingAction
+import com.intellij.openapi.projectRoots.impl.ExternalJavaConfigurationProvider
+import com.intellij.openapi.projectRoots.impl.ExternalJavaConfigurationService
+import org.jetbrains.plugins.terminal.TerminalToolWindowManager
+
+/**
+ * Provides an action to open a new terminal session and execute the download command.
+ */
+class ExternalJavaConfigurationDownloadInTerminalAction : ExternalJavaConfigurationMissingAction {
+ override fun createAction(project: Project, provider: ExternalJavaConfigurationProvider, releaseData: T): AnAction? {
+ val command = provider.getDownloadCommandFor(releaseData) ?: return null
+ return object : AnAction(JavaTerminalBundle.message("external.java.configuration.run.command", command), null, AllIcons.Actions.Download) {
+ override fun actionPerformed(e: AnActionEvent) {
+ val session = TerminalToolWindowManager.getInstance(project).createNewSession()
+ val service = project.service()
+ service.addTerminationCallback(session, provider)
+ session.sendCommandToExecute(command)
+ }
+ }
+ }
+}
diff --git a/java/openapi/resources/messages/JavaBundle.properties b/java/openapi/resources/messages/JavaBundle.properties
index b9c3ba34d1b2..87d6f02684db 100644
--- a/java/openapi/resources/messages/JavaBundle.properties
+++ b/java/openapi/resources/messages/JavaBundle.properties
@@ -1777,12 +1777,16 @@ 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
+code.vision.java.external.configuration.name=Java Configuration
+code.vision.java.external.configuration.description=Status of JDKs configured in external configuration files (.sdkmanrc, .tool-versions).
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
+external.java.configuration.inlay.missing.actions=JDK not found. More\u2026
+external.java.configuration.inlay.missing=JDK not found.
+external.java.configuration.copy.command=Copy Download Command
+external.java.configuration.refresh=Refresh
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?