From f67cdb26b8f1e1cdaa91c3aaf32efbe6a4723673 Mon Sep 17 00:00:00 2001 From: Hanxiao Liu Date: Thu, 23 Oct 2025 17:22:20 +0800 Subject: [PATCH 01/43] Upgrade to IntelliJ 2025.3 --- .../build.gradle.kts | 6 ++++ .../terminal/AzureCloudTerminalRunner.kt | 6 ++-- .../build.gradle.kts | 2 +- .../build.gradle.kts | 3 ++ .../build.gradle.kts | 2 +- .../spark/console/SparkLivySessionProcess.kt | 4 --- .../spark/run/SparkBatchJobProcessAdapter.kt | 4 --- .../build.gradle.kts | 4 +++ .../build.gradle.kts | 30 +++++++++---------- .../gradle.properties | 16 +++++----- .../gradle/libs.versions.toml | 9 +++--- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../hdinsight-node-common/pom.xml | 6 ++-- .../azure-toolkit-ide-hdinsight-libs/pom.xml | 6 ++++ .../spark-localrun-mock/pom.xml | 2 +- Utils/azure-toolkit-ide-libs/pom.xml | 6 ++++ 16 files changed, 62 insertions(+), 46 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice/build.gradle.kts b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice/build.gradle.kts index 651a0c7b441..f15865b9924 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice/build.gradle.kts +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice/build.gradle.kts @@ -21,3 +21,9 @@ dependencies { bundledPlugin("org.jetbrains.plugins.terminal") } } + +tasks { + instrumentCode { + instrumentationLogs = true + } +} \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-cloud-shell/src/main/kotlin/com/microsoft/azure/toolkit/intellij/cloudshell/terminal/AzureCloudTerminalRunner.kt b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-cloud-shell/src/main/kotlin/com/microsoft/azure/toolkit/intellij/cloudshell/terminal/AzureCloudTerminalRunner.kt index c64919d3290..9234e4820c9 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-cloud-shell/src/main/kotlin/com/microsoft/azure/toolkit/intellij/cloudshell/terminal/AzureCloudTerminalRunner.kt +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-cloud-shell/src/main/kotlin/com/microsoft/azure/toolkit/intellij/cloudshell/terminal/AzureCloudTerminalRunner.kt @@ -9,7 +9,7 @@ package com.microsoft.azure.toolkit.intellij.cloudshell.terminal import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer -import com.intellij.platform.util.coroutines.namedChildScope +import com.intellij.platform.util.coroutines.childScope import com.jediterm.terminal.TtyConnector import com.microsoft.azure.toolkit.intellij.cloudshell.CloudShellService import com.microsoft.azure.toolkit.intellij.cloudshell.controlchannel.CloudConsoleControlChannelClient @@ -56,7 +56,7 @@ class AzureCloudTerminalRunner( cloudConsoleBaseUrl, project ) - controlClient.connect(scope.namedChildScope("CloudConsoleControlChannelClient")) + controlClient.connect(scope.childScope("CloudConsoleControlChannelClient")) val connector = createConnector(process) Disposer.register(connector, controlClient) @@ -70,7 +70,7 @@ class AzureCloudTerminalRunner( private fun createConnector(process: CloudTerminalProcess) = AzureCloudProcessTtyConnector( process, project, - scope.namedChildScope("AzureCloudProcessTtyConnector"), + scope.childScope("AzureCloudProcessTtyConnector"), uploadFileToTerminalUrl, previewPortBaseUrl, resizeTerminalUrl diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-cosmos/build.gradle.kts b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-cosmos/build.gradle.kts index 4019ae664bb..a0f9b36c393 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-cosmos/build.gradle.kts +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-cosmos/build.gradle.kts @@ -19,7 +19,7 @@ dependencies { implementation("com.microsoft.azure:azure-toolkit-identity-lib") intellijPlatform { - intellijIdeaUltimate(properties("platformVersion").get()) + // intellijIdeaUltimate(properties("platformVersion").get()) // Plugin Dependencies. Uses `platformBundledPlugins` property from the gradle.properties file for bundled IntelliJ Platform plugins. bundledPlugin("com.intellij.java") bundledPlugin("com.intellij.database") diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-guidance/build.gradle.kts b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-guidance/build.gradle.kts index caeba7b18f2..98f30de8426 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-guidance/build.gradle.kts +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-guidance/build.gradle.kts @@ -5,4 +5,7 @@ dependencies { // runtimeOnly project(path: ":azure-intellij-resource-connector-lib", configuration: "instrumentedJar") implementation("com.microsoft.azure:azure-toolkit-ide-common-lib") implementation("org.yaml:snakeyaml:2.2") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.15.2") { + exclude(group = "com.fasterxml.jackson", module = "jackson-bom") + } } \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-hdinsight-lib/build.gradle.kts b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-hdinsight-lib/build.gradle.kts index 7360e3414d9..edd3bcda579 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-hdinsight-lib/build.gradle.kts +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-hdinsight-lib/build.gradle.kts @@ -9,7 +9,7 @@ dependencies { implementation("com.microsoft.hdinsight:azure-toolkit-ide-hdinsight-spark-lib") intellijPlatform { - intellijIdeaUltimate(properties("platformVersion").get()) + // intellijIdeaUltimate(properties("platformVersion").get()) // Plugin Dependencies. Uses `platformBundledPlugins` property from the gradle.properties file for bundled IntelliJ Platform plugins. bundledPlugin("com.intellij.java") plugin("org.intellij.scala:2024.2.5") diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-hdinsight-lib/src/main/kotlin/com/microsoft/azure/hdinsight/spark/console/SparkLivySessionProcess.kt b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-hdinsight-lib/src/main/kotlin/com/microsoft/azure/hdinsight/spark/console/SparkLivySessionProcess.kt index 6b044cfaf6d..4686d18afaa 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-hdinsight-lib/src/main/kotlin/com/microsoft/azure/hdinsight/spark/console/SparkLivySessionProcess.kt +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-hdinsight-lib/src/main/kotlin/com/microsoft/azure/hdinsight/spark/console/SparkLivySessionProcess.kt @@ -63,10 +63,6 @@ class SparkLivySessionProcess( return 0 } - override fun setWindowSize(columns: Int, rows: Int) { - - } - override fun getInputStream(): InputStream = stdOutStream fun start(): Observable = session.deploy() diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-hdinsight-lib/src/main/kotlin/com/microsoft/azure/hdinsight/spark/run/SparkBatchJobProcessAdapter.kt b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-hdinsight-lib/src/main/kotlin/com/microsoft/azure/hdinsight/spark/run/SparkBatchJobProcessAdapter.kt index 358d35295d0..2f557f079a4 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-hdinsight-lib/src/main/kotlin/com/microsoft/azure/hdinsight/spark/run/SparkBatchJobProcessAdapter.kt +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-hdinsight-lib/src/main/kotlin/com/microsoft/azure/hdinsight/spark/run/SparkBatchJobProcessAdapter.kt @@ -64,8 +64,4 @@ class SparkBatchJobProcessAdapter(val sparkJobProcess: SparkBatchJobRemoteProces override fun killProcessTree(): Boolean { return sparkJobProcess.killProcessTree() } - - override fun setWindowSize(columns: Int, rows: Int) { - - } } \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/build.gradle.kts b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/build.gradle.kts index fd4354002c6..8229d1b47d3 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/build.gradle.kts +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/build.gradle.kts @@ -20,8 +20,12 @@ dependencies { exclude(group="pull-parser", module="pull-parser") exclude(group="net.java.dev.msv", module="xsdlib") } + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.15.2") { + exclude(group = "com.fasterxml.jackson", module = "jackson-bom") + } intellijPlatform { // Plugin Dependencies. Uses `platformBundledPlugins` property from the gradle.properties file for bundled IntelliJ Platform plugins. bundledPlugin("org.jetbrains.plugins.terminal") } } + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/build.gradle.kts b/PluginsAndFeatures/azure-toolkit-for-intellij/build.gradle.kts index 64d13fd4d11..696be472581 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/build.gradle.kts +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/build.gradle.kts @@ -33,12 +33,12 @@ allprojects { java { toolchain { - languageVersion = JavaLanguageVersion.of(17) + languageVersion = JavaLanguageVersion.of(21) } } kotlin { - jvmToolchain(17) + jvmToolchain(21) } repositories { @@ -55,26 +55,21 @@ allprojects { intellijPlatform { buildSearchableOptions = false - instrumentCode = true + // instrumentCode = true } dependencies { intellijPlatform { - intellijIdeaUltimate(properties("platformVersion").get()) - // run from a local idea installation - // local(File("C:\\Program Files\\JetBrains\\IntelliJ IDEA 242.16677.21")); - instrumentationTools() - // https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-jetbrains-runtime.html#declared-explicitly - // jetbrainsRuntime() + intellijIdeaUltimate(properties("platformVersion").get(), useInstaller = false) } implementation(platform("com.microsoft.azure:azure-toolkit-libs:0.52.2")) implementation(platform("com.microsoft.azure:azure-toolkit-ide-libs:0.52.2")) implementation(platform("com.microsoft.hdinsight:azure-toolkit-ide-hdinsight-libs:0.1.1")) - compileOnly("org.projectlombok:lombok:1.18.24") + compileOnly("org.projectlombok:lombok:1.18.32") compileOnly("org.jetbrains:annotations:24.0.0") - annotationProcessor("org.projectlombok:lombok:1.18.24") + annotationProcessor("org.projectlombok:lombok:1.18.32") implementation("com.microsoft.azure:azure-toolkit-common-lib:0.52.2") aspect("com.microsoft.azure:azure-toolkit-common-lib:0.52.2") } @@ -91,21 +86,25 @@ allprojects { implementation { exclude(module = "xsdlib") } } + tasks.configureEach { + if (name == "instrumentCode") { + enabled = file("src/main/java").exists() + } + } tasks { + compileJava { - sourceCompatibility = "17" - targetCompatibility = "17" + sourceCompatibility = "21" + targetCompatibility = "21" } compileKotlin { - kotlinOptions.jvmTarget = "17" configure { enabled = false } } compileTestKotlin { - kotlinOptions.jvmTarget = "17" configure { enabled = false } @@ -134,7 +133,6 @@ allprojects { intellijPlatform { projectName = "azure-toolkit-for-intellij" buildSearchableOptions = false - instrumentCode = true pluginConfiguration { id = properties("pluginId").get() diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties index 82a2e43df31..9e1159748ce 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties @@ -1,16 +1,16 @@ pluginVersion=3.96.2 -intellijDisplayVersion=2025.2 -intellij_version=252-EAP-SNAPSHOT -platformVersion=252.23309.22 +intellijDisplayVersion=2025.3 +intellij_version=253-EAP-SNAPSHOT +platformVersion=253-EAP-SNAPSHOT # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html -pluginSinceBuild=252 -pluginUntilBuild=252.* +pluginSinceBuild=253 +pluginUntilBuild=253.* # Example: platformPlugins = com.jetbrains.php:203.4449.22, org.intellij.scala:2023.3.27@EAP -platformPlugins=org.intellij.scala:2025.2.9 +platformPlugins=org.intellij.scala:2025.3.12 # IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension platformType=IU needPatchVersion=true -javaVersion=17 +javaVersion=21 applicationinsights.key=57cc111a-36a8-44b3-b044-25d293b8b77c # IntelliJ Platform Artifacts Repositories -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html pluginGroup=com.microsoft.azuretools @@ -22,7 +22,7 @@ platformDownloadSources=true # Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib kotlin.stdlib.default.dependency=false # Gradle Releases -> https://github.com/gradle/gradle/releases -gradleVersion=8.8 +gradleVersion=8.14.1 # Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html org.gradle.configuration-cache=true org.gradle.configuration-cache.problems=warn diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle/libs.versions.toml b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle/libs.versions.toml index a411c10e5a8..fa3446576dd 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle/libs.versions.toml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle/libs.versions.toml @@ -2,15 +2,16 @@ # libraries # plugins -kotlin = "2.1.0" -changelog = "2.2.0" -intellijPlatform = "2.3.0" +kotlin = "2.2.20" +changelog = "2.4.0" +#intellijPlatform = "2.10.1" +intellijPlatform = "2.10.1" detekt = "1.23.6" ktlint = "12.1.1" #gradleIntelliJPlugin = "1.17.3" #qodana = "2024.1.5" aspectj = "8.6" -springDependencyManagement = "1.0.11.RELEASE" +springDependencyManagement = "1.1.6" serialization = "1.9.24" [plugins] diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle/wrapper/gradle-wrapper.properties b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle/wrapper/gradle-wrapper.properties index a4413138c96..002b867c48b 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle/wrapper/gradle-wrapper.properties +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/Utils/azure-toolkit-ide-hdinsight-libs/hdinsight-node-common/pom.xml b/Utils/azure-toolkit-ide-hdinsight-libs/hdinsight-node-common/pom.xml index 610e2938205..04212e12058 100644 --- a/Utils/azure-toolkit-ide-hdinsight-libs/hdinsight-node-common/pom.xml +++ b/Utils/azure-toolkit-ide-hdinsight-libs/hdinsight-node-common/pom.xml @@ -10,7 +10,7 @@ ${project.artifactId}-${project.version}.jar true UTF-8 - 2.12.10 + 2.12.18 3.4.0 true @@ -108,7 +108,7 @@ net.alchim31.maven scala-maven-plugin - 4.8.0 + 4.8.1 @@ -147,7 +147,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.5.1 + 3.13.0 diff --git a/Utils/azure-toolkit-ide-hdinsight-libs/pom.xml b/Utils/azure-toolkit-ide-hdinsight-libs/pom.xml index c09b1327fda..31696fe9002 100644 --- a/Utils/azure-toolkit-ide-hdinsight-libs/pom.xml +++ b/Utils/azure-toolkit-ide-hdinsight-libs/pom.xml @@ -24,6 +24,7 @@ 6.4.0 3.4.0 1.1.10.4 + 1.18.36 azuretools-core @@ -59,6 +60,11 @@ pom import + + org.projectlombok + lombok + ${lombok.version} + com.microsoft.hdinsight hdinsight-node-common diff --git a/Utils/azure-toolkit-ide-hdinsight-libs/spark-localrun-mock/pom.xml b/Utils/azure-toolkit-ide-hdinsight-libs/spark-localrun-mock/pom.xml index 0d70d77ba6c..f1262d60e65 100644 --- a/Utils/azure-toolkit-ide-hdinsight-libs/spark-localrun-mock/pom.xml +++ b/Utils/azure-toolkit-ide-hdinsight-libs/spark-localrun-mock/pom.xml @@ -12,7 +12,7 @@ ${project.artifactId}-${project.version}.jar false UTF-8 - 2.12.10 + 2.12.18 3.4.0 true diff --git a/Utils/azure-toolkit-ide-libs/pom.xml b/Utils/azure-toolkit-ide-libs/pom.xml index 6be3b1fb091..d1566380d9e 100644 --- a/Utils/azure-toolkit-ide-libs/pom.xml +++ b/Utils/azure-toolkit-ide-libs/pom.xml @@ -57,6 +57,7 @@ 17 17 + 1.18.36 0.52.2 @@ -75,6 +76,11 @@ pom import + + org.projectlombok + lombok + ${lombok.version} + com.microsoft.azure azure-toolkit-ide-common-lib From f65b5423622ecf8a1cf507e676b8f7704c81c94a Mon Sep 17 00:00:00 2001 From: Hanxiao Liu Date: Thu, 30 Oct 2025 19:56:08 +0800 Subject: [PATCH 02/43] Fix class cast exception in explorer dialogs --- .../intellij/legacy/appservice/AppServiceInfoBasicPanel.form | 5 +++-- .../intellij/legacy/appservice/AppServiceInfoBasicPanel.java | 1 + .../appservice/serviceplan/ServicePlanCreationDialog.form | 5 +++-- .../appservice/serviceplan/ServicePlanCreationDialog.java | 5 +++++ .../component/resourcegroup/ResourceGroupCreationDialog.form | 2 +- .../component/resourcegroup/ResourceGroupCreationDialog.java | 5 +++++ 6 files changed, 18 insertions(+), 5 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice-java/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/AppServiceInfoBasicPanel.form b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice-java/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/AppServiceInfoBasicPanel.form index a0b98ee74c2..958336737b0 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice-java/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/AppServiceInfoBasicPanel.form +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice-java/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/AppServiceInfoBasicPanel.form @@ -3,7 +3,7 @@ - + @@ -43,10 +43,11 @@ - + + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice-java/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/AppServiceInfoBasicPanel.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice-java/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/AppServiceInfoBasicPanel.java index 0eb3318a794..8f6a876ebe3 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice-java/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/AppServiceInfoBasicPanel.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice-java/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/AppServiceInfoBasicPanel.java @@ -135,6 +135,7 @@ public RuntimeComboBox getSelectorRuntime() { private void createUIComponents() { // TODO: place custom component creation code here this.selectorApplication = new AzureArtifactComboBox(project, true); + this.selectorRuntime = new RuntimeComboBox(); } public void setDeploymentVisible(boolean visible) { diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/serviceplan/ServicePlanCreationDialog.form b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/serviceplan/ServicePlanCreationDialog.form index 3a772c8ab76..1e7c6b7a310 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/serviceplan/ServicePlanCreationDialog.form +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/serviceplan/ServicePlanCreationDialog.form @@ -27,7 +27,7 @@ - + @@ -35,10 +35,11 @@ - + + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/serviceplan/ServicePlanCreationDialog.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/serviceplan/ServicePlanCreationDialog.java index 97a8dd0e955..d75b3b73d7e 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/serviceplan/ServicePlanCreationDialog.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/serviceplan/ServicePlanCreationDialog.java @@ -104,4 +104,9 @@ public List> getInputs() { return Collections.singletonList(this.textName); } + private void createUIComponents() { + // TODO: place custom component creation code here + this.textName = new AzureTextInput(); + this.comboBoxPricingTier = new PricingTierComboBox(); + } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/component/resourcegroup/ResourceGroupCreationDialog.form b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/component/resourcegroup/ResourceGroupCreationDialog.form index 0ce735be729..2ad8da67365 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/component/resourcegroup/ResourceGroupCreationDialog.form +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/component/resourcegroup/ResourceGroupCreationDialog.form @@ -19,7 +19,7 @@ - + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/component/resourcegroup/ResourceGroupCreationDialog.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/component/resourcegroup/ResourceGroupCreationDialog.java index 02ae158c5a1..5bb9d6b4d64 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/component/resourcegroup/ResourceGroupCreationDialog.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/component/resourcegroup/ResourceGroupCreationDialog.java @@ -69,4 +69,9 @@ public void setValue(final ResourceGroupDraft data) { public List> getInputs() { return Collections.singletonList(this.textName); } + + private void createUIComponents() { + // TODO: place custom component creation code here + this.textName = new ResourceGroupNameTextField(); + } } From 2cd176bc68f03a0a60f7f539ee765cc7172cb1cc Mon Sep 17 00:00:00 2001 From: Hanxiao Liu Date: Thu, 30 Oct 2025 19:57:49 +0800 Subject: [PATCH 03/43] Update sign pipeline --- .azure-pipelines/sign-for-stable-release.yml | 165 +++++++++---------- 1 file changed, 76 insertions(+), 89 deletions(-) diff --git a/.azure-pipelines/sign-for-stable-release.yml b/.azure-pipelines/sign-for-stable-release.yml index 8dcd3b607ea..7acb8f244b1 100644 --- a/.azure-pipelines/sign-for-stable-release.yml +++ b/.azure-pipelines/sign-for-stable-release.yml @@ -6,34 +6,34 @@ resources: repositories: - repository: self type: git - ref: refs/heads/wangmi/1es - - repository: 1esPipelines + ref: refs/heads/main + - repository: MicroBuildTemplate type: git - name: 1ESPipelineTemplates/1ESPipelineTemplates - ref: refs/tags/release + name: 1ESPipelineTemplates/MicroBuildTemplate trigger: none extends: - template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines + template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate parameters: pool: - name: 1ES_JavaTooling_Pool - image: 1ES_JavaTooling_Ubuntu-2004 - os: linux - timeoutInMinutes: 360 - sdl: - sourceAnalysisPool: - name: 1ES_JavaTooling_Pool - image: 1ES_JavaTooling_Windows_2022 - os: windows - customBuildTags: - - MigrationTooling-mseng-VSJava-10780-Tool + name: VSEngSS-MicroBuild2022-1ES + os: windows # allowed values: windows, linux, or macOS. The default value is windows. + featureFlags: + disableNetworkIsolation: true stages: - stage: Stage jobs: - - job: Job_1 - displayName: Build and Sign Azure Plugin for IntelliJ + - job: Build_and_Sign + displayName: Build and Sign Plugin timeoutInMinutes: 360 templateContext: + mb: + signing: + enabled: true + signType: real + signWithProd: true + zipSources: false + useEsrpCli: true + feedSource: 'https://mseng.pkgs.visualstudio.com/DefaultCollection/_packaging/MicroBuildToolset/nuget/v3/index.json' outputs: - output: pipelineArtifact artifactName: drop @@ -41,98 +41,85 @@ extends: displayName: "Publish Artifact: drop" steps: - checkout: self + clean: true fetchTags: false - - task: Bash@3 - displayName: Prepare JDK environment + - task: PowerShell@2 + displayName: Install JDK 21 inputs: targetType: inline script: | - # Create a directory for the JDK - mkdir -p $HOME/java - cd $HOME/java - - # Download Adoptium/Temurin JDK 17.0.7 - echo "Downloading Java 17.0.7..." - wget -q https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.7%2B7/OpenJDK17U-jdk_x64_linux_hotspot_17.0.7_7.tar.gz - - # Extract the archive - echo "Extracting Java 17.0.7..." - tar -xzf OpenJDK17U-jdk_x64_linux_hotspot_17.0.7_7.tar.gz - rm OpenJDK17U-jdk_x64_linux_hotspot_17.0.7_7.tar.gz - - # Set JAVA_HOME and add to PATH - export JAVA_HOME=$HOME/java/jdk-17.0.7+7 - export PATH=$JAVA_HOME/bin:$PATH - - echo "##vso[task.setvariable variable=JAVA_HOME]$JAVA_HOME" - echo "##vso[task.setvariable variable=PATH]$PATH" - - task: Bash@3 - displayName: Build Utils + # Download Adoptium/Temurin JDK 21.0.8 + Write-Host "Downloading Java 21.0.8..." + $source = "https://download.oracle.com/java/21/archive/jdk-21.0.2_windows-x64_bin.zip" + $destination = "$(build.sourcesdirectory)\jdk21_x64_windows.zip" + Invoke-WebRequest -Uri $source -OutFile $destination + - task: JavaToolInstaller@0 + displayName: 'Use Java 21' + inputs: + versionSpec: 21 + jdkArchitectureOption: x64 + jdkSourceOption: LocalDirectory + jdkFile: '$(build.sourcesdirectory)\jdk21_x64_windows.zip' + jdkDestinationDirectory: '$(agent.toolsDirectory)/jdk21' + - task: PowerShell@2 + displayName: Build Plugin inputs: targetType: inline - script: |- + script: | mvn -v # ./gradlew buildUtils || exit -1 - mvn clean install -f ./Utils/pom.xml -T 1C -Dcheckstyle.skip=true -Dmaven.test.skip=true -Dmaven.javadoc.skip=true + mvn clean install -f ./Utils/pom.xml -T 1C "-Dcheckstyle.skip=true" "-Dmaven.test.skip=true" "-Dmaven.javadoc.skip=true" mvn clean -f ./Utils/pom.xml - - task: Bash@3 - displayName: Build Plugin + cd PluginsAndFeatures/azure-toolkit-for-intellij + ./gradlew clean buildPlugin -s "-Papplicationinsights.key=$(INTELLIJ_KEY)" "-PneedPatchVersion=false" "-Psources=false" "-Porg.gradle.configureondemand=false" "-Porg.gradle.daemon=false" "-Porg.gradle.unsafe.configuration-cache=false" "-Porg.gradle.caching=false" + - task: PowerShell@2 + displayName: Unpackage inputs: targetType: inline script: | - (cd PluginsAndFeatures/azure-toolkit-for-intellij && ./gradlew clean buildPlugin -s -Papplicationinsights.key=$(INTELLIJ_KEY) -PneedPatchVersion=false -Psources=false -Porg.gradle.configureondemand=false -Porg.gradle.daemon=false -Porg.gradle.unsafe.configuration-cache=false -Porg.gradle.caching=false) - - mkdir -p ./artifacts/intellij/ - cp ./PluginsAndFeatures/azure-toolkit-for-intellij/build/distributions/*.zip ./artifacts/intellij/azure-toolkit-for-intellij.zip - unzip ./artifacts/intellij/azure-toolkit-for-intellij.zip -d ./artifacts/intellij/folder - rm ./artifacts/intellij/azure-toolkit-for-intellij.zip - - task: UsePythonVersion@0 - displayName: 'Use Python 3.11.x' - inputs: - versionSpec: 3.11.x - - task: UseDotNet@2 - displayName: 'Use .NET Core 3.1.x' - inputs: - packageType: 'sdk' - version: '3.1.x' - - task: MicroBuildSigningPlugin@4 - displayName: 'Install Signing Plugin' - inputs: - signType: real - azureSubscription: 'MicroBuild Signing Task (MSEng)' - feedSource: 'https://mseng.pkgs.visualstudio.com/DefaultCollection/_packaging/MicroBuildToolset/nuget/v3/index.json' - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - - task: CmdLine@2 + New-Item -ItemType Directory -Path ".\artifacts\intellij\" -Force + $zipFile = Get-ChildItem ".\PluginsAndFeatures\azure-toolkit-for-intellij\build\distributions\*.zip" | Select-Object -First 1 + Copy-Item $zipFile.FullName ".\artifacts\intellij\azure-toolkit-for-intellij.zip" + Expand-Archive -Path ".\artifacts\intellij\azure-toolkit-for-intellij.zip" -DestinationPath ".\artifacts\intellij\folder" -Force + Remove-Item ".\artifacts\intellij\azure-toolkit-for-intellij.zip" + - task: PowerShell@2 displayName: Sign jars timeoutInMinutes: 240 inputs: + targetType: inline script: | - ABSOLUTE_PATH="$(pwd)" + $ABSOLUTE_PATH = Get-Location # Generate the signing-filelist.xml file - echo '' > signing-filelist.xml - echo '' >> signing-filelist.xml - echo ' ' >> signing-filelist.xml - - find . -type f -name "azure-intellij-*.jar" -o -name "azure-toolkit-ide-*.jar" -o -name "azuretools-core-*.jar" -o -name "azure-explorer-common-*.jar" -o -name "hdinsight-node-common-*.jar" -o -name "azure-sdk-reference-book.jar" | while read -r file; do - clean_file="${file#./}" - abs_path="$ABSOLUTE_PATH/$clean_file" - echo " " >> signing-filelist.xml - done - - echo ' ' >> signing-filelist.xml - echo '' >> signing-filelist.xml - - dotnet "$MBSIGN_APPFOLDER/DDSignFiles.dll" -- /filelist:signing-filelist.xml - rm signing-filelist.xml - workingDirectory: 'artifacts/intellij/folder/azure-toolkit-for-intellij/lib' - - task: Bash@3 + '' | Out-File -FilePath signing-filelist.xml -Encoding utf8 + '' | Out-File -FilePath signing-filelist.xml -Append -Encoding utf8 + ' ' | Out-File -FilePath signing-filelist.xml -Append -Encoding utf8 + + Get-ChildItem -Path . -Recurse -File | Where-Object { + $_.Name -like "azure-intellij-*.jar" -or + $_.Name -like "azure-toolkit-for-intellij*.jar" -or + $_.Name -like "azure-toolkit-ide-*.jar" -or + $_.Name -like "azuretools-core-*.jar" -or + $_.Name -like "azure-explorer-common-*.jar" -or + $_.Name -like "hdinsight-node-common-*.jar" -or + $_.Name -eq "azure-sdk-reference-book.jar" + } | ForEach-Object { + $abs_path = $_.FullName + " " | Out-File -FilePath signing-filelist.xml -Append -Encoding utf8 + } + + ' ' | Out-File -FilePath signing-filelist.xml -Append -Encoding utf8 + '' | Out-File -FilePath signing-filelist.xml -Append -Encoding utf8 + + dotnet "$env:MBSIGN_APPFOLDER/DDSignFiles.dll" -- /filelist:signing-filelist.xml + Remove-Item signing-filelist.xml + workingDirectory: 'artifacts/intellij/folder' + - task: PowerShell@2 displayName: Repackage inputs: targetType: inline script: | - # Write your commands here - (cd ./artifacts/intellij/folder && zip -r ../../azure-toolkit-for-intellij-$(Build.BuildNumber).zip ./azure-toolkit-for-intellij/) + Set-Location ".\artifacts\intellij\folder" + Compress-Archive -Path ".\*" -DestinationPath "..\..\azure-toolkit-for-intellij.zip" -Force - task: CopyFiles@2 displayName: "Copy Files to: $(build.artifactstagingdirectory)" inputs: From a4fcb967483da82e11fdc928b74224bc85021ae1 Mon Sep 17 00:00:00 2001 From: Hanxiao Liu Date: Mon, 3 Nov 2025 14:10:38 +0800 Subject: [PATCH 04/43] Remove 'setRequired' from form which may break the UX in eap --- .../legacy/appservice/AppServiceInfoBasicPanel.form | 6 ++---- .../legacy/appservice/AppServiceInfoBasicPanel.java | 2 ++ .../serviceplan/ServicePlanCreationDialog.form | 3 --- .../serviceplan/ServicePlanCreationDialog.java | 1 + .../resourcegroup/ResourceGroupCreationDialog.form | 3 --- .../resourcegroup/ResourceGroupCreationDialog.java | 1 + .../connector/aad/RegisterAzureApplicationForm.form | 13 +++++-------- .../connector/aad/RegisterAzureApplicationForm.java | 5 +++++ 8 files changed, 16 insertions(+), 18 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice-java/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/AppServiceInfoBasicPanel.form b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice-java/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/AppServiceInfoBasicPanel.form index 958336737b0..b13aea0c833 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice-java/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/AppServiceInfoBasicPanel.form +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice-java/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/AppServiceInfoBasicPanel.form @@ -27,13 +27,11 @@ - + - - - + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice-java/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/AppServiceInfoBasicPanel.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice-java/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/AppServiceInfoBasicPanel.java index 8f6a876ebe3..48c26570d4b 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice-java/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/AppServiceInfoBasicPanel.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice-java/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/AppServiceInfoBasicPanel.java @@ -136,6 +136,8 @@ private void createUIComponents() { // TODO: place custom component creation code here this.selectorApplication = new AzureArtifactComboBox(project, true); this.selectorRuntime = new RuntimeComboBox(); + this.textName = new AppNameInput(); + this.textName.setRequired(true); } public void setDeploymentVisible(boolean visible) { diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/serviceplan/ServicePlanCreationDialog.form b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/serviceplan/ServicePlanCreationDialog.form index 1e7c6b7a310..4c2b43aca35 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/serviceplan/ServicePlanCreationDialog.form +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/serviceplan/ServicePlanCreationDialog.form @@ -31,9 +31,6 @@ - - - diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/serviceplan/ServicePlanCreationDialog.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/serviceplan/ServicePlanCreationDialog.java index d75b3b73d7e..2261fd9d027 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/serviceplan/ServicePlanCreationDialog.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appservice/src/main/java/com/microsoft/azure/toolkit/intellij/legacy/appservice/serviceplan/ServicePlanCreationDialog.java @@ -107,6 +107,7 @@ public List> getInputs() { private void createUIComponents() { // TODO: place custom component creation code here this.textName = new AzureTextInput(); + this.textName.setRequired(true); this.comboBoxPricingTier = new PricingTierComboBox(); } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/component/resourcegroup/ResourceGroupCreationDialog.form b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/component/resourcegroup/ResourceGroupCreationDialog.form index 2ad8da67365..cf40a38a6ee 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/component/resourcegroup/ResourceGroupCreationDialog.form +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/component/resourcegroup/ResourceGroupCreationDialog.form @@ -23,9 +23,6 @@ - - - diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/component/resourcegroup/ResourceGroupCreationDialog.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/component/resourcegroup/ResourceGroupCreationDialog.java index 5bb9d6b4d64..f001d7eb2c3 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/component/resourcegroup/ResourceGroupCreationDialog.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/component/resourcegroup/ResourceGroupCreationDialog.java @@ -73,5 +73,6 @@ public List> getInputs() { private void createUIComponents() { // TODO: place custom component creation code here this.textName = new ResourceGroupNameTextField(); + this.textName.setRequired(true); } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-aad/src/main/java/com/microsoft/azure/toolkit/intellij/connector/aad/RegisterAzureApplicationForm.form b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-aad/src/main/java/com/microsoft/azure/toolkit/intellij/connector/aad/RegisterAzureApplicationForm.form index 5b51800330e..ca18966ace6 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-aad/src/main/java/com/microsoft/azure/toolkit/intellij/connector/aad/RegisterAzureApplicationForm.form +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-aad/src/main/java/com/microsoft/azure/toolkit/intellij/connector/aad/RegisterAzureApplicationForm.form @@ -94,21 +94,17 @@ - + - - - + - + - - - + @@ -122,6 +118,7 @@ + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-aad/src/main/java/com/microsoft/azure/toolkit/intellij/connector/aad/RegisterAzureApplicationForm.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-aad/src/main/java/com/microsoft/azure/toolkit/intellij/connector/aad/RegisterAzureApplicationForm.java index 73dbbae1be6..8bef9ff4d6f 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-aad/src/main/java/com/microsoft/azure/toolkit/intellij/connector/aad/RegisterAzureApplicationForm.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-aad/src/main/java/com/microsoft/azure/toolkit/intellij/connector/aad/RegisterAzureApplicationForm.java @@ -119,6 +119,11 @@ private void createUIComponents() { clientIdInput = new AzureClientIdInput(); advancedSettingsSeparator = new AzureHideableTitledSeparator(); + + displayNameInput = new AzureTextInput(); + displayNameInput.setRequired(true); + domainInput = new AzureTextInput(); + domainInput.setRequired(true); } private void updateRegistrationModel(@Nonnull Project project, @Nonnull Subscription subscription) { From 57adfd55ca67ddec37b6c1bbf7a1a4e86ff968bf Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Tue, 20 Jan 2026 14:47:53 +0800 Subject: [PATCH 05/43] Add App Modernization support with Migrate to Azure feature --- .../.idea/gradle.xml | 4 + .../build.gradle.kts | 5 + .../appmod/IMigrateChildNodeProvider.java | 80 ++++ .../intellij/appmod/InstallPluginDialog.java | 82 ++++ .../intellij/appmod/MigrateNodeData.java | 354 ++++++++++++++++++ .../appmod/MigratePluginInstaller.java | 157 ++++++++ .../intellij/appmod/MigrateToAzureAction.java | 273 ++++++++++++++ .../intellij/appmod/MigrateToAzureNode.java | 125 +++++++ .../intellij/appmod/RestartIdeDialog.java | 82 ++++ .../META-INF/azure-intellij-plugin-appmod.xml | 11 + .../src/main/resources/icons/app_mod.svg | 22 ++ .../src/main/resources/META-INF/plugin.xml | 1 + .../build.gradle.kts | 2 + .../intellij/explorer/AzureExplorer.java | 26 ++ .../build.gradle.kts | 2 + .../projectexplorer/AzureFacetRootNode.java | 8 +- .../MigrateToAzureFacetNode.java | 174 +++++++++ .../gradle.properties | 4 +- .../settings.gradle.kts | 1 + .../src/main/resources/META-INF/plugin.xml | 3 + 20 files changed, 1412 insertions(+), 4 deletions(-) create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/build.gradle.kts create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateChildNodeProvider.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/InstallPluginDialog.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/RestartIdeDialog.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/app_mod.svg create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/gradle.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/gradle.xml index 25aace38038..d604fd81745 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/gradle.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/gradle.xml @@ -14,6 +14,7 @@ + + \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/build.gradle.kts b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/build.gradle.kts new file mode 100644 index 00000000000..2bcc6e6fb46 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/build.gradle.kts @@ -0,0 +1,5 @@ +dependencies { + implementation(project(":azure-intellij-plugin-lib")) + // runtimeOnly project(path: ":azure-intellij-plugin-lib", configuration: "instrumentedJar") + implementation("com.microsoft.azure:azure-toolkit-ide-common-lib") +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateChildNodeProvider.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateChildNodeProvider.java new file mode 100644 index 00000000000..88f4a456c27 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateChildNodeProvider.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod; + +import com.intellij.openapi.project.Project; + +import javax.annotation.Nonnull; +import java.util.List; + +/** + * Extension point interface for providing migration nodes. + * + * Implementations of this interface will be discovered via IntelliJ's extension point mechanism + * and used to dynamically construct the migration options tree in: + * - Service Explorer (MigrateToAzureNode) + * - Context Menu (MigrateToAzureAction) + * - Project Explorer (MigrateToAzureFacetNode) + * + * Example implementation: + *
+ * public class MyMigrationProvider implements IMigrateChildNodeProvider {
+ *     @Override
+ *     public List<MigrateNodeData> createNodeData(@Nonnull Project project) {
+ *         return List.of(
+ *             MigrateNodeData.builder("My Migration Option")
+ *                 .iconPath("/icons/my_icon.svg")
+ *                 .onClick(() -> performMigration(project))
+ *                 .build()
+ *         );
+ *     }
+ * }
+ * 
+ * + * Registration in plugin.xml: + *
+ * <extensions defaultExtensionNs="com.microsoft.tooling.msservices.intellij.azure">
+ *     <migrateChildNodeProvider implementation="your.package.MyMigrationProvider"/>
+ * </extensions>
+ * 
+ */ +public interface IMigrateChildNodeProvider { + + /** + * Creates migration node data for the Migrate to Azure section. + * + * This method is called each time the migration menu/tree is constructed. + * The returned list can contain multiple MigrateNodeData instances, + * each representing a single action or a group of options. + * + * @param project The current IntelliJ project + * @return A list of MigrateNodeData instances representing the migration option(s) + */ + @Nonnull + List createNodeData(@Nonnull Project project); + + /** + * Returns the priority/order of this node provider. + * Nodes will be displayed in ascending order of priority. + * Lower numbers appear first. + * + * @return Priority value (default: 100) + */ + default int getPriority() { + return 100; + } + + /** + * Determines whether this provider should create a node. + * Can be used to conditionally show/hide migration options based on context. + * + * @param project The current IntelliJ project + * @return true if this provider should contribute a node, false otherwise + */ + default boolean isApplicable(@Nonnull Project project) { + return true; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/InstallPluginDialog.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/InstallPluginDialog.java new file mode 100644 index 00000000000..9eaa3615e2d --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/InstallPluginDialog.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.DialogWrapper; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.util.Objects; + +/** + * Dialog for confirming plugin installation. + * Similar to ConfirmAndRunDialog used in AzdNode. + */ +public class InstallPluginDialog extends DialogWrapper { + + private final Project project; + private String label; + private Runnable onOkAction; + + public InstallPluginDialog(Project project, String title) { + super(project, true); + this.project = project; + setSize(400, 150); + setTitle(Objects.requireNonNull(title, "Title must not be null")); + setOKButtonText("Install"); + } + + public InstallPluginDialog setLabel(String label) { + this.label = Objects.requireNonNull(label, "Label must not be null"); + return this; + } + + public InstallPluginDialog setOnOkAction(Runnable onOkAction) { + this.onOkAction = onOkAction; + return this; + } + + @Override + public void show() { + init(); + super.show(); + } + + @Override + protected @Nullable JComponent createCenterPanel() { + JPanel panel = new JPanel(new BorderLayout()); + panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + // Support HTML for multi-line labels + JLabel labelComponent; + if (label != null && label.contains("\n")) { + // Convert newlines to HTML breaks + String htmlLabel = "" + label.replace("\n", "
") + ""; + labelComponent = new JLabel(htmlLabel); + } else { + labelComponent = new JLabel(label); + } + labelComponent.setHorizontalAlignment(SwingConstants.CENTER); + panel.add(labelComponent, BorderLayout.CENTER); + return panel; + } + + @Override + protected Action @NotNull [] createActions() { + return new Action[]{getOKAction(), getCancelAction()}; + } + + @Override + protected void doOKAction() { + super.doOKAction(); + if (onOkAction != null) { + onOkAction.run(); + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java new file mode 100644 index 00000000000..a13f18a1df3 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java @@ -0,0 +1,354 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod; + +import lombok.Getter; +import lombok.Setter; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Unified data structure for migration nodes. + * This class is used by MigrateToAzureNode, MigrateToAzureAction, and MigrateToAzureFacetNode. + * + * Features: + * - Basic properties: label, icon, description, tooltip + * - Click handling for both leaf and parent nodes + * - Static children or lazy-loaded children via childrenLoader + * + * Lazy Loading: + * Provider can declare lazy loading intent via childrenLoader. Each consumer handles it based on capability: + * - Service Explorer: Supports lazy loading → uses Node.withChildrenLoadLazily(true) + * - Project Explorer: Native lazy loading → buildChildren() calls loader + * - Action menus: No lazy loading → calls loader.get() immediately + * + * Usage examples: + * + * 1. Simple leaf node with click action: + *
+ * MigrateNodeData.builder("Option A")
+ *     .iconPath("/icons/my_icon.svg")
+ *     .onClick(e -> doSomething())
+ *     .build();
+ * 
+ * + * 2. Parent node with static children: + *
+ * MigrateNodeData.builder("Parent")
+ *     .addChild(childNode1)
+ *     .addChild(childNode2)
+ *     .build();
+ * 
+ * + * 3. Parent node with lazy-loaded children: + *
+ * MigrateNodeData.builder("Lazy Parent")
+ *     .childrenLoader(() -> loadChildrenFromServer())
+ *     .build();
+ * 
+ */ +@Getter +public class MigrateNodeData { + + // ==================== Basic Properties ==================== + + /** + * Display label for the node. + */ + @Nonnull + private final String label; + + /** + * Icon path for the node (e.g., "/icons/app_mod.svg"). If null, a default icon will be used. + */ + @Nullable + private String iconPath; + + /** + * Description text (shown as secondary text in some views). + */ + @Nullable + private String description; + + /** + * Tooltip text (shown on hover). + */ + @Nullable + private String tooltip; + + // ==================== State ==================== + + /** + * Whether the node is enabled (clickable). + */ + @Setter + private boolean enabled = true; + + /** + * Whether the node is visible. + */ + @Setter + private boolean visible = true; + + // ==================== Click Handling ==================== + + /** + * Click handler. Can be set on any node (leaf or parent). + * For parent nodes in menus, this may be triggered by a specific action. + */ + @Nullable + private Consumer clickHandler; + + /** + * Double-click handler. Useful for tree views where single-click selects + * and double-click performs action. + */ + @Nullable + private Consumer doubleClickHandler; + + // ==================== Children ==================== + + /** + * Static list of child nodes. + */ + @Nonnull + private final List children = new ArrayList<>(); + + /** + * Lazy loader for children. If set, consumers that support lazy loading + * will use this to load children on demand. Consumers without lazy loading + * support will call this immediately. + * + * Note: This is just a declaration of intent. MigrateNodeData does NOT + * manage loading state - each consumer handles that according to its capability. + */ + @Nullable + private Supplier> childrenLoader; + + // ==================== Constructor ==================== + + private MigrateNodeData(@Nonnull String label) { + this.label = label; + } + + // ==================== Builder ==================== + + /** + * Creates a new builder for MigrateNodeData. + * + * @param label The display label for the node + * @return A new builder instance + */ + public static Builder builder(@Nonnull String label) { + return new Builder(label); + } + + // ==================== Methods ==================== + + /** + * Checks if this node has children (static or via loader). + */ + public boolean hasChildren() { + return !children.isEmpty() || childrenLoader != null; + } + + /** + * Gets the static children list. + * For lazy-loaded children, use getChildrenLoader() instead. + */ + @Nonnull + public List getChildren() { + return children; + } + + /** + * Gets the children loader for lazy loading. + * Consumers should check this and handle according to their capability. + */ + @Nullable + public Supplier> getChildrenLoader() { + return childrenLoader; + } + + /** + * Checks if this node uses lazy loading for children. + */ + public boolean isLazyLoading() { + return childrenLoader != null; + } + + /** + * Triggers the click handler. + * + * @param event The event object (can be AnActionEvent, MouseEvent, or null) + */ + public void click(@Nullable Object event) { + if (clickHandler != null && enabled) { + clickHandler.accept(event); + } + } + + /** + * Triggers the double-click handler. + * Falls back to click handler if double-click is not set. + * + * @param event The event object + */ + public void doubleClick(@Nullable Object event) { + if (doubleClickHandler != null && enabled) { + doubleClickHandler.accept(event); + } else { + click(event); + } + } + + /** + * Checks if this node has a click handler. + */ + public boolean hasClickHandler() { + return clickHandler != null; + } + + /** + * Adds a child node dynamically. + */ + public void addChild(@Nonnull MigrateNodeData child) { + children.add(child); + } + + /** + * Removes a child node. + */ + public void removeChild(@Nonnull MigrateNodeData child) { + children.remove(child); + } + + // ==================== Builder Class ==================== + + public static class Builder { + private final MigrateNodeData data; + + private Builder(@Nonnull String label) { + this.data = new MigrateNodeData(label); + } + + /** + * Sets the icon path. + */ + public Builder iconPath(@Nullable String iconPath) { + data.iconPath = iconPath; + return this; + } + + /** + * Sets the description. + */ + public Builder description(@Nullable String description) { + data.description = description; + return this; + } + + /** + * Sets the tooltip. + */ + public Builder tooltip(@Nullable String tooltip) { + data.tooltip = tooltip; + return this; + } + + /** + * Sets the enabled state. + */ + public Builder enabled(boolean enabled) { + data.enabled = enabled; + return this; + } + + /** + * Sets the visible state. + */ + public Builder visible(boolean visible) { + data.visible = visible; + return this; + } + + /** + * Sets the click handler. + */ + public Builder onClick(@Nullable Consumer handler) { + data.clickHandler = handler; + return this; + } + + /** + * Sets the click handler (no-arg version). + */ + public Builder onClick(@Nullable Runnable handler) { + if (handler != null) { + data.clickHandler = e -> handler.run(); + } + return this; + } + + /** + * Sets the double-click handler. + */ + public Builder onDoubleClick(@Nullable Consumer handler) { + data.doubleClickHandler = handler; + return this; + } + + /** + * Sets the double-click handler (no-arg version). + */ + public Builder onDoubleClick(@Nullable Runnable handler) { + if (handler != null) { + data.doubleClickHandler = e -> handler.run(); + } + return this; + } + + /** + * Adds a child node. + */ + public Builder addChild(@Nonnull MigrateNodeData child) { + data.children.add(child); + return this; + } + + /** + * Adds multiple child nodes. + */ + public Builder addChildren(@Nonnull List children) { + data.children.addAll(children); + return this; + } + + /** + * Sets the children loader for lazy loading. + * Consumers that support lazy loading will use this to load children on demand. + * Consumers without lazy loading support will call this immediately. + * + * @param loader Supplier that returns the list of child nodes + */ + public Builder childrenLoader(@Nullable Supplier> loader) { + data.childrenLoader = loader; + return this; + } + + /** + * Builds the MigrateNodeData instance. + */ + public MigrateNodeData build() { + return data; + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java new file mode 100644 index 00000000000..37be801300a --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod; + +import com.intellij.ide.plugins.IdeaPluginDescriptor; +import com.intellij.ide.plugins.PluginManagerCore; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.extensions.PluginId; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.updateSettings.impl.pluginsAdvertisement.PluginsAdvertiser; +import com.microsoft.azure.toolkit.lib.common.event.AzureEventBus; +import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager; + +import javax.annotation.Nonnull; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Utility class for managing App Modernization plugin installation. + * This centralizes all plugin detection and installation logic to avoid code duplication + * between MigrateToAzureNode and MigrateToAzureAction. + */ +public class MigratePluginInstaller { + private static final String PLUGIN_ID = "com.github.copilot.appmod"; + private static final String COPILOT_PLUGIN_ID = "com.github.copilot"; + + private MigratePluginInstaller() { + // Utility class - prevent instantiation + } + + /** + * Checks if the App Modernization plugin is installed and enabled. + */ + public static boolean isAppModPluginInstalled() { + try { + final PluginId pluginId = PluginId.getId(PLUGIN_ID); + final IdeaPluginDescriptor plugin = PluginManagerCore.getPlugin(pluginId); + return plugin != null && plugin.isEnabled(); + } catch (Exception e) { + return false; + } + } + + /** + * Checks if GitHub Copilot plugin is installed and enabled. + */ + public static boolean isCopilotInstalled() { + try { + final PluginId pluginId = PluginId.getId(COPILOT_PLUGIN_ID); + final IdeaPluginDescriptor plugin = PluginManagerCore.getPlugin(pluginId); + return plugin != null && plugin.isEnabled(); + } catch (Exception e) { + return false; + } + } + + /** + * Detects if running in development mode (runIde task). + * This helps determine whether to show restart options or dev-mode instructions. + */ + public static boolean isRunningInDevMode() { + try { + final PluginId azureToolkitId = PluginId.getId("com.microsoft.tooling.msservices.intellij.azure"); + final IdeaPluginDescriptor descriptor = PluginManagerCore.getPlugin(azureToolkitId); + if (descriptor != null) { + final String path = descriptor.getPluginPath().toString(); + return path.contains("build") || path.contains("sandbox") || path.contains("out"); + } + } catch (Exception ex) { + // If we can't determine, assume production mode (safer) + } + return false; + } + + /** + * Shows a confirmation dialog for plugin installation. + * Uses a modal dialog similar to AzdNode's ConfirmAndRunDialog. + * + * @param project The current project + * @param onConfirm Callback to execute when user confirms installation + */ + public static void showInstallConfirmation(@Nonnull Project project, @Nonnull Runnable onConfirm) { + final boolean copilotInstalled = isCopilotInstalled(); + + final String title = copilotInstalled + ? "Install GitHub Copilot App Modernization" + : "Install GitHub Copilot and GitHub Copilot App Modernization"; + + final String message = copilotInstalled + ? "Do you want to install GitHub Copilot App Modernization plugin?" + : "Do you want to install GitHub Copilot and GitHub Copilot App Modernization plugins?"; + + new InstallPluginDialog(project, title) + .setLabel(message) + .setOnOkAction(onConfirm) + .show(); + } + + /** + * Installs the App Modernization plugin (and GitHub Copilot if needed). + * IntelliJ platform will handle the restart prompt after installation. + * In dev mode, shows instructions to manually restart runIde task instead. + * + * @param project The current project + */ + public static void installPlugin(@Nonnull Project project) { + final boolean copilotInstalled = isCopilotInstalled(); + final boolean appModInstalled = isAppModPluginInstalled(); + final boolean isDevMode = isRunningInDevMode(); + + // Build plugin ID set - only include plugins that are NOT already installed + final Set pluginsToInstall = new LinkedHashSet<>(); + if (!copilotInstalled) { + pluginsToInstall.add(PluginId.getId(COPILOT_PLUGIN_ID)); + } + if (!appModInstalled) { + pluginsToInstall.add(PluginId.getId(PLUGIN_ID)); + } + + // If all plugins are already installed, nothing to do + if (pluginsToInstall.isEmpty()) { + return; + } + + // Use PluginsAdvertiser.installAndEnable - IntelliJ handles the rest + // The platform will show plugin selection dialog, download, install, and prompt for restart + AzureTaskManager.getInstance().runAndWait(() -> { + PluginsAdvertiser.installAndEnable( + project, + pluginsToInstall, + true, // showDialog + true, // selectAllInDialog - pre-select all plugins + null, // modalityState + () -> { + // Called after user confirms installation + if (isDevMode) { + // Dev mode: Show special instructions + ApplicationManager.getApplication().invokeLater(() -> { + final String message = "Plugins are being installed.\n\n" + + "⚠️ DEVELOPMENT MODE:\n" + + "After installation completes, do NOT restart from this IDE window!\n" + + "Instead, stop your current runIde task and relaunch ./gradlew runIde"; + new RestartIdeDialog(project, "Development Mode Notice", message) + .setShowRestartOption(false) + .show(); + }); + } + // Emit event + AzureEventBus.emit("migrate.plugin.installed"); + } + ); + }); + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java new file mode 100644 index 00000000000..cee7bd27b8d --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java @@ -0,0 +1,273 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod; + +import com.intellij.openapi.actionSystem.ActionGroup; +import com.intellij.openapi.actionSystem.ActionUpdateThread; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.DefaultActionGroup; +import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.openapi.project.Project; +import com.microsoft.azure.toolkit.intellij.common.IntelliJAzureIcons; +import com.microsoft.azure.toolkit.lib.common.operation.AzureOperation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Action group that dynamically shows migration options as sub-menu. + * Uses the same extension point as MigrateToAzureNode for consistency. + */ +public class MigrateToAzureAction extends ActionGroup { + private static final ExtensionPointName migrationProviders = + ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateChildNodeProvider"); + + private static final String APP_MOD_ICON_PATH = "/icons/app_mod.svg"; + + public MigrateToAzureAction() { + // Set popup=true to show this as a root menu item with expandable sub-menu (▶) + // Without this, children would be displayed directly at root level + super("Migrate to Azure", true); + } + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + // Run on background thread to avoid blocking EDT when loading migration options + return ActionUpdateThread.BGT; + } + + @Override + public void update(@NotNull AnActionEvent e) { + super.update(e); + final Project project = e.getProject(); + + if (MigratePluginInstaller.isAppModPluginInstalled()) { + e.getPresentation().setText("Migrate to Azure"); + // Only show sub-menu if there are migration options available + e.getPresentation().setEnabledAndVisible(project != null && hasMigrationOptions(project)); + } else { + final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); + final String text = copilotInstalled + ? "Migrate to Azure (Install App Modernization)" + : "Migrate to Azure (Install Copilot & App Modernization)"; + e.getPresentation().setText(text); + e.getPresentation().setEnabledAndVisible(true); + } + } + + @Override + public AnAction @NotNull [] getChildren(@Nullable AnActionEvent e) { + if (e == null) { + return AnAction.EMPTY_ARRAY; + } + + final Project project = e.getProject(); + if (project == null) { + return AnAction.EMPTY_ARRAY; + } + + // If plugin not installed, show install action + if (!MigratePluginInstaller.isAppModPluginInstalled()) { + final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); + final String actionText = copilotInstalled + ? "Install GitHub Copilot App Modernization" + : "Install GitHub Copilot and GitHub Copilot App Modernization"; + return new AnAction[]{ + new AnAction(actionText) { + @Override + public void actionPerformed(@NotNull AnActionEvent event) { + MigratePluginInstaller.showInstallConfirmation(project, () -> MigratePluginInstaller.installPlugin(project)); + } + } + }; + } + + // Load migration options from extension points + final List migrationNodes = loadMigrationNodes(project); + + if (migrationNodes.isEmpty()) { + return AnAction.EMPTY_ARRAY; + } + + // Convert nodes to actions + return convertNodesToActions(migrationNodes); + } + + /** + * Loads migration nodes from extension point providers. + */ + private List loadMigrationNodes(@Nonnull Project project) { + return migrationProviders.getExtensionList().stream() + .filter(provider -> provider.isApplicable(project)) + .sorted(Comparator.comparingInt(IMigrateChildNodeProvider::getPriority)) + .flatMap(provider -> provider.createNodeData(project).stream()) + .filter(MigrateNodeData::isVisible) + .collect(Collectors.toList()); + } + + /** + * Checks if there are any migration options available. + */ + private boolean hasMigrationOptions(@Nonnull Project project) { + return migrationProviders.getExtensionList().stream() + .anyMatch(provider -> provider.isApplicable(project)); + } + + /** + * Converts node tree to action tree for sub-menu display. + */ + private AnAction[] convertNodesToActions(List nodes) { + final List actions = new ArrayList<>(); + for (MigrateNodeData node : nodes) { + actions.add(convertNodeToAction(node)); + } + return actions.toArray(new AnAction[0]); + } + + /** + * Converts a single node (and its children) to an action. + */ + private AnAction convertNodeToAction(MigrateNodeData nodeData) { + if (nodeData.hasChildren()) { + // For lazy loading nodes, create a LazyActionGroup that loads children on demand + if (nodeData.isLazyLoading()) { + return new LazyActionGroup(nodeData); + } + + // Node with static children -> create sub-menu with children added immediately + final DefaultActionGroup subgroup = new DefaultActionGroup(); + subgroup.getTemplatePresentation().setText(nodeData.getLabel(), false); + subgroup.setPopup(true); + + // Use node's icon if available, otherwise use default app_mod icon + if (nodeData.getIconPath() != null) { + subgroup.getTemplatePresentation().setIcon(IntelliJAzureIcons.getIcon(nodeData.getIconPath())); + } else { + subgroup.getTemplatePresentation().setIcon(IntelliJAzureIcons.getIcon(APP_MOD_ICON_PATH)); + } + + // Add static children + for (MigrateNodeData child : nodeData.getChildren()) { + if (child.isVisible()) { + subgroup.add(convertNodeToAction(child)); + } + } + + return subgroup; + } else { + // Leaf node -> create clickable action + return new AnAction(nodeData.getLabel()) { + { + // Use node's icon if available, otherwise use default app_mod icon + if (nodeData.getIconPath() != null) { + getTemplatePresentation().setIcon(IntelliJAzureIcons.getIcon(nodeData.getIconPath())); + } else { + getTemplatePresentation().setIcon(IntelliJAzureIcons.getIcon(APP_MOD_ICON_PATH)); + } + if (nodeData.getDescription() != null) { + getTemplatePresentation().setDescription(nodeData.getDescription()); + } + } + + @Override + public void update(@NotNull AnActionEvent e) { + e.getPresentation().setEnabled(nodeData.isEnabled()); + } + + @Override + @AzureOperation(name = "user/common.migrate_to_azure.trigger_option") + public void actionPerformed(@NotNull AnActionEvent e) { + nodeData.click(e); + } + }; + } + } + + /** + * ActionGroup that loads children lazily when the submenu is expanded. + * This avoids blocking the UI when the parent menu is shown. + */ + private class LazyActionGroup extends ActionGroup { + private final MigrateNodeData nodeData; + private volatile AnAction[] cachedChildren; + private volatile boolean isLoading = false; + + // Loading placeholder action + private final AnAction loadingAction = new AnAction("Loading...") { + { + getTemplatePresentation().setEnabled(false); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + // No-op, this is just a placeholder + } + }; + + LazyActionGroup(MigrateNodeData nodeData) { + super(nodeData.getLabel(), true); + this.nodeData = nodeData; + + // Set icon + if (nodeData.getIconPath() != null) { + getTemplatePresentation().setIcon(IntelliJAzureIcons.getIcon(nodeData.getIconPath())); + } else { + getTemplatePresentation().setIcon(IntelliJAzureIcons.getIcon(APP_MOD_ICON_PATH)); + } + } + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + // Load children on background thread to avoid blocking EDT + return ActionUpdateThread.BGT; + } + + @Override + public AnAction @NotNull [] getChildren(@Nullable AnActionEvent e) { + if (cachedChildren != null) { + return cachedChildren; + } + + if (!isLoading) { + isLoading = true; + // Load children lazily + final List children = nodeData.getChildrenLoader().get(); + final List actions = new ArrayList<>(); + for (MigrateNodeData child : children) { + if (child.isVisible()) { + actions.add(convertNodeToAction(child)); + } + } + cachedChildren = actions.isEmpty() + ? new AnAction[]{ createNoOptionsAction() } + : actions.toArray(new AnAction[0]); + return cachedChildren; + } + + // Still loading, show placeholder + return new AnAction[]{ loadingAction }; + } + + private AnAction createNoOptionsAction() { + return new AnAction("No migration options available") { + { + getTemplatePresentation().setEnabled(false); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + // No-op + } + }; + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java new file mode 100644 index 00000000000..b6395a1ee62 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod; + +import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.openapi.project.Project; +import com.microsoft.azure.toolkit.ide.common.component.Node; +import com.microsoft.azure.toolkit.ide.common.icon.AzureIcon; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Service Explorer node for "Migrate to Azure" functionality. + * This node extends the azure-toolkit-ide-common-lib Node class to integrate with the Service Explorer tree. + */ +public final class MigrateToAzureNode extends Node { + private static final ExtensionPointName childProviders = + ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateChildNodeProvider"); + + private final Project project; + + private static final AzureIcon APP_MOD_ICON = AzureIcon.builder().iconPath("/icons/app_mod.svg").build(); + + public MigrateToAzureNode(Project project) { + super("Migrate to Azure"); + this.project = project; + withIcon(APP_MOD_ICON); + initializeNode(); + } + + public void initializeNode() { + if (MigratePluginInstaller.isAppModPluginInstalled()) { + showMigrationOptions(); + } else { + showNotInstalled(); + } + } + + private void showNotInstalled() { + final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); + + // Dynamic description based on what needs to be installed + final String description = copilotInstalled + ? "Install GitHub Copilot App Modernization" + : "Install GitHub Copilot and GitHub Copilot App Modernization"; + withDescription(description); + + onClicked(e -> { + MigratePluginInstaller.showInstallConfirmation(project, () -> MigratePluginInstaller.installPlugin(project)); + }); + } + + + public void showMigrationOptions() { + clearClickHandlers(); + withDescription(""); + + // Load migration options from extension points and convert to Node + final List nodeDataList = childProviders.getExtensionList().stream() + .filter(provider -> provider.isApplicable(project)) + .sorted(Comparator.comparingInt(IMigrateChildNodeProvider::getPriority)) + .flatMap(provider -> provider.createNodeData(project).stream()) + .collect(Collectors.toList()); + + // Convert MigrateNodeData to Node and add as children + nodeDataList.stream() + .map(this::convertToNode) + .forEach(this::addChild); + } + + /** + * Converts MigrateNodeData to Node for Service Explorer compatibility. + */ + private Node convertToNode(MigrateNodeData data) { + Node node = new Node<>(data); + + // Set basic properties + node.withLabel(d -> d.getLabel()); + // Use node's icon if available, otherwise use default app_mod icon + node.withIcon(d -> d.getIconPath() != null ? AzureIcon.builder().iconPath(d.getIconPath()).build() : APP_MOD_ICON); + if (data.getDescription() != null) { + node.withDescription(d -> d.getDescription()); + } + if (data.getTooltip() != null) { + node.withTips(d -> d.getTooltip()); + } + + // Set click handler + if (data.hasClickHandler()) { + node.onClicked(d -> data.click(null)); + } + + // Handle children - lazy or static + if (data.isLazyLoading()) { + // Lazy loading: use Node's native lazy loading mechanism + node.withChildrenLoadLazily(true); + node.addChildren( + d -> d.getChildrenLoader().get(), + (childData, parent) -> convertToNode(childData) + ); + } else if (data.hasChildren()) { + // Static children: add directly + for (MigrateNodeData childData : data.getChildren()) { + node.addChild(convertToNode(childData)); + } + } + + return node; + } + + @Override + public synchronized void refreshView() { + super.refreshView(); + refreshChildrenLater(false); + } + + public static boolean isPluginInstalled() { + return MigratePluginInstaller.isAppModPluginInstalled(); + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/RestartIdeDialog.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/RestartIdeDialog.java new file mode 100644 index 00000000000..ab6ef884794 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/RestartIdeDialog.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.DialogWrapper; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; + +/** + * Dialog for prompting user after plugin installation. + * Can be configured to show restart options or just an info message. + */ +public class RestartIdeDialog extends DialogWrapper { + + private final String message; + private boolean showRestartOption = true; + + public RestartIdeDialog(Project project, String title, String message) { + super(project, true); + this.message = message; + setSize(450, 150); + setTitle(title); + setOKButtonText("Restart Now"); + setCancelButtonText("Restart Later"); + init(); + } + + /** + * Sets whether to show restart option or just an info dialog. + * @param showRestartOption true for restart dialog, false for info-only dialog + */ + public RestartIdeDialog setShowRestartOption(boolean showRestartOption) { + this.showRestartOption = showRestartOption; + if (!showRestartOption) { + setOKButtonText("Got it"); + } + return this; + } + + @Override + protected @Nullable JComponent createCenterPanel() { + JPanel panel = new JPanel(new BorderLayout()); + panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + // Support HTML for multi-line messages + JLabel labelComponent; + if (message != null && message.contains("\n")) { + String htmlMessage = "" + message.replace("\n", "
") + ""; + labelComponent = new JLabel(htmlMessage); + } else { + labelComponent = new JLabel(message); + } + labelComponent.setHorizontalAlignment(SwingConstants.CENTER); + panel.add(labelComponent, BorderLayout.CENTER); + return panel; + } + + @Override + protected Action @NotNull [] createActions() { + if (showRestartOption) { + return new Action[]{getOKAction(), getCancelAction()}; + } else { + return new Action[]{getOKAction()}; + } + } + + @Override + protected void doOKAction() { + super.doOKAction(); + if (showRestartOption) { + ApplicationManager.getApplication().restart(); + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml new file mode 100644 index 00000000000..00a9ad12c52 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/app_mod.svg b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/app_mod.svg new file mode 100644 index 00000000000..cc40914cd6c --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/app_mod.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-base/src/main/resources/META-INF/plugin.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-base/src/main/resources/META-INF/plugin.xml index 308a49b55f0..5f24c40ff44 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-base/src/main/resources/META-INF/plugin.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-base/src/main/resources/META-INF/plugin.xml @@ -30,6 +30,7 @@ org.jetbrains.idea.maven com.intellij.gradle + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-service-explorer/build.gradle.kts b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-service-explorer/build.gradle.kts index 2bcc6e6fb46..dbed828fbb9 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-service-explorer/build.gradle.kts +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-service-explorer/build.gradle.kts @@ -1,5 +1,7 @@ dependencies { implementation(project(":azure-intellij-plugin-lib")) // runtimeOnly project(path: ":azure-intellij-plugin-lib", configuration: "instrumentedJar") + implementation(project(":azure-intellij-plugin-appmod")) + // runtimeOnly project(path: ":azure-intellij-plugin-appmod", configuration: "instrumentedJar") implementation("com.microsoft.azure:azure-toolkit-ide-common-lib") } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-service-explorer/src/main/java/com/microsoft/azure/toolkit/intellij/explorer/AzureExplorer.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-service-explorer/src/main/java/com/microsoft/azure/toolkit/intellij/explorer/AzureExplorer.java index 1b450e7ad6a..9de3264688d 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-service-explorer/src/main/java/com/microsoft/azure/toolkit/intellij/explorer/AzureExplorer.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-service-explorer/src/main/java/com/microsoft/azure/toolkit/intellij/explorer/AzureExplorer.java @@ -32,6 +32,7 @@ import com.microsoft.azure.toolkit.intellij.common.component.Tree; import com.microsoft.azure.toolkit.intellij.common.component.TreeUtils; import com.microsoft.azure.toolkit.intellij.explorer.azd.AzdNode; +import com.microsoft.azure.toolkit.intellij.appmod.MigrateToAzureNode; import com.microsoft.azure.toolkit.lib.Azure; import com.microsoft.azure.toolkit.lib.auth.AzureAccount; import com.microsoft.azure.toolkit.lib.auth.IAccountActions; @@ -72,17 +73,20 @@ public class AzureExplorer extends Tree { public static final AzureExplorerNodeProviderManager manager = new AzureExplorerNodeProviderManager(); public static final String AZURE_ICON = AzureIcons.Common.AZURE.getIconPath(); private final AzdNode azdNode; + private final MigrateToAzureNode migrateToAzureNode; private AzureExplorer(Project project) { super(); this.putClientProperty(PLACE, ResourceCommonActionsContributor.AZURE_EXPLORER); this.azdNode = new AzdNode(project); + this.migrateToAzureNode = new MigrateToAzureNode(project); this.root = new Node<>("Azure") .withChildrenLoadLazily(false) .addChild(buildFavoriteRoot()) .addChild(buildAppGroupedResourcesRoot()) .addChild(buildTypeGroupedResourcesRoot()) .addChildren(buildNonAzServiceNodes()) + .addChild(migrateToAzureNode) .addChild(azdNode); this.init(this.root); @@ -130,6 +134,28 @@ private AzureExplorer(Project project) { } } })); + + AzureEventBus.on("migrate.plugin.installed", new AzureEventBus.EventListener(e -> { + final DefaultTreeModel model = (DefaultTreeModel) this.getModel(); + final TreeNode root = (TreeNode) model.getRoot(); + if (root != null && root.children() != null) { + Iterator iterator = root.children().asIterator(); + while (iterator.hasNext()) { + final TreeNode childNode = (TreeNode) iterator.next(); + final Node childInnerNode = childNode.getInner(); + if (childInnerNode instanceof MigrateToAzureNode) { + final MigrateToAzureNode migrateNode = (MigrateToAzureNode) childInnerNode; + childNode.setAllowsChildren(true); + migrateNode.clearClickHandlers(); + migrateNode.withDescription(""); + migrateNode.showMigrationOptions(); + migrateNode.refreshView(); + childNode.updateChildren(true); + break; + } + } + } + })); } @Override diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/build.gradle.kts b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/build.gradle.kts index 216c059e068..25e794a25ff 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/build.gradle.kts +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/build.gradle.kts @@ -1,6 +1,8 @@ dependencies { implementation(project(":azure-intellij-plugin-lib")) // runtimeOnly project(path: ":azure-intellij-plugin-lib", configuration: "instrumentedJar") + implementation(project(":azure-intellij-plugin-appmod")) + // runtimeOnly project(path: ":azure-intellij-plugin-appmod", configuration: "instrumentedJar") implementation(project(":azure-intellij-plugin-service-explorer")) // runtimeOnly project(path: ":azure-intellij-plugin-service-explorer", configuration: "instrumentedJar") implementation("com.microsoft.azure:azure-toolkit-ide-common-lib") diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/AzureFacetRootNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/AzureFacetRootNode.java index a526cac550f..e907ba27829 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/AzureFacetRootNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/AzureFacetRootNode.java @@ -94,10 +94,12 @@ public Collection> buildChildren() { if (Objects.isNull(profile)) { nodes.add(new ActionNode<>(this.getProject(), CONNECT_TO_MODULE, module)); nodes.add(new ActionNode<>(this.getProject(), Action.Id.of(ACTIONS_DEPLOY_TO_AZURE), module.getModule())); - return nodes; + } else { + nodes.add(new DeploymentTargetsNode(this.getProject(), profile.getDeploymentTargetManager())); + nodes.add(new ConnectionsNode(this.getProject(), profile.getConnectionManager())); } - nodes.add(new DeploymentTargetsNode(this.getProject(), profile.getDeploymentTargetManager())); - nodes.add(new ConnectionsNode(this.getProject(), profile.getConnectionManager())); + // Always add Migrate to Azure node at the end + nodes.add(new MigrateToAzureFacetNode(this.getProject(), module)); return nodes; } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java new file mode 100644 index 00000000000..8036d60313c --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.connector.projectexplorer; + +import com.intellij.ide.projectView.PresentationData; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.openapi.project.Project; +import com.intellij.ui.tree.LeafState; +import com.microsoft.azure.toolkit.intellij.common.IntelliJAzureIcons; +import com.microsoft.azure.toolkit.intellij.connector.dotazure.AzureModule; +import com.microsoft.azure.toolkit.intellij.appmod.IMigrateChildNodeProvider; +import com.microsoft.azure.toolkit.intellij.appmod.MigrateNodeData; +import com.microsoft.azure.toolkit.intellij.appmod.MigratePluginInstaller; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Project Explorer facet node for "Migrate to Azure" functionality. + * Uses the same extension point as MigrateToAzureNode and MigrateToAzureAction for consistency. + */ +public class MigrateToAzureFacetNode extends AbstractAzureFacetNode { + private static final ExtensionPointName migrationProviders = + ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateChildNodeProvider"); + + private static final String APP_MOD_ICON_PATH = "/icons/app_mod.svg"; + + public MigrateToAzureFacetNode(Project project, AzureModule module) { + super(project, module); + initializeNode(); + } + + public void initializeNode() { + updateChildren(); + } + + @Override + public Collection> buildChildren() { + final ArrayList> nodes = new ArrayList<>(); + + if (MigratePluginInstaller.isAppModPluginInstalled()) { + // Load migration options from extension points + final List migrationNodes = loadMigrationNodes(); + + // Convert MigrateNodeData to FacetNode + for (MigrateNodeData nodeData : migrationNodes) { + if (nodeData.isVisible()) { + nodes.add(new MigrationNodeWrapper(getProject(), nodeData)); + } + } + } else { + // Not installed - trigger install confirmation when user tries to expand + ApplicationManager.getApplication().invokeLater(() -> { + if (!MigratePluginInstaller.isAppModPluginInstalled()) { + MigratePluginInstaller.showInstallConfirmation(getProject(), + () -> MigratePluginInstaller.installPlugin(getProject())); + } + }); + } + + return nodes; + } + + /** + * Loads migration nodes from extension point providers. + */ + private List loadMigrationNodes() { + return migrationProviders.getExtensionList().stream() + .filter(provider -> provider.isApplicable(getProject())) + .sorted(Comparator.comparingInt(IMigrateChildNodeProvider::getPriority)) + .flatMap(provider -> provider.createNodeData(getProject()).stream()) + .collect(Collectors.toList()); + } + + @Override + protected void buildView(@Nonnull PresentationData presentation) { + if (MigratePluginInstaller.isAppModPluginInstalled()) { + presentation.addText("Migrate to Azure", com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); + presentation.setIcon(IntelliJAzureIcons.getIcon(APP_MOD_ICON_PATH)); + } else { + final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); + final String text = copilotInstalled + ? "Migrate to Azure (Install App Modernization)" + : "Migrate to Azure (Install Copilot & App Modernization)"; + presentation.addText(text, com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); + presentation.setIcon(IntelliJAzureIcons.getIcon(APP_MOD_ICON_PATH)); + } + } + + /** + * Wrapper class that converts MigrateNodeData to AbstractAzureFacetNode for Project Explorer display. + */ + private static class MigrationNodeWrapper extends AbstractAzureFacetNode { + private final MigrateNodeData nodeData; + + protected MigrationNodeWrapper(Project project, MigrateNodeData nodeData) { + super(project, nodeData); + this.nodeData = nodeData; + } + + @Override + public Collection> buildChildren() { + final ArrayList> children = new ArrayList<>(); + + // Get children - lazy or static (Project Explorer's buildChildren is already lazy) + final List childDataList = nodeData.isLazyLoading() + ? nodeData.getChildrenLoader().get() + : nodeData.getChildren(); + + for (MigrateNodeData child : childDataList) { + if (child.isVisible()) { + children.add(new MigrationNodeWrapper(getProject(), child)); + } + } + + return children; + } + + @Override + protected void buildView(@Nonnull PresentationData presentation) { + presentation.addText(nodeData.getLabel(), com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); + + // Set description if available + if (nodeData.getDescription() != null) { + presentation.setLocationString(nodeData.getDescription()); + } + + // Set tooltip if available + if (nodeData.getTooltip() != null) { + presentation.setTooltip(nodeData.getTooltip()); + } + + // Use node's icon if available, otherwise use default app_mod icon + if (nodeData.getIconPath() != null) { + presentation.setIcon(IntelliJAzureIcons.getIcon(nodeData.getIconPath())); + } else { + presentation.setIcon(IntelliJAzureIcons.getIcon(APP_MOD_ICON_PATH)); + } + } + + @Override + public void navigate(boolean requestFocus) { + // Trigger click handler + nodeData.doubleClick(null); + } + + @Override + public boolean canNavigate() { + // Enable navigation for leaf nodes OR nodes with click handlers + return !nodeData.hasChildren() || nodeData.hasClickHandler(); + } + + @Override + public boolean canNavigateToSource() { + // Enable source navigation only for leaf nodes + return !nodeData.hasChildren(); + } + + @Override + public @Nonnull LeafState getLeafState() { + // ALWAYS = leaf node (no expand arrow, double-click triggers navigate) + // NEVER = always show expand arrow + return nodeData.hasChildren() ? LeafState.NEVER : LeafState.ALWAYS; + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties index c608c642f3c..c5cf3cb789d 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties @@ -32,7 +32,9 @@ org.gradle.parallel=true org.gradle.console=rich org.gradle.configureondemand=true org.gradle.daemon=true -org.gradle.jvmargs='-Duser.language=en' +org.gradle.workers.max=8 +org.gradle.vfs.watch=true +org.gradle.jvmargs=-Xmx4096m -Xms1024m -XX:MaxMetaspaceSize=1024m -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParallelGC -Dfile.encoding=UTF-8 -Duser.language=en org.jetbrains.intellij.platform.buildFeature.useBinaryReleases=false diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/settings.gradle.kts b/PluginsAndFeatures/azure-toolkit-for-intellij/settings.gradle.kts index 8457ab5446d..e8bb0fa2905 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/settings.gradle.kts +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/settings.gradle.kts @@ -51,3 +51,4 @@ include("azure-intellij-plugin-integration-services") include("azure-intellij-plugin-java-sdk") include("azure-intellij-plugin-cloud-shell") include("azure-intellij-plugin-azuremcp") +include("azure-intellij-plugin-appmod") diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/src/main/resources/META-INF/plugin.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/src/main/resources/META-INF/plugin.xml index aa845630aad..b221ed6cb3b 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/src/main/resources/META-INF/plugin.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/src/main/resources/META-INF/plugin.xml @@ -44,6 +44,7 @@ + @@ -112,6 +113,8 @@ + + From 75cecba6ac85901345009d1e756b16408501bc18 Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Wed, 21 Jan 2026 11:38:37 +0800 Subject: [PATCH 06/43] use new extension point interface & refactor --- .../docs/architecture.md | 255 ++++++++++++++++++ ...vider.java => IMigrateOptionProvider.java} | 6 +- .../intellij/appmod/MigrateToAzureAction.java | 58 ++-- .../intellij/appmod/MigrateToAzureNode.java | 6 +- .../MigrateToAzureFacetNode.java | 40 ++- 5 files changed, 320 insertions(+), 45 deletions(-) create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/docs/architecture.md rename PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/{IMigrateChildNodeProvider.java => IMigrateOptionProvider.java} (92%) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/docs/architecture.md b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/docs/architecture.md new file mode 100644 index 00000000000..9ca4897f4e6 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/docs/architecture.md @@ -0,0 +1,255 @@ +# App Modernization Module Architecture + +## Overview + +The `azure-intellij-plugin-appmod` module provides the "Migrate to Azure" functionality in Azure Toolkit for IntelliJ. It serves as a bridge to integrate GitHub Copilot App Modernization plugin with Azure Toolkit. + +## Plugin Relationship Diagram + +``` +┌──────────────────────────────────────────────────────────────────────────────────┐ +│ IntelliJ IDEA │ +│ │ +│ ┌────────────────────────────────────────────────────────────────────────────┐ │ +│ │ Azure Toolkit for IntelliJ │ │ +│ │ │ │ +│ │ ┌────────────────────────┐ ┌────────────────────────────────────────┐ │ │ +│ │ │ service-explorer │ │ resource-connector-lib │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ ┌──────────────────┐ │ │ ┌──────────────────────────────────┐ │ │ │ +│ │ │ │MigrateToAzureNode│ │ │ │ MigrateToAzureFacetNode │ │ │ │ +│ │ │ └────────┬─────────┘ │ │ └───────────────┬──────────────────┘ │ │ │ +│ │ └───────────┼────────────┘ └──────────────────┼─────────────────────┘ │ │ +│ │ │ │ │ │ +│ │ └─────────────────┬──────────────────┘ │ │ +│ │ ▼ │ │ +│ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ +│ │ │ azure-intellij-plugin-appmod │ │ │ +│ │ │ │ │ │ +│ │ │ • IMigrateOptionProvider (Extension Point Interface) │ │ │ +│ │ │ • MigrateNodeData (Data Model) │ │ │ +│ │ │ • MigratePluginInstaller (Plugin Detection/Installation) │ │ │ +│ │ │ • MigrateToAzureAction (Context Menu) │ │ │ +│ │ │ │ │ │ +│ │ └──────────────────────────────────┬───────────────────────────────────┘ │ │ +│ │ │ │ │ +│ │ │ Extension Point │ │ +│ │ ▼ │ │ +│ └────────────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ │ implements │ +│ ▼ │ +│ ┌────────────────────────────────────────────────────────────────────────────┐ │ +│ │ GitHub Copilot App Modernization Plugin │ │ +│ │ (appmod-intellij) │ │ +│ │ │ │ +│ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ +│ │ │ MyMigrationProvider implements IMigrateOptionProvider │ │ │ +│ │ │ │ │ │ +│ │ │ • createNodeData() → Returns migration options │ │ │ +│ │ │ • isApplicable() → Check project compatibility │ │ │ +│ │ └──────────────────────────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ Depends on: com.github.copilot (GitHub Copilot) │ │ +│ └────────────────────────────────────────────────────────────────────────────┘ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────┘ +``` + +## Data Flow + +``` +User Action (click/expand) + │ + ▼ +┌─────────────────────┐ +│ Entry Point │ (MigrateToAzureNode / MigrateToAzureFacetNode / MigrateToAzureAction) +└─────────┬───────────┘ + │ + ▼ +┌─────────────────────┐ No ┌─────────────────────┐ +│ Plugin Installed? │────────────▶│ Show Install Dialog │ +└─────────┬───────────┘ └─────────────────────┘ + │ Yes + ▼ +┌─────────────────────┐ +│ Load Extension │ +│ Providers │ +└─────────┬───────────┘ + │ + ▼ +┌─────────────────────┐ +│ Filter by │ +│ isApplicable() │ +└─────────┬───────────┘ + │ + ▼ +┌─────────────────────┐ +│ Sort by Priority │ +└─────────┬───────────┘ + │ + ▼ +┌─────────────────────┐ +│ Call createNodeData │ +│ for each provider │ +└─────────┬───────────┘ + │ + ▼ +┌─────────────────────┐ +│ Display Nodes │ +│ in UI │ +└─────────────────────┘ +``` + +## Module Structure + +``` +azure-intellij-plugin-appmod/ +├── build.gradle.kts +├── docs/ +│ └── architecture.md +└── src/main/ + ├── java/com/microsoft/azure/toolkit/intellij/appmod/ + │ ├── IMigrateOptionProvider.java # Extension Point interface + │ ├── MigrateNodeData.java # Node data model + │ ├── MigratePluginInstaller.java # Plugin detection & installation + │ ├── MigrateToAzureNode.java # Service Explorer entry point + │ ├── MigrateToAzureAction.java # Context menu entry point + │ ├── InstallPluginDialog.java # Installation confirmation dialog + │ └── RestartIdeDialog.java # Restart prompt dialog + └── resources/ + ├── META-INF/azure-intellij-plugin-appmod.xml + └── icons/app_mod.svg +``` + +## Entry Points + +The module provides **three entry points** for users to access migration functionality: + +### 1. Service Explorer Node (`MigrateToAzureNode`) +- **Location**: Azure Explorer panel → "Migrate to Azure" node +- **Behavior**: + - If plugins installed → Shows child nodes from extension providers + - If plugins not installed → Double-click triggers installation dialog + +### 2. Project Explorer Node (`MigrateToAzureFacetNode`) +- **Location**: Project Explorer → Azure facet → "Migrate to Azure" node +- **Note**: Located in `azure-intellij-resource-connector-lib` module (due to `AbstractAzureFacetNode` inheritance) +- **Behavior**: Same as Service Explorer Node + +### 3. Context Menu Action (`MigrateToAzureAction`) +- **Location**: Right-click on project/module → "Migrate to Azure" submenu +- **Behavior**: + - If plugins installed → Shows child actions from extension providers + - If plugins not installed → Single "Install Plugins" action + +## Extension Point + +### Definition +```xml + +``` + +Full ID: `com.microsoft.tooling.msservices.intellij.azure.migrateOptionProvider` + +### Interface: `IMigrateOptionProvider` +```java +public interface IMigrateOptionProvider { + // Check if this provider applies to the given project + boolean isApplicable(@Nonnull Project project); + + // Create node data for display (can return multiple nodes) + @Nonnull List createNodeData(@Nonnull Project project); + + // Priority for ordering (lower = first) + default int getPriority() { return 100; } +} +``` + +### Data Model: `MigrateNodeData` +```java +MigrateNodeData.builder() + .label("Node Label") // Required: display text + .description("Optional description") // Shown as location string + .tooltip("Hover tooltip") // Tooltip text + .iconPath("/icons/my_icon.svg") // Icon path (falls back to app_mod.svg) + .visible(true) // Visibility control + .onDoubleClick(anActionEvent -> {...}) // Double-click handler + .children(childList) // Static children + .childrenLoader(() -> loadChildren()) // OR lazy-loaded children + .build(); +``` + +## Plugin Detection & Installation + +### `MigratePluginInstaller` +Central utility class for plugin management: + +```java +// Check if plugins are installed +MigratePluginInstaller.isAppModPluginInstalled(); // com.github.copilot.appmod +MigratePluginInstaller.isCopilotInstalled(); // com.github.copilot + +// Show installation confirmation dialog +MigratePluginInstaller.showInstallConfirmation(project, onConfirmCallback); + +// Trigger installation (IntelliJ handles the rest) +MigratePluginInstaller.installPlugin(project); + +// Dev mode detection (runIde task) +MigratePluginInstaller.isRunningInDevMode(); +``` + +### Installation Flow +1. User triggers install (double-click node or context menu) +2. `showInstallConfirmation()` shows confirmation dialog +3. On confirm, `installPlugin()` calls `PluginsAdvertiser.installAndEnable()` +4. IntelliJ platform handles: + - Plugin selection dialog (with all plugins pre-selected) + - Download and installation + - Restart prompt +5. In dev mode: Special message shown (don't click IDE restart, re-run `./gradlew runIde`) + +## Module Dependencies + +``` +azure-intellij-plugin-appmod (base module) + ↑ + ├── azure-intellij-plugin-service-explorer + │ └── Uses: MigrateToAzureNode, Extension Point + │ + └── azure-intellij-resource-connector-lib + └── Contains: MigrateToAzureFacetNode (due to inheritance constraint) +``` + +### Why `MigrateToAzureFacetNode` is in connector-lib? +- Must extend `AbstractAzureFacetNode` from connector-lib +- Moving `AbstractAzureFacetNode` to appmod would require moving many other classes +- Current design minimizes code changes while maintaining clean architecture + +## External Plugin Integration + +The `appmod-intellij` plugin (GitHub Copilot App Modernization) should: + +1. Add dependency on `azure-intellij-plugin-appmod` +2. Implement `IMigrateOptionProvider` extension +3. Register in its `plugin.xml`: +```xml + + + +``` + +## UI Behavior Summary + +| State | Service Explorer | Project Explorer | Context Menu | +|-------|-----------------|------------------|--------------| +| Plugins NOT installed | Node shows "(Install...)" suffix, double-click triggers install | Same as Service Explorer | Shows "Install Plugins" action | +| Plugins installed | Expand to show child nodes from providers | Same as Service Explorer | Shows submenu with actions from providers | + +## Icon + +- **Path**: `/icons/app_mod.svg` +- **Location**: `azure-intellij-plugin-appmod/src/main/resources/icons/` +- **Usage**: Centralized icon for all migrate-related nodes and actions diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateChildNodeProvider.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateOptionProvider.java similarity index 92% rename from PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateChildNodeProvider.java rename to PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateOptionProvider.java index 88f4a456c27..f71d88b9e09 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateChildNodeProvider.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateOptionProvider.java @@ -21,7 +21,7 @@ * * Example implementation: *
- * public class MyMigrationProvider implements IMigrateChildNodeProvider {
+ * public class MyMigrationProvider implements IMigrateOptionProvider {
  *     @Override
  *     public List<MigrateNodeData> createNodeData(@Nonnull Project project) {
  *         return List.of(
@@ -37,11 +37,11 @@
  * Registration in plugin.xml:
  * 
  * <extensions defaultExtensionNs="com.microsoft.tooling.msservices.intellij.azure">
- *     <migrateChildNodeProvider implementation="your.package.MyMigrationProvider"/>
+ *     <migrateOptionProvider implementation="your.package.MyMigrationProvider"/>
  * </extensions>
  * 
*/ -public interface IMigrateChildNodeProvider { +public interface IMigrateOptionProvider { /** * Creates migration node data for the Migrate to Azure section. diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java index cee7bd27b8d..886b38bac78 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java @@ -24,24 +24,22 @@ import java.util.stream.Collectors; /** - * Action group that dynamically shows migration options as sub-menu. - * Uses the same extension point as MigrateToAzureNode for consistency. + * Single action group for "Migrate to Azure" functionality. + * - When plugins not installed: sub-menu shows "Install Plugin" option + * - When plugins installed: sub-menu shows migration options from extension providers */ public class MigrateToAzureAction extends ActionGroup { - private static final ExtensionPointName migrationProviders = - ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateChildNodeProvider"); + private static final ExtensionPointName migrationProviders = + ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateOptionProvider"); private static final String APP_MOD_ICON_PATH = "/icons/app_mod.svg"; public MigrateToAzureAction() { - // Set popup=true to show this as a root menu item with expandable sub-menu (▶) - // Without this, children would be displayed directly at root level super("Migrate to Azure", true); } @Override public @NotNull ActionUpdateThread getActionUpdateThread() { - // Run on background thread to avoid blocking EDT when loading migration options return ActionUpdateThread.BGT; } @@ -52,14 +50,10 @@ public void update(@NotNull AnActionEvent e) { if (MigratePluginInstaller.isAppModPluginInstalled()) { e.getPresentation().setText("Migrate to Azure"); - // Only show sub-menu if there are migration options available e.getPresentation().setEnabledAndVisible(project != null && hasMigrationOptions(project)); } else { - final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); - final String text = copilotInstalled - ? "Migrate to Azure (Install App Modernization)" - : "Migrate to Azure (Install Copilot & App Modernization)"; - e.getPresentation().setText(text); + // Plugin not installed - still show menu with install option + e.getPresentation().setText("Migrate to Azure"); e.getPresentation().setEnabledAndVisible(true); } } @@ -77,18 +71,7 @@ public void update(@NotNull AnActionEvent e) { // If plugin not installed, show install action if (!MigratePluginInstaller.isAppModPluginInstalled()) { - final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); - final String actionText = copilotInstalled - ? "Install GitHub Copilot App Modernization" - : "Install GitHub Copilot and GitHub Copilot App Modernization"; - return new AnAction[]{ - new AnAction(actionText) { - @Override - public void actionPerformed(@NotNull AnActionEvent event) { - MigratePluginInstaller.showInstallConfirmation(project, () -> MigratePluginInstaller.installPlugin(project)); - } - } - }; + return new AnAction[]{ createInstallAction(project) }; } // Load migration options from extension points @@ -101,6 +84,29 @@ public void actionPerformed(@NotNull AnActionEvent event) { // Convert nodes to actions return convertNodesToActions(migrationNodes); } + + /** + * Creates the install plugin action shown when required plugins are not installed. + */ + private AnAction createInstallAction(@Nonnull Project project) { + final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); + final String text = copilotInstalled + ? "Install App Modernization Plugin..." + : "Install Copilot & App Modernization Plugins..."; + + return new AnAction(text) { + { + getTemplatePresentation().setIcon(IntelliJAzureIcons.getIcon(APP_MOD_ICON_PATH)); + } + + @Override + @AzureOperation(name = "user/appmod.install_plugin") + public void actionPerformed(@NotNull AnActionEvent e) { + MigratePluginInstaller.showInstallConfirmation(project, + () -> MigratePluginInstaller.installPlugin(project)); + } + }; + } /** * Loads migration nodes from extension point providers. @@ -108,7 +114,7 @@ public void actionPerformed(@NotNull AnActionEvent event) { private List loadMigrationNodes(@Nonnull Project project) { return migrationProviders.getExtensionList().stream() .filter(provider -> provider.isApplicable(project)) - .sorted(Comparator.comparingInt(IMigrateChildNodeProvider::getPriority)) + .sorted(Comparator.comparingInt(IMigrateOptionProvider::getPriority)) .flatMap(provider -> provider.createNodeData(project).stream()) .filter(MigrateNodeData::isVisible) .collect(Collectors.toList()); diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java index b6395a1ee62..a4c0f08c067 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java @@ -19,8 +19,8 @@ * This node extends the azure-toolkit-ide-common-lib Node class to integrate with the Service Explorer tree. */ public final class MigrateToAzureNode extends Node { - private static final ExtensionPointName childProviders = - ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateChildNodeProvider"); + private static final ExtensionPointName childProviders = + ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateOptionProvider"); private final Project project; @@ -63,7 +63,7 @@ public void showMigrationOptions() { // Load migration options from extension points and convert to Node final List nodeDataList = childProviders.getExtensionList().stream() .filter(provider -> provider.isApplicable(project)) - .sorted(Comparator.comparingInt(IMigrateChildNodeProvider::getPriority)) + .sorted(Comparator.comparingInt(IMigrateOptionProvider::getPriority)) .flatMap(provider -> provider.createNodeData(project).stream()) .collect(Collectors.toList()); diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java index 8036d60313c..2e812c1c0f9 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java @@ -6,13 +6,12 @@ package com.microsoft.azure.toolkit.intellij.connector.projectexplorer; import com.intellij.ide.projectView.PresentationData; -import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.project.Project; import com.intellij.ui.tree.LeafState; import com.microsoft.azure.toolkit.intellij.common.IntelliJAzureIcons; import com.microsoft.azure.toolkit.intellij.connector.dotazure.AzureModule; -import com.microsoft.azure.toolkit.intellij.appmod.IMigrateChildNodeProvider; +import com.microsoft.azure.toolkit.intellij.appmod.IMigrateOptionProvider; import com.microsoft.azure.toolkit.intellij.appmod.MigrateNodeData; import com.microsoft.azure.toolkit.intellij.appmod.MigratePluginInstaller; @@ -28,8 +27,8 @@ * Uses the same extension point as MigrateToAzureNode and MigrateToAzureAction for consistency. */ public class MigrateToAzureFacetNode extends AbstractAzureFacetNode { - private static final ExtensionPointName migrationProviders = - ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateChildNodeProvider"); + private static final ExtensionPointName migrationProviders = + ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateOptionProvider"); private static final String APP_MOD_ICON_PATH = "/icons/app_mod.svg"; @@ -56,15 +55,8 @@ public Collection> buildChildren() { nodes.add(new MigrationNodeWrapper(getProject(), nodeData)); } } - } else { - // Not installed - trigger install confirmation when user tries to expand - ApplicationManager.getApplication().invokeLater(() -> { - if (!MigratePluginInstaller.isAppModPluginInstalled()) { - MigratePluginInstaller.showInstallConfirmation(getProject(), - () -> MigratePluginInstaller.installPlugin(getProject())); - } - }); } + // When plugin not installed, return empty list - user must double-click to trigger install return nodes; } @@ -75,7 +67,7 @@ public Collection> buildChildren() { private List loadMigrationNodes() { return migrationProviders.getExtensionList().stream() .filter(provider -> provider.isApplicable(getProject())) - .sorted(Comparator.comparingInt(IMigrateChildNodeProvider::getPriority)) + .sorted(Comparator.comparingInt(IMigrateOptionProvider::getPriority)) .flatMap(provider -> provider.createNodeData(getProject()).stream()) .collect(Collectors.toList()); } @@ -95,6 +87,28 @@ protected void buildView(@Nonnull PresentationData presentation) { } } + @Override + public void navigate(boolean requestFocus) { + // When plugin not installed, trigger install on double-click + if (!MigratePluginInstaller.isAppModPluginInstalled()) { + MigratePluginInstaller.showInstallConfirmation(getProject(), + () -> MigratePluginInstaller.installPlugin(getProject())); + } + } + + @Override + public boolean canNavigate() { + // Enable navigation (double-click) when plugin is not installed + return !MigratePluginInstaller.isAppModPluginInstalled(); + } + + @Override + public @Nonnull LeafState getLeafState() { + // When plugin not installed, show as leaf node (no expand arrow, double-click triggers navigate) + // When installed, show expand arrow to reveal children + return MigratePluginInstaller.isAppModPluginInstalled() ? LeafState.NEVER : LeafState.ALWAYS; + } + /** * Wrapper class that converts MigrateNodeData to AbstractAzureFacetNode for Project Explorer display. */ From ad32f495decc491acf2da5d7b96f2cfb227f4897 Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Wed, 21 Jan 2026 11:39:45 +0800 Subject: [PATCH 07/43] update xml --- .../META-INF/azure-intellij-plugin-appmod.xml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml index 00a9ad12c52..e51b0888cd2 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml @@ -1,11 +1,13 @@ - + - + From d209bf77b9b73dc5d3992dd212cccb6d9b2e60c1 Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Thu, 22 Jan 2026 11:27:48 +0800 Subject: [PATCH 08/43] refactor --- .../toolkit/intellij/appmod/Constants.java | 7 + .../appmod/IMigrateOptionProvider.java | 1 - .../intellij/appmod/MigrateNodeData.java | 15 -- .../appmod/MigratePluginInstaller.java | 48 ++--- .../intellij/appmod/MigrateToAzureAction.java | 168 +++--------------- .../appmod/MigrateToAzureInstallAction.java | 55 ++++++ .../intellij/appmod/MigrateToAzureNode.java | 10 +- .../intellij/appmod/RestartIdeDialog.java | 82 --------- .../META-INF/azure-intellij-plugin-appmod.xml | 8 +- .../src/main/resources/icons/app_mod.svg | 22 --- .../src/main/resources/icons/appmod.svg | 13 ++ .../src/main/resources/icons/appmod_dark.svg | 13 ++ .../intellij/common/IntelliJAzureIcons.java | 1 + .../MigrateToAzureFacetNode.java | 23 +-- .../gradle.properties | 2 +- .../src/main/resources/META-INF/plugin.xml | 1 + 16 files changed, 152 insertions(+), 317 deletions(-) create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/Constants.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureInstallAction.java delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/RestartIdeDialog.java delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/app_mod.svg create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/appmod.svg create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/appmod_dark.svg diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/Constants.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/Constants.java new file mode 100644 index 00000000000..00b7b605325 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/Constants.java @@ -0,0 +1,7 @@ +package com.microsoft.azure.toolkit.intellij.appmod; + +public class Constants { + + public static final String ICON_APPMOD_PATH = "/icons/appmod.svg"; + +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateOptionProvider.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateOptionProvider.java index f71d88b9e09..df7befbf960 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateOptionProvider.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateOptionProvider.java @@ -26,7 +26,6 @@ * public List<MigrateNodeData> createNodeData(@Nonnull Project project) { * return List.of( * MigrateNodeData.builder("My Migration Option") - * .iconPath("/icons/my_icon.svg") * .onClick(() -> performMigration(project)) * .build() * ); diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java index a13f18a1df3..108096ca19a 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java @@ -35,7 +35,6 @@ * 1. Simple leaf node with click action: *
  * MigrateNodeData.builder("Option A")
- *     .iconPath("/icons/my_icon.svg")
  *     .onClick(e -> doSomething())
  *     .build();
  * 
@@ -66,12 +65,6 @@ public class MigrateNodeData { @Nonnull private final String label; - /** - * Icon path for the node (e.g., "/icons/app_mod.svg"). If null, a default icon will be used. - */ - @Nullable - private String iconPath; - /** * Description text (shown as secondary text in some views). */ @@ -240,14 +233,6 @@ private Builder(@Nonnull String label) { this.data = new MigrateNodeData(label); } - /** - * Sets the icon path. - */ - public Builder iconPath(@Nullable String iconPath) { - data.iconPath = iconPath; - return this; - } - /** * Sets the description. */ diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java index 37be801300a..82c0cbd6432 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java @@ -86,12 +86,12 @@ public static void showInstallConfirmation(@Nonnull Project project, @Nonnull Ru final boolean copilotInstalled = isCopilotInstalled(); final String title = copilotInstalled - ? "Install GitHub Copilot App Modernization" - : "Install GitHub Copilot and GitHub Copilot App Modernization"; + ? "Install App modernization" + : "Install GitHub Copilot and app modernization"; final String message = copilotInstalled - ? "Do you want to install GitHub Copilot App Modernization plugin?" - : "Do you want to install GitHub Copilot and GitHub Copilot App Modernization plugins?"; + ? "To migrate to Azure, you'll need a plugin: App modernization." + : "To migrate to Azure, you'll need two plugins: GitHub Copilot and app modernization."; new InstallPluginDialog(project, title) .setLabel(message) @@ -100,31 +100,24 @@ public static void showInstallConfirmation(@Nonnull Project project, @Nonnull Ru } /** - * Installs the App Modernization plugin (and GitHub Copilot if needed). - * IntelliJ platform will handle the restart prompt after installation. - * In dev mode, shows instructions to manually restart runIde task instead. + * Installs the App Modernization plugin. + * IntelliJ platform will automatically install Copilot as a dependency if AppMod declares it. * * @param project The current project */ public static void installPlugin(@Nonnull Project project) { - final boolean copilotInstalled = isCopilotInstalled(); final boolean appModInstalled = isAppModPluginInstalled(); - final boolean isDevMode = isRunningInDevMode(); - // Build plugin ID set - only include plugins that are NOT already installed - final Set pluginsToInstall = new LinkedHashSet<>(); - if (!copilotInstalled) { - pluginsToInstall.add(PluginId.getId(COPILOT_PLUGIN_ID)); - } - if (!appModInstalled) { - pluginsToInstall.add(PluginId.getId(PLUGIN_ID)); - } - - // If all plugins are already installed, nothing to do - if (pluginsToInstall.isEmpty()) { + // If already installed, nothing to do + if (appModInstalled) { return; } + // Only pass AppMod ID - IntelliJ will automatically install Copilot as dependency + // (AppMod's plugin.xml should declare com.github.copilot) + final Set pluginsToInstall = new LinkedHashSet<>(); + pluginsToInstall.add(PluginId.getId(PLUGIN_ID)); + // Use PluginsAdvertiser.installAndEnable - IntelliJ handles the rest // The platform will show plugin selection dialog, download, install, and prompt for restart AzureTaskManager.getInstance().runAndWait(() -> { @@ -135,20 +128,7 @@ public static void installPlugin(@Nonnull Project project) { true, // selectAllInDialog - pre-select all plugins null, // modalityState () -> { - // Called after user confirms installation - if (isDevMode) { - // Dev mode: Show special instructions - ApplicationManager.getApplication().invokeLater(() -> { - final String message = "Plugins are being installed.\n\n" + - "⚠️ DEVELOPMENT MODE:\n" + - "After installation completes, do NOT restart from this IDE window!\n" + - "Instead, stop your current runIde task and relaunch ./gradlew runIde"; - new RestartIdeDialog(project, "Development Mode Notice", message) - .setShowRestartOption(false) - .show(); - }); - } - // Emit event + // Emit event after installation AzureEventBus.emit("migrate.plugin.installed"); } ); diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java index 886b38bac78..d907191fb4a 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java @@ -5,6 +5,7 @@ package com.microsoft.azure.toolkit.intellij.appmod; +import com.intellij.icons.AllIcons; import com.intellij.openapi.actionSystem.ActionGroup; import com.intellij.openapi.actionSystem.ActionUpdateThread; import com.intellij.openapi.actionSystem.AnAction; @@ -24,16 +25,16 @@ import java.util.stream.Collectors; /** - * Single action group for "Migrate to Azure" functionality. - * - When plugins not installed: sub-menu shows "Install Plugin" option - * - When plugins installed: sub-menu shows migration options from extension providers + * ActionGroup for "Migrate to Azure" functionality. + * Only shown when App Modernization plugin IS installed. + * Shows migration options as sub-menu from extension providers. + * + * Mutually exclusive with MigrateToAzureInstallAction (AnAction) which is shown when plugin is NOT installed. */ public class MigrateToAzureAction extends ActionGroup { private static final ExtensionPointName migrationProviders = ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateOptionProvider"); - private static final String APP_MOD_ICON_PATH = "/icons/app_mod.svg"; - public MigrateToAzureAction() { super("Migrate to Azure", true); } @@ -48,14 +49,14 @@ public void update(@NotNull AnActionEvent e) { super.update(e); final Project project = e.getProject(); - if (MigratePluginInstaller.isAppModPluginInstalled()) { - e.getPresentation().setText("Migrate to Azure"); - e.getPresentation().setEnabledAndVisible(project != null && hasMigrationOptions(project)); - } else { - // Plugin not installed - still show menu with install option - e.getPresentation().setText("Migrate to Azure"); - e.getPresentation().setEnabledAndVisible(true); + // Only visible when plugin IS installed (MigrateToAzureInstallAction handles uninstalled case) + if (!MigratePluginInstaller.isAppModPluginInstalled()) { + e.getPresentation().setEnabledAndVisible(false); + return; } + + e.getPresentation().setText("Migrate to Azure"); + e.getPresentation().setEnabledAndVisible(project != null); } @Override @@ -69,16 +70,11 @@ public void update(@NotNull AnActionEvent e) { return AnAction.EMPTY_ARRAY; } - // If plugin not installed, show install action - if (!MigratePluginInstaller.isAppModPluginInstalled()) { - return new AnAction[]{ createInstallAction(project) }; - } - // Load migration options from extension points final List migrationNodes = loadMigrationNodes(project); if (migrationNodes.isEmpty()) { - return AnAction.EMPTY_ARRAY; + return new AnAction[]{ createNoOptionsAction() }; } // Convert nodes to actions @@ -86,24 +82,17 @@ public void update(@NotNull AnActionEvent e) { } /** - * Creates the install plugin action shown when required plugins are not installed. + * Creates a disabled action shown when no migration options are available. */ - private AnAction createInstallAction(@Nonnull Project project) { - final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); - final String text = copilotInstalled - ? "Install App Modernization Plugin..." - : "Install Copilot & App Modernization Plugins..."; - - return new AnAction(text) { + private AnAction createNoOptionsAction() { + return new AnAction("No migration options available") { { - getTemplatePresentation().setIcon(IntelliJAzureIcons.getIcon(APP_MOD_ICON_PATH)); + getTemplatePresentation().setEnabled(false); } @Override - @AzureOperation(name = "user/appmod.install_plugin") public void actionPerformed(@NotNull AnActionEvent e) { - MigratePluginInstaller.showInstallConfirmation(project, - () -> MigratePluginInstaller.installPlugin(project)); + // No-op } }; } @@ -120,14 +109,6 @@ private List loadMigrationNodes(@Nonnull Project project) { .collect(Collectors.toList()); } - /** - * Checks if there are any migration options available. - */ - private boolean hasMigrationOptions(@Nonnull Project project) { - return migrationProviders.getExtensionList().stream() - .anyMatch(provider -> provider.isApplicable(project)); - } - /** * Converts node tree to action tree for sub-menu display. */ @@ -144,25 +125,18 @@ private AnAction[] convertNodesToActions(List nodes) { */ private AnAction convertNodeToAction(MigrateNodeData nodeData) { if (nodeData.hasChildren()) { - // For lazy loading nodes, create a LazyActionGroup that loads children on demand - if (nodeData.isLazyLoading()) { - return new LazyActionGroup(nodeData); - } - - // Node with static children -> create sub-menu with children added immediately + // Node with children -> create sub-menu final DefaultActionGroup subgroup = new DefaultActionGroup(); subgroup.getTemplatePresentation().setText(nodeData.getLabel(), false); subgroup.setPopup(true); + subgroup.getTemplatePresentation().setIcon(AllIcons.Vcs.Changelist); + + // Handle lazy loading or static children + final List children = nodeData.isLazyLoading() + ? nodeData.getChildrenLoader().get() + : nodeData.getChildren(); - // Use node's icon if available, otherwise use default app_mod icon - if (nodeData.getIconPath() != null) { - subgroup.getTemplatePresentation().setIcon(IntelliJAzureIcons.getIcon(nodeData.getIconPath())); - } else { - subgroup.getTemplatePresentation().setIcon(IntelliJAzureIcons.getIcon(APP_MOD_ICON_PATH)); - } - - // Add static children - for (MigrateNodeData child : nodeData.getChildren()) { + for (MigrateNodeData child : children) { if (child.isVisible()) { subgroup.add(convertNodeToAction(child)); } @@ -173,12 +147,7 @@ private AnAction convertNodeToAction(MigrateNodeData nodeData) { // Leaf node -> create clickable action return new AnAction(nodeData.getLabel()) { { - // Use node's icon if available, otherwise use default app_mod icon - if (nodeData.getIconPath() != null) { - getTemplatePresentation().setIcon(IntelliJAzureIcons.getIcon(nodeData.getIconPath())); - } else { - getTemplatePresentation().setIcon(IntelliJAzureIcons.getIcon(APP_MOD_ICON_PATH)); - } + getTemplatePresentation().setIcon(AllIcons.Vcs.Changelist); if (nodeData.getDescription() != null) { getTemplatePresentation().setDescription(nodeData.getDescription()); } @@ -190,90 +159,11 @@ public void update(@NotNull AnActionEvent e) { } @Override - @AzureOperation(name = "user/common.migrate_to_azure.trigger_option") + @AzureOperation(name = "user/appmod.trigger_migrate_option") public void actionPerformed(@NotNull AnActionEvent e) { nodeData.click(e); } }; } } - - /** - * ActionGroup that loads children lazily when the submenu is expanded. - * This avoids blocking the UI when the parent menu is shown. - */ - private class LazyActionGroup extends ActionGroup { - private final MigrateNodeData nodeData; - private volatile AnAction[] cachedChildren; - private volatile boolean isLoading = false; - - // Loading placeholder action - private final AnAction loadingAction = new AnAction("Loading...") { - { - getTemplatePresentation().setEnabled(false); - } - - @Override - public void actionPerformed(@NotNull AnActionEvent e) { - // No-op, this is just a placeholder - } - }; - - LazyActionGroup(MigrateNodeData nodeData) { - super(nodeData.getLabel(), true); - this.nodeData = nodeData; - - // Set icon - if (nodeData.getIconPath() != null) { - getTemplatePresentation().setIcon(IntelliJAzureIcons.getIcon(nodeData.getIconPath())); - } else { - getTemplatePresentation().setIcon(IntelliJAzureIcons.getIcon(APP_MOD_ICON_PATH)); - } - } - - @Override - public @NotNull ActionUpdateThread getActionUpdateThread() { - // Load children on background thread to avoid blocking EDT - return ActionUpdateThread.BGT; - } - - @Override - public AnAction @NotNull [] getChildren(@Nullable AnActionEvent e) { - if (cachedChildren != null) { - return cachedChildren; - } - - if (!isLoading) { - isLoading = true; - // Load children lazily - final List children = nodeData.getChildrenLoader().get(); - final List actions = new ArrayList<>(); - for (MigrateNodeData child : children) { - if (child.isVisible()) { - actions.add(convertNodeToAction(child)); - } - } - cachedChildren = actions.isEmpty() - ? new AnAction[]{ createNoOptionsAction() } - : actions.toArray(new AnAction[0]); - return cachedChildren; - } - - // Still loading, show placeholder - return new AnAction[]{ loadingAction }; - } - - private AnAction createNoOptionsAction() { - return new AnAction("No migration options available") { - { - getTemplatePresentation().setEnabled(false); - } - - @Override - public void actionPerformed(@NotNull AnActionEvent e) { - // No-op - } - }; - } - } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureInstallAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureInstallAction.java new file mode 100644 index 00000000000..37b147d669e --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureInstallAction.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod; + +import com.intellij.openapi.actionSystem.ActionUpdateThread; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.project.Project; +import com.microsoft.azure.toolkit.lib.common.operation.AzureOperation; +import org.jetbrains.annotations.NotNull; + +/** + * Action shown when App Modernization plugin is NOT installed. + * Click directly triggers plugin installation (no sub-menu). + * + * Mutually exclusive with MigrateToAzureAction (ActionGroup) which is shown when plugin IS installed. + */ +public class MigrateToAzureInstallAction extends AnAction { + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.BGT; + } + + @Override + public void update(@NotNull AnActionEvent e) { + // Only visible when plugin is NOT installed + if (MigratePluginInstaller.isAppModPluginInstalled()) { + e.getPresentation().setEnabledAndVisible(false); + return; + } + + final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); + final String text = copilotInstalled + ? "Migrate to Azure (Install App Modernization)" + : "Migrate to Azure (Install Copilot and App Modernization)"; + e.getPresentation().setText(text); + e.getPresentation().setEnabledAndVisible(true); + } + + @Override + @AzureOperation(name = "user/appmod.install_plugin") + public void actionPerformed(@NotNull AnActionEvent e) { + final Project project = e.getProject(); + if (project == null) { + return; + } + + MigratePluginInstaller.showInstallConfirmation(project, + () -> MigratePluginInstaller.installPlugin(project)); + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java index a4c0f08c067..857df2a6012 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java @@ -24,7 +24,8 @@ public final class MigrateToAzureNode extends Node { private final Project project; - private static final AzureIcon APP_MOD_ICON = AzureIcon.builder().iconPath("/icons/app_mod.svg").build(); + private static final AzureIcon APP_MOD_ICON = AzureIcon.builder().iconPath(Constants.ICON_APPMOD_PATH).build(); + private static final AzureIcon CHANGELIST_ICON = AzureIcon.builder().iconPath("/icons/changelist").build(); public MigrateToAzureNode(Project project) { super("Migrate to Azure"); @@ -81,11 +82,8 @@ private Node convertToNode(MigrateNodeData data) { // Set basic properties node.withLabel(d -> d.getLabel()); - // Use node's icon if available, otherwise use default app_mod icon - node.withIcon(d -> d.getIconPath() != null ? AzureIcon.builder().iconPath(d.getIconPath()).build() : APP_MOD_ICON); - if (data.getDescription() != null) { - node.withDescription(d -> d.getDescription()); - } + // Use Changelist icon for child nodes + node.withIcon(CHANGELIST_ICON); if (data.getTooltip() != null) { node.withTips(d -> d.getTooltip()); } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/RestartIdeDialog.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/RestartIdeDialog.java deleted file mode 100644 index ab6ef884794..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/RestartIdeDialog.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - */ - -package com.microsoft.azure.toolkit.intellij.appmod; - -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.DialogWrapper; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import javax.swing.*; -import java.awt.*; - -/** - * Dialog for prompting user after plugin installation. - * Can be configured to show restart options or just an info message. - */ -public class RestartIdeDialog extends DialogWrapper { - - private final String message; - private boolean showRestartOption = true; - - public RestartIdeDialog(Project project, String title, String message) { - super(project, true); - this.message = message; - setSize(450, 150); - setTitle(title); - setOKButtonText("Restart Now"); - setCancelButtonText("Restart Later"); - init(); - } - - /** - * Sets whether to show restart option or just an info dialog. - * @param showRestartOption true for restart dialog, false for info-only dialog - */ - public RestartIdeDialog setShowRestartOption(boolean showRestartOption) { - this.showRestartOption = showRestartOption; - if (!showRestartOption) { - setOKButtonText("Got it"); - } - return this; - } - - @Override - protected @Nullable JComponent createCenterPanel() { - JPanel panel = new JPanel(new BorderLayout()); - panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - - // Support HTML for multi-line messages - JLabel labelComponent; - if (message != null && message.contains("\n")) { - String htmlMessage = "" + message.replace("\n", "
") + ""; - labelComponent = new JLabel(htmlMessage); - } else { - labelComponent = new JLabel(message); - } - labelComponent.setHorizontalAlignment(SwingConstants.CENTER); - panel.add(labelComponent, BorderLayout.CENTER); - return panel; - } - - @Override - protected Action @NotNull [] createActions() { - if (showRestartOption) { - return new Action[]{getOKAction(), getCancelAction()}; - } else { - return new Action[]{getOKAction()}; - } - } - - @Override - protected void doOKAction() { - super.doOKAction(); - if (showRestartOption) { - ApplicationManager.getApplication().restart(); - } - } -} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml index e51b0888cd2..97ccb101911 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml @@ -5,9 +5,15 @@ + + + + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/app_mod.svg b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/app_mod.svg deleted file mode 100644 index cc40914cd6c..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/app_mod.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/appmod.svg b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/appmod.svg new file mode 100644 index 00000000000..0558ebd7969 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/appmod.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/appmod_dark.svg b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/appmod_dark.svg new file mode 100644 index 00000000000..f44426f34f1 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/icons/appmod_dark.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/IntelliJAzureIcons.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/IntelliJAzureIcons.java index b6efe061ec0..4d5a4f93f11 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/IntelliJAzureIcons.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/IntelliJAzureIcons.java @@ -32,6 +32,7 @@ public class IntelliJAzureIcons { put("/icons/spinner", AnimatedIcon.Default.INSTANCE); put("/icons/error", AllIcons.General.Error); put("/icons/unknown", AllIcons.Nodes.Unknown); + put("/icons/changelist", AllIcons.Vcs.Changelist); } }; private static final Map azureIcons = new ConcurrentHashMap<>() { diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java index 2e812c1c0f9..5ef9f2de18d 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java @@ -5,10 +5,12 @@ package com.microsoft.azure.toolkit.intellij.connector.projectexplorer; +import com.intellij.icons.AllIcons; import com.intellij.ide.projectView.PresentationData; import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.project.Project; import com.intellij.ui.tree.LeafState; +import com.microsoft.azure.toolkit.intellij.appmod.Constants; import com.microsoft.azure.toolkit.intellij.common.IntelliJAzureIcons; import com.microsoft.azure.toolkit.intellij.connector.dotazure.AzureModule; import com.microsoft.azure.toolkit.intellij.appmod.IMigrateOptionProvider; @@ -30,8 +32,6 @@ public class MigrateToAzureFacetNode extends AbstractAzureFacetNode private static final ExtensionPointName migrationProviders = ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateOptionProvider"); - private static final String APP_MOD_ICON_PATH = "/icons/app_mod.svg"; - public MigrateToAzureFacetNode(Project project, AzureModule module) { super(project, module); initializeNode(); @@ -76,14 +76,14 @@ private List loadMigrationNodes() { protected void buildView(@Nonnull PresentationData presentation) { if (MigratePluginInstaller.isAppModPluginInstalled()) { presentation.addText("Migrate to Azure", com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); - presentation.setIcon(IntelliJAzureIcons.getIcon(APP_MOD_ICON_PATH)); + presentation.setIcon(IntelliJAzureIcons.getIcon(Constants.ICON_APPMOD_PATH)); } else { final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); final String text = copilotInstalled - ? "Migrate to Azure (Install App Modernization)" - : "Migrate to Azure (Install Copilot & App Modernization)"; + ? "Migrate to Azure (Install App modernization)" + : "Migrate to Azure (Install GitHub Copilot and app modernization)"; presentation.addText(text, com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); - presentation.setIcon(IntelliJAzureIcons.getIcon(APP_MOD_ICON_PATH)); + presentation.setIcon(IntelliJAzureIcons.getIcon(Constants.ICON_APPMOD_PATH)); } } @@ -142,22 +142,13 @@ public Collection> buildChildren() { protected void buildView(@Nonnull PresentationData presentation) { presentation.addText(nodeData.getLabel(), com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); - // Set description if available - if (nodeData.getDescription() != null) { - presentation.setLocationString(nodeData.getDescription()); - } - // Set tooltip if available if (nodeData.getTooltip() != null) { presentation.setTooltip(nodeData.getTooltip()); } // Use node's icon if available, otherwise use default app_mod icon - if (nodeData.getIconPath() != null) { - presentation.setIcon(IntelliJAzureIcons.getIcon(nodeData.getIconPath())); - } else { - presentation.setIcon(IntelliJAzureIcons.getIcon(APP_MOD_ICON_PATH)); - } + presentation.setIcon(AllIcons.Vcs.Changelist); } @Override diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties index c5cf3cb789d..f5051491877 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties @@ -3,7 +3,7 @@ intellijDisplayVersion=2025.3 intellij_version=253-EAP-SNAPSHOT platformVersion=253-EAP-SNAPSHOT # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html -pluginSinceBuild=253 +pluginSinceBuild=251 pluginUntilBuild=253.* # Example: platformPlugins = com.jetbrains.php:203.4449.22, org.intellij.scala:2023.3.27@EAP platformPlugins=org.intellij.scala:2025.3.12 diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/src/main/resources/META-INF/plugin.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/src/main/resources/META-INF/plugin.xml index b221ed6cb3b..fb568944674 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/src/main/resources/META-INF/plugin.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/src/main/resources/META-INF/plugin.xml @@ -113,6 +113,7 @@ + From d4e5f7a0251b4ec9b1a13b3b82e46a81e21202fa Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Thu, 22 Jan 2026 15:01:40 +0800 Subject: [PATCH 09/43] add jumpping to app mod panel if there is no recommendation data --- .../intellij/appmod/AppModPanelHelper.java | 41 ++++++++++++++++ .../intellij/appmod/MigrateToAzureAction.java | 15 +++--- .../intellij/appmod/MigrateToAzureNode.java | 29 ++++++++--- .../MigrateToAzureFacetNode.java | 49 +++++++++++++++++-- 4 files changed, 117 insertions(+), 17 deletions(-) create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModPanelHelper.java diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModPanelHelper.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModPanelHelper.java new file mode 100644 index 00000000000..76484f51829 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModPanelHelper.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.wm.ToolWindow; +import com.intellij.openapi.wm.ToolWindowManager; +import com.microsoft.azure.toolkit.lib.common.messager.AzureMessager; + +import javax.annotation.Nonnull; + +/** + * Helper class for opening the App Modernization Panel. + */ +public final class AppModPanelHelper { + + public static final String TOOL_WINDOW_ID = "GitHub Copilot app modernization"; + + private AppModPanelHelper() { + // Utility class + } + + /** + * Opens the App Modernization Panel tool window. + * + * @param project the current project + */ + public static void openAppModPanel(@Nonnull Project project) { + final ToolWindowManager toolWindowManager = ToolWindowManager.getInstance(project); + final ToolWindow toolWindow = toolWindowManager.getToolWindow(TOOL_WINDOW_ID); + + if (toolWindow != null) { + toolWindow.show(); + } else { + AzureMessager.getMessager().warning("App Modernization Panel is not available. Please ensure the GitHub Copilot App Modernization plugin is installed and enabled."); + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java index d907191fb4a..7d327e4ada8 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java @@ -13,6 +13,8 @@ import com.intellij.openapi.actionSystem.DefaultActionGroup; import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.project.Project; +import com.intellij.openapi.wm.ToolWindow; +import com.intellij.openapi.wm.ToolWindowManager; import com.microsoft.azure.toolkit.intellij.common.IntelliJAzureIcons; import com.microsoft.azure.toolkit.lib.common.operation.AzureOperation; import org.jetbrains.annotations.NotNull; @@ -82,17 +84,16 @@ public void update(@NotNull AnActionEvent e) { } /** - * Creates a disabled action shown when no migration options are available. + * Creates an action to open App Modernization Panel when no migration options are available. */ private AnAction createNoOptionsAction() { - return new AnAction("No migration options available") { - { - getTemplatePresentation().setEnabled(false); - } - + return new AnAction("Get Started with App Modernization") { @Override public void actionPerformed(@NotNull AnActionEvent e) { - // No-op + final Project project = e.getProject(); + if (project != null) { + AppModPanelHelper.openAppModPanel(project); + } } }; } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java index 857df2a6012..6647002ac6e 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java @@ -9,6 +9,7 @@ import com.intellij.openapi.project.Project; import com.microsoft.azure.toolkit.ide.common.component.Node; import com.microsoft.azure.toolkit.ide.common.icon.AzureIcon; +import com.microsoft.azure.toolkit.lib.common.messager.AzureMessager; import java.util.Comparator; import java.util.List; @@ -26,6 +27,7 @@ public final class MigrateToAzureNode extends Node { private static final AzureIcon APP_MOD_ICON = AzureIcon.builder().iconPath(Constants.ICON_APPMOD_PATH).build(); private static final AzureIcon CHANGELIST_ICON = AzureIcon.builder().iconPath("/icons/changelist").build(); + private static final AzureIcon TOOLWINDOW_ICON = AzureIcon.builder().iconPath("/icons/toolWindowProject").build(); public MigrateToAzureNode(Project project) { super("Migrate to Azure"); @@ -47,8 +49,8 @@ private void showNotInstalled() { // Dynamic description based on what needs to be installed final String description = copilotInstalled - ? "Install GitHub Copilot App Modernization" - : "Install GitHub Copilot and GitHub Copilot App Modernization"; + ? "Install App modernizationn" + : "Install GitHub Copilot and app modernization"; withDescription(description); onClicked(e -> { @@ -68,10 +70,25 @@ public void showMigrationOptions() { .flatMap(provider -> provider.createNodeData(project).stream()) .collect(Collectors.toList()); - // Convert MigrateNodeData to Node and add as children - nodeDataList.stream() - .map(this::convertToNode) - .forEach(this::addChild); + if (nodeDataList.isEmpty()) { + // No migration options - add prompt to open App Modernization Panel + addChild(createOpenPanelNode()); + } else { + // Convert MigrateNodeData to Node and add as children + nodeDataList.stream() + .map(this::convertToNode) + .forEach(this::addChild); + } + } + + /** + * Creates a node that opens the App Modernization Panel. + */ + private Node createOpenPanelNode() { + Node node = new Node<>("Get Started with App Modernization"); + node.withIcon(TOOLWINDOW_ICON); + node.onClicked(data -> AppModPanelHelper.openAppModPanel(project)); + return node; } /** diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java index 5ef9f2de18d..a4a705cbb3c 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java @@ -10,6 +10,7 @@ import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.project.Project; import com.intellij.ui.tree.LeafState; +import com.microsoft.azure.toolkit.intellij.appmod.AppModPanelHelper; import com.microsoft.azure.toolkit.intellij.appmod.Constants; import com.microsoft.azure.toolkit.intellij.common.IntelliJAzureIcons; import com.microsoft.azure.toolkit.intellij.connector.dotazure.AzureModule; @@ -49,10 +50,15 @@ public Collection> buildChildren() { // Load migration options from extension points final List migrationNodes = loadMigrationNodes(); - // Convert MigrateNodeData to FacetNode - for (MigrateNodeData nodeData : migrationNodes) { - if (nodeData.isVisible()) { - nodes.add(new MigrationNodeWrapper(getProject(), nodeData)); + if (migrationNodes.isEmpty()) { + // No migration options - add prompt to open App Modernization Panel + nodes.add(new OpenPanelNode(getProject())); + } else { + // Convert MigrateNodeData to FacetNode + for (MigrateNodeData nodeData : migrationNodes) { + if (nodeData.isVisible()) { + nodes.add(new MigrationNodeWrapper(getProject(), nodeData)); + } } } } @@ -109,6 +115,41 @@ public boolean canNavigate() { return MigratePluginInstaller.isAppModPluginInstalled() ? LeafState.NEVER : LeafState.ALWAYS; } + /** + * Node that opens the App Modernization Panel when no migration options are available. + */ + private static class OpenPanelNode extends AbstractAzureFacetNode { + protected OpenPanelNode(Project project) { + super(project, "Get Started with App Modernization"); + } + + @Override + public Collection> buildChildren() { + return List.of(); + } + + @Override + protected void buildView(@Nonnull PresentationData presentation) { + presentation.addText("Get Started with App Modernization", com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); + presentation.setIcon(AllIcons.Toolwindows.ToolWindowProject); + } + + @Override + public void navigate(boolean requestFocus) { + AppModPanelHelper.openAppModPanel(getProject()); + } + + @Override + public boolean canNavigate() { + return true; + } + + @Override + public @Nonnull LeafState getLeafState() { + return LeafState.ALWAYS; + } + } + /** * Wrapper class that converts MigrateNodeData to AbstractAzureFacetNode for Project Explorer display. */ From 6f437ae3f7c7984597af9d58a4c3daeb32fb1785 Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Thu, 22 Jan 2026 16:22:35 +0800 Subject: [PATCH 10/43] enhance open app mod panel --- .../intellij/appmod/MigrateToAzureAction.java | 39 +++++---- .../appmod/MigrateToAzureInstallAction.java | 81 +++++++++++++++---- .../intellij/appmod/MigrateToAzureNode.java | 18 +---- .../MigrateToAzureFacetNode.java | 79 ++++++------------ 4 files changed, 115 insertions(+), 102 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java index 7d327e4ada8..c1c343c828b 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java @@ -51,14 +51,30 @@ public void update(@NotNull AnActionEvent e) { super.update(e); final Project project = e.getProject(); - // Only visible when plugin IS installed (MigrateToAzureInstallAction handles uninstalled case) + // Only visible when plugin IS installed AND has migration options + // (MigrateToAzureInstallAction handles not-installed and no-options cases) if (!MigratePluginInstaller.isAppModPluginInstalled()) { e.getPresentation().setEnabledAndVisible(false); return; } + if (project == null) { + e.getPresentation().setEnabledAndVisible(false); + return; + } + + // Check if there are any migration options + final boolean hasOptions = loadMigrationNodes(project).stream() + .anyMatch(MigrateNodeData::isVisible); + + if (!hasOptions) { + // No options - hide, MigrateToAzureInstallAction will handle this + e.getPresentation().setEnabledAndVisible(false); + return; + } + e.getPresentation().setText("Migrate to Azure"); - e.getPresentation().setEnabledAndVisible(project != null); + e.getPresentation().setEnabledAndVisible(true); } @Override @@ -74,29 +90,10 @@ public void update(@NotNull AnActionEvent e) { // Load migration options from extension points final List migrationNodes = loadMigrationNodes(project); - - if (migrationNodes.isEmpty()) { - return new AnAction[]{ createNoOptionsAction() }; - } // Convert nodes to actions return convertNodesToActions(migrationNodes); } - - /** - * Creates an action to open App Modernization Panel when no migration options are available. - */ - private AnAction createNoOptionsAction() { - return new AnAction("Get Started with App Modernization") { - @Override - public void actionPerformed(@NotNull AnActionEvent e) { - final Project project = e.getProject(); - if (project != null) { - AppModPanelHelper.openAppModPanel(project); - } - } - }; - } /** * Loads migration nodes from extension point providers. diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureInstallAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureInstallAction.java index 37b147d669e..21c477af4a7 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureInstallAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureInstallAction.java @@ -8,17 +8,33 @@ import com.intellij.openapi.actionSystem.ActionUpdateThread; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.project.Project; import com.microsoft.azure.toolkit.lib.common.operation.AzureOperation; import org.jetbrains.annotations.NotNull; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + /** - * Action shown when App Modernization plugin is NOT installed. - * Click directly triggers plugin installation (no sub-menu). + * Action for "Migrate to Azure" when: + * 1. App Modernization plugin is NOT installed - click triggers installation + * 2. Plugin IS installed but no migration options available - click opens App Mod Panel * - * Mutually exclusive with MigrateToAzureAction (ActionGroup) which is shown when plugin IS installed. + * Mutually exclusive with MigrateToAzureAction (ActionGroup) which is shown when plugin IS installed AND has migration options. */ public class MigrateToAzureInstallAction extends AnAction { + private static final ExtensionPointName migrationProviders = + ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateOptionProvider"); + + private enum State { + NOT_INSTALLED, + NO_OPTIONS, + HAS_OPTIONS + } + + private State currentState = State.NOT_INSTALLED; @Override public @NotNull ActionUpdateThread getActionUpdateThread() { @@ -27,29 +43,66 @@ public class MigrateToAzureInstallAction extends AnAction { @Override public void update(@NotNull AnActionEvent e) { - // Only visible when plugin is NOT installed - if (MigratePluginInstaller.isAppModPluginInstalled()) { + final Project project = e.getProject(); + if (project == null) { e.getPresentation().setEnabledAndVisible(false); return; } - final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); - final String text = copilotInstalled - ? "Migrate to Azure (Install App Modernization)" - : "Migrate to Azure (Install Copilot and App Modernization)"; - e.getPresentation().setText(text); - e.getPresentation().setEnabledAndVisible(true); + currentState = determineState(project); + + switch (currentState) { + case NOT_INSTALLED: + final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); + final String text = copilotInstalled + ? "Migrate to Azure (Install App modernizationn)" + : "Migrate to Azure (Install GitHub Copilot and app modernization)"; + e.getPresentation().setText(text); + e.getPresentation().setEnabledAndVisible(true); + break; + case NO_OPTIONS: + e.getPresentation().setText("Migrate to Azure (Open GitHub Copilot app modernization)"); + e.getPresentation().setEnabledAndVisible(true); + break; + case HAS_OPTIONS: + // Hide - MigrateToAzureAction (ActionGroup) will show instead + e.getPresentation().setEnabledAndVisible(false); + break; + } + } + + private State determineState(Project project) { + if (!MigratePluginInstaller.isAppModPluginInstalled()) { + return State.NOT_INSTALLED; + } + + final boolean hasOptions = migrationProviders.getExtensionList().stream() + .filter(provider -> provider.isApplicable(project)) + .flatMap(provider -> provider.createNodeData(project).stream()) + .anyMatch(MigrateNodeData::isVisible); + + return hasOptions ? State.HAS_OPTIONS : State.NO_OPTIONS; } @Override - @AzureOperation(name = "user/appmod.install_plugin") + @AzureOperation(name = "user/appmod.migrate_action") public void actionPerformed(@NotNull AnActionEvent e) { final Project project = e.getProject(); if (project == null) { return; } - MigratePluginInstaller.showInstallConfirmation(project, - () -> MigratePluginInstaller.installPlugin(project)); + switch (currentState) { + case NOT_INSTALLED: + MigratePluginInstaller.showInstallConfirmation(project, + () -> MigratePluginInstaller.installPlugin(project)); + break; + case NO_OPTIONS: + AppModPanelHelper.openAppModPanel(project); + break; + case HAS_OPTIONS: + // Should not happen - action is hidden in this state + break; + } } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java index 6647002ac6e..111d8eb2d5a 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java @@ -27,7 +27,6 @@ public final class MigrateToAzureNode extends Node { private static final AzureIcon APP_MOD_ICON = AzureIcon.builder().iconPath(Constants.ICON_APPMOD_PATH).build(); private static final AzureIcon CHANGELIST_ICON = AzureIcon.builder().iconPath("/icons/changelist").build(); - private static final AzureIcon TOOLWINDOW_ICON = AzureIcon.builder().iconPath("/icons/toolWindowProject").build(); public MigrateToAzureNode(Project project) { super("Migrate to Azure"); @@ -61,7 +60,6 @@ private void showNotInstalled() { public void showMigrationOptions() { clearClickHandlers(); - withDescription(""); // Load migration options from extension points and convert to Node final List nodeDataList = childProviders.getExtensionList().stream() @@ -71,9 +69,11 @@ public void showMigrationOptions() { .collect(Collectors.toList()); if (nodeDataList.isEmpty()) { - // No migration options - add prompt to open App Modernization Panel - addChild(createOpenPanelNode()); + // No migration options - click to open App Modernization Panel + withDescription("Open GitHub Copilot app modernization"); + onClicked(e -> AppModPanelHelper.openAppModPanel(project)); } else { + withDescription(""); // Convert MigrateNodeData to Node and add as children nodeDataList.stream() .map(this::convertToNode) @@ -81,16 +81,6 @@ public void showMigrationOptions() { } } - /** - * Creates a node that opens the App Modernization Panel. - */ - private Node createOpenPanelNode() { - Node node = new Node<>("Get Started with App Modernization"); - node.withIcon(TOOLWINDOW_ICON); - node.onClicked(data -> AppModPanelHelper.openAppModPanel(project)); - return node; - } - /** * Converts MigrateNodeData to Node for Service Explorer compatibility. */ diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java index a4a705cbb3c..065c095e206 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java @@ -33,6 +33,8 @@ public class MigrateToAzureFacetNode extends AbstractAzureFacetNode private static final ExtensionPointName migrationProviders = ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateOptionProvider"); + private boolean hasMigrationOptions = false; + public MigrateToAzureFacetNode(Project project, AzureModule module) { super(project, module); initializeNode(); @@ -49,18 +51,15 @@ public Collection> buildChildren() { if (MigratePluginInstaller.isAppModPluginInstalled()) { // Load migration options from extension points final List migrationNodes = loadMigrationNodes(); + hasMigrationOptions = !migrationNodes.isEmpty(); - if (migrationNodes.isEmpty()) { - // No migration options - add prompt to open App Modernization Panel - nodes.add(new OpenPanelNode(getProject())); - } else { - // Convert MigrateNodeData to FacetNode - for (MigrateNodeData nodeData : migrationNodes) { - if (nodeData.isVisible()) { - nodes.add(new MigrationNodeWrapper(getProject(), nodeData)); - } + // Convert MigrateNodeData to FacetNode + for (MigrateNodeData nodeData : migrationNodes) { + if (nodeData.isVisible()) { + nodes.add(new MigrationNodeWrapper(getProject(), nodeData)); } } + // When no migration options, return empty - double-click opens App Mod Panel } // When plugin not installed, return empty list - user must double-click to trigger install @@ -80,74 +79,48 @@ private List loadMigrationNodes() { @Override protected void buildView(@Nonnull PresentationData presentation) { - if (MigratePluginInstaller.isAppModPluginInstalled()) { - presentation.addText("Migrate to Azure", com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); - presentation.setIcon(IntelliJAzureIcons.getIcon(Constants.ICON_APPMOD_PATH)); - } else { + presentation.setIcon(IntelliJAzureIcons.getIcon(Constants.ICON_APPMOD_PATH)); + + if (!MigratePluginInstaller.isAppModPluginInstalled()) { final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); final String text = copilotInstalled ? "Migrate to Azure (Install App modernization)" : "Migrate to Azure (Install GitHub Copilot and app modernization)"; presentation.addText(text, com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); - presentation.setIcon(IntelliJAzureIcons.getIcon(Constants.ICON_APPMOD_PATH)); + } else if (!hasMigrationOptions) { + presentation.addText("Migrate to Azure", com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); + presentation.setLocationString("Open GitHub Copilot app modernization"); + } else { + presentation.addText("Migrate to Azure", com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); } } @Override public void navigate(boolean requestFocus) { - // When plugin not installed, trigger install on double-click if (!MigratePluginInstaller.isAppModPluginInstalled()) { + // Plugin not installed - trigger install on double-click MigratePluginInstaller.showInstallConfirmation(getProject(), () -> MigratePluginInstaller.installPlugin(getProject())); + } else if (!hasMigrationOptions) { + // No migration options - open App Modernization Panel + AppModPanelHelper.openAppModPanel(getProject()); } } @Override public boolean canNavigate() { - // Enable navigation (double-click) when plugin is not installed - return !MigratePluginInstaller.isAppModPluginInstalled(); + // Enable navigation when plugin is not installed OR when no migration options + return !MigratePluginInstaller.isAppModPluginInstalled() || !hasMigrationOptions; } @Override public @Nonnull LeafState getLeafState() { - // When plugin not installed, show as leaf node (no expand arrow, double-click triggers navigate) - // When installed, show expand arrow to reveal children - return MigratePluginInstaller.isAppModPluginInstalled() ? LeafState.NEVER : LeafState.ALWAYS; - } - - /** - * Node that opens the App Modernization Panel when no migration options are available. - */ - private static class OpenPanelNode extends AbstractAzureFacetNode { - protected OpenPanelNode(Project project) { - super(project, "Get Started with App Modernization"); - } - - @Override - public Collection> buildChildren() { - return List.of(); - } - - @Override - protected void buildView(@Nonnull PresentationData presentation) { - presentation.addText("Get Started with App Modernization", com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); - presentation.setIcon(AllIcons.Toolwindows.ToolWindowProject); - } - - @Override - public void navigate(boolean requestFocus) { - AppModPanelHelper.openAppModPanel(getProject()); - } - - @Override - public boolean canNavigate() { - return true; - } - - @Override - public @Nonnull LeafState getLeafState() { + // Show as leaf node (no expand arrow) when plugin not installed or no migration options + // Show expand arrow when there are migration options to reveal + if (!MigratePluginInstaller.isAppModPluginInstalled() || !hasMigrationOptions) { return LeafState.ALWAYS; } + return LeafState.NEVER; } /** From 2c70d74a267805876943c4222d5b0b02f27eb1c6 Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Fri, 23 Jan 2026 01:18:15 +0800 Subject: [PATCH 11/43] 1. use one MigrateToAzureAction instead MigrateToAzureAction and MigrateToAzureInstallAction 2. Update the extension point invocation timing 3. add refresh button to manual refresh recommendation data --- .../appmod/MigratePluginInstaller.java | 2 - .../intellij/appmod/MigrateToAzureAction.java | 189 +++++++++++------- .../appmod/MigrateToAzureInstallAction.java | 108 ---------- .../intellij/appmod/MigrateToAzureNode.java | 81 ++++++-- .../META-INF/azure-intellij-plugin-appmod.xml | 10 +- .../intellij/explorer/AzureExplorer.java | 22 -- .../MigrateToAzureFacetNode.java | 107 ++++++---- .../src/main/resources/META-INF/plugin.xml | 1 - 8 files changed, 250 insertions(+), 270 deletions(-) delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureInstallAction.java diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java index 82c0cbd6432..054d09791b4 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java @@ -128,8 +128,6 @@ public static void installPlugin(@Nonnull Project project) { true, // selectAllInDialog - pre-select all plugins null, // modalityState () -> { - // Emit event after installation - AzureEventBus.emit("migrate.plugin.installed"); } ); }); diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java index c1c343c828b..5d5c6e6f4b8 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java @@ -11,70 +11,142 @@ import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.DefaultActionGroup; +import com.intellij.openapi.actionSystem.ex.ActionUtil; import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.project.Project; -import com.intellij.openapi.wm.ToolWindow; -import com.intellij.openapi.wm.ToolWindowManager; -import com.microsoft.azure.toolkit.intellij.common.IntelliJAzureIcons; +import com.intellij.openapi.util.Key; import com.microsoft.azure.toolkit.lib.common.operation.AzureOperation; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import javax.annotation.Nonnull; -import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; /** - * ActionGroup for "Migrate to Azure" functionality. - * Only shown when App Modernization plugin IS installed. - * Shows migration options as sub-menu from extension providers. + * Unified ActionGroup for "Migrate to Azure" functionality. + * Handles all three states: + * 1. Plugin NOT installed - direct click triggers installation + * 2. Plugin installed but no migration options - shows "Open App Mod Panel" action + * 3. Plugin installed with migration options - sub-menu shows migration options * - * Mutually exclusive with MigrateToAzureInstallAction (AnAction) which is shown when plugin is NOT installed. + * Data is loaded once on first access and cached in Project.getUserData. */ public class MigrateToAzureAction extends ActionGroup { - private static final ExtensionPointName migrationProviders = + private static final ExtensionPointName MIGRATION_PROVIDERS = ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateOptionProvider"); + private static final Key STATE_KEY = Key.create("azure.migrate.action.state"); - public MigrateToAzureAction() { - super("Migrate to Azure", true); + private enum State { NOT_INSTALLED, NO_OPTIONS, HAS_OPTIONS } + + private static class MigrationState { + final State state; + final List nodes; + + MigrationState(State state, List nodes) { + this.state = state; + this.nodes = nodes; + } } @Override public @NotNull ActionUpdateThread getActionUpdateThread() { return ActionUpdateThread.BGT; } + + /** + * Gets or computes migration state for the project. + * State is cached in Project.getUserData and loaded once on first access. + */ + private MigrationState getOrComputeState(Project project) { + MigrationState state = project.getUserData(STATE_KEY); + if (state == null) { + state = computeState(project); + project.putUserData(STATE_KEY, state); + } + return state; + } + + /** + * Computes migration state by calling providers. + */ + private MigrationState computeState(Project project) { + if (!MigratePluginInstaller.isAppModPluginInstalled()) { + return new MigrationState(State.NOT_INSTALLED, List.of()); + } + + final List nodes = MIGRATION_PROVIDERS.getExtensionList().stream() + .filter(provider -> provider.isApplicable(project)) + .sorted(Comparator.comparingInt(IMigrateOptionProvider::getPriority)) + .flatMap(provider -> provider.createNodeData(project).stream()) + .filter(MigrateNodeData::isVisible) + .collect(Collectors.toList()); + + return new MigrationState( + nodes.isEmpty() ? State.NO_OPTIONS : State.HAS_OPTIONS, + nodes + ); + } @Override public void update(@NotNull AnActionEvent e) { - super.update(e); final Project project = e.getProject(); - - // Only visible when plugin IS installed AND has migration options - // (MigrateToAzureInstallAction handles not-installed and no-options cases) - if (!MigratePluginInstaller.isAppModPluginInstalled()) { + if (project == null) { e.getPresentation().setEnabledAndVisible(false); return; } + final MigrationState migrationState = getOrComputeState(project); + + // Common settings for all states + e.getPresentation().setPopupGroup(true); + e.getPresentation().putClientProperty(ActionUtil.ALWAYS_VISIBLE_GROUP, true); + + switch (migrationState.state) { + case NOT_INSTALLED: + final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); + e.getPresentation().setText(copilotInstalled + ? "Migrate to Azure (Install App modernization)" + : "Migrate to Azure (Install GitHub Copilot and app modernization)"); + e.getPresentation().setPerformGroup(true); + e.getPresentation().putClientProperty(ActionUtil.SUPPRESS_SUBMENU, true); + break; + case NO_OPTIONS: + e.getPresentation().setText("Migrate to Azure (Open GitHub Copilot app modernization)"); + e.getPresentation().setPerformGroup(true); + e.getPresentation().putClientProperty(ActionUtil.SUPPRESS_SUBMENU, true); + break; + case HAS_OPTIONS: + e.getPresentation().setText("Migrate to Azure"); + e.getPresentation().setPerformGroup(false); + e.getPresentation().putClientProperty(ActionUtil.SUPPRESS_SUBMENU, false); + break; + } + e.getPresentation().setEnabledAndVisible(true); + } + + @Override + @AzureOperation(name = "user/appmod.migrate_action") + public void actionPerformed(@NotNull AnActionEvent e) { + final Project project = e.getProject(); if (project == null) { - e.getPresentation().setEnabledAndVisible(false); return; } - // Check if there are any migration options - final boolean hasOptions = loadMigrationNodes(project).stream() - .anyMatch(MigrateNodeData::isVisible); + final MigrationState migrationState = getOrComputeState(project); - if (!hasOptions) { - // No options - hide, MigrateToAzureInstallAction will handle this - e.getPresentation().setEnabledAndVisible(false); - return; + switch (migrationState.state) { + case NOT_INSTALLED: + MigratePluginInstaller.showInstallConfirmation(project, + () -> MigratePluginInstaller.installPlugin(project)); + break; + case NO_OPTIONS: + AppModPanelHelper.openAppModPanel(project); + break; + case HAS_OPTIONS: + // Handled by popup menu + break; } - - e.getPresentation().setText("Migrate to Azure"); - e.getPresentation().setEnabledAndVisible(true); } @Override @@ -82,54 +154,28 @@ public void update(@NotNull AnActionEvent e) { if (e == null) { return AnAction.EMPTY_ARRAY; } - + final Project project = e.getProject(); if (project == null) { return AnAction.EMPTY_ARRAY; } - - // Load migration options from extension points - final List migrationNodes = loadMigrationNodes(project); - - // Convert nodes to actions - return convertNodesToActions(migrationNodes); - } - - /** - * Loads migration nodes from extension point providers. - */ - private List loadMigrationNodes(@Nonnull Project project) { - return migrationProviders.getExtensionList().stream() - .filter(provider -> provider.isApplicable(project)) - .sorted(Comparator.comparingInt(IMigrateOptionProvider::getPriority)) - .flatMap(provider -> provider.createNodeData(project).stream()) - .filter(MigrateNodeData::isVisible) - .collect(Collectors.toList()); - } - - /** - * Converts node tree to action tree for sub-menu display. - */ - private AnAction[] convertNodesToActions(List nodes) { - final List actions = new ArrayList<>(); - for (MigrateNodeData node : nodes) { - actions.add(convertNodeToAction(node)); + + final MigrationState migrationState = getOrComputeState(project); + + if (migrationState.state == State.HAS_OPTIONS) { + return migrationState.nodes.stream() + .map(this::convertNodeToAction) + .toArray(AnAction[]::new); } - return actions.toArray(new AnAction[0]); + + return AnAction.EMPTY_ARRAY; } - - /** - * Converts a single node (and its children) to an action. - */ + private AnAction convertNodeToAction(MigrateNodeData nodeData) { if (nodeData.hasChildren()) { - // Node with children -> create sub-menu - final DefaultActionGroup subgroup = new DefaultActionGroup(); - subgroup.getTemplatePresentation().setText(nodeData.getLabel(), false); - subgroup.setPopup(true); + final DefaultActionGroup subgroup = new DefaultActionGroup(nodeData.getLabel(), true); subgroup.getTemplatePresentation().setIcon(AllIcons.Vcs.Changelist); - // Handle lazy loading or static children final List children = nodeData.isLazyLoading() ? nodeData.getChildrenLoader().get() : nodeData.getChildren(); @@ -139,18 +185,9 @@ private AnAction convertNodeToAction(MigrateNodeData nodeData) { subgroup.add(convertNodeToAction(child)); } } - return subgroup; } else { - // Leaf node -> create clickable action - return new AnAction(nodeData.getLabel()) { - { - getTemplatePresentation().setIcon(AllIcons.Vcs.Changelist); - if (nodeData.getDescription() != null) { - getTemplatePresentation().setDescription(nodeData.getDescription()); - } - } - + return new AnAction(nodeData.getLabel(), nodeData.getDescription(), AllIcons.Vcs.Changelist) { @Override public void update(@NotNull AnActionEvent e) { e.getPresentation().setEnabled(nodeData.isEnabled()); diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureInstallAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureInstallAction.java deleted file mode 100644 index 21c477af4a7..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureInstallAction.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - */ - -package com.microsoft.azure.toolkit.intellij.appmod; - -import com.intellij.openapi.actionSystem.ActionUpdateThread; -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.extensions.ExtensionPointName; -import com.intellij.openapi.project.Project; -import com.microsoft.azure.toolkit.lib.common.operation.AzureOperation; -import org.jetbrains.annotations.NotNull; - -import java.util.Comparator; -import java.util.List; -import java.util.stream.Collectors; - -/** - * Action for "Migrate to Azure" when: - * 1. App Modernization plugin is NOT installed - click triggers installation - * 2. Plugin IS installed but no migration options available - click opens App Mod Panel - * - * Mutually exclusive with MigrateToAzureAction (ActionGroup) which is shown when plugin IS installed AND has migration options. - */ -public class MigrateToAzureInstallAction extends AnAction { - private static final ExtensionPointName migrationProviders = - ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateOptionProvider"); - - private enum State { - NOT_INSTALLED, - NO_OPTIONS, - HAS_OPTIONS - } - - private State currentState = State.NOT_INSTALLED; - - @Override - public @NotNull ActionUpdateThread getActionUpdateThread() { - return ActionUpdateThread.BGT; - } - - @Override - public void update(@NotNull AnActionEvent e) { - final Project project = e.getProject(); - if (project == null) { - e.getPresentation().setEnabledAndVisible(false); - return; - } - - currentState = determineState(project); - - switch (currentState) { - case NOT_INSTALLED: - final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); - final String text = copilotInstalled - ? "Migrate to Azure (Install App modernizationn)" - : "Migrate to Azure (Install GitHub Copilot and app modernization)"; - e.getPresentation().setText(text); - e.getPresentation().setEnabledAndVisible(true); - break; - case NO_OPTIONS: - e.getPresentation().setText("Migrate to Azure (Open GitHub Copilot app modernization)"); - e.getPresentation().setEnabledAndVisible(true); - break; - case HAS_OPTIONS: - // Hide - MigrateToAzureAction (ActionGroup) will show instead - e.getPresentation().setEnabledAndVisible(false); - break; - } - } - - private State determineState(Project project) { - if (!MigratePluginInstaller.isAppModPluginInstalled()) { - return State.NOT_INSTALLED; - } - - final boolean hasOptions = migrationProviders.getExtensionList().stream() - .filter(provider -> provider.isApplicable(project)) - .flatMap(provider -> provider.createNodeData(project).stream()) - .anyMatch(MigrateNodeData::isVisible); - - return hasOptions ? State.HAS_OPTIONS : State.NO_OPTIONS; - } - - @Override - @AzureOperation(name = "user/appmod.migrate_action") - public void actionPerformed(@NotNull AnActionEvent e) { - final Project project = e.getProject(); - if (project == null) { - return; - } - - switch (currentState) { - case NOT_INSTALLED: - MigratePluginInstaller.showInstallConfirmation(project, - () -> MigratePluginInstaller.installPlugin(project)); - break; - case NO_OPTIONS: - AppModPanelHelper.openAppModPanel(project); - break; - case HAS_OPTIONS: - // Should not happen - action is hidden in this state - break; - } - } -} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java index 111d8eb2d5a..bb53f8462b0 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java @@ -9,8 +9,11 @@ import com.intellij.openapi.project.Project; import com.microsoft.azure.toolkit.ide.common.component.Node; import com.microsoft.azure.toolkit.ide.common.icon.AzureIcon; -import com.microsoft.azure.toolkit.lib.common.messager.AzureMessager; +import com.microsoft.azure.toolkit.ide.common.icon.AzureIcons; +import com.microsoft.azure.toolkit.lib.common.action.Action; +import com.microsoft.azure.toolkit.lib.common.action.ActionGroup; +import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; @@ -18,6 +21,8 @@ /** * Service Explorer node for "Migrate to Azure" functionality. * This node extends the azure-toolkit-ide-common-lib Node class to integrate with the Service Explorer tree. + * + * State is computed on initialization and can be refreshed via refresh() method. */ public final class MigrateToAzureNode extends Node { private static final ExtensionPointName childProviders = @@ -32,15 +37,44 @@ public MigrateToAzureNode(Project project) { super("Migrate to Azure"); this.project = project; withIcon(APP_MOD_ICON); + + // Add refresh action to context menu (only once in constructor) + withActions(new ActionGroup( + new Action<>(Action.Id.of("user/appmod.refresh_migrate_node")) + .withLabel("Refresh") + .withIcon(AzureIcons.Action.REFRESH.getIconPath()) + .withHandler((v, e) -> this.refresh()) + .withAuthRequired(false) + )); + + // Use addChildren with a function so it rebuilds on refresh + addChildren(data -> buildChildNodes()); + initializeNode(); } public void initializeNode() { - if (MigratePluginInstaller.isAppModPluginInstalled()) { - showMigrationOptions(); - } else { + // Clear previous state + clearClickHandlers(); + withDescription(""); + + if (!MigratePluginInstaller.isAppModPluginInstalled()) { showNotInstalled(); } + // Don't call showMigrationOptions() here - let buildChildNodes() handle it + // This avoids double loading of extension point data + } + + /** + * Refreshes the node by re-computing migration options. + * Called by RefreshMigrateToAzureAction from context menu. + */ + public void refresh() { + refreshChildren(); // This rebuilds children from addChildren function + } + + public Project getProject() { + return project; } private void showNotInstalled() { @@ -48,7 +82,7 @@ private void showNotInstalled() { // Dynamic description based on what needs to be installed final String description = copilotInstalled - ? "Install App modernizationn" + ? "Install App modernization" : "Install GitHub Copilot and app modernization"; withDescription(description); @@ -56,29 +90,42 @@ private void showNotInstalled() { MigratePluginInstaller.showInstallConfirmation(project, () -> MigratePluginInstaller.installPlugin(project)); }); } - - - public void showMigrationOptions() { - clearClickHandlers(); - - // Load migration options from extension points and convert to Node - final List nodeDataList = childProviders.getExtensionList().stream() + + /** + * Load migration options from extension points. + */ + private List loadMigrationNodeData() { + return childProviders.getExtensionList().stream() .filter(provider -> provider.isApplicable(project)) .sorted(Comparator.comparingInt(IMigrateOptionProvider::getPriority)) .flatMap(provider -> provider.createNodeData(project).stream()) + .filter(MigrateNodeData::isVisible) .collect(Collectors.toList()); + } + + /** + * Build child nodes - called by Node framework on refresh. + * Also updates description and click handler based on data. + */ + private List> buildChildNodes() { + if (!MigratePluginInstaller.isAppModPluginInstalled()) { + return List.of(); + } + + final List nodeDataList = loadMigrationNodeData(); + // Update description and click handler based on data + clearClickHandlers(); if (nodeDataList.isEmpty()) { - // No migration options - click to open App Modernization Panel withDescription("Open GitHub Copilot app modernization"); onClicked(e -> AppModPanelHelper.openAppModPanel(project)); } else { withDescription(""); - // Convert MigrateNodeData to Node and add as children - nodeDataList.stream() - .map(this::convertToNode) - .forEach(this::addChild); } + + return nodeDataList.stream() + .map(this::convertToNode) + .collect(Collectors.toList()); } /** diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml index 97ccb101911..e8647c39271 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml @@ -5,15 +5,9 @@ - - - - + + text="Migrate to Azure" description="Migrate application to Azure" icon="/icons/appmod.svg"/> diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-service-explorer/src/main/java/com/microsoft/azure/toolkit/intellij/explorer/AzureExplorer.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-service-explorer/src/main/java/com/microsoft/azure/toolkit/intellij/explorer/AzureExplorer.java index 9de3264688d..6c4f4cf35db 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-service-explorer/src/main/java/com/microsoft/azure/toolkit/intellij/explorer/AzureExplorer.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-service-explorer/src/main/java/com/microsoft/azure/toolkit/intellij/explorer/AzureExplorer.java @@ -134,28 +134,6 @@ private AzureExplorer(Project project) { } } })); - - AzureEventBus.on("migrate.plugin.installed", new AzureEventBus.EventListener(e -> { - final DefaultTreeModel model = (DefaultTreeModel) this.getModel(); - final TreeNode root = (TreeNode) model.getRoot(); - if (root != null && root.children() != null) { - Iterator iterator = root.children().asIterator(); - while (iterator.hasNext()) { - final TreeNode childNode = (TreeNode) iterator.next(); - final Node childInnerNode = childNode.getInner(); - if (childInnerNode instanceof MigrateToAzureNode) { - final MigrateToAzureNode migrateNode = (MigrateToAzureNode) childInnerNode; - childNode.setAllowsChildren(true); - migrateNode.clearClickHandlers(); - migrateNode.withDescription(""); - migrateNode.showMigrationOptions(); - migrateNode.refreshView(); - childNode.updateChildren(true); - break; - } - } - } - })); } @Override diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java index 065c095e206..b0646b2783f 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java @@ -17,8 +17,13 @@ import com.microsoft.azure.toolkit.intellij.appmod.IMigrateOptionProvider; import com.microsoft.azure.toolkit.intellij.appmod.MigrateNodeData; import com.microsoft.azure.toolkit.intellij.appmod.MigratePluginInstaller; +import com.microsoft.azure.toolkit.ide.common.icon.AzureIcons; +import com.microsoft.azure.toolkit.lib.common.action.Action; +import com.microsoft.azure.toolkit.lib.common.action.ActionGroup; +import com.microsoft.azure.toolkit.lib.common.action.IActionGroup; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; @@ -28,54 +33,83 @@ /** * Project Explorer facet node for "Migrate to Azure" functionality. * Uses the same extension point as MigrateToAzureNode and MigrateToAzureAction for consistency. + * + * State is computed on initialization and refreshed when buildChildren() is called (via tree refresh). */ public class MigrateToAzureFacetNode extends AbstractAzureFacetNode { private static final ExtensionPointName migrationProviders = ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateOptionProvider"); - private boolean hasMigrationOptions = false; + // Lazy-loaded state - computed on first access + private List migrationNodes = null; public MigrateToAzureFacetNode(Project project, AzureModule module) { super(project, module); - initializeNode(); + // Don't compute in constructor - use lazy loading } - - public void initializeNode() { - updateChildren(); - } - - @Override - public Collection> buildChildren() { - final ArrayList> nodes = new ArrayList<>(); - - if (MigratePluginInstaller.isAppModPluginInstalled()) { - // Load migration options from extension points - final List migrationNodes = loadMigrationNodes(); - hasMigrationOptions = !migrationNodes.isEmpty(); - - // Convert MigrateNodeData to FacetNode - for (MigrateNodeData nodeData : migrationNodes) { - if (nodeData.isVisible()) { - nodes.add(new MigrationNodeWrapper(getProject(), nodeData)); - } - } - // When no migration options, return empty - double-click opens App Mod Panel + + /** + * Gets migration nodes, computing them lazily on first access. + */ + private List getMigrationNodes() { + if (migrationNodes == null) { + migrationNodes = computeMigrationNodes(); } - // When plugin not installed, return empty list - user must double-click to trigger install - - return nodes; + return migrationNodes; } - + /** - * Loads migration nodes from extension point providers. + * Computes migration nodes from extension point providers. */ - private List loadMigrationNodes() { + private List computeMigrationNodes() { + if (!MigratePluginInstaller.isAppModPluginInstalled()) { + return List.of(); + } return migrationProviders.getExtensionList().stream() .filter(provider -> provider.isApplicable(getProject())) .sorted(Comparator.comparingInt(IMigrateOptionProvider::getPriority)) .flatMap(provider -> provider.createNodeData(getProject()).stream()) + .filter(MigrateNodeData::isVisible) .collect(Collectors.toList()); } + + /** + * Checks if there are any visible migration options available. + */ + private boolean hasMigrationOptions() { + return !getMigrationNodes().isEmpty(); + } + + /** + * Refreshes migration nodes and updates the tree view. + */ + public void refresh() { + migrationNodes = null; // Clear cached data to force recompute + updateChildren(); // This also refreshes the view + } + + @Nullable + @Override + public IActionGroup getActionGroup() { + final Action refreshAction = new Action<>(Action.Id.of("user/appmod.refresh_migrate_node")) + .withLabel("Refresh") + .withIcon(AzureIcons.Action.REFRESH.getIconPath()) + .withHandler((v, e) -> refresh()) + .withAuthRequired(false); + return new ActionGroup(refreshAction); + } + + @Override + public Collection> buildChildren() { + final ArrayList> nodes = new ArrayList<>(); + + // Convert MigrateNodeData to FacetNode + for (MigrateNodeData nodeData : getMigrationNodes()) { + nodes.add(new MigrationNodeWrapper(getProject(), nodeData)); + } + + return nodes; + } @Override protected void buildView(@Nonnull PresentationData presentation) { @@ -87,7 +121,7 @@ protected void buildView(@Nonnull PresentationData presentation) { ? "Migrate to Azure (Install App modernization)" : "Migrate to Azure (Install GitHub Copilot and app modernization)"; presentation.addText(text, com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); - } else if (!hasMigrationOptions) { + } else if (!hasMigrationOptions()) { presentation.addText("Migrate to Azure", com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); presentation.setLocationString("Open GitHub Copilot app modernization"); } else { @@ -101,7 +135,7 @@ public void navigate(boolean requestFocus) { // Plugin not installed - trigger install on double-click MigratePluginInstaller.showInstallConfirmation(getProject(), () -> MigratePluginInstaller.installPlugin(getProject())); - } else if (!hasMigrationOptions) { + } else if (!hasMigrationOptions()) { // No migration options - open App Modernization Panel AppModPanelHelper.openAppModPanel(getProject()); } @@ -110,17 +144,18 @@ public void navigate(boolean requestFocus) { @Override public boolean canNavigate() { // Enable navigation when plugin is not installed OR when no migration options - return !MigratePluginInstaller.isAppModPluginInstalled() || !hasMigrationOptions; + return !MigratePluginInstaller.isAppModPluginInstalled() || !hasMigrationOptions(); } @Override public @Nonnull LeafState getLeafState() { - // Show as leaf node (no expand arrow) when plugin not installed or no migration options - // Show expand arrow when there are migration options to reveal - if (!MigratePluginInstaller.isAppModPluginInstalled() || !hasMigrationOptions) { + // Use ASYNC to avoid triggering extension point loading synchronously + // The actual leaf state will be determined when buildChildren() is called + if (!MigratePluginInstaller.isAppModPluginInstalled()) { return LeafState.ALWAYS; } - return LeafState.NEVER; + // ASYNC means IntelliJ will call buildChildren() to determine if there are children + return LeafState.ASYNC; } /** diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/src/main/resources/META-INF/plugin.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/src/main/resources/META-INF/plugin.xml index fb568944674..b221ed6cb3b 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/src/main/resources/META-INF/plugin.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/src/main/resources/META-INF/plugin.xml @@ -113,7 +113,6 @@ - From 07e235b988415b251b9debbe657020f764c35c70 Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Fri, 23 Jan 2026 09:27:05 +0800 Subject: [PATCH 12/43] remove unuseful properties --- .../intellij/appmod/MigrateNodeData.java | 22 +++++-------------- .../intellij/appmod/MigrateToAzureNode.java | 4 ++-- .../MigrateToAzureFacetNode.java | 4 ++-- 3 files changed, 9 insertions(+), 21 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java index 108096ca19a..49541eb262a 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java @@ -20,7 +20,7 @@ * This class is used by MigrateToAzureNode, MigrateToAzureAction, and MigrateToAzureFacetNode. * * Features: - * - Basic properties: label, icon, description, tooltip + * - Basic properties: label, description (used as menu description and tooltip) * - Click handling for both leaf and parent nodes * - Static children or lazy-loaded children via childrenLoader * @@ -66,17 +66,13 @@ public class MigrateNodeData { private final String label; /** - * Description text (shown as secondary text in some views). + * Description text. Used as: + * - Menu item description in MigrateToAzureAction + * - Tooltip in MigrateToAzureNode and MigrateToAzureFacetNode */ @Nullable private String description; - /** - * Tooltip text (shown on hover). - */ - @Nullable - private String tooltip; - // ==================== State ==================== /** @@ -234,21 +230,13 @@ private Builder(@Nonnull String label) { } /** - * Sets the description. + * Sets the description (used as menu description and tooltip). */ public Builder description(@Nullable String description) { data.description = description; return this; } - /** - * Sets the tooltip. - */ - public Builder tooltip(@Nullable String tooltip) { - data.tooltip = tooltip; - return this; - } - /** * Sets the enabled state. */ diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java index bb53f8462b0..43864b1eba3 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java @@ -138,8 +138,8 @@ private Node convertToNode(MigrateNodeData data) { node.withLabel(d -> d.getLabel()); // Use Changelist icon for child nodes node.withIcon(CHANGELIST_ICON); - if (data.getTooltip() != null) { - node.withTips(d -> d.getTooltip()); + if (data.getDescription() != null) { + node.withTips(d -> d.getDescription()); } // Set click handler diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java index b0646b2783f..0e1037b1545 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java @@ -192,8 +192,8 @@ protected void buildView(@Nonnull PresentationData presentation) { presentation.addText(nodeData.getLabel(), com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); // Set tooltip if available - if (nodeData.getTooltip() != null) { - presentation.setTooltip(nodeData.getTooltip()); + if (nodeData.getDescription() != null) { + presentation.setTooltip(nodeData.getDescription()); } // Use node's icon if available, otherwise use default app_mod icon From fc74e630b9971b0bea97f7cb3d5a6bd6e8245d6b Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Fri, 23 Jan 2026 11:30:21 +0800 Subject: [PATCH 13/43] update text for github copilot already installed scenario --- .../azure/toolkit/intellij/appmod/MigratePluginInstaller.java | 4 ++-- .../azure/toolkit/intellij/appmod/MigrateToAzureAction.java | 2 +- .../azure/toolkit/intellij/appmod/MigrateToAzureNode.java | 2 +- .../connector/projectexplorer/MigrateToAzureFacetNode.java | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java index 054d09791b4..a73769caa4a 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java @@ -86,11 +86,11 @@ public static void showInstallConfirmation(@Nonnull Project project, @Nonnull Ru final boolean copilotInstalled = isCopilotInstalled(); final String title = copilotInstalled - ? "Install App modernization" + ? "Install Github Copilot app modernization" : "Install GitHub Copilot and app modernization"; final String message = copilotInstalled - ? "To migrate to Azure, you'll need a plugin: App modernization." + ? "Install this plugin to automate migrating your apps to Azure with Copilot." : "To migrate to Azure, you'll need two plugins: GitHub Copilot and app modernization."; new InstallPluginDialog(project, title) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java index 5d5c6e6f4b8..eecb8eda63c 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java @@ -106,7 +106,7 @@ public void update(@NotNull AnActionEvent e) { case NOT_INSTALLED: final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); e.getPresentation().setText(copilotInstalled - ? "Migrate to Azure (Install App modernization)" + ? "Migrate to Azure (Install Github Copilot app modernization)" : "Migrate to Azure (Install GitHub Copilot and app modernization)"); e.getPresentation().setPerformGroup(true); e.getPresentation().putClientProperty(ActionUtil.SUPPRESS_SUBMENU, true); diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java index 43864b1eba3..ae66f0e9341 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java @@ -82,7 +82,7 @@ private void showNotInstalled() { // Dynamic description based on what needs to be installed final String description = copilotInstalled - ? "Install App modernization" + ? "Install Github Copilot app modernization" : "Install GitHub Copilot and app modernization"; withDescription(description); diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java index 0e1037b1545..b4fab3d0e19 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java @@ -118,7 +118,7 @@ protected void buildView(@Nonnull PresentationData presentation) { if (!MigratePluginInstaller.isAppModPluginInstalled()) { final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); final String text = copilotInstalled - ? "Migrate to Azure (Install App modernization)" + ? "Migrate to Azure (Install Github Copilot app modernization)" : "Migrate to Azure (Install GitHub Copilot and app modernization)"; presentation.addText(text, com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); } else if (!hasMigrationOptions()) { From ff9d4846645e53548536a3f69296bd20cbe0dc23 Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Fri, 23 Jan 2026 15:16:06 +0800 Subject: [PATCH 14/43] add telemetry support --- .../intellij/appmod/AppModPanelHelper.java | 5 +- .../toolkit/intellij/appmod/AppModUtils.java | 71 +++++++++++++++++++ .../appmod/MigratePluginInstaller.java | 3 + .../intellij/appmod/MigrateToAzureAction.java | 11 ++- .../intellij/appmod/MigrateToAzureNode.java | 19 +++-- .../MigrateToAzureFacetNode.java | 16 ++++- 6 files changed, 114 insertions(+), 11 deletions(-) create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModUtils.java diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModPanelHelper.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModPanelHelper.java index 76484f51829..5cd329c3192 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModPanelHelper.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModPanelHelper.java @@ -27,14 +27,17 @@ private AppModPanelHelper() { * Opens the App Modernization Panel tool window. * * @param project the current project + * @param source the source of the open request (e.g., "node", "action", "facet") */ - public static void openAppModPanel(@Nonnull Project project) { + public static void openAppModPanel(@Nonnull Project project, @Nonnull String source) { final ToolWindowManager toolWindowManager = ToolWindowManager.getInstance(project); final ToolWindow toolWindow = toolWindowManager.getToolWindow(TOOL_WINDOW_ID); if (toolWindow != null) { + AppModUtils.logTelemetryEvent(source + ".open-panel"); toolWindow.show(); } else { + AppModUtils.logTelemetryEvent(source + ".open-panel-failed"); AzureMessager.getMessager().warning("App Modernization Panel is not available. Please ensure the GitHub Copilot App Modernization plugin is installed and enabled."); } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModUtils.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModUtils.java new file mode 100644 index 00000000000..9f265fd94b1 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModUtils.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod; + +import com.intellij.openapi.diagnostic.Logger; +import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager; +import com.microsoft.azure.toolkit.lib.common.telemetry.AzureTelemeter; +import com.microsoft.azure.toolkit.lib.common.telemetry.AzureTelemetry; + +import javax.annotation.Nonnull; +import java.util.Map; + +/** + * Utility class for App Modernization (Migrate to Azure) telemetry and common operations. + */ +public final class AppModUtils { + + private static final Logger LOG = Logger.getInstance(AppModUtils.class); + private static final String SERVICE_NAME = "appmod"; + // Set to true to enable telemetry debug logging (for development only) + private static final boolean DEBUG_TELEMETRY = true; + + private AppModUtils() { + // Utility class, no instantiation allowed + } + + /** + * Logs a telemetry event for App Modernization features. + * + * @param eventName The name of the event to log (e.g., "show-node", "click-option", "refresh") + */ + public static void logTelemetryEvent(@Nonnull String eventName) { + if (DEBUG_TELEMETRY) { + LOG.info("[AppMod Telemetry] " + eventName); + } + final Map properties = Map.of( + AzureTelemeter.OP_NAME, eventName, + AzureTelemeter.OP_PARENT_ID, SERVICE_NAME, + AzureTelemeter.OPERATION_NAME, eventName, + AzureTelemeter.SERVICE_NAME, SERVICE_NAME + ); + AzureTaskManager.getInstance().runLater(() -> { + AzureTelemeter.log(AzureTelemetry.Type.INFO, properties); + }); + } + + /** + * Logs a telemetry event with additional properties. + * + * @param eventName The name of the event to log + * @param additionalProperties Additional properties to include in the telemetry + */ + public static void logTelemetryEvent(@Nonnull String eventName, @Nonnull Map additionalProperties) { + if (DEBUG_TELEMETRY) { + LOG.info("[AppMod Telemetry] " + eventName + " " + additionalProperties); + } + final Map properties = new java.util.HashMap<>(Map.of( + AzureTelemeter.OP_NAME, eventName, + AzureTelemeter.OP_PARENT_ID, SERVICE_NAME, + AzureTelemeter.OPERATION_NAME, eventName, + AzureTelemeter.SERVICE_NAME, SERVICE_NAME + )); + properties.putAll(additionalProperties); + AzureTaskManager.getInstance().runLater(() -> { + AzureTelemeter.log(AzureTelemetry.Type.INFO, properties); + }); + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java index a73769caa4a..6290727cf83 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java @@ -16,6 +16,7 @@ import javax.annotation.Nonnull; import java.util.LinkedHashSet; +import java.util.Map; import java.util.Set; /** @@ -110,6 +111,7 @@ public static void installPlugin(@Nonnull Project project) { // If already installed, nothing to do if (appModInstalled) { + AppModUtils.logTelemetryEvent("plugin.install-skipped", Map.of("reason", "already-installed")); return; } @@ -128,6 +130,7 @@ public static void installPlugin(@Nonnull Project project) { true, // selectAllInDialog - pre-select all plugins null, // modalityState () -> { + AppModUtils.logTelemetryEvent("plugin.install-complete"); } ); }); diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java index eecb8eda63c..401791d0952 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java @@ -21,6 +21,7 @@ import java.util.Comparator; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; /** @@ -82,6 +83,10 @@ private MigrationState computeState(Project project) { .filter(MigrateNodeData::isVisible) .collect(Collectors.toList()); + if (nodes.isEmpty()) { + AppModUtils.logTelemetryEvent("action.no-options"); + } + return new MigrationState( nodes.isEmpty() ? State.NO_OPTIONS : State.HAS_OPTIONS, nodes @@ -126,7 +131,6 @@ public void update(@NotNull AnActionEvent e) { } @Override - @AzureOperation(name = "user/appmod.migrate_action") public void actionPerformed(@NotNull AnActionEvent e) { final Project project = e.getProject(); if (project == null) { @@ -137,11 +141,12 @@ public void actionPerformed(@NotNull AnActionEvent e) { switch (migrationState.state) { case NOT_INSTALLED: + AppModUtils.logTelemetryEvent("action.click-install"); MigratePluginInstaller.showInstallConfirmation(project, () -> MigratePluginInstaller.installPlugin(project)); break; case NO_OPTIONS: - AppModPanelHelper.openAppModPanel(project); + AppModPanelHelper.openAppModPanel(project, "action"); break; case HAS_OPTIONS: // Handled by popup menu @@ -194,8 +199,8 @@ public void update(@NotNull AnActionEvent e) { } @Override - @AzureOperation(name = "user/appmod.trigger_migrate_option") public void actionPerformed(@NotNull AnActionEvent e) { + AppModUtils.logTelemetryEvent("action.click-option", Map.of("label", nodeData.getLabel())); nodeData.click(e); } }; diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java index ae66f0e9341..7f1193791e3 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java @@ -13,9 +13,9 @@ import com.microsoft.azure.toolkit.lib.common.action.Action; import com.microsoft.azure.toolkit.lib.common.action.ActionGroup; -import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; /** @@ -70,6 +70,7 @@ public void initializeNode() { * Called by RefreshMigrateToAzureAction from context menu. */ public void refresh() { + AppModUtils.logTelemetryEvent("node.refresh"); refreshChildren(); // This rebuilds children from addChildren function } @@ -87,6 +88,7 @@ private void showNotInstalled() { withDescription(description); onClicked(e -> { + AppModUtils.logTelemetryEvent("node.click-install"); MigratePluginInstaller.showInstallConfirmation(project, () -> MigratePluginInstaller.installPlugin(project)); }); } @@ -95,12 +97,16 @@ private void showNotInstalled() { * Load migration options from extension points. */ private List loadMigrationNodeData() { - return childProviders.getExtensionList().stream() + final List nodes = childProviders.getExtensionList().stream() .filter(provider -> provider.isApplicable(project)) .sorted(Comparator.comparingInt(IMigrateOptionProvider::getPriority)) .flatMap(provider -> provider.createNodeData(project).stream()) .filter(MigrateNodeData::isVisible) .collect(Collectors.toList()); + if (nodes.isEmpty()) { + AppModUtils.logTelemetryEvent("node.no-options"); + } + return nodes; } /** @@ -118,7 +124,9 @@ private List> buildChildNodes() { clearClickHandlers(); if (nodeDataList.isEmpty()) { withDescription("Open GitHub Copilot app modernization"); - onClicked(e -> AppModPanelHelper.openAppModPanel(project)); + onClicked(e -> { + AppModPanelHelper.openAppModPanel(project, "node"); + }); } else { withDescription(""); } @@ -144,7 +152,10 @@ private Node convertToNode(MigrateNodeData data) { // Set click handler if (data.hasClickHandler()) { - node.onClicked(d -> data.click(null)); + node.onClicked(d -> { + AppModUtils.logTelemetryEvent("node.click-option", Map.of("label", data.getLabel())); + data.click(null); + }); } // Handle children - lazy or static diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java index b4fab3d0e19..f28053e83d6 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java @@ -11,6 +11,7 @@ import com.intellij.openapi.project.Project; import com.intellij.ui.tree.LeafState; import com.microsoft.azure.toolkit.intellij.appmod.AppModPanelHelper; +import com.microsoft.azure.toolkit.intellij.appmod.AppModUtils; import com.microsoft.azure.toolkit.intellij.appmod.Constants; import com.microsoft.azure.toolkit.intellij.common.IntelliJAzureIcons; import com.microsoft.azure.toolkit.intellij.connector.dotazure.AzureModule; @@ -65,12 +66,16 @@ private List computeMigrationNodes() { if (!MigratePluginInstaller.isAppModPluginInstalled()) { return List.of(); } - return migrationProviders.getExtensionList().stream() + final List nodes = migrationProviders.getExtensionList().stream() .filter(provider -> provider.isApplicable(getProject())) .sorted(Comparator.comparingInt(IMigrateOptionProvider::getPriority)) .flatMap(provider -> provider.createNodeData(getProject()).stream()) .filter(MigrateNodeData::isVisible) .collect(Collectors.toList()); + if (nodes.isEmpty()) { + AppModUtils.logTelemetryEvent("facet.no-options"); + } + return nodes; } /** @@ -94,7 +99,10 @@ public IActionGroup getActionGroup() { final Action refreshAction = new Action<>(Action.Id.of("user/appmod.refresh_migrate_node")) .withLabel("Refresh") .withIcon(AzureIcons.Action.REFRESH.getIconPath()) - .withHandler((v, e) -> refresh()) + .withHandler((v, e) -> { + AppModUtils.logTelemetryEvent("facet.refresh"); + refresh(); + }) .withAuthRequired(false); return new ActionGroup(refreshAction); } @@ -133,11 +141,12 @@ protected void buildView(@Nonnull PresentationData presentation) { public void navigate(boolean requestFocus) { if (!MigratePluginInstaller.isAppModPluginInstalled()) { // Plugin not installed - trigger install on double-click + AppModUtils.logTelemetryEvent("facet.click-install"); MigratePluginInstaller.showInstallConfirmation(getProject(), () -> MigratePluginInstaller.installPlugin(getProject())); } else if (!hasMigrationOptions()) { // No migration options - open App Modernization Panel - AppModPanelHelper.openAppModPanel(getProject()); + AppModPanelHelper.openAppModPanel(getProject(), "facet"); } } @@ -203,6 +212,7 @@ protected void buildView(@Nonnull PresentationData presentation) { @Override public void navigate(boolean requestFocus) { // Trigger click handler + AppModUtils.logTelemetryEvent("facet.click-option", java.util.Map.of("label", nodeData.getLabel())); nodeData.doubleClick(null); } From f1b37914a7321654643fb626648a3f50bdd284fd Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Fri, 23 Jan 2026 15:17:02 +0800 Subject: [PATCH 15/43] set DEBUG_TELEMETRY to false --- .../microsoft/azure/toolkit/intellij/appmod/AppModUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModUtils.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModUtils.java index 9f265fd94b1..49ace1fa9bd 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModUtils.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModUtils.java @@ -21,7 +21,7 @@ public final class AppModUtils { private static final Logger LOG = Logger.getInstance(AppModUtils.class); private static final String SERVICE_NAME = "appmod"; // Set to true to enable telemetry debug logging (for development only) - private static final boolean DEBUG_TELEMETRY = true; + private static final boolean DEBUG_TELEMETRY = false; private AppModUtils() { // Utility class, no instantiation allowed From 0b6f8613010dc2bd94e8dde73d688e8e3fe039db Mon Sep 17 00:00:00 2001 From: Ye Zhu Date: Mon, 26 Jan 2026 15:03:02 +0800 Subject: [PATCH 16/43] Add java-upgrade promotion --- .../build.gradle.kts | 10 + .../intellij/appmod/MigrateToAzureAction.java | 10 +- .../intellij/appmod/MigrateToAzureNode.java | 11 +- .../AppModPluginInstaller.java} | 9 +- .../{ => common}/InstallPluginDialog.java | 4 +- .../JavaUpgradeCheckStartupActivity.java | 99 +++ .../action/UpgradeActionRegistrar.java | 118 +++ .../action/UpgradeInProblemsViewAction.java | 602 ++++++++++++++++ .../UpgradeInQuickFixIntentionAction.java | 222 ++++++ .../action/UpgradeProjectAction.java | 110 +++ .../javaupgrade/dao/JavaUpgradeIssue.java | 136 ++++ .../JavaUpgradeIssuesInspection.java | 277 +++++++ .../javaupgrade/service/CVECheckService.java | 564 +++++++++++++++ .../service/JavaUpgradeIssuesCache.java | 122 ++++ .../JavaUpgradeIssuesDetectionService.java | 675 ++++++++++++++++++ .../JavaVersionNotificationService.java | 555 ++++++++++++++ .../settings/JavaUpgradeConfigurable.java | 155 ++++ .../META-INF/azure-intellij-plugin-appmod.xml | 40 +- .../MigrateToAzureFacetNode.java | 18 +- 19 files changed, 3710 insertions(+), 27 deletions(-) rename PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/{MigratePluginInstaller.java => common/AppModPluginInstaller.java} (95%) rename PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/{ => common}/InstallPluginDialog.java (94%) create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInProblemsViewAction.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInQuickFixIntentionAction.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeProjectAction.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/dao/JavaUpgradeIssue.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/CVECheckService.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesCache.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/settings/JavaUpgradeConfigurable.java diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/build.gradle.kts b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/build.gradle.kts index 2bcc6e6fb46..e1a7df7b7b9 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/build.gradle.kts +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/build.gradle.kts @@ -1,5 +1,15 @@ dependencies { implementation(project(":azure-intellij-plugin-lib")) + implementation(project(":azure-intellij-plugin-lib-java")) // runtimeOnly project(path: ":azure-intellij-plugin-lib", configuration: "instrumentedJar") implementation("com.microsoft.azure:azure-toolkit-ide-common-lib") + intellijPlatform { + // Plugin Dependencies. Uses `platformBundledPlugins` property from the gradle.properties file for bundled IntelliJ Platform plugins. + bundledPlugin("com.intellij.java") + bundledPlugin("org.jetbrains.idea.maven") + // bundledPlugin("org.jetbrains.idea.maven.model") + bundledPlugin("com.intellij.gradle") + // Copilot plugin for Java upgrade integration + plugin("com.github.copilot:1.5.59-243") + } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java index 401791d0952..f2c5945e469 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java @@ -15,7 +15,7 @@ import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Key; -import com.microsoft.azure.toolkit.lib.common.operation.AzureOperation; +import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -72,7 +72,7 @@ private MigrationState getOrComputeState(Project project) { * Computes migration state by calling providers. */ private MigrationState computeState(Project project) { - if (!MigratePluginInstaller.isAppModPluginInstalled()) { + if (!AppModPluginInstaller.isAppModPluginInstalled()) { return new MigrationState(State.NOT_INSTALLED, List.of()); } @@ -109,7 +109,7 @@ public void update(@NotNull AnActionEvent e) { switch (migrationState.state) { case NOT_INSTALLED: - final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); + final boolean copilotInstalled = AppModPluginInstaller.isCopilotInstalled(); e.getPresentation().setText(copilotInstalled ? "Migrate to Azure (Install Github Copilot app modernization)" : "Migrate to Azure (Install GitHub Copilot and app modernization)"); @@ -142,8 +142,8 @@ public void actionPerformed(@NotNull AnActionEvent e) { switch (migrationState.state) { case NOT_INSTALLED: AppModUtils.logTelemetryEvent("action.click-install"); - MigratePluginInstaller.showInstallConfirmation(project, - () -> MigratePluginInstaller.installPlugin(project)); + AppModPluginInstaller.showInstallConfirmation(project, + () -> AppModPluginInstaller.installPlugin(project)); break; case NO_OPTIONS: AppModPanelHelper.openAppModPanel(project, "action"); diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java index 7f1193791e3..a10e4598ff5 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java @@ -10,6 +10,7 @@ import com.microsoft.azure.toolkit.ide.common.component.Node; import com.microsoft.azure.toolkit.ide.common.icon.AzureIcon; import com.microsoft.azure.toolkit.ide.common.icon.AzureIcons; +import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; import com.microsoft.azure.toolkit.lib.common.action.Action; import com.microsoft.azure.toolkit.lib.common.action.ActionGroup; @@ -58,7 +59,7 @@ public void initializeNode() { clearClickHandlers(); withDescription(""); - if (!MigratePluginInstaller.isAppModPluginInstalled()) { + if (!AppModPluginInstaller.isAppModPluginInstalled()) { showNotInstalled(); } // Don't call showMigrationOptions() here - let buildChildNodes() handle it @@ -79,7 +80,7 @@ public Project getProject() { } private void showNotInstalled() { - final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); + final boolean copilotInstalled = AppModPluginInstaller.isCopilotInstalled(); // Dynamic description based on what needs to be installed final String description = copilotInstalled @@ -89,7 +90,7 @@ private void showNotInstalled() { onClicked(e -> { AppModUtils.logTelemetryEvent("node.click-install"); - MigratePluginInstaller.showInstallConfirmation(project, () -> MigratePluginInstaller.installPlugin(project)); + AppModPluginInstaller.showInstallConfirmation(project, () -> AppModPluginInstaller.installPlugin(project)); }); } @@ -114,7 +115,7 @@ private List loadMigrationNodeData() { * Also updates description and click handler based on data. */ private List> buildChildNodes() { - if (!MigratePluginInstaller.isAppModPluginInstalled()) { + if (!AppModPluginInstaller.isAppModPluginInstalled()) { return List.of(); } @@ -183,6 +184,6 @@ public synchronized void refreshView() { } public static boolean isPluginInstalled() { - return MigratePluginInstaller.isAppModPluginInstalled(); + return AppModPluginInstaller.isAppModPluginInstalled(); } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java similarity index 95% rename from PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java rename to PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java index 6290727cf83..51c0c60a008 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigratePluginInstaller.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java @@ -3,16 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -package com.microsoft.azure.toolkit.intellij.appmod; +package com.microsoft.azure.toolkit.intellij.appmod.common; import com.intellij.ide.plugins.IdeaPluginDescriptor; import com.intellij.ide.plugins.PluginManagerCore; -import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.extensions.PluginId; import com.intellij.openapi.project.Project; import com.intellij.openapi.updateSettings.impl.pluginsAdvertisement.PluginsAdvertiser; -import com.microsoft.azure.toolkit.lib.common.event.AzureEventBus; import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager; +import com.microsoft.azure.toolkit.intellij.appmod.AppModUtils; import javax.annotation.Nonnull; import java.util.LinkedHashSet; @@ -24,11 +23,11 @@ * This centralizes all plugin detection and installation logic to avoid code duplication * between MigrateToAzureNode and MigrateToAzureAction. */ -public class MigratePluginInstaller { +public class AppModPluginInstaller { private static final String PLUGIN_ID = "com.github.copilot.appmod"; private static final String COPILOT_PLUGIN_ID = "com.github.copilot"; - private MigratePluginInstaller() { + private AppModPluginInstaller() { // Utility class - prevent instantiation } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/InstallPluginDialog.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/InstallPluginDialog.java similarity index 94% rename from PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/InstallPluginDialog.java rename to PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/InstallPluginDialog.java index 9eaa3615e2d..0989aa160fa 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/InstallPluginDialog.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/InstallPluginDialog.java @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -package com.microsoft.azure.toolkit.intellij.appmod; +package com.microsoft.azure.toolkit.intellij.appmod.common; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.DialogWrapper; @@ -62,7 +62,7 @@ public void show() { } else { labelComponent = new JLabel(label); } - labelComponent.setHorizontalAlignment(SwingConstants.CENTER); + labelComponent.setHorizontalAlignment(SwingConstants.LEFT); panel.add(labelComponent, BorderLayout.CENTER); return panel; } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java new file mode 100644 index 00000000000..b0cc134fb81 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade; + +import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer; +import com.intellij.openapi.project.DumbAware; +import com.intellij.openapi.project.DumbService; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.startup.ProjectActivity; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesCache; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; +import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager; +import kotlin.Unit; +import kotlin.coroutines.Continuation; +import reactor.core.publisher.Mono; + +import javax.annotation.Nonnull; +import java.time.Duration; +import java.util.List; + +/** + * Startup activity that detects outdated JDK and framework versions when a project is opened. + * This runs after the project is fully loaded and shows notifications for any detected issues. + */ +public class JavaUpgradeCheckStartupActivity implements ProjectActivity, DumbAware { + + // Additional delay after smart mode to ensure Maven/Gradle sync is complete + private static final long POST_INDEXING_DELAY_SECONDS = 3; + + @Override + public Object execute(@Nonnull Project project, @Nonnull Continuation continuation) { + // Wait for indexing to complete before running the check + DumbService.getInstance(project).runWhenSmart(() -> { + // Add a small delay after smart mode to ensure Maven/Gradle sync is done + Mono.delay(Duration.ofSeconds(POST_INDEXING_DELAY_SECONDS)) + .subscribe( + next -> { + if (project.isDisposed()) { + return; + } + performJavaUpgradeCheck(project); + }, + error -> { /* Error during Java upgrade check startup */ } + ); + }); + + return null; + } + + /** + * Performs the jdk version, framework version and CVE issue check and shows notifications for any issues found. + */ + private void performJavaUpgradeCheck(@Nonnull Project project) { + try { + // Run the analysis in a background thread + AzureTaskManager.getInstance().runInBackground("Checking Java upgrade issues", () -> { + if (project.isDisposed()) { + return; + } + + // Refresh the cache (this populates JDK and dependency issues for use by inspections) + final JavaUpgradeIssuesCache cache = JavaUpgradeIssuesCache.getInstance(project); + cache.refresh(); + + // Get all issues including CVEs + final JavaUpgradeIssuesDetectionService detectionService = JavaUpgradeIssuesDetectionService.getInstance(); + final List allIssues = new java.util.ArrayList<>(); + allIssues.addAll(cache.getJdkIssues()); + allIssues.addAll(cache.getDependencyIssues()); + allIssues.addAll(detectionService.getCVEIssues(project)); + + // Update UI on the main thread + AzureTaskManager.getInstance().runLater(() -> { + if (project.isDisposed()) { + return; + } + + // Restart code analysis to refresh inspections in open editors + // This ensures wavy underlines appear for JDK/framework issues + DaemonCodeAnalyzer.getInstance(project).restart(); + + // Show notifications if there are issues + if (!allIssues.isEmpty()) { + final JavaVersionNotificationService notificationService = JavaVersionNotificationService.getInstance(); + notificationService.showNotifications(project, allIssues); + } + }); + }); + + } catch (Exception e) { + // Error performing Java version check + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java new file mode 100644 index 00000000000..fbb892636d8 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action; + +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.DefaultActionGroup; +import com.intellij.openapi.actionSystem.Presentation; +import com.intellij.openapi.actionSystem.Separator; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.startup.ProjectActivity; +import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; + +import kotlin.Unit; +import kotlin.coroutines.Continuation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Registers the Upgrade action into the GitHub Copilot context menu at runtime. + * This is needed because the Copilot plugin creates its context menu groups dynamically. + */ +public class UpgradeActionRegistrar implements ProjectActivity { + + private static final Logger LOG = Logger.getInstance(UpgradeActionRegistrar.class); + private static final String UPGRADE_ACTION_ID = "AzureToolkit.UpgradeProject"; + private static final String PROJECT_VIEW_POPUP_MENU = "ProjectViewPopupMenu"; + + @Nullable + @Override + public Object execute(@NotNull Project project, @NotNull Continuation continuation) { + discoverAndRegisterAction(); + return Unit.INSTANCE; + } + + private void discoverAndRegisterAction() { + // Only proceed if Copilot plugin is installed + if (!AppModPluginInstaller.isCopilotInstalled()) { + return; + } + + ActionManager actionManager = ActionManager.getInstance(); + + LOG.info("=== Searching for GitHub Copilot submenu in ProjectViewPopupMenu ==="); + + // Get the ProjectViewPopupMenu group + AnAction projectViewPopup = actionManager.getAction(PROJECT_VIEW_POPUP_MENU); + if (projectViewPopup instanceof DefaultActionGroup) { + DefaultActionGroup popupGroup = (DefaultActionGroup) projectViewPopup; + + // Search for the GitHub Copilot submenu within ProjectViewPopupMenu + DefaultActionGroup copilotGroup = findCopilotSubmenu(popupGroup, actionManager); + + if (copilotGroup != null) { + tryAddToGroup(actionManager, copilotGroup, "GitHub Copilot submenu"); + } else { + LOG.info("GitHub Copilot submenu not found in ProjectViewPopupMenu"); + } + } else { + LOG.warn("ProjectViewPopupMenu not found or not a DefaultActionGroup"); + } + + LOG.info("=== End Copilot Action Discovery ==="); + } + + /** + * Search for the GitHub Copilot submenu within a parent group. + * The Copilot plugin creates this dynamically, so we search by exact presentation text. + */ + private DefaultActionGroup findCopilotSubmenu(DefaultActionGroup parentGroup, ActionManager actionManager) { + for (AnAction child : parentGroup.getChildActionsOrStubs()) { + if (child instanceof DefaultActionGroup) { + DefaultActionGroup childGroup = (DefaultActionGroup) child; + Presentation presentation = childGroup.getTemplatePresentation(); + String text = presentation.getText(); + String actionId = actionManager.getId(child); + + // Match exactly "GitHub Copilot" to avoid false positives + if ("GitHub Copilot".equals(text)) { + LOG.info("Found Copilot submenu by exact text match: " + text + ", id=" + actionId); + return childGroup; + } + } + } + return null; + } + + private void tryAddToGroup(ActionManager actionManager, DefaultActionGroup group, String groupId) { + AnAction upgradeAction = actionManager.getAction(UPGRADE_ACTION_ID); + if (upgradeAction == null) { + LOG.warn("Upgrade action not found: " + UPGRADE_ACTION_ID); + return; + } + + // Check if action is not already added + if (!containsAction(group, UPGRADE_ACTION_ID, actionManager)) { + // Add a separator before the upgrade action to visually group it + group.add(Separator.create()); + group.add(upgradeAction); + LOG.info("Successfully added upgrade action to group: " + groupId); + } else { + LOG.info("Upgrade action already exists in group: " + groupId); + } + } + + private boolean containsAction(DefaultActionGroup group, String actionId, ActionManager actionManager) { + for (AnAction action : group.getChildActionsOrStubs()) { + if (action != null && actionId.equals(actionManager.getId(action))) { + return true; + } + } + return false; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInProblemsViewAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInProblemsViewAction.java new file mode 100644 index 00000000000..6746e560406 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInProblemsViewAction.java @@ -0,0 +1,602 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action; + +import com.intellij.analysis.problemsView.Problem; +import com.intellij.openapi.actionSystem.ActionUpdateThread; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.actionSystem.PlatformDataKeys; +import com.intellij.openapi.project.DumbAware; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiManager; +import com.intellij.psi.xml.XmlFile; +import com.intellij.psi.xml.XmlTag; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesCache; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; + +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService.*; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + + +/** + * Action to fix vulnerable dependencies by opening GitHub Copilot chat with an upgrade prompt. + * This action appears in the Problems View context menu for vulnerable dependency issues. + */ +public class UpgradeInProblemsViewAction extends AnAction implements DumbAware { + + private static final String CVE_MARKER = "CVE-"; + + // Data key for problems in the Problems View + private static final String PROBLEMS_VIEW_PROBLEM_KEY = "Problem"; + + public UpgradeInProblemsViewAction() { + super(); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + final Project project = e.getData(CommonDataKeys.PROJECT); + if (project == null || project.isDisposed()) { + return; + } + + // Try to get issue from cache using PsiElement context + final JavaUpgradeIssue cachedIssue = findIssueFromContext(e, project); + if (cachedIssue != null) { + final String prompt = buildPromptFromIssue(cachedIssue); + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + return; + } + + // Fallback: Get the problem description from the context + final String problemDescription = extractProblemDescription(e); + + if (problemDescription != null && !problemDescription.isEmpty()) { + // Extract dependency info and CVE from the problem description + final String prompt = buildUpgradePrompt(problemDescription); + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + } else { + // Fallback: generic CVE fix prompt + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt( + project, + "run CVE scan for this project using java upgrade tools by invoking #validate_cves_for_java" + ); + } + } + + /** + * Tries to find the JavaUpgradeIssue from the context by examining the PsiElement. + */ + @Nullable + private JavaUpgradeIssue findIssueFromContext(@NotNull AnActionEvent e, @NotNull Project project) { + final JavaUpgradeIssuesCache cache = JavaUpgradeIssuesCache.getInstance(project); + if (!cache.isInitialized()) { + return null; + } + + // Try to get PsiElement directly + PsiElement element = e.getData(CommonDataKeys.PSI_ELEMENT); + + // If no direct element, try to get from file + offset + if (element == null) { + element = findElementFromProblem(e, project); + } + + if (element == null) { + return null; + } + + // Navigate to find dependency/parent context + return findIssueFromElement(element, cache); + } + + /** + * Finds the PsiElement from a Problem in the Problems View. + * Uses reflection for cross-version compatibility as Problem API varies. + */ + @Nullable + private PsiElement findElementFromProblem(@NotNull AnActionEvent e, @NotNull Project project) { + try { + final Object problemData = e.getDataContext().getData(PROBLEMS_VIEW_PROBLEM_KEY); + if (problemData instanceof Problem) { + final Problem problem = (Problem) problemData; + + // Use reflection to get file - API varies across IntelliJ versions + VirtualFile file = null; + try { + java.lang.reflect.Method getFileMethod = problem.getClass().getMethod("getFile"); + Object fileObj = getFileMethod.invoke(problem); + if (fileObj instanceof VirtualFile) { + file = (VirtualFile) fileObj; + } + } catch (Exception ignored) { + // getFile method might not exist in this version + System.out.println("error" + ignored.getMessage()); + } + + if (file != null && file.getName().equals("pom.xml")) { + final PsiFile psiFile = PsiManager.getInstance(project).findFile(file); + if (psiFile instanceof XmlFile) { + // Try to get offset from problem and find element + try { + java.lang.reflect.Method getOffsetMethod = problem.getClass().getMethod("getOffset"); + Object offsetObj = getOffsetMethod.invoke(problem); + if (offsetObj instanceof Integer) { + int offset = (Integer) offsetObj; + if (offset >= 0) { + return psiFile.findElementAt(offset); + } + } + } catch (Exception ignored) { + // getOffset method might not exist + System.out.println("error" + ignored.getMessage()); + } + } + } + } + } catch (Exception ignored) { + System.out.println("error" + ignored.getMessage()); + + } + return null; + } + + /** + * Finds the JavaUpgradeIssue based on the XML element context. + */ + @Nullable + private JavaUpgradeIssue findIssueFromElement(@NotNull PsiElement element, @NotNull JavaUpgradeIssuesCache cache) { + // Find the containing XmlTag + XmlTag tag = findParentTag(element); + if (tag == null) { + return null; + } + + // Check if this is a Java version property + if (isJavaVersionContext(tag)) { + return cache.getJdkIssue(); + } + + // Check if this is a dependency or parent version + final String groupId = extractGroupId(tag); + if (groupId != null) { + if (groupId.equals(GROUP_ID_SPRING_BOOT)) { + return cache.findDependencyIssue(GROUP_ID_SPRING_BOOT); + } else if (groupId.equals(GROUP_ID_SPRING_SECURITY)) { + return cache.findDependencyIssue(GROUP_ID_SPRING_SECURITY); + } else if (groupId.equals(GROUP_ID_SPRING_FRAMEWORK)) { + return cache.findDependencyIssue(GROUP_ID_SPRING_FRAMEWORK + ":"); + } + } + + return null; + } + + /** + * Finds the parent XmlTag of an element. + */ + @Nullable + private XmlTag findParentTag(@NotNull PsiElement element) { + PsiElement current = element; + while (current != null) { + if (current instanceof XmlTag) { + return (XmlTag) current; + } + current = current.getParent(); + } + return null; + } + + /** + * Checks if the tag is in a Java version context. + */ + private boolean isJavaVersionContext(@NotNull XmlTag tag) { + String tagName = tag.getName(); + + // Check for properties like java.version, maven.compiler.source, etc. + if ("java.version".equals(tagName) || + "maven.compiler.source".equals(tagName) || + "maven.compiler.target".equals(tagName) || + "maven.compiler.release".equals(tagName)) { + XmlTag parent = tag.getParentTag(); + return parent != null && "properties".equals(parent.getName()); + } + + // Check for maven-compiler-plugin source/target/release + if ("source".equals(tagName) || "target".equals(tagName) || "release".equals(tagName)) { + XmlTag current = tag.getParentTag(); + while (current != null) { + if ("plugin".equals(current.getName())) { + XmlTag artifactIdTag = current.findFirstSubTag("artifactId"); + if (artifactIdTag != null && ARTIFACT_ID_MAVEN_COMPILER_PLUGIN.equals(artifactIdTag.getValue().getText())) { + return true; + } + } + current = current.getParentTag(); + } + } + + return false; + } + + /** + * Extracts the groupId from a dependency or parent tag context. + */ + @Nullable + private String extractGroupId(@NotNull XmlTag tag) { + // If we're on a version tag, look at parent (dependency or parent) + XmlTag container = tag; + if ("version".equals(tag.getName())) { + container = tag.getParentTag(); + } + + if (container == null) { + return null; + } + + // Check if it's a dependency or parent tag + String containerName = container.getName(); + if ("dependency".equals(containerName) || "parent".equals(containerName)) { + XmlTag groupIdTag = container.findFirstSubTag("groupId"); + if (groupIdTag != null) { + return groupIdTag.getValue().getText(); + } + } + + return null; + } + + /** + * Builds a prompt from a cached JavaUpgradeIssue. + */ + @NotNull + private String buildPromptFromIssue(@NotNull JavaUpgradeIssue issue) { + String packageId = issue.getPackageId(); + + // JDK upgrade + if (PACKAGE_ID_JDK.equals(packageId)) { + return String.format( + "Upgrade Java runtime from version %s to Java %s (LTS) using java upgrade tools by invoking #generate_upgrade_plan", + issue.getCurrentVersion(), issue.getSuggestedVersion() + ); + } + + // Framework upgrade (Spring Boot, Spring Framework, etc.) + return String.format( + "Upgrade %s from version %s to %s using java upgrade tools by invoking #generate_upgrade_plan", + issue.getPackageDisplayName(), issue.getCurrentVersion(), issue.getSuggestedVersion() + ); + } + + @Override + public void update(@NotNull AnActionEvent e) { + final Project project = e.getData(CommonDataKeys.PROJECT); + if (project == null || project.isDisposed()) { + e.getPresentation().setEnabledAndVisible(false); + return; + } + + // Check if we're in the Problems View context with a vulnerability + final String description = extractProblemDescription(e); + if (description == null) { + e.getPresentation().setEnabledAndVisible(false); + return; + } + + // Also check if the file is pom.xml or build.gradle (common for dependency issues) + final VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE); + final boolean isBuildFile = file != null && + (file.getName().equals("pom.xml") || file.getName().endsWith(".gradle") || file.getName().endsWith(".gradle.kts")); + + final boolean isVulnerability = isVulnerabilityDescription(description); + + if (!isBuildFile && !isVulnerability) { + e.getPresentation().setEnabledAndVisible(false); + return; + } + + // Set dynamic text based on issue type detected from description + String actionText = getDynamicActionText(description, project); + e.getPresentation().setText(actionText); + e.getPresentation().setEnabledAndVisible(true); + } + + /** + * Gets dynamic action text based on the problem description. + */ + private String getDynamicActionText(@NotNull String description, @NotNull Project project) { + // Try to detect JDK issue + if (description.toLowerCase().contains("jdk") || + description.toLowerCase().contains("java runtime") || + description.toLowerCase().contains("java version")) { + return "Upgrade JDK with Copilot"; + } + + // Try to detect Spring Boot + if (description.toLowerCase().contains("spring boot")) { + return "Upgrade Spring Boot with Copilot"; + } + + // Try to detect Spring Framework + if (description.toLowerCase().contains("spring framework")) { + return "Upgrade Spring Framework with Copilot"; + } + + // Try to detect Spring Security + if (description.toLowerCase().contains("spring security")) { + return "Upgrade Spring Security with Copilot"; + } + + // Try to get from cache if available + final JavaUpgradeIssuesCache cache = JavaUpgradeIssuesCache.getInstance(project); + if (cache.isInitialized()) { + // Check for JDK issue + if (cache.getJdkIssue() != null && description.contains(cache.getJdkIssue().getMessage())) { + return "Upgrade JDK with Copilot"; + } + // Check for Spring Boot + JavaUpgradeIssue springBootIssue = cache.findDependencyIssue(GROUP_ID_SPRING_BOOT); + if (springBootIssue != null && description.contains(springBootIssue.getMessage())) { + return "Upgrade Spring Boot with Copilot"; + } + } + + // Default text + return "Scan and Resolve CVEs with Copilot"; + } + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.BGT; + } + + /** + * Checks if the description indicates a vulnerability. + */ + private boolean isVulnerabilityDescription(@NotNull String description) { + final String lowerDescription = description.toLowerCase(); + return lowerDescription.contains("vulnerable") || + lowerDescription.contains("cve-") || + lowerDescription.contains("security") || + lowerDescription.contains("vulnerability"); + } + + /** + * Extracts the problem description from the action event context. + */ + @Nullable + private String extractProblemDescription(@NotNull AnActionEvent e) { + // Try multiple approaches to get the problem description + + // Approach 1: Try to get Problem object directly from Problems View + try { + final Object problemData = e.getDataContext().getData(PROBLEMS_VIEW_PROBLEM_KEY); + if (problemData instanceof Problem) { + final Problem problem = (Problem) problemData; + final String text = problem.getText(); + if (text != null && !text.isEmpty()) { + return text; + } + } + } catch (Exception ignored) { + // Problem class might not be available + } + + // Approach 2: Try "problem.description" data key + try { + @SuppressWarnings("deprecation") + final Object data = e.getDataContext().getData("problem.description"); + if (data instanceof String && !((String) data).isEmpty()) { + return (String) data; + } + } catch (Exception ignored) { + } + + // Approach 3: Try to get selected items from the tree + try { + final Object[] selectedItems = e.getData(PlatformDataKeys.SELECTED_ITEMS); + if (selectedItems != null && selectedItems.length > 0) { + // Concatenate all selected items' string representations + final StringBuilder sb = new StringBuilder(); + for (Object item : selectedItems) { + if (item != null) { + sb.append(item.toString()).append(" "); + } + } + final String result = sb.toString().trim(); + if (!result.isEmpty()) { + return result; + } + } + } catch (Exception ignored) { + } + + // Approach 4: Try SELECTED_ITEM + try { + final Object selectedItem = e.getData(PlatformDataKeys.SELECTED_ITEM); + if (selectedItem != null) { + final String text = selectedItem.toString(); + if (!text.isEmpty()) { + return text; + } + } + } catch (Exception ignored) { + } + + // Approach 5: Try getting from context component + try { + final java.awt.Component component = e.getData(PlatformDataKeys.CONTEXT_COMPONENT); + if (component instanceof javax.swing.JTree) { + final javax.swing.JTree tree = (javax.swing.JTree) component; + final javax.swing.tree.TreePath[] paths = tree.getSelectionPaths(); + if (paths != null && paths.length > 0) { + final StringBuilder sb = new StringBuilder(); + for (javax.swing.tree.TreePath path : paths) { + final Object lastComponent = path.getLastPathComponent(); + if (lastComponent != null) { + sb.append(lastComponent.toString()).append(" "); + } + } + final String result = sb.toString().trim(); + if (!result.isEmpty()) { + return result; + } + } + } + } catch (Exception ignored) { + } + + // Approach 6: Try getting selected text from editor + try { + final com.intellij.openapi.editor.Editor editor = e.getData(CommonDataKeys.EDITOR); + if (editor != null && editor.getSelectionModel().hasSelection()) { + final String selectedText = editor.getSelectionModel().getSelectedText(); + if (selectedText != null && !selectedText.isEmpty()) { + return selectedText; + } + } + } catch (Exception ignored) { + } + + return null; + } + + /** + * Builds an upgrade prompt based on the problem description. + */ + private String buildUpgradePrompt(@NotNull String problemDescription) { + + // Extract dependency coordinates if present + final String dependency = extractDependencyCoordinates(problemDescription); + + if (problemDescription != null) { + // Try to detect JDK issue + if (problemDescription.toLowerCase().contains("jdk") || + problemDescription.toLowerCase().contains("java runtime") || + problemDescription.toLowerCase().contains("java version")) { + return "upgrade java runtime to Java " + MATURE_JAVA_LTS_VERSION + " (LTS) using java upgrade tools by invoking #generate_upgrade_plan"; + } + + // Try to detect Spring Boot + if (problemDescription.toLowerCase().contains("spring boot") || problemDescription.toLowerCase().contains("spring framework") || problemDescription.toLowerCase().contains("spring security")) { + return "upgrade java framework dependencies of this project to latest LTS version using java upgrade tools by invoking #generate_upgrade_plan"; + } + + } + // Default text + return "run CVE scan for this project using java upgrade tools by invoking #validate_cves_for_java"; + } + + /** + * Extracts CVE ID from the problem description. + */ + private String extractCVEId(@NotNull String description) { + // Pattern: CVE-YYYY-NNNNN + final int cveIndex = description.toUpperCase().indexOf(CVE_MARKER); + if (cveIndex >= 0) { + final int endIndex = findCVEEndIndex(description, cveIndex); + if (endIndex > cveIndex) { + return description.substring(cveIndex, endIndex); + } + } + return null; + } + + /** + * Finds the end index of a CVE ID in the description. + */ + private int findCVEEndIndex(@NotNull String description, int startIndex) { + int index = startIndex + CVE_MARKER.length(); + // Skip year (4 digits) + while (index < description.length() && Character.isDigit(description.charAt(index))) { + index++; + } + // Skip separator + if (index < description.length() && description.charAt(index) == '-') { + index++; + } + // Skip ID number + while (index < description.length() && Character.isDigit(description.charAt(index))) { + index++; + } + return index; + } + + /** + * Extracts Maven dependency coordinates from the problem description. + * Looks for patterns like groupId:artifactId:version + */ + private String extractDependencyCoordinates(@NotNull String description) { + // Look for Maven coordinate pattern: groupId:artifactId:version + // Common patterns in vulnerability reports + final String[] patterns = { + "maven:", // maven:groupId:artifactId:version + "dependency " // dependency groupId:artifactId + }; + + for (String pattern : patterns) { + final int index = description.toLowerCase().indexOf(pattern); + if (index >= 0) { + return extractCoordinatesAfterPattern(description, index + pattern.length()); + } + } + + // Try to find standalone coordinate pattern (e.g., org.example:artifact:1.0.0) + return findStandaloneCoordinates(description); + } + + /** + * Extracts coordinates after a known pattern. + */ + private String extractCoordinatesAfterPattern(@NotNull String description, int startIndex) { + final StringBuilder coords = new StringBuilder(); + int colonCount = 0; + + for (int i = startIndex; i < description.length(); i++) { + final char c = description.charAt(i); + if (Character.isLetterOrDigit(c) || c == '.' || c == '-' || c == '_') { + coords.append(c); + } else if (c == ':' && colonCount < 2) { + coords.append(c); + colonCount++; + } else if (!coords.isEmpty()) { + break; + } + } + + final String result = coords.toString(); + return result.contains(":") ? result : null; + } + + /** + * Finds standalone Maven coordinates in the description. + */ + private String findStandaloneCoordinates(@NotNull String description) { + // Simple heuristic: look for pattern like "org.xxx:xxx" or "com.xxx:xxx" + final String[] prefixes = {"org.", "com.", "io.", "net."}; + + for (String prefix : prefixes) { + int index = description.indexOf(prefix); + while (index >= 0) { + final String coords = extractCoordinatesAfterPattern(description, index); + if (coords != null && coords.contains(":")) { + return coords; + } + index = description.indexOf(prefix, index + 1); + } + } + + return null; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInQuickFixIntentionAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInQuickFixIntentionAction.java new file mode 100644 index 00000000000..ff3db6287ef --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInQuickFixIntentionAction.java @@ -0,0 +1,222 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action; + +import com.intellij.codeInsight.intention.IntentionAction; +import com.intellij.codeInsight.intention.PriorityAction; +import com.intellij.codeInspection.util.IntentionFamilyName; +import com.intellij.codeInspection.util.IntentionName; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiFile; +import com.intellij.util.IncorrectOperationException; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; + +import org.jetbrains.annotations.NotNull; + +/** + * Intention action to upgrade vulnerable dependencies using GitHub Copilot. + * This action appears in the editor's quick-fix popup (More actions...) for pom.xml files + * when a vulnerable dependency is detected. + */ +public class UpgradeInQuickFixIntentionAction implements IntentionAction, PriorityAction { + + private static final String DEFAULT_TEXT = "Scan and Resolve CVEs by Copilot"; + + // Cached dependency info from isAvailable() for use in getText() + private String cachedGroupId; + private String cachedArtifactId; + + @Override + public @IntentionName @NotNull String getText() { + // Return dynamic text based on cached dependency info + if (cachedGroupId != null) { + // Use a friendly display name for known dependencies + String displayName = getDisplayName(cachedGroupId, cachedArtifactId); + return displayName; + } + return DEFAULT_TEXT; + } + + /** + * Gets a friendly display name for a dependency. + */ + private String getDisplayName(String groupId, String artifactId) { + return "Scan and Resolve CVEs by Copilot"; +// if (groupId == null) { +// return "Dependency"; +// } +// +// // Map known groupIds to friendly names +// if (groupId.equals("org.springframework.boot")) { +// return "Upgrade Spring Boot with Copilot"; +// } else if (groupId.equals("org.springframework.security")) { +// return "Upgrade Spring Security with Copilot"; +// } else if (groupId.equals("org.springframework")) { +// return "Spring Framework"; +// } else if (groupId.startsWith("org.springframework")) { +// return "Spring " + (artifactId != null ? artifactId : "dependency"); +// } +// +// // For other dependencies, use groupId:artifactId or just groupId +// if (artifactId != null) { +// return groupId + ":" + artifactId; +// } +// return groupId; + } + + @Override + public @IntentionFamilyName @NotNull String getFamilyName() { + return "Azure Toolkit"; + } + + @Override + public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) { + // Reset cached values + cachedGroupId = null; + cachedArtifactId = null; + + if (file == null || editor == null) { + return false; + } + + // Only available for pom.xml files + final String fileName = file.getName(); + if (!fileName.equals("pom.xml")) { + return false; + } + + try { + final int offset = editor.getCaretModel().getOffset(); + final String documentText = editor.getDocument().getText(); + + // Try to extract dependency info - only show if cursor is within a block + final int dependencyStart = findDependencyStart(documentText, offset); + final int dependencyEnd = findDependencyEnd(documentText, offset); + + if (dependencyStart >= 0 && dependencyEnd > dependencyStart) { + final String dependencyBlock = documentText.substring(dependencyStart, dependencyEnd); + cachedGroupId = extractXmlValue(dependencyBlock, "groupId"); + cachedArtifactId = extractXmlValue(dependencyBlock, "artifactId"); + + // Only show if we have valid dependency info (not for parent/plugin sections) + return cachedGroupId != null && cachedArtifactId != null; + } + } catch (Exception e) { + // Ignore and return false + } + + return false; + } + + @Override + public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException { + if (file == null || editor == null) { + return; + } + + // Try to extract dependency information from the current context + final String prompt = buildPromptFromContext(editor, file); + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + } + + /** + * Builds a prompt based on the current editor context. + */ + private String buildPromptFromContext(@NotNull Editor editor, @NotNull PsiFile file) { +// try { +// final int offset = editor.getCaretModel().getOffset(); +// final String documentText = editor.getDocument().getText(); +// +// // Find the dependency block around the cursor +// final int dependencyStart = findDependencyStart(documentText, offset); +// final int dependencyEnd = findDependencyEnd(documentText, offset); +// +// if (dependencyStart >= 0 && dependencyEnd > dependencyStart) { +// final String dependencyBlock = documentText.substring(dependencyStart, dependencyEnd); +// +// // Extract groupId and artifactId +// final String groupId = extractXmlValue(dependencyBlock, "groupId"); +// final String artifactId = extractXmlValue(dependencyBlock, "artifactId"); +// final String version = extractXmlValue(dependencyBlock, "version"); +// +// if (groupId != null && artifactId != null) { +// final StringBuilder prompt = new StringBuilder(); +// prompt.append("Fix security vulnerabilities in "); +// prompt.append(groupId).append(":").append(artifactId); +// if (version != null) { +// prompt.append(":").append(version); +// } +// prompt.append(" by using #validate_cves_for_java"); +// return prompt.toString(); +// } +// } +// } catch (Exception e) { +// // Fall back to generic prompt +// } + + return "run CVE scan for this project using java upgrade tools by invoking #validate_cves_for_java"; + } + + /** + * Finds the start of the dependency block containing the given offset. + */ + private int findDependencyStart(@NotNull String text, int offset) { + // Look for tag before the offset + int searchStart = Math.max(0, offset - 500); + String searchArea = text.substring(searchStart, offset); + int lastDependency = searchArea.lastIndexOf(""); + if (lastDependency >= 0) { + return searchStart + lastDependency; + } + return -1; + } + + /** + * Finds the end of the dependency block containing the given offset. + */ + private int findDependencyEnd(@NotNull String text, int offset) { + // Look for tag after the offset + int searchEnd = Math.min(text.length(), offset + 500); + String searchArea = text.substring(offset, searchEnd); + int endDependency = searchArea.indexOf(""); + if (endDependency >= 0) { + return offset + endDependency + "".length(); + } + return -1; + } + + /** + * Extracts a value from an XML tag. + */ + private String extractXmlValue(@NotNull String xml, @NotNull String tagName) { + final String startTag = "<" + tagName + ">"; + final String endTag = ""; + + int start = xml.indexOf(startTag); + if (start < 0) { + return null; + } + start += startTag.length(); + + int end = xml.indexOf(endTag, start); + if (end < 0) { + return null; + } + + return xml.substring(start, end).trim(); + } + + @Override + public boolean startInWriteAction() { + return false; + } + + @Override + public @NotNull Priority getPriority() { + return Priority.NORMAL; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeProjectAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeProjectAction.java new file mode 100644 index 00000000000..890848e1d94 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeProjectAction.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action; + +import com.intellij.openapi.actionSystem.ActionUpdateThread; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; + +import org.jetbrains.annotations.NotNull; + +/** + * Action to upgrade a Java project using GitHub Copilot. + * This action appears in the GitHub Copilot submenu when right-clicking on: + * - Project root folder + * - pom.xml (Maven projects) + * - build.gradle or build.gradle.kts (Gradle projects) + */ +public class UpgradeProjectAction extends AnAction { + + private static final String UPGRADE_JAVA_PROMPT = "Upgrade java runtime and java framework dependencies of this project to the latest LTS version using java upgrade tools by invoking #generate_upgrade_plan"; + + // text, description, and icon are defined in azure-intellij-plugin-appmod.xml + public UpgradeProjectAction() { + super(); + } + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.BGT; + } + + @Override + public void update(@NotNull AnActionEvent e) { + final Project project = e.getProject(); + final VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE); + + boolean visible = false; + + if (project != null && file != null) { + // Check if it's a project root, pom.xml, or build.gradle file + visible = isProjectRoot(project, file) || + isMavenBuildFile(file) || + isGradleBuildFile(file); + } + + e.getPresentation().setEnabledAndVisible(visible); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + final Project project = e.getProject(); + if (project == null) { + return; + } + + final VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE); + String prompt = buildUpgradePrompt(project, file); + + // Open Copilot chat with the upgrade prompt + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + } + + /** + * Builds the upgrade prompt based on the selected file. + */ + private String buildUpgradePrompt(Project project, VirtualFile file) { + return UPGRADE_JAVA_PROMPT; + } + + /** + * Checks if the file is the project root directory. + */ + private boolean isProjectRoot(Project project, VirtualFile file) { + if (!file.isDirectory()) { + return false; + } + + final VirtualFile projectBaseDir = project.getBaseDir(); + if (projectBaseDir == null) { + return false; + } + + return file.equals(projectBaseDir); + } + + /** + * Checks if the file is a Maven build file (pom.xml). + */ + private boolean isMavenBuildFile(VirtualFile file) { + return file != null && !file.isDirectory() && "pom.xml".equals(file.getName()); + } + + /** + * Checks if the file is a Gradle build file (build.gradle or build.gradle.kts). + */ + private boolean isGradleBuildFile(VirtualFile file) { + if (file == null || file.isDirectory()) { + return false; + } + final String name = file.getName(); + return "build.gradle".equals(name) || "build.gradle.kts".equals(name); + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/dao/JavaUpgradeIssue.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/dao/JavaUpgradeIssue.java new file mode 100644 index 00000000000..8224dc19887 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/dao/JavaUpgradeIssue.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao; + +import lombok.Builder; +import lombok.Data; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Represents a detected upgrade issue in a Java project. + * This class is aligned with the TypeScript implementation in vscode-java-dependency. + * + * @see type.ts + */ +@Data +@Builder +public class JavaUpgradeIssue { + + /** + * The reason why an upgrade is recommended. + * Aligned with UpgradeReason enum from vscode-java-dependency. + */ + public enum UpgradeReason { + /** + * The version has reached end of life and is no longer maintained. + */ + END_OF_LIFE, + /** + * The version is deprecated and should be upgraded. + */ + DEPRECATED, + /** + * The version has known security vulnerabilities (CVEs). + */ + CVE, + /** + * The JRE/JDK version is too old compared to the mature LTS version. + */ + JRE_TOO_OLD + } + + /** + * Severity level of the issue. + */ + public enum Severity { + /** + * The version is very old and may have security vulnerabilities or be unsupported. + */ + CRITICAL, + /** + * The version is outdated and should be upgraded. + */ + WARNING, + /** + * The version is slightly behind the latest, informational only. + */ + INFO + } + + /** + * The package identifier (groupId:artifactId or "jdk" for JDK issues). + */ + @Nonnull + private String packageId; + + /** + * The display name of the package (e.g., "Spring Boot", "JDK"). + */ + @Nonnull + private String packageDisplayName; + + /** + * The reason why upgrade is recommended. + */ + @Nonnull + private UpgradeReason upgradeReason; + + /** + * Severity level of this issue. + */ + @Nonnull + private Severity severity; + + /** + * The current version detected in the project. + */ + @Nullable + private String currentVersion; + + /** + * The supported version range (e.g., "2.7.x || >=3.2"). + */ + @Nullable + private String supportedVersion; + + /** + * The suggested version to upgrade to. + */ + @Nullable + private String suggestedVersion; + + /** + * Detailed message describing the issue. + */ + @Nonnull + private String message; + + /** + * URL for more information about the issue. + */ + @Nullable + private String learnMoreUrl; + + /** + * CVE identifier if this is a CVE issue. + */ + @Nullable + private String cveId; + + /** + * Gets a formatted title for the notification. + */ + public String getTitle() { + return switch (upgradeReason) { + case JRE_TOO_OLD -> "Outdated JDK Version Detected"; + case END_OF_LIFE -> packageDisplayName + " End of Life"; + case DEPRECATED -> packageDisplayName + " Deprecated"; + case CVE -> "Security Vulnerability in " + packageDisplayName; + }; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java new file mode 100644 index 00000000000..41222922e10 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java @@ -0,0 +1,277 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.inspection; + +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.codeInspection.LocalQuickFix; +import com.intellij.codeInspection.ProblemDescriptor; +import com.intellij.codeInspection.ProblemHighlightType; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.PsiFile; +import com.intellij.psi.XmlElementVisitor; +import com.intellij.psi.xml.XmlFile; +import com.intellij.psi.xml.XmlTag; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesCache; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; + +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService.*; + +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; + +/** + * Inspection that displays Java upgrade issues detected by JavaUpgradeDetectionService. + * Shows JDK version and framework version issues in pom.xml files with wavy underlines. + * + * Note: Issues are cached at project startup via JavaUpgradeIssueCache to avoid + * repeated expensive scans during inspection runs. + */ +public class JavaUpgradeIssuesInspection extends LocalInspectionTool { + + private static final Logger LOG = Logger.getInstance(JavaUpgradeIssuesInspection.class); + + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + final PsiFile file = holder.getFile(); + + // Only process pom.xml files + if (!(file instanceof XmlFile) || !file.getName().equals("pom.xml")) { + return PsiElementVisitor.EMPTY_VISITOR; + } + + final Project project = holder.getProject(); + final JavaUpgradeIssuesCache cache = JavaUpgradeIssuesCache.getInstance(project); + + // Skip if cache is not yet initialized (will show issues after startup completes) + if (!cache.isInitialized()) { + return PsiElementVisitor.EMPTY_VISITOR; + } + + // Get cached issues (computed once at project startup) + final JavaUpgradeIssue jdkIssue = cache.getJdkIssue(); + final JavaUpgradeIssue springBootIssue = cache.findDependencyIssue(GROUP_ID_SPRING_BOOT); + final JavaUpgradeIssue springFrameworkIssue = cache.findDependencyIssue(GROUP_ID_SPRING_FRAMEWORK + ":"); + final JavaUpgradeIssue springSecurityIssue = cache.findDependencyIssue(GROUP_ID_SPRING_SECURITY); + + // Debug logging + LOG.info("JavaUpgradeIssuesInspection: Cache initialized=" + cache.isInitialized() + + ", jdkIssue=" + (jdkIssue != null) + + ", springBootIssue=" + (springBootIssue != null ? springBootIssue.getCurrentVersion() : "null") + + ", dependencyIssues=" + cache.getDependencyIssues().size()); + + return new XmlElementVisitor() { + @Override + public void visitXmlTag(@NotNull XmlTag tag) { + super.visitXmlTag(tag); + + // Check for JDK version tags + if (jdkIssue != null) { + if (isJavaVersionProperty(tag) || isCompilerPluginVersionTag(tag)) { + registerProblem(holder, tag, jdkIssue); + } + } + + // Check for Spring Boot parent version + if (springBootIssue != null && isSpringBootParentVersion(tag)) { + registerProblem(holder, tag, springBootIssue); + } + + // Check for Spring Framework dependency version + if (springFrameworkIssue != null && isSpringFrameworkDependencyVersion(tag)) { + registerProblem(holder, tag, springFrameworkIssue); + } + + // Check for Spring Boot dependency version (when not using parent) + if (springBootIssue != null && isSpringBootDependencyVersion(tag)) { + registerProblem(holder, tag, springBootIssue); + } + + // Check for Spring Security dependency version + if (springSecurityIssue != null && isSpringSecurityDependencyVersion(tag)) { + registerProblem(holder, tag, springSecurityIssue); + } + } + }; + } + + private void registerProblem(@NotNull ProblemsHolder holder, @NotNull XmlTag tag, @NotNull JavaUpgradeIssue issue) { + // ProblemHighlightType highlightType = issue.getSeverity() == JavaUpgradeIssue.Severity.CRITICAL + // ? ProblemHighlightType.ERROR + // : ProblemHighlightType.WARNING; + + holder.registerProblem( + tag, + issue.getMessage(), + ProblemHighlightType.WARNING, + new UpgradeWithCopilotQuickFix(issue) + ); + } + + /** + * Checks if the tag is a Java version property (java.version, maven.compiler.source, maven.compiler.target). + */ + private boolean isJavaVersionProperty(@NotNull XmlTag tag) { + String tagName = tag.getName(); + XmlTag parent = tag.getParentTag(); + + if (parent == null || !"properties".equals(parent.getName())) { + return false; + } + + return "java.version".equals(tagName) || + "maven.compiler.source".equals(tagName) || + "maven.compiler.target".equals(tagName) || + "maven.compiler.release".equals(tagName); + } + + /** + * Checks if the tag is a version tag inside maven-compiler-plugin configuration. + */ + private boolean isCompilerPluginVersionTag(@NotNull XmlTag tag) { + String tagName = tag.getName(); + if (!"source".equals(tagName) && !"target".equals(tagName) && !"release".equals(tagName)) { + return false; + } + + // Check if we're inside maven-compiler-plugin + XmlTag current = tag.getParentTag(); + while (current != null) { + if ("plugin".equals(current.getName())) { + XmlTag artifactIdTag = current.findFirstSubTag("artifactId"); + if (artifactIdTag != null && ARTIFACT_ID_MAVEN_COMPILER_PLUGIN.equals(artifactIdTag.getValue().getText())) { + return true; + } + } + current = current.getParentTag(); + } + return false; + } + + /** + * Checks if the tag is the version tag inside a Spring Boot parent declaration. + */ + private boolean isSpringBootParentVersion(@NotNull XmlTag tag) { + if (!"version".equals(tag.getName())) { + return false; + } + + XmlTag parent = tag.getParentTag(); + if (parent == null || !"parent".equals(parent.getName())) { + return false; + } + + XmlTag groupIdTag = parent.findFirstSubTag("groupId"); + XmlTag artifactIdTag = parent.findFirstSubTag("artifactId"); + + return groupIdTag != null && GROUP_ID_SPRING_BOOT.equals(groupIdTag.getValue().getText()) && + artifactIdTag != null && ARTIFACT_ID_SPRING_BOOT_STARTER_PARENT.equals(artifactIdTag.getValue().getText()); + } + + /** + * Checks if the tag is a version tag inside a Spring Boot dependency. + */ + private boolean isSpringBootDependencyVersion(@NotNull XmlTag tag) { + if (!"version".equals(tag.getName())) { + return false; + } + + XmlTag dependency = tag.getParentTag(); + if (dependency == null || !"dependency".equals(dependency.getName())) { + return false; + } + + XmlTag groupIdTag = dependency.findFirstSubTag("groupId"); + return groupIdTag != null && GROUP_ID_SPRING_BOOT.equals(groupIdTag.getValue().getText()); + } + + /** + * Checks if the tag is a version tag inside a Spring Framework dependency. + */ + private boolean isSpringFrameworkDependencyVersion(@NotNull XmlTag tag) { + if (!"version".equals(tag.getName())) { + return false; + } + + XmlTag dependency = tag.getParentTag(); + if (dependency == null || !"dependency".equals(dependency.getName())) { + return false; + } + + XmlTag groupIdTag = dependency.findFirstSubTag("groupId"); + return groupIdTag != null && GROUP_ID_SPRING_FRAMEWORK.equals(groupIdTag.getValue().getText()); + } + + /** + * Checks if the tag is a version tag inside a Spring Security dependency. + */ + private boolean isSpringSecurityDependencyVersion(@NotNull XmlTag tag) { + if (!"version".equals(tag.getName())) { + return false; + } + + XmlTag dependency = tag.getParentTag(); + if (dependency == null || !"dependency".equals(dependency.getName())) { + return false; + } + + XmlTag groupIdTag = dependency.findFirstSubTag("groupId"); + return groupIdTag != null && GROUP_ID_SPRING_SECURITY.equals(groupIdTag.getValue().getText()); + } + + /** + * Quick fix to upgrade using Copilot based on the issue type. + */ + private static class UpgradeWithCopilotQuickFix implements LocalQuickFix { + private final JavaUpgradeIssue issue; + + public UpgradeWithCopilotQuickFix(@NotNull JavaUpgradeIssue issue) { + this.issue = issue; + } + + @Nls(capitalization = Nls.Capitalization.Sentence) + @NotNull + @Override + public String getFamilyName() { + return "Azure Toolkit"; + } + + @Nls(capitalization = Nls.Capitalization.Sentence) + @NotNull + @Override + public String getName() { + return "Upgrade " + issue.getPackageDisplayName() + " with Copilot"; + } + + @Override + public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { + String prompt = buildPromptForIssue(issue); + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + } + + private String buildPromptForIssue(@NotNull JavaUpgradeIssue issue) { + String packageId = issue.getPackageId(); + + // JDK upgrade + if (PACKAGE_ID_JDK.equals(packageId)) { + return String.format( + "Upgrade Java runtime from version %s to Java %s (LTS) using java upgrade tools by invoking #generate_upgrade_plan", + issue.getCurrentVersion(), issue.getSuggestedVersion() + ); + } + + // Framework upgrade (Spring Boot, Spring Framework, etc.) + return String.format( + "Upgrade %s from version %s to %s using java upgrade tools by invoking #generate_upgrade_plan", + issue.getPackageDisplayName(), issue.getCurrentVersion(), issue.getSuggestedVersion() + ); + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/CVECheckService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/CVECheckService.java new file mode 100644 index 00000000000..d6380a38f6c --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/CVECheckService.java @@ -0,0 +1,564 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service; + +import com.intellij.util.net.ssl.CertificateManager; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import lombok.Builder; +import lombok.Data; +import org.apache.commons.lang3.StringUtils; +import org.apache.maven.artifact.versioning.ComparableVersion; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Service to check for CVE (Common Vulnerabilities and Exposures) issues in Maven dependencies. + * Uses GitHub's Security Advisory API to fetch vulnerability information. + * + * This implementation is aligned with the TypeScript version in vscode-java-dependency. + * @see cve.ts + */ +public class CVECheckService { + + private static final String GITHUB_API_BASE = "https://api.github.com/advisories"; + private static final int BATCH_SIZE = 30; + private static final int REQUEST_TIMEOUT_SECONDS = 30; + + /** + * Severity levels ordered by criticality (higher number = more critical). + * Aligned with GitHub Security Advisory severity levels. + */ + public enum Severity { + UNKNOWN(0), + LOW(1), + MEDIUM(2), + HIGH(3), + CRITICAL(4); + + private final int level; + + Severity(int level) { + this.level = level; + } + + public int getLevel() { + return level; + } + + public static Severity fromString(String severity) { + if (severity == null) return UNKNOWN; + return switch (severity.toLowerCase()) { + case "critical" -> CRITICAL; + case "high" -> HIGH; + case "medium" -> MEDIUM; + case "low" -> LOW; + default -> UNKNOWN; + }; + } + } + + /** + * Represents a CVE (Common Vulnerabilities and Exposures) entry. + */ + @Data + @Builder + public static class CVE { + private String id; + private String ghsaId; + private Severity severity; + private String summary; + private String description; + private String htmlUrl; + private List affectedDeps; + } + + /** + * Represents a dependency affected by a CVE. + */ + @Data + @Builder + public static class AffectedDependency { + private String name; + private String vulnerableVersionRange; + private String patchedVersion; + } + + /** + * Represents a dependency coordinate (groupId:artifactId:version). + */ + @Data + @Builder + public static class DependencyCoordinate { + private String groupId; + private String artifactId; + private String version; + + public String getName() { + return groupId + ":" + artifactId; + } + + public String getCoordinate() { + return groupId + ":" + artifactId + ":" + version; + } + } + + private static CVECheckService instance; + private final HttpClient httpClient; + private final Gson gson; + + private CVECheckService() { + final HttpClient.Builder builder = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(REQUEST_TIMEOUT_SECONDS)); + try { + builder.sslContext(CertificateManager.getInstance().getSslContext()); + } catch (Throwable e) { + // Failed to get IntelliJ SSL context, using default + } + this.httpClient = builder.build(); + this.gson = new Gson(); + } + + public static synchronized CVECheckService getInstance() { + if (instance == null) { + instance = new CVECheckService(); + } + return instance; + } + + /** + * Batch check CVE issues for a list of Maven coordinates. + * Aligned with batchGetCVEIssues() from cve.ts. + * + * @param coordinates List of Maven coordinates in format "groupId:artifactId:version" + * @return List of CVE-related upgrade issues + */ + @Nonnull + public List batchGetCVEIssues(@Nonnull List coordinates) { + final List allIssues = new ArrayList<>(); + + // Process dependencies in batches to avoid URL length limits + for (int i = 0; i < coordinates.size(); i += BATCH_SIZE) { + final List batch = coordinates.subList(i, Math.min(i + BATCH_SIZE, coordinates.size())); + try { + final List batchIssues = getCveUpgradeIssues(batch); + allIssues.addAll(batchIssues); + } catch (Exception e) { + // Error checking CVEs for batch + } + } + + return allIssues; + } + + /** + * Get CVE upgrade issues for a batch of coordinates. + */ + @Nonnull + private List getCveUpgradeIssues(@Nonnull List coordinates) { + if (coordinates.isEmpty()) { + return Collections.emptyList(); + } + + final List deps = coordinates.stream() + .map(this::parseCoordinate) + .filter(Objects::nonNull) + .filter(d -> StringUtils.isNotBlank(d.getVersion())) + .collect(Collectors.toList()); + + if (deps.isEmpty()) { + return Collections.emptyList(); + } + + final List depsCves = fetchCves(deps); + return mapCvesToUpgradeIssues(depsCves); + } + + /** + * Parse a Maven coordinate string into a DependencyCoordinate object. + */ + @Nullable + private DependencyCoordinate parseCoordinate(@Nonnull String coordinate) { + final String[] parts = coordinate.split(":", 3); + if (parts.length < 3) { + return null; + } + return DependencyCoordinate.builder() + .groupId(parts[0]) + .artifactId(parts[1]) + .version(parts[2]) + .build(); + } + + /** + * Represents a dependency with its associated CVEs. + */ + @Data + @Builder + private static class DepsCves { + private String dep; + private String version; + private List cves; + } + + /** + * Fetch CVEs from GitHub Security Advisory API. + */ + @Nonnull + private List fetchCves(@Nonnull List deps) { + if (deps.isEmpty()) { + return Collections.emptyList(); + } + + try { + final List allCves = retrieveVulnerabilityData(deps); + + if (allCves.isEmpty()) { + return Collections.emptyList(); + } + + // Group the CVEs by coordinate + final List depsCves = new ArrayList<>(); + + for (DependencyCoordinate dep : deps) { + final List depCves = allCves.stream() + .filter(cve -> isCveAffectingDep(cve, dep.getName(), dep.getVersion())) + .collect(Collectors.toList()); + + if (!depCves.isEmpty()) { + depsCves.add(DepsCves.builder() + .dep(dep.getName()) + .version(dep.getVersion()) + .cves(depCves) + .build()); + } + } + + return depsCves; + } catch (Exception e) { + // Error fetching CVEs + return Collections.emptyList(); + } + } + + + + /** + * Retrieve vulnerability data from GitHub Security Advisory API. + * Only fetches critical and high severity CVEs for Maven ecosystem. + */ + @Nonnull + private List retrieveVulnerabilityData(@Nonnull List deps) { + if (deps.isEmpty()) { + return Collections.emptyList(); + } + + try { + // Build the affects parameter: package@version format + // Based on TS: deps.map((p) => `${p.name}@${p.version}`) passed as array to octokit. + // Octokit usually serializes array as repeated params affects=a&affects=b OR comma-sep. + // GitHub API docs say "iterable". Standard approach for "iterable" in query string is repeated params. + // But previous Java code used comma. I will stick to comma as it likely works, or change if needed. + // TS Octokit behavior for "affects" param -> comma separated string is often accepted. + final String affects = deps.stream() + .map(d -> URLEncoder.encode(d.getName() + "@" + d.getVersion(), StandardCharsets.UTF_8)) + .collect(Collectors.joining(",")); + + final List allCves = new ArrayList<>(); + int page = 1; + + while (true) { + final String url = String.format( + "%s?ecosystem=maven&affects=%s&per_page=100&sort=published&direction=asc&page=%d", + GITHUB_API_BASE, affects, page + ); + + final HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Accept", "application/vnd.github+json") + .header("X-GitHub-Api-Version", "2022-11-28") + .timeout(Duration.ofSeconds(REQUEST_TIMEOUT_SECONDS)) + .GET() + .build(); + + final HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + // GitHub API returned non-200 status + break; + } + + final JsonArray advisories = gson.fromJson(response.body(), JsonArray.class); + if (advisories.isEmpty()) { + break; + } + + // Parse and add to list + allCves.addAll(parseAdvisories(advisories)); + + if (advisories.size() < 100) { + break; + } + + page++; + } + + return allCves; + + } catch (Exception e) { + // Error retrieving vulnerability data from GitHub + return Collections.emptyList(); + } + } + + /** + * Parse GitHub Security Advisory API response into CVE objects. + */ + @Nonnull + private List parseAdvisories(@Nonnull JsonArray advisories) { + final List cves = new ArrayList<>(); + + try { + for (JsonElement element : advisories) { + final JsonObject advisory = element.getAsJsonObject(); + + // Skip withdrawn advisories + if (advisory.has("withdrawn_at") && !advisory.get("withdrawn_at").isJsonNull()) { + continue; + } + + final String severity = getStringOrNull(advisory, "severity"); + final Severity severityEnum = Severity.fromString(severity); + + // Only consider critical and high severity CVEs + if (severityEnum != Severity.CRITICAL && severityEnum != Severity.HIGH) { + continue; + } + + final String cveId = getStringOrNull(advisory, "cve_id"); + final String ghsaId = getStringOrNull(advisory, "ghsa_id"); + final String id = StringUtils.isNotBlank(cveId) ? cveId : ghsaId; + + final List affectedDeps = new ArrayList<>(); + if (advisory.has("vulnerabilities") && !advisory.get("vulnerabilities").isJsonNull()) { + final JsonArray vulnerabilities = advisory.getAsJsonArray("vulnerabilities"); + for (JsonElement vulnElement : vulnerabilities) { + final JsonObject vuln = vulnElement.getAsJsonObject(); + String packageName = null; + if (vuln.has("package") && !vuln.get("package").isJsonNull()) { + packageName = getStringOrNull(vuln.getAsJsonObject("package"), "name"); + } + + affectedDeps.add(AffectedDependency.builder() + .name(packageName) + .vulnerableVersionRange(getStringOrNull(vuln, "vulnerable_version_range")) + .patchedVersion(getStringOrNull(vuln, "first_patched_version")) + .build()); + } + } + + cves.add(CVE.builder() + .id(id) + .ghsaId(ghsaId) + .severity(severityEnum) + .summary(getStringOrNull(advisory, "summary")) + .description(getStringOrNull(advisory, "description")) + .htmlUrl(getStringOrNull(advisory, "html_url")) + .affectedDeps(affectedDeps) + .build()); + } + + } catch (Exception e) { + // Error parsing advisory JSON + } + + return cves; + } + + /** + * Parse GitHub Security Advisory API response into CVE objects. + * Legacy method for backward compatibility if needed, or helper + */ + @Nonnull + private List parseAdvisories(@Nonnull String jsonResponse) { + try { + return parseAdvisories(gson.fromJson(jsonResponse, JsonArray.class)); + } catch (Exception e) { + // Error parsing advisory JSON + return Collections.emptyList(); + } + } + + @Nullable + private String getStringOrNull(@Nonnull JsonObject obj, @Nonnull String key) { + if (obj.has(key) && !obj.get(key).isJsonNull()) { + return obj.get(key).getAsString(); + } + return null; + } + + /** + * Map CVEs to upgrade issues. + */ + @Nonnull + private List mapCvesToUpgradeIssues(@Nonnull List depsCves) { + if (depsCves.isEmpty()) { + return Collections.emptyList(); + } + + return depsCves.stream() + .map(this::mapDepCvesToUpgradeIssue) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + /** + * Map a single dependency's CVEs to an upgrade issue. + * Uses the most critical CVE for the issue details. + */ + @Nullable + private JavaUpgradeIssue mapDepCvesToUpgradeIssue(@Nonnull DepsCves depCve) { + if (depCve.getCves() == null || depCve.getCves().isEmpty()) { + return null; + } + + // Find the most critical CVE + final CVE mostCriticalCve = depCve.getCves().stream() + .max(Comparator.comparingInt(cve -> cve.getSeverity().getLevel())) + .orElse(depCve.getCves().get(0)); + + // Build the message + final String message = String.format( + "Security vulnerability %s detected in %s %s. %s", + mostCriticalCve.getId() != null ? mostCriticalCve.getId() : "CVE", + depCve.getDep(), + depCve.getVersion(), + StringUtils.isNotBlank(mostCriticalCve.getSummary()) ? + mostCriticalCve.getSummary() : + "Please upgrade to a patched version." + ); + + // Determine suggested version from patched versions + String suggestedVersion = null; + if (mostCriticalCve.getAffectedDeps() != null) { + suggestedVersion = mostCriticalCve.getAffectedDeps().stream() + .filter(ad -> depCve.getDep().equals(ad.getName())) + .map(AffectedDependency::getPatchedVersion) + .filter(StringUtils::isNotBlank) + .findFirst() + .orElse(null); + } + + return JavaUpgradeIssue.builder() + .packageId(depCve.getDep()) + .packageDisplayName(depCve.getDep()) + .upgradeReason(JavaUpgradeIssue.UpgradeReason.CVE) + .severity(mapCveSeverityToIssueSeverity(mostCriticalCve.getSeverity())) + .currentVersion(depCve.getVersion()) + .suggestedVersion(suggestedVersion) + .message(message) + .learnMoreUrl(mostCriticalCve.getHtmlUrl()) + .cveId(mostCriticalCve.getId()) + .build(); + } + + /** + * Map CVE severity to issue severity. + */ + @Nonnull + private JavaUpgradeIssue.Severity mapCveSeverityToIssueSeverity(@Nonnull Severity cveSeverity) { + return switch (cveSeverity) { + case CRITICAL -> JavaUpgradeIssue.Severity.CRITICAL; + case HIGH -> JavaUpgradeIssue.Severity.WARNING; + default -> JavaUpgradeIssue.Severity.INFO; + }; + } + + /** + * Check if a CVE affects a specific dependency at a specific version. + * Aligned with isCveAffectingDep() from cve.ts. + */ + private boolean isCveAffectingDep(@Nonnull CVE cve, @Nonnull String depName, @Nonnull String depVersion) { + if (cve.getAffectedDeps() == null || cve.getAffectedDeps().isEmpty()) { + return false; + } + + return cve.getAffectedDeps().stream() + .anyMatch(ad -> depName.equals(ad.getName()) && + isVersionInRange(depVersion, ad.getVulnerableVersionRange())); + } + + /** + * Check if a version satisfies a vulnerability version range. + * Handles common range formats like ">= 1.0, < 2.0", "< 3.0", etc. + */ + private boolean isVersionInRange(@Nonnull String version, @Nullable String range) { + if (StringUtils.isBlank(range)) { + return false; + } + + try { + final ComparableVersion currentVersion = new ComparableVersion(version); + + // Split by comma for compound ranges (e.g., ">= 1.0, < 2.0") + final String[] conditions = range.split(","); + + for (String condition : conditions) { + condition = condition.trim(); + + if (!satisfiesCondition(currentVersion, condition)) { + return false; + } + } + + return true; + } catch (Exception e) { + // Error checking version range + return false; + } + } + + /** + * Check if a version satisfies a single condition. + */ + private boolean satisfiesCondition(@Nonnull ComparableVersion version, @Nonnull String condition) { + condition = condition.trim(); + + if (condition.startsWith(">=")) { + final ComparableVersion min = new ComparableVersion(condition.substring(2).trim()); + return version.compareTo(min) >= 0; + } else if (condition.startsWith(">")) { + final ComparableVersion min = new ComparableVersion(condition.substring(1).trim()); + return version.compareTo(min) > 0; + } else if (condition.startsWith("<=")) { + final ComparableVersion max = new ComparableVersion(condition.substring(2).trim()); + return version.compareTo(max) <= 0; + } else if (condition.startsWith("<")) { + final ComparableVersion max = new ComparableVersion(condition.substring(1).trim()); + return version.compareTo(max) < 0; + } else if (condition.startsWith("=")) { + final ComparableVersion exact = new ComparableVersion(condition.substring(1).trim()); + return version.compareTo(exact) == 0; + } else { + // Exact version match + final ComparableVersion exact = new ComparableVersion(condition); + return version.compareTo(exact) == 0; + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesCache.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesCache.java new file mode 100644 index 00000000000..71fabe6fea0 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesCache.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.components.Service; +import com.intellij.openapi.project.Project; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Project-level cache for Java upgrade issues. + * Issues are computed once at startup and cached to avoid repeated expensive scans + * during inspection runs. The cache can be invalidated when project model changes. + */ +@Service(Service.Level.PROJECT) +public final class JavaUpgradeIssuesCache implements Disposable { + + private final Project project; + private final AtomicReference> jdkIssuesCache = new AtomicReference<>(); + private final AtomicReference> dependencyIssuesCache = new AtomicReference<>(); + private final AtomicBoolean initialized = new AtomicBoolean(false); + + public JavaUpgradeIssuesCache(@NotNull Project project) { + this.project = project; + } + + public static JavaUpgradeIssuesCache getInstance(@NotNull Project project) { + return project.getService(JavaUpgradeIssuesCache.class); + } + + /** + * Gets cached JDK issues. Returns empty list if not yet initialized. + */ + @Nonnull + public List getJdkIssues() { + List cached = jdkIssuesCache.get(); + return cached != null ? cached : Collections.emptyList(); + } + + /** + * Gets cached dependency issues. Returns empty list if not yet initialized. + */ + @Nonnull + public List getDependencyIssues() { + List cached = dependencyIssuesCache.get(); + return cached != null ? cached : Collections.emptyList(); + } + + /** + * Finds a specific issue by package ID prefix. + */ + @Nullable + public JavaUpgradeIssue findDependencyIssue(@Nonnull String packageIdPrefix) { + return getDependencyIssues().stream() + .filter(i -> i.getPackageId().startsWith(packageIdPrefix)) + .findFirst() + .orElse(null); + } + + /** + * Gets the first JDK issue if present. + */ + @Nullable + public JavaUpgradeIssue getJdkIssue() { + List issues = getJdkIssues(); + return issues.isEmpty() ? null : issues.get(0); + } + + /** + * Checks if the cache has been initialized. + */ + public boolean isInitialized() { + return initialized.get(); + } + + /** + * Refreshes the cache by re-scanning the project. + * This should be called at project startup and when the project model changes. + */ + public void refresh() { + if (project.isDisposed()) { + return; + } + + final JavaUpgradeIssuesDetectionService detectionService = JavaUpgradeIssuesDetectionService.getInstance(); + + // Scan for issues + List jdkIssues = detectionService.getJavaIssues(project); + List dependencyIssues = detectionService.getDependencyIssues(project); + + // Update cache + jdkIssuesCache.set(Collections.unmodifiableList(jdkIssues)); + dependencyIssuesCache.set(Collections.unmodifiableList(dependencyIssues)); + initialized.set(true); + } + + /** + * Invalidates the cache, forcing a refresh on next access. + */ + public void invalidate() { + jdkIssuesCache.set(null); + dependencyIssuesCache.set(null); + initialized.set(false); + } + + @Override + public void dispose() { + invalidate(); + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java new file mode 100644 index 00000000000..895b08d0362 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java @@ -0,0 +1,675 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; +import com.microsoft.azure.toolkit.intellij.common.utils.JdkUtils; +import com.microsoft.intellij.util.GradleUtils; +import com.microsoft.intellij.util.MavenUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.maven.artifact.versioning.ComparableVersion; +import org.jetbrains.idea.maven.model.MavenArtifact; +import org.jetbrains.idea.maven.model.MavenArtifactNode; +import org.jetbrains.idea.maven.project.MavenProject; +import org.jetbrains.idea.maven.project.MavenProjectsManager; +import org.jetbrains.plugins.gradle.model.ExternalDependency; +import org.jetbrains.plugins.gradle.model.ExternalProject; +import org.jetbrains.plugins.gradle.model.ExternalSourceSet; +import org.jetbrains.plugins.gradle.model.UnresolvedExternalDependency; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.*; + +/** + * Service to detect JDK version and framework dependency versions in Java projects. + * This service analyzes the project to identify outdated versions that may need upgrading. + * + * This implementation is aligned with the TypeScript version in vscode-java-dependency. + * @see assessmentManager.ts + */ +public class JavaUpgradeIssuesDetectionService { + + /** + * The mature LTS version of Java that is recommended. + * Aligned with MATURE_JAVA_LTS_VERSION from vscode-java-dependency. + */ + public static final int MATURE_JAVA_LTS_VERSION = 21; + + // Group ID constants for Spring dependencies + public static final String GROUP_ID_SPRING_BOOT = "org.springframework.boot"; + public static final String GROUP_ID_SPRING_FRAMEWORK = "org.springframework"; + public static final String GROUP_ID_SPRING_SECURITY = "org.springframework.security"; + + // Artifact ID constants + public static final String ARTIFACT_ID_SPRING_BOOT_STARTER_PARENT = "spring-boot-starter-parent"; + public static final String ARTIFACT_ID_MAVEN_COMPILER_PLUGIN = "maven-compiler-plugin"; + + // Package ID constants (used for cache lookups) + public static final String PACKAGE_ID_JDK = "jdk"; + + /** + * Metadata for dependencies to scan. + * Aligned with DEPENDENCIES_TO_SCAN from vscode-java-dependency. + */ + public static class DependencyCheckItem { + @Nonnull public final String groupId; + @Nonnull public final String artifactId; + @Nonnull public final String displayName; + @Nonnull public final String supportedVersion; + @Nonnull public final String suggestedVersion; + @Nonnull public final String learnMoreUrl; + @Nonnull public final Map eolDate; + + public DependencyCheckItem(@Nonnull String groupId, @Nonnull String artifactId, @Nonnull String displayName, + @Nonnull String supportedVersion, @Nonnull String suggestedVersion, @Nonnull String learnMoreUrl, + @Nonnull Map eolDate) { + this.groupId = groupId; + this.artifactId = artifactId; + this.displayName = displayName; + this.supportedVersion = supportedVersion; + this.suggestedVersion = suggestedVersion; + this.learnMoreUrl = learnMoreUrl; + this.eolDate = eolDate; + } + + public String getPackageId() { + return groupId + ":" + artifactId; + } + + /** + * Gets the EOL date for a specific version. + * @param version The version to check (e.g., "2.7.x", "3.5.x") + * @return The EOL date string (e.g., "2029-06") or null if not found + */ + @Nullable + public String getEolDateForVersion(@Nonnull String version) { + // Try exact match first + if (eolDate.containsKey(version)) { + return eolDate.get(version); + } + // Try to match major.minor.x pattern + String[] parts = version.split("\\."); + if (parts.length >= 2) { + String pattern = parts[0] + "." + parts[1] + ".x"; + return eolDate.get(pattern); + } + return null; + } + } + + /** + * Dependencies to scan for upgrade issues. + * Aligned with DEPENDENCIES_TO_SCAN from dependency.metadata.ts in vscode-java-dependency. + */ + private static final List DEPENDENCIES_TO_SCAN = List.of( + // Spring Boot - supported versions: 2.7.x or >=3.2.x + new DependencyCheckItem( + GROUP_ID_SPRING_BOOT, + "*", + "Spring Boot", + "2.7.x || >=3.2.x", + "3.5", + "https://spring.io/projects/spring-boot#support", + Map.ofEntries( + Map.entry("4.0.x", "2027-12"), + Map.entry("3.5.x", "2032-06"), + Map.entry("3.4.x", "2026-12"), + Map.entry("3.3.x", "2026-06"), + Map.entry("3.2.x", "2025-12"), + Map.entry("3.1.x", "2025-06"), + Map.entry("3.0.x", "2024-12"), + Map.entry("2.7.x", "2029-06"), + Map.entry("2.6.x", "2024-02"), + Map.entry("2.5.x", "2023-08"), + Map.entry("2.4.x", "2023-02"), + Map.entry("2.3.x", "2022-08"), + Map.entry("2.2.x", "2022-01"), + Map.entry("2.1.x", "2021-01"), + Map.entry("2.0.x", "2020-06"), + Map.entry("1.5.x", "2020-11") + ) + ), + // Spring Framework - supported versions: 5.3.x or >=6.2.x + new DependencyCheckItem( + GROUP_ID_SPRING_FRAMEWORK, + "*", + "Spring Framework", + "5.3.x || >=6.2.x", + "6.2", + "https://spring.io/projects/spring-framework#support", + Map.ofEntries( + Map.entry("7.0.x", "2028-06"), + Map.entry("6.2.x", "2032-06"), + Map.entry("6.1.x", "2026-06"), + Map.entry("6.0.x", "2025-08"), + Map.entry("5.3.x", "2029-06"), + Map.entry("5.2.x", "2023-12"), + Map.entry("5.1.x", "2022-12"), + Map.entry("5.0.x", "2022-12"), + Map.entry("4.3.x", "2020-12") + ) + ), + // Spring Security - supported versions: 5.7.x || 5.8.x || >=6.2.x + new DependencyCheckItem( + GROUP_ID_SPRING_SECURITY, + "*", + "Spring Security", + "5.7.x || 5.8.x || >=6.2.x", + "6.5", + "https://spring.io/projects/spring-security#support", + Map.ofEntries( + Map.entry("7.0.x", "2027-12"), + Map.entry("6.5.x", "2032-06"), + Map.entry("6.4.x", "2026-12"), + Map.entry("6.3.x", "2026-06"), + Map.entry("6.2.x", "2025-12"), + Map.entry("6.1.x", "2025-06"), + Map.entry("6.0.x", "2024-12"), + Map.entry("5.8.x", "2029-06"), + Map.entry("5.7.x", "2029-06"), + Map.entry("5.6.x", "2024-02"), + Map.entry("5.5.x", "2023-08"), + Map.entry("5.4.x", "2023-02"), + Map.entry("5.3.x", "2022-08"), + Map.entry("5.2.x", "2022-01"), + Map.entry("5.1.x", "2021-01"), + Map.entry("5.0.x", "2020-06"), + Map.entry("4.2.x", "2020-11") + ) + ) + ); + + private static final String JDK_LEARN_MORE_URL = + "https://learn.microsoft.com/azure/developer/java/fundamentals/java-support-on-azure"; + + private static final Logger LOG = Logger.getInstance(JavaUpgradeIssuesDetectionService.class); + + private static JavaUpgradeIssuesDetectionService instance; + + private JavaUpgradeIssuesDetectionService() { + } + + public static synchronized JavaUpgradeIssuesDetectionService getInstance() { + if (instance == null) { + instance = new JavaUpgradeIssuesDetectionService(); + } + return instance; + } + + /** + * Analyzes the given project and returns a list of detected outdated version issues. + * + * @param project The IntelliJ project to analyze + * @return List of detected outdated version issues + */ + @Nonnull + public List analyzeProject(@Nonnull Project project) { + final List issues = new ArrayList<>(); + + try { + // Get JDK issues + issues.addAll(getJavaIssues(project)); + + // Get dependency issues + issues.addAll(getDependencyIssues(project)); + + // Get CVE issues + issues.addAll(getCVEIssues(project)); + + } catch (Exception e) { + // Error analyzing project for upgrade issues + } + + return issues; + } + + /** + * Gets JDK/JRE version issues. + * Aligned with getJavaIssues() from assessmentManager.ts. + */ + @Nonnull + public List getJavaIssues(@Nonnull Project project) { + final List issues = new ArrayList<>(); + + try { + final Integer jdkVersion = JdkUtils.getJdkLanguageLevel(project); + if (jdkVersion == null) { + return issues; + } + + // Skip versions below 8 - out of scope + if (jdkVersion < 8) { + return issues; + } + + // Check against MATURE_JAVA_LTS_VERSION (21) + if (jdkVersion < MATURE_JAVA_LTS_VERSION) { + issues.add(JavaUpgradeIssue.builder() + .packageId("jdk") + .packageDisplayName("JDK") + .upgradeReason(JavaUpgradeIssue.UpgradeReason.JRE_TOO_OLD) + .severity(JavaUpgradeIssue.Severity.WARNING) + .currentVersion(String.valueOf(jdkVersion)) + .supportedVersion(">=" + MATURE_JAVA_LTS_VERSION) + .suggestedVersion(String.valueOf(MATURE_JAVA_LTS_VERSION)) + .message(String.format("This project is using an older Java runtime (%d). Would you like to upgrade it to %d (LTS)?", + jdkVersion, MATURE_JAVA_LTS_VERSION)) + .learnMoreUrl(JDK_LEARN_MORE_URL) + .build()); + } + } catch (Exception e) { + // Error checking JDK version + } + + return issues; + } + + /** + * Gets dependency issues by checking against DEPENDENCIES_TO_SCAN metadata. + * Aligned with getDependencyIssue() from assessmentManager.ts. + */ + @Nonnull + public List getDependencyIssues(@Nonnull Project project) { + final List issues = new ArrayList<>(); + + try { + final Set checkedPackages = new HashSet<>(); + + if (MavenUtils.isMavenProject(project)) { + final MavenProjectsManager mavenProjectsManager = MavenProjectsManager.getInstanceIfCreated(project); + if (mavenProjectsManager != null && mavenProjectsManager.isMavenizedProject()) { + final List mavenProjects = mavenProjectsManager.getProjects(); + + for (MavenProject mavenProject : mavenProjects) { + for (DependencyCheckItem checkItem : DEPENDENCIES_TO_SCAN) { + if (checkedPackages.contains(checkItem.getPackageId())) { + continue; + } + + final JavaUpgradeIssue issue = checkDependency(mavenProject, checkItem, checkedPackages); + if (issue != null) { + issues.add(issue); + } + } + } + } + } else if (GradleUtils.isGradleProject(project)) { + final List gradleProjects = GradleUtils.listGradleProjects(project); + for (ExternalProject gradleProject : gradleProjects) { + for (DependencyCheckItem checkItem : DEPENDENCIES_TO_SCAN) { + if (checkedPackages.contains(checkItem.getPackageId())) { + continue; + } + final JavaUpgradeIssue issue = checkGradleDependency(gradleProject, checkItem, checkedPackages); + if (issue != null) { + issues.add(issue); + } + } + } + } + + } catch (Exception e) { + // Error checking dependencies + } + + return issues; + } + + /** + * Gets CVE (Common Vulnerabilities and Exposures) issues for project dependencies. + * Aligned with getCVEIssues() from assessmentManager.ts. + * + * @param project The IntelliJ project to analyze + * @return List of CVE-related upgrade issues + */ + @Nonnull + public List getCVEIssues(@Nonnull Project project) { + try { + final Set coordinateSet = new HashSet<>(); + + if (MavenUtils.isMavenProject(project)) { + final MavenProjectsManager mavenProjectsManager = MavenProjectsManager.getInstanceIfCreated(project); + if (mavenProjectsManager != null && mavenProjectsManager.isMavenizedProject()) { + final List mavenProjects = mavenProjectsManager.getProjects(); + + for (MavenProject mavenProject : mavenProjects) { + // Get direct dependencies only (root level of dependency tree) + mavenProject.getDependencyTree().stream() + .map(MavenArtifactNode::getArtifact) + .filter(dep -> StringUtils.isNotBlank(dep.getVersion())) + .forEach(dep -> coordinateSet.add( + dep.getGroupId() + ":" + dep.getArtifactId() + ":" + dep.getVersion() + )); + } + } + } else if (GradleUtils.isGradleProject(project)) { + final List gradleProjects = GradleUtils.listGradleProjects(project); + for (ExternalProject gradleProject : gradleProjects) { + final ExternalSourceSet main = gradleProject.getSourceSets().get("main"); + if (main != null) { + main.getDependencies().stream() + .filter(dep -> !(dep instanceof UnresolvedExternalDependency)) + .filter(dep -> StringUtils.isNotBlank(dep.getVersion())) + .forEach(dep -> coordinateSet.add( + dep.getGroup() + ":" + dep.getName() + ":" + dep.getVersion() + )); + } + } + } + + if (coordinateSet.isEmpty()) { + return Collections.emptyList(); + } + + // Check CVEs for all collected dependencies + final List coordinates = new ArrayList<>(coordinateSet); + return CVECheckService.getInstance().batchGetCVEIssues(coordinates); + + } catch (Exception e) { + // Error checking CVE issues + return Collections.emptyList(); + } + } + + /** + * Checks a single dependency against its metadata. + * Aligned with the logic in getDependencyIssue() from assessmentManager.ts. + */ + @Nullable + private JavaUpgradeIssue checkDependency(@Nonnull MavenProject mavenProject, + @Nonnull DependencyCheckItem checkItem, + @Nonnull Set checkedPackages) { + String version = null; + + // Special handling for Spring Boot parent POM + if (GROUP_ID_SPRING_BOOT.equals(checkItem.groupId)) { + version = getParentVersion(mavenProject, checkItem.groupId, ARTIFACT_ID_SPRING_BOOT_STARTER_PARENT); + } + + // If not found in parent, check direct dependencies + if (version == null) { + String targetArtifactId = "*".equals(checkItem.artifactId) ? null : checkItem.artifactId; + final MavenArtifact dependency = findDirectDependency(mavenProject, checkItem.groupId, targetArtifactId); + if (dependency != null) { + version = dependency.getVersion(); + } + } + + if (version == null || StringUtils.isBlank(version)) { + return null; + } + + checkedPackages.add(checkItem.getPackageId()); + + // Check if version satisfies the supported version range + if (!satisfiesVersionRange(version, checkItem.supportedVersion)) { + return JavaUpgradeIssue.builder() + .packageId(checkItem.getPackageId()) + .packageDisplayName(checkItem.displayName) + .upgradeReason(determineUpgradeReason(version, checkItem)) + .severity(determineSeverity(version, checkItem)) + .currentVersion(version) + .supportedVersion(checkItem.supportedVersion) + .suggestedVersion(checkItem.suggestedVersion) + .message(buildUpgradeMessage(checkItem.displayName, version, checkItem)) + .learnMoreUrl(checkItem.learnMoreUrl) + .build(); + } + + return null; + } + + /** + * Gets the version from parent POM. + */ + @Nullable + private String getParentVersion(@Nonnull MavenProject mavenProject, + @Nonnull String groupId, + @Nonnull String artifactId) { + try { + final var parentId = mavenProject.getParentId(); + LOG.info("getParentVersion: parentId=" + (parentId != null ? + parentId.getGroupId() + ":" + parentId.getArtifactId() + ":" + parentId.getVersion() : "null") + + ", looking for " + groupId + ":" + artifactId); + if (parentId != null && + groupId.equals(parentId.getGroupId()) && + artifactId.equals(parentId.getArtifactId())) { + LOG.info("getParentVersion: Found matching parent version: " + parentId.getVersion()); + return parentId.getVersion(); + } + } catch (Exception e) { + LOG.warn("Error getting parent version", e); + } + return null; + } + + /** + * Checks if a version satisfies a version range. + * Supports ranges like: "2.7.x || >=3.2", ">=10", "5.3.x || >=6.1" + * Aligned with semver logic from assessmentManager.ts. + */ + private boolean satisfiesVersionRange(@Nonnull String version, @Nonnull String range) { + // Split by "||" for OR conditions + final String[] orConditions = range.split("\\|\\|"); + + for (String condition : orConditions) { + condition = condition.trim(); + + if (satisfiesSingleCondition(version, condition)) { + return true; + } + } + + return false; + } + + /** + * Checks if a version satisfies a single version condition. + */ + private boolean satisfiesSingleCondition(@Nonnull String version, @Nonnull String condition) { + try { + // Handle "x.y.z" pattern (e.g., "2.7.x" means any 2.7.*) + if (condition.endsWith(".x")) { + final String prefix = condition.substring(0, condition.length() - 2); + return version.startsWith(prefix + "."); + } + + // Handle ">=" pattern + if (condition.startsWith(">=")) { + String minVersion = condition.substring(2).trim(); + // Handle version with wildcard, e.g. ">=3.2.x" -> "3.2" + if (minVersion.endsWith(".x")) { + minVersion = minVersion.substring(0, minVersion.length() - 2); + } + final ComparableVersion current = new ComparableVersion(version); + final ComparableVersion min = new ComparableVersion(minVersion); + return current.compareTo(min) >= 0; + } + + // Handle ">" pattern + if (condition.startsWith(">")) { + String minVersion = condition.substring(1).trim(); + // Handle version with wildcard, e.g. ">3.2.x" -> "3.2" + if (minVersion.endsWith(".x")) { + minVersion = minVersion.substring(0, minVersion.length() - 2); + } + final ComparableVersion current = new ComparableVersion(version); + final ComparableVersion min = new ComparableVersion(minVersion); + return current.compareTo(min) > 0; + } + + // Handle exact version match + return version.equals(condition); + + } catch (Exception e) { + // Error checking version range + return false; + } + } + + /** + * Checks if a version has reached its end-of-life date based on the EOL map. + * @param version The version to check (e.g., "2.0.1.RELEASE") + * @param checkItem The dependency check item containing EOL dates + * @return true if the version is past its EOL date + */ + private boolean isVersionEndOfLife(@Nonnull String version, @Nonnull DependencyCheckItem checkItem) { + String eolDateStr = checkItem.getEolDateForVersion(version); + if (eolDateStr == null) { + return false; + } + + try { + // Parse EOL date (format: "YYYY-MM") + java.time.YearMonth eolDate = java.time.YearMonth.parse(eolDateStr); + java.time.YearMonth currentDate = java.time.YearMonth.now(); + return currentDate.isAfter(eolDate); + } catch (Exception e) { + return false; + } + } + + /** + * Gets the EOL date string for a version if available. + */ + @Nullable + private String getEolDateString(@Nonnull String version, @Nonnull DependencyCheckItem checkItem) { + return checkItem.getEolDateForVersion(version); + } + + /** + * Determines the upgrade reason based on version and EOL status. + */ + @Nonnull + private JavaUpgradeIssue.UpgradeReason determineUpgradeReason(@Nonnull String version, + @Nonnull DependencyCheckItem checkItem) { + // Check if version has reached EOL based on the EOL date map + if (isVersionEndOfLife(version, checkItem)) { + return JavaUpgradeIssue.UpgradeReason.END_OF_LIFE; + } + + // For deprecated but still maintained versions + return JavaUpgradeIssue.UpgradeReason.DEPRECATED; + } + + /** + * Determines the severity based on version and EOL status. + */ + @Nonnull + private JavaUpgradeIssue.Severity determineSeverity(@Nonnull String version, + @Nonnull DependencyCheckItem checkItem) { + // If version has reached EOL, mark as critical + if (isVersionEndOfLife(version, checkItem)) { + return JavaUpgradeIssue.Severity.INFO; + } + + // For other unsupported versions (not yet EOL but outside supported range) + return JavaUpgradeIssue.Severity.INFO; + } + + /** + * Builds a human-readable upgrade message. + */ + @Nonnull + private String buildUpgradeMessage(@Nonnull String displayName, + @Nonnull String currentVersion, + @Nonnull DependencyCheckItem checkItem) { + String eolDateStr = getEolDateString(currentVersion, checkItem); + boolean isEol = isVersionEndOfLife(currentVersion, checkItem); + if (isEol && eolDateStr != null) { + return String.format( + "This project is using %s %s, which has reached end of life in %s. " + + "Would you like to upgrade it to %s?", + displayName, currentVersion, eolDateStr, checkItem.suggestedVersion + ); + } else if (eolDateStr != null) { + return String.format( + "This project is using %s %s, which will reach end of life in %s. " + + "Would you like to upgrade it to %s?", + displayName, currentVersion, eolDateStr, checkItem.suggestedVersion + ); + } else { + return String.format( + "This project is using %s %s, which is outside the supported version range (%s). " + + "Would you like to upgrade it to %s?", + displayName, currentVersion, checkItem.supportedVersion, checkItem.suggestedVersion + ); + } + } + + /** + * Finds a direct dependency in the Maven project (excludes transitive dependencies). + * Uses getDependencyTree() to identify only dependencies explicitly declared in pom.xml. + * This aligns with the TypeScript implementation which parses pom.xml directly. + */ + @Nullable + private MavenArtifact findDirectDependency(@Nonnull MavenProject mavenProject, + @Nonnull String groupId, + @Nullable String artifactId) { + // getDependencyTree() returns the root-level nodes which are direct dependencies + // (transitive dependencies are children of these nodes) + List dependencyTree = mavenProject.getDependencyTree(); + return dependencyTree.stream() + .map(MavenArtifactNode::getArtifact) + .filter(dep -> groupId.equals(dep.getGroupId())) + .filter(dep -> artifactId == null || artifactId.equals(dep.getArtifactId())) + .findFirst() + .orElse(null); + } + + /** + * Checks a single dependency against its metadata for Gradle projects. + */ + @Nullable + private JavaUpgradeIssue checkGradleDependency(@Nonnull ExternalProject gradleProject, + @Nonnull DependencyCheckItem checkItem, + @Nonnull Set checkedPackages) { + final ExternalSourceSet main = gradleProject.getSourceSets().get("main"); + if (main == null) { + return null; + } + + // Find direct dependency + final ExternalDependency dependency = main.getDependencies().stream() + .filter(dep -> StringUtils.equalsIgnoreCase(checkItem.groupId, dep.getGroup()) && + ("*".equals(checkItem.artifactId) || StringUtils.equalsIgnoreCase(checkItem.artifactId, dep.getName()))) + .filter(dep -> !(dep instanceof UnresolvedExternalDependency)) + .findFirst() + .orElse(null); + + if (dependency == null) { + return null; + } + + final String version = dependency.getVersion(); + + if (version == null || StringUtils.isBlank(version)) { + return null; + } + + checkedPackages.add(checkItem.getPackageId()); + + // Check if version satisfies the supported version range + if (!satisfiesVersionRange(version, checkItem.supportedVersion)) { + return JavaUpgradeIssue.builder() + .packageId(checkItem.getPackageId()) + .packageDisplayName(checkItem.displayName) + .upgradeReason(determineUpgradeReason(version, checkItem)) + .severity(determineSeverity(version, checkItem)) + .currentVersion(version) + .supportedVersion(checkItem.supportedVersion) + .suggestedVersion(checkItem.suggestedVersion) + .message(buildUpgradeMessage(checkItem.displayName, version, checkItem)) + .learnMoreUrl(checkItem.learnMoreUrl) + .build(); + } + + return null; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java new file mode 100644 index 00000000000..50a7449bfff --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java @@ -0,0 +1,555 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service; + +import com.intellij.ide.plugins.IdeaPluginDescriptor; +import com.intellij.ide.plugins.PluginManagerCore; +import com.intellij.ide.util.PropertiesComponent; +import com.intellij.notification.Notification; +import com.intellij.notification.NotificationAction; +import com.intellij.notification.NotificationType; +import com.intellij.notification.Notifications; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.extensions.PluginId; +import com.intellij.openapi.project.Project; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; +import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager; + +import static com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller.installPlugin; +import static com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller.isAppModPluginInstalled; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService.*; +import java.lang.reflect.Method; + +import kotlin.Unit; +import kotlin.jvm.functions.Function1; +import org.jetbrains.annotations.NotNull; +import com.github.copilot.api.CopilotChatService; +import javax.annotation.Nonnull; +import java.util.List; +import java.util.Objects; + +/** + * Service to display notifications about outdated Java project versions. + * Notifications appear in the bottom-right corner of the IDE. + * Only shows one notification at a time (the first detected issue). + */ +public class JavaVersionNotificationService { + + private static final String NOTIFICATION_GROUP_ID = "Azure Toolkit - Java Version Check"; + private static final String NOTIFICATIONS_ENABLED_KEY = "azure.toolkit.java.version.notifications.enabled"; + private static final String DEFERRED_UNTIL_KEY = "azure.toolkit.java.version.deferred_until"; + private static final long DEFER_INTERVAL_MS = 10 * 24 * 60 * 60 * 1000L; // 10 days in milliseconds + private static final String DEFAULT_MODEL_NAME = "Claude Sonnet 4.5"; + // GitHub Copilot app modernization plugin ID + private static final String COPILOT_APPMOD_PLUGIN_ID = "com.github.copilot.appmod"; + // GitHub Copilot plugin ID + private static final String COPILOT_PLUGIN_ID = "com.github.copilot"; + + private static JavaVersionNotificationService instance; + + private JavaVersionNotificationService() { + } + + public static synchronized JavaVersionNotificationService getInstance() { + if (instance == null) { + instance = new JavaVersionNotificationService(); + } + return instance; + } + + /** + * Shows a notification for the first outdated version issue. + * Only shows if notifications are enabled for the Java upgrade feature. + * + * @param project The project context + * @param issues List of detected issues + */ + public void showNotifications(@Nonnull Project project, @Nonnull List issues) { + if (project.isDisposed() || issues.isEmpty()) { + return; + } + + // Check if notifications are enabled for this feature + if (!isNotificationsEnabled(project)) { + return; + } + + // Check if we should skip based on timing (deferred) + if (!shouldCheckNow(project)) { + return; + } + + // Only show notification for the first issue + final JavaUpgradeIssue firstIssue = issues.get(0); + showNotification(project, firstIssue); + } + + /** + * Shows a single notification for an outdated version issue. + */ + private void showNotification(@Nonnull Project project, + @Nonnull JavaUpgradeIssue issue) { + final NotificationType notificationType = getNotificationType(issue.getSeverity()); + + final Notification notification = new Notification( + NOTIFICATION_GROUP_ID, + issue.getTitle(), + formatMessage(issue), + notificationType + ); + + // Add upgrade action based on whether GitHub Copilot app modernization plugin is installed + if (isUpgradeSupported(issue)) { + if (isAppModPluginInstalled()) { + // Plugin is installed - show "Upgrade" action + notification.addAction(new NotificationAction("Upgrade") { + @Override + public void actionPerformed(@NotNull AnActionEvent e, @NotNull Notification notification) { + openCopilotChatWithUpgradePrompt(project, issue); + notification.expire(); + } + }); + } else { + // Plugin is not installed - show "Install and Upgrade" action + notification.addAction(new NotificationAction("Install and Upgrade") { + @Override + public void actionPerformed(@NotNull AnActionEvent e, @NotNull Notification notification) { + // installCopilotAppModPluginAndUpgrade(project, issue); + installPlugin(project); + notification.expire(); + } + }); + } + } + + // Add "Not Now" action - defers the notification for 10 days + notification.addAction(new NotificationAction("Not Now") { + @Override + public void actionPerformed(@NotNull AnActionEvent e, @NotNull Notification notification) { + deferNotifications(); + notification.expire(); + } + }); + + // Add "Don't Show Again" action - disables the entire Java upgrade notification feature + notification.addAction(new NotificationAction("Don't Show Again") { + @Override + public void actionPerformed(@NotNull AnActionEvent e, @NotNull Notification notification) { + setNotificationsEnabled(false); + notification.expire(); + } + }); + + // Show the notification + Notifications.Bus.notify(notification, project); + } + + /** + * Formats the notification message with HTML for better display. + */ + private String formatMessage(@Nonnull JavaUpgradeIssue issue) { + final StringBuilder sb = new StringBuilder(); + sb.append(""); + sb.append(issue.getMessage()); + + if (issue.getCurrentVersion() != null && issue.getSuggestedVersion() != null) { + sb.append("

"); + sb.append("Current: ").append(issue.getCurrentVersion()); + sb.append(" → Suggested: ").append(issue.getSuggestedVersion()); + } + + sb.append(""); + return sb.toString(); + } + + /** + * Gets the notification type based on issue severity. + */ + private NotificationType getNotificationType(@Nonnull JavaUpgradeIssue.Severity severity) { + return switch (severity) { + case CRITICAL -> NotificationType.ERROR; + case WARNING -> NotificationType.WARNING; + case INFO -> NotificationType.INFORMATION; + }; + } + + /** + * Generates a unique key for an issue to track dismissals. + */ + private String getIssueKey(@Nonnull JavaUpgradeIssue issue) { + return issue.getPackageId() + ":" + + issue.getUpgradeReason().name() + ":" + + Objects.requireNonNullElse(issue.getCurrentVersion(), "unknown"); + } + + /** + * Checks if Java upgrade notifications are enabled globally. + * @return true if notifications are enabled (default), false otherwise + */ + public boolean isNotificationsEnabled() { + final PropertiesComponent properties = PropertiesComponent.getInstance(); + return properties.getBoolean(NOTIFICATIONS_ENABLED_KEY, true); + } + + /** + * Checks if Java upgrade notifications are enabled for the project. + * Uses application-level setting. + * @param project The project (for backwards compatibility, not used) + * @return true if notifications are enabled (default), false otherwise + */ + public boolean isNotificationsEnabled(@Nonnull Project project) { + return isNotificationsEnabled(); + } + + /** + * Enables or disables Java upgrade notifications globally. + * @param enabled true to enable notifications, false to disable + */ + public void setNotificationsEnabled(boolean enabled) { + final PropertiesComponent properties = PropertiesComponent.getInstance(); + properties.setValue(NOTIFICATIONS_ENABLED_KEY, enabled, true); + } + + /** + * Enables or disables Java upgrade notifications. + * Uses application-level setting. + * @param project The project (for backwards compatibility, not used) + * @param enabled true to enable notifications, false to disable + */ + public void setNotificationsEnabled(@Nonnull Project project, boolean enabled) { + setNotificationsEnabled(enabled); + } + + /** + * Checks if the notification should be shown now. + * Returns false if the user has clicked "Not Now" and the defer period hasn't passed. + */ + private boolean shouldCheckNow(@Nonnull Project project) { + final PropertiesComponent properties = PropertiesComponent.getInstance(); + final long deferredUntil = properties.getLong(DEFERRED_UNTIL_KEY, 0); + final long now = System.currentTimeMillis(); + + // If we're still in the deferred period, don't show notification + return now >= deferredUntil; + } + + /** + * Defers notifications for 10 days. + * Called when user clicks "Not Now". + */ + public void deferNotifications() { + final PropertiesComponent properties = PropertiesComponent.getInstance(); + final long deferUntil = System.currentTimeMillis() + DEFER_INTERVAL_MS; + properties.setValue(DEFERRED_UNTIL_KEY, String.valueOf(deferUntil)); + } + + /** + * Gets the timestamp until which notifications are deferred. + * @return The deferred-until timestamp in milliseconds, or 0 if not deferred + */ + public long getDeferredUntil() { + final PropertiesComponent properties = PropertiesComponent.getInstance(); + return properties.getLong(DEFERRED_UNTIL_KEY, 0); + } + + /** + * Clears the deferred notification state. + */ + public void clearDeferral() { + final PropertiesComponent properties = PropertiesComponent.getInstance(); + properties.unsetValue(DEFERRED_UNTIL_KEY); + } + + /** + * Checks if upgrade is supported for this issue type. + */ + private boolean isUpgradeSupported(@Nonnull JavaUpgradeIssue issue) { + // Upgrade support for JDK and Spring Boot + return issue.getUpgradeReason() == JavaUpgradeIssue.UpgradeReason.JRE_TOO_OLD || + issue.getPackageId().startsWith(GROUP_ID_SPRING_BOOT + ":"); + } + +// /** +// * Checks if the GitHub Copilot app modernization plugin is installed. +// * @return true if the plugin is installed, false otherwise +// */ +// private boolean isCopilotAppModPluginInstalled() { +// return PluginManagerCore.isPluginInstalled(PluginId.getId(COPILOT_APPMOD_PLUGIN_ID)); +// } + + /** + * Checks if the GitHub Copilot plugin is installed. + * @return true if the plugin is installed, false otherwise + */ + private boolean isCopilotPluginInstalled() { + return PluginManagerCore.isPluginInstalled(PluginId.getId(COPILOT_PLUGIN_ID)); + } + + /** + * Opens GitHub Copilot chat in agent mode with an upgrade prompt. + * @param project The project context + * @param issue The upgrade issue to address + */ + private void openCopilotChatWithUpgradePrompt(@Nonnull Project project, @Nonnull JavaUpgradeIssue issue) { + final String prompt = buildUpgradePrompt(issue); + openCopilotChatWithPrompt(project, prompt); + } + + /** + * Opens GitHub Copilot chat in agent mode with a given prompt. + * Tries direct API first, falls back to reflection for cross-version compatibility. + * @param project The project context + * @param prompt The prompt to send to Copilot + */ + public void openCopilotChatWithPrompt(@Nonnull Project project, @Nonnull String prompt) { + AzureTaskManager.getInstance().runLater(() -> { + if (!isAppModPluginInstalled()) { + // showGenericUpgradeGuidance(project, prompt); + installPlugin(project); + return; + } + + // Try direct API call first (works when plugin versions match) + if (tryDirectCopilotCall(project, prompt)) { + System.out.println("Direct Copilot call succeeded."); + return; // Success, no need for reflection + } + + // Fallback to reflection for cross-version compatibility + if (tryReflectionCopilotCall(project, prompt)) { + System.out.println("Reflection Copilot call succeeded."); + return; // Success via reflection + } + + // Both approaches failed + System.out.println("Both direct and reflection Copilot calls failed."); + showGenericUpgradeGuidance(project, prompt); + }); + } + + /** + * Tries to call CopilotChatService directly (works when compile-time and runtime versions match). + * @return true if successful, false if an error occurred + */ + private boolean tryDirectCopilotCall(@Nonnull Project project, @Nonnull String prompt) { + try { + CopilotChatService service = project.getService(CopilotChatService.class); + if (service != null) { + service.query(DataContext.EMPTY_CONTEXT, builder -> { + builder.withInput(prompt); + builder.withAgentMode(); + builder.withNewSession(); + withModelCompatibility(builder, DEFAULT_MODEL_NAME); + builder.withSessionIdReceiver(sessionId -> null); + return null; + }); + System.out.println("Direct Copilot call succeeded."); + return true; + } + } catch (Error | Exception e) { + // Direct call failed (version mismatch, class not found, etc.) - will try reflection + System.out.println("Direct Copilot call failed: " + e.getMessage()); + } + return false; + } + + /** + * Tries to call CopilotChatService via reflection for cross-version compatibility. + * @return true if successful, false if an error occurred + */ + private boolean tryReflectionCopilotCall(@Nonnull Project project, @Nonnull String prompt) { + try { + // Get the Copilot plugin's classloader to load its classes + final IdeaPluginDescriptor copilotPlugin = PluginManagerCore.getPlugin(PluginId.getId(COPILOT_PLUGIN_ID)); + if (copilotPlugin == null || !copilotPlugin.isEnabled()) { + return false; + } + + final ClassLoader copilotClassLoader = copilotPlugin.getPluginClassLoader(); + if (copilotClassLoader == null) { + return false; + } + + // Use reflection to load CopilotChatService from the Copilot plugin's classloader + Class copilotChatServiceClass = copilotClassLoader.loadClass("com.github.copilot.api.CopilotChatService"); + Object service = project.getService(copilotChatServiceClass); + + if (service != null) { + // Find the query method dynamically - signature may vary across Copilot versions + Method queryMethod = findQueryMethod(copilotChatServiceClass); + if (queryMethod == null) { + return false; + } + + // Use Kotlin Function1 since the Copilot API is written in Kotlin + Function1 queryBuilder = builder -> { + try { + builder.getClass().getMethod("withInput", String.class).invoke(builder, prompt); + builder.getClass().getMethod("withAgentMode").invoke(builder); + builder.getClass().getMethod("withNewSession").invoke(builder); + withModelCompatibility(builder, DEFAULT_MODEL_NAME); + Method withSessionIdReceiverMethod = findMethodByName(builder.getClass(), "withSessionIdReceiver"); + if (withSessionIdReceiverMethod != null) { + Function1 sessionIdReceiver = sessionId -> Unit.INSTANCE; + withSessionIdReceiverMethod.invoke(builder, sessionIdReceiver); + } + } catch (Exception ex) { + // Error configuring query builder via reflection + } + return Unit.INSTANCE; + }; + queryMethod.invoke(service, DataContext.EMPTY_CONTEXT, queryBuilder); + return true; + } + } catch (Exception e) { + // Reflection call failed + } + return false; + } + + /** + * Shows generic guidance for upgrading when Copilot is not available. + * @param project The project context + * @param prompt The upgrade prompt that would be used + */ + private void showGenericUpgradeGuidance(@Nonnull Project project, @Nonnull String prompt) { + final String guidance = String.format( + "Open GitHub Copilot chat and use the following prompt in agent mode:\n\n%s", prompt); + + final Notification guidanceNotification = new Notification( + NOTIFICATION_GROUP_ID, + "Upgrade Guidance", + guidance, + NotificationType.INFORMATION + ); + + Notifications.Bus.notify(guidanceNotification, project); + } + + /** + * Sets the model for the query builder using reflection for compatibility with older versions of GitHub Copilot. + * Note: The API 'withModel' is supported starting from Copilot version '1.5.63'. + * @param builder the query option builder + * @param modelName the name of the model to set + */ + private static void withModelCompatibility(Object builder, String modelName) { + try { + builder.getClass().getMethod("withModel", String.class).invoke(builder, modelName); + } catch (NoSuchMethodException ex) { + // Method withModel not found in QueryOptionBuilder, skipping + } catch (Exception ex) { + // Error calling withModel via reflection, can be ignored + } + } + + /** + * Finds the 'query' method in CopilotChatService dynamically. + * The method signature may vary across Copilot versions. + * @param serviceClass The CopilotChatService class + * @return The query method, or null if not found + */ + private Method findQueryMethod(Class serviceClass) { + for (Method method : serviceClass.getMethods()) { + if ("query".equals(method.getName()) && method.getParameterCount() == 2) { + Class[] paramTypes = method.getParameterTypes(); + // Look for query(DataContext, Function/Consumer/etc) + if (DataContext.class.isAssignableFrom(paramTypes[0])) { + return method; + } + } + } + return null; + } + + /** + * Finds a method by name in a class (first match). + * @param clazz The class to search + * @param methodName The method name to find + * @return The method, or null if not found + */ + private Method findMethodByName(Class clazz, String methodName) { + for (Method method : clazz.getMethods()) { + if (methodName.equals(method.getName())) { + return method; + } + } + return null; + } + + /** + * Builds the upgrade prompt for Copilot based on the issue type. + * @param issue The upgrade issue + * @return The prompt string for Copilot + */ + private String buildUpgradePrompt(@Nonnull JavaUpgradeIssue issue) { + if (issue.getUpgradeReason() == JavaUpgradeIssue.UpgradeReason.JRE_TOO_OLD) { + return String.format("upgrade java runtime to Java %s (LTS) using java upgrade tools by invoking #generate_upgrade_plan", MATURE_JAVA_LTS_VERSION); + } else if (issue.getPackageId().startsWith(GROUP_ID_SPRING_BOOT + ":")) { + return String.format("upgrade java framework dependencies of this project to latest LTS version using java upgrade tools by invoking #generate_upgrade_plan"); + } + return "upgrade java framework dependencies of this project to latest LTS version using java upgrade tools by invoking #generate_upgrade_plan"; + } + + /** + * Installs the GitHub Copilot app modernization plugin and then opens Copilot chat. + * @param project The project context + * @param issue The upgrade issue to address + */ + private void installCopilotAppModPluginAndUpgrade(@Nonnull Project project, @Nonnull JavaUpgradeIssue issue) { +// final Set pluginIds = new HashSet<>(); +// pluginIds.add(COPILOT_APPMOD_PLUGIN_ID); +// +// AzureTaskManager.getInstance().runLater(() -> { +// PluginsAdvertiser.installAndEnablePlugins(pluginIds, () -> { +// PluginInstaller.addStateListener(new PluginStateListener() { +// @Override +// public void install(@NotNull IdeaPluginDescriptor descriptor) { +// if (COPILOT_APPMOD_PLUGIN_ID.equals(descriptor.getPluginId().getIdString())) { +// // Plugin installed successfully, now open Copilot chat +// ApplicationManager.getApplication().invokeLater(() -> { +// openCopilotChatWithUpgradePrompt(project, issue); +// }); +// } +// } +// +// @Override +// public void uninstall(@NotNull IdeaPluginDescriptor descriptor) { +// // Not needed +// } +// }); +// }); +// }); + } + + /** + * Shows guidance for upgrading the project when Copilot is not available. + * @param project The project context + * @param issue The upgrade issue + * @param prompt The upgrade prompt that would be used + */ + private void showUpgradeGuidance(@Nonnull Project project, @Nonnull JavaUpgradeIssue issue, @Nonnull String prompt) { + showGenericUpgradeGuidance(project, prompt); + } + + /** + * Resets notification settings (useful for testing or reset). + * Re-enables notifications and clears the deferral. + */ + public void resetNotificationSettings() { + final PropertiesComponent properties = PropertiesComponent.getInstance(); + properties.unsetValue(NOTIFICATIONS_ENABLED_KEY); + properties.unsetValue(DEFERRED_UNTIL_KEY); + } + + /** + * Resets notification settings for a project (for backwards compatibility). + * @param project The project (not used, settings are application-level) + */ + public void resetNotificationSettings(@Nonnull Project project) { + resetNotificationSettings(); + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/settings/JavaUpgradeConfigurable.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/settings/JavaUpgradeConfigurable.java new file mode 100644 index 00000000000..392c7ccbc87 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/settings/JavaUpgradeConfigurable.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.settings; + +import com.intellij.openapi.options.Configurable; +import com.intellij.openapi.options.ConfigurationException; +import com.intellij.openapi.util.NlsContexts; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; + +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Settings page for Java Upgrade feature. + * Allows users to enable/disable notifications and reset deferral settings. + * + * Accessible via: Settings → Tools → Azure Toolkit → Java Upgrade + */ +public class JavaUpgradeConfigurable implements Configurable { + + private JPanel mainPanel; + private JCheckBox enableNotificationsCheckBox; + private JButton resetDeferralButton; + private JLabel deferralStatusLabel; + + @Override + public @NlsContexts.ConfigurableName String getDisplayName() { + return "Java Upgrade"; + } + + @Override + public @Nullable JComponent createComponent() { + mainPanel = new JPanel(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.anchor = GridBagConstraints.NORTHWEST; + gbc.insets = new Insets(2, 0, 2, 5); + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weightx = 1.0; + + // Title label + gbc.gridx = 0; + gbc.gridy = 0; + JLabel titleLabel = new JLabel("Java Upgrade Notifications"); + mainPanel.add(titleLabel, gbc); + + // Description + gbc.gridy = 1; + JLabel descriptionLabel = new JLabel("Configure notifications for outdated JDK versions, " + + "framework versions, and security vulnerabilities."); + mainPanel.add(descriptionLabel, gbc); + + // Enable notifications checkbox + gbc.gridy = 2; + enableNotificationsCheckBox = new JCheckBox("Show notifications for Java upgrade issues"); + enableNotificationsCheckBox.setToolTipText( + "When enabled, balloon notifications will appear when outdated JDK or framework versions are detected."); + mainPanel.add(enableNotificationsCheckBox, gbc); + + // Deferral section subtitle (indented) + gbc.gridy = 3; + gbc.insets = new Insets(2, 20, 2, 5); + JLabel deferralTitle = new JLabel("Notification Deferral"); + mainPanel.add(deferralTitle, gbc); + + // Deferral status (indented) + gbc.gridy = 4; + deferralStatusLabel = new JLabel(); + updateDeferralStatusLabel(); + mainPanel.add(deferralStatusLabel, gbc); + + // Reset deferral button (indented) + gbc.gridy = 5; + gbc.fill = GridBagConstraints.NONE; + gbc.anchor = GridBagConstraints.WEST; + resetDeferralButton = new JButton("Reset Deferral"); + resetDeferralButton.setToolTipText("Clear the \"Not Now\" deferral and allow notifications to show immediately."); + resetDeferralButton.addActionListener(e -> { + JavaVersionNotificationService.getInstance().clearDeferral(); + updateDeferralStatusLabel(); + }); + mainPanel.add(resetDeferralButton, gbc); + + // Spacer to push content to the top + gbc.gridy = 6; + gbc.insets = new Insets(2, 0, 2, 5); + gbc.weighty = 1.0; + gbc.fill = GridBagConstraints.BOTH; + mainPanel.add(new JPanel(), gbc); + + return mainPanel; + } + + private void updateDeferralStatusLabel() { + if (deferralStatusLabel == null || resetDeferralButton == null) { + return; + } + final JavaVersionNotificationService service = JavaVersionNotificationService.getInstance(); + final long deferredUntil = service.getDeferredUntil(); + final long now = System.currentTimeMillis(); + + if (deferredUntil > now) { + // Notifications are deferred + SimpleDateFormat dateFormat = new SimpleDateFormat("MMM dd, yyyy HH:mm"); + String dateStr = dateFormat.format(new Date(deferredUntil)); + deferralStatusLabel.setText("Notifications deferred until: " + dateStr + ""); + resetDeferralButton.setEnabled(true); + } else { + deferralStatusLabel.setText("Notifications are not deferred."); + resetDeferralButton.setEnabled(false); + } + } + + @Override + public boolean isModified() { + if (enableNotificationsCheckBox == null) { + return false; + } + final JavaVersionNotificationService service = JavaVersionNotificationService.getInstance(); + return enableNotificationsCheckBox.isSelected() != service.isNotificationsEnabled(); + } + + @Override + public void apply() throws ConfigurationException { + if (enableNotificationsCheckBox == null) { + return; + } + final JavaVersionNotificationService service = JavaVersionNotificationService.getInstance(); + service.setNotificationsEnabled(enableNotificationsCheckBox.isSelected()); + } + + @Override + public void reset() { + if (enableNotificationsCheckBox == null) { + return; + } + final JavaVersionNotificationService service = JavaVersionNotificationService.getInstance(); + enableNotificationsCheckBox.setSelected(service.isNotificationsEnabled()); + updateDeferralStatusLabel(); + } + + @Override + public void disposeUIResources() { + mainPanel = null; + enableNotificationsCheckBox = null; + resetDeferralButton = null; + deferralStatusLabel = null; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml index e8647c39271..44574fea17b 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml @@ -3,11 +3,49 @@ - + + + + + + XML + com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action.UpgradeInQuickFixIntentionAction + Azure Toolkit + + + + + + + + + + + + + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java index f28053e83d6..ef99b223ea4 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java @@ -17,7 +17,7 @@ import com.microsoft.azure.toolkit.intellij.connector.dotazure.AzureModule; import com.microsoft.azure.toolkit.intellij.appmod.IMigrateOptionProvider; import com.microsoft.azure.toolkit.intellij.appmod.MigrateNodeData; -import com.microsoft.azure.toolkit.intellij.appmod.MigratePluginInstaller; +import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; import com.microsoft.azure.toolkit.ide.common.icon.AzureIcons; import com.microsoft.azure.toolkit.lib.common.action.Action; import com.microsoft.azure.toolkit.lib.common.action.ActionGroup; @@ -63,7 +63,7 @@ private List getMigrationNodes() { * Computes migration nodes from extension point providers. */ private List computeMigrationNodes() { - if (!MigratePluginInstaller.isAppModPluginInstalled()) { + if (!AppModPluginInstaller.isAppModPluginInstalled()) { return List.of(); } final List nodes = migrationProviders.getExtensionList().stream() @@ -123,8 +123,8 @@ public Collection> buildChildren() { protected void buildView(@Nonnull PresentationData presentation) { presentation.setIcon(IntelliJAzureIcons.getIcon(Constants.ICON_APPMOD_PATH)); - if (!MigratePluginInstaller.isAppModPluginInstalled()) { - final boolean copilotInstalled = MigratePluginInstaller.isCopilotInstalled(); + if (!AppModPluginInstaller.isAppModPluginInstalled()) { + final boolean copilotInstalled = AppModPluginInstaller.isCopilotInstalled(); final String text = copilotInstalled ? "Migrate to Azure (Install Github Copilot app modernization)" : "Migrate to Azure (Install GitHub Copilot and app modernization)"; @@ -139,11 +139,11 @@ protected void buildView(@Nonnull PresentationData presentation) { @Override public void navigate(boolean requestFocus) { - if (!MigratePluginInstaller.isAppModPluginInstalled()) { + if (!AppModPluginInstaller.isAppModPluginInstalled()) { // Plugin not installed - trigger install on double-click AppModUtils.logTelemetryEvent("facet.click-install"); - MigratePluginInstaller.showInstallConfirmation(getProject(), - () -> MigratePluginInstaller.installPlugin(getProject())); + AppModPluginInstaller.showInstallConfirmation(getProject(), + () -> AppModPluginInstaller.installPlugin(getProject())); } else if (!hasMigrationOptions()) { // No migration options - open App Modernization Panel AppModPanelHelper.openAppModPanel(getProject(), "facet"); @@ -153,14 +153,14 @@ public void navigate(boolean requestFocus) { @Override public boolean canNavigate() { // Enable navigation when plugin is not installed OR when no migration options - return !MigratePluginInstaller.isAppModPluginInstalled() || !hasMigrationOptions(); + return !AppModPluginInstaller.isAppModPluginInstalled() || !hasMigrationOptions(); } @Override public @Nonnull LeafState getLeafState() { // Use ASYNC to avoid triggering extension point loading synchronously // The actual leaf state will be determined when buildChildren() is called - if (!MigratePluginInstaller.isAppModPluginInstalled()) { + if (!AppModPluginInstaller.isAppModPluginInstalled()) { return LeafState.ALWAYS; } // ASYNC means IntelliJ will call buildChildren() to determine if there are children From a2f78128d1694d5c18e5156abaf165fac627aefe Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Mon, 26 Jan 2026 15:12:12 +0800 Subject: [PATCH 17/43] move migration code to javamigration package and add utils package --- .../docs/architecture.md | 255 ------------------ .../appmod/common/AppModPluginInstaller.java | 2 +- .../IMigrateOptionProvider.java | 2 +- .../{ => javamigration}/MigrateNodeData.java | 2 +- .../MigrateToAzureAction.java | 4 +- .../MigrateToAzureNode.java | 5 +- .../appmod/{ => utils}/AppModPanelHelper.java | 2 +- .../appmod/{ => utils}/AppModUtils.java | 2 +- .../appmod/{ => utils}/Constants.java | 2 +- .../META-INF/azure-intellij-plugin-appmod.xml | 4 +- .../intellij/explorer/AzureExplorer.java | 4 +- .../MigrateToAzureFacetNode.java | 10 +- 12 files changed, 21 insertions(+), 273 deletions(-) delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/docs/architecture.md rename PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/{ => javamigration}/IMigrateOptionProvider.java (97%) rename PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/{ => javamigration}/MigrateNodeData.java (99%) rename PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/{ => javamigration}/MigrateToAzureAction.java (97%) rename PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/{ => javamigration}/MigrateToAzureNode.java (96%) rename PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/{ => utils}/AppModPanelHelper.java (96%) rename PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/{ => utils}/AppModUtils.java (97%) rename PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/{ => utils}/Constants.java (63%) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/docs/architecture.md b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/docs/architecture.md deleted file mode 100644 index 9ca4897f4e6..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/docs/architecture.md +++ /dev/null @@ -1,255 +0,0 @@ -# App Modernization Module Architecture - -## Overview - -The `azure-intellij-plugin-appmod` module provides the "Migrate to Azure" functionality in Azure Toolkit for IntelliJ. It serves as a bridge to integrate GitHub Copilot App Modernization plugin with Azure Toolkit. - -## Plugin Relationship Diagram - -``` -┌──────────────────────────────────────────────────────────────────────────────────┐ -│ IntelliJ IDEA │ -│ │ -│ ┌────────────────────────────────────────────────────────────────────────────┐ │ -│ │ Azure Toolkit for IntelliJ │ │ -│ │ │ │ -│ │ ┌────────────────────────┐ ┌────────────────────────────────────────┐ │ │ -│ │ │ service-explorer │ │ resource-connector-lib │ │ │ -│ │ │ │ │ │ │ │ -│ │ │ ┌──────────────────┐ │ │ ┌──────────────────────────────────┐ │ │ │ -│ │ │ │MigrateToAzureNode│ │ │ │ MigrateToAzureFacetNode │ │ │ │ -│ │ │ └────────┬─────────┘ │ │ └───────────────┬──────────────────┘ │ │ │ -│ │ └───────────┼────────────┘ └──────────────────┼─────────────────────┘ │ │ -│ │ │ │ │ │ -│ │ └─────────────────┬──────────────────┘ │ │ -│ │ ▼ │ │ -│ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ -│ │ │ azure-intellij-plugin-appmod │ │ │ -│ │ │ │ │ │ -│ │ │ • IMigrateOptionProvider (Extension Point Interface) │ │ │ -│ │ │ • MigrateNodeData (Data Model) │ │ │ -│ │ │ • MigratePluginInstaller (Plugin Detection/Installation) │ │ │ -│ │ │ • MigrateToAzureAction (Context Menu) │ │ │ -│ │ │ │ │ │ -│ │ └──────────────────────────────────┬───────────────────────────────────┘ │ │ -│ │ │ │ │ -│ │ │ Extension Point │ │ -│ │ ▼ │ │ -│ └────────────────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ │ implements │ -│ ▼ │ -│ ┌────────────────────────────────────────────────────────────────────────────┐ │ -│ │ GitHub Copilot App Modernization Plugin │ │ -│ │ (appmod-intellij) │ │ -│ │ │ │ -│ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ -│ │ │ MyMigrationProvider implements IMigrateOptionProvider │ │ │ -│ │ │ │ │ │ -│ │ │ • createNodeData() → Returns migration options │ │ │ -│ │ │ • isApplicable() → Check project compatibility │ │ │ -│ │ └──────────────────────────────────────────────────────────────────────┘ │ │ -│ │ │ │ -│ │ Depends on: com.github.copilot (GitHub Copilot) │ │ -│ └────────────────────────────────────────────────────────────────────────────┘ │ -│ │ -└──────────────────────────────────────────────────────────────────────────────────┘ -``` - -## Data Flow - -``` -User Action (click/expand) - │ - ▼ -┌─────────────────────┐ -│ Entry Point │ (MigrateToAzureNode / MigrateToAzureFacetNode / MigrateToAzureAction) -└─────────┬───────────┘ - │ - ▼ -┌─────────────────────┐ No ┌─────────────────────┐ -│ Plugin Installed? │────────────▶│ Show Install Dialog │ -└─────────┬───────────┘ └─────────────────────┘ - │ Yes - ▼ -┌─────────────────────┐ -│ Load Extension │ -│ Providers │ -└─────────┬───────────┘ - │ - ▼ -┌─────────────────────┐ -│ Filter by │ -│ isApplicable() │ -└─────────┬───────────┘ - │ - ▼ -┌─────────────────────┐ -│ Sort by Priority │ -└─────────┬───────────┘ - │ - ▼ -┌─────────────────────┐ -│ Call createNodeData │ -│ for each provider │ -└─────────┬───────────┘ - │ - ▼ -┌─────────────────────┐ -│ Display Nodes │ -│ in UI │ -└─────────────────────┘ -``` - -## Module Structure - -``` -azure-intellij-plugin-appmod/ -├── build.gradle.kts -├── docs/ -│ └── architecture.md -└── src/main/ - ├── java/com/microsoft/azure/toolkit/intellij/appmod/ - │ ├── IMigrateOptionProvider.java # Extension Point interface - │ ├── MigrateNodeData.java # Node data model - │ ├── MigratePluginInstaller.java # Plugin detection & installation - │ ├── MigrateToAzureNode.java # Service Explorer entry point - │ ├── MigrateToAzureAction.java # Context menu entry point - │ ├── InstallPluginDialog.java # Installation confirmation dialog - │ └── RestartIdeDialog.java # Restart prompt dialog - └── resources/ - ├── META-INF/azure-intellij-plugin-appmod.xml - └── icons/app_mod.svg -``` - -## Entry Points - -The module provides **three entry points** for users to access migration functionality: - -### 1. Service Explorer Node (`MigrateToAzureNode`) -- **Location**: Azure Explorer panel → "Migrate to Azure" node -- **Behavior**: - - If plugins installed → Shows child nodes from extension providers - - If plugins not installed → Double-click triggers installation dialog - -### 2. Project Explorer Node (`MigrateToAzureFacetNode`) -- **Location**: Project Explorer → Azure facet → "Migrate to Azure" node -- **Note**: Located in `azure-intellij-resource-connector-lib` module (due to `AbstractAzureFacetNode` inheritance) -- **Behavior**: Same as Service Explorer Node - -### 3. Context Menu Action (`MigrateToAzureAction`) -- **Location**: Right-click on project/module → "Migrate to Azure" submenu -- **Behavior**: - - If plugins installed → Shows child actions from extension providers - - If plugins not installed → Single "Install Plugins" action - -## Extension Point - -### Definition -```xml - -``` - -Full ID: `com.microsoft.tooling.msservices.intellij.azure.migrateOptionProvider` - -### Interface: `IMigrateOptionProvider` -```java -public interface IMigrateOptionProvider { - // Check if this provider applies to the given project - boolean isApplicable(@Nonnull Project project); - - // Create node data for display (can return multiple nodes) - @Nonnull List createNodeData(@Nonnull Project project); - - // Priority for ordering (lower = first) - default int getPriority() { return 100; } -} -``` - -### Data Model: `MigrateNodeData` -```java -MigrateNodeData.builder() - .label("Node Label") // Required: display text - .description("Optional description") // Shown as location string - .tooltip("Hover tooltip") // Tooltip text - .iconPath("/icons/my_icon.svg") // Icon path (falls back to app_mod.svg) - .visible(true) // Visibility control - .onDoubleClick(anActionEvent -> {...}) // Double-click handler - .children(childList) // Static children - .childrenLoader(() -> loadChildren()) // OR lazy-loaded children - .build(); -``` - -## Plugin Detection & Installation - -### `MigratePluginInstaller` -Central utility class for plugin management: - -```java -// Check if plugins are installed -MigratePluginInstaller.isAppModPluginInstalled(); // com.github.copilot.appmod -MigratePluginInstaller.isCopilotInstalled(); // com.github.copilot - -// Show installation confirmation dialog -MigratePluginInstaller.showInstallConfirmation(project, onConfirmCallback); - -// Trigger installation (IntelliJ handles the rest) -MigratePluginInstaller.installPlugin(project); - -// Dev mode detection (runIde task) -MigratePluginInstaller.isRunningInDevMode(); -``` - -### Installation Flow -1. User triggers install (double-click node or context menu) -2. `showInstallConfirmation()` shows confirmation dialog -3. On confirm, `installPlugin()` calls `PluginsAdvertiser.installAndEnable()` -4. IntelliJ platform handles: - - Plugin selection dialog (with all plugins pre-selected) - - Download and installation - - Restart prompt -5. In dev mode: Special message shown (don't click IDE restart, re-run `./gradlew runIde`) - -## Module Dependencies - -``` -azure-intellij-plugin-appmod (base module) - ↑ - ├── azure-intellij-plugin-service-explorer - │ └── Uses: MigrateToAzureNode, Extension Point - │ - └── azure-intellij-resource-connector-lib - └── Contains: MigrateToAzureFacetNode (due to inheritance constraint) -``` - -### Why `MigrateToAzureFacetNode` is in connector-lib? -- Must extend `AbstractAzureFacetNode` from connector-lib -- Moving `AbstractAzureFacetNode` to appmod would require moving many other classes -- Current design minimizes code changes while maintaining clean architecture - -## External Plugin Integration - -The `appmod-intellij` plugin (GitHub Copilot App Modernization) should: - -1. Add dependency on `azure-intellij-plugin-appmod` -2. Implement `IMigrateOptionProvider` extension -3. Register in its `plugin.xml`: -```xml - - - -``` - -## UI Behavior Summary - -| State | Service Explorer | Project Explorer | Context Menu | -|-------|-----------------|------------------|--------------| -| Plugins NOT installed | Node shows "(Install...)" suffix, double-click triggers install | Same as Service Explorer | Shows "Install Plugins" action | -| Plugins installed | Expand to show child nodes from providers | Same as Service Explorer | Shows submenu with actions from providers | - -## Icon - -- **Path**: `/icons/app_mod.svg` -- **Location**: `azure-intellij-plugin-appmod/src/main/resources/icons/` -- **Usage**: Centralized icon for all migrate-related nodes and actions diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java index 51c0c60a008..150f3c755a9 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java @@ -11,7 +11,7 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.updateSettings.impl.pluginsAdvertisement.PluginsAdvertiser; import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager; -import com.microsoft.azure.toolkit.intellij.appmod.AppModUtils; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; import javax.annotation.Nonnull; import java.util.LinkedHashSet; diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateOptionProvider.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/IMigrateOptionProvider.java similarity index 97% rename from PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateOptionProvider.java rename to PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/IMigrateOptionProvider.java index df7befbf960..a82fb4277f7 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/IMigrateOptionProvider.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/IMigrateOptionProvider.java @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -package com.microsoft.azure.toolkit.intellij.appmod; +package com.microsoft.azure.toolkit.intellij.appmod.javamigration; import com.intellij.openapi.project.Project; diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateNodeData.java similarity index 99% rename from PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java rename to PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateNodeData.java index 49541eb262a..1733fddd1aa 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateNodeData.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateNodeData.java @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -package com.microsoft.azure.toolkit.intellij.appmod; +package com.microsoft.azure.toolkit.intellij.appmod.javamigration; import lombok.Getter; import lombok.Setter; diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java similarity index 97% rename from PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java rename to PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java index f2c5945e469..1e5d6fa5143 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -package com.microsoft.azure.toolkit.intellij.appmod; +package com.microsoft.azure.toolkit.intellij.appmod.javamigration; import com.intellij.icons.AllIcons; import com.intellij.openapi.actionSystem.ActionGroup; @@ -15,6 +15,8 @@ import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Key; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModPanelHelper; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java similarity index 96% rename from PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java rename to PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java index a10e4598ff5..cd4c43dea06 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/MigrateToAzureNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java @@ -3,13 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -package com.microsoft.azure.toolkit.intellij.appmod; +package com.microsoft.azure.toolkit.intellij.appmod.javamigration; import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.project.Project; import com.microsoft.azure.toolkit.ide.common.component.Node; import com.microsoft.azure.toolkit.ide.common.icon.AzureIcon; import com.microsoft.azure.toolkit.ide.common.icon.AzureIcons; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModPanelHelper; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; +import com.microsoft.azure.toolkit.intellij.appmod.utils.Constants; import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; import com.microsoft.azure.toolkit.lib.common.action.Action; import com.microsoft.azure.toolkit.lib.common.action.ActionGroup; diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModPanelHelper.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/utils/AppModPanelHelper.java similarity index 96% rename from PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModPanelHelper.java rename to PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/utils/AppModPanelHelper.java index 5cd329c3192..b84084136c5 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModPanelHelper.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/utils/AppModPanelHelper.java @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -package com.microsoft.azure.toolkit.intellij.appmod; +package com.microsoft.azure.toolkit.intellij.appmod.utils; import com.intellij.openapi.project.Project; import com.intellij.openapi.wm.ToolWindow; diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModUtils.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/utils/AppModUtils.java similarity index 97% rename from PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModUtils.java rename to PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/utils/AppModUtils.java index 49ace1fa9bd..b114b7f3f85 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/AppModUtils.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/utils/AppModUtils.java @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -package com.microsoft.azure.toolkit.intellij.appmod; +package com.microsoft.azure.toolkit.intellij.appmod.utils; import com.intellij.openapi.diagnostic.Logger; import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager; diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/Constants.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/utils/Constants.java similarity index 63% rename from PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/Constants.java rename to PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/utils/Constants.java index 00b7b605325..8d1bf611621 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/Constants.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/utils/Constants.java @@ -1,4 +1,4 @@ -package com.microsoft.azure.toolkit.intellij.appmod; +package com.microsoft.azure.toolkit.intellij.appmod.utils; public class Constants { diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml index 44574fea17b..5cbc896ed4b 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml @@ -1,7 +1,7 @@ + interface="com.microsoft.azure.toolkit.intellij.appmod.javamigration.IMigrateOptionProvider"/> @@ -30,7 +30,7 @@ - Date: Mon, 26 Jan 2026 23:35:28 +0800 Subject: [PATCH 18/43] Enhance the feature function and remove unused code --- .../appmod/common/AppModPluginInstaller.java | 2 +- .../intellij/appmod/javaupgrade/Contants.java | 15 + .../JavaUpgradeCheckStartupActivity.java | 2 +- .../action/CveFixInProblemsViewAction.java | 126 ++++ ...Action.java => CveFixIntentionAction.java} | 90 +-- ...java => JavaUpgradeContextMenuAction.java} | 19 +- .../action/JavaUpgradeQuickFix.java | 73 +++ .../action/UpgradeActionRegistrar.java | 17 +- .../action/UpgradeInProblemsViewAction.java | 602 ------------------ .../JavaUpgradeIssuesInspection.java | 196 ++---- .../javaupgrade/service/CVECheckService.java | 4 + .../service/JavaUpgradeIssuesCache.java | 39 +- .../JavaUpgradeIssuesDetectionService.java | 34 - .../JavaVersionNotificationService.java | 116 +--- .../META-INF/azure-intellij-plugin-appmod.xml | 15 +- 15 files changed, 365 insertions(+), 985 deletions(-) create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/Contants.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixInProblemsViewAction.java rename PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/{UpgradeInQuickFixIntentionAction.java => CveFixIntentionAction.java} (60%) rename PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/{UpgradeProjectAction.java => JavaUpgradeContextMenuAction.java} (82%) create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInProblemsViewAction.java diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java index 150f3c755a9..198890e7137 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java @@ -26,7 +26,7 @@ public class AppModPluginInstaller { private static final String PLUGIN_ID = "com.github.copilot.appmod"; private static final String COPILOT_PLUGIN_ID = "com.github.copilot"; - + public static final String TO_INSTALL_APP_MODE_PLUGIN = " (Install Github Copilot app modernization)"; private AppModPluginInstaller() { // Utility class - prevent instantiation } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/Contants.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/Contants.java new file mode 100644 index 00000000000..019092aff81 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/Contants.java @@ -0,0 +1,15 @@ +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade; + +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService.MATURE_JAVA_LTS_VERSION; + +public class Contants { + public static final String UPGRADE_JAVA_AND_FRAMEWORK_PROMPT = "Upgrade java runtime and java framework dependencies of this project to the latest LTS version using java upgrade tools by invoking #generate_upgrade_plan"; + public static final String UPGRADE_JAVA_VERSION_PROMPT = "upgrade java runtime to Java " + MATURE_JAVA_LTS_VERSION + " (LTS) using java upgrade tools by invoking #generate_upgrade_plan"; + public static final String UPGRADE_JAVA_FRAMEWORK_PROMPT = "upgrade java framework dependencies of this project to latest LTS version using java upgrade tools by invoking #generate_upgrade_plan"; + public static final String SCAN_AND_RESOLVE_CVES_PROMPT = "run CVE scan for this project using java upgrade tools by invoking #validate_cves_for_java"; + public static final String UPGRADE_JDK_WITH_COPILOT_DISPLAY_NAME = "Upgrade JDK with Copilot"; + public static final String UPGRADE_SPRING_BOOT_WITH_COPILOT_DISPLAY_NAME = "Upgrade Spring Boot with Copilot"; + public static final String UPGRADE_SPRING_FRAMEWORK_WITH_COPILOT_DISPLAY_NAME = "Upgrade Spring Framework with Copilot"; + public static final String UPGRADE_SPRING_SECURITY_WITH_COPILOT_DISPLAY_NAME = "Upgrade Spring Security with Copilot"; + public static final String SCAN_AND_RESOLVE_CVES_WITH_COPILOT_DISPLAY_NAME = "Scan and Resolve CVEs with Copilot"; +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java index b0cc134fb81..5eb1606dad9 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java @@ -72,7 +72,7 @@ private void performJavaUpgradeCheck(@Nonnull Project project) { final List allIssues = new java.util.ArrayList<>(); allIssues.addAll(cache.getJdkIssues()); allIssues.addAll(cache.getDependencyIssues()); - allIssues.addAll(detectionService.getCVEIssues(project)); + allIssues.addAll(cache.getCveIssues()); // Update UI on the main thread AzureTaskManager.getInstance().runLater(() -> { diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixInProblemsViewAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixInProblemsViewAction.java new file mode 100644 index 00000000000..ee97e00cfa1 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixInProblemsViewAction.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action; + +import com.intellij.openapi.actionSystem.ActionUpdateThread; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.actionSystem.PlatformDataKeys; +import com.intellij.openapi.project.DumbAware; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.*; + +/** + * Action to fix vulnerable dependencies by opening GitHub Copilot chat with an upgrade prompt. + * This action appears in the Problems View context menu for vulnerable dependency issues. + */ +public class CveFixInProblemsViewAction extends AnAction implements DumbAware { + + private static final String CVE_MARKER = "CVE-"; + + public CveFixInProblemsViewAction() { + super(); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + final Project project = e.getData(CommonDataKeys.PROJECT); + if (project == null || project.isDisposed()) { + return; + } + + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt( + project, + SCAN_AND_RESOLVE_CVES_PROMPT + ); + } + + @Override + public void update(@NotNull AnActionEvent e) { + final Project project = e.getData(CommonDataKeys.PROJECT); + if (project == null || project.isDisposed()) { + e.getPresentation().setEnabledAndVisible(false); + return; + } + + // Check if we're in the Problems View context with a vulnerability + final String description = extractProblemDescription(e); + if (description == null) { + e.getPresentation().setEnabledAndVisible(false); + return; + } + + // Also check if the file is pom.xml or build.gradle (common for dependency issues) + final VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE); + final boolean isBuildFile = isBuildFile(file); + + if (!isBuildFile || !isCVEIssue(description)) { + e.getPresentation().setEnabledAndVisible(false); + return; + } + + e.getPresentation().setEnabledAndVisible(true); + e.getPresentation().setText(SCAN_AND_RESOLVE_CVES_WITH_COPILOT_DISPLAY_NAME); + if (!AppModPluginInstaller.isAppModPluginInstalled()) { + e.getPresentation().setText(SCAN_AND_RESOLVE_CVES_WITH_COPILOT_DISPLAY_NAME + AppModPluginInstaller.TO_INSTALL_APP_MODE_PLUGIN); + } + } + + private boolean isBuildFile(VirtualFile file){ + return file != null && + (file.getName().equals("pom.xml") || file.getName().endsWith(".gradle") || file.getName().endsWith(".gradle.kts")); + } + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.BGT; + } + + /** + * Extracts the problem description from the action event context. + */ + @Nullable + private String extractProblemDescription(@NotNull AnActionEvent e) { + + // Approach 3: Try to get selected items from the tree + try { + final Object[] selectedItems = e.getData(PlatformDataKeys.SELECTED_ITEMS); + if (selectedItems != null && selectedItems.length > 0) { + // Concatenate all selected items' string representations + final StringBuilder sb = new StringBuilder(); + for (Object item : selectedItems) { + if (item != null) { + sb.append(item.toString()).append(" "); + } + } + final String result = sb.toString().trim(); + if (!result.isEmpty()) { + return result; + } + } + } catch (Exception ignored) { + } + return null; + } + + /** + * Extracts CVE ID from the problem description. + */ + private boolean isCVEIssue(@NotNull String description) { + // Pattern: CVE-YYYY-NNNNN + final int cveIndex = description.toUpperCase().indexOf(CVE_MARKER); + if (cveIndex >= 0) { + return true; + } + return false; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInQuickFixIntentionAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java similarity index 60% rename from PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInQuickFixIntentionAction.java rename to PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java index ff3db6287ef..b22f817b71b 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInQuickFixIntentionAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java @@ -13,18 +13,21 @@ import com.intellij.openapi.project.Project; import com.intellij.psi.PsiFile; import com.intellij.util.IncorrectOperationException; +import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesCache; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; import org.jetbrains.annotations.NotNull; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.SCAN_AND_RESOLVE_CVES_PROMPT; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.SCAN_AND_RESOLVE_CVES_WITH_COPILOT_DISPLAY_NAME; + /** - * Intention action to upgrade vulnerable dependencies using GitHub Copilot. - * This action appears in the editor's quick-fix popup (More actions...) for pom.xml files - * when a vulnerable dependency is detected. + * Intention action to fix CVE vulnerabilities in dependencies using GitHub Copilot. + * This action appears in the editor's quick-fix popup (More actions...) for pom.xml files(yellow wavy dependency with CVE issue) + * when a vulnerable dependency with CVE issues is detected. */ -public class UpgradeInQuickFixIntentionAction implements IntentionAction, PriorityAction { - - private static final String DEFAULT_TEXT = "Scan and Resolve CVEs by Copilot"; +public class CveFixIntentionAction implements IntentionAction, PriorityAction { // Cached dependency info from isAvailable() for use in getText() private String cachedGroupId; @@ -32,40 +35,10 @@ public class UpgradeInQuickFixIntentionAction implements IntentionAction, Priori @Override public @IntentionName @NotNull String getText() { - // Return dynamic text based on cached dependency info - if (cachedGroupId != null) { - // Use a friendly display name for known dependencies - String displayName = getDisplayName(cachedGroupId, cachedArtifactId); - return displayName; + if (!AppModPluginInstaller.isAppModPluginInstalled()) { + return SCAN_AND_RESOLVE_CVES_WITH_COPILOT_DISPLAY_NAME + AppModPluginInstaller.TO_INSTALL_APP_MODE_PLUGIN; } - return DEFAULT_TEXT; - } - - /** - * Gets a friendly display name for a dependency. - */ - private String getDisplayName(String groupId, String artifactId) { - return "Scan and Resolve CVEs by Copilot"; -// if (groupId == null) { -// return "Dependency"; -// } -// -// // Map known groupIds to friendly names -// if (groupId.equals("org.springframework.boot")) { -// return "Upgrade Spring Boot with Copilot"; -// } else if (groupId.equals("org.springframework.security")) { -// return "Upgrade Spring Security with Copilot"; -// } else if (groupId.equals("org.springframework")) { -// return "Spring Framework"; -// } else if (groupId.startsWith("org.springframework")) { -// return "Spring " + (artifactId != null ? artifactId : "dependency"); -// } -// -// // For other dependencies, use groupId:artifactId or just groupId -// if (artifactId != null) { -// return groupId + ":" + artifactId; -// } -// return groupId; + return SCAN_AND_RESOLVE_CVES_WITH_COPILOT_DISPLAY_NAME; } @Override @@ -103,7 +76,11 @@ public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file cachedArtifactId = extractXmlValue(dependencyBlock, "artifactId"); // Only show if we have valid dependency info (not for parent/plugin sections) - return cachedGroupId != null && cachedArtifactId != null; + if(cachedGroupId != null && cachedArtifactId != null) { + //if the artifact is in the cached cve issues, show the intention + final var issue = JavaUpgradeIssuesCache.getInstance(project).findCveIssue(cachedGroupId + ":" + cachedArtifactId); + return issue != null; + } } } catch (Exception e) { // Ignore and return false @@ -127,38 +104,7 @@ public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws * Builds a prompt based on the current editor context. */ private String buildPromptFromContext(@NotNull Editor editor, @NotNull PsiFile file) { -// try { -// final int offset = editor.getCaretModel().getOffset(); -// final String documentText = editor.getDocument().getText(); -// -// // Find the dependency block around the cursor -// final int dependencyStart = findDependencyStart(documentText, offset); -// final int dependencyEnd = findDependencyEnd(documentText, offset); -// -// if (dependencyStart >= 0 && dependencyEnd > dependencyStart) { -// final String dependencyBlock = documentText.substring(dependencyStart, dependencyEnd); -// -// // Extract groupId and artifactId -// final String groupId = extractXmlValue(dependencyBlock, "groupId"); -// final String artifactId = extractXmlValue(dependencyBlock, "artifactId"); -// final String version = extractXmlValue(dependencyBlock, "version"); -// -// if (groupId != null && artifactId != null) { -// final StringBuilder prompt = new StringBuilder(); -// prompt.append("Fix security vulnerabilities in "); -// prompt.append(groupId).append(":").append(artifactId); -// if (version != null) { -// prompt.append(":").append(version); -// } -// prompt.append(" by using #validate_cves_for_java"); -// return prompt.toString(); -// } -// } -// } catch (Exception e) { -// // Fall back to generic prompt -// } - - return "run CVE scan for this project using java upgrade tools by invoking #validate_cves_for_java"; + return SCAN_AND_RESOLVE_CVES_PROMPT; } /** diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeProjectAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java similarity index 82% rename from PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeProjectAction.java rename to PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java index 890848e1d94..c398b14252f 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeProjectAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java @@ -15,19 +15,20 @@ import org.jetbrains.annotations.NotNull; +import static com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller.TO_INSTALL_APP_MODE_PLUGIN; +import static com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller.isAppModPluginInstalled; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.UPGRADE_JAVA_AND_FRAMEWORK_PROMPT; + /** - * Action to upgrade a Java project using GitHub Copilot. + * Context menu action to upgrade a Java project using GitHub Copilot. * This action appears in the GitHub Copilot submenu when right-clicking on: * - Project root folder * - pom.xml (Maven projects) * - build.gradle or build.gradle.kts (Gradle projects) */ -public class UpgradeProjectAction extends AnAction { - - private static final String UPGRADE_JAVA_PROMPT = "Upgrade java runtime and java framework dependencies of this project to the latest LTS version using java upgrade tools by invoking #generate_upgrade_plan"; - +public class JavaUpgradeContextMenuAction extends AnAction { // text, description, and icon are defined in azure-intellij-plugin-appmod.xml - public UpgradeProjectAction() { + public JavaUpgradeContextMenuAction() { super(); } @@ -49,7 +50,9 @@ public void update(@NotNull AnActionEvent e) { isMavenBuildFile(file) || isGradleBuildFile(file); } - + if (!isAppModPluginInstalled()) { + e.getPresentation().setText(e.getPresentation().getText() + TO_INSTALL_APP_MODE_PLUGIN); + } e.getPresentation().setEnabledAndVisible(visible); } @@ -71,7 +74,7 @@ public void actionPerformed(@NotNull AnActionEvent e) { * Builds the upgrade prompt based on the selected file. */ private String buildUpgradePrompt(Project project, VirtualFile file) { - return UPGRADE_JAVA_PROMPT; + return UPGRADE_JAVA_AND_FRAMEWORK_PROMPT; } /** diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java new file mode 100644 index 00000000000..5841a552c8b --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action; + +import com.intellij.codeInspection.LocalQuickFix; +import com.intellij.codeInspection.ProblemDescriptor; +import com.intellij.openapi.project.Project; +import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; + +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService.PACKAGE_ID_JDK; + +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; + +/** + * Quick fix for Java/Spring version upgrade issues detected in pom.xml files. + * This action is triggered from Java upgrade issue inspections and opens Copilot + * to assist with the upgrade process. + */ +public class JavaUpgradeQuickFix implements LocalQuickFix { + private final JavaUpgradeIssue issue; + + public JavaUpgradeQuickFix(@NotNull JavaUpgradeIssue issue) { + this.issue = issue; + } + + @Nls(capitalization = Nls.Capitalization.Sentence) + @NotNull + @Override + public String getFamilyName() { + return "Azure Toolkit"; + } + + @Nls(capitalization = Nls.Capitalization.Sentence) + @NotNull + @Override + public String getName() { + String name = "Upgrade " + issue.getPackageDisplayName() + " with Copilot"; + if (!AppModPluginInstaller.isAppModPluginInstalled()) { + return name + AppModPluginInstaller.TO_INSTALL_APP_MODE_PLUGIN; + } + return name; + } + + @Override + public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { + String prompt = buildPromptForIssue(issue); + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + } + + private String buildPromptForIssue(@NotNull JavaUpgradeIssue issue) { + String packageId = issue.getPackageId(); + + // JDK upgrade + if (PACKAGE_ID_JDK.equals(packageId)) { + return String.format( + "Upgrade Java runtime from version %s to Java %s (LTS) using java upgrade tools by invoking #generate_upgrade_plan", + issue.getCurrentVersion(), issue.getSuggestedVersion() + ); + } + + // Framework upgrade (Spring Boot, Spring Framework, etc.) + return String.format( + "Upgrade %s from version %s to %s using java upgrade tools by invoking #generate_upgrade_plan", + issue.getPackageDisplayName(), issue.getCurrentVersion(), issue.getSuggestedVersion() + ); + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java index fbb892636d8..1b8ca3fbfc6 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java @@ -10,7 +10,6 @@ import com.intellij.openapi.actionSystem.DefaultActionGroup; import com.intellij.openapi.actionSystem.Presentation; import com.intellij.openapi.actionSystem.Separator; -import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.openapi.startup.ProjectActivity; import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; @@ -26,8 +25,7 @@ */ public class UpgradeActionRegistrar implements ProjectActivity { - private static final Logger LOG = Logger.getInstance(UpgradeActionRegistrar.class); - private static final String UPGRADE_ACTION_ID = "AzureToolkit.UpgradeProject"; + private static final String UPGRADE_ACTION_ID = "AzureToolkit.JavaUpgradeContextMenu"; private static final String PROJECT_VIEW_POPUP_MENU = "ProjectViewPopupMenu"; @Nullable @@ -45,8 +43,6 @@ private void discoverAndRegisterAction() { ActionManager actionManager = ActionManager.getInstance(); - LOG.info("=== Searching for GitHub Copilot submenu in ProjectViewPopupMenu ==="); - // Get the ProjectViewPopupMenu group AnAction projectViewPopup = actionManager.getAction(PROJECT_VIEW_POPUP_MENU); if (projectViewPopup instanceof DefaultActionGroup) { @@ -57,14 +53,8 @@ private void discoverAndRegisterAction() { if (copilotGroup != null) { tryAddToGroup(actionManager, copilotGroup, "GitHub Copilot submenu"); - } else { - LOG.info("GitHub Copilot submenu not found in ProjectViewPopupMenu"); } - } else { - LOG.warn("ProjectViewPopupMenu not found or not a DefaultActionGroup"); } - - LOG.info("=== End Copilot Action Discovery ==="); } /** @@ -81,7 +71,6 @@ private DefaultActionGroup findCopilotSubmenu(DefaultActionGroup parentGroup, Ac // Match exactly "GitHub Copilot" to avoid false positives if ("GitHub Copilot".equals(text)) { - LOG.info("Found Copilot submenu by exact text match: " + text + ", id=" + actionId); return childGroup; } } @@ -92,7 +81,6 @@ private DefaultActionGroup findCopilotSubmenu(DefaultActionGroup parentGroup, Ac private void tryAddToGroup(ActionManager actionManager, DefaultActionGroup group, String groupId) { AnAction upgradeAction = actionManager.getAction(UPGRADE_ACTION_ID); if (upgradeAction == null) { - LOG.warn("Upgrade action not found: " + UPGRADE_ACTION_ID); return; } @@ -101,9 +89,6 @@ private void tryAddToGroup(ActionManager actionManager, DefaultActionGroup group // Add a separator before the upgrade action to visually group it group.add(Separator.create()); group.add(upgradeAction); - LOG.info("Successfully added upgrade action to group: " + groupId); - } else { - LOG.info("Upgrade action already exists in group: " + groupId); } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInProblemsViewAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInProblemsViewAction.java deleted file mode 100644 index 6746e560406..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeInProblemsViewAction.java +++ /dev/null @@ -1,602 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - */ - -package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action; - -import com.intellij.analysis.problemsView.Problem; -import com.intellij.openapi.actionSystem.ActionUpdateThread; -import com.intellij.openapi.actionSystem.AnAction; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.actionSystem.CommonDataKeys; -import com.intellij.openapi.actionSystem.PlatformDataKeys; -import com.intellij.openapi.project.DumbAware; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; -import com.intellij.psi.PsiManager; -import com.intellij.psi.xml.XmlFile; -import com.intellij.psi.xml.XmlTag; -import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; -import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesCache; -import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; - -import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService.*; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - - -/** - * Action to fix vulnerable dependencies by opening GitHub Copilot chat with an upgrade prompt. - * This action appears in the Problems View context menu for vulnerable dependency issues. - */ -public class UpgradeInProblemsViewAction extends AnAction implements DumbAware { - - private static final String CVE_MARKER = "CVE-"; - - // Data key for problems in the Problems View - private static final String PROBLEMS_VIEW_PROBLEM_KEY = "Problem"; - - public UpgradeInProblemsViewAction() { - super(); - } - - @Override - public void actionPerformed(@NotNull AnActionEvent e) { - final Project project = e.getData(CommonDataKeys.PROJECT); - if (project == null || project.isDisposed()) { - return; - } - - // Try to get issue from cache using PsiElement context - final JavaUpgradeIssue cachedIssue = findIssueFromContext(e, project); - if (cachedIssue != null) { - final String prompt = buildPromptFromIssue(cachedIssue); - JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); - return; - } - - // Fallback: Get the problem description from the context - final String problemDescription = extractProblemDescription(e); - - if (problemDescription != null && !problemDescription.isEmpty()) { - // Extract dependency info and CVE from the problem description - final String prompt = buildUpgradePrompt(problemDescription); - JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); - } else { - // Fallback: generic CVE fix prompt - JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt( - project, - "run CVE scan for this project using java upgrade tools by invoking #validate_cves_for_java" - ); - } - } - - /** - * Tries to find the JavaUpgradeIssue from the context by examining the PsiElement. - */ - @Nullable - private JavaUpgradeIssue findIssueFromContext(@NotNull AnActionEvent e, @NotNull Project project) { - final JavaUpgradeIssuesCache cache = JavaUpgradeIssuesCache.getInstance(project); - if (!cache.isInitialized()) { - return null; - } - - // Try to get PsiElement directly - PsiElement element = e.getData(CommonDataKeys.PSI_ELEMENT); - - // If no direct element, try to get from file + offset - if (element == null) { - element = findElementFromProblem(e, project); - } - - if (element == null) { - return null; - } - - // Navigate to find dependency/parent context - return findIssueFromElement(element, cache); - } - - /** - * Finds the PsiElement from a Problem in the Problems View. - * Uses reflection for cross-version compatibility as Problem API varies. - */ - @Nullable - private PsiElement findElementFromProblem(@NotNull AnActionEvent e, @NotNull Project project) { - try { - final Object problemData = e.getDataContext().getData(PROBLEMS_VIEW_PROBLEM_KEY); - if (problemData instanceof Problem) { - final Problem problem = (Problem) problemData; - - // Use reflection to get file - API varies across IntelliJ versions - VirtualFile file = null; - try { - java.lang.reflect.Method getFileMethod = problem.getClass().getMethod("getFile"); - Object fileObj = getFileMethod.invoke(problem); - if (fileObj instanceof VirtualFile) { - file = (VirtualFile) fileObj; - } - } catch (Exception ignored) { - // getFile method might not exist in this version - System.out.println("error" + ignored.getMessage()); - } - - if (file != null && file.getName().equals("pom.xml")) { - final PsiFile psiFile = PsiManager.getInstance(project).findFile(file); - if (psiFile instanceof XmlFile) { - // Try to get offset from problem and find element - try { - java.lang.reflect.Method getOffsetMethod = problem.getClass().getMethod("getOffset"); - Object offsetObj = getOffsetMethod.invoke(problem); - if (offsetObj instanceof Integer) { - int offset = (Integer) offsetObj; - if (offset >= 0) { - return psiFile.findElementAt(offset); - } - } - } catch (Exception ignored) { - // getOffset method might not exist - System.out.println("error" + ignored.getMessage()); - } - } - } - } - } catch (Exception ignored) { - System.out.println("error" + ignored.getMessage()); - - } - return null; - } - - /** - * Finds the JavaUpgradeIssue based on the XML element context. - */ - @Nullable - private JavaUpgradeIssue findIssueFromElement(@NotNull PsiElement element, @NotNull JavaUpgradeIssuesCache cache) { - // Find the containing XmlTag - XmlTag tag = findParentTag(element); - if (tag == null) { - return null; - } - - // Check if this is a Java version property - if (isJavaVersionContext(tag)) { - return cache.getJdkIssue(); - } - - // Check if this is a dependency or parent version - final String groupId = extractGroupId(tag); - if (groupId != null) { - if (groupId.equals(GROUP_ID_SPRING_BOOT)) { - return cache.findDependencyIssue(GROUP_ID_SPRING_BOOT); - } else if (groupId.equals(GROUP_ID_SPRING_SECURITY)) { - return cache.findDependencyIssue(GROUP_ID_SPRING_SECURITY); - } else if (groupId.equals(GROUP_ID_SPRING_FRAMEWORK)) { - return cache.findDependencyIssue(GROUP_ID_SPRING_FRAMEWORK + ":"); - } - } - - return null; - } - - /** - * Finds the parent XmlTag of an element. - */ - @Nullable - private XmlTag findParentTag(@NotNull PsiElement element) { - PsiElement current = element; - while (current != null) { - if (current instanceof XmlTag) { - return (XmlTag) current; - } - current = current.getParent(); - } - return null; - } - - /** - * Checks if the tag is in a Java version context. - */ - private boolean isJavaVersionContext(@NotNull XmlTag tag) { - String tagName = tag.getName(); - - // Check for properties like java.version, maven.compiler.source, etc. - if ("java.version".equals(tagName) || - "maven.compiler.source".equals(tagName) || - "maven.compiler.target".equals(tagName) || - "maven.compiler.release".equals(tagName)) { - XmlTag parent = tag.getParentTag(); - return parent != null && "properties".equals(parent.getName()); - } - - // Check for maven-compiler-plugin source/target/release - if ("source".equals(tagName) || "target".equals(tagName) || "release".equals(tagName)) { - XmlTag current = tag.getParentTag(); - while (current != null) { - if ("plugin".equals(current.getName())) { - XmlTag artifactIdTag = current.findFirstSubTag("artifactId"); - if (artifactIdTag != null && ARTIFACT_ID_MAVEN_COMPILER_PLUGIN.equals(artifactIdTag.getValue().getText())) { - return true; - } - } - current = current.getParentTag(); - } - } - - return false; - } - - /** - * Extracts the groupId from a dependency or parent tag context. - */ - @Nullable - private String extractGroupId(@NotNull XmlTag tag) { - // If we're on a version tag, look at parent (dependency or parent) - XmlTag container = tag; - if ("version".equals(tag.getName())) { - container = tag.getParentTag(); - } - - if (container == null) { - return null; - } - - // Check if it's a dependency or parent tag - String containerName = container.getName(); - if ("dependency".equals(containerName) || "parent".equals(containerName)) { - XmlTag groupIdTag = container.findFirstSubTag("groupId"); - if (groupIdTag != null) { - return groupIdTag.getValue().getText(); - } - } - - return null; - } - - /** - * Builds a prompt from a cached JavaUpgradeIssue. - */ - @NotNull - private String buildPromptFromIssue(@NotNull JavaUpgradeIssue issue) { - String packageId = issue.getPackageId(); - - // JDK upgrade - if (PACKAGE_ID_JDK.equals(packageId)) { - return String.format( - "Upgrade Java runtime from version %s to Java %s (LTS) using java upgrade tools by invoking #generate_upgrade_plan", - issue.getCurrentVersion(), issue.getSuggestedVersion() - ); - } - - // Framework upgrade (Spring Boot, Spring Framework, etc.) - return String.format( - "Upgrade %s from version %s to %s using java upgrade tools by invoking #generate_upgrade_plan", - issue.getPackageDisplayName(), issue.getCurrentVersion(), issue.getSuggestedVersion() - ); - } - - @Override - public void update(@NotNull AnActionEvent e) { - final Project project = e.getData(CommonDataKeys.PROJECT); - if (project == null || project.isDisposed()) { - e.getPresentation().setEnabledAndVisible(false); - return; - } - - // Check if we're in the Problems View context with a vulnerability - final String description = extractProblemDescription(e); - if (description == null) { - e.getPresentation().setEnabledAndVisible(false); - return; - } - - // Also check if the file is pom.xml or build.gradle (common for dependency issues) - final VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE); - final boolean isBuildFile = file != null && - (file.getName().equals("pom.xml") || file.getName().endsWith(".gradle") || file.getName().endsWith(".gradle.kts")); - - final boolean isVulnerability = isVulnerabilityDescription(description); - - if (!isBuildFile && !isVulnerability) { - e.getPresentation().setEnabledAndVisible(false); - return; - } - - // Set dynamic text based on issue type detected from description - String actionText = getDynamicActionText(description, project); - e.getPresentation().setText(actionText); - e.getPresentation().setEnabledAndVisible(true); - } - - /** - * Gets dynamic action text based on the problem description. - */ - private String getDynamicActionText(@NotNull String description, @NotNull Project project) { - // Try to detect JDK issue - if (description.toLowerCase().contains("jdk") || - description.toLowerCase().contains("java runtime") || - description.toLowerCase().contains("java version")) { - return "Upgrade JDK with Copilot"; - } - - // Try to detect Spring Boot - if (description.toLowerCase().contains("spring boot")) { - return "Upgrade Spring Boot with Copilot"; - } - - // Try to detect Spring Framework - if (description.toLowerCase().contains("spring framework")) { - return "Upgrade Spring Framework with Copilot"; - } - - // Try to detect Spring Security - if (description.toLowerCase().contains("spring security")) { - return "Upgrade Spring Security with Copilot"; - } - - // Try to get from cache if available - final JavaUpgradeIssuesCache cache = JavaUpgradeIssuesCache.getInstance(project); - if (cache.isInitialized()) { - // Check for JDK issue - if (cache.getJdkIssue() != null && description.contains(cache.getJdkIssue().getMessage())) { - return "Upgrade JDK with Copilot"; - } - // Check for Spring Boot - JavaUpgradeIssue springBootIssue = cache.findDependencyIssue(GROUP_ID_SPRING_BOOT); - if (springBootIssue != null && description.contains(springBootIssue.getMessage())) { - return "Upgrade Spring Boot with Copilot"; - } - } - - // Default text - return "Scan and Resolve CVEs with Copilot"; - } - - @Override - public @NotNull ActionUpdateThread getActionUpdateThread() { - return ActionUpdateThread.BGT; - } - - /** - * Checks if the description indicates a vulnerability. - */ - private boolean isVulnerabilityDescription(@NotNull String description) { - final String lowerDescription = description.toLowerCase(); - return lowerDescription.contains("vulnerable") || - lowerDescription.contains("cve-") || - lowerDescription.contains("security") || - lowerDescription.contains("vulnerability"); - } - - /** - * Extracts the problem description from the action event context. - */ - @Nullable - private String extractProblemDescription(@NotNull AnActionEvent e) { - // Try multiple approaches to get the problem description - - // Approach 1: Try to get Problem object directly from Problems View - try { - final Object problemData = e.getDataContext().getData(PROBLEMS_VIEW_PROBLEM_KEY); - if (problemData instanceof Problem) { - final Problem problem = (Problem) problemData; - final String text = problem.getText(); - if (text != null && !text.isEmpty()) { - return text; - } - } - } catch (Exception ignored) { - // Problem class might not be available - } - - // Approach 2: Try "problem.description" data key - try { - @SuppressWarnings("deprecation") - final Object data = e.getDataContext().getData("problem.description"); - if (data instanceof String && !((String) data).isEmpty()) { - return (String) data; - } - } catch (Exception ignored) { - } - - // Approach 3: Try to get selected items from the tree - try { - final Object[] selectedItems = e.getData(PlatformDataKeys.SELECTED_ITEMS); - if (selectedItems != null && selectedItems.length > 0) { - // Concatenate all selected items' string representations - final StringBuilder sb = new StringBuilder(); - for (Object item : selectedItems) { - if (item != null) { - sb.append(item.toString()).append(" "); - } - } - final String result = sb.toString().trim(); - if (!result.isEmpty()) { - return result; - } - } - } catch (Exception ignored) { - } - - // Approach 4: Try SELECTED_ITEM - try { - final Object selectedItem = e.getData(PlatformDataKeys.SELECTED_ITEM); - if (selectedItem != null) { - final String text = selectedItem.toString(); - if (!text.isEmpty()) { - return text; - } - } - } catch (Exception ignored) { - } - - // Approach 5: Try getting from context component - try { - final java.awt.Component component = e.getData(PlatformDataKeys.CONTEXT_COMPONENT); - if (component instanceof javax.swing.JTree) { - final javax.swing.JTree tree = (javax.swing.JTree) component; - final javax.swing.tree.TreePath[] paths = tree.getSelectionPaths(); - if (paths != null && paths.length > 0) { - final StringBuilder sb = new StringBuilder(); - for (javax.swing.tree.TreePath path : paths) { - final Object lastComponent = path.getLastPathComponent(); - if (lastComponent != null) { - sb.append(lastComponent.toString()).append(" "); - } - } - final String result = sb.toString().trim(); - if (!result.isEmpty()) { - return result; - } - } - } - } catch (Exception ignored) { - } - - // Approach 6: Try getting selected text from editor - try { - final com.intellij.openapi.editor.Editor editor = e.getData(CommonDataKeys.EDITOR); - if (editor != null && editor.getSelectionModel().hasSelection()) { - final String selectedText = editor.getSelectionModel().getSelectedText(); - if (selectedText != null && !selectedText.isEmpty()) { - return selectedText; - } - } - } catch (Exception ignored) { - } - - return null; - } - - /** - * Builds an upgrade prompt based on the problem description. - */ - private String buildUpgradePrompt(@NotNull String problemDescription) { - - // Extract dependency coordinates if present - final String dependency = extractDependencyCoordinates(problemDescription); - - if (problemDescription != null) { - // Try to detect JDK issue - if (problemDescription.toLowerCase().contains("jdk") || - problemDescription.toLowerCase().contains("java runtime") || - problemDescription.toLowerCase().contains("java version")) { - return "upgrade java runtime to Java " + MATURE_JAVA_LTS_VERSION + " (LTS) using java upgrade tools by invoking #generate_upgrade_plan"; - } - - // Try to detect Spring Boot - if (problemDescription.toLowerCase().contains("spring boot") || problemDescription.toLowerCase().contains("spring framework") || problemDescription.toLowerCase().contains("spring security")) { - return "upgrade java framework dependencies of this project to latest LTS version using java upgrade tools by invoking #generate_upgrade_plan"; - } - - } - // Default text - return "run CVE scan for this project using java upgrade tools by invoking #validate_cves_for_java"; - } - - /** - * Extracts CVE ID from the problem description. - */ - private String extractCVEId(@NotNull String description) { - // Pattern: CVE-YYYY-NNNNN - final int cveIndex = description.toUpperCase().indexOf(CVE_MARKER); - if (cveIndex >= 0) { - final int endIndex = findCVEEndIndex(description, cveIndex); - if (endIndex > cveIndex) { - return description.substring(cveIndex, endIndex); - } - } - return null; - } - - /** - * Finds the end index of a CVE ID in the description. - */ - private int findCVEEndIndex(@NotNull String description, int startIndex) { - int index = startIndex + CVE_MARKER.length(); - // Skip year (4 digits) - while (index < description.length() && Character.isDigit(description.charAt(index))) { - index++; - } - // Skip separator - if (index < description.length() && description.charAt(index) == '-') { - index++; - } - // Skip ID number - while (index < description.length() && Character.isDigit(description.charAt(index))) { - index++; - } - return index; - } - - /** - * Extracts Maven dependency coordinates from the problem description. - * Looks for patterns like groupId:artifactId:version - */ - private String extractDependencyCoordinates(@NotNull String description) { - // Look for Maven coordinate pattern: groupId:artifactId:version - // Common patterns in vulnerability reports - final String[] patterns = { - "maven:", // maven:groupId:artifactId:version - "dependency " // dependency groupId:artifactId - }; - - for (String pattern : patterns) { - final int index = description.toLowerCase().indexOf(pattern); - if (index >= 0) { - return extractCoordinatesAfterPattern(description, index + pattern.length()); - } - } - - // Try to find standalone coordinate pattern (e.g., org.example:artifact:1.0.0) - return findStandaloneCoordinates(description); - } - - /** - * Extracts coordinates after a known pattern. - */ - private String extractCoordinatesAfterPattern(@NotNull String description, int startIndex) { - final StringBuilder coords = new StringBuilder(); - int colonCount = 0; - - for (int i = startIndex; i < description.length(); i++) { - final char c = description.charAt(i); - if (Character.isLetterOrDigit(c) || c == '.' || c == '-' || c == '_') { - coords.append(c); - } else if (c == ':' && colonCount < 2) { - coords.append(c); - colonCount++; - } else if (!coords.isEmpty()) { - break; - } - } - - final String result = coords.toString(); - return result.contains(":") ? result : null; - } - - /** - * Finds standalone Maven coordinates in the description. - */ - private String findStandaloneCoordinates(@NotNull String description) { - // Simple heuristic: look for pattern like "org.xxx:xxx" or "com.xxx:xxx" - final String[] prefixes = {"org.", "com.", "io.", "net."}; - - for (String prefix : prefixes) { - int index = description.indexOf(prefix); - while (index >= 0) { - final String coords = extractCoordinatesAfterPattern(description, index); - if (coords != null && coords.contains(":")) { - return coords; - } - index = description.indexOf(prefix, index + 1); - } - } - - return null; - } -} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java index 41222922e10..999dd32d36c 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java @@ -6,26 +6,24 @@ package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.inspection; import com.intellij.codeInspection.LocalInspectionTool; -import com.intellij.codeInspection.LocalQuickFix; -import com.intellij.codeInspection.ProblemDescriptor; import com.intellij.codeInspection.ProblemHighlightType; import com.intellij.codeInspection.ProblemsHolder; -import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.psi.PsiElementVisitor; import com.intellij.psi.PsiFile; import com.intellij.psi.XmlElementVisitor; import com.intellij.psi.xml.XmlFile; import com.intellij.psi.xml.XmlTag; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action.JavaUpgradeQuickFix; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesCache; -import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService.*; -import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; +import java.util.List; + /** * Inspection that displays Java upgrade issues detected by JavaUpgradeDetectionService. * Shows JDK version and framework version issues in pom.xml files with wavy underlines. @@ -35,8 +33,6 @@ */ public class JavaUpgradeIssuesInspection extends LocalInspectionTool { - private static final Logger LOG = Logger.getInstance(JavaUpgradeIssuesInspection.class); - @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { @@ -57,15 +53,7 @@ public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean is // Get cached issues (computed once at project startup) final JavaUpgradeIssue jdkIssue = cache.getJdkIssue(); - final JavaUpgradeIssue springBootIssue = cache.findDependencyIssue(GROUP_ID_SPRING_BOOT); - final JavaUpgradeIssue springFrameworkIssue = cache.findDependencyIssue(GROUP_ID_SPRING_FRAMEWORK + ":"); - final JavaUpgradeIssue springSecurityIssue = cache.findDependencyIssue(GROUP_ID_SPRING_SECURITY); - - // Debug logging - LOG.info("JavaUpgradeIssuesInspection: Cache initialized=" + cache.isInitialized() + - ", jdkIssue=" + (jdkIssue != null) + - ", springBootIssue=" + (springBootIssue != null ? springBootIssue.getCurrentVersion() : "null") + - ", dependencyIssues=" + cache.getDependencyIssues().size()); + final List dependencyIssues = cache.getDependencyIssues(); return new XmlElementVisitor() { @Override @@ -79,42 +67,52 @@ public void visitXmlTag(@NotNull XmlTag tag) { } } - // Check for Spring Boot parent version - if (springBootIssue != null && isSpringBootParentVersion(tag)) { - registerProblem(holder, tag, springBootIssue); - } - - // Check for Spring Framework dependency version - if (springFrameworkIssue != null && isSpringFrameworkDependencyVersion(tag)) { - registerProblem(holder, tag, springFrameworkIssue); - } - - // Check for Spring Boot dependency version (when not using parent) - if (springBootIssue != null && isSpringBootDependencyVersion(tag)) { - registerProblem(holder, tag, springBootIssue); - } - - // Check for Spring Security dependency version - if (springSecurityIssue != null && isSpringSecurityDependencyVersion(tag)) { - registerProblem(holder, tag, springSecurityIssue); + // Check for dependency/parent version tags and register all matching issues + if ("version".equals(tag.getName())) { + XmlTag parentElement = tag.getParentTag(); + if (parentElement != null) { + String parentTagName = parentElement.getName(); + if ("dependency".equals(parentTagName) || "parent".equals(parentTagName)) { + XmlTag groupIdTag = parentElement.findFirstSubTag("groupId"); + XmlTag artifactIdTag = parentElement.findFirstSubTag("artifactId"); + if (groupIdTag != null && artifactIdTag != null) { + String packageId = groupIdTag.getValue().getText() + ":" + artifactIdTag.getValue().getText(); + // Register all issues that match this package + for (JavaUpgradeIssue issue : dependencyIssues) { + if (matchesPackageId(packageId, issue.getPackageId())) { + registerProblem(holder, tag, issue); + } + } + } + } + } } } }; } private void registerProblem(@NotNull ProblemsHolder holder, @NotNull XmlTag tag, @NotNull JavaUpgradeIssue issue) { - // ProblemHighlightType highlightType = issue.getSeverity() == JavaUpgradeIssue.Severity.CRITICAL - // ? ProblemHighlightType.ERROR - // : ProblemHighlightType.WARNING; - holder.registerProblem( tag, issue.getMessage(), ProblemHighlightType.WARNING, - new UpgradeWithCopilotQuickFix(issue) + new JavaUpgradeQuickFix(issue) ); } + /** + * Checks if the packageId matches the issue's packageId pattern. + * Supports wildcard patterns like "org.springframework.boot:*" to match any artifact in a group. + */ + private boolean matchesPackageId(@NotNull String packageId, @NotNull String issuePackageId) { + if (issuePackageId.endsWith(":*")) { + // Wildcard match: check if packageId starts with the group prefix + String groupPrefix = issuePackageId.substring(0, issuePackageId.length() - 1); // "org.springframework.boot:" + return packageId.startsWith(groupPrefix); + } + return packageId.equals(issuePackageId); + } + /** * Checks if the tag is a Java version property (java.version, maven.compiler.source, maven.compiler.target). */ @@ -154,124 +152,4 @@ private boolean isCompilerPluginVersionTag(@NotNull XmlTag tag) { } return false; } - - /** - * Checks if the tag is the version tag inside a Spring Boot parent declaration. - */ - private boolean isSpringBootParentVersion(@NotNull XmlTag tag) { - if (!"version".equals(tag.getName())) { - return false; - } - - XmlTag parent = tag.getParentTag(); - if (parent == null || !"parent".equals(parent.getName())) { - return false; - } - - XmlTag groupIdTag = parent.findFirstSubTag("groupId"); - XmlTag artifactIdTag = parent.findFirstSubTag("artifactId"); - - return groupIdTag != null && GROUP_ID_SPRING_BOOT.equals(groupIdTag.getValue().getText()) && - artifactIdTag != null && ARTIFACT_ID_SPRING_BOOT_STARTER_PARENT.equals(artifactIdTag.getValue().getText()); - } - - /** - * Checks if the tag is a version tag inside a Spring Boot dependency. - */ - private boolean isSpringBootDependencyVersion(@NotNull XmlTag tag) { - if (!"version".equals(tag.getName())) { - return false; - } - - XmlTag dependency = tag.getParentTag(); - if (dependency == null || !"dependency".equals(dependency.getName())) { - return false; - } - - XmlTag groupIdTag = dependency.findFirstSubTag("groupId"); - return groupIdTag != null && GROUP_ID_SPRING_BOOT.equals(groupIdTag.getValue().getText()); - } - - /** - * Checks if the tag is a version tag inside a Spring Framework dependency. - */ - private boolean isSpringFrameworkDependencyVersion(@NotNull XmlTag tag) { - if (!"version".equals(tag.getName())) { - return false; - } - - XmlTag dependency = tag.getParentTag(); - if (dependency == null || !"dependency".equals(dependency.getName())) { - return false; - } - - XmlTag groupIdTag = dependency.findFirstSubTag("groupId"); - return groupIdTag != null && GROUP_ID_SPRING_FRAMEWORK.equals(groupIdTag.getValue().getText()); - } - - /** - * Checks if the tag is a version tag inside a Spring Security dependency. - */ - private boolean isSpringSecurityDependencyVersion(@NotNull XmlTag tag) { - if (!"version".equals(tag.getName())) { - return false; - } - - XmlTag dependency = tag.getParentTag(); - if (dependency == null || !"dependency".equals(dependency.getName())) { - return false; - } - - XmlTag groupIdTag = dependency.findFirstSubTag("groupId"); - return groupIdTag != null && GROUP_ID_SPRING_SECURITY.equals(groupIdTag.getValue().getText()); - } - - /** - * Quick fix to upgrade using Copilot based on the issue type. - */ - private static class UpgradeWithCopilotQuickFix implements LocalQuickFix { - private final JavaUpgradeIssue issue; - - public UpgradeWithCopilotQuickFix(@NotNull JavaUpgradeIssue issue) { - this.issue = issue; - } - - @Nls(capitalization = Nls.Capitalization.Sentence) - @NotNull - @Override - public String getFamilyName() { - return "Azure Toolkit"; - } - - @Nls(capitalization = Nls.Capitalization.Sentence) - @NotNull - @Override - public String getName() { - return "Upgrade " + issue.getPackageDisplayName() + " with Copilot"; - } - - @Override - public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { - String prompt = buildPromptForIssue(issue); - JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); - } - - private String buildPromptForIssue(@NotNull JavaUpgradeIssue issue) { - String packageId = issue.getPackageId(); - - // JDK upgrade - if (PACKAGE_ID_JDK.equals(packageId)) { - return String.format( - "Upgrade Java runtime from version %s to Java %s (LTS) using java upgrade tools by invoking #generate_upgrade_plan", - issue.getCurrentVersion(), issue.getSuggestedVersion() - ); - } - - // Framework upgrade (Spring Boot, Spring Framework, etc.) - return String.format( - "Upgrade %s from version %s to %s using java upgrade tools by invoking #generate_upgrade_plan", - issue.getPackageDisplayName(), issue.getCurrentVersion(), issue.getSuggestedVersion() - ); - } - } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/CVECheckService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/CVECheckService.java index d6380a38f6c..87fd952036d 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/CVECheckService.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/CVECheckService.java @@ -5,6 +5,7 @@ package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service; +import com.intellij.util.net.JdkProxyProvider; import com.intellij.util.net.ssl.CertificateManager; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; import com.google.gson.Gson; @@ -127,6 +128,9 @@ private CVECheckService() { final HttpClient.Builder builder = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(REQUEST_TIMEOUT_SECONDS)); try { + // Configure proxy using IntelliJ's JdkProxyProvider + // This respects the IDE's proxy settings (Settings → HTTP Proxy) + builder.proxy(JdkProxyProvider.getInstance().getProxySelector()); builder.sslContext(CertificateManager.getInstance().getSslContext()); } catch (Throwable e) { // Failed to get IntelliJ SSL context, using default diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesCache.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesCache.java index 71fabe6fea0..87364af5f19 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesCache.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesCache.java @@ -30,6 +30,7 @@ public final class JavaUpgradeIssuesCache implements Disposable { private final Project project; private final AtomicReference> jdkIssuesCache = new AtomicReference<>(); private final AtomicReference> dependencyIssuesCache = new AtomicReference<>(); + private final AtomicReference> cvesIssuesCache = new AtomicReference<>(); private final AtomicBoolean initialized = new AtomicBoolean(false); public JavaUpgradeIssuesCache(@NotNull Project project) { @@ -59,9 +60,30 @@ public List getDependencyIssues() { } /** - * Finds a specific issue by package ID prefix. + * Gets cached CVE issues. Returns empty list if not yet initialized. + */ + @Nonnull + public List getCveIssues() { + List cached = cvesIssuesCache.get(); + return cached != null ? cached : Collections.emptyList(); + } + + /** + * Finds a specific CVE issue by package ID prefix. */ @Nullable + public JavaUpgradeIssue findCveIssue(@Nonnull String packageIdPrefix) { + return getCveIssues().stream() + .filter(i -> i.getPackageId().startsWith(packageIdPrefix)) + .findFirst() + .orElse(null); + } + /** + * Finds the first issue matching a package ID prefix. + * @deprecated Use {@link #findDependencyIssues(String)} to handle multiple dependencies with the same groupId. + */ + @Deprecated + @Nullable public JavaUpgradeIssue findDependencyIssue(@Nonnull String packageIdPrefix) { return getDependencyIssues().stream() .filter(i -> i.getPackageId().startsWith(packageIdPrefix)) @@ -69,6 +91,17 @@ public JavaUpgradeIssue findDependencyIssue(@Nonnull String packageIdPrefix) { .orElse(null); } + /** + * Finds all issues matching a package ID prefix (groupId). + * This handles the case where multiple dependencies share the same groupId. + */ + @Nonnull + public List findDependencyIssues(@Nonnull String packageIdPrefix) { + return getDependencyIssues().stream() + .filter(i -> i.getPackageId().startsWith(packageIdPrefix)) + .toList(); + } + /** * Gets the first JDK issue if present. */ @@ -99,10 +132,11 @@ public void refresh() { // Scan for issues List jdkIssues = detectionService.getJavaIssues(project); List dependencyIssues = detectionService.getDependencyIssues(project); - + List cveIssues = detectionService.getCVEIssues(project); // Update cache jdkIssuesCache.set(Collections.unmodifiableList(jdkIssues)); dependencyIssuesCache.set(Collections.unmodifiableList(dependencyIssues)); + cvesIssuesCache.set(Collections.unmodifiableList(cveIssues)); initialized.set(true); } @@ -112,6 +146,7 @@ public void refresh() { public void invalidate() { jdkIssuesCache.set(null); dependencyIssuesCache.set(null); + cvesIssuesCache.set(null); initialized.set(false); } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java index 895b08d0362..9389d20d734 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java @@ -188,8 +188,6 @@ public String getEolDateForVersion(@Nonnull String version) { private static final String JDK_LEARN_MORE_URL = "https://learn.microsoft.com/azure/developer/java/fundamentals/java-support-on-azure"; - private static final Logger LOG = Logger.getInstance(JavaUpgradeIssuesDetectionService.class); - private static JavaUpgradeIssuesDetectionService instance; private JavaUpgradeIssuesDetectionService() { @@ -202,33 +200,6 @@ public static synchronized JavaUpgradeIssuesDetectionService getInstance() { return instance; } - /** - * Analyzes the given project and returns a list of detected outdated version issues. - * - * @param project The IntelliJ project to analyze - * @return List of detected outdated version issues - */ - @Nonnull - public List analyzeProject(@Nonnull Project project) { - final List issues = new ArrayList<>(); - - try { - // Get JDK issues - issues.addAll(getJavaIssues(project)); - - // Get dependency issues - issues.addAll(getDependencyIssues(project)); - - // Get CVE issues - issues.addAll(getCVEIssues(project)); - - } catch (Exception e) { - // Error analyzing project for upgrade issues - } - - return issues; - } - /** * Gets JDK/JRE version issues. * Aligned with getJavaIssues() from assessmentManager.ts. @@ -434,17 +405,12 @@ private String getParentVersion(@Nonnull MavenProject mavenProject, @Nonnull String artifactId) { try { final var parentId = mavenProject.getParentId(); - LOG.info("getParentVersion: parentId=" + (parentId != null ? - parentId.getGroupId() + ":" + parentId.getArtifactId() + ":" + parentId.getVersion() : "null") + - ", looking for " + groupId + ":" + artifactId); if (parentId != null && groupId.equals(parentId.getGroupId()) && artifactId.equals(parentId.getArtifactId())) { - LOG.info("getParentVersion: Found matching parent version: " + parentId.getVersion()); return parentId.getVersion(); } } catch (Exception e) { - LOG.warn("Error getting parent version", e); } return null; } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java index 50a7449bfff..af1bc385e28 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java @@ -21,6 +21,7 @@ import static com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller.installPlugin; import static com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller.isAppModPluginInstalled; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.*; import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService.*; import java.lang.reflect.Method; @@ -44,8 +45,7 @@ public class JavaVersionNotificationService { private static final String DEFERRED_UNTIL_KEY = "azure.toolkit.java.version.deferred_until"; private static final long DEFER_INTERVAL_MS = 10 * 24 * 60 * 60 * 1000L; // 10 days in milliseconds private static final String DEFAULT_MODEL_NAME = "Claude Sonnet 4.5"; - // GitHub Copilot app modernization plugin ID - private static final String COPILOT_APPMOD_PLUGIN_ID = "com.github.copilot.appmod"; + // GitHub Copilot plugin ID private static final String COPILOT_PLUGIN_ID = "com.github.copilot"; @@ -94,38 +94,35 @@ public void showNotifications(@Nonnull Project project, @Nonnull List clazz, String methodName) { */ private String buildUpgradePrompt(@Nonnull JavaUpgradeIssue issue) { if (issue.getUpgradeReason() == JavaUpgradeIssue.UpgradeReason.JRE_TOO_OLD) { - return String.format("upgrade java runtime to Java %s (LTS) using java upgrade tools by invoking #generate_upgrade_plan", MATURE_JAVA_LTS_VERSION); - } else if (issue.getPackageId().startsWith(GROUP_ID_SPRING_BOOT + ":")) { - return String.format("upgrade java framework dependencies of this project to latest LTS version using java upgrade tools by invoking #generate_upgrade_plan"); + return UPGRADE_JAVA_VERSION_PROMPT; + } else if (issue.getUpgradeReason() == JavaUpgradeIssue.UpgradeReason.CVE){ + return SCAN_AND_RESOLVE_CVES_PROMPT; + }else if (issue.getUpgradeReason() == JavaUpgradeIssue.UpgradeReason.DEPRECATED || issue.getUpgradeReason() == JavaUpgradeIssue.UpgradeReason.END_OF_LIFE) { + return UPGRADE_JAVA_FRAMEWORK_PROMPT; } - return "upgrade java framework dependencies of this project to latest LTS version using java upgrade tools by invoking #generate_upgrade_plan"; - } - - /** - * Installs the GitHub Copilot app modernization plugin and then opens Copilot chat. - * @param project The project context - * @param issue The upgrade issue to address - */ - private void installCopilotAppModPluginAndUpgrade(@Nonnull Project project, @Nonnull JavaUpgradeIssue issue) { -// final Set pluginIds = new HashSet<>(); -// pluginIds.add(COPILOT_APPMOD_PLUGIN_ID); -// -// AzureTaskManager.getInstance().runLater(() -> { -// PluginsAdvertiser.installAndEnablePlugins(pluginIds, () -> { -// PluginInstaller.addStateListener(new PluginStateListener() { -// @Override -// public void install(@NotNull IdeaPluginDescriptor descriptor) { -// if (COPILOT_APPMOD_PLUGIN_ID.equals(descriptor.getPluginId().getIdString())) { -// // Plugin installed successfully, now open Copilot chat -// ApplicationManager.getApplication().invokeLater(() -> { -// openCopilotChatWithUpgradePrompt(project, issue); -// }); -// } -// } -// -// @Override -// public void uninstall(@NotNull IdeaPluginDescriptor descriptor) { -// // Not needed -// } -// }); -// }); -// }); + return UPGRADE_JAVA_AND_FRAMEWORK_PROMPT; } /** diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml index 5cbc896ed4b..bf394c89e59 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml @@ -9,7 +9,7 @@ XML - com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action.UpgradeInQuickFixIntentionAction + com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action.CveFixIntentionAction Azure Toolkit @@ -34,17 +34,16 @@ id="Actions.MigrateToAzure" text="Migrate to Azure" description="Migrate application to Azure" icon="/icons/appmod.svg"/> - + From bf4c5ae7c8232082db62adb9b696acbc42c79fc5 Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Tue, 27 Jan 2026 11:09:22 +0800 Subject: [PATCH 19/43] revert .idea file --- .../azure-toolkit-for-intellij/.idea/gradle.xml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/gradle.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/gradle.xml index d604fd81745..25aace38038 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/gradle.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/.idea/gradle.xml @@ -14,7 +14,6 @@ - - \ No newline at end of file From 3ee458a441b2211dca659a25880132de74fc142b Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Tue, 27 Jan 2026 14:12:21 +0800 Subject: [PATCH 20/43] remove InstallPluginDialog and add built-in dialog --- .../appmod/common/AppModPluginInstaller.java | 9 +- .../appmod/common/InstallPluginDialog.java | 82 ------------------- 2 files changed, 4 insertions(+), 87 deletions(-) delete mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/InstallPluginDialog.java diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java index 198890e7137..3db9cc9aea0 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java @@ -9,6 +9,7 @@ import com.intellij.ide.plugins.PluginManagerCore; import com.intellij.openapi.extensions.PluginId; import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.Messages; import com.intellij.openapi.updateSettings.impl.pluginsAdvertisement.PluginsAdvertiser; import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager; import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; @@ -77,7 +78,6 @@ public static boolean isRunningInDevMode() { /** * Shows a confirmation dialog for plugin installation. - * Uses a modal dialog similar to AzdNode's ConfirmAndRunDialog. * * @param project The current project * @param onConfirm Callback to execute when user confirms installation @@ -93,10 +93,9 @@ public static void showInstallConfirmation(@Nonnull Project project, @Nonnull Ru ? "Install this plugin to automate migrating your apps to Azure with Copilot." : "To migrate to Azure, you'll need two plugins: GitHub Copilot and app modernization."; - new InstallPluginDialog(project, title) - .setLabel(message) - .setOnOkAction(onConfirm) - .show(); + if (Messages.showOkCancelDialog(project, message, title, "Install", "Cancel", Messages.getQuestionIcon()) == Messages.OK) { + onConfirm.run(); + } } /** diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/InstallPluginDialog.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/InstallPluginDialog.java deleted file mode 100644 index 0989aa160fa..00000000000 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/InstallPluginDialog.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - */ - -package com.microsoft.azure.toolkit.intellij.appmod.common; - -import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.DialogWrapper; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import javax.swing.*; -import java.awt.*; -import java.util.Objects; - -/** - * Dialog for confirming plugin installation. - * Similar to ConfirmAndRunDialog used in AzdNode. - */ -public class InstallPluginDialog extends DialogWrapper { - - private final Project project; - private String label; - private Runnable onOkAction; - - public InstallPluginDialog(Project project, String title) { - super(project, true); - this.project = project; - setSize(400, 150); - setTitle(Objects.requireNonNull(title, "Title must not be null")); - setOKButtonText("Install"); - } - - public InstallPluginDialog setLabel(String label) { - this.label = Objects.requireNonNull(label, "Label must not be null"); - return this; - } - - public InstallPluginDialog setOnOkAction(Runnable onOkAction) { - this.onOkAction = onOkAction; - return this; - } - - @Override - public void show() { - init(); - super.show(); - } - - @Override - protected @Nullable JComponent createCenterPanel() { - JPanel panel = new JPanel(new BorderLayout()); - panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - - // Support HTML for multi-line labels - JLabel labelComponent; - if (label != null && label.contains("\n")) { - // Convert newlines to HTML breaks - String htmlLabel = "" + label.replace("\n", "
") + ""; - labelComponent = new JLabel(htmlLabel); - } else { - labelComponent = new JLabel(label); - } - labelComponent.setHorizontalAlignment(SwingConstants.LEFT); - panel.add(labelComponent, BorderLayout.CENTER); - return panel; - } - - @Override - protected Action @NotNull [] createActions() { - return new Action[]{getOKAction(), getCancelAction()}; - } - - @Override - protected void doOKAction() { - super.doOKAction(); - if (onOkAction != null) { - onOkAction.run(); - } - } -} From 9445e3fdcb0b92e901680505c70637be5e39c70b Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Wed, 28 Jan 2026 09:52:54 +0800 Subject: [PATCH 21/43] revert gradle.properties --- .../azure-toolkit-for-intellij/gradle.properties | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties index f5051491877..43485d7ea98 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties @@ -3,7 +3,7 @@ intellijDisplayVersion=2025.3 intellij_version=253-EAP-SNAPSHOT platformVersion=253-EAP-SNAPSHOT # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html -pluginSinceBuild=251 +pluginSinceBuild=253 pluginUntilBuild=253.* # Example: platformPlugins = com.jetbrains.php:203.4449.22, org.intellij.scala:2023.3.27@EAP platformPlugins=org.intellij.scala:2025.3.12 @@ -31,10 +31,7 @@ org.gradle.caching=true org.gradle.parallel=true org.gradle.console=rich org.gradle.configureondemand=true -org.gradle.daemon=true -org.gradle.workers.max=8 -org.gradle.vfs.watch=true -org.gradle.jvmargs=-Xmx4096m -Xms1024m -XX:MaxMetaspaceSize=1024m -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParallelGC -Dfile.encoding=UTF-8 -Duser.language=en +org.gradle.jvmargs='-Duser.language=en' org.jetbrains.intellij.platform.buildFeature.useBinaryReleases=false From 0e1cc290828cae9ffd9abf05cd618985d9c97307 Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Wed, 28 Jan 2026 09:53:33 +0800 Subject: [PATCH 22/43] revert gradle.properties --- PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties index 43485d7ea98..c608c642f3c 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties @@ -31,6 +31,7 @@ org.gradle.caching=true org.gradle.parallel=true org.gradle.console=rich org.gradle.configureondemand=true +org.gradle.daemon=true org.gradle.jvmargs='-Duser.language=en' org.jetbrains.intellij.platform.buildFeature.useBinaryReleases=false From 407a07ed263d606401010b86ca1b99fce3ea7194 Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Wed, 28 Jan 2026 15:49:02 +0800 Subject: [PATCH 23/43] telemetry update click-option to click-task --- .../intellij/appmod/javamigration/MigrateToAzureAction.java | 2 +- .../intellij/appmod/javamigration/MigrateToAzureNode.java | 2 +- .../connector/projectexplorer/MigrateToAzureFacetNode.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java index 1e5d6fa5143..79147560b67 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java @@ -202,7 +202,7 @@ public void update(@NotNull AnActionEvent e) { @Override public void actionPerformed(@NotNull AnActionEvent e) { - AppModUtils.logTelemetryEvent("action.click-option", Map.of("label", nodeData.getLabel())); + AppModUtils.logTelemetryEvent("action.click-task", Map.of("label", nodeData.getLabel())); nodeData.click(e); } }; diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java index cd4c43dea06..67b377a4111 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java @@ -157,7 +157,7 @@ private Node convertToNode(MigrateNodeData data) { // Set click handler if (data.hasClickHandler()) { node.onClicked(d -> { - AppModUtils.logTelemetryEvent("node.click-option", Map.of("label", data.getLabel())); + AppModUtils.logTelemetryEvent("node.click-task", Map.of("label", data.getLabel())); data.click(null); }); } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java index d7d89596215..e8f57a21866 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java @@ -212,7 +212,7 @@ protected void buildView(@Nonnull PresentationData presentation) { @Override public void navigate(boolean requestFocus) { // Trigger click handler - AppModUtils.logTelemetryEvent("facet.click-option", java.util.Map.of("label", nodeData.getLabel())); + AppModUtils.logTelemetryEvent("facet.click-task", java.util.Map.of("label", nodeData.getLabel())); nodeData.doubleClick(null); } From 38b2625291f5bf7cdec43f30b03d77dfcd8424d2 Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Wed, 28 Jan 2026 15:54:28 +0800 Subject: [PATCH 24/43] telemetry update no-options to no-tasks --- .../intellij/appmod/javamigration/MigrateToAzureAction.java | 2 +- .../intellij/appmod/javamigration/MigrateToAzureNode.java | 2 +- .../connector/projectexplorer/MigrateToAzureFacetNode.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java index 79147560b67..9ff70b001a5 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java @@ -86,7 +86,7 @@ private MigrationState computeState(Project project) { .collect(Collectors.toList()); if (nodes.isEmpty()) { - AppModUtils.logTelemetryEvent("action.no-options"); + AppModUtils.logTelemetryEvent("action.no-tasks"); } return new MigrationState( diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java index 67b377a4111..41eae1252bc 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java @@ -108,7 +108,7 @@ private List loadMigrationNodeData() { .filter(MigrateNodeData::isVisible) .collect(Collectors.toList()); if (nodes.isEmpty()) { - AppModUtils.logTelemetryEvent("node.no-options"); + AppModUtils.logTelemetryEvent("node.no-tasks"); } return nodes; } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java index e8f57a21866..583ebd14a94 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java @@ -73,7 +73,7 @@ private List computeMigrationNodes() { .filter(MigrateNodeData::isVisible) .collect(Collectors.toList()); if (nodes.isEmpty()) { - AppModUtils.logTelemetryEvent("facet.no-options"); + AppModUtils.logTelemetryEvent("facet.no-tasks"); } return nodes; } From 26818ea60868078617cb45530b0eb27a013a9056 Mon Sep 17 00:00:00 2001 From: Ye Zhu Date: Wed, 28 Jan 2026 22:31:25 +0800 Subject: [PATCH 25/43] Add fix for single CVE dependency; Polish the UI text --- .../appmod/common/AppModPluginInstaller.java | 15 ++ .../intellij/appmod/javaupgrade/Contants.java | 7 +- .../CveFixDependencyInProblemsViewAction.java | 145 +++++++++++++++ .../CveFixDependencyIntentionAction.java | 165 ++++++++++++++++++ .../action/CveFixIntentionAction.java | 11 +- .../action/JavaUpgradeQuickFix.java | 14 +- .../javaupgrade/dao/JavaUpgradeIssue.java | 5 +- .../javaupgrade/dao/VulnerabilityInfo.java | 88 ++++++++++ .../JavaUpgradeIssuesInspection.java | 2 +- .../JavaUpgradeIssuesDetectionService.java | 63 ++++--- .../JavaVersionNotificationService.java | 47 ++--- .../META-INF/azure-intellij-plugin-appmod.xml | 13 +- .../after.xml.template | 5 + .../before.xml.template | 5 + .../description.html | 13 ++ .../CveFixIntentionAction/after.xml.template | 5 + .../CveFixIntentionAction/before.xml.template | 5 + .../CveFixIntentionAction/description.html | 13 ++ 18 files changed, 549 insertions(+), 72 deletions(-) create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyIntentionAction.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/dao/VulnerabilityInfo.java create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixDependencyIntentionAction/after.xml.template create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixDependencyIntentionAction/before.xml.template create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixDependencyIntentionAction/description.html create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixIntentionAction/after.xml.template create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixIntentionAction/before.xml.template create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixIntentionAction/description.html diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java index 3db9cc9aea0..011624edc29 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java @@ -98,6 +98,21 @@ public static void showInstallConfirmation(@Nonnull Project project, @Nonnull Ru } } + public static void showAppModInstallationConfirmation(@Nonnull Project project) { + final boolean copilotInstalled = isCopilotInstalled(); + + final String title = copilotInstalled + ? "Install Github Copilot app modernization" + : "Install GitHub Copilot and app modernization"; + + final String message = copilotInstalled + ? "Install this plugin to upgrade your apps with Copilot." + : "To upgrade your apps, you'll need two plugins: GitHub Copilot and app modernization."; + + if (Messages.showOkCancelDialog(project, message, title, "Install", "Cancel", Messages.getQuestionIcon()) == Messages.OK) { + installPlugin(project); + } + } /** * Installs the App Modernization plugin. * IntelliJ platform will automatically install Copilot as a dependency if AppMod declares it. diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/Contants.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/Contants.java index 019092aff81..f5f37d64e60 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/Contants.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/Contants.java @@ -4,12 +4,15 @@ public class Contants { public static final String UPGRADE_JAVA_AND_FRAMEWORK_PROMPT = "Upgrade java runtime and java framework dependencies of this project to the latest LTS version using java upgrade tools by invoking #generate_upgrade_plan"; - public static final String UPGRADE_JAVA_VERSION_PROMPT = "upgrade java runtime to Java " + MATURE_JAVA_LTS_VERSION + " (LTS) using java upgrade tools by invoking #generate_upgrade_plan"; - public static final String UPGRADE_JAVA_FRAMEWORK_PROMPT = "upgrade java framework dependencies of this project to latest LTS version using java upgrade tools by invoking #generate_upgrade_plan"; + public static final String UPGRADE_JAVA_VERSION_PROMPT = "Upgrade Java runtime from version %s to Java %s (LTS) using java upgrade tools by invoking #generate_upgrade_plan"; + public static final String UPGRADE_JAVA_FRAMEWORK_PROMPT = "Upgrade %s from version %s to %s using java upgrade tools by invoking #generate_upgrade_plan"; public static final String SCAN_AND_RESOLVE_CVES_PROMPT = "run CVE scan for this project using java upgrade tools by invoking #validate_cves_for_java"; public static final String UPGRADE_JDK_WITH_COPILOT_DISPLAY_NAME = "Upgrade JDK with Copilot"; public static final String UPGRADE_SPRING_BOOT_WITH_COPILOT_DISPLAY_NAME = "Upgrade Spring Boot with Copilot"; public static final String UPGRADE_SPRING_FRAMEWORK_WITH_COPILOT_DISPLAY_NAME = "Upgrade Spring Framework with Copilot"; public static final String UPGRADE_SPRING_SECURITY_WITH_COPILOT_DISPLAY_NAME = "Upgrade Spring Security with Copilot"; public static final String SCAN_AND_RESOLVE_CVES_WITH_COPILOT_DISPLAY_NAME = "Scan and Resolve CVEs with Copilot"; + public static final String FIX_VULNERABLE_DEPENDENCY_WITH_COPILOT_PROMPT = "Fix the vulnerable dependency %s by using #validate_cves_for_java"; + public static final String FIX_VULNERABLE_DEPENDENCY_WITH_COPILOT_DISPLAY_NAME = "Fix the vulnerable dependency with Copilot"; + public static final String ISSUE_DISPLAY_NAME = "Your project uses %s %s. Consider upgrading to %s %s or higher for better performance and support"; } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java new file mode 100644 index 00000000000..5cc25fe80d9 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action; + +import com.intellij.openapi.actionSystem.*; +import com.intellij.openapi.project.DumbAware; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.VulnerabilityInfo; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesCache; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.*; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.VulnerabilityInfo.parseVulnerabilityDescription; + +/** + * Action to fix vulnerable dependencies by opening GitHub Copilot chat with an upgrade prompt. + * This action appears in the Problems View context menu for vulnerable dependency issues. + */ +public class CveFixDependencyInProblemsViewAction extends AnAction implements DumbAware { + + private static final String CVE_MARKER = "CVE-"; + + private VulnerabilityInfo vulnerabilityInfo; + public CveFixDependencyInProblemsViewAction() { + super(); + } + + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + final Project project = e.getData(CommonDataKeys.PROJECT); + if (project == null || project.isDisposed()) { + return; + } + if (vulnerabilityInfo == null) { + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt( + project, + SCAN_AND_RESOLVE_CVES_PROMPT + ); + } else { + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt( + project, + String.format(FIX_VULNERABLE_DEPENDENCY_WITH_COPILOT_PROMPT, + vulnerabilityInfo.getDependencyCoordinate()) + ); + } + + } + + @Override + public void update(@NotNull AnActionEvent e) { + final Project project = e.getData(CommonDataKeys.PROJECT); + if (project == null || project.isDisposed()) { + e.getPresentation().setEnabledAndVisible(false); + return; + } + + // Check if we're in the Problems View context with a vulnerability + final String description = extractProblemDescription(e); + if (description == null) { + e.getPresentation().setEnabledAndVisible(false); + return; + } + vulnerabilityInfo = parseVulnerabilityDescription(description); + // Also check if the file is pom.xml or build.gradle (common for dependency issues) + final VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE); + final boolean isBuildFile = isBuildFile(file); + + if (!isBuildFile || !isCVEIssue(description)) { + e.getPresentation().setEnabledAndVisible(false); + return; + } + final var issue = JavaUpgradeIssuesCache.getInstance(project).findCveIssue(vulnerabilityInfo.getGroupId() + ":" + vulnerabilityInfo.getArtifactId()); + if (issue == null){ + e.getPresentation().setEnabledAndVisible(false); + return; + } + e.getPresentation().setEnabledAndVisible(true); + // e.getPresentation().setText(SCAN_AND_RESOLVE_CVES_WITH_COPILOT_DISPLAY_NAME); + if (!AppModPluginInstaller.isAppModPluginInstalled()) { + e.getPresentation().setText(e.getPresentation().getText() + AppModPluginInstaller.TO_INSTALL_APP_MODE_PLUGIN); + } + } + + private boolean isBuildFile(VirtualFile file){ + return file != null && + (file.getName().equals("pom.xml") || file.getName().endsWith(".gradle") || file.getName().endsWith(".gradle.kts")); + } + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.BGT; + } + + /** + * Extracts the problem description from the action event context. + */ + @Nullable + private String extractProblemDescription(@NotNull AnActionEvent e) { + + // Approach 3: Try to get selected items from the tree + try { + final Object[] selectedItems = e.getData(PlatformDataKeys.SELECTED_ITEMS); + if (selectedItems != null && selectedItems.length > 0) { + // Concatenate all selected items' string representations + final StringBuilder sb = new StringBuilder(); + for (Object item : selectedItems) { + if (item != null) { + sb.append(item.toString()).append(" "); + } + } + final String result = sb.toString().trim(); + if (!result.isEmpty()) { + return result; + } + } + } catch (Exception ignored) { + } + return null; + } + + /** + * Extracts CVE ID from the problem description. + */ + private boolean isCVEIssue(@NotNull String description) { + // Pattern: CVE-YYYY-NNNNN + final int cveIndex = description.toUpperCase().indexOf(CVE_MARKER); + if (cveIndex >= 0) { + return true; + } + return false; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyIntentionAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyIntentionAction.java new file mode 100644 index 00000000000..41244535d3a --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyIntentionAction.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action; + +import com.intellij.codeInsight.intention.HighPriorityAction; +import com.intellij.codeInsight.intention.IntentionAction; +import com.intellij.codeInspection.util.IntentionFamilyName; +import com.intellij.codeInspection.util.IntentionName; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiFile; +import com.intellij.util.IncorrectOperationException; +import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.VulnerabilityInfo; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesCache; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; + +import org.jetbrains.annotations.NotNull; + +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.*; + +/** + * Intention action to fix CVE vulnerabilities in dependencies using GitHub Copilot. + * This action appears in the editor's quick-fix popup (More actions...) for pom.xml files(yellow wavy dependency with CVE issue) + * when a vulnerable dependency with CVE issues is detected. + * + * Implements HighPriorityAction to appear at the top of the quick-fix list. + */ +public class CveFixDependencyIntentionAction implements IntentionAction, HighPriorityAction { + + // Cached dependency info from isAvailable() for use in getText() + private VulnerabilityInfo vulnerabilityInfo; + + @Override + public @IntentionName @NotNull String getText() { + if (!AppModPluginInstaller.isAppModPluginInstalled()) { + return FIX_VULNERABLE_DEPENDENCY_WITH_COPILOT_DISPLAY_NAME + AppModPluginInstaller.TO_INSTALL_APP_MODE_PLUGIN; + } + return FIX_VULNERABLE_DEPENDENCY_WITH_COPILOT_DISPLAY_NAME; + } + + @Override + public @IntentionFamilyName @NotNull String getFamilyName() { + return "Azure Toolkit"; + } + + @Override + public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) { + // Reset cached values + vulnerabilityInfo = null; + + if (file == null || editor == null) { + return false; + } + + // Only available for pom.xml files + final String fileName = file.getName(); + if (!fileName.equals("pom.xml")) { + return false; + } + + try { + final int offset = editor.getCaretModel().getOffset(); + final String documentText = editor.getDocument().getText(); + + // Try to extract dependency info - only show if cursor is within a block + final int dependencyStart = findDependencyStart(documentText, offset); + final int dependencyEnd = findDependencyEnd(documentText, offset); + + if (dependencyStart >= 0 && dependencyEnd > dependencyStart) { + final String dependencyBlock = documentText.substring(dependencyStart, dependencyEnd); + String cachedGroupId = extractXmlValue(dependencyBlock, "groupId"); + String cachedArtifactId = extractXmlValue(dependencyBlock, "artifactId"); + String cachedVersion = extractXmlValue(dependencyBlock, "version"); + vulnerabilityInfo = VulnerabilityInfo.builder().groupId(cachedGroupId).artifactId(cachedArtifactId).version(cachedVersion).build(); + // Only show if we have valid dependency info (not for parent/plugin sections) + if(cachedGroupId != null && cachedArtifactId != null) { + //if the artifact is in the cached cve issues, show the intention + final var issue = JavaUpgradeIssuesCache.getInstance(project).findCveIssue(cachedGroupId + ":" + cachedArtifactId); + return issue != null; + } + } + } catch (Exception e) { + // Ignore and return false + } + + return false; + } + + @Override + public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException { + if (file == null || editor == null) { + return; + } + + // Try to extract dependency information from the current context + final String prompt = buildPromptFromContext(editor, file); + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + } + + /** + * Builds a prompt based on the current editor context. + */ + private String buildPromptFromContext(@NotNull Editor editor, @NotNull PsiFile file) { + return String.format(FIX_VULNERABLE_DEPENDENCY_WITH_COPILOT_PROMPT, + vulnerabilityInfo.getDependencyCoordinate()); + } + + /** + * Finds the start of the dependency block containing the given offset. + */ + private int findDependencyStart(@NotNull String text, int offset) { + // Look for tag before the offset + int searchStart = Math.max(0, offset - 500); + String searchArea = text.substring(searchStart, offset); + int lastDependency = searchArea.lastIndexOf(""); + if (lastDependency >= 0) { + return searchStart + lastDependency; + } + return -1; + } + + /** + * Finds the end of the dependency block containing the given offset. + */ + private int findDependencyEnd(@NotNull String text, int offset) { + // Look for tag after the offset + int searchEnd = Math.min(text.length(), offset + 500); + String searchArea = text.substring(offset, searchEnd); + int endDependency = searchArea.indexOf(""); + if (endDependency >= 0) { + return offset + endDependency + "".length(); + } + return -1; + } + + /** + * Extracts a value from an XML tag. + */ + private String extractXmlValue(@NotNull String xml, @NotNull String tagName) { + final String startTag = "<" + tagName + ">"; + final String endTag = ""; + + int start = xml.indexOf(startTag); + if (start < 0) { + return null; + } + start += startTag.length(); + + int end = xml.indexOf(endTag, start); + if (end < 0) { + return null; + } + + return xml.substring(start, end).trim(); + } + + @Override + public boolean startInWriteAction() { + return false; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java index b22f817b71b..febf7850e28 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java @@ -5,8 +5,8 @@ package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action; +import com.intellij.codeInsight.intention.HighPriorityAction; import com.intellij.codeInsight.intention.IntentionAction; -import com.intellij.codeInsight.intention.PriorityAction; import com.intellij.codeInspection.util.IntentionFamilyName; import com.intellij.codeInspection.util.IntentionName; import com.intellij.openapi.editor.Editor; @@ -26,8 +26,10 @@ * Intention action to fix CVE vulnerabilities in dependencies using GitHub Copilot. * This action appears in the editor's quick-fix popup (More actions...) for pom.xml files(yellow wavy dependency with CVE issue) * when a vulnerable dependency with CVE issues is detected. + * + * Implements HighPriorityAction to appear at the top of the quick-fix list. */ -public class CveFixIntentionAction implements IntentionAction, PriorityAction { +public class CveFixIntentionAction implements IntentionAction, HighPriorityAction { // Cached dependency info from isAvailable() for use in getText() private String cachedGroupId; @@ -160,9 +162,4 @@ private String extractXmlValue(@NotNull String xml, @NotNull String tagName) { public boolean startInWriteAction() { return false; } - - @Override - public @NotNull Priority getPriority() { - return Priority.NORMAL; - } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java index 5841a552c8b..711a28b92a0 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java @@ -12,7 +12,7 @@ import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; -import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService.PACKAGE_ID_JDK; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.UPGRADE_JAVA_FRAMEWORK_PROMPT; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; @@ -54,19 +54,9 @@ public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descri } private String buildPromptForIssue(@NotNull JavaUpgradeIssue issue) { - String packageId = issue.getPackageId(); - // JDK upgrade - if (PACKAGE_ID_JDK.equals(packageId)) { - return String.format( - "Upgrade Java runtime from version %s to Java %s (LTS) using java upgrade tools by invoking #generate_upgrade_plan", - issue.getCurrentVersion(), issue.getSuggestedVersion() - ); - } - - // Framework upgrade (Spring Boot, Spring Framework, etc.) return String.format( - "Upgrade %s from version %s to %s using java upgrade tools by invoking #generate_upgrade_plan", + UPGRADE_JAVA_FRAMEWORK_PROMPT, issue.getPackageDisplayName(), issue.getCurrentVersion(), issue.getSuggestedVersion() ); } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/dao/JavaUpgradeIssue.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/dao/JavaUpgradeIssue.java index 8224dc19887..b4ddceedd89 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/dao/JavaUpgradeIssue.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/dao/JavaUpgradeIssue.java @@ -121,13 +121,16 @@ public enum Severity { */ @Nullable private String cveId; + + @Nullable + private String eofDate; /** * Gets a formatted title for the notification. */ public String getTitle() { return switch (upgradeReason) { - case JRE_TOO_OLD -> "Outdated JDK Version Detected"; + case JRE_TOO_OLD -> "Outdated JDK Detected"; case END_OF_LIFE -> packageDisplayName + " End of Life"; case DEPRECATED -> packageDisplayName + " Deprecated"; case CVE -> "Security Vulnerability in " + packageDisplayName; diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/dao/VulnerabilityInfo.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/dao/VulnerabilityInfo.java new file mode 100644 index 00000000000..0910b62f7a5 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/dao/VulnerabilityInfo.java @@ -0,0 +1,88 @@ +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao; + + +import lombok.Builder; +import lombok.Data; +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Represents a parsed vulnerability with dependency information and CVE IDs. + */ +@Data +@Builder +public class VulnerabilityInfo { + // Pattern to match Maven dependency: maven:groupId:artifactId:version + private static final Pattern MAVEN_DEPENDENCY_PATTERN = + Pattern.compile("maven:([^:]+):([^:]+):([^\\s]+)"); + + // Pattern to match CVE IDs: CVE-YYYY-NNNNN (year and number) + private static final Pattern CVE_PATTERN = + Pattern.compile("CVE-\\d{4}-\\d{4,}"); + @NotNull + private String groupId; + @NotNull + private String artifactId; + @Nullable + private String version; + @Nullable + private List cveIds; + + /** + * Returns the full Maven coordinate in format groupId:artifactId:version + */ + public String getDependencyCoordinate() { + if (version == null){ + return groupId + ":" + artifactId; + } + return groupId + ":" + artifactId + ":" + version; + } + + @Override + public String toString() { + return "VulnerabilityInfo{" + + "dependency='" + getDependencyCoordinate() + '\'' + + ", cveIds=" + cveIds + + '}'; + } + + + /** + * Parses a vulnerability description to extract the dependency coordinate and CVE IDs. + *

+ * Expected format: "Provides transitive vulnerable dependency maven:groupId:artifactId:version CVE-YYYY-NNNNN score ..." + * + * @param description The vulnerability description from the Problems View + * @return VulnerabilityInfo containing the parsed dependency and CVE IDs, or null if parsing fails + */ + @Nullable + public static VulnerabilityInfo parseVulnerabilityDescription(@Nullable String description) { + if (description == null || description.isEmpty()) { + return null; + } + + // Extract Maven dependency coordinate + final Matcher dependencyMatcher = MAVEN_DEPENDENCY_PATTERN.matcher(description); + if (!dependencyMatcher.find()) { + return null; + } + + final String groupId = dependencyMatcher.group(1); + final String artifactId = dependencyMatcher.group(2); + final String version = dependencyMatcher.group(3); + + // Extract all CVE IDs + final List cveIds = new ArrayList<>(); + final Matcher cveMatcher = CVE_PATTERN.matcher(description.toUpperCase()); + while (cveMatcher.find()) { + cveIds.add(cveMatcher.group()); + } + + return new VulnerabilityInfo(groupId, artifactId, version, cveIds); + } +} \ No newline at end of file diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java index 999dd32d36c..8725a9b5812 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java @@ -95,7 +95,7 @@ private void registerProblem(@NotNull ProblemsHolder holder, @NotNull XmlTag tag holder.registerProblem( tag, issue.getMessage(), - ProblemHighlightType.WARNING, + ProblemHighlightType.WEAK_WARNING, new JavaUpgradeQuickFix(issue) ); } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java index 9389d20d734..80372993f6b 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java @@ -24,8 +24,12 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; import java.util.*; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.ISSUE_DISPLAY_NAME; + /** * Service to detect JDK version and framework dependency versions in Java projects. * This service analyzes the project to identify outdated versions that may need upgrading. @@ -52,6 +56,7 @@ public class JavaUpgradeIssuesDetectionService { // Package ID constants (used for cache lookups) public static final String PACKAGE_ID_JDK = "jdk"; + public static final String JDK_DISPLAY_NAME = "JDK"; /** * Metadata for dependencies to scan. @@ -190,6 +195,12 @@ public String getEolDateForVersion(@Nonnull String version) { private static JavaUpgradeIssuesDetectionService instance; + /** Formatter for parsing EOL dates in "yyyy-MM" format */ + private static final DateTimeFormatter EOL_DATE_PARSER = DateTimeFormatter.ofPattern("yyyy-MM"); + + /** Formatter for displaying EOL dates in "MMMM yyyy" format (e.g., "June 2020") */ + private static final DateTimeFormatter EOL_DATE_DISPLAY = DateTimeFormatter.ofPattern("MMMM yyyy", Locale.ENGLISH); + private JavaUpgradeIssuesDetectionService() { } @@ -200,6 +211,24 @@ public static synchronized JavaUpgradeIssuesDetectionService getInstance() { return instance; } + /** + * Formats an EOL date from "yyyy-MM" format to "Month yyyy" format. + * For example, "2020-06" becomes "June 2020". + * + * @param eofDate The EOL date string in "yyyy-MM" format (e.g., "2020-06") + * @return The formatted date string (e.g., "June 2020"), or the original string if parsing fails + */ + @Nonnull + public static String formatEofDate(@Nonnull String eofDate) { + try { + YearMonth yearMonth = YearMonth.parse(eofDate, EOL_DATE_PARSER); + return yearMonth.format(EOL_DATE_DISPLAY); + } catch (Exception e) { + // If parsing fails, return the original string + return eofDate; + } + } + /** * Gets JDK/JRE version issues. * Aligned with getJavaIssues() from assessmentManager.ts. @@ -222,15 +251,14 @@ public List getJavaIssues(@Nonnull Project project) { // Check against MATURE_JAVA_LTS_VERSION (21) if (jdkVersion < MATURE_JAVA_LTS_VERSION) { issues.add(JavaUpgradeIssue.builder() - .packageId("jdk") - .packageDisplayName("JDK") + .packageId(PACKAGE_ID_JDK) + .packageDisplayName(JDK_DISPLAY_NAME) .upgradeReason(JavaUpgradeIssue.UpgradeReason.JRE_TOO_OLD) .severity(JavaUpgradeIssue.Severity.WARNING) .currentVersion(String.valueOf(jdkVersion)) .supportedVersion(">=" + MATURE_JAVA_LTS_VERSION) .suggestedVersion(String.valueOf(MATURE_JAVA_LTS_VERSION)) - .message(String.format("This project is using an older Java runtime (%d). Would you like to upgrade it to %d (LTS)?", - jdkVersion, MATURE_JAVA_LTS_VERSION)) + .message(String.format(ISSUE_DISPLAY_NAME, JDK_DISPLAY_NAME, jdkVersion, JDK_DISPLAY_NAME, MATURE_JAVA_LTS_VERSION)) .learnMoreUrl(JDK_LEARN_MORE_URL) .build()); } @@ -390,6 +418,7 @@ private JavaUpgradeIssue checkDependency(@Nonnull MavenProject mavenProject, .suggestedVersion(checkItem.suggestedVersion) .message(buildUpgradeMessage(checkItem.displayName, version, checkItem)) .learnMoreUrl(checkItem.learnMoreUrl) + .eofDate(checkItem.getEolDateForVersion(version)) .build(); } @@ -546,27 +575,10 @@ private JavaUpgradeIssue.Severity determineSeverity(@Nonnull String version, private String buildUpgradeMessage(@Nonnull String displayName, @Nonnull String currentVersion, @Nonnull DependencyCheckItem checkItem) { - String eolDateStr = getEolDateString(currentVersion, checkItem); - boolean isEol = isVersionEndOfLife(currentVersion, checkItem); - if (isEol && eolDateStr != null) { - return String.format( - "This project is using %s %s, which has reached end of life in %s. " + - "Would you like to upgrade it to %s?", - displayName, currentVersion, eolDateStr, checkItem.suggestedVersion - ); - } else if (eolDateStr != null) { - return String.format( - "This project is using %s %s, which will reach end of life in %s. " + - "Would you like to upgrade it to %s?", - displayName, currentVersion, eolDateStr, checkItem.suggestedVersion - ); - } else { - return String.format( - "This project is using %s %s, which is outside the supported version range (%s). " + - "Would you like to upgrade it to %s?", - displayName, currentVersion, checkItem.supportedVersion, checkItem.suggestedVersion - ); - } + return String.format( + ISSUE_DISPLAY_NAME, + displayName, currentVersion, displayName, checkItem.suggestedVersion + ); } /** @@ -633,6 +645,7 @@ private JavaUpgradeIssue checkGradleDependency(@Nonnull ExternalProject gradlePr .suggestedVersion(checkItem.suggestedVersion) .message(buildUpgradeMessage(checkItem.displayName, version, checkItem)) .learnMoreUrl(checkItem.learnMoreUrl) + .eofDate(checkItem.getEolDateForVersion(version)) .build(); } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java index af1bc385e28..adebff00933 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java @@ -19,8 +19,7 @@ import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager; -import static com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller.installPlugin; -import static com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller.isAppModPluginInstalled; +import static com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller.*; import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.*; import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService.*; import java.lang.reflect.Method; @@ -94,10 +93,15 @@ public void showNotifications(@Nonnull Project project, @Nonnull List"); - sb.append(issue.getMessage()); - - if (issue.getCurrentVersion() != null && issue.getSuggestedVersion() != null) { - sb.append("

"); - sb.append("Current: ").append(issue.getCurrentVersion()); - sb.append(" → Suggested: ").append(issue.getSuggestedVersion()); + if (isAppModPluginInstalled()){ + sb.append(issue.getMessage()); + } else { + sb.append(issue.getMessage() + TO_INSTALL_APP_MODE_PLUGIN); } + sb.append("."); + +// if (issue.getCurrentVersion() != null && issue.getSuggestedVersion() != null) { +// sb.append("

"); +// sb.append("Current: ").append(issue.getCurrentVersion()); +// sb.append(" → Suggested: ").append(issue.getSuggestedVersion()); +// } sb.append(""); return sb.toString(); @@ -290,24 +299,21 @@ public void openCopilotChatWithPrompt(@Nonnull Project project, @Nonnull String AzureTaskManager.getInstance().runLater(() -> { if (!isAppModPluginInstalled()) { // showGenericUpgradeGuidance(project, prompt); - installPlugin(project); + showAppModInstallationConfirmation(project); return; } // Try direct API call first (works when plugin versions match) if (tryDirectCopilotCall(project, prompt)) { - System.out.println("Direct Copilot call succeeded."); return; // Success, no need for reflection } // Fallback to reflection for cross-version compatibility if (tryReflectionCopilotCall(project, prompt)) { - System.out.println("Reflection Copilot call succeeded."); return; // Success via reflection } // Both approaches failed - System.out.println("Both direct and reflection Copilot calls failed."); showGenericUpgradeGuidance(project, prompt); }); } @@ -328,7 +334,6 @@ private boolean tryDirectCopilotCall(@Nonnull Project project, @Nonnull String p builder.withSessionIdReceiver(sessionId -> null); return null; }); - System.out.println("Direct Copilot call succeeded."); return true; } } catch (Error | Exception e) { @@ -467,14 +472,10 @@ private Method findMethodByName(Class clazz, String methodName) { * @return The prompt string for Copilot */ private String buildUpgradePrompt(@Nonnull JavaUpgradeIssue issue) { - if (issue.getUpgradeReason() == JavaUpgradeIssue.UpgradeReason.JRE_TOO_OLD) { - return UPGRADE_JAVA_VERSION_PROMPT; - } else if (issue.getUpgradeReason() == JavaUpgradeIssue.UpgradeReason.CVE){ - return SCAN_AND_RESOLVE_CVES_PROMPT; - }else if (issue.getUpgradeReason() == JavaUpgradeIssue.UpgradeReason.DEPRECATED || issue.getUpgradeReason() == JavaUpgradeIssue.UpgradeReason.END_OF_LIFE) { - return UPGRADE_JAVA_FRAMEWORK_PROMPT; + if (issue.getUpgradeReason() == JavaUpgradeIssue.UpgradeReason.CVE){ + return String.format(FIX_VULNERABLE_DEPENDENCY_WITH_COPILOT_PROMPT, issue.getPackageId()); } - return UPGRADE_JAVA_AND_FRAMEWORK_PROMPT; + return String.format(UPGRADE_JAVA_FRAMEWORK_PROMPT, issue.getPackageDisplayName(), issue.getCurrentVersion(), issue.getSuggestedVersion()); } /** diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml index bf394c89e59..52ef298c447 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml @@ -9,9 +9,14 @@ XML - com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action.CveFixIntentionAction + com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action.CveFixDependencyIntentionAction Azure Toolkit + + XML + com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action.CveFixIntentionAction + Azure Toolkit + + + + + org.example + vulnerable-library + 2.0.0 + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixDependencyIntentionAction/before.xml.template b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixDependencyIntentionAction/before.xml.template new file mode 100644 index 00000000000..b1bef2c076a --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixDependencyIntentionAction/before.xml.template @@ -0,0 +1,5 @@ + + org.example + vulnerable-library + 1.0.0 + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixDependencyIntentionAction/description.html b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixDependencyIntentionAction/description.html new file mode 100644 index 00000000000..9225dc45325 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixDependencyIntentionAction/description.html @@ -0,0 +1,13 @@ + + +Fixes CVE vulnerabilities in Maven dependencies using GitHub Copilot. +

+When the cursor is positioned on a dependency in pom.xml that has known CVE issues, +this intention action opens GitHub Copilot Chat with a predefined prompt to help identify and fix +the security vulnerabilities in that specific dependency. +

+

+This action is available only for Maven pom.xml files and requires GitHub Copilot and GitHub Copilot app modernization to be installed. +

+ + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixIntentionAction/after.xml.template b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixIntentionAction/after.xml.template new file mode 100644 index 00000000000..47d971d72b9 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixIntentionAction/after.xml.template @@ -0,0 +1,5 @@ + + org.example + vulnerable-library + 2.0.0 + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixIntentionAction/before.xml.template b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixIntentionAction/before.xml.template new file mode 100644 index 00000000000..b1bef2c076a --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixIntentionAction/before.xml.template @@ -0,0 +1,5 @@ + + org.example + vulnerable-library + 1.0.0 + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixIntentionAction/description.html b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixIntentionAction/description.html new file mode 100644 index 00000000000..67067b85919 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/intentionDescriptions/CveFixIntentionAction/description.html @@ -0,0 +1,13 @@ + + +Scans and resolves CVE vulnerabilities in Maven dependencies using java upgrade tools. +

+When the cursor is positioned on a dependency in pom.xml that has known CVE issues, +this intention action opens GitHub Copilot Chat with a predefined prompt to help identify and fix +the security vulnerabilities. +

+

+This action is available only for Maven pom.xml files and requires GitHub Copilot and Github Copilot app modernization to be installed. +

+ + From 6cc112a66dc401a8def135286e35b5f40092360b Mon Sep 17 00:00:00 2001 From: Ye Zhu Date: Thu, 29 Jan 2026 17:04:01 +0800 Subject: [PATCH 26/43] Add telemetry --- .../intellij/appmod/common/AppModPluginInstaller.java | 3 +++ .../action/CveFixDependencyInProblemsViewAction.java | 3 ++- .../action/CveFixDependencyIntentionAction.java | 2 ++ .../javaupgrade/action/CveFixInProblemsViewAction.java | 2 ++ .../javaupgrade/action/CveFixIntentionAction.java | 2 ++ .../action/JavaUpgradeContextMenuAction.java | 2 ++ .../appmod/javaupgrade/action/JavaUpgradeQuickFix.java | 2 ++ .../javaupgrade/action/UpgradeActionRegistrar.java | 2 ++ .../service/JavaUpgradeIssuesDetectionService.java | 3 ++- .../service/JavaVersionNotificationService.java | 10 +++++++--- .../javaupgrade/settings/JavaUpgradeConfigurable.java | 4 ++++ 11 files changed, 30 insertions(+), 5 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java index 011624edc29..e14725d4b16 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java @@ -110,7 +110,10 @@ public static void showAppModInstallationConfirmation(@Nonnull Project project) : "To upgrade your apps, you'll need two plugins: GitHub Copilot and app modernization."; if (Messages.showOkCancelDialog(project, message, title, "Install", "Cancel", Messages.getQuestionIcon()) == Messages.OK) { + AppModUtils.logTelemetryEvent("plugin.install-upgrade-confirmed"); installPlugin(project); + } else { + AppModUtils.logTelemetryEvent("plugin.install-upgrade-cancelled"); } } /** diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java index 5cc25fe80d9..8119be9392a 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java @@ -13,6 +13,7 @@ import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.VulnerabilityInfo; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesCache; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -56,7 +57,7 @@ public void actionPerformed(@NotNull AnActionEvent e) { vulnerabilityInfo.getDependencyCoordinate()) ); } - + AppModUtils.logTelemetryEvent("openCopilotChatForCveFixDependencyInProblemsViewAction"); } @Override diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyIntentionAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyIntentionAction.java index 41244535d3a..a6b638e4761 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyIntentionAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyIntentionAction.java @@ -18,6 +18,7 @@ import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesCache; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; import org.jetbrains.annotations.NotNull; import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.*; @@ -99,6 +100,7 @@ public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws // Try to extract dependency information from the current context final String prompt = buildPromptFromContext(editor, file); JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + AppModUtils.logTelemetryEvent("openCveFixDependencyCopilotChatFromIntentionAction"); } /** diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixInProblemsViewAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixInProblemsViewAction.java index ee97e00cfa1..5996a3de197 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixInProblemsViewAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixInProblemsViewAction.java @@ -15,6 +15,7 @@ import com.intellij.openapi.vfs.VirtualFile; import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.*; @@ -42,6 +43,7 @@ public void actionPerformed(@NotNull AnActionEvent e) { project, SCAN_AND_RESOLVE_CVES_PROMPT ); + AppModUtils.logTelemetryEvent("openCopilotChatForCveFixInProblemsViewAction"); } @Override diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java index febf7850e28..7ed7215af14 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java @@ -17,6 +17,7 @@ import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesCache; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; import org.jetbrains.annotations.NotNull; import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.SCAN_AND_RESOLVE_CVES_PROMPT; @@ -100,6 +101,7 @@ public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws // Try to extract dependency information from the current context final String prompt = buildPromptFromContext(editor, file); JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + AppModUtils.logTelemetryEvent("openCveFixCopilotChatFromIntentionAction"); } /** diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java index c398b14252f..36da8242dc4 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java @@ -13,6 +13,7 @@ import com.intellij.openapi.vfs.VirtualFile; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; import org.jetbrains.annotations.NotNull; import static com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller.TO_INSTALL_APP_MODE_PLUGIN; @@ -68,6 +69,7 @@ public void actionPerformed(@NotNull AnActionEvent e) { // Open Copilot chat with the upgrade prompt JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + AppModUtils.logTelemetryEvent("openJavaUpgradeCopilotChatFromContextMenu"); } /** diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java index 711a28b92a0..077007d8ee0 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java @@ -14,6 +14,7 @@ import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.UPGRADE_JAVA_FRAMEWORK_PROMPT; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; @@ -51,6 +52,7 @@ public String getName() { public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { String prompt = buildPromptForIssue(issue); JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + AppModUtils.logTelemetryEvent("openCopilotChatForJavaUpgradeQuickFix"); } private String buildPromptForIssue(@NotNull JavaUpgradeIssue issue) { diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java index 1b8ca3fbfc6..986df78c58b 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java @@ -14,6 +14,7 @@ import com.intellij.openapi.startup.ProjectActivity; import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; import kotlin.Unit; import kotlin.coroutines.Continuation; import org.jetbrains.annotations.NotNull; @@ -88,6 +89,7 @@ private void tryAddToGroup(ActionManager actionManager, DefaultActionGroup group if (!containsAction(group, UPGRADE_ACTION_ID, actionManager)) { // Add a separator before the upgrade action to visually group it group.add(Separator.create()); + AppModUtils.logTelemetryEvent("java-upgrade.contextmenu.action.registered"); group.add(upgradeAction); } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java index 80372993f6b..001321702db 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java @@ -5,9 +5,9 @@ package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service; -import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; import com.microsoft.azure.toolkit.intellij.common.utils.JdkUtils; import com.microsoft.intellij.util.GradleUtils; import com.microsoft.intellij.util.MavenUtils; @@ -239,6 +239,7 @@ public List getJavaIssues(@Nonnull Project project) { try { final Integer jdkVersion = JdkUtils.getJdkLanguageLevel(project); + AppModUtils.logTelemetryEvent("getJavaVersion", Map.of("jdkVersion", String.valueOf(jdkVersion))); if (jdkVersion == null) { return issues; } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java index adebff00933..3499b2c2c99 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java @@ -17,6 +17,7 @@ import com.intellij.openapi.extensions.PluginId; import com.intellij.openapi.project.Project; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager; import static com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller.*; @@ -30,6 +31,7 @@ import com.github.copilot.api.CopilotChatService; import javax.annotation.Nonnull; import java.util.List; +import java.util.Map; import java.util.Objects; /** @@ -105,23 +107,25 @@ private void showNotification(@Nonnull Project project, formatMessage(issue), notificationType ); - + String issueStr = issue.toString(); if (isAppModPluginInstalled()) { // Plugin is installed - show "Upgrade" action + AppModUtils.logTelemetryEvent("showNotification.install.appmod", Map.of("javaupgrade.issue", issueStr)); notification.addAction(new NotificationAction("Upgrade") { @Override public void actionPerformed(@NotNull AnActionEvent e, @NotNull Notification notification) { openCopilotChatWithUpgradePrompt(project, issue); - notification.expire(); + // notification.expire(); } }); } else { // Plugin is not installed - show "Install and Upgrade" action + AppModUtils.logTelemetryEvent("showNotification.upgrade", Map.of("javaupgrade.issue", issueStr)); notification.addAction(new NotificationAction("Install and Upgrade") { @Override public void actionPerformed(@NotNull AnActionEvent e, @NotNull Notification notification) { showAppModInstallationConfirmation(project); - notification.expire(); + // notification.expire(); } }); } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/settings/JavaUpgradeConfigurable.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/settings/JavaUpgradeConfigurable.java index 392c7ccbc87..2a9cbacf37f 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/settings/JavaUpgradeConfigurable.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/settings/JavaUpgradeConfigurable.java @@ -10,12 +10,14 @@ import com.intellij.openapi.util.NlsContexts; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; +import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; import java.text.SimpleDateFormat; import java.util.Date; +import java.util.Map; /** * Settings page for Java Upgrade feature. @@ -133,6 +135,7 @@ public void apply() throws ConfigurationException { } final JavaVersionNotificationService service = JavaVersionNotificationService.getInstance(); service.setNotificationsEnabled(enableNotificationsCheckBox.isSelected()); + AppModUtils.logTelemetryEvent("applyJavaUpgradeNotificationSettings", Map.of("notificationsEnabled", String.valueOf(enableNotificationsCheckBox.isSelected()))); } @Override @@ -143,6 +146,7 @@ public void reset() { final JavaVersionNotificationService service = JavaVersionNotificationService.getInstance(); enableNotificationsCheckBox.setSelected(service.isNotificationsEnabled()); updateDeferralStatusLabel(); + AppModUtils.logTelemetryEvent("resetJavaUpgradeNotificationDeferralSettings"); } @Override From 46b410deb529041215176450d11907dec3c768ae Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Thu, 29 Jan 2026 17:48:02 +0800 Subject: [PATCH 27/43] remove duplicate and unuseful code --- .../appmod/common/AppModPluginInstaller.java | 63 +++++++------------ .../javamigration/MigrateToAzureAction.java | 2 +- .../javamigration/MigrateToAzureNode.java | 2 +- .../JavaVersionNotificationService.java | 5 +- .../MigrateToAzureFacetNode.java | 2 +- 5 files changed, 29 insertions(+), 45 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java index 011624edc29..f195c062a3c 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java @@ -11,7 +11,6 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.updateSettings.impl.pluginsAdvertisement.PluginsAdvertiser; -import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager; import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; import javax.annotation.Nonnull; @@ -80,39 +79,33 @@ public static boolean isRunningInDevMode() { * Shows a confirmation dialog for plugin installation. * * @param project The current project - * @param onConfirm Callback to execute when user confirms installation + * @param forUpgrade true for "upgrade" scenario, false for "migrate to Azure" scenario + * @param onConfirm Callback to execute when user confirms installation (if null, calls installPlugin directly) */ - public static void showInstallConfirmation(@Nonnull Project project, @Nonnull Runnable onConfirm) { + public static void showInstallConfirmation(@Nonnull Project project, boolean forUpgrade, @Nonnull Runnable onConfirm) { final boolean copilotInstalled = isCopilotInstalled(); final String title = copilotInstalled ? "Install Github Copilot app modernization" : "Install GitHub Copilot and app modernization"; - final String message = copilotInstalled - ? "Install this plugin to automate migrating your apps to Azure with Copilot." - : "To migrate to Azure, you'll need two plugins: GitHub Copilot and app modernization."; + final String message; + if (copilotInstalled) { + message = forUpgrade + ? "Install this plugin to upgrade your apps with Copilot." + : "Install this plugin to automate migrating your apps to Azure with Copilot."; + } else { + message = forUpgrade + ? "To upgrade your apps, you'll need two plugins: GitHub Copilot and app modernization." + : "To migrate to Azure, you'll need two plugins: GitHub Copilot and app modernization."; + } if (Messages.showOkCancelDialog(project, message, title, "Install", "Cancel", Messages.getQuestionIcon()) == Messages.OK) { onConfirm.run(); } } - public static void showAppModInstallationConfirmation(@Nonnull Project project) { - final boolean copilotInstalled = isCopilotInstalled(); - - final String title = copilotInstalled - ? "Install Github Copilot app modernization" - : "Install GitHub Copilot and app modernization"; - - final String message = copilotInstalled - ? "Install this plugin to upgrade your apps with Copilot." - : "To upgrade your apps, you'll need two plugins: GitHub Copilot and app modernization."; - - if (Messages.showOkCancelDialog(project, message, title, "Install", "Cancel", Messages.getQuestionIcon()) == Messages.OK) { - installPlugin(project); - } - } + /** * Installs the App Modernization plugin. * IntelliJ platform will automatically install Copilot as a dependency if AppMod declares it. @@ -120,32 +113,22 @@ public static void showAppModInstallationConfirmation(@Nonnull Project project) * @param project The current project */ public static void installPlugin(@Nonnull Project project) { - final boolean appModInstalled = isAppModPluginInstalled(); - - // If already installed, nothing to do - if (appModInstalled) { + if (isAppModPluginInstalled()) { AppModUtils.logTelemetryEvent("plugin.install-skipped", Map.of("reason", "already-installed")); return; } // Only pass AppMod ID - IntelliJ will automatically install Copilot as dependency - // (AppMod's plugin.xml should declare com.github.copilot) final Set pluginsToInstall = new LinkedHashSet<>(); pluginsToInstall.add(PluginId.getId(PLUGIN_ID)); - // Use PluginsAdvertiser.installAndEnable - IntelliJ handles the rest - // The platform will show plugin selection dialog, download, install, and prompt for restart - AzureTaskManager.getInstance().runAndWait(() -> { - PluginsAdvertiser.installAndEnable( - project, - pluginsToInstall, - true, // showDialog - true, // selectAllInDialog - pre-select all plugins - null, // modalityState - () -> { - AppModUtils.logTelemetryEvent("plugin.install-complete"); - } - ); - }); + PluginsAdvertiser.installAndEnable( + project, + pluginsToInstall, + true, // showDialog + true, // selectAllInDialog - pre-select all plugins + null, // modalityState + () -> AppModUtils.logTelemetryEvent("plugin.install-complete") + ); } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java index 9ff70b001a5..30b54e89fb9 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java @@ -144,7 +144,7 @@ public void actionPerformed(@NotNull AnActionEvent e) { switch (migrationState.state) { case NOT_INSTALLED: AppModUtils.logTelemetryEvent("action.click-install"); - AppModPluginInstaller.showInstallConfirmation(project, + AppModPluginInstaller.showInstallConfirmation(project, false, () -> AppModPluginInstaller.installPlugin(project)); break; case NO_OPTIONS: diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java index 41eae1252bc..6b917bf9ea8 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java @@ -93,7 +93,7 @@ private void showNotInstalled() { onClicked(e -> { AppModUtils.logTelemetryEvent("node.click-install"); - AppModPluginInstaller.showInstallConfirmation(project, () -> AppModPluginInstaller.installPlugin(project)); + AppModPluginInstaller.showInstallConfirmation(project, false, () -> AppModPluginInstaller.installPlugin(project)); }); } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java index adebff00933..8b3e5d6a07d 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java @@ -16,6 +16,7 @@ import com.intellij.openapi.actionSystem.DataContext; import com.intellij.openapi.extensions.PluginId; import com.intellij.openapi.project.Project; +import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager; @@ -120,7 +121,7 @@ public void actionPerformed(@NotNull AnActionEvent e, @NotNull Notification noti notification.addAction(new NotificationAction("Install and Upgrade") { @Override public void actionPerformed(@NotNull AnActionEvent e, @NotNull Notification notification) { - showAppModInstallationConfirmation(project); + AppModPluginInstaller.showInstallConfirmation(project, true, () -> AppModPluginInstaller.installPlugin(project)); notification.expire(); } }); @@ -299,7 +300,7 @@ public void openCopilotChatWithPrompt(@Nonnull Project project, @Nonnull String AzureTaskManager.getInstance().runLater(() -> { if (!isAppModPluginInstalled()) { // showGenericUpgradeGuidance(project, prompt); - showAppModInstallationConfirmation(project); + AppModPluginInstaller.showInstallConfirmation(project, true, () -> AppModPluginInstaller.installPlugin(project)); return; } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java index 583ebd14a94..dbe18f444bf 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java @@ -142,7 +142,7 @@ public void navigate(boolean requestFocus) { if (!AppModPluginInstaller.isAppModPluginInstalled()) { // Plugin not installed - trigger install on double-click AppModUtils.logTelemetryEvent("facet.click-install"); - AppModPluginInstaller.showInstallConfirmation(getProject(), + AppModPluginInstaller.showInstallConfirmation(getProject(), false, () -> AppModPluginInstaller.installPlugin(getProject())); } else if (!hasMigrationOptions()) { // No migration options - open App Modernization Panel From def8daca3fb7eb58d14fc876276a4d657d462070 Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Thu, 29 Jan 2026 18:02:40 +0800 Subject: [PATCH 28/43] text update --- .../appmod/common/AppModPluginInstaller.java | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java index e0975285f58..ced3a618228 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java @@ -84,14 +84,21 @@ public static boolean isRunningInDevMode() { */ public static void showInstallConfirmation(@Nonnull Project project, boolean forUpgrade, @Nonnull Runnable onConfirm) { final boolean copilotInstalled = isCopilotInstalled(); - - final String title = copilotInstalled - ? "Install Github Copilot app modernization" - : "Install GitHub Copilot and app modernization"; - - final String message = copilotInstalled - ? "Install this plugin to automate migrating your apps to Azure with Copilot." - : "To migrate to Azure, you'll need two plugins: GitHub Copilot and app modernization."; + + final String title = copilotInstalled + ? "Install Github Copilot app modernization" + : "Install GitHub Copilot and app modernization"; + + final String message; + if (copilotInstalled) { + message = forUpgrade + ? "Install this plugin to upgrade your apps with Copilot." + : "Install this plugin to automate migrating your apps to Azure with Copilot."; + } else { + message = forUpgrade + ? "To upgrade your apps, you'll need two plugins: GitHub Copilot and app modernization." + : "To migrate to Azure, you'll need two plugins: GitHub Copilot and app modernization."; + } final String action = forUpgrade ? "upgrade" : "migration"; From 67e996977f4d13c416b413cc3d74fc8a3adeee8e Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Thu, 29 Jan 2026 19:07:12 +0800 Subject: [PATCH 29/43] telemetry update --- .../intellij/appmod/common/AppModPluginInstaller.java | 7 ++++--- .../appmod/javamigration/MigrateToAzureAction.java | 2 +- .../intellij/appmod/javamigration/MigrateToAzureNode.java | 2 +- .../service/JavaVersionNotificationService.java | 4 ++-- .../connector/projectexplorer/MigrateToAzureFacetNode.java | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java index ced3a618228..0e3f814b3b8 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java @@ -116,10 +116,10 @@ public static void showInstallConfirmation(@Nonnull Project project, boolean for * IntelliJ platform will automatically install Copilot as a dependency if AppMod declares it. * * @param project The current project + * @param forUpgrade true for "upgrade" scenario, false for "migrate to Azure" scenario */ - public static void installPlugin(@Nonnull Project project) { + public static void installPlugin(@Nonnull Project project, boolean forUpgrade) { if (isAppModPluginInstalled()) { - AppModUtils.logTelemetryEvent("plugin.install-skipped", Map.of("reason", "already-installed")); return; } @@ -127,13 +127,14 @@ public static void installPlugin(@Nonnull Project project) { final Set pluginsToInstall = new LinkedHashSet<>(); pluginsToInstall.add(PluginId.getId(PLUGIN_ID)); + final String source = forUpgrade ? "upgrade" : "migration"; PluginsAdvertiser.installAndEnable( project, pluginsToInstall, true, // showDialog true, // selectAllInDialog - pre-select all plugins null, // modalityState - () -> AppModUtils.logTelemetryEvent("plugin.install-complete") + () -> AppModUtils.logTelemetryEvent("plugin." + source + ".install-complete") ); } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java index 30b54e89fb9..31027677b61 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java @@ -145,7 +145,7 @@ public void actionPerformed(@NotNull AnActionEvent e) { case NOT_INSTALLED: AppModUtils.logTelemetryEvent("action.click-install"); AppModPluginInstaller.showInstallConfirmation(project, false, - () -> AppModPluginInstaller.installPlugin(project)); + () -> AppModPluginInstaller.installPlugin(project, false)); break; case NO_OPTIONS: AppModPanelHelper.openAppModPanel(project, "action"); diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java index 6b917bf9ea8..6f759f5affb 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java @@ -93,7 +93,7 @@ private void showNotInstalled() { onClicked(e -> { AppModUtils.logTelemetryEvent("node.click-install"); - AppModPluginInstaller.showInstallConfirmation(project, false, () -> AppModPluginInstaller.installPlugin(project)); + AppModPluginInstaller.showInstallConfirmation(project, false, () -> AppModPluginInstaller.installPlugin(project, false)); }); } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java index 769825498aa..cb0d2e78c28 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java @@ -125,7 +125,7 @@ public void actionPerformed(@NotNull AnActionEvent e, @NotNull Notification noti notification.addAction(new NotificationAction("Install and Upgrade") { @Override public void actionPerformed(@NotNull AnActionEvent e, @NotNull Notification notification) { - AppModPluginInstaller.showInstallConfirmation(project, true, () -> AppModPluginInstaller.installPlugin(project)); + AppModPluginInstaller.showInstallConfirmation(project, true, () -> AppModPluginInstaller.installPlugin(project, true)); // notification.expire(); } }); @@ -304,7 +304,7 @@ public void openCopilotChatWithPrompt(@Nonnull Project project, @Nonnull String AzureTaskManager.getInstance().runLater(() -> { if (!isAppModPluginInstalled()) { // showGenericUpgradeGuidance(project, prompt); - AppModPluginInstaller.showInstallConfirmation(project, true, () -> AppModPluginInstaller.installPlugin(project)); + AppModPluginInstaller.showInstallConfirmation(project, true, () -> AppModPluginInstaller.installPlugin(project, true)); return; } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java index dbe18f444bf..a425319943c 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java @@ -143,7 +143,7 @@ public void navigate(boolean requestFocus) { // Plugin not installed - trigger install on double-click AppModUtils.logTelemetryEvent("facet.click-install"); AppModPluginInstaller.showInstallConfirmation(getProject(), false, - () -> AppModPluginInstaller.installPlugin(getProject())); + () -> AppModPluginInstaller.installPlugin(getProject(), false)); } else if (!hasMigrationOptions()) { // No migration options - open App Modernization Panel AppModPanelHelper.openAppModPanel(getProject(), "facet"); From 57213c707f4b52b997153ebd299997188e4caef5 Mon Sep 17 00:00:00 2001 From: Ye Zhu Date: Thu, 29 Jan 2026 20:51:59 +0800 Subject: [PATCH 30/43] Fix bug of version match and add EOF date comparison --- .../JavaUpgradeIssuesDetectionService.java | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java index 001321702db..febd15764a6 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java @@ -408,7 +408,7 @@ private JavaUpgradeIssue checkDependency(@Nonnull MavenProject mavenProject, checkedPackages.add(checkItem.getPackageId()); // Check if version satisfies the supported version range - if (!satisfiesVersionRange(version, checkItem.supportedVersion)) { + if (!satisfiesVersionRange(version, checkItem.supportedVersion) && isEOF(version, checkItem)) { return JavaUpgradeIssue.builder() .packageId(checkItem.getPackageId()) .packageDisplayName(checkItem.displayName) @@ -425,7 +425,21 @@ private JavaUpgradeIssue checkDependency(@Nonnull MavenProject mavenProject, return null; } - + + private boolean isEOF(String version, DependencyCheckItem checkItem) { + String eolDateStr = checkItem.getEolDateForVersion(version); + if (eolDateStr == null) { + return false; + } + try { + YearMonth eolDate = YearMonth.parse(eolDateStr, EOL_DATE_PARSER); + YearMonth currentDate = YearMonth.now(); + return currentDate.isAfter(eolDate); + } catch (Exception e) { + return false; + } + + } /** * Gets the version from parent POM. */ @@ -470,13 +484,7 @@ private boolean satisfiesVersionRange(@Nonnull String version, @Nonnull String r */ private boolean satisfiesSingleCondition(@Nonnull String version, @Nonnull String condition) { try { - // Handle "x.y.z" pattern (e.g., "2.7.x" means any 2.7.*) - if (condition.endsWith(".x")) { - final String prefix = condition.substring(0, condition.length() - 2); - return version.startsWith(prefix + "."); - } - - // Handle ">=" pattern + // Handle ">=" pattern (check before ".x" pattern to handle ">=3.2.x" correctly) if (condition.startsWith(">=")) { String minVersion = condition.substring(2).trim(); // Handle version with wildcard, e.g. ">=3.2.x" -> "3.2" @@ -488,7 +496,7 @@ private boolean satisfiesSingleCondition(@Nonnull String version, @Nonnull Strin return current.compareTo(min) >= 0; } - // Handle ">" pattern + // Handle ">" pattern (check before ".x" pattern to handle ">3.2.x" correctly) if (condition.startsWith(">")) { String minVersion = condition.substring(1).trim(); // Handle version with wildcard, e.g. ">3.2.x" -> "3.2" @@ -500,6 +508,12 @@ private boolean satisfiesSingleCondition(@Nonnull String version, @Nonnull Strin return current.compareTo(min) > 0; } + // Handle "x.y.x" pattern (e.g., "2.7.x" means any 2.7.*) + if (condition.endsWith(".x")) { + final String prefix = condition.substring(0, condition.length() - 2); + return version.startsWith(prefix + "."); + } + // Handle exact version match return version.equals(condition); From bc939745c7363b91e3c865e58fd835dd731149c0 Mon Sep 17 00:00:00 2001 From: Ye Zhu Date: Fri, 30 Jan 2026 13:35:41 +0800 Subject: [PATCH 31/43] Add try catch to escape error and log. Fix comments --- .../JavaUpgradeCheckStartupActivity.java | 13 +- .../CveFixDependencyInProblemsViewAction.java | 115 +++++++++--------- .../CveFixDependencyIntentionAction.java | 114 ++++++----------- .../action/CveFixInProblemsViewAction.java | 80 ++++++------ .../action/CveFixIntentionAction.java | 113 ++++++----------- .../action/JavaUpgradeContextMenuAction.java | 67 ++++++---- .../action/JavaUpgradeQuickFix.java | 17 +-- .../action/UpgradeActionRegistrar.java | 10 +- .../javaupgrade/dao/VulnerabilityInfo.java | 2 +- .../JavaUpgradeIssuesInspection.java | 3 + .../javaupgrade/service/CVECheckService.java | 14 ++- .../service/JavaUpgradeIssuesCache.java | 56 +++++---- .../JavaUpgradeIssuesDetectionService.java | 44 ++++--- .../JavaVersionNotificationService.java | 59 +++++---- .../{Contants.java => utils/Constants.java} | 6 +- .../appmod/javaupgrade/utils/PomXmlUtils.java | 83 +++++++++++++ 16 files changed, 439 insertions(+), 357 deletions(-) rename PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/{Contants.java => utils/Constants.java} (88%) create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/utils/PomXmlUtils.java diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java index 5eb1606dad9..023d7c58dbc 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java @@ -11,12 +11,12 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.startup.ProjectActivity; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; -import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesCache; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager; import kotlin.Unit; import kotlin.coroutines.Continuation; +import lombok.extern.slf4j.Slf4j; import reactor.core.publisher.Mono; import javax.annotation.Nonnull; @@ -27,6 +27,7 @@ * Startup activity that detects outdated JDK and framework versions when a project is opened. * This runs after the project is fully loaded and shows notifications for any detected issues. */ +@Slf4j public class JavaUpgradeCheckStartupActivity implements ProjectActivity, DumbAware { // Additional delay after smart mode to ensure Maven/Gradle sync is complete @@ -45,7 +46,10 @@ public Object execute(@Nonnull Project project, @Nonnull Continuation { /* Error during Java upgrade check startup */ } + error -> { + /* Error during Java upgrade check startup */ + log.error("Error during Java upgrade check startup for project: {}", project.getName(), error); + } ); }); @@ -57,6 +61,7 @@ public Object execute(@Nonnull Project project, @Nonnull Continuation { if (project.isDisposed()) { @@ -68,7 +73,6 @@ private void performJavaUpgradeCheck(@Nonnull Project project) { cache.refresh(); // Get all issues including CVEs - final JavaUpgradeIssuesDetectionService detectionService = JavaUpgradeIssuesDetectionService.getInstance(); final List allIssues = new java.util.ArrayList<>(); allIssues.addAll(cache.getJdkIssues()); allIssues.addAll(cache.getDependencyIssues()); @@ -92,8 +96,9 @@ private void performJavaUpgradeCheck(@Nonnull Project project) { }); }); - } catch (Exception e) { + } catch (Throwable e) { // Error performing Java version check + log.error("Error performing Java upgrade check for project: {}", project.getName(), e); } } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java index 8119be9392a..7169950c9d0 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java @@ -14,21 +14,18 @@ import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesCache; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; +import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.*; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils.Constants.*; import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.VulnerabilityInfo.parseVulnerabilityDescription; /** * Action to fix vulnerable dependencies by opening GitHub Copilot chat with an upgrade prompt. * This action appears in the Problems View context menu for vulnerable dependency issues. */ +@Slf4j public class CveFixDependencyInProblemsViewAction extends AnAction implements DumbAware { private static final String CVE_MARKER = "CVE-"; @@ -41,57 +38,67 @@ public CveFixDependencyInProblemsViewAction() { @Override public void actionPerformed(@NotNull AnActionEvent e) { - final Project project = e.getData(CommonDataKeys.PROJECT); - if (project == null || project.isDisposed()) { - return; - } - if (vulnerabilityInfo == null) { - JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt( - project, - SCAN_AND_RESOLVE_CVES_PROMPT - ); - } else { - JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt( - project, - String.format(FIX_VULNERABLE_DEPENDENCY_WITH_COPILOT_PROMPT, - vulnerabilityInfo.getDependencyCoordinate()) - ); + try { + final Project project = e.getData(CommonDataKeys.PROJECT); + if (project == null || project.isDisposed()) { + return; + } + if (vulnerabilityInfo == null) { + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt( + project, + SCAN_AND_RESOLVE_CVES_PROMPT + ); + } else { + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt( + project, + String.format(FIX_VULNERABLE_DEPENDENCY_WITH_COPILOT_PROMPT, + vulnerabilityInfo.getDependencyCoordinate()) + ); + } + AppModUtils.logTelemetryEvent("openCopilotChatForCveFixDependencyInProblemsViewAction"); + } catch (Throwable ex) { + log.error("Failed to open Copilot chat for CVE fix", ex.getMessage()); } - AppModUtils.logTelemetryEvent("openCopilotChatForCveFixDependencyInProblemsViewAction"); } @Override public void update(@NotNull AnActionEvent e) { - final Project project = e.getData(CommonDataKeys.PROJECT); - if (project == null || project.isDisposed()) { - e.getPresentation().setEnabledAndVisible(false); - return; - } - - // Check if we're in the Problems View context with a vulnerability - final String description = extractProblemDescription(e); - if (description == null) { - e.getPresentation().setEnabledAndVisible(false); - return; - } - vulnerabilityInfo = parseVulnerabilityDescription(description); - // Also check if the file is pom.xml or build.gradle (common for dependency issues) - final VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE); - final boolean isBuildFile = isBuildFile(file); - - if (!isBuildFile || !isCVEIssue(description)) { - e.getPresentation().setEnabledAndVisible(false); - return; - } - final var issue = JavaUpgradeIssuesCache.getInstance(project).findCveIssue(vulnerabilityInfo.getGroupId() + ":" + vulnerabilityInfo.getArtifactId()); - if (issue == null){ + try { + final Project project = e.getData(CommonDataKeys.PROJECT); + if (project == null || project.isDisposed()) { + e.getPresentation().setEnabledAndVisible(false); + return; + } + + // Check if we're in the Problems View context with a vulnerability + final String description = extractProblemDescription(e); + if (description == null) { + e.getPresentation().setEnabledAndVisible(false); + return; + } + vulnerabilityInfo = parseVulnerabilityDescription(description); + // Also check if the file is pom.xml or build.gradle (common for dependency issues) + final VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE); + final boolean isBuildFile = isBuildFile(file); + + if (!isBuildFile || !isCVEIssue(description)) { + e.getPresentation().setEnabledAndVisible(false); + return; + } + final var issue = JavaUpgradeIssuesCache.getInstance(project).findCveIssue(vulnerabilityInfo.getGroupId() + ":" + vulnerabilityInfo.getArtifactId()); + if (issue == null) { + e.getPresentation().setEnabledAndVisible(false); + return; + } + e.getPresentation().setEnabledAndVisible(true); + // e.getPresentation().setText(SCAN_AND_RESOLVE_CVES_WITH_COPILOT_DISPLAY_NAME); + if (!AppModPluginInstaller.isAppModPluginInstalled()) { + e.getPresentation().setText(e.getPresentation().getText() + AppModPluginInstaller.TO_INSTALL_APP_MODE_PLUGIN); + } + } catch (Throwable ex) { + // In case of any error, hide the action e.getPresentation().setEnabledAndVisible(false); - return; - } - e.getPresentation().setEnabledAndVisible(true); - // e.getPresentation().setText(SCAN_AND_RESOLVE_CVES_WITH_COPILOT_DISPLAY_NAME); - if (!AppModPluginInstaller.isAppModPluginInstalled()) { - e.getPresentation().setText(e.getPresentation().getText() + AppModPluginInstaller.TO_INSTALL_APP_MODE_PLUGIN); + log.error("Failed to update CVE fix action visibility, hide the action", ex); } } @@ -137,10 +144,6 @@ private String extractProblemDescription(@NotNull AnActionEvent e) { */ private boolean isCVEIssue(@NotNull String description) { // Pattern: CVE-YYYY-NNNNN - final int cveIndex = description.toUpperCase().indexOf(CVE_MARKER); - if (cveIndex >= 0) { - return true; - } - return false; + return description.toUpperCase().contains(CVE_MARKER); } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyIntentionAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyIntentionAction.java index a6b638e4761..8f3114202b4 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyIntentionAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyIntentionAction.java @@ -19,9 +19,11 @@ import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils.PomXmlUtils; +import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; -import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.*; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils.Constants.*; /** * Intention action to fix CVE vulnerabilities in dependencies using GitHub Copilot. @@ -30,6 +32,7 @@ * * Implements HighPriorityAction to appear at the top of the quick-fix list. */ +@Slf4j public class CveFixDependencyIntentionAction implements IntentionAction, HighPriorityAction { // Cached dependency info from isAvailable() for use in getText() @@ -50,32 +53,31 @@ public class CveFixDependencyIntentionAction implements IntentionAction, HighPri @Override public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) { - // Reset cached values - vulnerabilityInfo = null; - - if (file == null || editor == null) { - return false; - } - - // Only available for pom.xml files - final String fileName = file.getName(); - if (!fileName.equals("pom.xml")) { - return false; - } - try { + // Reset cached values + vulnerabilityInfo = null; + + if (file == null || editor == null) { + return false; + } + + // Only available for pom.xml files + final String fileName = file.getName(); + if (!fileName.equals("pom.xml")) { + return false; + } final int offset = editor.getCaretModel().getOffset(); final String documentText = editor.getDocument().getText(); // Try to extract dependency info - only show if cursor is within a block - final int dependencyStart = findDependencyStart(documentText, offset); - final int dependencyEnd = findDependencyEnd(documentText, offset); + final int dependencyStart = PomXmlUtils.findDependencyStart(documentText, offset); + final int dependencyEnd = PomXmlUtils.findDependencyEnd(documentText, offset); if (dependencyStart >= 0 && dependencyEnd > dependencyStart) { final String dependencyBlock = documentText.substring(dependencyStart, dependencyEnd); - String cachedGroupId = extractXmlValue(dependencyBlock, "groupId"); - String cachedArtifactId = extractXmlValue(dependencyBlock, "artifactId"); - String cachedVersion = extractXmlValue(dependencyBlock, "version"); + String cachedGroupId = PomXmlUtils.extractXmlValue(dependencyBlock, "groupId"); + String cachedArtifactId = PomXmlUtils.extractXmlValue(dependencyBlock, "artifactId"); + String cachedVersion = PomXmlUtils.extractXmlValue(dependencyBlock, "version"); vulnerabilityInfo = VulnerabilityInfo.builder().groupId(cachedGroupId).artifactId(cachedArtifactId).version(cachedVersion).build(); // Only show if we have valid dependency info (not for parent/plugin sections) if(cachedGroupId != null && cachedArtifactId != null) { @@ -84,8 +86,9 @@ public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file return issue != null; } } - } catch (Exception e) { + } catch (Throwable e) { // Ignore and return false + log.error("Error checking availability of CveFixDependencyIntentionAction", e); } return false; @@ -93,73 +96,32 @@ public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file @Override public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException { - if (file == null || editor == null) { - return; + try { + if (file == null || editor == null) { + return; + } + + // Try to extract dependency information from the current context + final String prompt = buildPromptFromContext(editor, file); + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + AppModUtils.logTelemetryEvent("openCveFixDependencyCopilotChatFromIntentionAction"); + } catch (Throwable e) { + log.error("Failed to invoke CveFixDependencyIntentionAction: ", e); } - - // Try to extract dependency information from the current context - final String prompt = buildPromptFromContext(editor, file); - JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); - AppModUtils.logTelemetryEvent("openCveFixDependencyCopilotChatFromIntentionAction"); } /** * Builds a prompt based on the current editor context. */ private String buildPromptFromContext(@NotNull Editor editor, @NotNull PsiFile file) { + if (vulnerabilityInfo == null) { + log.error("Vulnerability info is null in buildPromptFromContext"); + return SCAN_AND_RESOLVE_CVES_PROMPT; + } return String.format(FIX_VULNERABLE_DEPENDENCY_WITH_COPILOT_PROMPT, vulnerabilityInfo.getDependencyCoordinate()); } - /** - * Finds the start of the dependency block containing the given offset. - */ - private int findDependencyStart(@NotNull String text, int offset) { - // Look for tag before the offset - int searchStart = Math.max(0, offset - 500); - String searchArea = text.substring(searchStart, offset); - int lastDependency = searchArea.lastIndexOf(""); - if (lastDependency >= 0) { - return searchStart + lastDependency; - } - return -1; - } - - /** - * Finds the end of the dependency block containing the given offset. - */ - private int findDependencyEnd(@NotNull String text, int offset) { - // Look for tag after the offset - int searchEnd = Math.min(text.length(), offset + 500); - String searchArea = text.substring(offset, searchEnd); - int endDependency = searchArea.indexOf(""); - if (endDependency >= 0) { - return offset + endDependency + "".length(); - } - return -1; - } - - /** - * Extracts a value from an XML tag. - */ - private String extractXmlValue(@NotNull String xml, @NotNull String tagName) { - final String startTag = "<" + tagName + ">"; - final String endTag = ""; - - int start = xml.indexOf(startTag); - if (start < 0) { - return null; - } - start += startTag.length(); - - int end = xml.indexOf(endTag, start); - if (end < 0) { - return null; - } - - return xml.substring(start, end).trim(); - } - @Override public boolean startInWriteAction() { return false; diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixInProblemsViewAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixInProblemsViewAction.java index 5996a3de197..3bc7e0cd71e 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixInProblemsViewAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixInProblemsViewAction.java @@ -16,14 +16,16 @@ import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; +import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.*; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils.Constants.*; /** * Action to fix vulnerable dependencies by opening GitHub Copilot chat with an upgrade prompt. * This action appears in the Problems View context menu for vulnerable dependency issues. */ +@Slf4j public class CveFixInProblemsViewAction extends AnAction implements DumbAware { private static final String CVE_MARKER = "CVE-"; @@ -34,46 +36,56 @@ public CveFixInProblemsViewAction() { @Override public void actionPerformed(@NotNull AnActionEvent e) { - final Project project = e.getData(CommonDataKeys.PROJECT); - if (project == null || project.isDisposed()) { - return; - } + try { + final Project project = e.getData(CommonDataKeys.PROJECT); + if (project == null || project.isDisposed()) { + return; + } - JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt( - project, - SCAN_AND_RESOLVE_CVES_PROMPT - ); - AppModUtils.logTelemetryEvent("openCopilotChatForCveFixInProblemsViewAction"); + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt( + project, + SCAN_AND_RESOLVE_CVES_PROMPT + ); + AppModUtils.logTelemetryEvent("openCopilotChatForCveFixInProblemsViewAction"); + } catch (Throwable ex) { + log.error("Failed to open Copilot chat for CVE fix", ex.getMessage()); + } } @Override public void update(@NotNull AnActionEvent e) { - final Project project = e.getData(CommonDataKeys.PROJECT); - if (project == null || project.isDisposed()) { - e.getPresentation().setEnabledAndVisible(false); - return; - } - - // Check if we're in the Problems View context with a vulnerability - final String description = extractProblemDescription(e); - if (description == null) { - e.getPresentation().setEnabledAndVisible(false); - return; - } + try { + final Project project = e.getData(CommonDataKeys.PROJECT); + if (project == null || project.isDisposed()) { + e.getPresentation().setEnabledAndVisible(false); + return; + } - // Also check if the file is pom.xml or build.gradle (common for dependency issues) - final VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE); - final boolean isBuildFile = isBuildFile(file); - - if (!isBuildFile || !isCVEIssue(description)) { - e.getPresentation().setEnabledAndVisible(false); - return; - } + // Check if we're in the Problems View context with a vulnerability + final String description = extractProblemDescription(e); + if (description == null) { + e.getPresentation().setEnabledAndVisible(false); + return; + } + + // Also check if the file is pom.xml or build.gradle (common for dependency issues) + final VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE); + final boolean isBuildFile = isBuildFile(file); - e.getPresentation().setEnabledAndVisible(true); - e.getPresentation().setText(SCAN_AND_RESOLVE_CVES_WITH_COPILOT_DISPLAY_NAME); - if (!AppModPluginInstaller.isAppModPluginInstalled()) { - e.getPresentation().setText(SCAN_AND_RESOLVE_CVES_WITH_COPILOT_DISPLAY_NAME + AppModPluginInstaller.TO_INSTALL_APP_MODE_PLUGIN); + if (!isBuildFile || !isCVEIssue(description)) { + e.getPresentation().setEnabledAndVisible(false); + return; + } + + e.getPresentation().setEnabledAndVisible(true); + e.getPresentation().setText(SCAN_AND_RESOLVE_CVES_WITH_COPILOT_DISPLAY_NAME); + if (!AppModPluginInstaller.isAppModPluginInstalled()) { + e.getPresentation().setText(SCAN_AND_RESOLVE_CVES_WITH_COPILOT_DISPLAY_NAME + AppModPluginInstaller.TO_INSTALL_APP_MODE_PLUGIN); + } + } catch (Throwable ex) { + // In case of any error, hide the action + e.getPresentation().setEnabledAndVisible(false); + log.error("Failed to update CVE fix action visibility, hide the action", ex); } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java index 7ed7215af14..fc55d986a8f 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixIntentionAction.java @@ -18,10 +18,12 @@ import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils.PomXmlUtils; +import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; -import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.SCAN_AND_RESOLVE_CVES_PROMPT; -import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.SCAN_AND_RESOLVE_CVES_WITH_COPILOT_DISPLAY_NAME; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils.Constants.SCAN_AND_RESOLVE_CVES_PROMPT; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils.Constants.SCAN_AND_RESOLVE_CVES_WITH_COPILOT_DISPLAY_NAME; /** * Intention action to fix CVE vulnerabilities in dependencies using GitHub Copilot. @@ -30,6 +32,7 @@ * * Implements HighPriorityAction to appear at the top of the quick-fix list. */ +@Slf4j public class CveFixIntentionAction implements IntentionAction, HighPriorityAction { // Cached dependency info from isAvailable() for use in getText() @@ -51,32 +54,32 @@ public class CveFixIntentionAction implements IntentionAction, HighPriorityActio @Override public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) { - // Reset cached values - cachedGroupId = null; - cachedArtifactId = null; - - if (file == null || editor == null) { - return false; - } - - // Only available for pom.xml files - final String fileName = file.getName(); - if (!fileName.equals("pom.xml")) { - return false; - } try { + // Reset cached values + cachedGroupId = null; + cachedArtifactId = null; + + if (file == null || editor == null) { + return false; + } + + // Only available for pom.xml files + final String fileName = file.getName(); + if (!fileName.equals("pom.xml")) { + return false; + } final int offset = editor.getCaretModel().getOffset(); final String documentText = editor.getDocument().getText(); // Try to extract dependency info - only show if cursor is within a block - final int dependencyStart = findDependencyStart(documentText, offset); - final int dependencyEnd = findDependencyEnd(documentText, offset); + final int dependencyStart = PomXmlUtils.findDependencyStart(documentText, offset); + final int dependencyEnd = PomXmlUtils.findDependencyEnd(documentText, offset); if (dependencyStart >= 0 && dependencyEnd > dependencyStart) { final String dependencyBlock = documentText.substring(dependencyStart, dependencyEnd); - cachedGroupId = extractXmlValue(dependencyBlock, "groupId"); - cachedArtifactId = extractXmlValue(dependencyBlock, "artifactId"); + cachedGroupId = PomXmlUtils.extractXmlValue(dependencyBlock, "groupId"); + cachedArtifactId = PomXmlUtils.extractXmlValue(dependencyBlock, "artifactId"); // Only show if we have valid dependency info (not for parent/plugin sections) if(cachedGroupId != null && cachedArtifactId != null) { @@ -85,8 +88,9 @@ public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file return issue != null; } } - } catch (Exception e) { + } catch (Throwable e) { // Ignore and return false + log.error("Error in CveFixIntentionAction.isAvailable: ", e); } return false; @@ -94,72 +98,27 @@ public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file @Override public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException { - if (file == null || editor == null) { - return; + try { + if (file == null || editor == null) { + return; + } + + // Try to extract dependency information from the current context + final String prompt = buildPromptFromContext(); + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + AppModUtils.logTelemetryEvent("openCveFixCopilotChatFromIntentionAction"); + } catch (Throwable e) { + log.error("Failed to invoke CveFixIntentionAction: ", e); } - - // Try to extract dependency information from the current context - final String prompt = buildPromptFromContext(editor, file); - JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); - AppModUtils.logTelemetryEvent("openCveFixCopilotChatFromIntentionAction"); } /** * Builds a prompt based on the current editor context. */ - private String buildPromptFromContext(@NotNull Editor editor, @NotNull PsiFile file) { + private String buildPromptFromContext() { return SCAN_AND_RESOLVE_CVES_PROMPT; } - /** - * Finds the start of the dependency block containing the given offset. - */ - private int findDependencyStart(@NotNull String text, int offset) { - // Look for tag before the offset - int searchStart = Math.max(0, offset - 500); - String searchArea = text.substring(searchStart, offset); - int lastDependency = searchArea.lastIndexOf(""); - if (lastDependency >= 0) { - return searchStart + lastDependency; - } - return -1; - } - - /** - * Finds the end of the dependency block containing the given offset. - */ - private int findDependencyEnd(@NotNull String text, int offset) { - // Look for tag after the offset - int searchEnd = Math.min(text.length(), offset + 500); - String searchArea = text.substring(offset, searchEnd); - int endDependency = searchArea.indexOf(""); - if (endDependency >= 0) { - return offset + endDependency + "".length(); - } - return -1; - } - - /** - * Extracts a value from an XML tag. - */ - private String extractXmlValue(@NotNull String xml, @NotNull String tagName) { - final String startTag = "<" + tagName + ">"; - final String endTag = ""; - - int start = xml.indexOf(startTag); - if (start < 0) { - return null; - } - start += startTag.length(); - - int end = xml.indexOf(endTag, start); - if (end < 0) { - return null; - } - - return xml.substring(start, end).trim(); - } - @Override public boolean startInWriteAction() { return false; diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java index 36da8242dc4..a376688950a 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java @@ -14,11 +14,12 @@ import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; +import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import static com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller.TO_INSTALL_APP_MODE_PLUGIN; import static com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller.isAppModPluginInstalled; -import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.UPGRADE_JAVA_AND_FRAMEWORK_PROMPT; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils.Constants.UPGRADE_JAVA_AND_FRAMEWORK_PROMPT; /** * Context menu action to upgrade a Java project using GitHub Copilot. @@ -27,6 +28,7 @@ * - pom.xml (Maven projects) * - build.gradle or build.gradle.kts (Gradle projects) */ +@Slf4j public class JavaUpgradeContextMenuAction extends AnAction { // text, description, and icon are defined in azure-intellij-plugin-appmod.xml public JavaUpgradeContextMenuAction() { @@ -40,42 +42,55 @@ public JavaUpgradeContextMenuAction() { @Override public void update(@NotNull AnActionEvent e) { - final Project project = e.getProject(); - final VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE); - - boolean visible = false; - - if (project != null && file != null) { - // Check if it's a project root, pom.xml, or build.gradle file - visible = isProjectRoot(project, file) || - isMavenBuildFile(file) || - isGradleBuildFile(file); - } - if (!isAppModPluginInstalled()) { - e.getPresentation().setText(e.getPresentation().getText() + TO_INSTALL_APP_MODE_PLUGIN); + try { + final Project project = e.getProject(); + final VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE); + + boolean visible = false; + + if (project != null && file != null) { + // Check if it's a project root, pom.xml, or build.gradle file + visible = isProjectRoot(project, file) || + isMavenBuildFile(file) || + isGradleBuildFile(file); + } + if (!isAppModPluginInstalled()) { + e.getPresentation().setText(e.getPresentation().getText() + TO_INSTALL_APP_MODE_PLUGIN); + } + if (visible){ + AppModUtils.logTelemetryEvent("showJavaUpgradeContextMenuAction"); + } + e.getPresentation().setEnabledAndVisible(visible); + } catch (Throwable ex) { + // In case of any error, hide the action + e.getPresentation().setEnabledAndVisible(false); } - e.getPresentation().setEnabledAndVisible(visible); } @Override public void actionPerformed(@NotNull AnActionEvent e) { - final Project project = e.getProject(); - if (project == null) { - return; - } + try { + final Project project = e.getProject(); + if (project == null) { + return; + } - final VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE); - String prompt = buildUpgradePrompt(project, file); - - // Open Copilot chat with the upgrade prompt - JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); - AppModUtils.logTelemetryEvent("openJavaUpgradeCopilotChatFromContextMenu"); + final VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE); + String prompt = buildUpgradePrompt(); + + // Open Copilot chat with the upgrade prompt + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + AppModUtils.logTelemetryEvent("openJavaUpgradeCopilotChatFromContextMenu"); + } catch (Throwable ex) { + // Log error but do not crash + log.error("Failed to perform Java upgrade action from context menu", ex); + } } /** * Builds the upgrade prompt based on the selected file. */ - private String buildUpgradePrompt(Project project, VirtualFile file) { + private String buildUpgradePrompt() { return UPGRADE_JAVA_AND_FRAMEWORK_PROMPT; } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java index 077007d8ee0..aa6076f24bb 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeQuickFix.java @@ -11,18 +11,18 @@ import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; - -import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.UPGRADE_JAVA_FRAMEWORK_PROMPT; - import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; +import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils.Constants.UPGRADE_JAVA_FRAMEWORK_PROMPT; /** * Quick fix for Java/Spring version upgrade issues detected in pom.xml files. * This action is triggered from Java upgrade issue inspections and opens Copilot * to assist with the upgrade process. */ +@Slf4j public class JavaUpgradeQuickFix implements LocalQuickFix { private final JavaUpgradeIssue issue; @@ -50,13 +50,16 @@ public String getName() { @Override public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { - String prompt = buildPromptForIssue(issue); - JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); - AppModUtils.logTelemetryEvent("openCopilotChatForJavaUpgradeQuickFix"); + try { + String prompt = buildPromptForIssue(issue); + JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); + AppModUtils.logTelemetryEvent("openCopilotChatForJavaUpgradeQuickFix"); + } catch (Throwable ex) { + log.error("Failed to apply Java upgrade quick fix", ex); + } } private String buildPromptForIssue(@NotNull JavaUpgradeIssue issue) { - return String.format( UPGRADE_JAVA_FRAMEWORK_PROMPT, issue.getPackageDisplayName(), issue.getCurrentVersion(), issue.getSuggestedVersion() diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java index 986df78c58b..d0d58a23ce7 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/UpgradeActionRegistrar.java @@ -17,6 +17,7 @@ import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; import kotlin.Unit; import kotlin.coroutines.Continuation; +import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -24,6 +25,7 @@ * Registers the Upgrade action into the GitHub Copilot context menu at runtime. * This is needed because the Copilot plugin creates its context menu groups dynamically. */ +@Slf4j public class UpgradeActionRegistrar implements ProjectActivity { private static final String UPGRADE_ACTION_ID = "AzureToolkit.JavaUpgradeContextMenu"; @@ -32,13 +34,18 @@ public class UpgradeActionRegistrar implements ProjectActivity { @Nullable @Override public Object execute(@NotNull Project project, @NotNull Continuation continuation) { - discoverAndRegisterAction(); + try{ + discoverAndRegisterAction(); + } catch (Throwable e) { + log.error("Failed to register Upgrade action in Copilot context menu.", e); + } return Unit.INSTANCE; } private void discoverAndRegisterAction() { // Only proceed if Copilot plugin is installed if (!AppModPluginInstaller.isCopilotInstalled()) { + log.info("GitHub Copilot plugin not installed; skipping UpgradeActionRegistrar."); return; } @@ -91,6 +98,7 @@ private void tryAddToGroup(ActionManager actionManager, DefaultActionGroup group group.add(Separator.create()); AppModUtils.logTelemetryEvent("java-upgrade.contextmenu.action.registered"); group.add(upgradeAction); + log.info("Registered Upgrade action into {}.", groupId); } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/dao/VulnerabilityInfo.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/dao/VulnerabilityInfo.java index 0910b62f7a5..ceb44ab7f47 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/dao/VulnerabilityInfo.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/dao/VulnerabilityInfo.java @@ -85,4 +85,4 @@ public static VulnerabilityInfo parseVulnerabilityDescription(@Nullable String d return new VulnerabilityInfo(groupId, artifactId, version, cveIds); } -} \ No newline at end of file +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java index 8725a9b5812..d998ed69044 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java @@ -20,6 +20,7 @@ import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService.*; +import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import java.util.List; @@ -31,6 +32,7 @@ * Note: Issues are cached at project startup via JavaUpgradeIssueCache to avoid * repeated expensive scans during inspection runs. */ +@Slf4j public class JavaUpgradeIssuesInspection extends LocalInspectionTool { @NotNull @@ -92,6 +94,7 @@ public void visitXmlTag(@NotNull XmlTag tag) { } private void registerProblem(@NotNull ProblemsHolder holder, @NotNull XmlTag tag, @NotNull JavaUpgradeIssue issue) { + log.info("Registering Java upgrade issue in inspection: {}", issue); holder.registerProblem( tag, issue.getMessage(), diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/CVECheckService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/CVECheckService.java index 87fd952036d..baed0899a2a 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/CVECheckService.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/CVECheckService.java @@ -14,6 +14,7 @@ import com.google.gson.JsonObject; import lombok.Builder; import lombok.Data; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.maven.artifact.versioning.ComparableVersion; @@ -36,6 +37,7 @@ * This implementation is aligned with the TypeScript version in vscode-java-dependency. * @see cve.ts */ +@Slf4j public class CVECheckService { private static final String GITHUB_API_BASE = "https://api.github.com/advisories"; @@ -131,9 +133,14 @@ private CVECheckService() { // Configure proxy using IntelliJ's JdkProxyProvider // This respects the IDE's proxy settings (Settings → HTTP Proxy) builder.proxy(JdkProxyProvider.getInstance().getProxySelector()); + } catch (Throwable e) { + log.error("Failed to configure proxy from IntelliJ proxy settings", e); + } + try{ builder.sslContext(CertificateManager.getInstance().getSslContext()); } catch (Throwable e) { // Failed to get IntelliJ SSL context, using default + log.error("Failed to configure HTTP client SSL context from IntelliJ CertificateManager, using default.", e); } this.httpClient = builder.build(); this.gson = new Gson(); @@ -164,7 +171,7 @@ public List batchGetCVEIssues(@Nonnull List coordinate final List batchIssues = getCveUpgradeIssues(batch); allIssues.addAll(batchIssues); } catch (Exception e) { - // Error checking CVEs for batch + log.error("Error fetching CVE issues for batch starting at index " + i, e); } } @@ -257,6 +264,7 @@ private List fetchCves(@Nonnull List deps) { return depsCves; } catch (Exception e) { // Error fetching CVEs + log.error("Error fetching CVEs from GitHub Security Advisory API", e); return Collections.emptyList(); } } @@ -327,6 +335,7 @@ private List retrieveVulnerabilityData(@Nonnull List } catch (Exception e) { // Error retrieving vulnerability data from GitHub + log.error("Error retrieving vulnerability data from GitHub Security Advisory API", e); return Collections.emptyList(); } } @@ -390,6 +399,7 @@ private List parseAdvisories(@Nonnull JsonArray advisories) { } catch (Exception e) { // Error parsing advisory JSON + log.error("Error parsing advisories JsonArray from GitHub Security Advisory API", e); } return cves; @@ -405,6 +415,7 @@ private List parseAdvisories(@Nonnull String jsonResponse) { return parseAdvisories(gson.fromJson(jsonResponse, JsonArray.class)); } catch (Exception e) { // Error parsing advisory JSON + log.error("Error parsing advisories JsonString from GitHub Security Advisory API", e); return Collections.emptyList(); } } @@ -534,6 +545,7 @@ private boolean isVersionInRange(@Nonnull String version, @Nullable String range return true; } catch (Exception e) { // Error checking version range + log.error("Error checking if version {} is in range {}", version, range, e); return false; } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesCache.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesCache.java index 87364af5f19..7cd57e86067 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesCache.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesCache.java @@ -10,10 +10,12 @@ import com.intellij.openapi.project.Project; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; +import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @@ -24,13 +26,14 @@ * Issues are computed once at startup and cached to avoid repeated expensive scans * during inspection runs. The cache can be invalidated when project model changes. */ +@Slf4j @Service(Service.Level.PROJECT) public final class JavaUpgradeIssuesCache implements Disposable { private final Project project; - private final AtomicReference> jdkIssuesCache = new AtomicReference<>(); - private final AtomicReference> dependencyIssuesCache = new AtomicReference<>(); - private final AtomicReference> cvesIssuesCache = new AtomicReference<>(); + private List jdkIssuesCache = new ArrayList<>(); + private List dependencyIssuesCache = new ArrayList<>(); + private List cvesIssuesCache = new ArrayList<>(); private final AtomicBoolean initialized = new AtomicBoolean(false); public JavaUpgradeIssuesCache(@NotNull Project project) { @@ -46,8 +49,7 @@ public static JavaUpgradeIssuesCache getInstance(@NotNull Project project) { */ @Nonnull public List getJdkIssues() { - List cached = jdkIssuesCache.get(); - return cached != null ? cached : Collections.emptyList(); + return jdkIssuesCache != null ? jdkIssuesCache : Collections.emptyList(); } /** @@ -55,8 +57,7 @@ public List getJdkIssues() { */ @Nonnull public List getDependencyIssues() { - List cached = dependencyIssuesCache.get(); - return cached != null ? cached : Collections.emptyList(); + return dependencyIssuesCache != null ? dependencyIssuesCache : Collections.emptyList(); } /** @@ -64,8 +65,7 @@ public List getDependencyIssues() { */ @Nonnull public List getCveIssues() { - List cached = cvesIssuesCache.get(); - return cached != null ? cached : Collections.emptyList(); + return cvesIssuesCache != null ? cvesIssuesCache : Collections.emptyList(); } /** @@ -123,30 +123,34 @@ public boolean isInitialized() { * This should be called at project startup and when the project model changes. */ public void refresh() { - if (project.isDisposed()) { - return; + try { + if (project.isDisposed()) { + return; + } + log.info("Refreshing Java upgrade issues cache for project: {}", project.getName()); + final JavaUpgradeIssuesDetectionService detectionService = JavaUpgradeIssuesDetectionService.getInstance(); + + // Scan for issues + jdkIssuesCache = detectionService.getJavaIssues(project); + log.info("Detected {} JDK issues", jdkIssuesCache.size()); + dependencyIssuesCache= detectionService.getDependencyIssues(project); + log.info("Detected {} dependency issues", dependencyIssuesCache.size()); + cvesIssuesCache = detectionService.getCVEIssues(project); + log.info("Detected {} CVE issues", cvesIssuesCache.size()); + + initialized.set(true); + } catch (Throwable e) { + log.error("Error refreshing Java upgrade issues cache for project: {}", project.getName(), e); } - - final JavaUpgradeIssuesDetectionService detectionService = JavaUpgradeIssuesDetectionService.getInstance(); - - // Scan for issues - List jdkIssues = detectionService.getJavaIssues(project); - List dependencyIssues = detectionService.getDependencyIssues(project); - List cveIssues = detectionService.getCVEIssues(project); - // Update cache - jdkIssuesCache.set(Collections.unmodifiableList(jdkIssues)); - dependencyIssuesCache.set(Collections.unmodifiableList(dependencyIssues)); - cvesIssuesCache.set(Collections.unmodifiableList(cveIssues)); - initialized.set(true); } /** * Invalidates the cache, forcing a refresh on next access. */ public void invalidate() { - jdkIssuesCache.set(null); - dependencyIssuesCache.set(null); - cvesIssuesCache.set(null); + jdkIssuesCache = null; + dependencyIssuesCache = null; + cvesIssuesCache = null; initialized.set(false); } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java index febd15764a6..d8499172a0b 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java @@ -11,6 +11,7 @@ import com.microsoft.azure.toolkit.intellij.common.utils.JdkUtils; import com.microsoft.intellij.util.GradleUtils; import com.microsoft.intellij.util.MavenUtils; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.maven.artifact.versioning.ComparableVersion; import org.jetbrains.idea.maven.model.MavenArtifact; @@ -28,7 +29,7 @@ import java.time.format.DateTimeFormatter; import java.util.*; -import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.ISSUE_DISPLAY_NAME; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils.Constants.ISSUE_DISPLAY_NAME; /** * Service to detect JDK version and framework dependency versions in Java projects. @@ -37,6 +38,7 @@ * This implementation is aligned with the TypeScript version in vscode-java-dependency. * @see assessmentManager.ts */ +@Slf4j public class JavaUpgradeIssuesDetectionService { /** @@ -215,17 +217,18 @@ public static synchronized JavaUpgradeIssuesDetectionService getInstance() { * Formats an EOL date from "yyyy-MM" format to "Month yyyy" format. * For example, "2020-06" becomes "June 2020". * - * @param eofDate The EOL date string in "yyyy-MM" format (e.g., "2020-06") + * @param eolDate The EOL date string in "yyyy-MM" format (e.g., "2020-06") * @return The formatted date string (e.g., "June 2020"), or the original string if parsing fails */ @Nonnull - public static String formatEofDate(@Nonnull String eofDate) { + public static String formatEolDate(@Nonnull String eolDate) { try { - YearMonth yearMonth = YearMonth.parse(eofDate, EOL_DATE_PARSER); + YearMonth yearMonth = YearMonth.parse(eolDate, EOL_DATE_PARSER); return yearMonth.format(EOL_DATE_DISPLAY); } catch (Exception e) { // If parsing fails, return the original string - return eofDate; + log.error("Error formatting EOL date '{}': {}", eolDate, e.getMessage()); + return eolDate; } } @@ -239,6 +242,7 @@ public List getJavaIssues(@Nonnull Project project) { try { final Integer jdkVersion = JdkUtils.getJdkLanguageLevel(project); + log.info("Got JDK version: {}", jdkVersion); AppModUtils.logTelemetryEvent("getJavaVersion", Map.of("jdkVersion", String.valueOf(jdkVersion))); if (jdkVersion == null) { return issues; @@ -246,6 +250,8 @@ public List getJavaIssues(@Nonnull Project project) { // Skip versions below 8 - out of scope if (jdkVersion < 8) { + AppModUtils.logTelemetryEvent("getJavaVersionSkipped", Map.of("jdkVersion", String.valueOf(jdkVersion))); + log.warn("JDK version below 8 detected ({}), skipping JDK upgrade check", jdkVersion); return issues; } @@ -265,6 +271,7 @@ public List getJavaIssues(@Nonnull Project project) { } } catch (Exception e) { // Error checking JDK version + log.error("Error checking JDK version: {}", e.getMessage(), e); } return issues; @@ -282,6 +289,7 @@ public List getDependencyIssues(@Nonnull Project project) { final Set checkedPackages = new HashSet<>(); if (MavenUtils.isMavenProject(project)) { + log.info("Checking Maven project dependencies for upgrade issues"); final MavenProjectsManager mavenProjectsManager = MavenProjectsManager.getInstanceIfCreated(project); if (mavenProjectsManager != null && mavenProjectsManager.isMavenizedProject()) { final List mavenProjects = mavenProjectsManager.getProjects(); @@ -300,6 +308,7 @@ public List getDependencyIssues(@Nonnull Project project) { } } } else if (GradleUtils.isGradleProject(project)) { + log.info("Checking Gradle project dependencies for upgrade issues"); final List gradleProjects = GradleUtils.listGradleProjects(project); for (ExternalProject gradleProject : gradleProjects) { for (DependencyCheckItem checkItem : DEPENDENCIES_TO_SCAN) { @@ -316,6 +325,7 @@ public List getDependencyIssues(@Nonnull Project project) { } catch (Exception e) { // Error checking dependencies + log.error("Error checking dependency issues: {}", e.getMessage(), e); } return issues; @@ -334,6 +344,7 @@ public List getCVEIssues(@Nonnull Project project) { final Set coordinateSet = new HashSet<>(); if (MavenUtils.isMavenProject(project)) { + log.info("Checking Maven project dependencies for CVE issues"); final MavenProjectsManager mavenProjectsManager = MavenProjectsManager.getInstanceIfCreated(project); if (mavenProjectsManager != null && mavenProjectsManager.isMavenizedProject()) { final List mavenProjects = mavenProjectsManager.getProjects(); @@ -349,6 +360,7 @@ public List getCVEIssues(@Nonnull Project project) { } } } else if (GradleUtils.isGradleProject(project)) { + log.info("Checking Gradle project dependencies for CVE issues"); final List gradleProjects = GradleUtils.listGradleProjects(project); for (ExternalProject gradleProject : gradleProjects) { final ExternalSourceSet main = gradleProject.getSourceSets().get("main"); @@ -369,10 +381,12 @@ public List getCVEIssues(@Nonnull Project project) { // Check CVEs for all collected dependencies final List coordinates = new ArrayList<>(coordinateSet); + log.info("Checking CVE issues for {} dependencies", coordinates.size()); return CVECheckService.getInstance().batchGetCVEIssues(coordinates); } catch (Exception e) { // Error checking CVE issues + log.error("Error checking CVE issues: {}", e.getMessage(), e); return Collections.emptyList(); } } @@ -408,7 +422,7 @@ private JavaUpgradeIssue checkDependency(@Nonnull MavenProject mavenProject, checkedPackages.add(checkItem.getPackageId()); // Check if version satisfies the supported version range - if (!satisfiesVersionRange(version, checkItem.supportedVersion) && isEOF(version, checkItem)) { + if (!satisfiesVersionRange(version, checkItem.supportedVersion) && isVersionEndOfLife(version, checkItem)) { return JavaUpgradeIssue.builder() .packageId(checkItem.getPackageId()) .packageDisplayName(checkItem.displayName) @@ -426,20 +440,6 @@ private JavaUpgradeIssue checkDependency(@Nonnull MavenProject mavenProject, return null; } - private boolean isEOF(String version, DependencyCheckItem checkItem) { - String eolDateStr = checkItem.getEolDateForVersion(version); - if (eolDateStr == null) { - return false; - } - try { - YearMonth eolDate = YearMonth.parse(eolDateStr, EOL_DATE_PARSER); - YearMonth currentDate = YearMonth.now(); - return currentDate.isAfter(eolDate); - } catch (Exception e) { - return false; - } - - } /** * Gets the version from parent POM. */ @@ -455,6 +455,8 @@ private String getParentVersion(@Nonnull MavenProject mavenProject, return parentId.getVersion(); } } catch (Exception e) { + // Error getting parent version + log.error("Error getting parent version for {}:{} - {}", groupId, artifactId, e.getMessage(), e); } return null; } @@ -519,6 +521,7 @@ private boolean satisfiesSingleCondition(@Nonnull String version, @Nonnull Strin } catch (Exception e) { // Error checking version range + log.error("Error checking version '{}' against condition '{}': {}", version, condition, e.getMessage(), e); return false; } } @@ -541,6 +544,7 @@ private boolean isVersionEndOfLife(@Nonnull String version, @Nonnull DependencyC java.time.YearMonth currentDate = java.time.YearMonth.now(); return currentDate.isAfter(eolDate); } catch (Exception e) { + log.error("Error parsing EOL date '{}': {}", eolDateStr, e.getMessage()); return false; } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java index cb0d2e78c28..71aab4abe3c 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaVersionNotificationService.java @@ -22,12 +22,13 @@ import com.microsoft.azure.toolkit.lib.common.task.AzureTaskManager; import static com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller.*; -import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.Contants.*; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils.Constants.*; import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService.*; import java.lang.reflect.Method; import kotlin.Unit; import kotlin.jvm.functions.Function1; +import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import com.github.copilot.api.CopilotChatService; import javax.annotation.Nonnull; @@ -40,6 +41,7 @@ * Notifications appear in the bottom-right corner of the IDE. * Only shows one notification at a time (the first detected issue). */ +@Slf4j public class JavaVersionNotificationService { private static final String NOTIFICATION_GROUP_ID = "Azure Toolkit - Java Version Check"; @@ -77,11 +79,13 @@ public void showNotifications(@Nonnull Project project, @Nonnull List AppModPluginInstaller.installPlugin(project, true)); - // notification.expire(); } }); } @@ -301,26 +303,32 @@ private void openCopilotChatWithUpgradePrompt(@Nonnull Project project, @Nonnull * @param prompt The prompt to send to Copilot */ public void openCopilotChatWithPrompt(@Nonnull Project project, @Nonnull String prompt) { - AzureTaskManager.getInstance().runLater(() -> { - if (!isAppModPluginInstalled()) { - // showGenericUpgradeGuidance(project, prompt); - AppModPluginInstaller.showInstallConfirmation(project, true, () -> AppModPluginInstaller.installPlugin(project, true)); - return; - } - - // Try direct API call first (works when plugin versions match) - if (tryDirectCopilotCall(project, prompt)) { - return; // Success, no need for reflection - } - - // Fallback to reflection for cross-version compatibility - if (tryReflectionCopilotCall(project, prompt)) { - return; // Success via reflection - } - - // Both approaches failed + try { + AzureTaskManager.getInstance().runLater(() -> { + if (!isAppModPluginInstalled()) { + // showGenericUpgradeGuidance(project, prompt); + AppModPluginInstaller.showInstallConfirmation(project, true, () -> AppModPluginInstaller.installPlugin(project, true)); + return; + } + +// //TODO Try direct API call first (works when plugin versions match) +// if (tryDirectCopilotCall(project, prompt)) { +// return; // Success, no need for reflection +// } + + // Fallback to reflection for cross-version compatibility + if (tryReflectionCopilotCall(project, prompt)) { + return; // Success via reflection + } + + // Both approaches failed + log.info("Failed to open Copilot chat via both direct and reflection methods."); + showGenericUpgradeGuidance(project, prompt); + }); + } catch (Exception e) { + log.error("Error opening Copilot chat: " + e.getMessage()); showGenericUpgradeGuidance(project, prompt); - }); + } } /** @@ -343,7 +351,7 @@ private boolean tryDirectCopilotCall(@Nonnull Project project, @Nonnull String p } } catch (Error | Exception e) { // Direct call failed (version mismatch, class not found, etc.) - will try reflection - System.out.println("Direct Copilot call failed: " + e.getMessage()); + log.info("Direct Copilot call failed: " + e.getMessage()); } return false; } @@ -390,6 +398,7 @@ private boolean tryReflectionCopilotCall(@Nonnull Project project, @Nonnull Stri } } catch (Exception ex) { // Error configuring query builder via reflection + log.error("Error configuring Copilot query via reflection: " + ex.getMessage()); } return Unit.INSTANCE; }; @@ -398,6 +407,7 @@ private boolean tryReflectionCopilotCall(@Nonnull Project project, @Nonnull Stri } } catch (Exception e) { // Reflection call failed + log.error("Reflection Copilot call failed: " + e.getMessage()); } return false; } @@ -434,6 +444,7 @@ private static void withModelCompatibility(Object builder, String modelName) { // Method withModel not found in QueryOptionBuilder, skipping } catch (Exception ex) { // Error calling withModel via reflection, can be ignored + log.error("Error setting model via reflection: " + ex.getMessage()); } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/Contants.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/utils/Constants.java similarity index 88% rename from PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/Contants.java rename to PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/utils/Constants.java index f5f37d64e60..78bca711e25 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/Contants.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/utils/Constants.java @@ -1,8 +1,6 @@ -package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade; +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils; -import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService.MATURE_JAVA_LTS_VERSION; - -public class Contants { +public class Constants { public static final String UPGRADE_JAVA_AND_FRAMEWORK_PROMPT = "Upgrade java runtime and java framework dependencies of this project to the latest LTS version using java upgrade tools by invoking #generate_upgrade_plan"; public static final String UPGRADE_JAVA_VERSION_PROMPT = "Upgrade Java runtime from version %s to Java %s (LTS) using java upgrade tools by invoking #generate_upgrade_plan"; public static final String UPGRADE_JAVA_FRAMEWORK_PROMPT = "Upgrade %s from version %s to %s using java upgrade tools by invoking #generate_upgrade_plan"; diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/utils/PomXmlUtils.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/utils/PomXmlUtils.java new file mode 100644 index 00000000000..b36c28e2c03 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/utils/PomXmlUtils.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Utility class for parsing and extracting information from pom.xml files. + */ +public final class PomXmlUtils { + + private static final int MAX_SEARCH_OFFSET = 500; + + private PomXmlUtils() { + // Utility class, no instantiation + } + + /** + * Finds the start of the dependency block containing the given offset. + * + * @param text the full text of the pom.xml file + * @param offset the current cursor offset + * @return the start index of the dependency block, or -1 if not found + */ + public static int findDependencyStart(@NotNull String text, int offset) { + // Look for tag before the offset + int searchStart = Math.max(0, offset - MAX_SEARCH_OFFSET); + String searchArea = text.substring(searchStart, offset); + int lastDependency = searchArea.lastIndexOf(""); + if (lastDependency >= 0) { + return searchStart + lastDependency; + } + return -1; + } + + /** + * Finds the end of the dependency block containing the given offset. + * + * @param text the full text of the pom.xml file + * @param offset the current cursor offset + * @return the end index of the dependency block (after closing tag), or -1 if not found + */ + public static int findDependencyEnd(@NotNull String text, int offset) { + // Look for tag after the offset + int searchEnd = Math.min(text.length(), offset + MAX_SEARCH_OFFSET); + String searchArea = text.substring(offset, searchEnd); + int endDependency = searchArea.indexOf(""); + if (endDependency >= 0) { + return offset + endDependency + "".length(); + } + return -1; + } + + /** + * Extracts a value from an XML tag. + * + * @param xml the XML string to search in + * @param tagName the name of the tag to extract the value from + * @return the value inside the tag, or null if not found + */ + @Nullable + public static String extractXmlValue(@NotNull String xml, @NotNull String tagName) { + final String startTag = "<" + tagName + ">"; + final String endTag = ""; + + int start = xml.indexOf(startTag); + if (start < 0) { + return null; + } + start += startTag.length(); + + int end = xml.indexOf(endTag, start); + if (end < 0) { + return null; + } + + return xml.substring(start, end).trim(); + } +} From b3c2a8847f0cdadc83c6401a5bfb2e1ee8f451c3 Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Fri, 30 Jan 2026 13:42:52 +0800 Subject: [PATCH 32/43] add more logs and add MigrationStatePreloader to pre load migration data --- .../appmod/common/AppModPluginInstaller.java | 32 +++- .../javamigration/MigrateToAzureAction.java | 164 ++++++++++++------ .../javamigration/MigrateToAzureNode.java | 77 +++++--- .../MigrationStatePreloader.java | 69 ++++++++ .../META-INF/azure-intellij-plugin-appmod.xml | 1 + .../MigrateToAzureFacetNode.java | 59 +++++-- 6 files changed, 302 insertions(+), 100 deletions(-) create mode 100644 PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrationStatePreloader.java diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java index 0e3f814b3b8..1683423bbd5 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/common/AppModPluginInstaller.java @@ -12,6 +12,7 @@ import com.intellij.openapi.ui.Messages; import com.intellij.openapi.updateSettings.impl.pluginsAdvertisement.PluginsAdvertiser; import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; +import lombok.extern.slf4j.Slf4j; import javax.annotation.Nonnull; import java.util.LinkedHashSet; @@ -23,6 +24,7 @@ * This centralizes all plugin detection and installation logic to avoid code duplication * between MigrateToAzureNode and MigrateToAzureAction. */ +@Slf4j public class AppModPluginInstaller { private static final String PLUGIN_ID = "com.github.copilot.appmod"; private static final String COPILOT_PLUGIN_ID = "com.github.copilot"; @@ -38,8 +40,11 @@ public static boolean isAppModPluginInstalled() { try { final PluginId pluginId = PluginId.getId(PLUGIN_ID); final IdeaPluginDescriptor plugin = PluginManagerCore.getPlugin(pluginId); - return plugin != null && plugin.isEnabled(); + final boolean result = plugin != null && plugin.isEnabled(); + log.debug("[AppModPluginInstaller] isAppModPluginInstalled: {}", result); + return result; } catch (Exception e) { + log.error("[AppModPluginInstaller] Failed to check AppMod plugin status", e); return false; } } @@ -51,8 +56,11 @@ public static boolean isCopilotInstalled() { try { final PluginId pluginId = PluginId.getId(COPILOT_PLUGIN_ID); final IdeaPluginDescriptor plugin = PluginManagerCore.getPlugin(pluginId); - return plugin != null && plugin.isEnabled(); + final boolean result = plugin != null && plugin.isEnabled(); + log.debug("[AppModPluginInstaller] isCopilotInstalled: {}", result); + return result; } catch (Exception e) { + log.error("[AppModPluginInstaller] Failed to check Copilot plugin status", e); return false; } } @@ -67,10 +75,12 @@ public static boolean isRunningInDevMode() { final IdeaPluginDescriptor descriptor = PluginManagerCore.getPlugin(azureToolkitId); if (descriptor != null) { final String path = descriptor.getPluginPath().toString(); - return path.contains("build") || path.contains("sandbox") || path.contains("out"); + final boolean result = path.contains("build") || path.contains("sandbox") || path.contains("out"); + log.debug("[AppModPluginInstaller] isRunningInDevMode: {}, path: {}", result, path); + return result; } } catch (Exception ex) { - // If we can't determine, assume production mode (safer) + log.error("[AppModPluginInstaller] Failed to check dev mode status", ex); } return false; } @@ -84,6 +94,8 @@ public static boolean isRunningInDevMode() { */ public static void showInstallConfirmation(@Nonnull Project project, boolean forUpgrade, @Nonnull Runnable onConfirm) { final boolean copilotInstalled = isCopilotInstalled(); + final String action = forUpgrade ? "upgrade" : "migration"; + log.debug("[AppModPluginInstaller] showInstallConfirmation - forUpgrade: {}, copilotInstalled: {}", forUpgrade, copilotInstalled); final String title = copilotInstalled ? "Install Github Copilot app modernization" @@ -99,13 +111,13 @@ public static void showInstallConfirmation(@Nonnull Project project, boolean for ? "To upgrade your apps, you'll need two plugins: GitHub Copilot and app modernization." : "To migrate to Azure, you'll need two plugins: GitHub Copilot and app modernization."; } - - final String action = forUpgrade ? "upgrade" : "migration"; if (Messages.showOkCancelDialog(project, message, title, "Install", "Cancel", Messages.getQuestionIcon()) == Messages.OK) { + log.info("[AppModPluginInstaller] User confirmed plugin installation for {}", action); AppModUtils.logTelemetryEvent("plugin." + action + ".install-confirmed"); onConfirm.run(); } else { + log.info("[AppModPluginInstaller] User cancelled plugin installation for {}", action); AppModUtils.logTelemetryEvent("plugin." + action + ".install-cancelled"); } } @@ -119,7 +131,9 @@ public static void showInstallConfirmation(@Nonnull Project project, boolean for * @param forUpgrade true for "upgrade" scenario, false for "migrate to Azure" scenario */ public static void installPlugin(@Nonnull Project project, boolean forUpgrade) { + log.debug("[AppModPluginInstaller] installPlugin - forUpgrade: {}", forUpgrade); if (isAppModPluginInstalled()) { + log.debug("[AppModPluginInstaller] installPlugin - skipping, already installed"); return; } @@ -128,13 +142,17 @@ public static void installPlugin(@Nonnull Project project, boolean forUpgrade) { pluginsToInstall.add(PluginId.getId(PLUGIN_ID)); final String source = forUpgrade ? "upgrade" : "migration"; + log.info("[AppModPluginInstaller] Starting plugin installation via PluginsAdvertiser"); PluginsAdvertiser.installAndEnable( project, pluginsToInstall, true, // showDialog true, // selectAllInDialog - pre-select all plugins null, // modalityState - () -> AppModUtils.logTelemetryEvent("plugin." + source + ".install-complete") + () -> { + log.info("[AppModPluginInstaller] Plugin installation completed"); + AppModUtils.logTelemetryEvent("plugin." + source + ".install-complete"); + } ); } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java index 31027677b61..ff469c78141 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureAction.java @@ -12,12 +12,14 @@ import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.DefaultActionGroup; import com.intellij.openapi.actionSystem.ex.ActionUtil; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Key; import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModPanelHelper; import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; +import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -33,16 +35,19 @@ * 2. Plugin installed but no migration options - shows "Open App Mod Panel" action * 3. Plugin installed with migration options - sub-menu shows migration options * - * Data is loaded once on first access and cached in Project.getUserData. + * Data is preloaded by MigrationStatePreloader when project opens. + * If data is not ready yet, shows "Open App Mod Panel" as fallback. */ +@Slf4j public class MigrateToAzureAction extends ActionGroup { private static final ExtensionPointName MIGRATION_PROVIDERS = ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateOptionProvider"); - private static final Key STATE_KEY = Key.create("azure.migrate.action.state"); + static final Key STATE_KEY = Key.create("azure.migrate.action.state"); + static final Key LOADING_KEY = Key.create("azure.migrate.action.loading"); - private enum State { NOT_INSTALLED, NO_OPTIONS, HAS_OPTIONS } + enum State { NOT_INSTALLED, LOADING, NO_OPTIONS, HAS_OPTIONS } - private static class MigrationState { + static class MigrationState { final State state; final List nodes; @@ -58,45 +63,82 @@ private static class MigrationState { } /** - * Gets or computes migration state for the project. - * State is cached in Project.getUserData and loaded once on first access. + * Gets migration state for the project. + * State is preloaded by MigrationStatePreloader on project open. + * Returns LOADING state if data is not ready yet, and triggers async loading. */ private MigrationState getOrComputeState(Project project) { + // Fast path: if plugin not installed, return immediately + if (!AppModPluginInstaller.isAppModPluginInstalled()) { + log.debug("[MigrateToAzureAction] Plugin not installed, returning NOT_INSTALLED"); + return new MigrationState(State.NOT_INSTALLED, List.of()); + } + + // Check if we have preloaded state MigrationState state = project.getUserData(STATE_KEY); - if (state == null) { - state = computeState(project); - project.putUserData(STATE_KEY, state); + if (state != null) { + return state; } - return state; + + // State not ready yet - trigger async loading if not already loading + Boolean isLoading = project.getUserData(LOADING_KEY); + if (!Boolean.TRUE.equals(isLoading)) { + project.putUserData(LOADING_KEY, Boolean.TRUE); + log.debug("[MigrateToAzureAction] State not ready, triggering async load"); + ApplicationManager.getApplication().executeOnPooledThread(() -> { + try { + final MigrationState computedState = computeState(project); + if (computedState != null) { + project.putUserData(STATE_KEY, computedState); + log.debug("[MigrateToAzureAction] Async load completed, state: {}", computedState.state); + } + } finally { + project.putUserData(LOADING_KEY, Boolean.FALSE); + } + }); + } + + log.debug("[MigrateToAzureAction] State not ready yet, returning LOADING"); + return new MigrationState(State.LOADING, List.of()); } /** * Computes migration state by calling providers. + * Called by MigrationStatePreloader during project startup. */ - private MigrationState computeState(Project project) { - if (!AppModPluginInstaller.isAppModPluginInstalled()) { - return new MigrationState(State.NOT_INSTALLED, List.of()); - } - - final List nodes = MIGRATION_PROVIDERS.getExtensionList().stream() - .filter(provider -> provider.isApplicable(project)) - .sorted(Comparator.comparingInt(IMigrateOptionProvider::getPriority)) - .flatMap(provider -> provider.createNodeData(project).stream()) - .filter(MigrateNodeData::isVisible) - .collect(Collectors.toList()); - - if (nodes.isEmpty()) { - AppModUtils.logTelemetryEvent("action.no-tasks"); + static MigrationState computeState(Project project) { + final long startTime = System.currentTimeMillis(); + log.debug("[MigrateToAzureAction] computeState - start"); + try { + final long providerStartTime = System.currentTimeMillis(); + final List nodes = MIGRATION_PROVIDERS.getExtensionList().stream() + .filter(provider -> provider.isApplicable(project)) + .sorted(Comparator.comparingInt(IMigrateOptionProvider::getPriority)) + .flatMap(provider -> provider.createNodeData(project).stream()) + .filter(MigrateNodeData::isVisible) + .collect(Collectors.toList()); + + log.debug("[MigrateToAzureAction] computeState - loaded {} nodes, provider call took {}ms, total {}ms", + nodes.size(), System.currentTimeMillis() - providerStartTime, System.currentTimeMillis() - startTime); + + if (nodes.isEmpty()) { + AppModUtils.logTelemetryEvent("action.no-tasks"); + } + + return new MigrationState( + nodes.isEmpty() ? State.NO_OPTIONS : State.HAS_OPTIONS, + nodes + ); + } catch (Exception e) { + log.error("[MigrateToAzureAction] Failed to compute migration state, took {}ms", System.currentTimeMillis() - startTime, e); + // Return null to indicate failure - caller should not cache this result + return null; } - - return new MigrationState( - nodes.isEmpty() ? State.NO_OPTIONS : State.HAS_OPTIONS, - nodes - ); } @Override public void update(@NotNull AnActionEvent e) { + final long startTime = System.currentTimeMillis(); final Project project = e.getProject(); if (project == null) { e.getPresentation().setEnabledAndVisible(false); @@ -104,6 +146,7 @@ public void update(@NotNull AnActionEvent e) { } final MigrationState migrationState = getOrComputeState(project); + log.debug("[MigrateToAzureAction] update - state: {}, took {}ms", migrationState.state, System.currentTimeMillis() - startTime); // Common settings for all states e.getPresentation().setPopupGroup(true); @@ -118,6 +161,7 @@ public void update(@NotNull AnActionEvent e) { e.getPresentation().setPerformGroup(true); e.getPresentation().putClientProperty(ActionUtil.SUPPRESS_SUBMENU, true); break; + case LOADING: case NO_OPTIONS: e.getPresentation().setText("Migrate to Azure (Open GitHub Copilot app modernization)"); e.getPresentation().setPerformGroup(true); @@ -140,14 +184,18 @@ public void actionPerformed(@NotNull AnActionEvent e) { } final MigrationState migrationState = getOrComputeState(project); + log.debug("[MigrateToAzureAction] actionPerformed - state: {}", migrationState.state); switch (migrationState.state) { case NOT_INSTALLED: + log.info("[MigrateToAzureAction] Install click triggered"); AppModUtils.logTelemetryEvent("action.click-install"); AppModPluginInstaller.showInstallConfirmation(project, false, () -> AppModPluginInstaller.installPlugin(project, false)); break; + case LOADING: case NO_OPTIONS: + log.info("[MigrateToAzureAction] Opening AppMod panel (state: {})", migrationState.state); AppModPanelHelper.openAppModPanel(project, "action"); break; case HAS_OPTIONS: @@ -158,24 +206,33 @@ public void actionPerformed(@NotNull AnActionEvent e) { @Override public AnAction @NotNull [] getChildren(@Nullable AnActionEvent e) { - if (e == null) { + final long startTime = System.currentTimeMillis(); + try { + if (e == null) { + return AnAction.EMPTY_ARRAY; + } + + final Project project = e.getProject(); + if (project == null) { + return AnAction.EMPTY_ARRAY; + } + + final MigrationState migrationState = getOrComputeState(project); + + if (migrationState.state == State.HAS_OPTIONS) { + final AnAction[] result = migrationState.nodes.stream() + .map(this::convertNodeToAction) + .toArray(AnAction[]::new); + log.debug("[MigrateToAzureAction] getChildren - returned {} actions, took {}ms", result.length, System.currentTimeMillis() - startTime); + return result; + } + + log.debug("[MigrateToAzureAction] getChildren - no options, took {}ms", System.currentTimeMillis() - startTime); return AnAction.EMPTY_ARRAY; - } - - final Project project = e.getProject(); - if (project == null) { + } catch (Exception ex) { + log.error("[MigrateToAzureAction] Failed to get children, took {}ms", System.currentTimeMillis() - startTime, ex); return AnAction.EMPTY_ARRAY; } - - final MigrationState migrationState = getOrComputeState(project); - - if (migrationState.state == State.HAS_OPTIONS) { - return migrationState.nodes.stream() - .map(this::convertNodeToAction) - .toArray(AnAction[]::new); - } - - return AnAction.EMPTY_ARRAY; } private AnAction convertNodeToAction(MigrateNodeData nodeData) { @@ -183,14 +240,21 @@ private AnAction convertNodeToAction(MigrateNodeData nodeData) { final DefaultActionGroup subgroup = new DefaultActionGroup(nodeData.getLabel(), true); subgroup.getTemplatePresentation().setIcon(AllIcons.Vcs.Changelist); - final List children = nodeData.isLazyLoading() - ? nodeData.getChildrenLoader().get() - : nodeData.getChildren(); + try { + final long loadStartTime = System.currentTimeMillis(); + final List children = nodeData.isLazyLoading() + ? nodeData.getChildrenLoader().get() + : nodeData.getChildren(); + log.debug("[MigrateToAzureAction] convertNodeToAction - loaded {} children for '{}', lazy={}, took {}ms", + children.size(), nodeData.getLabel(), nodeData.isLazyLoading(), System.currentTimeMillis() - loadStartTime); - for (MigrateNodeData child : children) { - if (child.isVisible()) { - subgroup.add(convertNodeToAction(child)); + for (MigrateNodeData child : children) { + if (child.isVisible()) { + subgroup.add(convertNodeToAction(child)); + } } + } catch (Exception e) { + log.error("[MigrateToAzureAction] Failed to load children for node: {}", nodeData.getLabel(), e); } return subgroup; } else { diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java index 6f759f5affb..17c919f0f80 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrateToAzureNode.java @@ -7,6 +7,7 @@ import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.project.Project; +import lombok.extern.slf4j.Slf4j; import com.microsoft.azure.toolkit.ide.common.component.Node; import com.microsoft.azure.toolkit.ide.common.icon.AzureIcon; import com.microsoft.azure.toolkit.ide.common.icon.AzureIcons; @@ -28,6 +29,7 @@ * * State is computed on initialization and can be refreshed via refresh() method. */ +@Slf4j public final class MigrateToAzureNode extends Node { private static final ExtensionPointName childProviders = ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateOptionProvider"); @@ -40,6 +42,7 @@ public final class MigrateToAzureNode extends Node { public MigrateToAzureNode(Project project) { super("Migrate to Azure"); this.project = project; + log.debug("[MigrateToAzureNode] Creating node for project: {}", project.getName()); withIcon(APP_MOD_ICON); // Add refresh action to context menu (only once in constructor) @@ -58,6 +61,7 @@ public MigrateToAzureNode(Project project) { } public void initializeNode() { + log.debug("[MigrateToAzureNode] initializeNode - appModInstalled: {}", AppModPluginInstaller.isAppModPluginInstalled()); // Clear previous state clearClickHandlers(); withDescription(""); @@ -74,6 +78,7 @@ public void initializeNode() { * Called by RefreshMigrateToAzureAction from context menu. */ public void refresh() { + log.debug("[MigrateToAzureNode] refresh called"); AppModUtils.logTelemetryEvent("node.refresh"); refreshChildren(); // This rebuilds children from addChildren function } @@ -84,6 +89,7 @@ public Project getProject() { private void showNotInstalled() { final boolean copilotInstalled = AppModPluginInstaller.isCopilotInstalled(); + log.debug("[MigrateToAzureNode] showNotInstalled - copilotInstalled: {}", copilotInstalled); // Dynamic description based on what needs to be installed final String description = copilotInstalled @@ -92,6 +98,7 @@ private void showNotInstalled() { withDescription(description); onClicked(e -> { + log.info("[MigrateToAzureNode] Install click triggered"); AppModUtils.logTelemetryEvent("node.click-install"); AppModPluginInstaller.showInstallConfirmation(project, false, () -> AppModPluginInstaller.installPlugin(project, false)); }); @@ -101,16 +108,22 @@ private void showNotInstalled() { * Load migration options from extension points. */ private List loadMigrationNodeData() { - final List nodes = childProviders.getExtensionList().stream() - .filter(provider -> provider.isApplicable(project)) - .sorted(Comparator.comparingInt(IMigrateOptionProvider::getPriority)) - .flatMap(provider -> provider.createNodeData(project).stream()) - .filter(MigrateNodeData::isVisible) - .collect(Collectors.toList()); - if (nodes.isEmpty()) { - AppModUtils.logTelemetryEvent("node.no-tasks"); + log.debug("[MigrateToAzureNode] loadMigrationNodeData - loading extension points"); + try { + final List nodes = childProviders.getExtensionList().stream() + .filter(provider -> provider.isApplicable(project)) + .sorted(Comparator.comparingInt(IMigrateOptionProvider::getPriority)) + .flatMap(provider -> provider.createNodeData(project).stream()) + .filter(MigrateNodeData::isVisible) + .collect(Collectors.toList()); + if (nodes.isEmpty()) { + AppModUtils.logTelemetryEvent("node.no-tasks"); + } + return nodes; + } catch (Exception e) { + log.error("[MigrateToAzureNode] Failed to load migration node data", e); + return List.of(); } - return nodes; } /** @@ -118,26 +131,36 @@ private List loadMigrationNodeData() { * Also updates description and click handler based on data. */ private List> buildChildNodes() { - if (!AppModPluginInstaller.isAppModPluginInstalled()) { + log.debug("[MigrateToAzureNode] buildChildNodes - appModInstalled: {}", AppModPluginInstaller.isAppModPluginInstalled()); + try { + if (!AppModPluginInstaller.isAppModPluginInstalled()) { + log.debug("[MigrateToAzureNode] buildChildNodes - returning empty (plugin not installed)"); + return List.of(); + } + + final List nodeDataList = loadMigrationNodeData(); + log.debug("[MigrateToAzureNode] buildChildNodes - loaded {} nodes", nodeDataList.size()); + + // Update description and click handler based on data + clearClickHandlers(); + if (nodeDataList.isEmpty()) { + log.debug("[MigrateToAzureNode] buildChildNodes - no migration options, setting click to open panel"); + withDescription("Open GitHub Copilot app modernization"); + onClicked(e -> { + log.info("[MigrateToAzureNode] Opening AppMod panel (no options)"); + AppModPanelHelper.openAppModPanel(project, "node"); + }); + } else { + withDescription(""); + } + + return nodeDataList.stream() + .map(this::convertToNode) + .collect(Collectors.toList()); + } catch (Exception e) { + log.error("[MigrateToAzureNode] Failed to build child nodes", e); return List.of(); } - - final List nodeDataList = loadMigrationNodeData(); - - // Update description and click handler based on data - clearClickHandlers(); - if (nodeDataList.isEmpty()) { - withDescription("Open GitHub Copilot app modernization"); - onClicked(e -> { - AppModPanelHelper.openAppModPanel(project, "node"); - }); - } else { - withDescription(""); - } - - return nodeDataList.stream() - .map(this::convertToNode) - .collect(Collectors.toList()); } /** diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrationStatePreloader.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrationStatePreloader.java new file mode 100644 index 00000000000..d98caaa5c56 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javamigration/MigrationStatePreloader.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javamigration; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.startup.ProjectActivity; +import com.microsoft.azure.toolkit.intellij.appmod.common.AppModPluginInstaller; +import kotlin.Unit; +import kotlin.coroutines.Continuation; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Preloads migration state when project opens. + * This ensures that when user opens the context menu, the data is already available. + */ +@Slf4j +public class MigrationStatePreloader implements ProjectActivity { + + @Nullable + @Override + public Object execute(@NotNull Project project, @NotNull Continuation continuation) { + // Only preload if AppMod plugin is installed + if (!AppModPluginInstaller.isAppModPluginInstalled()) { + log.info("[MigrationStatePreloader] AppMod plugin not installed, skipping preload"); + return Unit.INSTANCE; + } + + // Check if already loading or loaded + if (Boolean.TRUE.equals(project.getUserData(MigrateToAzureAction.LOADING_KEY)) || + project.getUserData(MigrateToAzureAction.STATE_KEY) != null) { + log.debug("[MigrationStatePreloader] Already loading or loaded, skipping"); + return Unit.INSTANCE; + } + + // Mark as loading to prevent duplicate loading from MigrateToAzureAction + project.putUserData(MigrateToAzureAction.LOADING_KEY, Boolean.TRUE); + log.info("[MigrationStatePreloader] Starting preload for project: {}", project.getName()); + + // Load in background thread + ApplicationManager.getApplication().executeOnPooledThread(() -> { + try { + final long startTime = System.currentTimeMillis(); + final MigrateToAzureAction.MigrationState state = MigrateToAzureAction.computeState(project); + // Only cache if computation succeeded (state != null) + // If failed (e.g., MCP server error), leave cache empty so next access will retry + if (state != null) { + project.putUserData(MigrateToAzureAction.STATE_KEY, state); + log.info("[MigrationStatePreloader] Preload completed for project: {}, state: {}, took {}ms", + project.getName(), state.state, System.currentTimeMillis() - startTime); + } else { + log.warn("[MigrationStatePreloader] Preload failed for project: {}, will retry on next access", + project.getName()); + } + } catch (Exception e) { + log.error("[MigrationStatePreloader] Preload failed for project: {}", project.getName(), e); + } finally { + project.putUserData(MigrateToAzureAction.LOADING_KEY, Boolean.FALSE); + } + }); + + return Unit.INSTANCE; + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml index 52ef298c447..29c2ad3de56 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml @@ -6,6 +6,7 @@ + XML diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java index a425319943c..efc9017de65 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/MigrateToAzureFacetNode.java @@ -22,6 +22,7 @@ import com.microsoft.azure.toolkit.lib.common.action.Action; import com.microsoft.azure.toolkit.lib.common.action.ActionGroup; import com.microsoft.azure.toolkit.lib.common.action.IActionGroup; +import lombok.extern.slf4j.Slf4j; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -37,6 +38,7 @@ * * State is computed on initialization and refreshed when buildChildren() is called (via tree refresh). */ +@Slf4j public class MigrateToAzureFacetNode extends AbstractAzureFacetNode { private static final ExtensionPointName migrationProviders = ExtensionPointName.create("com.microsoft.tooling.msservices.intellij.azure.migrateOptionProvider"); @@ -46,6 +48,7 @@ public class MigrateToAzureFacetNode extends AbstractAzureFacetNode public MigrateToAzureFacetNode(Project project, AzureModule module) { super(project, module); + log.debug("[MigrateToAzureFacetNode] Creating node for project: {}", project.getName()); // Don't compute in constructor - use lazy loading } @@ -54,6 +57,7 @@ public MigrateToAzureFacetNode(Project project, AzureModule module) { */ private List getMigrationNodes() { if (migrationNodes == null) { + log.debug("[MigrateToAzureFacetNode] getMigrationNodes - computing (first access)"); migrationNodes = computeMigrationNodes(); } return migrationNodes; @@ -63,32 +67,49 @@ private List getMigrationNodes() { * Computes migration nodes from extension point providers. */ private List computeMigrationNodes() { - if (!AppModPluginInstaller.isAppModPluginInstalled()) { + log.debug("[MigrateToAzureFacetNode] computeMigrationNodes - appModInstalled: {}", AppModPluginInstaller.isAppModPluginInstalled()); + try { + if (!AppModPluginInstaller.isAppModPluginInstalled()) { + return List.of(); + } + final List nodes = migrationProviders.getExtensionList().stream() + .filter(provider -> provider.isApplicable(getProject())) + .sorted(Comparator.comparingInt(IMigrateOptionProvider::getPriority)) + .flatMap(provider -> provider.createNodeData(getProject()).stream()) + .filter(MigrateNodeData::isVisible) + .collect(Collectors.toList()); + log.debug("[MigrateToAzureFacetNode] computeMigrationNodes - loaded {} nodes", nodes.size()); + if (nodes.isEmpty()) { + AppModUtils.logTelemetryEvent("facet.no-tasks"); + } + return nodes; + } catch (Exception e) { + log.error("[MigrateToAzureFacetNode] Failed to compute migration nodes", e); return List.of(); } - final List nodes = migrationProviders.getExtensionList().stream() - .filter(provider -> provider.isApplicable(getProject())) - .sorted(Comparator.comparingInt(IMigrateOptionProvider::getPriority)) - .flatMap(provider -> provider.createNodeData(getProject()).stream()) - .filter(MigrateNodeData::isVisible) - .collect(Collectors.toList()); - if (nodes.isEmpty()) { - AppModUtils.logTelemetryEvent("facet.no-tasks"); - } - return nodes; } /** * Checks if there are any visible migration options available. + * Only returns true if data has already been loaded (non-blocking). */ private boolean hasMigrationOptions() { - return !getMigrationNodes().isEmpty(); + // Only check cached data - don't trigger loading on UI thread + return migrationNodes != null && !migrationNodes.isEmpty(); + } + + /** + * Checks if migration nodes have been loaded. + */ + private boolean isMigrationNodesLoaded() { + return migrationNodes != null; } /** * Refreshes migration nodes and updates the tree view. */ public void refresh() { + log.debug("[MigrateToAzureFacetNode] refresh called"); migrationNodes = null; // Clear cached data to force recompute updateChildren(); // This also refreshes the view } @@ -129,31 +150,37 @@ protected void buildView(@Nonnull PresentationData presentation) { ? "Migrate to Azure (Install Github Copilot app modernization)" : "Migrate to Azure (Install GitHub Copilot and app modernization)"; presentation.addText(text, com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); - } else if (!hasMigrationOptions()) { + } else if (isMigrationNodesLoaded() && !hasMigrationOptions()) { + // Only show "Open..." if we've already loaded and found no options presentation.addText("Migrate to Azure", com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); presentation.setLocationString("Open GitHub Copilot app modernization"); } else { + // Default state or has options presentation.addText("Migrate to Azure", com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES); } } @Override public void navigate(boolean requestFocus) { + log.debug("[MigrateToAzureFacetNode] navigate - appModInstalled: {}, hasMigrationOptions: {}", + AppModPluginInstaller.isAppModPluginInstalled(), hasMigrationOptions()); if (!AppModPluginInstaller.isAppModPluginInstalled()) { // Plugin not installed - trigger install on double-click + log.info("[MigrateToAzureFacetNode] Install click triggered"); AppModUtils.logTelemetryEvent("facet.click-install"); AppModPluginInstaller.showInstallConfirmation(getProject(), false, () -> AppModPluginInstaller.installPlugin(getProject(), false)); - } else if (!hasMigrationOptions()) { + } else if (isMigrationNodesLoaded() && !hasMigrationOptions()) { // No migration options - open App Modernization Panel + log.info("[MigrateToAzureFacetNode] Opening AppMod panel (no options)"); AppModPanelHelper.openAppModPanel(getProject(), "facet"); } } @Override public boolean canNavigate() { - // Enable navigation when plugin is not installed OR when no migration options - return !AppModPluginInstaller.isAppModPluginInstalled() || !hasMigrationOptions(); + // Enable navigation when plugin is not installed OR when loaded and no migration options + return !AppModPluginInstaller.isAppModPluginInstalled() || (isMigrationNodesLoaded() && !hasMigrationOptions()); } @Override From f272480b1c443644c184c45592a268071494c345 Mon Sep 17 00:00:00 2001 From: Jimmy Fang Date: Fri, 30 Jan 2026 14:28:28 +0800 Subject: [PATCH 33/43] use correct parameter to avoid error log --- .../projectexplorer/AzureFacetTreeStructureProvider.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/AzureFacetTreeStructureProvider.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/AzureFacetTreeStructureProvider.java index 0a96d288749..59fd6ad9e64 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/AzureFacetTreeStructureProvider.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-resource-connector-lib/src/main/java/com/microsoft/azure/toolkit/intellij/connector/projectexplorer/AzureFacetTreeStructureProvider.java @@ -179,12 +179,12 @@ private void updatePopupMenuActions(final IAzureFacetNode node) { final ActionManager manager = ActionManager.getInstance(); final DefaultActionGroup popupMenu = (DefaultActionGroup) manager.getAction("ProjectViewPopupMenu"); if (this.currentNode == null && CollectionUtils.isEmpty(backupActions)) { - this.backupActions = Arrays.stream(popupMenu.getChildren((AnActionEvent) null)).collect(Collectors.toList()); + this.backupActions = Arrays.stream(popupMenu.getChildren(manager)).collect(Collectors.toList()); } final List actions = Optional.ofNullable(node.getActionGroup()).map(IActionGroup::getActions).orElse(Collections.emptyList()); final IntellijAzureActionManager.ActionGroupWrapper wrapper = new IntellijAzureActionManager.ActionGroupWrapper(new ActionGroup(actions)); popupMenu.removeAll(); - Arrays.stream(wrapper.getChildren((AnActionEvent) null)).forEach(popupMenu::add); + Arrays.stream(wrapper.getChildren(manager)).forEach(popupMenu::add); } } From 607eaa50114e50e9a237602e2f00b29d46ed17ef Mon Sep 17 00:00:00 2001 From: wangmi Date: Fri, 30 Jan 2026 17:10:02 +0800 Subject: [PATCH 34/43] bump version and update changelog. --- CHANGELOG.md | 11 +++++++++++ .../AddLibrary/AzureLibraries/pom.xml | 4 ++-- .../azure-intellij-plugin-base/gradle.properties | 2 +- .../src/main/resources/META-INF/plugin.xml | 2 +- .../src/main/resources/whatsnew.md | 10 ++++++++++ .../azure-toolkit-for-intellij/gradle.properties | 2 +- .../src/main/resources/META-INF/plugin.xml | 16 ++++++++++++---- Utils/pom.xml | 2 +- 8 files changed, 39 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b45cceea382..e64d3f283cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to "Azure Toolkit for IntelliJ IDEA" will be documented in this file. - [Change Log](#change-log) + - [3.97.0](#3970) - [3.96.2](#3962) - [3.96.1](#3961) - [3.95.0](#3950) @@ -119,6 +120,16 @@ All notable changes to "Azure Toolkit for IntelliJ IDEA" will be documented in t - [3.0.7](#307) - [3.0.6](#306) +## 3.97.0 +### Added +- Integrate GitHub Copilot app modernization with "Migrate to Azure" entry points + - New "Migrate to Azure" node under Azure root node in Service Explorer + - New "Migrate to Azure" action in project/module right-click context menu + - New "Migrate to Azure" node under Azure facet in Project Explorer + - Auto-detection and installation prompt for GitHub Copilot app modernization plugin +- Detect outdated Java/framework and suggest upgrades with "GitHub Copilot app modernization" +- Detect CVEs in Java project dependencies and suggest upgrades with "GitHub Copilot app modernization" + ## 3.96.3 - Support IntelliJ 2025.3 EAP - Update the Azure MCP server name from releases list diff --git a/PluginsAndFeatures/AddLibrary/AzureLibraries/pom.xml b/PluginsAndFeatures/AddLibrary/AzureLibraries/pom.xml index 63de620373e..d8fd5f1bc84 100644 --- a/PluginsAndFeatures/AddLibrary/AzureLibraries/pom.xml +++ b/PluginsAndFeatures/AddLibrary/AzureLibraries/pom.xml @@ -27,7 +27,7 @@ com.microsoft.azuretools utils - 3.96.3 + 3.97.0 com.microsoft.azuretools com.microsoft.azuretools.sdk.lib @@ -39,7 +39,7 @@ - 3.96.3 + 3.97.0 3.32.0.qualifier 0.52.2 diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-base/gradle.properties b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-base/gradle.properties index d6b93f31e83..17840b56d91 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-base/gradle.properties +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-base/gradle.properties @@ -1,4 +1,4 @@ -pluginVersion=3.96.3 +pluginVersion=3.97.0 intellijDisplayVersion=2024.2 intellij_version=IU-242-EAP-SNAPSHOT scala_plugin=org.intellij.scala:2024.2.5 diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-base/src/main/resources/META-INF/plugin.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-base/src/main/resources/META-INF/plugin.xml index 5f24c40ff44..1f3d0061605 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-base/src/main/resources/META-INF/plugin.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-base/src/main/resources/META-INF/plugin.xml @@ -2,7 +2,7 @@ xmlns:xi="http://www.w3.org/2001/XInclude"> com.microsoft.tooling.msservices.intellij.azure Azure Toolkit - 3.96.3 + 3.97.0 Microsoft diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib-java/src/main/resources/whatsnew.md b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib-java/src/main/resources/whatsnew.md index 340650cb301..b28b21f8c33 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib-java/src/main/resources/whatsnew.md +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib-java/src/main/resources/whatsnew.md @@ -1,6 +1,16 @@ # What's new in Azure Toolkit for IntelliJ +## 3.97.0 +### Added +- Integrate GitHub Copilot app modernization with "Migrate to Azure" entry points + - New "Migrate to Azure" node under Azure root node in Service Explorer + - New "Migrate to Azure" action in project/module right-click context menu + - New "Migrate to Azure" node under Azure facet in Project Explorer + - Auto-detection and installation prompt for GitHub Copilot app modernization plugin +- Detect outdated Java/framework and suggest upgrades with "GitHub Copilot app modernization" +- Detect CVEs in Java project dependencies and suggest upgrades with "GitHub Copilot app modernization" + ## 3.96.3 - Support IntelliJ 2025.3 EAP - Update the Azure MCP server name from releases list diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties index c608c642f3c..f86fcc66e8f 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties @@ -1,4 +1,4 @@ -pluginVersion=3.96.3 +pluginVersion=3.97.0 intellijDisplayVersion=2025.3 intellij_version=253-EAP-SNAPSHOT platformVersion=253-EAP-SNAPSHOT diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/src/main/resources/META-INF/plugin.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/src/main/resources/META-INF/plugin.xml index b221ed6cb3b..9498afff0b8 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/src/main/resources/META-INF/plugin.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/src/main/resources/META-INF/plugin.xml @@ -2,7 +2,7 @@ xmlns:xi="http://www.w3.org/2001/XInclude"> com.microsoft.tooling.msservices.intellij.azure Azure Toolkit for IntelliJ - 3.96.3 + 3.97.0 Microsoft -

3.96.3

+

3.97.0

+

Added

    -
  • Support IntelliJ 2025.3 EAP.
  • -
  • Update the Azure MCP server name from releases list.
  • +
  • Integrate GitHub Copilot app modernization with "Migrate to Azure" entry points
      +
    • New "Migrate to Azure" node under Azure root node in Service Explorer
    • +
    • New "Migrate to Azure" action in project/module right-click context menu
    • +
    • New "Migrate to Azure" node under Azure facet in Project Explorer
    • +
    • Auto-detection and installation prompt for GitHub Copilot app modernization plugin
    • +
    +
  • +
  • Detect outdated Java/framework and suggest upgrades with "GitHub Copilot app modernization"
  • +
  • Detect CVEs in Java project dependencies and suggest upgrades with "GitHub Copilot app modernization"

You may get the full change log here

diff --git a/Utils/pom.xml b/Utils/pom.xml index 83c266906e6..45110b7a5aa 100644 --- a/Utils/pom.xml +++ b/Utils/pom.xml @@ -27,7 +27,7 @@ 4.0.0 com.microsoft.azuretools utils - 3.96.3 + 3.97.0 pom ${project.artifactId}-${project.version} From 230a5cc4e6624a6fe2b8ddea0f2ae3f36a8fac16 Mon Sep 17 00:00:00 2001 From: Miller Wang Date: Mon, 9 Mar 2026 10:06:23 +0800 Subject: [PATCH 35/43] Support IntelliJ IDEA 2026.1 EAP Co-Authored-By: Claude Opus 4.6 --- .../azure-toolkit-for-intellij/gradle.properties | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties index 17188bac220..f4bffe8450a 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties @@ -1,12 +1,12 @@ pluginVersion=3.97.2 -intellijDisplayVersion=2025.3 -intellij_version=253-EAP-SNAPSHOT -platformVersion=253-EAP-SNAPSHOT +intellijDisplayVersion=2026.1 +intellij_version=261-EAP-SNAPSHOT +platformVersion=261-EAP-SNAPSHOT # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html -pluginSinceBuild=253 -pluginUntilBuild=253.* +pluginSinceBuild=261 +pluginUntilBuild=261.* # Example: platformPlugins = com.jetbrains.php:203.4449.22, org.intellij.scala:2023.3.27@EAP -platformPlugins=org.intellij.scala:2025.3.12 +platformPlugins=org.intellij.scala:2026.1.8 # IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension platformType=IU needPatchVersion=true From 3d1f3320ed04a8eabd5dda9ba4759c163885a3b0 Mon Sep 17 00:00:00 2001 From: Miller Wang Date: Mon, 9 Mar 2026 11:11:49 +0800 Subject: [PATCH 36/43] Fix: Upgrade Gradle to 9.0 and IntelliJ Platform Plugin to 2.12.0 for EAP support --- .azure-pipelines/build_and_sign.yml | 212 ++++++++++++++++++ CLAUDE.md | 125 +++++++++++ .../gradle.properties | 2 +- .../gradle/libs.versions.toml | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 43462 -> 43453 bytes gradlew | 2 +- gradlew.bat | 20 +- 8 files changed, 351 insertions(+), 14 deletions(-) create mode 100644 .azure-pipelines/build_and_sign.yml create mode 100644 CLAUDE.md diff --git a/.azure-pipelines/build_and_sign.yml b/.azure-pipelines/build_and_sign.yml new file mode 100644 index 00000000000..dcd24963caf --- /dev/null +++ b/.azure-pipelines/build_and_sign.yml @@ -0,0 +1,212 @@ +name: $(Date:yyyyMMdd).$(Rev:r) +variables: + - name: Codeql.Enabled + value: true + - name: IsStableBuild + value: ${{ or(eq(variables['Build.SourceBranch'], 'refs/heads/main'), startsWith(variables['Build.SourceBranch'], 'refs/heads/release-')) }} + - name: IsNightlyBuild + value: ${{ and(eq(variables['Build.Reason'], 'Schedule'), eq(variables['Build.SourceBranch'], 'refs/heads/develop')) }} + - name: DoRealSign + value: ${{ or(eq(parameters.ForceRealSign, true), eq(variables['IsStableBuild'], true), eq(variables['IsNightlyBuild'], true)) }} + - name: DoProceedToRelease + value: ${{ or(eq(variables['DoRealSign'], true), eq(parameters.ForceTestSignRelease, true)) }} + - name: RELEASE_CHANNEL + ${{ if or(eq(variables['Build.SourceBranch'], 'refs/heads/main'), startsWith(variables['Build.SourceBranch'], 'refs/heads/release-')) }}: + value: stable + ${{ else }}: + value: nightly + - name: SIGNTYPE + ${{ if eq(variables['DoRealSign'], true) }}: + value: real + ${{ else }}: + value: test + +parameters: + - name: ForceRealSign + displayName: "Force usage of real sign (PME PoP needed)" + type: boolean + default: false + - name: ForceTestSignRelease + displayName: "Force release on test sign (for test ONLY)" + type: boolean + default: false + +resources: + repositories: + - repository: self + type: git + ref: refs/heads/main + - repository: 1esPipelines + type: git + name: 1ESPipelineTemplates/1ESPipelineTemplates + ref: refs/tags/release + +trigger: + branches: + include: + - main + - develop + - release-* + +pr: + - main + - develop + - release-* + +schedules: + - cron: "0 22 * * 0-4" # 6:00 Mon-Fri UTC+8 + displayName: "Nightly build" + branches: + include: + - develop + always: true + +extends: + template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines + parameters: + featureFlags: + disableNetworkIsolation: true + pool: + name: VSEngSS-MicroBuild2022-1ES + stages: + - stage: Build_Plugin + displayName: "Build Plugin" + jobs: + - job: Build_and_Sign + displayName: Build and Sign Plugin + timeoutInMinutes: 360 + templateContext: + outputs: + - output: pipelineArtifact + artifactName: plugin + targetPath: $(build.artifactstagingdirectory)/plugin + displayName: "Publish Artifact: plugin" + steps: + - checkout: self + clean: true + fetchTags: false + - task: PowerShell@2 + displayName: Install JDK 21 + inputs: + targetType: inline + script: | + # Download Adoptium/Temurin JDK 21.0.8 + Write-Host "Downloading Java 21.0.8..." + $source = "https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.8%2B9/OpenJDK21U-jdk_x64_windows_hotspot_21.0.8_9.zip" + $destination = "$(build.sourcesdirectory)\jdk21_x64_windows.zip" + Invoke-WebRequest -Uri $source -OutFile $destination + - task: JavaToolInstaller@0 + displayName: "Use Java 21" + inputs: + versionSpec: 21 + jdkArchitectureOption: x64 + jdkSourceOption: LocalDirectory + jdkFile: '$(build.sourcesdirectory)\jdk21_x64_windows.zip' + jdkDestinationDirectory: "$(agent.toolsDirectory)/jdk21" + - task: UseNode@1 + inputs: + version: "22.x" + - task: NuGetAuthenticate@1 + displayName: "🔩 NuGet Authenticate" + - task: MicroBuildSigningPlugin@4 + displayName: "Add MicroBuild Signing Plugin" + inputs: + ${{if eq(variables.DoRealSign, true) }}: + signType: "real" + ConnectedPMEServiceName: "40012d40-9ded-4f75-8476-d60758200346" + ${{ else }}: + signType: "test" + feedSource: "https://mseng.pkgs.visualstudio.com/DefaultCollection/_packaging/MicroBuildToolset/nuget/v3/index.json" + env: + MicroBuildOutputFolderOverride: "$(Agent.TempDirectory)" + - task: PowerShell@2 + displayName: Build Plugin + inputs: + targetType: inline + script: | + mvn -v + # ./gradlew buildUtils || exit -1 + mvn clean install -f ./Utils/pom.xml -T 1C "-Dcheckstyle.skip=true" "-Dmaven.test.skip=true" "-Dmaven.javadoc.skip=true" + mvn clean -f ./Utils/pom.xml + cd PluginsAndFeatures/azure-toolkit-for-intellij + ./gradlew clean buildPlugin -s "-Papplicationinsights.key=$(INTELLIJ_KEY)" "-PneedPatchVersion=false" "-Psources=false" "-Porg.gradle.configureondemand=false" "-Porg.gradle.daemon=false" "-Porg.gradle.unsafe.configuration-cache=false" "-Porg.gradle.caching=false" + env: + USE_STABLE_VERSION: $(IsStableBuild) + - task: PowerShell@2 + displayName: Unpackage + inputs: + targetType: inline + script: | + Write-Host "Creating artifacts directory..." + New-Item -ItemType Directory -Path ".\artifacts\intellij\" -Force + Write-Host "Finding plugin zip file..." + $zipFile = Get-ChildItem ".\PluginsAndFeatures\azure-toolkit-for-intellij\build\distributions\*.zip" | Select-Object -First 1 + Write-Host "Found: $($zipFile.FullName)" + Write-Host "Copying to artifacts..." + Copy-Item $zipFile.FullName ".\artifacts\intellij\azure-intellij.zip" + Write-Host "Extracting archive..." + Expand-Archive -Path ".\artifacts\intellij\azure-intellij.zip" -DestinationPath ".\artifacts\intellij\folder" -Force + Write-Host "Cleaning up temporary zip file..." + Remove-Item ".\artifacts\intellij\azure-intellij.zip" + Write-Host "Unpackaging completed successfully" + - task: PowerShell@2 + displayName: Sign jars + timeoutInMinutes: 240 + inputs: + targetType: inline + script: | + Write-Host "Searching for JAR files to sign..." + $files = Get-ChildItem -Path . -Include "azure-intellij-*.jar", "azure-toolkit-for-intellij*.jar", "azure-toolkit-ide-*.jar", "azuretools-core.jar", "azure-explorer-common.jar", "hdinsight-node-common.jar" -Recurse -File + Write-Host "Found $($files.Count) JAR file(s) to sign" + $counter = 0 + foreach ($file in $files) { + $counter++ + Write-Host "[$counter/$($files.Count)] Signing: $($file.Name) with sign type: $(SIGNTYPE)" + dotnet "$env:MBSIGN_APPFOLDER/DDSignFiles.dll" -- /file:"$($file.FullName)" /certs:100010171 /signType:$(SIGNTYPE) + Write-Host " ✓ Signed successfully" + } + Write-Host "All JAR files signed successfully" + workingDirectory: 'artifacts/intellij/folder/azure-intellij/lib' + - task: CmdLine@2 + displayName: Repackage + inputs: + script: | + echo Starting repackaging... + cd artifacts\intellij\folder + echo Creating final zip archive: azure-intellij-$(Build.BuildNumber).zip + tar -a -c -f ..\..\azure-intellij-$(Build.BuildNumber).zip * + echo Repackaging completed successfully + - task: CopyFiles@2 + displayName: "Copy Files to: $(build.artifactstagingdirectory)" + inputs: + SourceFolder: $(system.defaultworkingdirectory)/artifacts + Contents: "*.zip" + TargetFolder: $(build.artifactstagingdirectory)/plugin + - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 + displayName: "Manifest Generator " + inputs: + BuildDropPath: $(build.artifactstagingdirectory)/plugin + - stage: Release_Plugin + condition: ${{ variables.DoProceedToRelease }} + displayName: Release Plugin to Marketplace + dependsOn: Build_Plugin + jobs: + - deployment: releasePlugin + templateContext: + type: releaseJob + isProduction: true + inputs: + - input: pipelineArtifact + artifactName: "plugin" + targetPath: "$(Pipeline.Workspace)/plugin" + environment: azure-intellij-plugin-release + strategy: + runOnce: + deploy: + steps: + - task: PowerShell@2 + displayName: Release + inputs: + targetType: inline + script: | + curl.exe -i --header "Authorization: Bearer ${env:RELEASE_TOKEN}" -F pluginId=28791 -F "file=@$(Pipeline.Workspace)/plugin/azure-intellij-$(Build.BuildNumber).zip" -F channel=$(RELEASE_CHANNEL) https://plugins.jetbrains.com/plugin/uploadPlugin diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000000..56304c79d04 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,125 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Azure Toolkits for Java — IDE plugins for IntelliJ IDEA and Eclipse that help Java developers create, develop, configure, test, and deploy applications to Azure. The primary focus of active development is the IntelliJ plugin. + +## Build Commands + +### Prerequisites +- Java 17 (required for IntelliJ plugin; set `JAVA_HOME` accordingly) +- The shared toolkit libraries from [azure-maven-plugins](https://github.com/microsoft/azure-maven-plugins) must be installed to local Maven repo first + +### Build shared toolkit libs (one-time or when libs change) +```bash +# Clone and build azure-toolkit-libs into local Maven repo +git clone https://github.com/microsoft/azure-maven-plugins.git +cd azure-maven-plugins/azure-toolkit-libs +mvn clean install -B -T 4 -Dmaven.test.skip=true +``` + +### Build Utils (Maven modules in Utils/) +```bash +# From repo root — builds the IDE-specific wrapper libraries +mvn clean install -f Utils/pom.xml +# Or via the root Gradle task: +./gradlew buildUtils -x buildToolkitsLib +``` + +### Build IntelliJ plugin +```bash +cd PluginsAndFeatures/azure-toolkit-for-intellij +./gradlew clean buildPlugin -s +``` +Output ZIP: `PluginsAndFeatures/azure-toolkit-for-intellij/build/distributions/` + +### Build Eclipse plugin +```bash +mvn clean install -f Utils/pom.xml +mvn clean install -f PluginsAndFeatures/AddLibrary/AzureLibraries/pom.xml +mvn clean install -f PluginsAndFeatures/azure-toolkit-for-eclipse/pom.xml +``` + +### Build everything (from repo root) +```bash +./gradlew buildAll +``` + +### Run/Debug IntelliJ plugin locally +Open `PluginsAndFeatures/azure-toolkit-for-intellij` in IntelliJ and use the Gradle `runIde` task. Remote debug port: 5005. + +### Tests +```bash +# IntelliJ plugin tests (run from the IntelliJ plugin dir) +cd PluginsAndFeatures/azure-toolkit-for-intellij +./gradlew test + +# Utils tests (Maven) +mvn test -f Utils/pom.xml + +# Skip tests during build +./gradlew buildAll -PskipTest=true +# Or for Maven modules: +mvn install -f Utils/pom.xml -Dmaven.test.skip=true +``` + +### Checkstyle +Checkstyle runs by default on Maven builds. Skip with: +```bash +mvn install -f Utils/pom.xml -Dcheckstyle.skip=true +``` +IntelliJ plugin checkstyle config: `PluginsAndFeatures/azure-toolkit-for-intellij/config/checkstyle/checkstyle.xml` + +## Architecture + +### Three-Layer Dependency Stack + +1. **azure-toolkit-libs** (external repo: `azure-maven-plugins`) — Core SDK wrappers for Azure services. IDE-agnostic. Must be `mvn install`ed to local repo before building this project. + +2. **Utils/** (this repo, Maven) — IDE-specific wrapper libraries (`azure-toolkit-ide-libs` and `azure-toolkit-ide-hdinsight-libs`). Each Azure service has a corresponding `azure-toolkit-ide-*-lib` module (e.g., `azure-toolkit-ide-appservice-lib`, `azure-toolkit-ide-cosmos-lib`). Built with Maven and published to local repo for the Gradle IntelliJ build to consume. + +3. **PluginsAndFeatures/** (this repo) — IDE plugin implementations: + - `azure-toolkit-for-intellij/` — IntelliJ plugin (Gradle + Kotlin DSL, ~40 submodules) + - `azure-toolkit-for-eclipse/` — Eclipse plugin (Maven/Tycho) + +### IntelliJ Plugin Module Structure + +The IntelliJ plugin is a single composite plugin built from ~40 Gradle submodules, each declared in `settings.gradle.kts`. Key module categories: + +- **`azure-intellij-plugin-lib`** — Core IntelliJ integration: UI components (AzureComboBox, AzureDialog, Tree), action framework, auth, messager, task manager, telemetry. This is the foundation module. +- **`azure-intellij-plugin-lib-java`** — Java-specific IntelliJ utilities (run configs, artifact handling). +- **`azure-intellij-plugin-base`** — Plugin entry point (`AzurePlugin` as `StartupActivity`), lifecycle management. +- **`azure-intellij-plugin-service-explorer`** — Azure Explorer tool window. +- **`azure-intellij-resource-connector-lib`** — Resource connection framework (connecting Azure resources to project modules). +- **Service modules** — One per Azure service (e.g., `azure-intellij-plugin-appservice`, `azure-intellij-plugin-cosmos`, `azure-intellij-plugin-database`, `azure-intellij-plugin-storage`, etc.) +- **`-java` suffix modules** — Java-language-specific features for that service (e.g., `azure-intellij-plugin-appservice-java`, `azure-intellij-plugin-database-java`). + +### Extension Point Pattern + +The plugin uses an IntelliJ extension point `com.microsoft.tooling.msservices.intellij.azure.actions` with the `IActionsContributor` interface. Each service module registers its own `ActionsContributor` implementation via its `META-INF/*.xml` descriptor. These are composed into the main `plugin.xml` using `xi:include`. + +To add a new service module: +1. Create the module directory under `PluginsAndFeatures/azure-toolkit-for-intellij/` +2. Add it to `settings.gradle.kts` +3. Add an `implementation(project(":module-name"))` dependency in the root `build.gradle.kts` +4. Create a `META-INF/.xml` descriptor +5. Include it in `src/main/resources/META-INF/plugin.xml` via `xi:include` + +### Build System Details + +- **Root Gradle** (`build.gradle` at repo root) — Orchestrates cross-project builds. Uses Groovy DSL with Gradle 8.8. +- **IntelliJ Gradle** (`PluginsAndFeatures/azure-toolkit-for-intellij/build.gradle.kts`) — Uses Kotlin DSL with IntelliJ Platform Gradle Plugin 2.3.0. Targets IntelliJ IDEA Ultimate for compilation. Version catalog at `gradle/libs.versions.toml`. +- **Maven** (`Utils/pom.xml`) — Builds the IDE wrapper libraries. Uses AspectJ for cross-cutting concerns (via `azure-toolkit-common-lib`). +- AspectJ weaving is used at both Maven (aspectj-maven-plugin) and Gradle (io.freefair.aspectj.post-compile-weaving) levels. + +### Key Conventions + +- Java 17 source/target for all modules. +- Lombok is used project-wide (`@Data`, `@Builder`, etc.). +- All Java source files require the MIT license header (enforced by checkstyle). +- LF line endings required (no CRLF). +- No tabs — spaces only. +- TODOs must be named: `TODO (name): description`. +- Base package: `com.microsoft.azure.toolkit.intellij` for IntelliJ modules, `com.microsoft.azure.toolkit.lib` for library modules. diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties index f4bffe8450a..a24154a4ff0 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle.properties @@ -22,7 +22,7 @@ platformDownloadSources=true # Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib kotlin.stdlib.default.dependency=false # Gradle Releases -> https://github.com/gradle/gradle/releases -gradleVersion=8.14.1 +gradleVersion=9.0 # Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html org.gradle.configuration-cache=true org.gradle.configuration-cache.problems=warn diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle/libs.versions.toml b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle/libs.versions.toml index fa3446576dd..160c9f77b9d 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle/libs.versions.toml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle/libs.versions.toml @@ -5,7 +5,7 @@ kotlin = "2.2.20" changelog = "2.4.0" #intellijPlatform = "2.10.1" -intellijPlatform = "2.10.1" +intellijPlatform = "2.12.0" detekt = "1.23.6" ktlint = "12.1.1" #gradleIntelliJPlugin = "1.17.3" diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle/wrapper/gradle-wrapper.properties b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle/wrapper/gradle-wrapper.properties index 002b867c48b..2dcec856bd0 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle/wrapper/gradle-wrapper.properties +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd4917707c1f8861d8cb53dd15194d4248596..e6441136f3d4ba8a0da8d277868979cfbc8ad796 100644 GIT binary patch delta 34118 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 delta 34176 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 SGW6ptSs!X7{rG*o_g?|vpSEZK diff --git a/gradlew b/gradlew index 1aa94a42690..b740cf13397 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. diff --git a/gradlew.bat b/gradlew.bat index 93e3f59f135..25da30dbdee 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -43,11 +43,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail From 5127af94f67ea33798331fc7a58a5d779adbc7cd Mon Sep 17 00:00:00 2001 From: Miller Wang Date: Mon, 9 Mar 2026 14:09:26 +0800 Subject: [PATCH 37/43] Support IntelliJ IDEA 2026.1 EAP - fix build.gradle.kts for Plugin 2.12.0 API changes --- .../azure-toolkit-for-intellij/build.gradle.kts | 10 ++++++---- .../gradle/libs.versions.toml | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/build.gradle.kts b/PluginsAndFeatures/azure-toolkit-for-intellij/build.gradle.kts index 696be472581..2d07cb42196 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/build.gradle.kts +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType import org.jetbrains.intellij.platform.gradle.tasks.VerifyPluginTask import java.time.LocalDateTime import java.time.format.DateTimeFormatter -import java.net.URL +import java.net.URI fun properties(key: String) = providers.gradleProperty(key) fun environment(key: String) = providers.environmentVariable(key) @@ -60,7 +60,9 @@ allprojects { dependencies { intellijPlatform { - intellijIdeaUltimate(properties("platformVersion").get(), useInstaller = false) + intellijIdeaUltimate(properties("platformVersion").get()) { + useInstaller = false + } } implementation(platform("com.microsoft.azure:azure-toolkit-libs:0.52.2")) @@ -146,7 +148,7 @@ intellijPlatform { pluginVerification { ides { - ide(IntelliJPlatformType.IntellijIdeaCommunity, properties("platformVersion").get()) + create(IntelliJPlatformType.IntellijIdeaCommunity, properties("platformVersion").get()) } } } @@ -256,7 +258,7 @@ tasks { if (!langServerDir.exists()) { logger.info("Downloading bicep language server ...") val zipFile = file("azure-intellij-plugin-bicep/downloaded.zip") - URL("https://aka.ms/java-toolkit-bicep-ls").openStream().use { input -> + URI("https://aka.ms/java-toolkit-bicep-ls").toURL().openStream().use { input -> zipFile.outputStream().use { it.write(input.readBytes()) } } logger.info("Unzipping bicep language server ...") diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle/libs.versions.toml b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle/libs.versions.toml index 160c9f77b9d..6527cbeaabe 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/gradle/libs.versions.toml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/gradle/libs.versions.toml @@ -10,7 +10,7 @@ detekt = "1.23.6" ktlint = "12.1.1" #gradleIntelliJPlugin = "1.17.3" #qodana = "2024.1.5" -aspectj = "8.6" +aspectj = "9.2.0" springDependencyManagement = "1.1.6" serialization = "1.9.24" From 67d894c2e0a93b8013d5e1e67a556f46b1835c3f Mon Sep 17 00:00:00 2001 From: Miller Wang Date: Mon, 9 Mar 2026 14:16:17 +0800 Subject: [PATCH 38/43] Fix ktlint code style violations for IntelliJ 2026.1 EAP support --- .../build.gradle.kts | 6 +- .../spark/common/SparkSubmitModelScenario.kt | 43 +-- .../hdinsight/spark/common/SparkUITest.kt | 2 +- .../spark/ui/ImmutableComboBoxModelTest.kt | 6 +- .../hdinsight/spark/ui/SparkJobTableTest.kt | 332 +++++++++++------- 5 files changed, 242 insertions(+), 147 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/build.gradle.kts b/PluginsAndFeatures/azure-toolkit-for-intellij/build.gradle.kts index 2d07cb42196..19708ad2aa8 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/build.gradle.kts +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/build.gradle.kts @@ -1,12 +1,12 @@ import io.freefair.gradle.plugins.aspectj.AjcAction import org.apache.tools.ant.filters.ReplaceTokens import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType -import org.jetbrains.intellij.platform.gradle.tasks.VerifyPluginTask +import java.net.URI import java.time.LocalDateTime import java.time.format.DateTimeFormatter -import java.net.URI fun properties(key: String) = providers.gradleProperty(key) + fun environment(key: String) = providers.environmentVariable(key) plugins { @@ -303,4 +303,4 @@ tasks { // publishPlugin { // dependsOn(patchChangelog) // } -} \ No newline at end of file +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/src/test/java/com/microsoft/azure/hdinsight/spark/common/SparkSubmitModelScenario.kt b/PluginsAndFeatures/azure-toolkit-for-intellij/src/test/java/com/microsoft/azure/hdinsight/spark/common/SparkSubmitModelScenario.kt index 4a3bc9661fb..6d3630dcf48 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/src/test/java/com/microsoft/azure/hdinsight/spark/common/SparkSubmitModelScenario.kt +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/src/test/java/com/microsoft/azure/hdinsight/spark/common/SparkSubmitModelScenario.kt @@ -40,15 +40,17 @@ class SparkSubmitModelScenario { @Given("^set SparkSubmitModel properties as following$") fun initSparkSubmitModel(properties: Map) { - properties.forEach { key, value -> when (key) { - "cluster_name" -> submitModel.clusterName = value - "is_local_artifact" -> submitModel.isLocalArtifact = value.toBoolean() - "local_artifact_path" -> submitModel.localArtifactPath = value - "classname" -> submitModel.mainClassName = value - "cmd_line_args" -> submitModel.commandLineArgs = value.split(" ") - "ref_jars" -> submitModel.referenceJars = value.split(";") - "ref_files" -> submitModel.referenceFiles = value.split(";") - } } + properties.forEach { key, value -> + when (key) { + "cluster_name" -> submitModel.clusterName = value + "is_local_artifact" -> submitModel.isLocalArtifact = value.toBoolean() + "local_artifact_path" -> submitModel.localArtifactPath = value + "classname" -> submitModel.mainClassName = value + "cmd_line_args" -> submitModel.commandLineArgs = value.split(" ") + "ref_jars" -> submitModel.referenceJars = value.split(";") + "ref_files" -> submitModel.referenceFiles = value.split(";") + } + } } @Then("^checking XML serialized should to be '(.*)'$") @@ -57,7 +59,6 @@ class SparkSubmitModelScenario { val actual = XMLOutputter().outputString(element) assertEquals(expect, actual) - } @Given("^the SparkSubmitModel XML input '(.*)' to deserialize$") @@ -69,14 +70,16 @@ class SparkSubmitModelScenario { @Then("^check SparkSubmitModel properties as following$") fun checkSparkSubmitMode(expect: Map) { - expect.forEach { key, value -> when (key) { - "cluster_name" -> assertEquals(value, submitModel.clusterName) - "is_local_artifact" -> assertEquals(value.toBoolean(), submitModel.isLocalArtifact) - "local_artifact_path" -> assertEquals(value, submitModel.localArtifactPath) - "classname" -> assertEquals(value, submitModel.mainClassName) - "cmd_line_args" -> assertEquals(value, submitModel.commandLineArgs.joinToString(" ")) - "ref_jars" -> assertEquals(value, submitModel.referenceJars.joinToString(";")) - "ref_files" -> assertEquals(value, submitModel.referenceFiles.joinToString(";")) - } } + expect.forEach { key, value -> + when (key) { + "cluster_name" -> assertEquals(value, submitModel.clusterName) + "is_local_artifact" -> assertEquals(value.toBoolean(), submitModel.isLocalArtifact) + "local_artifact_path" -> assertEquals(value, submitModel.localArtifactPath) + "classname" -> assertEquals(value, submitModel.mainClassName) + "cmd_line_args" -> assertEquals(value, submitModel.commandLineArgs.joinToString(" ")) + "ref_jars" -> assertEquals(value, submitModel.referenceJars.joinToString(";")) + "ref_files" -> assertEquals(value, submitModel.referenceFiles.joinToString(";")) + } + } } -} \ No newline at end of file +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/src/test/java/com/microsoft/azure/hdinsight/spark/common/SparkUITest.kt b/PluginsAndFeatures/azure-toolkit-for-intellij/src/test/java/com/microsoft/azure/hdinsight/spark/common/SparkUITest.kt index 8b5c5d9745e..0187aa13f06 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/src/test/java/com/microsoft/azure/hdinsight/spark/common/SparkUITest.kt +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/src/test/java/com/microsoft/azure/hdinsight/spark/common/SparkUITest.kt @@ -51,4 +51,4 @@ open class SparkUITest : LightJavaCodeInsightFixtureTestCase() { isVisible = true } } -} \ No newline at end of file +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/src/test/java/com/microsoft/azure/hdinsight/spark/ui/ImmutableComboBoxModelTest.kt b/PluginsAndFeatures/azure-toolkit-for-intellij/src/test/java/com/microsoft/azure/hdinsight/spark/ui/ImmutableComboBoxModelTest.kt index 8e1d8bf2c57..7d6f792109d 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/src/test/java/com/microsoft/azure/hdinsight/spark/ui/ImmutableComboBoxModelTest.kt +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/src/test/java/com/microsoft/azure/hdinsight/spark/ui/ImmutableComboBoxModelTest.kt @@ -25,10 +25,10 @@ package com.microsoft.azure.hdinsight.spark.ui import com.microsoft.intellij.ui.util.findFirst import com.microsoft.intellij.ui.util.iterator import junit.framework.TestCase -import org.assertj.core.api.Assertions.* +import org.assertj.core.api.Assertions.assertThat import org.junit.Test -class ImmutableComboBoxModelTest: TestCase() { +class ImmutableComboBoxModelTest : TestCase() { @Test fun testIterator() { val data = listOf(3, 5, 4, 2) @@ -51,4 +51,4 @@ class ImmutableComboBoxModelTest: TestCase() { val emptyModel: ImmutableComboBoxModel? = null assertNull(emptyModel.findFirst { true }) } -} \ No newline at end of file +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/src/test/java/com/microsoft/azure/hdinsight/spark/ui/SparkJobTableTest.kt b/PluginsAndFeatures/azure-toolkit-for-intellij/src/test/java/com/microsoft/azure/hdinsight/spark/ui/SparkJobTableTest.kt index b482f9cd4cf..f294523a2ae 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/src/test/java/com/microsoft/azure/hdinsight/spark/ui/SparkJobTableTest.kt +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/src/test/java/com/microsoft/azure/hdinsight/spark/ui/SparkJobTableTest.kt @@ -42,40 +42,48 @@ import java.awt.event.WindowEvent import java.util.concurrent.TimeUnit class KillLivyJobAction : AzureAnAction(AllIcons.Actions.Cancel) { - override fun onActionPerformed(anActionEvent: AnActionEvent, operation: Operation?): Boolean { + override fun onActionPerformed( + anActionEvent: AnActionEvent, + operation: Operation?, + ): Boolean { System.out.println("Clicked ${anActionEvent.place} kill job button") return true } } class RestartLivyJobAction : AzureAnAction(AllIcons.Actions.Restart) { - override fun onActionPerformed(anActionEvent: AnActionEvent, operation: Operation?): Boolean { + override fun onActionPerformed( + anActionEvent: AnActionEvent, + operation: Operation?, + ): Boolean { System.out.println("Clicked ${anActionEvent.place} restart job button") return true } } -const val killActionColName = "KillAction" -const val restartActionColName = "RestartAction" -const val idColName = "ID" -const val appIdColName = "AppID" -const val stateColName = "State" - -class MockSparkLivyJobsTableSchema - : UniqueColumnNameTableSchema(arrayOf( - ActionColumnInfo(killActionColName), - ActionColumnInfo(restartActionColName), - PlainColumnInfo(idColName), - PlainColumnInfo(appIdColName), - PlainColumnInfo(stateColName))) { +const val KILL_ACTION_COL_NAME = "KillAction" +const val RESTART_ACTION_COL_NAME = "RestartAction" +const val ID_COL_NAME = "ID" +const val APP_ID_COL_NAME = "AppID" +const val STATE_COL_NAME = "State" +class MockSparkLivyJobsTableSchema : + UniqueColumnNameTableSchema( + arrayOf( + ActionColumnInfo(KILL_ACTION_COL_NAME), + ActionColumnInfo(RESTART_ACTION_COL_NAME), + PlainColumnInfo(ID_COL_NAME), + PlainColumnInfo(APP_ID_COL_NAME), + PlainColumnInfo(STATE_COL_NAME), + ), + ) { inner class MockSparkJobDescriptor(val jobStatus: SparkSubmitResponse) : RowDescriptor( - killActionColName to KillLivyJobAction(), - restartActionColName to RestartLivyJobAction(), - idColName to jobStatus.id, - appIdColName to jobStatus.appId, - stateColName to jobStatus.state) - + KILL_ACTION_COL_NAME to KillLivyJobAction(), + RESTART_ACTION_COL_NAME to RestartLivyJobAction(), + ID_COL_NAME to jobStatus.id, + APP_ID_COL_NAME to jobStatus.appId, + STATE_COL_NAME to jobStatus.state, + ) } class MockSparkBatchJobViewerControl(private val view: MockSparkBatchJobViewer) : LivyBatchJobViewer.Control { @@ -84,26 +92,28 @@ class MockSparkBatchJobViewerControl(private val view: MockSparkBatchJobViewer) } override fun onJobSelected(jobSelected: UniqueColumnNameTableSchema.RowDescriptor?) { - val sparkJobDesc = (jobSelected as? MockSparkLivyJobsTableSchema.MockSparkJobDescriptor)?.let { arrayOf(it)} - ?: emptyArray() + val sparkJobDesc = + (jobSelected as? MockSparkLivyJobsTableSchema.MockSparkJobDescriptor)?.let { arrayOf(it) } + ?: emptyArray() Observable.from(sparkJobDesc) - .delay(500, TimeUnit.MILLISECONDS) - .subscribe { view.getModel(LivyBatchJobViewer.Model::class.java).apply { - jobDetail = if (it.jobStatus.id == 1) { - // Unclosed JSON string - """{"message":"A broken response for ${it.jobStatus.appId}!","error no": 0, "id": ${it.jobStatus.id}""" - - } else { - """{"message":"hello ${it.jobStatus.appId}!","error no": 0, "id": ${it.jobStatus.id}}""" - } + .delay(500, TimeUnit.MILLISECONDS) + .subscribe { + view.getModel(LivyBatchJobViewer.Model::class.java).apply { + jobDetail = + if (it.jobStatus.id == 1) { + // Unclosed JSON string + """{"message":"A broken response for ${it.jobStatus.appId}!","error no": 0, "id": ${it.jobStatus.id}""" + } else { + """{"message":"hello ${it.jobStatus.appId}!","error no": 0, "id": ${it.jobStatus.id}}""" + } view.setData(this) - }} + } + } } } - class MockSparkBatchJobViewer : LivyBatchJobViewer() { override val jobViewerControl: Control by lazy { MockSparkBatchJobViewerControl(this@MockSparkBatchJobViewer) } } @@ -119,100 +129,180 @@ fun getJobListPage(pageLink: String?): JobPage? { println("Get job list from $pageLink") return when (pageLink) { - "http://page1" -> object : JobPage { - override fun nextPageLink(): String? { - return "http://page2" - } + "http://page1" -> + object : JobPage { + override fun nextPageLink(): String? { + return "http://page2" + } - override fun items(): List? { - return listOf( - tableSchema.MockSparkJobDescriptor(parseJSON("""{ - "id": 1, - "appId": "application-134124194-1", - "state": "running" - }""".trimIndent())), - tableSchema.MockSparkJobDescriptor(parseJSON("""{ - "id": 2, - "appId": null, - "state": "dead" - }""".trimIndent())), - tableSchema.MockSparkJobDescriptor(parseJSON("""{ - "id": 3, - "state": "success" - }""".trimIndent())), - tableSchema.MockSparkJobDescriptor(parseJSON("""{ - "id": 4, - "appId": "application-134124194-4" - }""".trimIndent())) - ) - } - } - "http://page2" -> object : JobPage { - override fun nextPageLink(): String? { - return "http://page3" + override fun items(): List? { + return listOf( + tableSchema.MockSparkJobDescriptor( + parseJSON( + """ + { + "id": 1, + "appId": "application-134124194-1", + "state": "running" + } + """.trimIndent(), + ), + ), + tableSchema.MockSparkJobDescriptor( + parseJSON( + """ + { + "id": 2, + "appId": null, + "state": "dead" + } + """.trimIndent(), + ), + ), + tableSchema.MockSparkJobDescriptor( + parseJSON( + """ + { + "id": 3, + "state": "success" + } + """.trimIndent(), + ), + ), + tableSchema.MockSparkJobDescriptor( + parseJSON( + """ + { + "id": 4, + "appId": "application-134124194-4" + } + """.trimIndent(), + ), + ), + ) + } } + "http://page2" -> + object : JobPage { + override fun nextPageLink(): String? { + return "http://page3" + } - override fun items(): List? { - return listOf( - tableSchema.MockSparkJobDescriptor(parseJSON("""{ - "id": 5, - "appId": "application-134124194-5", - "state": "running" - }""".trimIndent())), - tableSchema.MockSparkJobDescriptor(parseJSON("""{ - "id": 6, - "appId": null, - "state": "dead" - }""".trimIndent())), - tableSchema.MockSparkJobDescriptor(parseJSON("""{ - "id": 7, - "state": "success" - }""".trimIndent())), - tableSchema.MockSparkJobDescriptor(parseJSON("""{ - "id": 8, - "appId": "application-134124194-8" - }""".trimIndent())) - ) - } - } - "http://page3" -> object : JobPage { - override fun nextPageLink(): String? { - return null + override fun items(): List? { + return listOf( + tableSchema.MockSparkJobDescriptor( + parseJSON( + """ + { + "id": 5, + "appId": "application-134124194-5", + "state": "running" + } + """.trimIndent(), + ), + ), + tableSchema.MockSparkJobDescriptor( + parseJSON( + """ + { + "id": 6, + "appId": null, + "state": "dead" + } + """.trimIndent(), + ), + ), + tableSchema.MockSparkJobDescriptor( + parseJSON( + """ + { + "id": 7, + "state": "success" + } + """.trimIndent(), + ), + ), + tableSchema.MockSparkJobDescriptor( + parseJSON( + """ + { + "id": 8, + "appId": "application-134124194-8" + } + """.trimIndent(), + ), + ), + ) + } } + "http://page3" -> + object : JobPage { + override fun nextPageLink(): String? { + return null + } - override fun items(): List? { - return listOf( - tableSchema.MockSparkJobDescriptor(parseJSON("""{ - "id": 9, - "appId": "application-134124194-9", - "state": "running" - }""".trimIndent())), - tableSchema.MockSparkJobDescriptor(parseJSON("""{ - "id": 10, - "appId": null, - "state": "dead" - }""".trimIndent())), - tableSchema.MockSparkJobDescriptor(parseJSON("""{ - "id": 11, - "state": "success" - }""".trimIndent())), - tableSchema.MockSparkJobDescriptor(parseJSON("""{ - "id": 12, - "appId": "application-134124194-12" - }""".trimIndent())) - ) + override fun items(): List? { + return listOf( + tableSchema.MockSparkJobDescriptor( + parseJSON( + """ + { + "id": 9, + "appId": "application-134124194-9", + "state": "running" + } + """.trimIndent(), + ), + ), + tableSchema.MockSparkJobDescriptor( + parseJSON( + """ + { + "id": 10, + "appId": null, + "state": "dead" + } + """.trimIndent(), + ), + ), + tableSchema.MockSparkJobDescriptor( + parseJSON( + """ + { + "id": 11, + "state": "success" + } + """.trimIndent(), + ), + ), + tableSchema.MockSparkJobDescriptor( + parseJSON( + """ + { + "id": 12, + "appId": "application-134124194-12" + } + """.trimIndent(), + ), + ), + ) + } } - } else -> null } } + @Ignore class SparkJobTableTest : SparkUITest() { - @Test fun testLivyTable() { - val model = LivyBatchJobViewer.Model(LivyBatchJobTableViewport.Model( - LivyBatchJobTableModel(tableSchema), getJobListPage("http://page1"))) + val model = + LivyBatchJobViewer.Model( + LivyBatchJobTableViewport.Model( + LivyBatchJobTableModel(tableSchema), + getJobListPage("http://page1"), + ), + ) jobView.setData(model) @@ -220,11 +310,13 @@ class SparkJobTableTest : SparkUITest() { contentPane.add(jobView.component) pack() - addWindowListener(object: WindowAdapter() { - override fun windowClosing(e: WindowEvent?) { - jobView.dispose() - } - }) + addWindowListener( + object : WindowAdapter() { + override fun windowClosing(e: WindowEvent?) { + jobView.dispose() + } + }, + ) isVisible = true } } From 3782f32ba94e8538aa0cdd93e646a6299a2b5c70 Mon Sep 17 00:00:00 2001 From: Miller Wang Date: Mon, 9 Mar 2026 15:47:45 +0800 Subject: [PATCH 39/43] Fix IntelliJ 2026.1 EAP API breaking changes - Add bundledPlugin('org.jetbrains.idea.reposearch') for MavenId class (moved from maven plugin) - Add testImplementation('junit:junit:4.13.2') (no longer bundled with platform) - Add maven-artifact dependency for ComparableVersion (no longer on platform classpath) --- .gitignore | 2 ++ .../build.gradle.kts | 2 ++ .../AzureCosmosDbAccountParamEditor.java | 4 ++- .../build.gradle.kts | 29 +++++++++++++++++++ 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f2db4d08b45..fa05dc8a554 100644 --- a/.gitignore +++ b/.gitignore @@ -111,3 +111,5 @@ pom.xml.versionsBackup # Bicep Language Server **/bicep-langserver/** **/azure-intellij-plugin-bicep/downloaded.zip +*.log +deps.txt diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-bicep/build.gradle.kts b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-bicep/build.gradle.kts index 6309b2736b6..a8ef64b4d8d 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-bicep/build.gradle.kts +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-bicep/build.gradle.kts @@ -8,6 +8,8 @@ dependencies { implementation("com.vladsch.flexmark:flexmark:0.64.0") implementation("com.vladsch.flexmark:flexmark-util:0.64.0") implementation("org.apache.commons:commons-lang3:3.12.0") + // ComparableVersion is no longer on the platform classpath in 261 + implementation("org.apache.maven:maven-artifact:3.9.11") testImplementation("junit:junit:4.13.2") testImplementation("org.mockito:mockito-core:3.9.0") testImplementation("org.powermock:powermock-api-mockito2:2.0.9") diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-cosmos/src/main/java/com/microsoft/azure/toolkit/intellij/cosmos/dbtools/AzureCosmosDbAccountParamEditor.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-cosmos/src/main/java/com/microsoft/azure/toolkit/intellij/cosmos/dbtools/AzureCosmosDbAccountParamEditor.java index 22947591ec6..86f17c4aeb7 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-cosmos/src/main/java/com/microsoft/azure/toolkit/intellij/cosmos/dbtools/AzureCosmosDbAccountParamEditor.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-cosmos/src/main/java/com/microsoft/azure/toolkit/intellij/cosmos/dbtools/AzureCosmosDbAccountParamEditor.java @@ -209,7 +209,9 @@ private void setUsername(String user) { @SneakyThrows private void setUseSsl(boolean useSsl) { final DataSourceConfigurable configurable = this.getDataSourceConfigurable(); - final JBCheckBox useSSLCheckBox = (JBCheckBox) FieldUtils.readField(configurable.getSshSslPanel(), "myUseSSLJBCheckBox", true); + // getSshSslPanel() was removed in IntelliJ 261; use reflection to access the panel field directly + final Object sshSslPanel = FieldUtils.readField(configurable, "mySshSslPanel", true); + final JBCheckBox useSSLCheckBox = (JBCheckBox) FieldUtils.readField(sshSslPanel, "myUseSSLJBCheckBox", true); useSSLCheckBox.setSelected(useSsl); } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/build.gradle.kts b/PluginsAndFeatures/azure-toolkit-for-intellij/build.gradle.kts index 19708ad2aa8..47bf01a5352 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/build.gradle.kts +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/build.gradle.kts @@ -63,6 +63,13 @@ allprojects { intellijIdeaUltimate(properties("platformVersion").get()) { useInstaller = false } + // JBR 25 required to run IntelliJ 2026.1 (PathClassLoader is JBR-only) + jetbrainsRuntimeLocal("C:/Users/wangmi/.jdks/jbr-25/jbr_jcef-25.0.2-windows-x64-b329.72") + // MavenId/MavenCoordinate classes moved from maven plugin to repository-search plugin in 261 + bundledPlugin("org.jetbrains.idea.reposearch") + // Test framework classes moved to separate modules in 261 + testFramework(org.jetbrains.intellij.platform.gradle.TestFrameworkType.Platform) + testFramework(org.jetbrains.intellij.platform.gradle.TestFrameworkType.Plugin.Java) } implementation(platform("com.microsoft.azure:azure-toolkit-libs:0.52.2")) @@ -74,6 +81,8 @@ allprojects { annotationProcessor("org.projectlombok:lombok:1.18.32") implementation("com.microsoft.azure:azure-toolkit-common-lib:0.52.2") aspect("com.microsoft.azure:azure-toolkit-common-lib:0.52.2") + // junit was removed from IntelliJ platform bundled libs in 261 + testImplementation("junit:junit:4.13.2") } configurations { @@ -116,6 +125,17 @@ allprojects { duplicatesStrategy = DuplicatesStrategy.WARN } + // Gradle 9 requires explicit dependency declaration for shared sandbox outputs + withType { + dependsOn(rootProject.tasks.named("prepareTestSandbox")) + // Each subproject's test sandbox may be produced by other subproject tasks + rootProject.subprojects.forEach { sub -> + sub.tasks.matching { it.name == "prepareTestSandbox" }.configureEach { + this@withType.dependsOn(this) + } + } + } + sourceSets { main { java.srcDirs("src/main/java") @@ -127,6 +147,10 @@ allprojects { java.srcDir("src/test/java") kotlin.srcDirs("src/test/kotlin") resources.srcDir("src/test/resources") + // Exclude legacy duplicate hdinsight test files from root module; + // they are properly maintained in azure-intellij-plugin-hdinsight-base + java.exclude("com/microsoft/azure/hdinsight/**") + kotlin.exclude("com/microsoft/azure/hdinsight/**") } } } @@ -216,6 +240,11 @@ dependencies { implementation("com.microsoft.azure:azure-toolkit-auth-lib") implementation("com.microsoft.azure:azure-toolkit-ide-common-lib") implementation("com.microsoft.azure:azure-toolkit-ide-appservice-lib") + + // Test dependencies for root module tests (cucumber, assertj) + testImplementation("io.cucumber:cucumber-java:7.0.0") + testImplementation("io.cucumber:cucumber-junit:7.0.0") + testImplementation("org.assertj:assertj-core:3.19.0") } tasks { From b7c85c5ebcc069cf61ae2f147b7ea821217ab07a Mon Sep 17 00:00:00 2001 From: Miller Wang Date: Tue, 10 Mar 2026 00:08:21 +0800 Subject: [PATCH 40/43] fix: remove override of final ConsoleViewImpl.isDisposed() method In IntelliJ 261, ConsoleViewImpl.isDisposed() became final. Remove the custom isDisposed field and override, relying on the parent implementation instead. Callers already use the inherited method through the ConsoleView interface. --- .../common/streaminglog/StreamingLogsConsoleView.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/streaminglog/StreamingLogsConsoleView.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/streaminglog/StreamingLogsConsoleView.java index 0596ea516af..3add2cecc30 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/streaminglog/StreamingLogsConsoleView.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/streaminglog/StreamingLogsConsoleView.java @@ -14,12 +14,10 @@ public class StreamingLogsConsoleView extends ConsoleViewImpl { private static final String SEPARATOR = System.getProperty("line.separator"); - private boolean isDisposed; private Disposable subscription; public StreamingLogsConsoleView(@NotNull Project project) { super(project, true); - this.isDisposed = false; this.setUpdateFoldingsEnabled(false); } @@ -43,10 +41,6 @@ public boolean isActive() { return subscription != null && !subscription.isDisposed(); } - public boolean isDisposed() { - return this.isDisposed; - } - private void printlnToConsole(String message, ConsoleViewContentType consoleViewContentType) { this.print(message + SEPARATOR, consoleViewContentType); } @@ -54,7 +48,6 @@ private void printlnToConsole(String message, ConsoleViewContentType consoleView @Override public void dispose() { super.dispose(); - this.isDisposed = true; closeStreamingLog(); } } From e7c547ca9e8a5b2fc8ae13c405b34199c4718a06 Mon Sep 17 00:00:00 2001 From: Miller Wang Date: Tue, 10 Mar 2026 00:09:51 +0800 Subject: [PATCH 41/43] fix: add missing resource bundle key 'user/appmod.refresh_migrate_node' Add the missing operation description for the migrate-to-azure node refresh action to prevent MissingResourceException at runtime. --- .../bundles/com/microsoft/azure/toolkit/operation.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Utils/azure-toolkit-ide-libs/azure-toolkit-ide-common-lib/src/main/resources/bundles/com/microsoft/azure/toolkit/operation.properties b/Utils/azure-toolkit-ide-libs/azure-toolkit-ide-common-lib/src/main/resources/bundles/com/microsoft/azure/toolkit/operation.properties index b32b9bd5f4a..1ddda68a956 100644 --- a/Utils/azure-toolkit-ide-libs/azure-toolkit-ide-common-lib/src/main/resources/bundles/com/microsoft/azure/toolkit/operation.properties +++ b/Utils/azure-toolkit-ide-libs/azure-toolkit-ide-common-lib/src/main/resources/bundles/com/microsoft/azure/toolkit/operation.properties @@ -1012,4 +1012,5 @@ internal/guidance.create_resource_group=create Resource Group internal/guidance.open_container_app_in_browser=open Container App in browser user/guidance.open_container_app_log_streaming=open Container App log streaming -actions.common.deploy_to_azure=deploy to Azure \ No newline at end of file +actions.common.deploy_to_azure=deploy to Azure +user/appmod.refresh_migrate_node=Refresh Migrate to Azure Node From 4dbbd18fa4cc3afa61db4dca15e551647e5744bc Mon Sep 17 00:00:00 2001 From: Miller Wang Date: Tue, 10 Mar 2026 08:01:46 +0800 Subject: [PATCH 42/43] fix: use reflection for Scala plugin SBT versions API compatibility The Scala plugin for IntelliJ 2026.1 changed the SBT versions API. Use reflection to try new API first, fall back to old API for cross-version compatibility. --- .../projects/SbtVersionOptionsPanel.java | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-hdinsight/src/main/java/com/microsoft/azure/hdinsight/projects/SbtVersionOptionsPanel.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-hdinsight/src/main/java/com/microsoft/azure/hdinsight/projects/SbtVersionOptionsPanel.java index 7ff14936b86..d186a228cea 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-hdinsight/src/main/java/com/microsoft/azure/hdinsight/projects/SbtVersionOptionsPanel.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-hdinsight/src/main/java/com/microsoft/azure/hdinsight/projects/SbtVersionOptionsPanel.java @@ -8,11 +8,12 @@ import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.ui.ComboBox; import com.microsoft.azure.hdinsight.common.logger.ILogger; -import org.jetbrains.plugins.scala.project.Versions; +import scala.collection.immutable.Seq; import scala.reflect.ClassTag; import javax.swing.*; import java.awt.*; +import java.lang.reflect.Method; public class SbtVersionOptionsPanel extends JPanel implements ILogger { private ComboBox sbtVersionComboBox; @@ -36,7 +37,35 @@ public SbtVersionOptionsPanel() { public void updateSbtVersions() { final String[][] versions = new String[1][1]; ProgressManager.getInstance().runProcess(() -> { - versions[0] = (String[]) Versions.SBT$.MODULE$.loadVersionsWithProgress(null).versions().toArray(ClassTag.apply(String.class)); + try { + // In newer Scala plugin versions, the API changed. Use reflection for cross-version compatibility. + // Old: Versions.SBT$.MODULE$.loadVersionsWithProgress(null).versions() -> Seq + // New: Versions.loadSbtVersions(false, null) -> Seq + final Class versionsClass = Class.forName("org.jetbrains.plugins.scala.project.Versions"); + try { + // Try new API first + final Method loadSbtVersionsMethod = versionsClass.getMethod("loadSbtVersions", boolean.class, com.intellij.openapi.progress.ProgressIndicator.class); + final Seq sbtVersions = (Seq) loadSbtVersionsMethod.invoke(null, false, null); + final int size = sbtVersions.size(); + final String[] result = new String[size]; + for (int i = 0; i < size; i++) { + result[i] = sbtVersions.apply(i).toString(); + } + versions[0] = result; + } catch (final NoSuchMethodException e) { + // Fallback to old API + final Class versionsSbtClass = Class.forName("org.jetbrains.plugins.scala.project.Versions$SBT$"); + final Object module = versionsSbtClass.getField("MODULE$").get(null); + final Method loadMethod = module.getClass().getMethod("loadVersionsWithProgress", com.intellij.openapi.progress.ProgressIndicator.class); + final Object loadedVersions = loadMethod.invoke(module, (Object) null); + final Method versionsMethod = loadedVersions.getClass().getMethod("versions"); + final Seq versionSeq = (Seq) versionsMethod.invoke(loadedVersions); + versions[0] = (String[]) versionSeq.toArray(ClassTag.apply(String.class)); + } + } catch (final Exception e) { + log().warn("Failed to get SBT versions from scala plugin.", e); + versions[0] = new String[0]; + } }, null); for (String version : versions[0]) { From 0a0662cbfcfea09b83c5dba15d40da31e7646d85 Mon Sep 17 00:00:00 2001 From: Miller Wang Date: Tue, 10 Mar 2026 08:01:53 +0800 Subject: [PATCH 43/43] fix: adapt StreamingLogsConsoleView for ConsoleViewImpl.isDisposed() being private final In IntelliJ 261, ConsoleViewImpl.isDisposed() changed from protected/public to private final. Add a new public isDisposed() method (not an override) to maintain the existing public API for callers. --- .../streaminglog/StreamingLogsConsoleView.java | 12 ++++++++++++ .../azure-toolkit-for-intellij/build.gradle.kts | 8 +++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/streaminglog/StreamingLogsConsoleView.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/streaminglog/StreamingLogsConsoleView.java index 3add2cecc30..673f62e0b65 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/streaminglog/StreamingLogsConsoleView.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-lib/src/main/java/com/microsoft/azure/toolkit/intellij/common/streaminglog/StreamingLogsConsoleView.java @@ -14,10 +14,12 @@ public class StreamingLogsConsoleView extends ConsoleViewImpl { private static final String SEPARATOR = System.getProperty("line.separator"); + private boolean isDisposed; private Disposable subscription; public StreamingLogsConsoleView(@NotNull Project project) { super(project, true); + this.isDisposed = false; this.setUpdateFoldingsEnabled(false); } @@ -41,6 +43,15 @@ public boolean isActive() { return subscription != null && !subscription.isDisposed(); } + /** + * Returns whether this console view has been disposed. + * Note: In IntelliJ 261+, ConsoleViewImpl.isDisposed() is private final, + * so this is NOT an override but a new public method for callers. + */ + public boolean isDisposed() { + return this.isDisposed; + } + private void printlnToConsole(String message, ConsoleViewContentType consoleViewContentType) { this.print(message + SEPARATOR, consoleViewContentType); } @@ -48,6 +59,7 @@ private void printlnToConsole(String message, ConsoleViewContentType consoleView @Override public void dispose() { super.dispose(); + this.isDisposed = true; closeStreamingLog(); } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/build.gradle.kts b/PluginsAndFeatures/azure-toolkit-for-intellij/build.gradle.kts index 47bf01a5352..0f1c245f8f1 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/build.gradle.kts +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/build.gradle.kts @@ -172,8 +172,14 @@ intellijPlatform { pluginVerification { ides { - create(IntelliJPlatformType.IntellijIdeaCommunity, properties("platformVersion").get()) + // IC (Community) no longer published since 253; use IU (Ultimate) for verification + create(IntelliJPlatformType.IntellijIdeaUltimate, properties("platformVersion").get()) } + // Suppress known structural warnings — plugin ID/name historically contain "intellij" + freeArgs = listOf( + "-mute", "TemplateWordInPluginId", + "-mute", "TemplateWordInPluginName" + ) } }