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?