diff --git a/.changes/3.63.json b/.changes/3.63.json
new file mode 100644
index 00000000000..806e763d55a
--- /dev/null
+++ b/.changes/3.63.json
@@ -0,0 +1,11 @@
+{
+ "date" : "2025-04-08",
+ "version" : "3.63",
+ "entries" : [ {
+ "type" : "feature",
+ "description" : "Enterprise users can choose their preferred Amazon Q profile to improve personalization and workflow across different business regions"
+ }, {
+ "type" : "bugfix",
+ "description" : "Amazon Q /doc: close diff tab and open README file in preview mode after user accept changes"
+ } ]
+}
\ No newline at end of file
diff --git a/.changes/3.64.json b/.changes/3.64.json
new file mode 100644
index 00000000000..942136a9fd8
--- /dev/null
+++ b/.changes/3.64.json
@@ -0,0 +1,8 @@
+{
+ "date" : "2025-04-10",
+ "version" : "3.64",
+ "entries" : [ {
+ "type" : "bugfix",
+ "description" : "Fix issue where IDE freezes when logging into Amazon Q"
+ } ]
+}
\ No newline at end of file
diff --git a/.changes/3.65.json b/.changes/3.65.json
new file mode 100644
index 00000000000..0c50976e354
--- /dev/null
+++ b/.changes/3.65.json
@@ -0,0 +1,8 @@
+{
+ "date" : "2025-04-10",
+ "version" : "3.65",
+ "entries" : [ {
+ "type" : "bugfix",
+ "description" : "Fix issue where Amazon Q cannot process chunks from local `@workspace` context"
+ } ]
+}
\ No newline at end of file
diff --git a/.changes/3.66.json b/.changes/3.66.json
new file mode 100644
index 00000000000..602551b5fd1
--- /dev/null
+++ b/.changes/3.66.json
@@ -0,0 +1,11 @@
+{
+ "date" : "2025-04-11",
+ "version" : "3.66",
+ "entries" : [ {
+ "type" : "feature",
+ "description" : "The logs emitted by the Agent during user command execution will be accepted and written to `.amazonq/dev/run_command.log` file in the user's local repository."
+ }, {
+ "type" : "bugfix",
+ "description" : "Unit test generation now completes successfully when using the `/test` command "
+ } ]
+}
\ No newline at end of file
diff --git a/.changes/3.67.json b/.changes/3.67.json
new file mode 100644
index 00000000000..ab39cadb25c
--- /dev/null
+++ b/.changes/3.67.json
@@ -0,0 +1,20 @@
+{
+ "date" : "2025-04-18",
+ "version" : "3.67",
+ "entries" : [ {
+ "type" : "bugfix",
+ "description" : "Amazon Q: Customization now resets with a warning if unavailable in the selected profile."
+ }, {
+ "type" : "bugfix",
+ "description" : "Q panel will get stuck while signin if users have multiple windows"
+ }, {
+ "type" : "bugfix",
+ "description" : "Fix integer overflow when local context index input is larger than 2GB"
+ }, {
+ "type" : "bugfix",
+ "description" : "Fix workspace index process quits when hitting a race condition"
+ }, {
+ "type" : "bugfix",
+ "description" : "Fix infinite loop when workspace indexing server fails to initialize"
+ } ]
+}
\ No newline at end of file
diff --git a/.changes/3.68.json b/.changes/3.68.json
new file mode 100644
index 00000000000..59672f4e793
--- /dev/null
+++ b/.changes/3.68.json
@@ -0,0 +1,20 @@
+{
+ "date" : "2025-04-23",
+ "version" : "3.68",
+ "entries" : [ {
+ "type" : "feature",
+ "description" : "Amazon Q: Show visual indicator in status bar if profile selection is needed to continue with Q Inline / Q Chat"
+ }, {
+ "type" : "feature",
+ "description" : "Amazon Q /test: Remove unsupported message for non-java python languages"
+ }, {
+ "type" : "bugfix",
+ "description" : "/dev: Fix prompt to enable devfile build not triggering when devfile is present."
+ }, {
+ "type" : "bugfix",
+ "description" : "/review disable auto scan by default"
+ }, {
+ "type" : "bugfix",
+ "description" : "/review: disabled highlighter for ignored issues"
+ } ]
+}
\ No newline at end of file
diff --git a/.changes/next-release/bugfix-19118cf8-9378-4bd6-bf5e-7e57520181d0.json b/.changes/next-release/bugfix-19118cf8-9378-4bd6-bf5e-7e57520181d0.json
deleted file mode 100644
index 4121214a68a..00000000000
--- a/.changes/next-release/bugfix-19118cf8-9378-4bd6-bf5e-7e57520181d0.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "type" : "bugfix",
- "description" : "Amazon Q /doc: close diff tab and open README file in preview mode after user accept changes"
-}
\ No newline at end of file
diff --git a/.changes/next-release/removal-5979b9e4-898d-405a-9fc4-5919bb972a0e.json b/.changes/next-release/removal-5979b9e4-898d-405a-9fc4-5919bb972a0e.json
new file mode 100644
index 00000000000..0a712912815
--- /dev/null
+++ b/.changes/next-release/removal-5979b9e4-898d-405a-9fc4-5919bb972a0e.json
@@ -0,0 +1,4 @@
+{
+ "type" : "removal",
+ "description" : "Removed support for 2024.1.x IDEs"
+}
\ No newline at end of file
diff --git a/.changes/next-release/removal-6b54ddb0-1385-4788-a225-ddae6ac958d2.json b/.changes/next-release/removal-6b54ddb0-1385-4788-a225-ddae6ac958d2.json
new file mode 100644
index 00000000000..a9c2061c505
--- /dev/null
+++ b/.changes/next-release/removal-6b54ddb0-1385-4788-a225-ddae6ac958d2.json
@@ -0,0 +1,4 @@
+{
+ "type" : "removal",
+ "description" : "Removed support for Gateway 2024.3"
+}
\ No newline at end of file
diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml
index 7747ca912ff..8b5d44c0f46 100644
--- a/.github/workflows/prerelease.yml
+++ b/.github/workflows/prerelease.yml
@@ -18,7 +18,7 @@ jobs:
strategy:
matrix:
build_target: [ ':plugin-core:buildPlugin', ':plugin-toolkit:intellij-standalone:buildPlugin', ':plugin-amazonq:buildPlugin' ]
- version: [ '2024.1', '2024.2', '2024.3', '2025.1' ]
+ version: [ '2024.2', '2024.3', '2025.1' ]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
diff --git a/.run/Run AWS Toolkit - Community [2024.1].run.xml b/.run/Run AWS Toolkit - Community [2024.1].run.xml
deleted file mode 100644
index 3d66c9885ff..00000000000
--- a/.run/Run AWS Toolkit - Community [2024.1].run.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- false
- true
- false
- false
-
-
-
\ No newline at end of file
diff --git a/.run/Run AWS Toolkit - Gateway [2024.3].run.xml b/.run/Run AWS Toolkit - Gateway [2024.3].run.xml
deleted file mode 100644
index f59c70407d5..00000000000
--- a/.run/Run AWS Toolkit - Gateway [2024.3].run.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- false
- true
- false
- false
-
-
-
\ No newline at end of file
diff --git a/.run/Run AWS Toolkit - Rider [2024.1].run.xml b/.run/Run AWS Toolkit - Rider [2024.1].run.xml
deleted file mode 100644
index 6748da64f44..00000000000
--- a/.run/Run AWS Toolkit - Rider [2024.1].run.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- false
- true
- false
- false
-
-
-
\ No newline at end of file
diff --git a/.run/Run AWS Toolkit - Ultimate [2024.1].run.xml b/.run/Run AWS Toolkit - Ultimate [2024.1].run.xml
deleted file mode 100644
index b1f9246c803..00000000000
--- a/.run/Run AWS Toolkit - Ultimate [2024.1].run.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- false
- true
- false
- false
-
-
-
\ No newline at end of file
diff --git a/.run/Run All - Community [2024.1].run.xml b/.run/Run All - Community [2024.1].run.xml
deleted file mode 100644
index e1760d16510..00000000000
--- a/.run/Run All - Community [2024.1].run.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- false
- true
- false
- false
-
-
-
\ No newline at end of file
diff --git a/.run/Run All - Rider [2024.1].run.xml b/.run/Run All - Rider [2024.1].run.xml
deleted file mode 100644
index 2fe2c1b538f..00000000000
--- a/.run/Run All - Rider [2024.1].run.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- false
- true
- false
- false
-
-
-
\ No newline at end of file
diff --git a/.run/Run All - Ultimate [2024.1].run.xml b/.run/Run All - Ultimate [2024.1].run.xml
deleted file mode 100644
index 606517ed2ed..00000000000
--- a/.run/Run All - Ultimate [2024.1].run.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- false
- true
- false
- false
-
-
-
\ No newline at end of file
diff --git a/.run/Run Amazon Q - Community [2024.1].run.xml b/.run/Run Amazon Q - Community [2024.1].run.xml
deleted file mode 100644
index 48037481044..00000000000
--- a/.run/Run Amazon Q - Community [2024.1].run.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- false
- true
- false
- false
-
-
-
\ No newline at end of file
diff --git a/.run/Run Amazon Q - Rider [2024.1].run.xml b/.run/Run Amazon Q - Rider [2024.1].run.xml
deleted file mode 100644
index a66d2fb3045..00000000000
--- a/.run/Run Amazon Q - Rider [2024.1].run.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- false
- true
- false
- false
-
-
-
\ No newline at end of file
diff --git a/.run/Run Amazon Q - Ultimate [2024.1].run.xml b/.run/Run Amazon Q - Ultimate [2024.1].run.xml
deleted file mode 100644
index c16f5123324..00000000000
--- a/.run/Run Amazon Q - Ultimate [2024.1].run.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- false
- true
- false
- false
-
-
-
\ No newline at end of file
diff --git a/.run/generateConfigs.py b/.run/generateConfigs.py
index 7a939d627c8..2041a6a85e9 100644
--- a/.run/generateConfigs.py
+++ b/.run/generateConfigs.py
@@ -52,7 +52,7 @@ def write_config(mv: str, ide: IdeVariant, plugin: PluginVariant):
f.write(TEMPLATE.format(plugin = plugin, variant = ide, major_version = mv))
if __name__ == '__main__':
- mvs = ["2024.1", "2024.2", "2024.3", "2025.1"]
+ mvs = ["2024.2", "2024.3", "2025.1"]
ides = [
IdeVariant("Community", "IC"),
IdeVariant("Rider", "RD"),
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 321e017b9fe..2e673ce7a2f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,31 @@
+# _3.68_ (2025-04-23)
+- **(Feature)** Amazon Q: Show visual indicator in status bar if profile selection is needed to continue with Q Inline / Q Chat
+- **(Feature)** Amazon Q /test: Remove unsupported message for non-java python languages
+- **(Bug Fix)** /dev: Fix prompt to enable devfile build not triggering when devfile is present.
+- **(Bug Fix)** /review disable auto scan by default
+- **(Bug Fix)** /review: disabled highlighter for ignored issues
+
+# _3.67_ (2025-04-18)
+- **(Bug Fix)** Amazon Q: Customization now resets with a warning if unavailable in the selected profile.
+- **(Bug Fix)** Q panel will get stuck while signin if users have multiple windows
+- **(Bug Fix)** Fix integer overflow when local context index input is larger than 2GB
+- **(Bug Fix)** Fix workspace index process quits when hitting a race condition
+- **(Bug Fix)** Fix infinite loop when workspace indexing server fails to initialize
+
+# _3.66_ (2025-04-11)
+- **(Feature)** The logs emitted by the Agent during user command execution will be accepted and written to `.amazonq/dev/run_command.log` file in the user's local repository.
+- **(Bug Fix)** Unit test generation now completes successfully when using the `/test` command
+
+# _3.64_ (2025-04-10)
+- **(Bug Fix)** Fix issue where IDE freezes when logging into Amazon Q
+
+# _3.65_ (2025-04-10)
+- **(Bug Fix)** Fix issue where Amazon Q cannot process chunks from local `@workspace` context
+
+# _3.63_ (2025-04-08)
+- **(Feature)** Enterprise users can choose their preferred Amazon Q profile to improve personalization and workflow across different business regions
+- **(Bug Fix)** Amazon Q /doc: close diff tab and open README file in preview mode after user accept changes
+
# _3.62_ (2025-04-03)
- **(Feature)** /review: automatically generate fix without clicking Generate Fix button
- **(Bug Fix)** /transform: prompt user to re-authenticate if credentials expire during transformation
diff --git a/buildSrc/src/main/kotlin/software/aws/toolkits/gradle/BuildScriptUtils.kt b/buildSrc/src/main/kotlin/software/aws/toolkits/gradle/BuildScriptUtils.kt
index 84c520c9b12..b5b02fc20b8 100644
--- a/buildSrc/src/main/kotlin/software/aws/toolkits/gradle/BuildScriptUtils.kt
+++ b/buildSrc/src/main/kotlin/software/aws/toolkits/gradle/BuildScriptUtils.kt
@@ -22,16 +22,13 @@ fun Project.ciOnly(block: () -> Unit) {
fun Project.isCi() : Boolean = providers.environmentVariable("CI").isPresent
fun Project.jvmTarget(): Provider = withCurrentProfileName {
- when (it) {
- "2023.3", "2024.1" -> JavaVersion.VERSION_17
- else -> JavaVersion.VERSION_21
- }
+ JavaVersion.VERSION_21
}
// https://plugins.jetbrains.com/docs/intellij/using-kotlin.html#stdlib-miscellaneous
fun Project.kotlinTarget(): Provider = withCurrentProfileName {
when (it) {
- "2023.3", "2024.1", "2024.2" -> KotlinVersionEnum.KOTLIN_1_9
+ "2024.2" -> KotlinVersionEnum.KOTLIN_1_9
"2024.3" -> KotlinVersionEnum.KOTLIN_2_0
"2025.1" -> KotlinVersionEnum.KOTLIN_2_1
else -> error("not set")
diff --git a/buildSrc/src/main/kotlin/software/aws/toolkits/gradle/intellij/IdeVersions.kt b/buildSrc/src/main/kotlin/software/aws/toolkits/gradle/intellij/IdeVersions.kt
index 9a9107d503b..db4062b9f68 100644
--- a/buildSrc/src/main/kotlin/software/aws/toolkits/gradle/intellij/IdeVersions.kt
+++ b/buildSrc/src/main/kotlin/software/aws/toolkits/gradle/intellij/IdeVersions.kt
@@ -26,78 +26,6 @@ object IdeVersions {
)
private val ideProfiles = listOf(
- Profile(
- name = "2023.3",
- community = ProductProfile(
- sdkVersion = "2023.3",
- bundledPlugins = commonPlugins + listOf(
- "com.intellij.java",
- "com.intellij.gradle",
- "org.jetbrains.idea.maven",
- ),
- marketplacePlugins = listOf(
- "PythonCore:233.11799.241",
- "Docker:233.11799.244"
- )
- ),
- ultimate = ProductProfile(
- sdkVersion = "2023.3",
- bundledPlugins = commonPlugins + listOf(
- "JavaScript",
- "JavaScriptDebugger",
- "com.intellij.database",
- "com.jetbrains.codeWithMe",
- ),
- marketplacePlugins = listOf(
- "Pythonid:233.11799.241",
- "org.jetbrains.plugins.go:233.11799.196",
- )
- ),
- rider = RiderProfile(
- sdkVersion = "2023.3",
- bundledPlugins = commonPlugins,
- netFrameworkTarget = "net472",
- rdGenVersion = "2023.3.2",
- nugetVersion = "2023.3.0"
- )
- ),
- Profile(
- name = "2024.1",
- community = ProductProfile(
- sdkVersion = "2024.1",
- bundledPlugins = commonPlugins + listOf(
- "com.intellij.java",
- "com.intellij.gradle",
- "org.jetbrains.idea.maven",
- ),
- marketplacePlugins = listOf(
- "org.toml.lang:241.14494.150",
- "PythonCore:241.14494.240",
- "Docker:241.14494.251"
- )
- ),
- ultimate = ProductProfile(
- sdkVersion = "2024.1",
- bundledPlugins = commonPlugins + listOf(
- "JavaScript",
- "JavaScriptDebugger",
- "com.intellij.database",
- "com.jetbrains.codeWithMe",
- ),
- marketplacePlugins = listOf(
- "org.toml.lang:241.14494.150",
- "Pythonid:241.14494.314",
- "org.jetbrains.plugins.go:241.14494.240",
- )
- ),
- rider = RiderProfile(
- sdkVersion = "2024.1",
- bundledPlugins = commonPlugins,
- netFrameworkTarget = "net472",
- rdGenVersion = "2024.1.1",
- nugetVersion = "2024.1.0"
- )
- ),
Profile(
name = "2024.2",
community = ProductProfile(
@@ -137,10 +65,6 @@ object IdeVersions {
),
Profile(
name = "2024.3",
- gateway = ProductProfile(
- sdkVersion = "243.21565.196-CUSTOM-SNAPSHOT",
- bundledPlugins = listOf("org.jetbrains.plugins.terminal")
- ),
community = ProductProfile(
sdkVersion = "2024.3",
bundledPlugins = commonPlugins + listOf(
diff --git a/gradle.properties b/gradle.properties
index 5921806d5e7..aa67fe52d2d 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -2,7 +2,7 @@
# SPDX-License-Identifier: Apache-2.0
# Toolkit Version
-toolkitVersion=3.63-SNAPSHOT
+toolkitVersion=3.69-SNAPSHOT
# Publish Settings
publishToken=
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index e017dce6fed..1d5acb6e703 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -106,6 +106,7 @@ kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref =
kotlin-stdLibJdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" }
+mockito-junit-jupiter = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" }
mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoKotlin" }
mockk = { module = "io.mockk:mockk", version.ref="mockk" }
nimbus-jose-jwt = {module = "com.nimbusds:nimbus-jose-jwt", version.ref = "nimbus-jose-jwt"}
@@ -121,7 +122,7 @@ zjsonpatch = { module = "com.flipkart.zjsonpatch:zjsonpatch", version.ref = "zjs
[bundles]
jackson = ["jackson-datetime", "jackson-kotlin", "jackson-yaml", "jackson-xml"]
kotlin = ["kotlin-stdLibJdk8", "kotlin-reflect"]
-mockito = ["mockito-core", "mockito-kotlin"]
+mockito = ["mockito-core", "mockito-junit-jupiter", "mockito-kotlin"]
sshd = ["sshd-core", "sshd-scp", "sshd-sftp"]
[plugins]
diff --git a/kotlinResolution.settings.gradle.kts b/kotlinResolution.settings.gradle.kts
index cf5ce3351f2..d78853740bc 100644
--- a/kotlinResolution.settings.gradle.kts
+++ b/kotlinResolution.settings.gradle.kts
@@ -6,10 +6,6 @@ dependencyResolutionManagement {
maybeCreate("libs").apply {
// pull value from IJ library list: https://github.com/JetBrains/intellij-community/blob//.idea/libraries/kotlinx_coroutines_core.xml
val version = when (providers.gradleProperty("ideProfileName").getOrNull() ?: return@apply) {
- "2023.3", "2024.1" -> {
- "1.7.3"
- }
-
"2024.2" -> {
"1.8.0-intellij-9"
}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/clients/AmazonQCodeGenerateClient.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/clients/AmazonQCodeGenerateClient.kt
index 2c28c3240f3..3957d9fdf46 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/clients/AmazonQCodeGenerateClient.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/common/clients/AmazonQCodeGenerateClient.kt
@@ -31,10 +31,10 @@ import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.info
import software.aws.toolkits.jetbrains.common.session.Intent
-import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
import software.aws.toolkits.jetbrains.services.amazonq.clients.AmazonQStreamingClient
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
import software.aws.toolkits.jetbrains.services.amazonqDoc.FEATURE_EVALUATION_PRODUCT_NAME
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.calculateTotalLatency
import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata
@@ -72,7 +72,7 @@ class AmazonQCodeGenerateClient(private val project: Project) {
fun connection() = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance())
?: error("Attempted to use connection while one does not exist")
- fun bearerClient() = connection().getConnectionSettings().awsClient()
+ fun bearerClient() = QRegionProfileManager.getInstance().getQClient(project)
private val amazonQStreamingClient
get() = AmazonQStreamingClient.getInstance(project)
@@ -88,6 +88,7 @@ class AmazonQCodeGenerateClient(private val project: Project) {
}
requestBuilder.optOutPreference(getTelemetryOptOutPreference())
requestBuilder.userContext(docUserContext)
+ requestBuilder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}
fun sendDocMetricData(operationName: String, result: String): SendTelemetryEventResponse =
@@ -118,7 +119,9 @@ class AmazonQCodeGenerateClient(private val project: Project) {
}
fun createTaskAssistConversation(): CreateTaskAssistConversationResponse = bearerClient().createTaskAssistConversation(
- CreateTaskAssistConversationRequest.builder().build()
+ CreateTaskAssistConversationRequest.builder()
+ .profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
+ .build()
)
fun createTaskAssistUploadUrl(conversationId: String, contentChecksumSha256: String, contentLength: Long): CreateUploadUrlResponse =
@@ -137,6 +140,7 @@ class AmazonQCodeGenerateClient(private val project: Project) {
)
.build()
)
+ .profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}
fun startTaskAssistCodeGeneration(conversationId: String, uploadId: String, userMessage: String, intent: Intent): StartTaskAssistCodeGenerationResponse =
@@ -155,6 +159,7 @@ class AmazonQCodeGenerateClient(private val project: Project) {
.uploadId(uploadId)
}
.intent(intent.name)
+ .profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}
fun getTaskAssistCodeGeneration(conversationId: String, codeGenerationId: String): GetTaskAssistCodeGenerationResponse = bearerClient()
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QLoginWebview.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QLoginWebview.kt
index 67ceff60677..1d614bef9bf 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QLoginWebview.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QLoginWebview.kt
@@ -3,9 +3,11 @@
package software.aws.toolkits.jetbrains.services.amazonq
+import com.intellij.ide.BrowserUtil
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DataContext
+import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
@@ -16,6 +18,7 @@ import com.intellij.ui.components.panels.Wrapper
import com.intellij.ui.dsl.builder.Align
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.jcef.JBCefJSQuery
+import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
@@ -32,14 +35,20 @@ import software.aws.toolkits.jetbrains.core.webview.BrowserState
import software.aws.toolkits.jetbrains.core.webview.LocalAssetJBCefRequestHandler
import software.aws.toolkits.jetbrains.core.webview.LoginBrowser
import software.aws.toolkits.jetbrains.isDeveloperMode
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QProfileSwitchIntent
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
import software.aws.toolkits.jetbrains.services.amazonq.util.createBrowser
import software.aws.toolkits.jetbrains.utils.isQConnected
import software.aws.toolkits.jetbrains.utils.isQExpired
import software.aws.toolkits.jetbrains.utils.isQWebviewsAvailable
import software.aws.toolkits.telemetry.FeatureId
+import software.aws.toolkits.telemetry.MetricResult
+import software.aws.toolkits.telemetry.Telemetry
import software.aws.toolkits.telemetry.UiTelemetry
import software.aws.toolkits.telemetry.WebviewTelemetry
import java.awt.event.ActionListener
+import java.net.URI
import javax.swing.JButton
import javax.swing.JComponent
@@ -192,6 +201,26 @@ class QWebviewBrowser(val project: Project, private val parentDisposable: Dispos
UiTelemetry.click(project, signInOption)
}
}
+
+ is BrowserMessage.SwitchProfile -> {
+ QRegionProfileManager.getInstance().switchProfile(
+ project,
+ QRegionProfile(profileName = message.profileName, arn = message.arn),
+ intent = QProfileSwitchIntent.Auth
+ )
+ }
+
+ is BrowserMessage.ListProfiles -> {
+ handleListProfilesMessage()
+ }
+
+ is BrowserMessage.PublishWebviewTelemetry -> {
+// publishTelemetry(message)
+ }
+
+ is BrowserMessage.OpenUrl -> {
+ BrowserUtil.browse(URI(message.externalLink))
+ }
}
}
@@ -231,28 +260,44 @@ class QWebviewBrowser(val project: Project, private val parentDisposable: Dispos
writeValueAsString(it)
}
- // TODO: pass "REAUTH" if connection expires
val stage = if (isQExpired(project)) {
"REAUTH"
+ } else if (isQConnected(project) && QRegionProfileManager.getInstance().isPendingProfileSelection(project)) {
+ "PROFILE_SELECT"
} else {
"START"
}
- val jsonData = """
- {
- stage: '$stage',
- regions: $regions,
- idcInfo: {
- profileName: '${lastLoginIdcInfo.profileName}',
- startUrl: '${lastLoginIdcInfo.startUrl}',
- region: '${lastLoginIdcInfo.region}'
- },
- cancellable: ${state.browserCancellable},
- feature: '${state.feature}',
- existConnections: ${writeValueAsString(selectionSettings.values.map { it.currentSelection }.toList())}
+ when (stage) {
+ "PROFILE_SELECT" -> {
+ val jsonData = """
+ {
+ stage: '$stage',
+ status: 'pending'
+ }
+ """.trimIndent()
+ executeJS("window.ideClient.prepareUi($jsonData)")
+ }
+
+ else -> {
+ val jsonData = """
+ {
+ stage: '$stage',
+ regions: $regions,
+ idcInfo: {
+ profileName: '${lastLoginIdcInfo.profileName}',
+ startUrl: '${lastLoginIdcInfo.startUrl}',
+ region: '${lastLoginIdcInfo.region}'
+ },
+ cancellable: ${state.browserCancellable},
+ feature: '${state.feature}',
+ existConnections: ${writeValueAsString(selectionSettings.values.map { it.currentSelection }.toList())},
+ }
+ """.trimIndent()
+
+ executeJS("window.ideClient.prepareUi($jsonData)")
}
- """.trimIndent()
- executeJS("window.ideClient.prepareUi($jsonData)")
+ }
}
override fun loginIAM(profileName: String, accessKey: String, secretKey: String) {
@@ -269,6 +314,52 @@ class QWebviewBrowser(val project: Project, private val parentDisposable: Dispos
jcefBrowser.loadURL(assetHandler.createResource("content.html", getWebviewHTML(webScriptUri, query)))
}
+ private fun handleListProfilesMessage() {
+ ApplicationManager.getApplication().executeOnPooledThread {
+ var errorMessage = ""
+ val profiles = try {
+ QRegionProfileManager.getInstance().listRegionProfiles(project)
+ } catch (e: Exception) {
+ e.message?.let {
+ errorMessage = it
+ }
+ LOG.warn { "Failed to call listRegionProfiles API: $errorMessage" }
+ val qConn = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance())
+ Telemetry.amazonq.didSelectProfile.use { span ->
+ span.source(QProfileSwitchIntent.Auth.value)
+ .amazonQProfileRegion(QRegionProfileManager.getInstance().activeProfile(project)?.region ?: "not-set")
+ .ssoRegion((qConn as? AwsBearerTokenConnection)?.region)
+ .credentialStartUrl((qConn as? AwsBearerTokenConnection)?.startUrl)
+ .result(MetricResult.Failed)
+ .reason(e.message)
+ }
+
+ null
+ }
+
+ // auto-select the profile if users only have 1 and don't show the UI
+ if (profiles?.size == 1) {
+ LOG.debug { "User only have access to 1 Q profile, auto-selecting profile ${profiles.first().profileName} for ${project.name}" }
+ QRegionProfileManager.getInstance().switchProfile(project, profiles.first(), QProfileSwitchIntent.Update)
+ return@executeOnPooledThread
+ }
+
+ // required EDT as this entire block is executed on thread pool
+ runInEdt {
+ val jsonData = """
+ {
+ stage: 'PROFILE_SELECT',
+ status: '${if (profiles != null) "succeeded" else "failed"}',
+ profiles: ${writeValueAsString(profiles ?: "")},
+ errorMessage: '$errorMessage'
+ }
+ """.trimIndent()
+
+ executeJS("window.ideClient.prepareUi($jsonData)")
+ }
+ }
+ }
+
companion object {
private val LOG = getLogger()
private const val WEB_SCRIPT = "js/getStart.js"
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/startup/AmazonQStartupActivity.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/startup/AmazonQStartupActivity.kt
index f114ae98239..b85d94db10f 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/startup/AmazonQStartupActivity.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/startup/AmazonQStartupActivity.kt
@@ -19,6 +19,8 @@ import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
import software.aws.toolkits.jetbrains.core.gettingstarted.emitUserState
import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
import software.aws.toolkits.jetbrains.services.amazonq.project.ProjectContextController
import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindow
import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindowFactory
@@ -52,6 +54,10 @@ class AmazonQStartupActivity : ProjectActivity {
CodeWhispererExplorerActionManager.getInstance().setIsFirstRestartAfterQInstall(false)
}
}
+
+ QRegionProfileManager.getInstance().validateProfile(project)
+
+ AmazonQLspService.getInstance(project)
startLsp(project)
if (runOnce.get()) return
emitUserState(project)
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt
index 15a533d1191..4c1191cd85d 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt
@@ -24,6 +24,7 @@ import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage
import software.aws.toolkits.jetbrains.services.amazonq.messages.MessageConnector
import software.aws.toolkits.jetbrains.services.amazonq.onboarding.OnboardingPageInteraction
import software.aws.toolkits.jetbrains.services.amazonq.onboarding.OnboardingPageInteractionType
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
import software.aws.toolkits.jetbrains.services.amazonq.util.highlightCommand
import software.aws.toolkits.jetbrains.services.amazonq.webview.BrowserConnector
import software.aws.toolkits.jetbrains.services.amazonq.webview.FqnWebviewAdapter
@@ -66,7 +67,9 @@ class AmazonQToolWindow private constructor(
connectUi()
connectApps()
- ApplicationManager.getApplication().messageBus.syncPublisher(LafManagerListener.TOPIC).lookAndFeelChanged(LafManager.getInstance())
+ runInEdt {
+ ApplicationManager.getApplication().messageBus.syncPublisher(LafManagerListener.TOPIC).lookAndFeelChanged(LafManager.getInstance())
+ }
}
private fun sendMessage(message: AmazonQMessage, tabType: String) {
@@ -127,7 +130,8 @@ class AmazonQToolWindow private constructor(
isCodeScanAvailable = isCodeScanAvailable(project),
isCodeTestAvailable = isCodeTestAvailable(project),
isDocAvailable = isDocAvailable(project),
- highlightCommand = highlightCommand()
+ highlightCommand = highlightCommand(),
+ activeProfile = QRegionProfileManager.getInstance().takeIf { it.shouldDisplayProfileInfo(project) }?.activeProfile(project)
)
scope.launch {
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindowFactory.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindowFactory.kt
index 3435075caaf..f9279eb61be 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindowFactory.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindowFactory.kt
@@ -14,12 +14,10 @@ import com.intellij.ui.components.panels.Wrapper
import com.intellij.util.ui.components.BorderLayoutPanel
import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.getLogger
-import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManagerListener
import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
-import software.aws.toolkits.jetbrains.core.credentials.sono.Q_SCOPES
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenAuthState
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProviderListener
import software.aws.toolkits.jetbrains.core.notifications.NotificationPanel
@@ -28,6 +26,9 @@ import software.aws.toolkits.jetbrains.core.webview.BrowserState
import software.aws.toolkits.jetbrains.services.amazonq.QWebviewPanel
import software.aws.toolkits.jetbrains.services.amazonq.RefreshQChatPanelButtonPressedListener
import software.aws.toolkits.jetbrains.services.amazonq.gettingstarted.openMeetQPage
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileSelectedListener
import software.aws.toolkits.jetbrains.utils.isQConnected
import software.aws.toolkits.jetbrains.utils.isQExpired
import software.aws.toolkits.jetbrains.utils.isQWebviewsAvailable
@@ -62,7 +63,10 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
ToolkitConnectionManagerListener.TOPIC,
object : ToolkitConnectionManagerListener {
override fun activeConnectionChanged(newConnection: ToolkitConnection?) {
- onConnectionChanged(project, newConnection, qPanel)
+ ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance())?.let { qConn ->
+ openMeetQPage(project)
+ }
+ prepareChatContent(project, qPanel)
}
}
)
@@ -71,9 +75,7 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
RefreshQChatPanelButtonPressedListener.TOPIC,
object : RefreshQChatPanelButtonPressedListener {
override fun onRefresh() {
- runInEdt {
- prepareChatContent(project, qPanel)
- }
+ prepareChatContent(project, qPanel)
}
}
)
@@ -83,16 +85,25 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
object : BearerTokenProviderListener {
override fun onChange(providerId: String, newScopes: List?) {
if (ToolkitConnectionManager.getInstance(project).connectionStateForFeature(QConnection.getInstance()) == BearerTokenAuthState.AUTHORIZED) {
- val qComponent = AmazonQToolWindow.getInstance(project).component
-
- runInEdt {
- qPanel.setContent(qComponent)
- }
+ AmazonQToolWindow.getInstance(project).disposeAndRecreate()
+ prepareChatContent(project, qPanel)
}
}
}
)
+ project.messageBus.connect(toolWindow.disposable).subscribe(
+ QRegionProfileSelectedListener.TOPIC,
+ object : QRegionProfileSelectedListener {
+ // note we name myProject intentionally ow it will shadow the "project" provided by the IDE
+ override fun onProfileSelected(myProject: Project, profile: QRegionProfile?) {
+ if (project.isDisposed) return
+ AmazonQToolWindow.getInstance(project).disposeAndRecreate()
+ qPanel.setContent(AmazonQToolWindow.getInstance(project).component)
+ }
+ }
+ )
+
prepareChatContent(project, qPanel)
val content = contentManager.factory.createContent(mainPanel, null, false).also {
@@ -107,13 +118,21 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
project: Project,
qPanel: Wrapper,
) {
- val component = if (isQConnected(project) && !isQExpired(project)) {
+ /**
+ * only render Q Chat when
+ * 1. There is a Q connection
+ * 2. Q connection is not expired
+ * 3. User is not pending region profile selection
+ */
+ val component = if (isQConnected(project) && !isQExpired(project) && !QRegionProfileManager.getInstance().isPendingProfileSelection(project)) {
AmazonQToolWindow.getInstance(project).component
} else {
QWebviewPanel.getInstance(project).browser?.prepareBrowser(BrowserState(FeatureId.AmazonQ))
QWebviewPanel.getInstance(project).component
}
- qPanel.setContent(component)
+ runInEdt {
+ qPanel.setContent(component)
+ }
}
override fun init(toolWindow: ToolWindow) {
@@ -134,36 +153,6 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
override fun shouldBeAvailable(project: Project): Boolean = isQWebviewsAvailable()
- private fun onConnectionChanged(project: Project, newConnection: ToolkitConnection?, qPanel: Wrapper) {
- val isNewConnectionForQ = newConnection?.let {
- (it as? AwsBearerTokenConnection)?.let { conn ->
- val scopeShouldHave = Q_SCOPES
-
- LOG.debug { "newConnection: ${conn.id}; scope: ${conn.scopes}; scope must-have: $scopeShouldHave" }
-
- scopeShouldHave.all { s -> s in conn.scopes }
- } ?: false
- } ?: false
-
- if (isNewConnectionForQ) {
- openMeetQPage(project)
- }
-
- QWebviewPanel.getInstance(project).browser?.prepareBrowser(BrowserState(FeatureId.AmazonQ))
-
- // isQConnected alone is not robust and there is race condition (read/update connection states)
- val component = if (isNewConnectionForQ || (isQConnected(project) && !isQExpired(project))) {
- LOG.debug { "returning Q-chat window; isQConnection=$isNewConnectionForQ; hasPinnedConnection=$isNewConnectionForQ" }
- AmazonQToolWindow.getInstance(project).component
- } else {
- LOG.debug { "returning login window; no Q connection found" }
- QWebviewPanel.getInstance(project).component
- }
- runInEdt {
- qPanel.setContent(component)
- }
- }
-
companion object {
private val LOG = getLogger()
const val WINDOW_ID = AMAZON_Q_WINDOW_ID
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/Browser.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/Browser.kt
index a62c8e1191f..40f2b76baa2 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/Browser.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/Browser.kt
@@ -8,6 +8,7 @@ import com.intellij.openapi.Disposable
import com.intellij.openapi.util.Disposer
import com.intellij.ui.jcef.JBCefJSQuery
import software.aws.toolkits.jetbrains.core.webview.LocalAssetJBCefRequestHandler
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile
import software.aws.toolkits.jetbrains.services.amazonq.util.HighlightCommand
import software.aws.toolkits.jetbrains.services.amazonq.util.createBrowser
import software.aws.toolkits.jetbrains.settings.MeetQSettings
@@ -40,8 +41,9 @@ class Browser(parent: Disposable) : Disposable {
isCodeScanAvailable: Boolean,
isCodeTestAvailable: Boolean,
highlightCommand: HighlightCommand?,
+ activeProfile: QRegionProfile?,
) {
- loadWebView(isCodeTransformAvailable, isFeatureDevAvailable, isDocAvailable, isCodeScanAvailable, isCodeTestAvailable, highlightCommand)
+ loadWebView(isCodeTransformAvailable, isFeatureDevAvailable, isDocAvailable, isCodeScanAvailable, isCodeTestAvailable, highlightCommand, activeProfile)
}
override fun dispose() {
@@ -63,6 +65,7 @@ class Browser(parent: Disposable) : Disposable {
isCodeScanAvailable: Boolean,
isCodeTestAvailable: Boolean,
highlightCommand: HighlightCommand?,
+ activeProfile: QRegionProfile?,
) {
// setup empty state. The message request handlers use this for storing state
// that's persistent between page loads.
@@ -72,7 +75,15 @@ class Browser(parent: Disposable) : Disposable {
jcefBrowser.loadURL(
assetRequestHandler.createResource(
"webview/chat.html",
- getWebviewHTML(isCodeTransformAvailable, isFeatureDevAvailable, isDocAvailable, isCodeScanAvailable, isCodeTestAvailable, highlightCommand)
+ getWebviewHTML(
+ isCodeTransformAvailable,
+ isFeatureDevAvailable,
+ isDocAvailable,
+ isCodeScanAvailable,
+ isCodeTestAvailable,
+ highlightCommand,
+ activeProfile,
+ )
)
)
}
@@ -88,6 +99,7 @@ class Browser(parent: Disposable) : Disposable {
isCodeScanAvailable: Boolean,
isCodeTestAvailable: Boolean,
highlightCommand: HighlightCommand?,
+ activeProfile: QRegionProfile?,
): String {
val postMessageToJavaJsCode = receiveMessageQuery.inject("JSON.stringify(message)")
@@ -108,7 +120,8 @@ class Browser(parent: Disposable) : Disposable {
$isDocAvailable, // whether /doc is available
$isCodeScanAvailable, // whether /scan is available
$isCodeTestAvailable, // whether /test is available
- ${OBJECT_MAPPER.writeValueAsString(highlightCommand)}
+ ${OBJECT_MAPPER.writeValueAsString(highlightCommand)},
+ "${activeProfile?.profileName.orEmpty()}"
);
}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanChatApp.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanChatApp.kt
index 166223d623a..0aa8dc42b04 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanChatApp.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/CodeScanChatApp.kt
@@ -21,6 +21,8 @@ import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQApp
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController
import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileSelectedListener
import software.aws.toolkits.jetbrains.services.amazonqCodeScan.auth.isCodeScanAvailable
import software.aws.toolkits.jetbrains.services.amazonqCodeScan.commands.CodeScanActionMessage
import software.aws.toolkits.jetbrains.services.amazonqCodeScan.commands.CodeScanMessageListener
@@ -141,6 +143,15 @@ class CodeScanChatApp(private val scope: CoroutineScope) : AmazonQApp {
}
}
)
+
+ context.project.messageBus.connect(this).subscribe(
+ QRegionProfileSelectedListener.TOPIC,
+ object : QRegionProfileSelectedListener {
+ override fun onProfileSelected(project: Project, profile: QRegionProfile?) {
+ chatSessionStorage.deleteAllSessions()
+ }
+ }
+ )
}
private fun getQTokenProvider(project: Project) = (
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/storage/ChatSessionStorage.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/storage/ChatSessionStorage.kt
index fb05a7beda8..57dcb85f44a 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/storage/ChatSessionStorage.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeScan/storage/ChatSessionStorage.kt
@@ -30,4 +30,8 @@ class ChatSessionStorage {
fun changeAuthenticationNeededNotified(authNeededNotified: Boolean) {
sessions.keys.forEach { sessions[it]?.authNeededNotified = authNeededNotified }
}
+
+ fun deleteAllSessions() {
+ sessions.clear()
+ }
}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeTestChatApp.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeTestChatApp.kt
index 9da12272cdc..7972e45fb9c 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeTestChatApp.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeTestChatApp.kt
@@ -4,6 +4,7 @@
package software.aws.toolkits.jetbrains.services.amazonqCodeTest
import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.project.Project
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection
@@ -11,6 +12,8 @@ import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQApp
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileSelectedListener
import software.aws.toolkits.jetbrains.services.amazonqCodeScan.auth.isCodeScanAvailable
import software.aws.toolkits.jetbrains.services.amazonqCodeTest.auth.isCodeTestAvailable
import software.aws.toolkits.jetbrains.services.amazonqCodeTest.controller.CodeTestChatController
@@ -71,6 +74,15 @@ class CodeTestChatApp(private val scope: CoroutineScope) : AmazonQApp {
}
}
)
+
+ context.project.messageBus.connect(this).subscribe(
+ QRegionProfileSelectedListener.TOPIC,
+ object : QRegionProfileSelectedListener {
+ override fun onProfileSelected(project: Project, profile: QRegionProfile?) {
+ chatSessionStorage.deleteAllSessions()
+ }
+ }
+ )
}
private suspend fun handleMessage(message: AmazonQMessage, inboundAppMessagesHandler: InboundAppMessagesHandler) {
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeWhispererUTGChatManager.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeWhispererUTGChatManager.kt
index dc60a60e747..aecede14c15 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeWhispererUTGChatManager.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/CodeWhispererUTGChatManager.kt
@@ -541,8 +541,8 @@ Please see the unit tests generated below. Click 'View Diff' to review the chang
jobGroup = session.testGenerationJobGroupName,
jobId = session.testGenerationJob,
result = if (e.message == message("testgen.message.cancelled")) MetricResult.Cancelled else MetricResult.Failed,
- reason = (e as CodeTestException).code ?: "DefaultError",
- reasonDesc = if (e.message == message("testgen.message.cancelled")) "${e.code}: ${e.message}" else e.message,
+ reason = (e as? CodeTestException)?.code ?: "DefaultError",
+ reasonDesc = if (e.message == message("testgen.message.cancelled")) "${(e as? CodeTestException)?.code}: ${e.message}" else e.message,
perfClientLatency = (Instant.now().toEpochMilli() - session.startTimeOfTestGeneration),
isCodeBlockSelected = session.isCodeBlockSelected,
artifactsUploadDuration = session.artifactUploadDuration,
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/controller/CodeTestChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/controller/CodeTestChatController.kt
index f146d0791b9..f83bb1e1649 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/controller/CodeTestChatController.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/controller/CodeTestChatController.kt
@@ -52,13 +52,11 @@ import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.info
import software.aws.toolkits.core.utils.warn
-import software.aws.toolkits.jetbrains.core.AwsClientManager
import software.aws.toolkits.jetbrains.core.coroutines.EDT
-import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
-import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
import software.aws.toolkits.jetbrains.core.credentials.sono.isInternalUser
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
import software.aws.toolkits.jetbrains.services.amazonq.project.RelevantDocument
import software.aws.toolkits.jetbrains.services.amazonqCodeTest.CodeWhispererUTGChatManager
import software.aws.toolkits.jetbrains.services.amazonqCodeTest.ConversationState
@@ -249,26 +247,22 @@ class CodeTestChatController(
})
}
.build()
+ if (!fileInfo.fileInWorkspace) {
+ val messageContent =
+ "⚠ I can't generate tests for ${fileInfo.fileName}" +
+ " because it's outside the project directory. " +
+ "I can still provide examples, instructions and code suggestions."
- val messageContent = if (fileInfo.fileInWorkspace) {
- "⚠ ${fileInfo.fileLanguage.languageId} is not a " +
- "language I support specialized unit test generation for at the moment. The languages " +
- "I support now are Python and Java. I can still provide examples, instructions and code suggestions."
- } else {
- "⚠ I can't generate tests for ${fileInfo.fileName}" +
- " because it's outside the project directory. " +
- "I can still provide examples, instructions and code suggestions."
+ codeTestChatHelper.addNewMessage(
+ CodeTestChatMessageContent(
+ message = messageContent,
+ type = ChatMessageType.Answer,
+ canBeVoted = false
+ ),
+ message.tabId,
+ false
+ )
}
-
- codeTestChatHelper.addNewMessage(
- CodeTestChatMessageContent(
- message = messageContent,
- type = ChatMessageType.Answer,
- canBeVoted = false
- ),
- message.tabId,
- false
- )
testResponseMessageId = codeTestChatHelper.addAnswer(
CodeTestChatMessageContent(
message = "",
@@ -280,9 +274,6 @@ class CodeTestChatController(
promptInputDisabledState = true,
)
// Send Request to Sync UTG API
- val connection = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance())
- // this should never happen because it should have been handled upstream by [AuthController]
- ?: error("connection was found to be null")
val contextExtractor = ActiveFileContextExtractor.create(fqnWebviewAdapter = null, project = project)
val activeFileContext = ActiveFileContext(
fileContext = FileContext(
@@ -304,7 +295,7 @@ class CodeTestChatController(
useRelevantDocuments = false,
)
- val client = AwsClientManager.getInstance().getClient(connection.getConnectionSettings())
+ val client = QRegionProfileManager.getInstance().getQClient(project)
val request = requestData.toChatRequest()
client.generateAssistantResponse(request, responseHandler).await()
// TODO: Need to send isCodeBlockSelected field
@@ -417,6 +408,7 @@ class CodeTestChatController(
.build()
return GenerateAssistantResponseRequest.builder()
.conversationState(conversationState)
+ .profileArn(QRegionProfileManager.getInstance().activeProfile(context.project)?.arn)
.build()
}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/storage/ChatSessionStorage.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/storage/ChatSessionStorage.kt
index 0e38f06c6de..295bd9abe15 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/storage/ChatSessionStorage.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/storage/ChatSessionStorage.kt
@@ -17,4 +17,8 @@ class ChatSessionStorage {
// Find all sessions that are currently waiting to be authenticated
fun getAuthenticatingSessions(): List = this.sessions.values.filter { it.isAuthenticating }
+
+ fun deleteAllSessions() {
+ sessions.clear()
+ }
}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/DocApp.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/DocApp.kt
index 0e55a655579..ed913374e08 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/DocApp.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/DocApp.kt
@@ -4,6 +4,7 @@
package software.aws.toolkits.jetbrains.services.amazonqDoc
import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.project.Project
import kotlinx.coroutines.launch
import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection
@@ -11,6 +12,8 @@ import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQApp
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileSelectedListener
import software.aws.toolkits.jetbrains.services.amazonqCodeScan.auth.isCodeScanAvailable
import software.aws.toolkits.jetbrains.services.amazonqCodeTest.auth.isCodeTestAvailable
import software.aws.toolkits.jetbrains.services.amazonqDoc.auth.isDocAvailable
@@ -74,6 +77,15 @@ class DocApp : AmazonQApp {
}
}
)
+
+ context.project.messageBus.connect(this).subscribe(
+ QRegionProfileSelectedListener.TOPIC,
+ object : QRegionProfileSelectedListener {
+ override fun onProfileSelected(project: Project, profile: QRegionProfile?) {
+ chatSessionStorage.deleteAllSessions()
+ }
+ }
+ )
}
private suspend fun handleMessage(message: AmazonQMessage, inboundAppMessagesHandler: InboundAppMessagesHandler) {
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/storage/ChatSessionStorage.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/storage/ChatSessionStorage.kt
index 2344dac5c94..e54a6b1d7fb 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/storage/ChatSessionStorage.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/storage/ChatSessionStorage.kt
@@ -23,4 +23,11 @@ class ChatSessionStorage {
// Find all sessions that are currently waiting to be authenticated
fun getAuthenticatingSessions(): List = this.sessions.values.filter { it.isAuthenticating }
+
+ fun deleteAllSessions() {
+ sessions.values.forEach { session ->
+ session.sessionState.token?.cancel()
+ }
+ sessions.clear()
+ }
}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevApp.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevApp.kt
index 7169e391507..c9fe08efc64 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevApp.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevApp.kt
@@ -4,6 +4,7 @@
package software.aws.toolkits.jetbrains.services.amazonqFeatureDev
import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.project.Project
import kotlinx.coroutines.launch
import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection
@@ -11,6 +12,8 @@ import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQApp
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileSelectedListener
import software.aws.toolkits.jetbrains.services.amazonqCodeScan.auth.isCodeScanAvailable
import software.aws.toolkits.jetbrains.services.amazonqCodeTest.auth.isCodeTestAvailable
import software.aws.toolkits.jetbrains.services.amazonqDoc.auth.isDocAvailable
@@ -75,6 +78,15 @@ class FeatureDevApp : AmazonQApp {
}
}
)
+
+ context.project.messageBus.connect(this).subscribe(
+ QRegionProfileSelectedListener.TOPIC,
+ object : QRegionProfileSelectedListener {
+ override fun onProfileSelected(project: Project, profile: QRegionProfile?) {
+ chatSessionStorage.deleteAllSessions()
+ }
+ }
+ )
}
private suspend fun handleMessage(message: AmazonQMessage, inboundAppMessagesHandler: InboundAppMessagesHandler) {
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/clients/FeatureDevClient.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/clients/FeatureDevClient.kt
index 2d73f5fb4ba..7b25fb3b00e 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/clients/FeatureDevClient.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/clients/FeatureDevClient.kt
@@ -28,10 +28,8 @@ import software.amazon.awssdk.services.codewhispererstreaming.model.ExportIntent
import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.info
-import software.aws.toolkits.jetbrains.core.awsClient
-import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
-import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
import software.aws.toolkits.jetbrains.services.amazonq.clients.AmazonQStreamingClient
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FEATURE_EVALUATION_PRODUCT_NAME
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.calculateTotalLatency
import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata
@@ -70,11 +68,7 @@ class FeatureDevClient(
.build()
}
- private fun connection() =
- ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance())
- ?: error("Attempted to use connection while one does not exist")
-
- private fun bearerClient() = connection().getConnectionSettings().awsClient()
+ private fun bearerClient() = QRegionProfileManager.getInstance().getQClient(project)
private val amazonQStreamingClient
get() = AmazonQStreamingClient.getInstance(project)
@@ -88,6 +82,7 @@ class FeatureDevClient(
}
requestBuilder.optOutPreference(getTelemetryOptOutPreference())
requestBuilder.userContext(featureDevUserContext)
+ requestBuilder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}
fun sendFeatureDevMetricData(operationName: String, result: String): SendTelemetryEventResponse =
@@ -115,6 +110,7 @@ class FeatureDevClient(
}
requestBuilder.optOutPreference(getTelemetryOptOutPreference())
requestBuilder.userContext(featureDevUserContext)
+ requestBuilder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}
fun sendFeatureDevCodeGenerationEvent(
@@ -133,6 +129,7 @@ class FeatureDevClient(
}
requestBuilder.optOutPreference(getTelemetryOptOutPreference())
requestBuilder.userContext(featureDevUserContext)
+ requestBuilder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}
fun sendFeatureDevCodeAcceptanceEvent(
@@ -151,11 +148,14 @@ class FeatureDevClient(
}
requestBuilder.optOutPreference(getTelemetryOptOutPreference())
requestBuilder.userContext(featureDevUserContext)
+ requestBuilder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}
fun createTaskAssistConversation(): CreateTaskAssistConversationResponse =
bearerClient().createTaskAssistConversation(
- CreateTaskAssistConversationRequest.builder().build(),
+ CreateTaskAssistConversationRequest.builder()
+ .profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
+ .build(),
)
fun createTaskAssistUploadUrl(
@@ -182,6 +182,7 @@ class FeatureDevClient(
.build(),
).build(),
)
+ .profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}
fun startTaskAssistCodeGeneration(
@@ -205,6 +206,7 @@ class FeatureDevClient(
.uploadId(uploadId)
}.codeGenerationId(codeGenerationId.toString())
.currentCodeGenerationId(currentCodeGenerationId)
+ .profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}
fun getTaskAssistCodeGeneration(
@@ -216,6 +218,7 @@ class FeatureDevClient(
it
.conversationId(conversationId)
.codeGenerationId(codeGenerationId)
+ .profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}
suspend fun exportTaskAssistResultArchive(conversationId: String): MutableList =
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt
index 9401d8cacbd..75e1cf0fbd0 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt
@@ -32,6 +32,8 @@ import software.aws.toolkits.jetbrains.core.coroutines.EDT
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController
import software.aws.toolkits.jetbrains.services.amazonq.messages.MessagePublisher
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileSelectedListener
import software.aws.toolkits.jetbrains.services.amazonq.project.RepoSizeError
import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindowFactory
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.CodeIterationLimitException
@@ -94,6 +96,17 @@ class FeatureDevController(
private val authController: AuthController = AuthController(),
) : InboundAppMessagesHandler {
+ init {
+ context.project.messageBus.connect().subscribe(
+ QRegionProfileSelectedListener.TOPIC,
+ object : QRegionProfileSelectedListener {
+ override fun onProfileSelected(project: Project, profile: QRegionProfile?) {
+ chatSessionStorage.deleteAllSessions()
+ }
+ }
+ )
+ }
+
val messenger = context.messagesFromAppToUi
val toolWindow = ToolWindowManager.getInstance(context.project).getToolWindow(AmazonQToolWindowFactory.WINDOW_ID)
@@ -466,7 +479,7 @@ class FeatureDevController(
),
)
- if (!session.context.checkForDevFile()) {
+ if (!session.context.hasDevFile()) {
followUps.add(
FollowUp(
pillText = message("amazonqFeatureDev.follow_up.generate_dev_file"),
@@ -728,7 +741,7 @@ class FeatureDevController(
}
val codeWhispererSettings = CodeWhispererSettings.getInstance().getAutoBuildSetting()
- val hasDevFile = session.context.checkForDevFile()
+ val hasDevFile = session.context.hasDevFile()
val isPromptedForAutoBuildFeature = codeWhispererSettings.containsKey(session.context.workspaceRoot.path)
if (hasDevFile && !isPromptedForAutoBuildFeature) {
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/CodeGenerationState.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/CodeGenerationState.kt
index 1c664733869..5448a51b55e 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/CodeGenerationState.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/CodeGenerationState.kt
@@ -30,6 +30,7 @@ import software.aws.toolkits.telemetry.MetricResult
import java.util.UUID
private val logger = getLogger()
+private const val RUN_COMMAND_LOG_PATH = ".amazonq/dev/run_command.log"
class CodeGenerationState(
override val tabID: String,
@@ -211,9 +212,23 @@ private suspend fun CodeGenerationState.generateCode(
conversationId = config.conversationId,
)
- val newFileInfo = registerNewFiles(newFileContents = codeGenerationStreamResult.new_file_contents)
- val deletedFileInfo = registerDeletedFiles(deletedFiles = codeGenerationStreamResult.deleted_files)
+ val fileContents = codeGenerationStreamResult.new_file_contents.filterKeys { file ->
+ if (file.endsWith(RUN_COMMAND_LOG_PATH)) {
+ val contents: String = codeGenerationStreamResult.new_file_contents[file].orEmpty()
+ val truncatedContents = if (contents.length > 10000000) {
+ contents.substring(0, 10000000)
+ } else {
+ contents
+ }
+ logger.info(truncatedContents) { "Run command log: $truncatedContents" }
+ false
+ } else {
+ true
+ }
+ }
+ val newFileInfo = registerNewFiles(newFileContents = fileContents)
+ val deletedFileInfo = registerDeletedFiles(deletedFiles = codeGenerationStreamResult.deleted_files)
return CodeGenerationResult(
newFiles = newFileInfo,
deletedFiles = deletedFileInfo,
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/storage/ChatSessionStorage.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/storage/ChatSessionStorage.kt
index 216d9cd1c8b..ed0f7eab7be 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/storage/ChatSessionStorage.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/storage/ChatSessionStorage.kt
@@ -23,4 +23,11 @@ class ChatSessionStorage {
// Find all sessions that are currently waiting to be authenticated
fun getAuthenticatingSessions(): List = this.sessions.values.filter { it.isAuthenticating }
+
+ fun deleteAllSessions() {
+ sessions.values.forEach { session ->
+ session.sessionState.token?.cancel()
+ }
+ sessions.clear()
+ }
}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/App.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/App.kt
index b206595f9e8..34bb6bca614 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/App.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/App.kt
@@ -4,6 +4,7 @@
package software.aws.toolkits.jetbrains.services.cwc
import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.project.Project
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.launch
import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope
@@ -12,6 +13,8 @@ import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQApp
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage
import software.aws.toolkits.jetbrains.services.amazonq.onboarding.OnboardingPageInteraction
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileSelectedListener
import software.aws.toolkits.jetbrains.services.amazonq.util.highlightCommand
import software.aws.toolkits.jetbrains.services.cwc.commands.ActionRegistrar
import software.aws.toolkits.jetbrains.services.cwc.commands.CodeScanIssueActionMessage
@@ -75,6 +78,15 @@ class App : AmazonQApp {
}
}
)
+
+ ApplicationManager.getApplication().messageBus.connect(this).subscribe(
+ QRegionProfileSelectedListener.TOPIC,
+ object : QRegionProfileSelectedListener {
+ override fun onProfileSelected(project: Project, profile: QRegionProfile?) {
+ inboundAppMessagesHandler.processSessionClear()
+ }
+ }
+ )
}
private suspend fun handleMessage(message: AmazonQMessage, inboundAppMessagesHandler: InboundAppMessagesHandler) {
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/InboundAppMessagesHandler.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/InboundAppMessagesHandler.kt
index ff8a12e70ec..bbcddd84552 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/InboundAppMessagesHandler.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/InboundAppMessagesHandler.kt
@@ -34,4 +34,6 @@ interface InboundAppMessagesHandler {
suspend fun processCodeScanIssueAction(message: CodeScanIssueActionMessage)
suspend fun processLinkClick(message: IncomingCwcMessage.ClickedLink)
+
+ fun processSessionClear()
}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/clients/chat/v1/ChatSessionV1.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/clients/chat/v1/ChatSessionV1.kt
index a55d9cc2075..e8d967a8e65 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/clients/chat/v1/ChatSessionV1.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/clients/chat/v1/ChatSessionV1.kt
@@ -36,10 +36,8 @@ import software.amazon.awssdk.services.codewhispererstreaming.model.UserInputMes
import software.amazon.awssdk.services.codewhispererstreaming.model.UserIntent
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.info
-import software.aws.toolkits.jetbrains.core.AwsClientManager
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext
-import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
-import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
import software.aws.toolkits.jetbrains.services.amazonq.project.RelevantDocument
import software.aws.toolkits.jetbrains.services.cwc.ChatConstants
import software.aws.toolkits.jetbrains.services.cwc.clients.chat.ChatSession
@@ -170,11 +168,7 @@ class ChatSessionV1(
try {
withTimeout(ChatConstants.REQUEST_TIMEOUT_MS.toLong()) {
- val connection = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance())
- // this should never happen because it should have been handled upstream by [AuthController]
- ?: error("connection was found to be null")
-
- val client = AwsClientManager.getInstance().getClient(connection.getConnectionSettings())
+ val client = QRegionProfileManager.getInstance().getQClient(project)
val request = data.toChatRequest()
logger.info { "Request from tab: ${data.tabId}, conversationId: $conversationId, request: $request" }
client.generateAssistantResponse(request, responseHandler).await()
@@ -216,6 +210,7 @@ class ChatSessionV1(
.build()
return GenerateAssistantResponseRequest.builder()
.conversationState(conversationState)
+ .profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
.build()
}
@@ -300,6 +295,8 @@ class ChatSessionV1(
UserIntent.EXPLAIN_CODE_SELECTION -> FollowUpType.ExplainInDetail
UserIntent.UNKNOWN_TO_SDK_VERSION -> FollowUpType.Generated
UserIntent.GENERATE_UNIT_TESTS -> FollowUpType.Generated
+ UserIntent.GENERATE_CLOUDFORMATION_TEMPLATE -> FollowUpType.Generated
+ UserIntent.CODE_GENERATION -> FollowUpType.Generated
null -> FollowUpType.Generated
}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/SendToQActionGroup.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/SendToQActionGroup.kt
index d3e77515cd1..d903f842020 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/SendToQActionGroup.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/SendToQActionGroup.kt
@@ -8,6 +8,7 @@ import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.wm.ToolWindowManager
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindowFactory
class SendToQActionGroup : DefaultActionGroup(), DumbAware {
@@ -16,6 +17,7 @@ class SendToQActionGroup : DefaultActionGroup(), DumbAware {
override fun update(e: AnActionEvent) {
val project = e.project ?: return
val amazonQWindow = ToolWindowManager.getInstance(project).getToolWindow(AmazonQToolWindowFactory.WINDOW_ID)
- e.presentation.isEnabledAndVisible = amazonQWindow?.isAvailable ?: false
+ e.presentation.isEnabledAndVisible = (amazonQWindow?.isAvailable == true) &&
+ !QRegionProfileManager.getInstance().hasValidConnectionButNoActiveProfile(project)
}
}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt
index 84ebaa60cef..30d3aeeba16 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt
@@ -541,6 +541,10 @@ class ChatController private constructor(
.map { it.tabId }
.first()
+ override fun processSessionClear() {
+ chatSessionStorage.deleteAllSessions()
+ }
+
companion object {
private val logger = getLogger()
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt
index de137756bff..4d7cff9667f 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt
@@ -64,6 +64,8 @@ class TelemetryHelper(private val project: Project, private val sessionStorage:
UserIntent.EXPLAIN_CODE_SELECTION -> CwsprChatUserIntent.ExplainCodeSelection
UserIntent.GENERATE_UNIT_TESTS -> CwsprChatUserIntent.GenerateUnitTests
UserIntent.UNKNOWN_TO_SDK_VERSION -> CwsprChatUserIntent.Unknown
+ UserIntent.GENERATE_CLOUDFORMATION_TEMPLATE -> CwsprChatUserIntent.Unknown
+ UserIntent.CODE_GENERATION -> CwsprChatUserIntent.Unknown
}
private fun getTelemetryTriggerType(triggerType: TriggerType): CwsprChatTriggerInteraction = when (triggerType) {
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt
index 1a6f8d2d10f..bf468976a2f 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/InlineChatController.kt
@@ -55,6 +55,8 @@ import software.aws.toolkits.jetbrains.core.gettingstarted.requestCredentialsFor
import software.aws.toolkits.jetbrains.core.webview.BrowserState
import software.aws.toolkits.jetbrains.services.amazonq.QWebviewPanel
import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileSelectedListener
import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AMAZON_Q_WINDOW_ID
import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator
import software.aws.toolkits.jetbrains.services.codewhisperer.model.CaretPosition
@@ -101,6 +103,14 @@ class InlineChatController(
init {
Disposer.register(this, listener)
project.messageBus.connect(this).subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, listener)
+ project.messageBus.connect(this).subscribe(
+ QRegionProfileSelectedListener.TOPIC,
+ object : QRegionProfileSelectedListener {
+ override fun onProfileSelected(project: Project, profile: QRegionProfile?) {
+ sessionStorage.deleteAllSessions()
+ }
+ }
+ )
}
data class InlineChatMetrics(
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt
index ebc6b53cd13..f581a217226 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/OpenChatInputAction.kt
@@ -3,10 +3,12 @@
package software.aws.toolkits.jetbrains.services.cwc.inline
+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.util.Key
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
class OpenChatInputAction : AnAction() {
override fun actionPerformed(e: AnActionEvent) {
@@ -24,4 +26,9 @@ class OpenChatInputAction : AnAction() {
val inlineChatController = InlineChatController.getInstance(project)
inlineChatController.initPopup(editor)
}
+ override fun update(e: AnActionEvent) {
+ val project = e.project ?: return
+ e.presentation.isEnabledAndVisible = !QRegionProfileManager.getInstance().hasValidConnectionButNoActiveProfile(project)
+ }
+ override fun getActionUpdateThread() = ActionUpdateThread.BGT
}
diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/storage/ChatSessionStorage.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/storage/ChatSessionStorage.kt
index 231cdd21d5f..e11d0693b7c 100644
--- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/storage/ChatSessionStorage.kt
+++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/storage/ChatSessionStorage.kt
@@ -26,4 +26,8 @@ class ChatSessionStorage(
fun deleteSession(tabId: String) {
sessions.remove(tabId)?.scope?.cancel()
}
+
+ fun deleteAllSessions() {
+ sessions.clear()
+ }
}
diff --git a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevTestBase.kt b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevTestBase.kt
index 6c1a2fb6389..da392cd08cd 100644
--- a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevTestBase.kt
+++ b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevTestBase.kt
@@ -65,6 +65,8 @@ open class FeatureDevTestBase(
internal val otherStatus = "Other"
internal val testTabId = "test-tab-id"
internal val testFilePaths = mapOf(Pair("test.ts", "This is a comment"))
+ internal val testRunCommandLogPath = ".amazonq/dev/run_command.log"
+ internal val testLogPath = mapOf(Pair(testRunCommandLogPath, "This is a log"))
internal val testDeletedFiles = listOf("deleted.ts")
internal val testReferences = listOf(CodeReferenceGenerated())
internal val testChecksumSha = "test-sha"
diff --git a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/CodeGenerationStateTest.kt b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/CodeGenerationStateTest.kt
index 45d613f6b24..8e60630ac21 100644
--- a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/CodeGenerationStateTest.kt
+++ b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/CodeGenerationStateTest.kt
@@ -39,13 +39,14 @@ class CodeGenerationStateTest : FeatureDevTestBase() {
private lateinit var messenger: MessagePublisher
private val action = SessionStateAction("test-task", userMessage)
private lateinit var featureDevService: FeatureDevService
+ private lateinit var repoContext: FeatureDevSessionContext
@Before
override fun setup() {
featureDevService = mockk()
every { featureDevService.project } returns projectRule.project
messenger = mock()
- val repoContext = mock()
+ repoContext = mockk()
val sessionStateConfig = SessionStateConfig(testConversationId, repoContext, featureDevService)
codeGenerationState =
@@ -103,6 +104,34 @@ class CodeGenerationStateTest : FeatureDevTestBase() {
coVerify(exactly = 1) { featureDevService.exportTaskAssistArchiveResult(testConversationId) }
}
+ @Test
+ fun `test generateCode excludes run_command log file`() {
+ val runCommandLogFileName = "run_command.log"
+
+ val archiveFiles = mapOf(
+ runCommandLogFileName to "newLog",
+ "other.ts" to "other content"
+ )
+ val deletedFiles = emptyList()
+ val references = emptyList()
+
+ every { featureDevService.getTaskAssistCodeGeneration(any(), any()) } returns exampleCompleteGetTaskAssistCodeGenerationResponse
+ every { featureDevService.startTaskAssistCodeGeneration(any(), any(), any(), any(), any()) } returns exampleStartTaskAssistConversationResponse
+ coEvery { featureDevService.exportTaskAssistArchiveResult(any()) } returns
+ CodeGenerationStreamResult(archiveFiles, listOf("deleted.ts"), listOf(CodeReferenceGenerated()))
+
+ runTest {
+ val actual = codeGenerationState.interact(action)
+ val nextState = actual.nextState as PrepareCodeGenerationState
+ assertThat(nextState.filePaths).contains(
+ NewFileZipInfo(runCommandLogFileName, "newLog", rejected = false, changeApplied = false)
+ )
+ assertThat(nextState.filePaths).contains(
+ NewFileZipInfo("other.ts", "other content", rejected = false, changeApplied = false)
+ )
+ }
+ }
+
@Test(expected = FeatureDevException::class)
fun `test code generation failed`() =
runTest {
diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/ArtifactHandler.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/ArtifactHandler.kt
index a55c2488fe8..93d3da48e7e 100644
--- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/ArtifactHandler.kt
+++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/ArtifactHandler.kt
@@ -528,6 +528,7 @@ class ArtifactHandler(
TransformationDownloadArtifactType.CLIENT_INSTRUCTIONS -> CodeTransformArtifactType.ClientInstructions
TransformationDownloadArtifactType.LOGS -> CodeTransformArtifactType.Logs
TransformationDownloadArtifactType.UNKNOWN_TO_SDK_VERSION -> CodeTransformArtifactType.Unknown
+ TransformationDownloadArtifactType.GENERATED_CODE -> CodeTransformArtifactType.Unknown
}
companion object {
diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeModernizerManager.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeModernizerManager.kt
index c2038db7a86..9549cb8578d 100644
--- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeModernizerManager.kt
+++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeModernizerManager.kt
@@ -32,6 +32,8 @@ import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.core.coroutines.projectCoroutineScope
import software.aws.toolkits.jetbrains.services.amazonq.CODE_TRANSFORM_TROUBLESHOOT_DOC_MVN_FAILURE
import software.aws.toolkits.jetbrains.services.amazonq.CODE_TRANSFORM_TROUBLESHOOT_DOC_PROJECT_SIZE
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileSelectedListener
import software.aws.toolkits.jetbrains.services.codemodernizer.client.GumbyClient
import software.aws.toolkits.jetbrains.services.codemodernizer.commands.CodeTransformMessageListener
import software.aws.toolkits.jetbrains.services.codemodernizer.constants.HIL_POM_FILE_NAME
@@ -129,6 +131,23 @@ class CodeModernizerManager(private val project: Project) : PersistentStateCompo
init {
CodeModernizerSessionState.getInstance(project).setDefaults()
+ initQRegionProfileSelectedListener()
+ }
+
+ private fun initQRegionProfileSelectedListener() {
+ project.messageBus.connect(this).subscribe(
+ QRegionProfileSelectedListener.TOPIC,
+ object : QRegionProfileSelectedListener {
+ override fun onProfileSelected(project: Project, profile: QRegionProfile?) {
+ stopModernize()
+ codeTransformationSession?.let {
+ Disposer.dispose(it)
+ }
+ managerState = CodeModernizerState()
+ codeTransformationSession = null
+ }
+ }
+ )
}
fun validate(project: Project, transformationType: CodeTransformType): ValidationResult {
@@ -250,7 +269,7 @@ class CodeModernizerManager(private val project: Project) : PersistentStateCompo
fun runModernize(copyResult: MavenCopyCommandsResult? = null) {
initStopParameters()
- val session = codeTransformationSession as CodeModernizerSession
+ val session = codeTransformationSession ?: return
initModernizationJobUI(true, project.getModuleOrProjectNameForFile(session.sessionContext.configurationFile))
launchModernizationJob(session, copyResult)
}
diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeModernizerSession.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeModernizerSession.kt
index 9ba17c22cb1..921a9ac0f2c 100644
--- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeModernizerSession.kt
+++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeModernizerSession.kt
@@ -5,6 +5,7 @@ package software.aws.toolkits.jetbrains.services.codemodernizer
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.runInEdt
+import com.intellij.openapi.util.Disposer
import com.intellij.serviceContainer.AlreadyDisposedException
import com.intellij.util.io.HttpRequests
import kotlinx.coroutines.delay
@@ -149,6 +150,9 @@ class CodeModernizerSession(
* Based on [CodeWhispererCodeScanSession]
*/
suspend fun createModernizationJob(copyResult: MavenCopyCommandsResult?): CodeModernizerStartJobResult {
+ if (this.isDisposed.get()) {
+ return CodeModernizerStartJobResult.Cancelled
+ }
LOG.info { "Compressing local project" }
val payload: File?
var payloadSize = 0
@@ -182,6 +186,9 @@ class CodeModernizerSession(
payloadSize = payload.length().toInt()
LOG.info { "Uploading zip file with size: $payloadSize bytes" }
+ if (this.isDisposed.get()) {
+ return CodeModernizerStartJobResult.Cancelled
+ }
if (payloadSize > MAX_ZIP_SIZE) {
telemetryErrorMessage = "Project exceeds max upload size"
@@ -210,7 +217,7 @@ class CodeModernizerSession(
telemetryErrorMessage = "Credential expired before uploading project"
return CodeModernizerStartJobResult.ZipUploadFailed(UploadFailureReason.CREDENTIALS_EXPIRED)
}
- if (shouldStop.get()) {
+ if (shouldStop.get() || this.isDisposed.get()) {
LOG.warn { "Job was cancelled by user before upload was called" }
telemetryErrorMessage = "Cancelled when about to upload project"
return CodeModernizerStartJobResult.Cancelled
@@ -285,7 +292,7 @@ class CodeModernizerSession(
CodeTransformMessageListener.instance.onUploadResult()
return try {
- if (shouldStop.get()) {
+ if (shouldStop.get() || this.isDisposed.get()) {
LOG.warn { "Job was cancelled by user before start job was called" }
return CodeModernizerStartJobResult.Cancelled
}
@@ -625,6 +632,8 @@ class CodeModernizerSession(
override fun dispose() {
isDisposed.set(true)
+ shouldStop.set(true)
+ Disposer.dispose(sessionContext)
}
fun getActiveJobId() = state.currentJobId
diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeTransformChatApp.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeTransformChatApp.kt
index 1c32f2f07c5..b102d348d11 100644
--- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeTransformChatApp.kt
+++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeTransformChatApp.kt
@@ -4,6 +4,7 @@
package software.aws.toolkits.jetbrains.services.codemodernizer
import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.project.Project
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.launch
import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope
@@ -15,6 +16,8 @@ import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQApp
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController
import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileSelectedListener
import software.aws.toolkits.jetbrains.services.amazonqCodeScan.auth.isCodeScanAvailable
import software.aws.toolkits.jetbrains.services.amazonqCodeTest.auth.isCodeTestAvailable
import software.aws.toolkits.jetbrains.services.amazonqDoc.auth.isDocAvailable
@@ -161,6 +164,15 @@ class CodeTransformChatApp : AmazonQApp {
}
}
)
+
+ context.project.messageBus.connect(this).subscribe(
+ QRegionProfileSelectedListener.TOPIC,
+ object : QRegionProfileSelectedListener {
+ override fun onProfileSelected(project: Project, profile: QRegionProfile?) {
+ chatSessionStorage.deleteAllSessions()
+ }
+ }
+ )
}
private suspend fun handleMessage(message: AmazonQMessage, inboundAppMessagesHandler: InboundAppMessagesHandler) {
diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/client/GumbyClient.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/client/GumbyClient.kt
index 097b9231d4a..6134e69ca22 100644
--- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/client/GumbyClient.kt
+++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/client/GumbyClient.kt
@@ -38,9 +38,6 @@ import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.info
import software.aws.toolkits.jetbrains.core.AwsClientManager
-import software.aws.toolkits.jetbrains.core.awsClient
-import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
-import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
import software.aws.toolkits.jetbrains.services.amazonq.APPLICATION_ZIP
import software.aws.toolkits.jetbrains.services.amazonq.AWS_KMS
import software.aws.toolkits.jetbrains.services.amazonq.CONTENT_SHA256
@@ -48,6 +45,7 @@ import software.aws.toolkits.jetbrains.services.amazonq.SERVER_SIDE_ENCRYPTION
import software.aws.toolkits.jetbrains.services.amazonq.SERVER_SIDE_ENCRYPTION_AWS_KMS_KEY_ID
import software.aws.toolkits.jetbrains.services.amazonq.clients.AmazonQStreamingClient
import software.aws.toolkits.jetbrains.services.amazonq.codeWhispererUserContext
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeModernizerMetrics
import software.aws.toolkits.jetbrains.services.codemodernizer.model.JobId
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.calculateTotalLatency
@@ -58,10 +56,7 @@ import java.time.Instant
@Service(Service.Level.PROJECT)
class GumbyClient(private val project: Project) {
- private fun connection() = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance())
- ?: error("Attempted to use connection while one does not exist")
-
- private fun bearerClient() = connection().getConnectionSettings().awsClient()
+ private fun bearerClient() = QRegionProfileManager.getInstance().getQClient(project)
private val amazonQStreamingClient
get() = AmazonQStreamingClient.getInstance(project)
@@ -71,6 +66,7 @@ class GumbyClient(private val project: Project) {
.contentChecksumType(ContentChecksumType.SHA_256)
.contentChecksum(sha256Checksum)
.uploadIntent(UploadIntent.TRANSFORMATION)
+ .profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
.build()
return callApi({ bearerClient().createUploadUrl(request) }, apiName = "CreateUploadUrl")
}
@@ -92,12 +88,16 @@ class GumbyClient(private val project: Project) {
)
.build()
)
+ .profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
.build()
return callApi({ bearerClient().createUploadUrl(request) }, apiName = "CreateUploadUrl")
}
fun getCodeModernizationJob(jobId: String): GetTransformationResponse {
- val request = GetTransformationRequest.builder().transformationJobId(jobId).build()
+ val request = GetTransformationRequest.builder()
+ .transformationJobId(jobId)
+ .profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
+ .build()
return callApi({ bearerClient().getTransformation(request) }, apiName = "GetTransformation")
}
@@ -116,6 +116,7 @@ class GumbyClient(private val project: Project) {
.source { it.language(sourceLanguage) }
.target { it.language(targetLanguage) }
}
+ .profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
.build()
return callApi({ bearerClient().startTransformation(request) }, apiName = "StartTransformation")
}
@@ -127,17 +128,22 @@ class GumbyClient(private val project: Project) {
val request = ResumeTransformationRequest.builder()
.transformationJobId(jobId.id)
.userActionStatus(userActionStatus)
+ .profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
.build()
return callApi({ bearerClient().resumeTransformation(request) }, apiName = "ResumeTransformation")
}
fun getCodeModernizationPlan(jobId: JobId): GetTransformationPlanResponse {
- val request = GetTransformationPlanRequest.builder().transformationJobId(jobId.id).build()
+ val request = GetTransformationPlanRequest.builder()
+ .profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
+ .transformationJobId(jobId.id).build()
return callApi({ bearerClient().getTransformationPlan(request) }, apiName = "GetTransformationPlan")
}
fun stopTransformation(transformationJobId: String): StopTransformationResponse {
- val request = StopTransformationRequest.builder().transformationJobId(transformationJobId).build()
+ val request = StopTransformationRequest.builder().transformationJobId(transformationJobId)
+ .profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
+ .build()
return callApi({ bearerClient().stopTransformation(request) }, apiName = "StopTransformation")
}
@@ -232,6 +238,7 @@ class GumbyClient(private val project: Project) {
}
requestBuilder.optOutPreference(getTelemetryOptOutPreference())
requestBuilder.userContext(codeWhispererUserContext())
+ requestBuilder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}
}
diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/constants/CodeTransformChatItems.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/constants/CodeTransformChatItems.kt
index 4584ca1e985..c39d530e6b2 100644
--- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/constants/CodeTransformChatItems.kt
+++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/constants/CodeTransformChatItems.kt
@@ -778,4 +778,5 @@ fun getDownloadedArtifactTextFromType(artifactType: TransformationDownloadArtifa
TransformationDownloadArtifactType.CLIENT_INSTRUCTIONS -> "upgraded code"
TransformationDownloadArtifactType.LOGS -> "build log"
TransformationDownloadArtifactType.UNKNOWN_TO_SDK_VERSION -> "code"
+ TransformationDownloadArtifactType.GENERATED_CODE -> "code"
}
diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/ideMaven/MavenRunnerUtils.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/ideMaven/MavenRunnerUtils.kt
index 74ec6aeae3d..6629b8c2d34 100644
--- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/ideMaven/MavenRunnerUtils.kt
+++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/ideMaven/MavenRunnerUtils.kt
@@ -16,6 +16,7 @@ import org.slf4j.Logger
import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.info
import software.aws.toolkits.jetbrains.services.codemodernizer.CodeTransformTelemetryManager
+import software.aws.toolkits.jetbrains.services.codemodernizer.model.CodeModernizerSessionContext
import software.aws.toolkits.jetbrains.services.codemodernizer.model.MavenCopyCommandsResult
import software.aws.toolkits.jetbrains.services.codemodernizer.model.MavenDependencyReportCommandsResult
import software.aws.toolkits.telemetry.CodeTransformBuildCommand
@@ -25,6 +26,7 @@ import java.nio.file.Files
import java.nio.file.Path
fun runHilMavenCopyDependency(
+ context: CodeModernizerSessionContext,
sourceFolder: File,
destinationDir: File,
logBuilder: StringBuilder,
@@ -35,6 +37,7 @@ fun runHilMavenCopyDependency(
try {
// Create shared parameters
val transformMvnRunner = TransformMavenRunner(project)
+ context.mavenRunnerQueue.add(transformMvnRunner)
val mvnSettings = MavenRunner.getInstance(project).settings.clone() // clone required to avoid editing user settings
// run copy dependencies
@@ -57,7 +60,14 @@ fun runHilMavenCopyDependency(
return MavenCopyCommandsResult.Success(destinationDir)
}
-fun runMavenCopyCommands(sourceFolder: File, logBuilder: StringBuilder, logger: Logger, project: Project, shouldSkipTests: Boolean): MavenCopyCommandsResult {
+fun runMavenCopyCommands(
+ context: CodeModernizerSessionContext,
+ sourceFolder: File,
+ logBuilder: StringBuilder,
+ logger: Logger,
+ project: Project,
+ shouldSkipTests: Boolean,
+): MavenCopyCommandsResult {
val currentTimestamp = System.currentTimeMillis()
val destinationDir = Files.createTempDirectory("transformation_dependencies_temp_$currentTimestamp")
val telemetry = CodeTransformTelemetryManager.getInstance(project)
@@ -68,6 +78,7 @@ fun runMavenCopyCommands(sourceFolder: File, logBuilder: StringBuilder, logger:
try {
// Create shared parameters
val transformMvnRunner = TransformMavenRunner(project)
+ context.mavenRunnerQueue.add(transformMvnRunner)
val mvnSettings = MavenRunner.getInstance(project).settings.clone() // clone required to avoid editing user settings
val sourceVirtualFile = LocalFileSystem.getInstance().findFileByIoFile(sourceFolder)
@@ -282,10 +293,17 @@ private fun runMavenDependencyUpdatesReport(
return dependencyUpdatesReportRunnable
}
-fun runDependencyReportCommands(sourceFolder: File, logBuilder: StringBuilder, logger: Logger, project: Project): MavenDependencyReportCommandsResult {
+fun runDependencyReportCommands(
+ context: CodeModernizerSessionContext,
+ sourceFolder: File,
+ logBuilder: StringBuilder,
+ logger: Logger,
+ project: Project,
+): MavenDependencyReportCommandsResult {
logger.info { "Executing IntelliJ bundled Maven" }
val transformMvnRunner = TransformMavenRunner(project)
+ context.mavenRunnerQueue.add(transformMvnRunner)
val mvnSettings = MavenRunner.getInstance(project).settings.clone() // clone required to avoid editing user settings
val runnable = runMavenDependencyUpdatesReport(sourceFolder, logBuilder, mvnSettings, transformMvnRunner, logger)
diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/ideMaven/TransformMavenRunner.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/ideMaven/TransformMavenRunner.kt
index f202cf6896f..a68c659ba6f 100644
--- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/ideMaven/TransformMavenRunner.kt
+++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/ideMaven/TransformMavenRunner.kt
@@ -5,6 +5,7 @@ package software.aws.toolkits.jetbrains.services.codemodernizer.ideMaven
import com.intellij.execution.process.ProcessAdapter
import com.intellij.execution.process.ProcessEvent
+import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.process.ProcessOutputTypes
import com.intellij.execution.runners.ProgramRunner
import com.intellij.execution.ui.RunContentDescriptor
@@ -16,11 +17,13 @@ import org.jetbrains.idea.maven.execution.MavenRunnerParameters
import org.jetbrains.idea.maven.execution.MavenRunnerSettings
class TransformMavenRunner(val project: Project) {
+ private var handler: ProcessHandler? = null
fun run(parameters: MavenRunnerParameters, settings: MavenRunnerSettings, onComplete: TransformRunnable) {
FileDocumentManager.getInstance().saveAllDocuments()
val callback = ProgramRunner.Callback { descriptor: RunContentDescriptor ->
val handler = descriptor.processHandler
+ this.handler = handler
if (handler == null) {
// add log error here
onComplete.setExitCode(-1)
@@ -50,4 +53,8 @@ class TransformMavenRunner(val project: Project) {
// Setting isDelegateBuild = true allows us to set the JRE used by Maven during runtime
MavenRunConfigurationType.runConfiguration(project, parameters, null, settings, callback, false)
}
+
+ fun cancel() {
+ this.handler?.destroyProcess()
+ }
}
diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/CodeModernizerSessionContext.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/CodeModernizerSessionContext.kt
index 6a1a75b9d8a..a0da0824a76 100644
--- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/CodeModernizerSessionContext.kt
+++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/model/CodeModernizerSessionContext.kt
@@ -4,6 +4,7 @@
package software.aws.toolkits.jetbrains.services.codemodernizer.model
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
+import com.intellij.openapi.Disposable
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.project.Project
@@ -19,6 +20,7 @@ import software.aws.toolkits.core.utils.putNextEntry
import software.aws.toolkits.jetbrains.services.codemodernizer.EXPLAINABILITY_V1
import software.aws.toolkits.jetbrains.services.codemodernizer.constants.HIL_DEPENDENCIES_ROOT_NAME
import software.aws.toolkits.jetbrains.services.codemodernizer.constants.HIL_MANIFEST_FILE_NAME
+import software.aws.toolkits.jetbrains.services.codemodernizer.ideMaven.TransformMavenRunner
import software.aws.toolkits.jetbrains.services.codemodernizer.ideMaven.runDependencyReportCommands
import software.aws.toolkits.jetbrains.services.codemodernizer.ideMaven.runHilMavenCopyDependency
import software.aws.toolkits.jetbrains.services.codemodernizer.ideMaven.runMavenCopyCommands
@@ -59,6 +61,7 @@ const val INVALID_SUFFIX_REPOSITORIES = "repositories"
const val ORACLE_DB = "ORACLE"
const val AURORA_DB = "AURORA_POSTGRESQL"
const val RDS_DB = "POSTGRESQL"
+
data class CodeModernizerSessionContext(
val project: Project,
var configurationFile: VirtualFile? = null, // used to ZIP module
@@ -71,9 +74,11 @@ data class CodeModernizerSessionContext(
val sourceServerName: String? = null,
var schema: String? = null,
val sqlMetadataZip: File? = null,
-) {
+) : Disposable {
private val mapper = jacksonObjectMapper()
private val ignoredDependencyFileExtensions = setOf(INVALID_SUFFIX_SHA, INVALID_SUFFIX_REPOSITORIES)
+ private var isDisposed = false
+ val mavenRunnerQueue: MutableList = mutableListOf()
private fun File.isMavenTargetFolder(): Boolean {
val hasPomSibling = this.resolveSibling(MAVEN_CONFIGURATION_FILE_NAME).exists()
@@ -100,19 +105,22 @@ data class CodeModernizerSessionContext(
}
fun executeMavenCopyCommands(sourceFolder: File, buildLogBuilder: StringBuilder): MavenCopyCommandsResult {
+ if (isDisposed) return MavenCopyCommandsResult.Cancelled
val shouldSkipTests = customBuildCommand == MAVEN_BUILD_SKIP_UNIT_TESTS
- return runMavenCopyCommands(sourceFolder, buildLogBuilder, LOG, project, shouldSkipTests)
+ return runMavenCopyCommands(this, sourceFolder, buildLogBuilder, LOG, project, shouldSkipTests)
}
private fun executeHilMavenCopyDependency(sourceFolder: File, destinationFolder: File, buildLogBuilder: StringBuilder) = runHilMavenCopyDependency(
+ this,
sourceFolder,
destinationFolder,
buildLogBuilder,
LOG,
- project
+ project,
)
fun copyHilDependencyUsingMaven(hilTepDirPath: Path): MavenCopyCommandsResult {
+ if (isDisposed) return MavenCopyCommandsResult.Cancelled
val sourceFolder = File(getPathToHilArtifactPomFolder(hilTepDirPath).pathString)
val destinationFolder = Files.createDirectories(getPathToHilDependenciesRootDir(hilTepDirPath)).toFile()
val buildLogBuilder = StringBuilder("Starting Build Log...\n")
@@ -121,6 +129,7 @@ data class CodeModernizerSessionContext(
}
fun getDependenciesUsingMaven(): MavenCopyCommandsResult {
+ if (isDisposed) return MavenCopyCommandsResult.Cancelled
val root = configurationFile?.parent
val sourceFolder = File(root?.path)
val buildLogBuilder = StringBuilder("Starting Build Log...\n")
@@ -128,14 +137,16 @@ data class CodeModernizerSessionContext(
}
fun createDependencyReportUsingMaven(hilTempPomPath: Path): MavenDependencyReportCommandsResult {
+ if (isDisposed) return MavenDependencyReportCommandsResult.Cancelled
val sourceFolder = File(hilTempPomPath.pathString)
val buildLogBuilder = StringBuilder("Starting Build Log...\n")
return executeDependencyVersionReportUsingMaven(sourceFolder, buildLogBuilder)
}
+
private fun executeDependencyVersionReportUsingMaven(
sourceFolder: File,
buildLogBuilder: StringBuilder,
- ) = runDependencyReportCommands(sourceFolder, buildLogBuilder, LOG, project)
+ ) = runDependencyReportCommands(this, sourceFolder, buildLogBuilder, LOG, project)
fun createZipForHilUpload(hilTempPath: Path, manifest: CodeTransformHilDownloadManifest?, targetVersion: String): ZipCreationResult =
runReadAction {
@@ -326,6 +337,13 @@ data class CodeModernizerSessionContext(
CodeModernizerBottomWindowPanelManager.getInstance(project).setJobStartingUI()
}
+ override fun dispose() {
+ isDisposed = true
+ this.mavenRunnerQueue.forEach {
+ it.cancel()
+ }
+ }
+
companion object {
private val LOG = getLogger()
}
diff --git a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/session/ChatSessionStorage.kt b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/session/ChatSessionStorage.kt
index 807e5dcbe1b..99198a56550 100644
--- a/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/session/ChatSessionStorage.kt
+++ b/plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/session/ChatSessionStorage.kt
@@ -28,4 +28,8 @@ class ChatSessionStorage {
fun changeAuthenticationNeededNotified(authNeededNotified: Boolean) {
sessions.keys.forEach { sessions[it]?.authNeededNotified = authNeededNotified }
}
+
+ fun deleteAllSessions() {
+ sessions.clear()
+ }
}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/resources/META-INF/plugin-codewhisperer.xml b/plugins/amazonq/codewhisperer/jetbrains-community/resources/META-INF/plugin-codewhisperer.xml
index 07d8f29e713..f8a2de49534 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/resources/META-INF/plugin-codewhisperer.xml
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/resources/META-INF/plugin-codewhisperer.xml
@@ -32,6 +32,7 @@
+
+
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src-241/software/aws/toolkits/jetbrains/services/codewhisperer/inlay/InlineCompletionRemoteRendererFactory.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src-241/software/aws/toolkits/jetbrains/services/codewhisperer/inlay/InlineCompletionRemoteRendererFactory.kt
deleted file mode 100644
index 9c2319ed18a..00000000000
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src-241/software/aws/toolkits/jetbrains/services/codewhisperer/inlay/InlineCompletionRemoteRendererFactory.kt
+++ /dev/null
@@ -1,65 +0,0 @@
-// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-package software.aws.toolkits.jetbrains.services.codewhisperer.inlay
-
-import com.intellij.codeInsight.inline.completion.render.InlineBlockElementRenderer
-import com.intellij.openapi.editor.Editor
-import com.intellij.openapi.editor.EditorCustomElementRenderer
-import com.intellij.openapi.editor.markup.TextAttributes
-import com.intellij.xdebugger.ui.DebuggerColors
-
-// from 232-241.1, we have `InlineSuffixRenderer`, but with 241.2+ it becomes `InlineCompletionLineRenderer`
-// for both line and block inlays. Also InlineBlockElementRenderer is deprecated
-// 242 is not yet handled by this
-object InlineCompletionRemoteRendererFactory {
- private var hasOldLineConstructor = true
- private val lineConstructor = run {
- val clazz =
- try {
- Class.forName("com.intellij.codeInsight.inline.completion.render.InlineSuffixRenderer")
- } catch (e: ClassNotFoundException) {
- hasOldLineConstructor = false
- Class.forName("com.intellij.codeInsight.inline.completion.render.InlineCompletionLineRenderer")
- }
- if (hasOldLineConstructor) {
- clazz.getConstructor(Editor::class.java, String::class.java)
- } else {
- clazz.getConstructor(Editor::class.java, String::class.java, TextAttributes::class.java)
- }
- }
- private var hasNewBlockConstructor = true
- private val blockConstructor = run {
- val clazz =
- try {
- Class.forName("com.intellij.codeInsight.inline.completion.render.InlineCompletionLineRenderer")
- } catch (e: ClassNotFoundException) {
- hasNewBlockConstructor = false
- Class.forName("com.intellij.codeInsight.inline.completion.render.InlineBlockElementRenderer")
- }
- clazz.getConstructor(Editor::class.java, List::class.java)
- }
-
- fun createLineInlay(editor: Editor, text: String): EditorCustomElementRenderer =
- (
- if (hasOldLineConstructor) {
- lineConstructor.newInstance(editor, text)
- } else {
- lineConstructor.newInstance(editor, text, editor.colorsScheme.getAttributes(DebuggerColors.INLINED_VALUES_EXECUTION_LINE))
- }
- ) as EditorCustomElementRenderer
-
- fun createBlockInlays(editor: Editor, block: List): List =
- if (hasNewBlockConstructor) {
- // 241.2+
- val textBlockClazz = Class.forName("com.intellij.codeInsight.inline.completion.render.InlineCompletionRenderTextBlock")
- val textBlockConstructor = textBlockClazz.getConstructor(String::class.java, TextAttributes::class.java)
- block.map {
- blockConstructor.newInstance(
- editor,
- listOf(textBlockConstructor.newInstance(it, editor.colorsScheme.getAttributes(DebuggerColors.INLINED_VALUES_EXECUTION_LINE)))
- ) as EditorCustomElementRenderer
- }
- } else {
- listOf(InlineBlockElementRenderer(editor, block))
- }
-}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/migration/software/aws/toolkits/jetbrains/services/codewhisperer/explorer/CodeWhispererExplorerActionManager.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/migration/software/aws/toolkits/jetbrains/services/codewhisperer/explorer/CodeWhispererExplorerActionManager.kt
index 7c52a9c90b7..e5f07904e64 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/migration/software/aws/toolkits/jetbrains/services/codewhisperer/explorer/CodeWhispererExplorerActionManager.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/migration/software/aws/toolkits/jetbrains/services/codewhisperer/explorer/CodeWhispererExplorerActionManager.kt
@@ -48,7 +48,7 @@ class CodeWhispererExplorerActionManager : PersistentStateComponent
// Add range highlighters for all the issues found.
runInEdt {
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/listeners/CodeWhispererCodeScanEditorMouseMotionListener.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/listeners/CodeWhispererCodeScanEditorMouseMotionListener.kt
index 5e993efc543..ef6d75d4fda 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/listeners/CodeWhispererCodeScanEditorMouseMotionListener.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/listeners/CodeWhispererCodeScanEditorMouseMotionListener.kt
@@ -41,8 +41,8 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.sen
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.utils.truncateIssueTitle
import software.aws.toolkits.jetbrains.services.codewhisperer.language.programmingLanguage
import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererTelemetryService
-import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererColorUtil.getHexString
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.getHexString
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.CodeFixAction
import software.aws.toolkits.telemetry.MetricResult
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt
index 36e60c6aa9f..aabefda3365 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt
@@ -37,10 +37,8 @@ import software.amazon.awssdk.services.codewhispererruntime.model.TargetCode
import software.amazon.awssdk.services.codewhispererruntime.model.UserIntent
import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.getLogger
-import software.aws.toolkits.jetbrains.core.awsClient
-import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
-import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
import software.aws.toolkits.jetbrains.services.amazonq.codeWhispererUserContext
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererCustomization
import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
import software.aws.toolkits.jetbrains.services.codewhisperer.model.SessionContextNew
@@ -257,11 +255,8 @@ interface CodeWhispererClientAdaptor {
}
}
-class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeWhispererClientAdaptor {
- fun bearerClient(): CodeWhispererRuntimeClient =
- ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance())?.getConnectionSettings()
- ?.awsClient()
- ?: throw Exception("attempt to get bearer client while there is no valid credential")
+open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeWhispererClientAdaptor {
+ fun bearerClient() = QRegionProfileManager.getInstance().getQClient(project)
override fun generateCompletionsPaginator(firstRequest: GenerateCompletionsRequest) = sequence {
var nextToken: String? = firstRequest.nextToken()
@@ -288,7 +283,9 @@ class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeWhispe
// DO NOT directly use this method to fetch customizations, use wrapper [CodeWhispererModelConfigurator.listCustomization()] instead
override fun listAvailableCustomizations(): List =
- bearerClient().listAvailableCustomizationsPaginator(ListAvailableCustomizationsRequest.builder().build())
+ bearerClient().listAvailableCustomizationsPaginator(
+ ListAvailableCustomizationsRequest.builder().profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn).build()
+ )
.stream()
.toList()
.flatMap { resp ->
@@ -311,6 +308,7 @@ class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeWhispe
builder.uploadId(uploadId)
builder.targetCodeList(targetCode)
builder.userInput(userInput)
+ builder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
// TODO: client token
}
@@ -318,6 +316,7 @@ class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeWhispe
bearerClient().getTestGeneration { builder ->
builder.testGenerationJobId(jobId)
builder.testGenerationJobGroupName(jobGroupName)
+ builder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}
override fun sendUserTriggerDecisionTelemetry(
@@ -363,6 +362,7 @@ class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeWhispe
}
requestBuilder.optOutPreference(getTelemetryOptOutPreference())
requestBuilder.userContext(codeWhispererUserContext())
+ requestBuilder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}
}
@@ -409,6 +409,7 @@ class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeWhispe
}
requestBuilder.optOutPreference(getTelemetryOptOutPreference())
requestBuilder.userContext(codeWhispererUserContext())
+ requestBuilder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}
}
@@ -435,6 +436,7 @@ class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeWhispe
}
requestBuilder.optOutPreference(getTelemetryOptOutPreference())
requestBuilder.userContext(codeWhispererUserContext())
+ requestBuilder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}
override fun sendUserModificationTelemetry(
@@ -462,6 +464,7 @@ class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeWhispe
}
requestBuilder.optOutPreference(getTelemetryOptOutPreference())
requestBuilder.userContext(codeWhispererUserContext())
+ requestBuilder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}
override fun sendCodeScanTelemetry(
@@ -481,6 +484,7 @@ class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeWhispe
}
requestBuilder.optOutPreference(getTelemetryOptOutPreference())
requestBuilder.userContext(codeWhispererUserContext())
+ requestBuilder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}
override fun sendCodeScanSucceededTelemetry(
@@ -503,6 +507,7 @@ class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeWhispe
}
requestBuilder.optOutPreference(getTelemetryOptOutPreference())
requestBuilder.userContext(codeWhispererUserContext())
+ requestBuilder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}
override fun sendCodeScanFailedTelemetry(
@@ -522,6 +527,7 @@ class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeWhispe
}
requestBuilder.optOutPreference(getTelemetryOptOutPreference())
requestBuilder.userContext(codeWhispererUserContext())
+ requestBuilder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}
override fun sendCodeFixGenerationTelemetry(
@@ -548,6 +554,7 @@ class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeWhispe
}
requestBuilder.optOutPreference(getTelemetryOptOutPreference())
requestBuilder.userContext(codeWhispererUserContext())
+ requestBuilder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}
override fun sendCodeFixAcceptanceTelemetry(
@@ -574,6 +581,7 @@ class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeWhispe
}
requestBuilder.optOutPreference(getTelemetryOptOutPreference())
requestBuilder.userContext(codeWhispererUserContext())
+ requestBuilder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}
override fun sendCodeScanRemediationTelemetry(
@@ -605,6 +613,7 @@ class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeWhispe
}
requestBuilder.optOutPreference(getTelemetryOptOutPreference())
requestBuilder.userContext(codeWhispererUserContext())
+ requestBuilder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}
override fun sendTestGenerationEvent(
@@ -638,10 +647,12 @@ class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeWhispe
}
requestBuilder.optOutPreference(getTelemetryOptOutPreference())
requestBuilder.userContext(codeWhispererUserContext())
+ requestBuilder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}
override fun listFeatureEvaluations(): ListFeatureEvaluationsResponse = bearerClient().listFeatureEvaluations {
it.userContext(codeWhispererUserContext())
+ it.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}
override fun sendMetricDataTelemetry(eventName: String, metadata: Map): SendTelemetryEventResponse =
@@ -656,6 +667,7 @@ class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeWhispe
}
requestBuilder.optOutPreference(getTelemetryOptOutPreference())
requestBuilder.userContext(codeWhispererUserContext())
+ requestBuilder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}
override fun sendChatAddMessageTelemetry(
@@ -694,6 +706,7 @@ class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeWhispe
}
requestBuilder.optOutPreference(getTelemetryOptOutPreference())
requestBuilder.userContext(codeWhispererUserContext())
+ requestBuilder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}
override fun sendChatInteractWithMessageTelemetry(
@@ -723,6 +736,7 @@ class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeWhispe
}
requestBuilder.optOutPreference(getTelemetryOptOutPreference())
requestBuilder.userContext(codeWhispererUserContext())
+ requestBuilder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}
override fun sendChatUserModificationTelemetry(
@@ -747,6 +761,7 @@ class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeWhispe
}
requestBuilder.optOutPreference(getTelemetryOptOutPreference())
requestBuilder.userContext(codeWhispererUserContext())
+ requestBuilder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}
override fun sendInlineChatTelemetry(
@@ -782,6 +797,7 @@ class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeWhispe
}
requestBuilder.optOutPreference(getTelemetryOptOutPreference())
requestBuilder.userContext(codeWhispererUserContext())
+ requestBuilder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}
companion object {
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererModelConfigurator.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererModelConfigurator.kt
index f19df1595bf..6f3a84fcbf9 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererModelConfigurator.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererModelConfigurator.kt
@@ -22,6 +22,8 @@ import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService
import software.aws.toolkits.jetbrains.services.amazonq.calculateIfIamIdentityCenterConnection
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileSelectedListener
import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
import software.aws.toolkits.jetbrains.utils.notifyInfo
@@ -48,6 +50,7 @@ private fun notifyInvalidSelectedCustomization(project: Project) {
}
private fun notifyNewCustomization(project: Project) {
+ if (ApplicationManager.getApplication().isUnitTestMode) return
notifyInfo(
title = message("codewhisperer.custom.dialog.title"),
content = message("codewhisperer.notification.custom.new_customization"),
@@ -81,6 +84,18 @@ class DefaultCodeWhispererModelConfigurator : CodeWhispererModelConfigurator, Pe
private var customizationArnOverrideV2: String? = null
+ init {
+ ApplicationManager.getApplication().messageBus.connect(this).subscribe(
+ QRegionProfileSelectedListener.TOPIC,
+ object : QRegionProfileSelectedListener {
+ override fun onProfileSelected(project: Project, profile: QRegionProfile?) {
+ pluginAwareExecuteOnPooledThread {
+ CodeWhispererModelConfigurator.getInstance().listCustomizations(project, passive = true)
+ }
+ }
+ }
+ )
+ }
override fun showConfigDialog(project: Project) {
runInEdt {
calculateIfIamIdentityCenterConnection(project) {
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/explorer/QStatusBarLoggedInActionGroup.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/explorer/QStatusBarLoggedInActionGroup.kt
index 4f7833a1fc0..3dd88d45d22 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/explorer/QStatusBarLoggedInActionGroup.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/explorer/QStatusBarLoggedInActionGroup.kt
@@ -8,10 +8,15 @@ import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.actionSystem.Separator
+import com.intellij.openapi.project.Project
import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
import software.aws.toolkits.jetbrains.core.credentials.actions.SsoLogoutAction
import software.aws.toolkits.jetbrains.core.credentials.pinning.CodeWhispererConnection
+import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
+import software.aws.toolkits.jetbrains.core.credentials.sono.isSono
+import software.aws.toolkits.jetbrains.services.amazonq.actions.QSwitchProfilesAction
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
import software.aws.toolkits.jetbrains.services.codewhisperer.actions.CodeWhispererConnectOnGithubAction
import software.aws.toolkits.jetbrains.services.codewhisperer.actions.CodeWhispererLearnMoreAction
import software.aws.toolkits.jetbrains.services.codewhisperer.actions.CodeWhispererProvideFeedbackAction
@@ -48,18 +53,11 @@ class QStatusBarLoggedInActionGroup : DefaultActionGroup() {
}
override fun getChildren(e: AnActionEvent?) = e?.project?.let {
+ val isPendingActiveProfile = QRegionProfileManager.getInstance().hasValidConnectionButNoActiveProfile(it)
buildList {
- add(Separator.create())
- add(Separator.create(message("codewhisperer.statusbar.sub_menu.inline.title")))
- addAll(buildActionListForInlineSuggestions(it, actionProvider))
-
- add(Separator.create())
- add(Separator.create(message("codewhisperer.statusbar.sub_menu.security_scans.title")))
- addAll(buildActionListForCodeScan(it, actionProvider))
-
- add(Separator.create())
- add(Separator.create(message("codewhisperer.statusbar.sub_menu.other_features.title")))
- addAll(buildActionListForOtherFeatures(it, actionProvider))
+ if (!isPendingActiveProfile) {
+ addAll(buildActionListForActiveProfileSelected(it, actionProvider))
+ }
add(Separator.create())
add(Separator.create(message("codewhisperer.statusbar.sub_menu.connect_help.title")))
@@ -67,6 +65,10 @@ class QStatusBarLoggedInActionGroup : DefaultActionGroup() {
add(Separator.create())
add(CodeWhispererShowSettingsAction())
+ (
+ ToolkitConnectionManager.getInstance(it).activeConnectionForFeature(QConnection.getInstance()) as? AwsBearerTokenConnection
+ )?.takeIf { !it.isSono() }
+ ?.let { add(QSwitchProfilesAction()) }
ToolkitConnectionManager.getInstance(it).activeConnectionForFeature(CodeWhispererConnection.getInstance())?.let { c ->
(c as? AwsBearerTokenConnection)?.let { connection ->
add(SsoLogoutAction(connection))
@@ -74,4 +76,21 @@ class QStatusBarLoggedInActionGroup : DefaultActionGroup() {
}
}.toTypedArray()
}.orEmpty()
+
+ private fun buildActionListForActiveProfileSelected(
+ project: Project,
+ actionProvider: ActionProvider,
+ ): List = buildList {
+ add(Separator.create())
+ add(Separator.create(message("codewhisperer.statusbar.sub_menu.inline.title")))
+ addAll(buildActionListForInlineSuggestions(project, actionProvider))
+
+ add(Separator.create())
+ add(Separator.create(message("codewhisperer.statusbar.sub_menu.security_scans.title")))
+ addAll(buildActionListForCodeScan(project, actionProvider))
+
+ add(Separator.create())
+ add(Separator.create(message("codewhisperer.statusbar.sub_menu.other_features.title")))
+ addAll(buildActionListForOtherFeatures(project, actionProvider))
+ }
}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src-242+/software/aws/toolkits/jetbrains/services/codewhisperer/inlay/InlineCompletionRemoteRendererFactory.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/inlay/InlineCompletionRemoteRendererFactory.kt
similarity index 60%
rename from plugins/amazonq/codewhisperer/jetbrains-community/src-242+/software/aws/toolkits/jetbrains/services/codewhisperer/inlay/InlineCompletionRemoteRendererFactory.kt
rename to plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/inlay/InlineCompletionRemoteRendererFactory.kt
index 68e82ab604e..e62d0fb1a1a 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src-242+/software/aws/toolkits/jetbrains/services/codewhisperer/inlay/InlineCompletionRemoteRendererFactory.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/inlay/InlineCompletionRemoteRendererFactory.kt
@@ -1,5 +1,6 @@
-// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
+
package software.aws.toolkits.jetbrains.services.codewhisperer.inlay
import com.intellij.codeInsight.inline.completion.render.InlineCompletionLineRenderer
@@ -13,13 +14,24 @@ import com.intellij.xdebugger.ui.DebuggerColors
)
object InlineCompletionRemoteRendererFactory {
fun createLineInlay(editor: Editor, text: String): EditorCustomElementRenderer =
- InlineCompletionLineRenderer(editor, text, editor.colorsScheme.getAttributes(DebuggerColors.INLINED_VALUES_EXECUTION_LINE))
+ InlineCompletionLineRenderer(
+ editor,
+ text,
+ editor.colorsScheme.getAttributes(DebuggerColors.INLINED_VALUES_EXECUTION_LINE)
+ )
fun createBlockInlays(editor: Editor, block: List): List =
block.map {
InlineCompletionLineRenderer(
editor,
- listOf(InlineCompletionRenderTextBlock(it, editor.colorsScheme.getAttributes(DebuggerColors.INLINED_VALUES_EXECUTION_LINE)))
+ listOf(
+ InlineCompletionRenderTextBlock(
+ it,
+ editor.colorsScheme.getAttributes(
+ DebuggerColors.INLINED_VALUES_EXECUTION_LINE
+ )
+ )
+ )
)
}
}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererAutoTriggerHandler.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererAutoTriggerHandler.kt
index fe73ff91e48..73b9f59be6f 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererAutoTriggerHandler.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererAutoTriggerHandler.kt
@@ -7,6 +7,7 @@ import com.intellij.openapi.editor.Editor
import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
import software.aws.toolkits.jetbrains.services.codewhisperer.model.LatencyContext
import software.aws.toolkits.jetbrains.services.codewhisperer.model.TriggerTypeInfo
import software.aws.toolkits.telemetry.CodewhispererTriggerType
@@ -17,6 +18,10 @@ interface CodeWhispererAutoTriggerHandler {
automatedTriggerType: CodeWhispererAutomatedTriggerType,
latencyContext: LatencyContext,
) {
+ val project = editor.project ?: return
+ if (QRegionProfileManager.getInstance().hasValidConnectionButNoActiveProfile(project)) {
+ return
+ }
val triggerTypeInfo = TriggerTypeInfo(CodewhispererTriggerType.AutoTrigger, automatedTriggerType)
LOG.debug { "autotriggering CodeWhisperer with type ${automatedTriggerType.telemetryType}" }
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererService.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererService.kt
index 111ccdc509f..50a579fedd1 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererService.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererService.kt
@@ -53,6 +53,10 @@ import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
import software.aws.toolkits.jetbrains.core.credentials.pinning.CodeWhispererConnection
import software.aws.toolkits.jetbrains.services.amazonq.SUPPLEMENTAL_CONTEXT_TIMEOUT
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.GetConfigurationFromServerParams
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.LspServerConfigurations
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor
import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator
import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorManager
@@ -91,6 +95,9 @@ import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.CodewhispererCompletionType
import software.aws.toolkits.telemetry.CodewhispererSuggestionState
import software.aws.toolkits.telemetry.CodewhispererTriggerType
+import java.net.URI
+import java.nio.file.Paths
+import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
@Service
@@ -231,7 +238,9 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable {
buildCodeWhispererRequest(
requestContext.fileContextInfo,
requestContext.awaitSupplementalContext(),
- requestContext.customizationArn
+ requestContext.customizationArn,
+ requestContext.profileArn,
+ requestContext.workspaceId,
)
)
@@ -666,7 +675,44 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable {
// 5. customization
val customizationArn = CodeWhispererModelConfigurator.getInstance().activeCustomization(project)?.arn
- return RequestContext(project, editor, triggerTypeInfo, caretPosition, fileContext, supplementalContext, connection, latencyContext, customizationArn)
+ val profileArn = QRegionProfileManager.getInstance().activeProfile(project)?.arn
+
+ var workspaceId: String? = null
+ try {
+ val workspacesInfos = getWorkspaceIds(project).get().workspaces
+ for (workspaceInfo in workspacesInfos) {
+ val workspaceRootPath = Paths.get(URI(workspaceInfo.workspaceRoot)).toString()
+ if (psiFile.virtualFile.path.startsWith(workspaceRootPath)) {
+ workspaceId = workspaceInfo.workspaceId
+ LOG.info { "Found workspaceId from LSP '$workspaceId'" }
+ break
+ }
+ }
+ } catch (e: Exception) {
+ LOG.warn { "Cannot get workspaceId from LSP'$e'" }
+ }
+ return RequestContext(
+ project,
+ editor,
+ triggerTypeInfo,
+ caretPosition,
+ fileContext,
+ supplementalContext,
+ connection,
+ latencyContext,
+ customizationArn,
+ profileArn,
+ workspaceId,
+ )
+ }
+
+ private fun getWorkspaceIds(project: Project): CompletableFuture {
+ val payload = GetConfigurationFromServerParams(
+ section = "aws.q.workspaceContext"
+ )
+ return AmazonQLspService.executeIfRunning(project) { server ->
+ server.getConfigurationFromServer(payload)
+ } ?: (CompletableFuture.failedFuture(IllegalStateException("LSP Server not running")))
}
fun validateResponse(response: GenerateCompletionsResponse): GenerateCompletionsResponse {
@@ -800,6 +846,8 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable {
fileContextInfo: FileContextInfo,
supplementalContext: SupplementalContextInfo?,
customizationArn: String?,
+ profileArn: String?,
+ workspaceId: String?,
): GenerateCompletionsRequest {
val programmingLanguage = ProgrammingLanguage.builder()
.languageName(fileContextInfo.programmingLanguage.toCodeWhispererRuntimeLanguage().languageId)
@@ -828,6 +876,8 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable {
.referenceTrackerConfiguration { it.recommendationsWithReferences(includeCodeWithReference) }
.customizationArn(customizationArn)
.optOutPreference(getTelemetryOptOutPreference())
+ .profileArn(profileArn)
+ .workspaceId(workspaceId)
.build()
}
}
@@ -843,6 +893,8 @@ data class RequestContext(
val connection: ToolkitConnection?,
val latencyContext: LatencyContext,
val customizationArn: String?,
+ val profileArn: String?,
+ val workspaceId: String?,
) {
// TODO: should make the entire getRequestContext() suspend function instead of making supplemental context only
var supplementalContext: SupplementalContextInfo? = null
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererServiceNew.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererServiceNew.kt
index c79ff7c0123..633a20ac0f9 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererServiceNew.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererServiceNew.kt
@@ -53,6 +53,7 @@ import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
import software.aws.toolkits.jetbrains.core.credentials.pinning.CodeWhispererConnection
import software.aws.toolkits.jetbrains.services.amazonq.SUPPLEMENTAL_CONTEXT_TIMEOUT
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor
import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator
import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorManagerNew
@@ -243,7 +244,8 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable {
buildCodeWhispererRequest(
requestContext.fileContextInfo,
requestContext.awaitSupplementalContext(),
- requestContext.customizationArn
+ requestContext.customizationArn,
+ requestContext.profileArn
)
)
@@ -707,7 +709,9 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable {
// 5. customization
val customizationArn = CodeWhispererModelConfigurator.getInstance().activeCustomization(project)?.arn
- return RequestContextNew(project, editor, triggerTypeInfo, caretPosition, fileContext, supplementalContext, connection, customizationArn)
+ val profileArn = QRegionProfileManager.getInstance().activeProfile(project)?.arn
+
+ return RequestContextNew(project, editor, triggerTypeInfo, caretPosition, fileContext, supplementalContext, connection, customizationArn, profileArn)
}
fun validateResponse(response: GenerateCompletionsResponse): GenerateCompletionsResponse {
@@ -825,6 +829,7 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable {
fileContextInfo: FileContextInfo,
supplementalContext: SupplementalContextInfo?,
customizationArn: String?,
+ profileArn: String?,
): GenerateCompletionsRequest {
val programmingLanguage = ProgrammingLanguage.builder()
.languageName(fileContextInfo.programmingLanguage.toCodeWhispererRuntimeLanguage().languageId)
@@ -853,6 +858,7 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable {
.referenceTrackerConfiguration { it.recommendationsWithReferences(includeCodeWithReference) }
.customizationArn(customizationArn)
.optOutPreference(getTelemetryOptOutPreference())
+ .profileArn(profileArn)
.build()
}
}
@@ -867,6 +873,7 @@ data class RequestContextNew(
private val supplementalContextDeferred: Deferred,
val connection: ToolkitConnection?,
val customizationArn: String?,
+ val profileArn: String?,
) {
// TODO: should make the entire getRequestContext() suspend function instead of making supplemental context only
var supplementalContext: SupplementalContextInfo? = null
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt
index 9589d8c0a96..77f901950c7 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt
@@ -5,13 +5,16 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.settings
import com.intellij.icons.AllIcons
import com.intellij.ide.DataManager
+import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.options.BoundConfigurable
import com.intellij.openapi.options.Configurable
import com.intellij.openapi.options.SearchableConfigurable
import com.intellij.openapi.options.ex.Settings
import com.intellij.openapi.project.Project
+import com.intellij.openapi.ui.emptyText
import com.intellij.ui.components.ActionLink
import com.intellij.ui.components.fields.ExpandableTextField
+import com.intellij.ui.dsl.builder.Align
import com.intellij.ui.dsl.builder.bindIntText
import com.intellij.ui.dsl.builder.bindSelected
import com.intellij.ui.dsl.builder.bindText
@@ -24,6 +27,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWh
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.isCodeWhispererEnabled
import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings
+import software.aws.toolkits.jetbrains.settings.LspSettings
import software.aws.toolkits.resources.message
import java.awt.Font
import java.util.concurrent.TimeUnit
@@ -61,6 +65,24 @@ class CodeWhispererConfigurable(private val project: Project) :
}
}
+ group(message("amazonqFeatureDev.placeholder.lsp")) {
+ row(message("amazonqFeatureDev.placeholder.select_lsp_artifact")) {
+ val fileChooserDescriptor = FileChooserDescriptorFactory.createSingleFileDescriptor()
+ fileChooserDescriptor.isForcedToUseIdeaFileChooser = true
+
+ textFieldWithBrowseButton(fileChooserDescriptor = fileChooserDescriptor)
+ .bindText(
+ { LspSettings.getInstance().getArtifactPath().orEmpty() },
+ { LspSettings.getInstance().setArtifactPath(it) }
+ )
+ .applyToComponent {
+ emptyText.text = message("executableCommon.auto_managed")
+ }
+ .resizableColumn()
+ .align(Align.FILL)
+ }
+ }
+
group(message("aws.settings.codewhisperer.group.general")) {
row {
checkBox(message("aws.settings.codewhisperer.include_code_with_reference")).apply {
@@ -116,6 +138,20 @@ class CodeWhispererConfigurable(private val project: Project) :
}
group(message("aws.settings.codewhisperer.group.q_chat")) {
+ row {
+ checkBox(message("aws.settings.codewhisperer.workspace_context")).apply {
+ connect.subscribe(
+ ToolkitConnectionManagerListener.TOPIC,
+ object : ToolkitConnectionManagerListener {
+ override fun activeConnectionChanged(newConnection: ToolkitConnection?) {
+ enabled(isCodeWhispererEnabled(project))
+ }
+ }
+ )
+ enabled(invoke)
+ bindSelected(codeWhispererSettings::isWorkspaceContextEnabled, codeWhispererSettings::toggleWorkspaceContextEnabled)
+ }.comment(message("aws.settings.codewhisperer.workspace_context.tooltip"))
+ }.visible(false)
row {
checkBox(message("aws.settings.codewhisperer.project_context")).apply {
connect.subscribe(
@@ -133,7 +169,7 @@ class CodeWhispererConfigurable(private val project: Project) :
row(message("aws.settings.codewhisperer.project_context_index_thread")) {
intTextField(
- range = IntRange(0, 50)
+ range = CodeWhispererSettings.CONTEXT_INDEX_THREADS
).bindIntText(codeWhispererSettings::getProjectContextIndexThreadCount, codeWhispererSettings::setProjectContextIndexThreadCount)
.apply {
connect.subscribe(
@@ -150,7 +186,7 @@ class CodeWhispererConfigurable(private val project: Project) :
row(message("aws.settings.codewhisperer.project_context_index_max_size")) {
intTextField(
- range = IntRange(1, 4096)
+ range = CodeWhispererSettings.CONTEXT_INDEX_SIZE
).bindIntText(codeWhispererSettings::getProjectContextIndexMaxSize, codeWhispererSettings::setProjectContextIndexMaxSize)
.apply {
connect.subscribe(
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/status/CodeWhispererStatusBarWidget.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/status/CodeWhispererStatusBarWidget.kt
index 2ee5f8be678..529e20d3a8e 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/status/CodeWhispererStatusBarWidget.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/status/CodeWhispererStatusBarWidget.kt
@@ -22,6 +22,9 @@ import software.aws.toolkits.jetbrains.core.credentials.profiles.ProfileWatcher
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProviderListener
import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService
import software.aws.toolkits.jetbrains.services.amazonq.gettingstarted.QActionGroups.Q_SIGNED_OUT_ACTION_GROUP
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileSelectedListener
import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererCustomizationListener
import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.QStatusBarLoggedInActionGroup
@@ -76,6 +79,20 @@ class CodeWhispererStatusBarWidget(project: Project) :
}
}
)
+
+ ApplicationManager.getApplication().messageBus.connect(this).subscribe(
+ QRegionProfileSelectedListener.TOPIC,
+ object : QRegionProfileSelectedListener {
+ override fun onProfileSelected(
+ project: Project,
+ profile: QRegionProfile?,
+ ) {
+ if (project == this@CodeWhispererStatusBarWidget.project) {
+ statusBar.updateWidget(ID)
+ }
+ }
+ }
+ )
}
override fun ID(): String = ID
@@ -121,7 +138,7 @@ class CodeWhispererStatusBarWidget(project: Project) :
}
override fun getIcon(): Icon =
- if (isQExpired(project)) {
+ if (isQExpired(project) || QRegionProfileManager.getInstance().isPendingProfileSelection(project)) {
AllIcons.General.BalloonWarning
} else if (!isQConnected(project)) {
AllIcons.RunConfigurations.TestState.Run
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererConstants.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererConstants.kt
index 1e36ceddc5d..9e328c54b81 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererConstants.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererConstants.kt
@@ -6,7 +6,6 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.util
import com.intellij.openapi.actionSystem.DataKey
import com.intellij.openapi.editor.markup.EffectType
import com.intellij.openapi.editor.markup.TextAttributes
-import com.intellij.openapi.util.registry.Registry
import com.intellij.ui.JBColor
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.codewhispererruntime.model.AccessDeniedException
@@ -154,12 +153,8 @@ object CodeWhispererConstants {
}
object Config {
- val CODEWHISPERER_ENDPOINT
- get() = System.getenv("__CODEWHISPERER_ENDPOINT") ?: Registry.get("amazon.q.endpoint").asString()
-
const val CODEWHISPERER_IDPOOL_ID = "us-east-1:70717e99-906f-4add-908c-bd9074a2f5b9"
val Sigv4ClientRegion = Region.US_EAST_1
- val BearerClientRegion = Region.US_EAST_1
}
object Customization {
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererEndpointCustomizer.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererEndpointCustomizer.kt
index 004181a6ec5..8d74ecca7e0 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererEndpointCustomizer.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererEndpointCustomizer.kt
@@ -17,9 +17,12 @@ import software.amazon.awssdk.core.retry.RetryPolicy
import software.amazon.awssdk.http.SdkHttpRequest
import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient
import software.amazon.awssdk.http.nio.netty.ProxyConfiguration
+import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.codewhispererruntime.CodeWhispererRuntimeClientBuilder
import software.amazon.awssdk.services.codewhispererstreaming.CodeWhispererStreamingAsyncClientBuilder
import software.aws.toolkits.core.ToolkitClientCustomizer
+import software.aws.toolkits.core.utils.tryOrNull
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QEndpoints
import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings
import java.net.Proxy
import java.net.URI
@@ -37,10 +40,12 @@ class CodeWhispererEndpointCustomizer : ToolkitClientCustomizer {
clientOverrideConfiguration: ClientOverrideConfiguration.Builder,
) {
if (builder is CodeWhispererRuntimeClientBuilder || builder is CodeWhispererStreamingAsyncClientBuilder) {
- val endpoint = URI.create(CodeWhispererConstants.Config.CODEWHISPERER_ENDPOINT)
+ val endpoint = tryOrNull { QEndpoints.getQEndpointWithRegion(regionId) }
+ ?.let { URI.create(it) }
+ ?: URI.create(QEndpoints.Q_DEFAULT_SERVICE_CONFIG.ENDPOINT)
builder
.endpointOverride(endpoint)
- .region(CodeWhispererConstants.Config.BearerClientRegion)
+ .region(Region.of(regionId))
clientOverrideConfiguration.retryPolicy(RetryPolicy.none())
clientOverrideConfiguration.addExecutionInterceptor(
object : ExecutionInterceptor {
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererZipUploadManager.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererZipUploadManager.kt
index 8d997645a14..f964092a8d5 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererZipUploadManager.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererZipUploadManager.kt
@@ -22,6 +22,7 @@ import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.jetbrains.core.AwsClientManager
import software.aws.toolkits.jetbrains.services.amazonq.RetryableOperation
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanServerException
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanSession.Companion.APPLICATION_ZIP
import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanSession.Companion.AWS_KMS
@@ -202,6 +203,7 @@ class CodeWhispererZipUploadManager(private val project: Project) {
UploadContext.fromCodeAnalysisUploadContext(CodeAnalysisUploadContext.builder().codeScanName(taskName).build())
}
)
+ .profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
.build()
)
},
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererCodeCoverageTrackerTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererCodeCoverageTrackerTest.kt
index a82f8700eb4..bf3db6cdf29 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererCodeCoverageTrackerTest.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererCodeCoverageTrackerTest.kt
@@ -176,7 +176,9 @@ internal class CodeWhispererCodeCoverageTrackerTestPython : CodeWhispererCodeCov
},
null,
mock(),
- aString()
+ aString(),
+ aString(),
+ aString(),
)
val responseContext = ResponseContext("sessionId")
val recommendationContext = RecommendationContext(
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererConfigurableTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererConfigurableTest.kt
index 5224284b42a..67aff7a14e3 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererConfigurableTest.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererConfigurableTest.kt
@@ -39,17 +39,18 @@ class CodeWhispererConfigurableTest : CodeWhispererTestBase() {
val checkboxes = panel.components.filterIsInstance()
- assertThat(checkboxes.size).isEqualTo(5)
+ assertThat(checkboxes.size).isEqualTo(6)
assertThat(checkboxes.map { it.text }).containsExactlyInAnyOrder(
message("aws.settings.codewhisperer.include_code_with_reference"),
message("aws.settings.codewhisperer.configurable.opt_out.title"),
message("aws.settings.codewhisperer.automatic_import_adder"),
+ "Workspace context",
message("aws.settings.codewhisperer.project_context"),
message("aws.settings.codewhisperer.project_context_gpu")
)
val comments = panel.components.filterIsInstance()
- assertThat(comments.size).isEqualTo(8)
+ assertThat(comments.size).isEqualTo(9)
mockCodeWhispererEnabledStatus(false)
ApplicationManager.getApplication().messageBus.syncPublisher(ToolkitConnectionManagerListener.TOPIC)
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererFeatureConfigServiceTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererFeatureConfigServiceTest.kt
index 56f6f66b517..378e2703756 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererFeatureConfigServiceTest.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererFeatureConfigServiceTest.kt
@@ -3,6 +3,7 @@
package software.aws.toolkits.jetbrains.services.codewhisperer
+import com.intellij.openapi.project.Project
import com.intellij.testFramework.ApplicationRule
import com.intellij.testFramework.DisposableRule
import com.intellij.testFramework.ProjectRule
@@ -34,6 +35,7 @@ import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
import software.aws.toolkits.jetbrains.core.credentials.sono.SONO_URL
import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
import kotlin.reflect.full.memberFunctions
import kotlin.test.Test
@@ -62,7 +64,7 @@ class CodeWhispererFeatureConfigServiceTest {
@Test
fun `test highlightCommand returns non-empty`() {
- mockClientManagerRule.create().stub {
+ val mockClient = mockClientManagerRule.create().stub {
on { listFeatureEvaluations(any()) } doReturn ListFeatureEvaluationsResponse.builder().featureEvaluations(
listOf(
FeatureEvaluation.builder()
@@ -74,9 +76,16 @@ class CodeWhispererFeatureConfigServiceTest {
).build()
}
+ projectRule.project.replaceService(
+ QRegionProfileManager::class.java,
+ mock { on { getQClient(any(), eq(CodeWhispererRuntimeClient::class)) } doReturn mockClient },
+ disposableRule.disposable
+ )
+
val mockTokenSettings = mock {
on { providerId } doReturn "mock"
on { region } doReturn AwsRegion.GLOBAL
+ on { withRegion(any()) } doReturn this.mock
}
val mockSsoConnection = mock {
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererModelConfiguratorTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererModelConfiguratorTest.kt
index b1fa0ef5592..751db478b5b 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererModelConfiguratorTest.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererModelConfiguratorTest.kt
@@ -7,8 +7,10 @@ import com.intellij.openapi.application.ApplicationManager
import com.intellij.testFramework.ApplicationRule
import com.intellij.testFramework.DisposableRule
import com.intellij.testFramework.ProjectRule
+import com.intellij.testFramework.registerServiceInstance
import com.intellij.testFramework.replaceService
import com.intellij.util.xmlb.XmlSerializer
+import migration.software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator
import org.assertj.core.api.Assertions.assertThat
import org.jdom.output.XMLOutputter
import org.junit.Before
@@ -40,10 +42,14 @@ import software.aws.toolkits.jetbrains.core.credentials.sono.isSono
import software.aws.toolkits.jetbrains.core.region.MockRegionProviderRule
import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService
import software.aws.toolkits.jetbrains.services.amazonq.FeatureContext
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileSelectedListener
+import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor
import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererCustomization
import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererCustomizationState
import software.aws.toolkits.jetbrains.services.codewhisperer.customization.DefaultCodeWhispererModelConfigurator
import software.aws.toolkits.jetbrains.utils.xmlElement
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible
@@ -75,6 +81,7 @@ class CodeWhispererModelConfiguratorTest {
private lateinit var sut: DefaultCodeWhispererModelConfigurator
private lateinit var mockClient: CodeWhispererRuntimeClient
private lateinit var abManager: CodeWhispererFeatureConfigService
+ private lateinit var mockClintAdaptor: CodeWhispererClientAdaptor
@Before
fun setup() {
@@ -83,7 +90,11 @@ class CodeWhispererModelConfiguratorTest {
regionProvider.addRegion(Region.US_EAST_1)
regionProvider.addRegion(Region.US_EAST_2)
- sut = DefaultCodeWhispererModelConfigurator()
+ sut = spy(CodeWhispererModelConfigurator.getInstance() as DefaultCodeWhispererModelConfigurator).also { spyInstance ->
+ ApplicationManager.getApplication().replaceService(
+ DefaultCodeWhispererModelConfigurator::class.java, spyInstance, disposableRule.disposable
+ )
+ }
(ToolkitConnectionManager.getInstance(projectRule.project) as DefaultToolkitConnectionManager).loadState(ToolkitConnectionManagerState())
mockClient.stub {
@@ -110,6 +121,9 @@ class CodeWhispererModelConfiguratorTest {
abManager,
disposableRule.disposable
)
+
+ mockClintAdaptor = mock()
+ projectRule.project.registerServiceInstance(CodeWhispererClientAdaptor::class.java, mockClintAdaptor)
}
@Test
@@ -550,4 +564,48 @@ class CodeWhispererModelConfiguratorTest {
assertThat(actual.previousAvailableCustomizations).hasSize(1)
assertThat(actual.previousAvailableCustomizations["fake-sso-url"]).isEqualTo(listOf("arn_1", "arn_2", "arn_3"))
}
+
+ @Test
+ fun `profile switch should keep using existing customization if new list still contains that arn`() {
+ val ssoConn = spy(LegacyManagedBearerSsoConnection(region = "us-east-1", startUrl = "url 1", scopes = Q_SCOPES))
+ ToolkitConnectionManager.getInstance(projectRule.project).switchConnection(ssoConn)
+ val oldCustomization = CodeWhispererCustomization("oldArn", "oldName", "oldDescription")
+ sut.switchCustomization(projectRule.project, oldCustomization)
+
+ assertThat(sut.activeCustomization(projectRule.project)).isEqualTo(oldCustomization)
+
+ val fakeCustomizations = listOf(
+ CodeWhispererCustomization("oldArn", "oldName", "oldDescription")
+ )
+ mockClintAdaptor.stub { on { listAvailableCustomizations() } doReturn fakeCustomizations }
+
+ ApplicationManager.getApplication().messageBus
+ .syncPublisher(QRegionProfileSelectedListener.TOPIC)
+ .onProfileSelected(projectRule.project, null)
+
+ assertThat(sut.activeCustomization(projectRule.project)).isEqualTo(oldCustomization)
+ }
+
+ @Test
+ fun `profile switch should invalidate obsolete customization if it's not in the new list`() {
+ val ssoConn = spy(LegacyManagedBearerSsoConnection(region = "us-east-1", startUrl = "url 1", scopes = Q_SCOPES))
+ ToolkitConnectionManager.getInstance(projectRule.project).switchConnection(ssoConn)
+ val oldCustomization = CodeWhispererCustomization("oldArn", "oldName", "oldDescription")
+ sut.switchCustomization(projectRule.project, oldCustomization)
+ assertThat(sut.activeCustomization(projectRule.project)).isEqualTo(oldCustomization)
+ val fakeCustomizations = listOf(
+ CodeWhispererCustomization("newArn", "newName", "newDescription")
+ )
+ mockClintAdaptor.stub { on { listAvailableCustomizations() } doReturn fakeCustomizations }
+
+ val latch = CountDownLatch(1)
+
+ ApplicationManager.getApplication().messageBus
+ .syncPublisher(QRegionProfileSelectedListener.TOPIC)
+ .onProfileSelected(projectRule.project, null)
+
+ latch.await(2, TimeUnit.SECONDS)
+
+ assertThat(sut.activeCustomization(projectRule.project)).isNull()
+ }
}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererServiceTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererServiceTest.kt
index 1d5646d3869..4ac3f45eddd 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererServiceTest.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererServiceTest.kt
@@ -212,7 +212,9 @@ class CodeWhispererServiceTest {
supplementalContextDeferred = async { mockSupContext },
connection = ToolkitConnectionManager.getInstance(projectRule.project).activeConnection(),
latencyContext = LatencyContext(),
- customizationArn = "fake-arn"
+ customizationArn = "fake-arn",
+ profileArn = "fake-arn",
+ workspaceId = null,
)
)
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererSettingsTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererSettingsTest.kt
index 94474eefa6a..84263c62ad2 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererSettingsTest.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererSettingsTest.kt
@@ -13,10 +13,14 @@ import com.intellij.openapi.wm.impl.status.widget.StatusBarWidgetsManager
import com.intellij.testFramework.replaceService
import com.intellij.testFramework.runInEdtAndWait
import com.intellij.util.xmlb.XmlSerializer
+import io.mockk.every
+import io.mockk.junit4.MockKRule
+import io.mockk.mockkObject
import org.assertj.core.api.Assertions.assertThat
import org.jdom.output.XMLOutputter
import org.junit.Before
import org.junit.Ignore
+import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.never
@@ -24,6 +28,7 @@ import org.mockito.kotlin.spy
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import software.aws.toolkits.jetbrains.core.ToolWindowHeadlessManagerImpl
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService
import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererLoginType
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExploreActionState
import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.isCodeWhispererEnabled
@@ -40,6 +45,9 @@ class CodeWhispererSettingsTest : CodeWhispererTestBase() {
private lateinit var codewhispererServiceSpy: CodeWhispererService
private lateinit var toolWindowHeadlessManager: ToolWindowHeadlessManagerImpl
+ @get:Rule
+ val mockkRule = MockKRule(this)
+
@Before
override fun setUp() {
super.setUp()
@@ -211,6 +219,53 @@ class CodeWhispererSettingsTest : CodeWhispererTestBase() {
assertThat(actual.autoBuildSetting).hasSize(1)
assertThat(actual.autoBuildSetting["project1"]).isTrue()
}
+
+ @Test
+ fun `context thread count is returned in range`() {
+ val sut = CodeWhispererSettings.getInstance()
+
+ mapOf(
+ 1 to 1,
+ 0 to 0,
+ -1 to 0,
+ 123 to 50,
+ 50 to 50,
+ 51 to 50,
+ ).forEach { s, expected ->
+ sut.setProjectContextIndexThreadCount(s)
+ assertThat(sut.getProjectContextIndexThreadCount()).isEqualTo(expected)
+ }
+ }
+
+ @Test
+ fun `context index size is returned in range`() {
+ val sut = CodeWhispererSettings.getInstance()
+
+ mapOf(
+ 1 to 1,
+ 0 to 1,
+ -1 to 1,
+ 123 to 123,
+ 2047 to 2047,
+ 4096 to 4096,
+ 4097 to 4096,
+ ).forEach { s, expected ->
+ sut.setProjectContextIndexMaxSize(s)
+ assertThat(sut.getProjectContextIndexMaxSize()).isEqualTo(expected)
+ }
+ }
+
+ @Test
+ fun `toggleMetricOptIn should trigger LSP didChangeConfiguration`() {
+ mockkObject(AmazonQLspService)
+ every { AmazonQLspService.didChangeConfiguration(any()) } returns Unit
+ settingsManager.toggleMetricOptIn(true)
+ settingsManager.toggleMetricOptIn(false)
+
+ io.mockk.verify(atLeast = 2) {
+ AmazonQLspService.didChangeConfiguration(any())
+ }
+ }
}
class CodeWhispererSettingUnitTest {
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTestBase.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTestBase.kt
index a3e97f60c38..e000bbaeafc 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTestBase.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTestBase.kt
@@ -37,6 +37,7 @@ import software.aws.toolkits.jetbrains.core.credentials.MockCredentialManagerRul
import software.aws.toolkits.jetbrains.core.credentials.MockToolkitAuthManagerRule
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
import software.aws.toolkits.jetbrains.core.credentials.sono.Q_SCOPES
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.codeWhispererRecommendationActionId
import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonFileName
import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonResponse
@@ -88,6 +89,7 @@ open class CodeWhispererTestBase {
protected lateinit var settingsManager: CodeWhispererSettings
private lateinit var originalExplorerActionState: CodeWhispererExploreActionState
private lateinit var originalSettings: CodeWhispererConfiguration
+ private lateinit var qRegionProfileManagerSpy: QRegionProfileManager
@Before
open fun setUp() {
@@ -169,6 +171,16 @@ open class CodeWhispererTestBase {
val conn = authManagerRule.createConnection(ManagedSsoProfile("us-east-1", "url", Q_SCOPES))
ToolkitConnectionManager.getInstance(projectRule.project).switchConnection(conn)
+
+ qRegionProfileManagerSpy = spy(QRegionProfileManager.getInstance())
+ qRegionProfileManagerSpy.stub {
+ onGeneric {
+ hasValidConnectionButNoActiveProfile(any())
+ } doAnswer {
+ false
+ }
+ }
+ ApplicationManager.getApplication().replaceService(QRegionProfileManager::class.java, qRegionProfileManagerSpy, disposableRule.disposable)
}
@After
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/QEndpointsTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/QEndpointsTest.kt
new file mode 100644
index 00000000000..057c61030f7
--- /dev/null
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/QEndpointsTest.kt
@@ -0,0 +1,41 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.codewhisperer
+
+import com.intellij.testFramework.ApplicationExtension
+import com.intellij.testFramework.fixtures.BasePlatformTestCase
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.junit.jupiter.api.extension.RegisterExtension
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QEndpoints
+import software.aws.toolkits.jetbrains.utils.rules.RegistryExtension
+
+@ExtendWith(ApplicationExtension::class)
+class QEndpointsTest : BasePlatformTestCase() {
+
+ @JvmField
+ @RegisterExtension
+ val registryExtension = RegistryExtension()
+
+ @Test
+ fun `test default registry value and parse`() {
+ val testJson = """
+ [
+ {"region": "us-east-1", "endpoint": "https://codewhisperer.us-east-1.amazonaws.com/"},
+ {"region": "eu-central-1", "endpoint": "https://rts.prod-eu-central-1.codewhisperer.ai.aws.dev/"}
+ ]
+ """.trimIndent()
+
+ registryExtension.setValue("amazon.q.endpoints.json", testJson)
+
+ val parsed = QEndpoints.listRegionEndpoints()
+ assertEquals(2, parsed.size)
+
+ val iad = parsed.first { it.region == "us-east-1" }
+ assertEquals("https://codewhisperer.us-east-1.amazonaws.com/", iad.endpoint)
+
+ val fra = parsed.first { it.region == "eu-central-1" }
+ assertEquals("https://rts.prod-eu-central-1.codewhisperer.ai.aws.dev/", fra.endpoint)
+ }
+}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/QRegionProfileManagerTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/QRegionProfileManagerTest.kt
new file mode 100644
index 00000000000..628ab8e36c9
--- /dev/null
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/QRegionProfileManagerTest.kt
@@ -0,0 +1,419 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.codewhisperer
+
+import com.intellij.openapi.project.Project
+import com.intellij.testFramework.DisposableRule
+import com.intellij.testFramework.ProjectRule
+import com.intellij.testFramework.replaceService
+import com.intellij.util.xmlb.XmlSerializer
+import org.assertj.core.api.Assertions.assertThat
+import org.jdom.output.XMLOutputter
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.stub
+import org.mockito.kotlin.whenever
+import software.amazon.awssdk.core.pagination.sync.SdkIterable
+import software.amazon.awssdk.regions.Region
+import software.amazon.awssdk.services.codewhispererruntime.CodeWhispererRuntimeClient
+import software.amazon.awssdk.services.codewhispererruntime.model.ListAvailableProfilesRequest
+import software.amazon.awssdk.services.codewhispererruntime.model.Profile
+import software.amazon.awssdk.services.codewhispererruntime.paginators.ListAvailableProfilesIterable
+import software.amazon.awssdk.services.ssooidc.SsoOidcClient
+import software.aws.toolkits.core.region.AwsRegion
+import software.aws.toolkits.jetbrains.core.MockClientManager
+import software.aws.toolkits.jetbrains.core.MockClientManagerRule
+import software.aws.toolkits.jetbrains.core.MockResourceCacheRule
+import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection
+import software.aws.toolkits.jetbrains.core.credentials.ManagedSsoProfile
+import software.aws.toolkits.jetbrains.core.credentials.MockToolkitAuthManagerRule
+import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
+import software.aws.toolkits.jetbrains.core.credentials.logoutFromSsoConnection
+import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
+import software.aws.toolkits.jetbrains.core.credentials.sono.Q_SCOPES
+import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenAuthState
+import software.aws.toolkits.jetbrains.core.region.MockRegionProviderRule
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QEndpoints
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QProfileResources
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QProfileState
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QProfileSwitchIntent
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileSelectedListener
+import software.aws.toolkits.jetbrains.utils.satisfiesKt
+import software.aws.toolkits.jetbrains.utils.xmlElement
+import java.net.URI
+import java.util.function.Consumer
+import kotlin.test.fail
+
+// TODO: should use junit5
+class QRegionProfileManagerTest {
+ @Rule
+ @JvmField
+ val projectRule = ProjectRule()
+
+ @Rule
+ @JvmField
+ val authRule = MockToolkitAuthManagerRule()
+
+ @JvmField
+ @Rule
+ val clientRule = MockClientManagerRule()
+
+ @Rule
+ @JvmField
+ val regionProviderRule = MockRegionProviderRule()
+
+ @JvmField
+ @Rule
+ val disposableRule = DisposableRule()
+
+ @get:Rule
+ val resourceCache = MockResourceCacheRule()
+
+ private lateinit var sut: QRegionProfileManager
+ private val project: Project
+ get() = projectRule.project
+
+ @Before
+ fun setup() {
+ clientRule.create()
+ regionProviderRule.addRegion(AwsRegion("us-east-1", "US East (N. Virginia)", "aws"))
+ regionProviderRule.addRegion(AwsRegion("eu-central-1", "Europe (Frankfurt)", "aws"))
+ sut = QRegionProfileManager()
+ val conn = authRule.createConnection(ManagedSsoProfile(ssoRegion = "us-east-1", startUrl = "", scopes = Q_SCOPES))
+ ToolkitConnectionManager.getInstance(project).switchConnection(conn)
+ val realManager = ToolkitConnectionManager.getInstance(project)
+ val managerSpy = spy(realManager)
+ doReturn(BearerTokenAuthState.AUTHORIZED).whenever(managerSpy).connectionStateForFeature(QConnection.getInstance())
+ project.replaceService(ToolkitConnectionManager::class.java, managerSpy, disposableRule.disposable)
+ }
+
+ @Test
+ fun `switchProfile should switch the current connection(project) to the selected profile`() {
+ sut.switchProfile(project, QRegionProfile(arn = "arn", profileName = "foo_profile"), QProfileSwitchIntent.User)
+ assertThat(sut.activeProfile(project)).isEqualTo(QRegionProfile(arn = "arn", profileName = "foo_profile"))
+
+ sut.switchProfile(project, QRegionProfile(arn = "another_arn", profileName = "bar_profile"), QProfileSwitchIntent.User)
+ assertThat(sut.activeProfile(project)).isEqualTo(QRegionProfile(arn = "another_arn", profileName = "bar_profile"))
+ }
+
+ @Test
+ fun `switchProfile should return null if user is not connected`() {
+ sut.switchProfile(project, QRegionProfile(arn = "arn", profileName = "foo_profile"), QProfileSwitchIntent.User)
+ assertThat(sut.activeProfile(project)).isEqualTo(QRegionProfile(arn = "arn", profileName = "foo_profile"))
+
+ ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance())?.let {
+ if (it is AwsBearerTokenConnection) {
+ logoutFromSsoConnection(project, it)
+ }
+ }
+ ToolkitConnectionManager.getInstance(project).switchConnection(null)
+ assertThat(ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance())).isNull()
+ assertThat(sut.activeProfile(project)).isNull()
+ }
+
+ @Test
+ fun `data is cleared when user logs out`() {
+ sut.switchProfile(project, QRegionProfile(arn = "arn", profileName = "foo_profile"), QProfileSwitchIntent.User)
+ assertThat(sut.activeProfile(project)).isEqualTo(QRegionProfile(arn = "arn", profileName = "foo_profile"))
+
+ ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance())?.let {
+ if (it is AwsBearerTokenConnection) {
+ logoutFromSsoConnection(project, it)
+ }
+ }
+
+ assertThat(sut.state).satisfiesKt {
+ assertThat(it.connectionIdToActiveProfile).isEmpty()
+ assertThat(it.connectionIdToProfileList).isEmpty()
+ }
+ }
+
+ @Test
+ fun `switch should send message onProfileChanged for active switch`() {
+ var cnt = 0
+ project.messageBus.connect(disposableRule.disposable).subscribe(
+ QRegionProfileSelectedListener.TOPIC,
+ object : QRegionProfileSelectedListener {
+ override fun onProfileSelected(project: Project, profile: QRegionProfile?) {
+ cnt += 1
+ }
+ }
+ )
+
+ assertThat(cnt).isEqualTo(0)
+ sut.switchProfile(project, QRegionProfile(arn = "arn", profileName = "foo_profile"), QProfileSwitchIntent.Reload)
+ assertThat(cnt).isEqualTo(1)
+ sut.switchProfile(project, QRegionProfile(arn = "another_arn", profileName = "BAR_PROFILE"), QProfileSwitchIntent.Reload)
+ assertThat(cnt).isEqualTo(2)
+ }
+
+ @Test
+ fun `listProfiles will call each client to get profiles`() {
+ val client = clientRule.create()
+ val mockResponse: SdkIterable = SdkIterable {
+ listOf(
+ Profile.builder().profileName("FOO").arn("foo").build(),
+ ).toMutableList().iterator()
+ }
+
+ val mockResponse2: SdkIterable = SdkIterable {
+ listOf(
+ Profile.builder().profileName("BAR").arn("bar").build(),
+ ).toMutableList().iterator()
+ }
+
+ val iterable: ListAvailableProfilesIterable = mock {
+ on { it.profiles() } doReturn mockResponse doReturn mockResponse2
+ }
+
+ // TODO: not sure if we can mock client with different region different response?
+ client.stub {
+ onGeneric { listAvailableProfilesPaginator(any>()) } doReturn iterable
+ }
+ val connectionSettings = sut.getQClientSettings(project)
+ resourceCache.addEntry(connectionSettings, QProfileResources.LIST_REGION_PROFILES, QProfileResources.LIST_REGION_PROFILES.fetch(connectionSettings))
+
+ assertThat(sut.listRegionProfiles(project))
+ .hasSize(2)
+ .containsExactlyInAnyOrder(
+ QRegionProfile("FOO", "foo"),
+ QRegionProfile("BAR", "bar")
+ )
+ }
+
+ @Test
+ fun `validateProfile should cross validate selected profile with latest API response for current project and remove it if its not longer accessible`() {
+ val client = clientRule.create()
+ val mockResponse: SdkIterable = SdkIterable {
+ listOf(
+ Profile.builder().profileName("foo").arn("foo-arn-v2").build(),
+ Profile.builder().profileName("bar").arn("bar-arn").build(),
+ ).toMutableList().iterator()
+ }
+ val iterable: ListAvailableProfilesIterable = mock {
+ on { it.profiles() } doReturn mockResponse
+ }
+ client.stub {
+ onGeneric { listAvailableProfilesPaginator(any>()) } doReturn iterable
+ }
+
+ val activeConn =
+ ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance()) ?: fail("connection shouldn't be null")
+ val anotherConn = authRule.createConnection(ManagedSsoProfile(ssoRegion = "us-east-1", startUrl = "anotherUrl", scopes = Q_SCOPES))
+ val fooProfile = QRegionProfile("foo", "foo-arn")
+ val barProfile = QRegionProfile("bar", "bar-arn")
+ val state = QProfileState().apply {
+ this.connectionIdToActiveProfile[activeConn.id] = fooProfile
+ this.connectionIdToActiveProfile[anotherConn.id] = barProfile
+ }
+ sut.loadState(state)
+ assertThat(sut.activeProfile(project)).isEqualTo(fooProfile)
+
+ sut.validateProfile(project)
+ assertThat(sut.activeProfile(project)).isNull()
+ assertThat(sut.state.connectionIdToActiveProfile).isEqualTo(mapOf(anotherConn.id to barProfile))
+ }
+
+ @Test
+ fun `clientSettings should return the region Q profile specify`() {
+ MockClientManager.useRealImplementations(disposableRule.disposable)
+ sut.switchProfile(
+ project,
+ QRegionProfile(arn = "arn:aws:codewhisperer:eu-central-1:123456789012:profile/FOO_PROFILE", profileName = "FOO_PROFILE"),
+ QProfileSwitchIntent.User
+ )
+ assertThat(
+ sut.activeProfile(project)
+ ).isEqualTo(QRegionProfile(arn = "arn:aws:codewhisperer:eu-central-1:123456789012:profile/FOO_PROFILE", profileName = "FOO_PROFILE"))
+
+ val settings = sut.getQClientSettings(project)
+ assertThat(settings.region.id).isEqualTo(Region.EU_CENTRAL_1.id())
+
+ sut.switchProfile(
+ project,
+ QRegionProfile(arn = "arn:aws:codewhisperer:us-east-1:123456789012:profile/BAR_PROFILE", profileName = "BAR_PROFILE"),
+ QProfileSwitchIntent.User
+ )
+ assertThat(
+ sut.activeProfile(project)
+ ).isEqualTo(QRegionProfile(arn = "arn:aws:codewhisperer:us-east-1:123456789012:profile/BAR_PROFILE", profileName = "BAR_PROFILE"))
+
+ val settings2 = sut.getQClientSettings(project)
+ assertThat(settings2.region.id).isEqualTo(Region.US_EAST_1.id())
+ }
+
+ @Test
+ fun `getClient should return correct client with region and endpoint`() {
+ MockClientManager.useRealImplementations(disposableRule.disposable)
+
+ sut.switchProfile(
+ project,
+ QRegionProfile(arn = "arn:aws:codewhisperer:eu-central-1:123456789012:profile/FOO_PROFILE", profileName = "FOO_PROFILE"),
+ QProfileSwitchIntent.User
+ )
+ assertThat(
+ sut.activeProfile(project)
+ ).isEqualTo(QRegionProfile(arn = "arn:aws:codewhisperer:eu-central-1:123456789012:profile/FOO_PROFILE", profileName = "FOO_PROFILE"))
+ assertThat(sut.getQClientSettings(project).region.id).isEqualTo(Region.EU_CENTRAL_1.id())
+
+ val client = sut.getQClient(project)
+ assertThat(client).isInstanceOf(CodeWhispererRuntimeClient::class.java)
+ assertThat(client.serviceClientConfiguration().region()).isEqualTo(Region.EU_CENTRAL_1)
+ assertThat(
+ client.serviceClientConfiguration().endpointOverride().get()
+ ).isEqualTo(URI.create(QEndpoints.getQEndpointWithRegion(Region.EU_CENTRAL_1.id())))
+
+ sut.switchProfile(
+ project,
+ QRegionProfile(arn = "arn:aws:codewhisperer:us-east-1:123456789012:profile/BAR_PROFILE", profileName = "BAR_PROFILE"),
+ QProfileSwitchIntent.User
+ )
+ assertThat(
+ sut.activeProfile(project)
+ ).isEqualTo(QRegionProfile(arn = "arn:aws:codewhisperer:us-east-1:123456789012:profile/BAR_PROFILE", profileName = "BAR_PROFILE"))
+ assertThat(sut.getQClientSettings(project).region.id).isEqualTo(Region.US_EAST_1.id())
+
+ val client2 = sut.getQClient(project)
+ assertThat(client2).isInstanceOf(CodeWhispererRuntimeClient::class.java)
+ assertThat(client2.serviceClientConfiguration().region()).isEqualTo(Region.US_EAST_1)
+ assertThat(
+ client2.serviceClientConfiguration().endpointOverride().get()
+ ).isEqualTo(URI.create(QEndpoints.getQEndpointWithRegion(Region.US_EAST_1.id())))
+ }
+
+ @Test
+ fun `deserialize empty data`() {
+ val element = xmlElement(
+ """
+
+
+ """
+ )
+ val actual = XmlSerializer.deserialize(element, QProfileState::class.java)
+ assertThat(actual.connectionIdToActiveProfile).hasSize(0)
+ assertThat(actual.connectionIdToProfileList).hasSize(0)
+ }
+
+ @Test
+ fun `serialize with data`() {
+ val element = xmlElement(
+ """
+
+
+ """.trimIndent()
+ )
+
+ val state = QProfileState().apply {
+ this.connectionIdToActiveProfile.putAll(
+ mapOf(
+ "conn-123" to QRegionProfile(
+ profileName = "myActiveProfile", arn = "arn:aws:codewhisperer:us-west-2:123456789012:profile/myActiveProfile"
+ )
+ )
+ )
+
+ connectionIdToProfileList.putAll(
+ mapOf("conn-123" to 2)
+ )
+ }
+
+ XmlSerializer.serializeInto(state, element)
+ val actualXmlString = XMLOutputter().outputString(element)
+ val expectedXmlString =
+ "\n" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ " " +
+ " " +
+ " " +
+ " " +
+ " " +
+ " " +
+ " " +
+ "" +
+ "" +
+ " " +
+ " " +
+ " " +
+ " "
+
+ assertThat(actualXmlString).isEqualTo(expectedXmlString)
+ }
+
+ @Test
+ fun `deserialize with data`() {
+ val element = xmlElement(
+ """
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """.trimIndent()
+ )
+
+ val actualState = XmlSerializer.deserialize(element, QProfileState::class.java)
+
+ assertThat(actualState.connectionIdToActiveProfile).hasSize(1)
+ val activeProfile = actualState.connectionIdToActiveProfile["conn-123"]
+ assertThat(activeProfile).isEqualTo(
+ QRegionProfile(
+ profileName = "myActiveProfile",
+ arn = "arn:aws:codewhisperer:us-west-2:123456789012:profile/myActiveProfile"
+ )
+ )
+
+ assertThat(actualState.connectionIdToProfileList).hasSize(1)
+ val profileList = actualState.connectionIdToProfileList["conn-123"]
+ assertThat(profileList).isEqualTo(2)
+ }
+
+ @Test
+ fun `getIdcConnectionOrNull handles NOT_AUTH and AUTHORIZED correctly`() {
+ val managerSpy = ToolkitConnectionManager.getInstance(project)
+ doReturn(BearerTokenAuthState.NOT_AUTHENTICATED).whenever(managerSpy)
+ .connectionStateForFeature(QConnection.getInstance())
+
+ // NOT AUTHORIZED
+ val notAuthConn = sut.getIdcConnectionOrNull(project)
+ assertThat(notAuthConn).isNull()
+
+ doReturn(BearerTokenAuthState.AUTHORIZED)
+ .whenever(managerSpy).connectionStateForFeature(QConnection.getInstance())
+
+ // AUTHORIZED
+ val normalConn = authRule.createConnection(
+ ManagedSsoProfile(ssoRegion = "us-east-1", startUrl = "", scopes = Q_SCOPES)
+ )
+ managerSpy.switchConnection(normalConn)
+
+ val normalConnectionResult = sut.getIdcConnectionOrNull(project)
+ assertThat(normalConnectionResult).isNotNull()
+ assertThat(normalConnectionResult).isEqualTo(normalConn)
+ }
+}
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tstFixtures/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTestUtil.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tstFixtures/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTestUtil.kt
index 330ce9a092e..1178a7ff967 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/tstFixtures/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTestUtil.kt
+++ b/plugins/amazonq/codewhisperer/jetbrains-community/tstFixtures/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTestUtil.kt
@@ -257,7 +257,9 @@ fun aRequestContext(
Random.nextLong(),
aString()
),
- customizationArn = null
+ customizationArn = null,
+ profileArn = null,
+ workspaceId = null,
)
}
diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/main.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/main.ts
index 1be8f151a72..1d033f1cc65 100644
--- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/main.ts
+++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/main.ts
@@ -42,6 +42,7 @@ export const createMynahUI = (
codeScanEnabled: boolean,
codeTestEnabled: boolean,
highlightCommand?: QuickActionCommand,
+ profileName?: string
) => {
let disclaimerCardActive = !disclaimerAcknowledged
@@ -88,7 +89,8 @@ export const createMynahUI = (
isDocEnabled,
isCodeScanEnabled,
isCodeTestEnabled,
- highlightCommand
+ highlightCommand,
+ profileName
})
// eslint-disable-next-line prefer-const
diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/tabs/generator.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/tabs/generator.ts
index 63682d596e7..f94eb193c98 100644
--- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/tabs/generator.ts
+++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/tabs/generator.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { ChatItemType, MynahUIDataModel, QuickActionCommandGroup, QuickActionCommand } from '@aws/mynah-ui-chat'
+import {ChatItemType, MynahUIDataModel, QuickActionCommandGroup, QuickActionCommand, ChatItem} from '@aws/mynah-ui-chat'
import { TabType } from '../storages/tabsStorage'
import { FollowUpGenerator } from '../followUps/generator'
import { QuickActionGenerator } from '../quickActions/generator'
@@ -16,12 +16,14 @@ export interface TabDataGeneratorProps {
isCodeScanEnabled: boolean
isCodeTestEnabled: boolean
highlightCommand?: QuickActionCommand
+ profileName?: string
}
export class TabDataGenerator {
private followUpsGenerator: FollowUpGenerator
public quickActionsGenerator: QuickActionGenerator
public highlightCommand?: QuickActionCommand
+ profileName?: string
private tabTitle: Map = new Map([
['unknown', 'Chat'],
@@ -91,6 +93,20 @@ What would you like to work on?`,
isCodeTestEnabled: props.isCodeTestEnabled,
})
this.highlightCommand = props.highlightCommand
+ this.profileName = props.profileName
+ }
+
+ private get regionProfileCard(): ChatItem | undefined {
+ console.log('[DEBUG] Received profileName:', this.profileName)
+ if (!this.profileName) {
+ return undefined
+ }
+ return {
+ type: ChatItemType.ANSWER,
+ body: `You are using the ${this.profileName} profile for this chat period`,
+ status: 'info',
+ messageId: 'regionProfile',
+ }
}
public getTabData(tabType: TabType, needWelcomeMessages: boolean, taskName?: string): MynahUIDataModel {
@@ -103,7 +119,8 @@ What would you like to work on?`,
contextCommands: this.getContextCommands(tabType),
chatItems: needWelcomeMessages
? [
- {
+ ...(this.regionProfileCard ? [this.regionProfileCard] : []),
+ {
type: ChatItemType.ANSWER,
body: this.tabWelcomeMessage.get(tabType),
},
@@ -112,7 +129,7 @@ What would you like to work on?`,
followUp: this.followUpsGenerator.generateWelcomeBlockForTab(tabType),
},
]
- : [],
+ : [...(this.regionProfileCard ? [this.regionProfileCard] : [])],
}
}
diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/walkthrough/welcome.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/walkthrough/welcome.ts
index e5fe67fdd7b..feb879d34f5 100644
--- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/walkthrough/welcome.ts
+++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/walkthrough/welcome.ts
@@ -14,6 +14,14 @@ export const welcomeScreenTabData = (tabs: TabDataGenerator): MynahUITabStoreTab
tabTitle: 'Welcome to Q',
tabBackground: true,
chatItems: [
+ ...(tabs.profileName
+ ? [{
+ type: ChatItemType.ANSWER,
+ icon: MynahIcons.INFO,
+ messageId: 'profile-info',
+ body: `You're using the ${tabs.profileName} profile for this chat period.`,
+ }]
+ : []),
{
type: ChatItemType.ANSWER,
icon: MynahIcons.ASTERISK,
diff --git a/plugins/amazonq/shared/jetbrains-community/resources/META-INF/module-amazonq.xml b/plugins/amazonq/shared/jetbrains-community/resources/META-INF/module-amazonq.xml
index 6336e3526d6..95bb5c886e2 100644
--- a/plugins/amazonq/shared/jetbrains-community/resources/META-INF/module-amazonq.xml
+++ b/plugins/amazonq/shared/jetbrains-community/resources/META-INF/module-amazonq.xml
@@ -9,4 +9,5 @@
+
diff --git a/plugins/amazonq/shared/jetbrains-community/resources/software/aws/toolkits/resources/AmazonQBundle.properties b/plugins/amazonq/shared/jetbrains-community/resources/software/aws/toolkits/resources/AmazonQBundle.properties
index fab9733aa8d..ab7face28d9 100644
--- a/plugins/amazonq/shared/jetbrains-community/resources/software/aws/toolkits/resources/AmazonQBundle.properties
+++ b/plugins/amazonq/shared/jetbrains-community/resources/software/aws/toolkits/resources/AmazonQBundle.properties
@@ -1,4 +1,3 @@
-action.q.hello.description=Hello description
amazonqInlineChat.hint.edit = Edit
amazonqInlineChat.popup.accept=Accept \u23CE
amazonqInlineChat.popup.cancel=Cancel \u238B
@@ -10,4 +9,14 @@ amazonqInlineChat.popup.title=Enter Instructions for Q
amazonq.refresh.panel=Refresh Chat Session
amazonq.title=Amazon Q
amazonq.workspace.settings.open.prompt=Workspace index is now enabled. You can disable it from Amazon Q settings.
-q.hello=Hello
+action.q.profile.usage.text=You changed your profile
+action.q.profile.usage=You''re using the ''{0} '' profile for Amazon Q.
+action.q.switchProfiles.text=Change Profile
+action.q.switchProfiles.text.action_required=Change Profile Select a profile to proceed
+action.q.switchProfiles.dialog.text=Amazon Q Developer Profile
+action.q.switchProfiles.dialog.account.label=Account: {0}
+action.q.switchProfiles.dialog.panel.text=Change your Q Developer profile
+action.q.switchProfiles.dialog.panel.description=Choose the profile that meets your current working needs.
+action.q.switchProfiles.dialog.panel.warning=When you change profiles, you will no longer have access to your current customizations, chats, code reviews, or any other code or content being generated by Amazon Q.
+general.ok=OK
+general.cancel=Cancel
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/CodeWhispererFeatureConfigService.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/CodeWhispererFeatureConfigService.kt
index 667e8e8f844..d77663430fb 100644
--- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/CodeWhispererFeatureConfigService.kt
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/CodeWhispererFeatureConfigService.kt
@@ -12,9 +12,9 @@ import software.amazon.awssdk.services.codewhispererruntime.model.FeatureValue
import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
-import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
import software.aws.toolkits.jetbrains.utils.isQExpired
@Service
@@ -32,8 +32,9 @@ class CodeWhispererFeatureConfigService {
LOG.debug { "Fetching feature configs" }
try {
- val response = connection.getConnectionSettings().awsClient().listFeatureEvaluations {
+ val response = QRegionProfileManager.getInstance().getQClient(project).listFeatureEvaluations {
it.userContext(codeWhispererUserContext())
+ it.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
} ?: return
// Simply force overwrite feature configs from server response, no needed to check existing values.
@@ -113,7 +114,8 @@ class CodeWhispererFeatureConfigService {
val availableCustomizations =
calculateIfIamIdentityCenterConnection(project) {
try {
- connection.getConnectionSettings().awsClient().listAvailableCustomizationsPaginator {}
+ QRegionProfileManager.getInstance().getQClient(project)
+ .listAvailableCustomizationsPaginator {}
.flatMap { resp ->
resp.customizations().map {
it.arn()
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/actions/QSwitchProfilesAction.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/actions/QSwitchProfilesAction.kt
new file mode 100644
index 00000000000..9e6b40fd677
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/actions/QSwitchProfilesAction.kt
@@ -0,0 +1,36 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.actions
+
+import com.intellij.icons.AllIcons
+import com.intellij.openapi.actionSystem.ActionUpdateThread
+import com.intellij.openapi.actionSystem.AnAction
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.project.DumbAware
+import com.intellij.ui.JBColor
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileDialog
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
+import software.aws.toolkits.jetbrains.services.codewhisperer.util.getHexString
+import software.aws.toolkits.resources.AmazonQBundle.message
+
+class QSwitchProfilesAction : AnAction(message("action.q.switchProfiles.text")), DumbAware {
+
+ override fun getActionUpdateThread() = ActionUpdateThread.BGT
+
+ override fun update(e: AnActionEvent) {
+ e.presentation.icon = AllIcons.Actions.SwapPanels
+ val project = e.project ?: return
+ if (QRegionProfileManager.getInstance().isPendingProfileSelection(project)) {
+ e.presentation.text = message("action.q.switchProfiles.text.action_required", JBColor.GRAY.getHexString())
+ }
+ }
+
+ override fun actionPerformed(e: AnActionEvent) {
+ val project = e.project ?: return
+ QRegionProfileDialog(
+ project,
+ selectedProfile = QRegionProfileManager.getInstance().activeProfile(project)
+ ).show()
+ }
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/clients/AmazonQStreamingClient.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/clients/AmazonQStreamingClient.kt
index 43c6e85c8b1..f4f31f715c6 100644
--- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/clients/AmazonQStreamingClient.kt
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/clients/AmazonQStreamingClient.kt
@@ -16,20 +16,15 @@ import software.amazon.awssdk.services.codewhispererstreaming.model.ThrottlingEx
import software.amazon.awssdk.services.codewhispererstreaming.model.ValidationException
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
-import software.aws.toolkits.jetbrains.core.awsClient
-import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
-import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
import software.aws.toolkits.jetbrains.services.amazonq.RetryableOperation
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
import java.time.Instant
import java.util.concurrent.TimeoutException
import java.util.concurrent.atomic.AtomicReference
@Service(Service.Level.PROJECT)
class AmazonQStreamingClient(private val project: Project) {
- private fun connection() = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance())
- ?: error("Attempted to use connection while one does not exist")
-
- private fun streamingBearerClient() = connection().getConnectionSettings().awsClient()
+ private fun streamingBearerClient() = QRegionProfileManager.getInstance().getQClient(project)
suspend fun exportResultArchive(
exportId: String,
@@ -66,6 +61,7 @@ class AmazonQStreamingClient(private val project: Project) {
it.exportId(exportId)
it.exportIntent(exportIntent)
it.exportContext(exportContext)
+ it.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
},
ExportResultArchiveResponseHandler.builder().subscriber(
ExportResultArchiveResponseHandler.Visitor.builder()
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClient.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClient.kt
new file mode 100644
index 00000000000..8932881568f
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClient.kt
@@ -0,0 +1,18 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp
+
+import org.eclipse.lsp4j.jsonrpc.services.JsonRequest
+import org.eclipse.lsp4j.services.LanguageClient
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.ConnectionMetadata
+import java.util.concurrent.CompletableFuture
+
+/**
+ * Requests sent by server to client
+ */
+@Suppress("unused")
+interface AmazonQLanguageClient : LanguageClient {
+ @JsonRequest("aws/credentials/getConnectionMetadata")
+ fun getConnectionMetadata(): CompletableFuture
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImpl.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImpl.kt
new file mode 100644
index 00000000000..50b1be3626d
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImpl.kt
@@ -0,0 +1,96 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp
+
+import com.intellij.notification.NotificationType
+import com.intellij.openapi.project.Project
+import org.eclipse.lsp4j.ConfigurationParams
+import org.eclipse.lsp4j.MessageActionItem
+import org.eclipse.lsp4j.MessageParams
+import org.eclipse.lsp4j.MessageType
+import org.eclipse.lsp4j.PublishDiagnosticsParams
+import org.eclipse.lsp4j.ShowMessageRequestParams
+import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection
+import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
+import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.ConnectionMetadata
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.SsoProfileData
+import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings
+import java.util.concurrent.CompletableFuture
+
+/**
+ * Concrete implementation of [AmazonQLanguageClient] to handle messages sent from server
+ */
+class AmazonQLanguageClientImpl(private val project: Project) : AmazonQLanguageClient {
+ override fun telemetryEvent(`object`: Any) {
+ println(`object`)
+ }
+
+ override fun publishDiagnostics(diagnostics: PublishDiagnosticsParams) {
+ println(diagnostics)
+ }
+
+ override fun showMessage(messageParams: MessageParams) {
+ val type = when (messageParams.type) {
+ MessageType.Error -> NotificationType.ERROR
+ MessageType.Warning -> NotificationType.WARNING
+ MessageType.Info, MessageType.Log -> NotificationType.INFORMATION
+ }
+ println("$type: ${messageParams.message}")
+ }
+
+ override fun showMessageRequest(requestParams: ShowMessageRequestParams): CompletableFuture? {
+ println(requestParams)
+
+ return CompletableFuture.completedFuture(null)
+ }
+
+ override fun logMessage(message: MessageParams) {
+ showMessage(message)
+ }
+
+ override fun getConnectionMetadata(): CompletableFuture =
+ CompletableFuture.supplyAsync {
+ val connection = ToolkitConnectionManager.getInstance(project)
+ .activeConnectionForFeature(QConnection.getInstance())
+
+ when (connection) {
+ is AwsBearerTokenConnection -> {
+ ConnectionMetadata(
+ SsoProfileData(connection.startUrl)
+ )
+ }
+ else -> {
+ // If no connection or not a bearer token connection return default builderID start url
+ ConnectionMetadata(
+ SsoProfileData(AmazonQLspConstants.AWS_BUILDER_ID_URL)
+ )
+ }
+ }
+ }
+
+ override fun configuration(params: ConfigurationParams): CompletableFuture> {
+ if (params.items.isEmpty()) {
+ return CompletableFuture.completedFuture(null)
+ }
+
+ return CompletableFuture.completedFuture(
+ buildList {
+ params.items.forEach {
+ when (it.section) {
+ AmazonQLspConstants.LSP_CW_CONFIGURATION_KEY -> {
+ add(
+ CodeWhispererLspConfiguration(
+ shouldShareData = CodeWhispererSettings.getInstance().isMetricOptIn(),
+ shouldShareCodeReferences = CodeWhispererSettings.getInstance().isIncludeCodeWithReference(),
+ shouldEnableWorkspaceContext = CodeWhispererSettings.getInstance().isWorkspaceContextEnabled()
+ )
+ )
+ }
+ }
+ }
+ }
+ )
+ }
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageServer.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageServer.kt
new file mode 100644
index 00000000000..2396e273f18
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageServer.kt
@@ -0,0 +1,36 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp
+
+import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage
+import org.eclipse.lsp4j.jsonrpc.services.JsonNotification
+import org.eclipse.lsp4j.jsonrpc.services.JsonRequest
+import org.eclipse.lsp4j.services.LanguageServer
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.GetConfigurationFromServerParams
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.LspServerConfigurations
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.UpdateConfigurationParams
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.UpdateCredentialsPayload
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.dependencies.DidChangeDependencyPathsParams
+import java.util.concurrent.CompletableFuture
+
+/**
+ * Remote interface exposed by the Amazon Q language server
+ */
+@Suppress("unused")
+interface AmazonQLanguageServer : LanguageServer {
+ @JsonNotification("aws/didChangeDependencyPaths")
+ fun didChangeDependencyPaths(params: DidChangeDependencyPathsParams): CompletableFuture
+
+ @JsonRequest("aws/credentials/token/update")
+ fun updateTokenCredentials(payload: UpdateCredentialsPayload): CompletableFuture
+
+ @JsonNotification("aws/credentials/token/delete")
+ fun deleteTokenCredentials(): CompletableFuture
+
+ @JsonRequest("aws/getConfigurationFromServer")
+ fun getConfigurationFromServer(params: GetConfigurationFromServerParams): CompletableFuture
+
+ @JsonRequest("aws/updateConfiguration")
+ fun updateConfiguration(params: UpdateConfigurationParams): CompletableFuture
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspConstants.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspConstants.kt
new file mode 100644
index 00000000000..ca8fffcbb51
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspConstants.kt
@@ -0,0 +1,12 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp
+
+object AmazonQLspConstants {
+ const val AWS_BUILDER_ID_URL = "https://view.awsapps.com/start"
+ const val LSP_CW_CONFIGURATION_KEY = "aws.codeWhisperer"
+ const val LSP_CW_OPT_OUT_KEY = "shareCodeWhispererContentWithAWS"
+ const val LSP_CODE_REFERENCES_OPT_OUT_KEY = "includeSuggestionsWithCodeReferences"
+ const val LSP_WORKSPACE_CONTEXT_ENABLED_KEY = "workspaceContext"
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt
new file mode 100644
index 00000000000..59658c3a878
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt
@@ -0,0 +1,352 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp
+
+import com.google.gson.ToNumberPolicy
+import com.intellij.execution.configurations.GeneralCommandLine
+import com.intellij.execution.impl.ExecutionManagerImpl
+import com.intellij.execution.process.KillableColoredProcessHandler
+import com.intellij.execution.process.KillableProcessHandler
+import com.intellij.execution.process.ProcessEvent
+import com.intellij.execution.process.ProcessListener
+import com.intellij.execution.process.ProcessOutputType
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.components.Service
+import com.intellij.openapi.components.service
+import com.intellij.openapi.components.serviceIfCreated
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.util.Disposer
+import com.intellij.openapi.util.Key
+import com.intellij.openapi.util.SystemInfo
+import com.intellij.util.io.await
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.async
+import kotlinx.coroutines.future.asCompletableFuture
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withTimeout
+import org.eclipse.lsp4j.ClientCapabilities
+import org.eclipse.lsp4j.ClientInfo
+import org.eclipse.lsp4j.DidChangeConfigurationParams
+import org.eclipse.lsp4j.FileOperationsWorkspaceCapabilities
+import org.eclipse.lsp4j.InitializeParams
+import org.eclipse.lsp4j.InitializeResult
+import org.eclipse.lsp4j.InitializedParams
+import org.eclipse.lsp4j.SynchronizationCapabilities
+import org.eclipse.lsp4j.TextDocumentClientCapabilities
+import org.eclipse.lsp4j.WorkspaceClientCapabilities
+import org.eclipse.lsp4j.jsonrpc.Launcher
+import org.eclipse.lsp4j.launch.LSPLauncher
+import org.slf4j.event.Level
+import software.aws.toolkits.core.utils.getLogger
+import software.aws.toolkits.core.utils.info
+import software.aws.toolkits.core.utils.warn
+import software.aws.toolkits.jetbrains.isDeveloperMode
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.ArtifactManager
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.auth.DefaultAuthCredentialsService
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.dependencies.DefaultModuleDependenciesService
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.encryption.JwtEncryptionManager
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.createExtendedClientMetadata
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.textdocument.TextDocumentServiceHandler
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.WorkspaceFolderUtil.createWorkspaceFolders
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.workspace.WorkspaceServiceHandler
+import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata
+import software.aws.toolkits.jetbrains.settings.LspSettings
+import java.io.IOException
+import java.io.OutputStreamWriter
+import java.io.PipedInputStream
+import java.io.PipedOutputStream
+import java.io.PrintWriter
+import java.io.StringWriter
+import java.nio.charset.StandardCharsets
+import java.util.concurrent.Future
+import kotlin.time.Duration.Companion.seconds
+
+// https://github.com/redhat-developer/lsp4ij/blob/main/src/main/java/com/redhat/devtools/lsp4ij/server/LSPProcessListener.java
+// JB impl and redhat both use a wrapper to handle input buffering issue
+internal class LSPProcessListener : ProcessListener {
+ private val outputStream = PipedOutputStream()
+ private val outputStreamWriter = OutputStreamWriter(outputStream, StandardCharsets.UTF_8)
+ val inputStream = PipedInputStream(outputStream)
+
+ override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) {
+ if (ProcessOutputType.isStdout(outputType)) {
+ try {
+ this.outputStreamWriter.write(event.text)
+ this.outputStreamWriter.flush()
+ } catch (_: IOException) {
+ ExecutionManagerImpl.stopProcess(event.processHandler)
+ }
+ } else if (ProcessOutputType.isStderr(outputType)) {
+ LOG.warn { "LSP process stderr: ${event.text}" }
+ }
+ }
+
+ override fun processTerminated(event: ProcessEvent) {
+ try {
+ this.outputStreamWriter.close()
+ this.outputStream.close()
+ } catch (_: IOException) {
+ }
+ }
+
+ companion object {
+ private val LOG = getLogger()
+ }
+}
+
+@Service(Service.Level.PROJECT)
+class AmazonQLspService(private val project: Project, private val cs: CoroutineScope) : Disposable {
+ private var instance: Deferred
+ val capabilities
+ get() = instance.getCompleted().initializeResult.getCompleted().capabilities
+
+ // dont allow lsp commands if server is restarting
+ private val mutex = Mutex(false)
+
+ private fun start() = cs.async {
+ // manage lifecycle RAII-like so we can restart at arbitrary time
+ // and suppress IDE error if server fails to start
+ var attempts = 0
+ while (attempts < 3) {
+ try {
+ return@async withTimeout(30.seconds) {
+ val instance = AmazonQServerInstance(project, cs).also {
+ Disposer.register(this@AmazonQLspService, it)
+ }
+ // wait for handshake to complete
+ instance.initializeResult.join()
+
+ instance
+ }
+ } catch (e: Exception) {
+ LOG.warn(e) { "Failed to start LSP server" }
+ }
+ attempts++
+ }
+
+ error("Failed to start LSP server in 3 attempts")
+ }
+
+ init {
+ instance = start()
+ }
+
+ override fun dispose() {
+ }
+
+ suspend fun restart() = mutex.withLock {
+ // stop if running
+ instance.let {
+ if (it.isActive) {
+ // not even running yet
+ return
+ }
+
+ try {
+ val i = it.await()
+ if (i.initializeResult.isActive) {
+ // not initialized
+ return
+ }
+
+ Disposer.dispose(i)
+ } catch (e: Exception) {
+ LOG.info(e) { "Exception while disposing LSP server" }
+ }
+ }
+
+ instance = start()
+ }
+
+ suspend fun execute(runnable: suspend AmazonQLspService.(AmazonQLanguageServer) -> T): T {
+ val lsp = withTimeout(10.seconds) {
+ val holder = mutex.withLock { instance }.await()
+ holder.initializeResult.join()
+
+ holder.languageServer
+ }
+ return runnable(lsp)
+ }
+
+ fun executeSync(runnable: suspend AmazonQLspService.(AmazonQLanguageServer) -> T): T =
+ runBlocking(cs.coroutineContext) {
+ execute(runnable)
+ }
+
+ companion object {
+ private val LOG = getLogger()
+ fun getInstance(project: Project) = project.service()
+
+ fun executeIfRunning(project: Project, runnable: AmazonQLspService.(AmazonQLanguageServer) -> T): T? =
+ project.serviceIfCreated()?.executeSync(runnable)
+
+ fun didChangeConfiguration(project: Project) {
+ executeIfRunning(project) {
+ it.workspaceService.didChangeConfiguration(DidChangeConfigurationParams())
+ }
+ }
+ }
+}
+
+private class AmazonQServerInstance(private val project: Project, private val cs: CoroutineScope) : Disposable {
+ private val encryptionManager = JwtEncryptionManager()
+
+ private val launcher: Launcher
+
+ val languageServer: AmazonQLanguageServer
+ get() = launcher.remoteProxy
+
+ @Suppress("ForbiddenVoid")
+ private val launcherFuture: Future
+ private val launcherHandler: KillableProcessHandler
+ val initializeResult: Deferred
+
+ private fun createClientCapabilities(): ClientCapabilities =
+ ClientCapabilities().apply {
+ textDocument = TextDocumentClientCapabilities().apply {
+ // For didSaveTextDocument, other textDocument/ messages always mandatory
+ synchronization = SynchronizationCapabilities().apply {
+ didSave = true
+ }
+ }
+
+ workspace = WorkspaceClientCapabilities().apply {
+ applyEdit = false
+
+ // For workspace folder changes
+ workspaceFolders = true
+
+ // For file operations (create, delete)
+ fileOperations = FileOperationsWorkspaceCapabilities().apply {
+ didCreate = true
+ didDelete = true
+ didRename = true
+ }
+ }
+ }
+
+ private fun createClientInfo(): ClientInfo {
+ val metadata = ClientMetadata.getDefault()
+ return ClientInfo().apply {
+ name = metadata.awsProduct.toString()
+ version = metadata.awsVersion
+ }
+ }
+
+ private fun createInitializeParams(): InitializeParams =
+ InitializeParams().apply {
+ processId = ProcessHandle.current().pid().toInt()
+ capabilities = createClientCapabilities()
+ clientInfo = createClientInfo()
+ workspaceFolders = createWorkspaceFolders(project)
+ initializationOptions = createExtendedClientMetadata()
+ }
+
+ init {
+ // will cause slow service init, but maybe fine for now. will not block UI since fetch/extract will be under background progress
+ val artifact = runBlocking { ArtifactManager(project, manifestRange = null).fetchArtifact() }.toAbsolutePath()
+ val node = if (SystemInfo.isWindows) "node.exe" else "node"
+ val cmd = GeneralCommandLine(
+ artifact.resolve(node).toString(),
+ LspSettings.getInstance().getArtifactPath() ?: artifact.resolve("aws-lsp-codewhisperer.js").toString(),
+ "--stdio",
+ "--set-credentials-encryption-key",
+ )
+
+ launcherHandler = KillableColoredProcessHandler.Silent(cmd)
+ val inputWrapper = LSPProcessListener()
+ launcherHandler.addProcessListener(inputWrapper)
+ launcherHandler.startNotify()
+
+ launcher = LSPLauncher.Builder()
+ .setLocalService(AmazonQLanguageClientImpl(project))
+ .setRemoteInterface(AmazonQLanguageServer::class.java)
+ .configureGson {
+ // TODO: maybe need adapter for initialize:
+ // https://github.com/aws/amazon-q-eclipse/blob/b9d5bdcd5c38e1dd8ad371d37ab93a16113d7d4b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/QLspTypeAdapterFactory.java
+
+ // otherwise Gson treats all numbers as double which causes deser issues
+ it.setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
+ }.traceMessages(
+ PrintWriter(
+ object : StringWriter() {
+ private val traceLogger = LOG.atLevel(if (isDeveloperMode()) Level.INFO else Level.DEBUG)
+
+ override fun flush() {
+ traceLogger.log { buffer.toString() }
+ buffer.setLength(0)
+ }
+ }
+ )
+ )
+ .setInput(inputWrapper.inputStream)
+ .setOutput(launcherHandler.process.outputStream)
+ .create()
+
+ launcherFuture = launcher.startListening()
+
+ initializeResult = cs.async {
+ // encryption info must be sent within 5s or Flare process will exit
+ encryptionManager.writeInitializationPayload(launcherHandler.process.outputStream)
+
+ val initializeResult = try {
+ withTimeout(5.seconds) {
+ languageServer.initialize(createInitializeParams()).await()
+ }
+ } catch (_: TimeoutCancellationException) {
+ LOG.warn { "LSP initialization timed out" }
+ null
+ } catch (e: Exception) {
+ LOG.warn(e) { "LSP initialization failed" }
+ null
+ }
+
+ // then if this succeeds then we can allow the client to send requests
+ if (initializeResult == null) {
+ launcherHandler.destroyProcess()
+ error("LSP initialization failed")
+ }
+ languageServer.initialized(InitializedParams())
+
+ initializeResult
+ }
+
+ // invokeOnCompletion results in weird lock/timeout error
+ initializeResult.asCompletableFuture().handleAsync { r, ex ->
+ if (ex != null) {
+ return@handleAsync
+ }
+
+ this@AmazonQServerInstance.apply {
+ DefaultAuthCredentialsService(project, encryptionManager, this)
+ TextDocumentServiceHandler(project, this)
+ WorkspaceServiceHandler(project, this)
+ DefaultModuleDependenciesService(project, this)
+ }
+ }
+ }
+
+ override fun dispose() {
+ if (!launcherFuture.isDone) {
+ try {
+ languageServer.apply {
+ shutdown().thenRun { exit() }
+ }
+ } catch (e: Exception) {
+ LOG.warn(e) { "LSP shutdown failed" }
+ launcherHandler.destroyProcess()
+ }
+ } else if (!launcherHandler.isProcessTerminated) {
+ launcherHandler.destroyProcess()
+ }
+ }
+
+ companion object {
+ private val LOG = getLogger()
+ }
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/CodeWhispererLspConfiguration.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/CodeWhispererLspConfiguration.kt
new file mode 100644
index 00000000000..d54acf55fbe
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/CodeWhispererLspConfiguration.kt
@@ -0,0 +1,17 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp
+
+import com.google.gson.annotations.SerializedName
+
+data class CodeWhispererLspConfiguration(
+ @SerializedName(AmazonQLspConstants.LSP_CW_OPT_OUT_KEY)
+ val shouldShareData: Boolean? = null,
+
+ @SerializedName(AmazonQLspConstants.LSP_WORKSPACE_CONTEXT_ENABLED_KEY)
+ val shouldEnableWorkspaceContext: Boolean? = null,
+
+ @SerializedName(AmazonQLspConstants.LSP_CODE_REFERENCES_OPT_OUT_KEY)
+ val shouldShareCodeReferences: Boolean? = null,
+)
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelper.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelper.kt
new file mode 100644
index 00000000000..8787259bf08
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelper.kt
@@ -0,0 +1,215 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts
+
+import com.intellij.openapi.project.Project
+import com.intellij.platform.ide.progress.withBackgroundProgress
+import com.intellij.util.io.createDirectories
+import com.intellij.util.text.SemVer
+import kotlinx.coroutines.CancellationException
+import org.jetbrains.annotations.VisibleForTesting
+import software.aws.toolkits.core.utils.deleteIfExists
+import software.aws.toolkits.core.utils.error
+import software.aws.toolkits.core.utils.exists
+import software.aws.toolkits.core.utils.getLogger
+import software.aws.toolkits.core.utils.info
+import software.aws.toolkits.core.utils.warn
+import software.aws.toolkits.jetbrains.core.saveFileFromUrl
+import software.aws.toolkits.jetbrains.services.amazonq.project.manifest.ManifestManager
+import software.aws.toolkits.resources.AwsCoreBundle
+import java.nio.file.Path
+import java.util.concurrent.atomic.AtomicInteger
+
+class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH, private val maxDownloadAttempts: Int = MAX_DOWNLOAD_ATTEMPTS) {
+
+ companion object {
+ private val DEFAULT_ARTIFACT_PATH = getToolkitsCommonCacheRoot().resolve("aws").resolve("toolkits").resolve("language-servers")
+ private val logger = getLogger()
+ private const val MAX_DOWNLOAD_ATTEMPTS = 3
+ }
+ private val currentAttempt = AtomicInteger(0)
+
+ fun removeDelistedVersions(delistedVersions: List) {
+ val localFolders = getSubFolders(lspArtifactsPath)
+
+ delistedVersions.forEach { delistedVersion ->
+ val versionToDelete = delistedVersion.serverVersion ?: return@forEach
+
+ localFolders
+ .filter { folder -> folder.fileName.toString() == versionToDelete }
+ .forEach { folder ->
+ try {
+ folder.toFile().deleteRecursively()
+ logger.info { "Successfully deleted deListed version: ${folder.fileName}" }
+ } catch (e: Exception) {
+ logger.error(e) { "Failed to delete deListed version ${folder.fileName}: ${e.message}" }
+ }
+ }
+ }
+ }
+
+ fun deleteOlderLspArtifacts(manifestVersionRanges: ArtifactManager.SupportedManifestVersionRange) {
+ val validVersions = getAllLocalLspArtifactsWithinManifestRange(manifestVersionRanges)
+
+ // Keep the latest 2 versions, delete others
+ validVersions.drop(2).forEach { (folder, _) ->
+ try {
+ folder.toFile().deleteRecursively()
+ logger.info { "Deleted older LSP artifact: ${folder.fileName}" }
+ } catch (e: Exception) {
+ logger.error(e) { "Failed to delete older LSP artifact: ${folder.fileName}" }
+ }
+ }
+ }
+
+ fun getAllLocalLspArtifactsWithinManifestRange(manifestVersionRanges: ArtifactManager.SupportedManifestVersionRange): List> {
+ val localFolders = getSubFolders(lspArtifactsPath)
+
+ return localFolders
+ .mapNotNull { localFolder ->
+ SemVer.parseFromText(localFolder.fileName.toString())?.let { semVer ->
+ if (semVer in manifestVersionRanges.startVersion..manifestVersionRanges.endVersion) {
+ localFolder to semVer
+ } else {
+ null
+ }
+ }
+ }
+ .sortedByDescending { (_, semVer) -> semVer }
+ }
+
+ fun getExistingLspArtifacts(versions: List, target: ManifestManager.VersionTarget?): Boolean {
+ if (versions.isEmpty() || target?.contents == null) return false
+
+ val localLSPPath = lspArtifactsPath.resolve(versions.first().serverVersion.toString())
+ if (!localLSPPath.exists()) return false
+
+ val hasInvalidFiles = target.contents.any { content ->
+ content.filename?.let { filename ->
+ val filePath = localLSPPath.resolve(filename)
+ !filePath.exists() || !validateFileHash(filePath, content.hashes?.firstOrNull())
+ } ?: false
+ }
+
+ if (hasInvalidFiles) {
+ try {
+ localLSPPath.toFile().deleteRecursively()
+ logger.info { "Deleted mismatched LSP artifacts at: $localLSPPath" }
+ } catch (e: Exception) {
+ logger.error(e) { "Failed to delete mismatched LSP artifacts at: $localLSPPath" }
+ }
+ }
+ return !hasInvalidFiles
+ }
+
+ suspend fun tryDownloadLspArtifacts(project: Project, versions: List, target: ManifestManager.VersionTarget?): Path? {
+ val temporaryDownloadPath = lspArtifactsPath.resolve("temp")
+ val downloadPath = lspArtifactsPath.resolve(versions.first().serverVersion.toString())
+
+ while (currentAttempt.get() < maxDownloadAttempts) {
+ currentAttempt.incrementAndGet()
+ logger.info { "Attempt ${currentAttempt.get()} of $maxDownloadAttempts to download LSP artifacts" }
+
+ try {
+ return withBackgroundProgress(
+ project,
+ AwsCoreBundle.message("amazonqFeatureDev.placeholder.downloading_and_extracting_lsp_artifacts"),
+ cancellable = true
+ ) {
+ if (downloadLspArtifacts(temporaryDownloadPath, target) && target != null && !target.contents.isNullOrEmpty()) {
+ moveFilesFromSourceToDestination(temporaryDownloadPath, downloadPath)
+ target.contents
+ .mapNotNull { it.filename }
+ .forEach { filename -> extractZipFile(downloadPath.resolve(filename), downloadPath) }
+ logger.info { "Successfully downloaded and moved LSP artifacts to $downloadPath" }
+
+ return@withBackgroundProgress downloadPath
+ }
+
+ return@withBackgroundProgress null
+ }
+ } catch (e: Exception) {
+ when (e) {
+ is CancellationException -> {
+ logger.error(e) { "User cancelled download and extracting of LSP artifacts.." }
+ currentAttempt.set(maxDownloadAttempts) // To exit the while loop.
+ }
+ else -> { logger.error(e) { "Failed to download/move LSP artifacts on attempt ${currentAttempt.get()}" } }
+ }
+ temporaryDownloadPath.toFile().deleteRecursively()
+ downloadPath.toFile().deleteRecursively()
+ }
+ }
+ logger.error { "Failed to download LSP artifacts after $maxDownloadAttempts attempts" }
+ return null
+ }
+
+ @VisibleForTesting
+ internal fun downloadLspArtifacts(downloadPath: Path, target: ManifestManager.VersionTarget?): Boolean {
+ if (target == null || target.contents.isNullOrEmpty()) {
+ logger.warn { "No target contents available for download" }
+ return false
+ }
+ try {
+ downloadPath.createDirectories()
+ target.contents.forEach { content ->
+ if (content.url == null || content.filename == null) {
+ logger.warn { "Missing URL or filename in content" }
+ return@forEach
+ }
+ val filePath = downloadPath.resolve(content.filename)
+ val contentHash = content.hashes?.firstOrNull() ?: run {
+ logger.warn { "No hash available for ${content.filename}" }
+ return@forEach
+ }
+ downloadAndValidateFile(content.url, filePath, contentHash)
+ }
+ validateDownloadedFiles(downloadPath, target.contents)
+ } catch (e: Exception) {
+ logger.error(e) { "Failed to download LSP artifacts: ${e.message}" }
+ downloadPath.toFile().deleteRecursively()
+ return false
+ }
+ return true
+ }
+
+ private fun downloadAndValidateFile(url: String, filePath: Path, expectedHash: String) {
+ try {
+ if (!filePath.exists()) {
+ logger.info { "Downloading file: ${filePath.fileName}" }
+ saveFileFromUrl(url, filePath)
+ }
+ if (!validateFileHash(filePath, expectedHash)) {
+ logger.warn { "Hash mismatch for ${filePath.fileName}, re-downloading" }
+ filePath.deleteIfExists()
+ saveFileFromUrl(url, filePath)
+ if (!validateFileHash(filePath, expectedHash)) {
+ throw LspException("Hash mismatch after re-download for ${filePath.fileName}", LspException.ErrorCode.HASH_MISMATCH)
+ }
+ }
+ } catch (e: Exception) {
+ throw IllegalStateException("Failed to download/validate file: ${filePath.fileName}", e)
+ }
+ }
+
+ @VisibleForTesting
+ internal fun validateFileHash(filePath: Path, expectedHash: String?): Boolean {
+ if (expectedHash == null) return false
+ val contentHash = generateSHA384Hash(filePath)
+ return "sha384:$contentHash" == expectedHash
+ }
+
+ private fun validateDownloadedFiles(downloadPath: Path, contents: List) {
+ val missingFiles = contents
+ .mapNotNull { it.filename }
+ .filter { filename ->
+ !downloadPath.resolve(filename).exists()
+ }
+ if (missingFiles.isNotEmpty()) {
+ val errorMessage = "Missing required files: ${missingFiles.joinToString(", ")}"
+ logger.error { errorMessage }
+ throw LspException(errorMessage, LspException.ErrorCode.DOWNLOAD_FAILED)
+ }
+ }
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactManager.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactManager.kt
new file mode 100644
index 00000000000..b74bbde2886
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactManager.kt
@@ -0,0 +1,108 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts
+
+import com.intellij.openapi.project.Project
+import com.intellij.util.text.SemVer
+import org.jetbrains.annotations.VisibleForTesting
+import software.aws.toolkits.core.utils.error
+import software.aws.toolkits.core.utils.getLogger
+import software.aws.toolkits.core.utils.info
+import software.aws.toolkits.jetbrains.services.amazonq.project.manifest.ManifestManager
+import java.nio.file.Path
+
+class ArtifactManager(
+ private val project: Project,
+ private val manifestFetcher: ManifestFetcher = ManifestFetcher(),
+ private val artifactHelper: ArtifactHelper = ArtifactHelper(),
+ manifestRange: SupportedManifestVersionRange?,
+) {
+
+ data class SupportedManifestVersionRange(
+ val startVersion: SemVer,
+ val endVersion: SemVer,
+ )
+ data class LSPVersions(
+ val deListedVersions: List,
+ val inRangeVersions: List,
+ )
+
+ private val manifestVersionRanges: SupportedManifestVersionRange = manifestRange ?: DEFAULT_VERSION_RANGE
+
+ companion object {
+ private val DEFAULT_VERSION_RANGE = SupportedManifestVersionRange(
+ startVersion = SemVer("1.0.0", 1, 0, 0),
+ endVersion = SemVer("2.0.0", 2, 0, 0)
+ )
+ private val logger = getLogger()
+ }
+
+ suspend fun fetchArtifact(): Path {
+ val manifest = manifestFetcher.fetch() ?: throw LspException(
+ "Language Support is not available, as manifest is missing.",
+ LspException.ErrorCode.MANIFEST_FETCH_FAILED
+ )
+ val lspVersions = getLSPVersionsFromManifestWithSpecifiedRange(manifest)
+
+ this.artifactHelper.removeDelistedVersions(lspVersions.deListedVersions)
+
+ if (lspVersions.inRangeVersions.isEmpty()) {
+ // No versions are found which are in the given range. Fallback to local lsp artifacts.
+ val localLspArtifacts = this.artifactHelper.getAllLocalLspArtifactsWithinManifestRange(manifestVersionRanges)
+ if (localLspArtifacts.isNotEmpty()) {
+ return localLspArtifacts.first().first
+ }
+ throw LspException("Language server versions not found in manifest.", LspException.ErrorCode.NO_COMPATIBLE_LSP_VERSION)
+ }
+
+ // If there is an LSP Manifest with the same version
+ val target = getTargetFromLspManifest(lspVersions.inRangeVersions)
+ // Get Local LSP files and check if we can re-use existing LSP Artifacts
+ val artifactPath: Path = if (this.artifactHelper.getExistingLspArtifacts(lspVersions.inRangeVersions, target)) {
+ this.artifactHelper.getAllLocalLspArtifactsWithinManifestRange(manifestVersionRanges).first().first
+ } else {
+ this.artifactHelper.tryDownloadLspArtifacts(project, lspVersions.inRangeVersions, target)
+ ?: throw LspException("Failed to download LSP artifacts", LspException.ErrorCode.DOWNLOAD_FAILED)
+ }
+ this.artifactHelper.deleteOlderLspArtifacts(manifestVersionRanges)
+ return artifactPath
+ }
+
+ @VisibleForTesting
+ internal fun getLSPVersionsFromManifestWithSpecifiedRange(manifest: ManifestManager.Manifest): LSPVersions {
+ if (manifest.versions.isNullOrEmpty()) return LSPVersions(emptyList(), emptyList())
+
+ val (deListed, inRange) = manifest.versions.mapNotNull { version ->
+ version.serverVersion?.let { serverVersion ->
+ SemVer.parseFromText(serverVersion)?.let { semVer ->
+ when {
+ version.isDelisted != false -> Pair(version, true) // Is deListed
+ semVer in manifestVersionRanges.startVersion..manifestVersionRanges.endVersion -> Pair(version, false) // Is in range
+ else -> null
+ }
+ }
+ }
+ }.partition { it.second }
+
+ return LSPVersions(
+ deListedVersions = deListed.map { it.first },
+ inRangeVersions = inRange.map { it.first }.sortedByDescending { (_, semVer) -> semVer }
+ )
+ }
+
+ private fun getTargetFromLspManifest(versions: List): ManifestManager.VersionTarget {
+ val currentOS = getCurrentOS()
+ val currentArchitecture = getCurrentArchitecture()
+
+ val currentTarget = versions.first().targets?.find { target ->
+ target.platform == currentOS && target.arch == currentArchitecture
+ }
+ if (currentTarget == null) {
+ logger.error { "Failed to obtain target for $currentOS and $currentArchitecture" }
+ throw LspException("Target not found in the current Version: ${versions.first().serverVersion}", LspException.ErrorCode.TARGET_NOT_FOUND)
+ }
+ logger.info { "Target found in the current Version: ${versions.first().serverVersion}" }
+ return currentTarget
+ }
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/LspException.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/LspException.kt
new file mode 100644
index 00000000000..110acd14b5d
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/LspException.kt
@@ -0,0 +1,21 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts
+
+class LspException(message: String, private val errorCode: ErrorCode, cause: Throwable? = null) : Exception(message, cause) {
+
+ enum class ErrorCode {
+ MANIFEST_FETCH_FAILED,
+ DOWNLOAD_FAILED,
+ HASH_MISMATCH,
+ TARGET_NOT_FOUND,
+ NO_COMPATIBLE_LSP_VERSION,
+ UNZIP_FAILED,
+ }
+
+ override fun toString(): String = buildString {
+ append("LSP Error [$errorCode]: $message")
+ cause?.let { append(", Cause: ${it.message}") }
+ }
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/LspUtils.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/LspUtils.kt
new file mode 100644
index 00000000000..7724a8c2255
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/LspUtils.kt
@@ -0,0 +1,101 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts
+
+import com.intellij.openapi.util.SystemInfo
+import com.intellij.openapi.util.text.StringUtil
+import com.intellij.util.io.DigestUtil
+import com.intellij.util.system.CpuArch
+import software.aws.toolkits.core.utils.ZIP_PROPERTY_POSIX
+import software.aws.toolkits.core.utils.createParentDirectories
+import software.aws.toolkits.core.utils.exists
+import software.aws.toolkits.core.utils.hasPosixFilePermissions
+import java.io.FileNotFoundException
+import java.net.URI
+import java.nio.file.FileSystems
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.nio.file.StandardCopyOption
+import java.security.MessageDigest
+import kotlin.io.path.isDirectory
+import kotlin.io.path.listDirectoryEntries
+
+fun getToolkitsCommonCacheRoot(): Path = when {
+ SystemInfo.isWindows -> {
+ Paths.get(System.getenv("LOCALAPPDATA"))
+ }
+ SystemInfo.isMac -> {
+ Paths.get(System.getProperty("user.home"), "Library", "Caches")
+ }
+ else -> {
+ Paths.get(System.getProperty("user.home"), ".cache")
+ }
+}
+
+fun getCurrentOS(): String = when {
+ SystemInfo.isWindows -> "windows"
+ SystemInfo.isMac -> "darwin"
+ else -> "linux"
+}
+
+fun getCurrentArchitecture() = when (CpuArch.CURRENT) {
+ CpuArch.X86_64 -> "x64"
+ CpuArch.ARM64 -> "arm64"
+ else -> "unknown"
+}
+
+fun generateMD5Hash(filePath: Path): String {
+ val messageDigest = DigestUtil.md5()
+ DigestUtil.updateContentHash(messageDigest, filePath)
+ return StringUtil.toHexString(messageDigest.digest())
+}
+
+fun generateSHA384Hash(filePath: Path): String {
+ val messageDigest = MessageDigest.getInstance("SHA-384")
+ DigestUtil.updateContentHash(messageDigest, filePath)
+ return StringUtil.toHexString(messageDigest.digest())
+}
+
+fun getSubFolders(basePath: Path): List = try {
+ basePath.listDirectoryEntries()
+ .filter { it.isDirectory() }
+} catch (e: Exception) {
+ emptyList()
+}
+
+fun moveFilesFromSourceToDestination(sourceDir: Path, targetDir: Path) {
+ try {
+ Files.createDirectories(targetDir.parent)
+ Files.move(sourceDir, targetDir, StandardCopyOption.REPLACE_EXISTING)
+ } catch (e: Exception) {
+ throw IllegalStateException("Failed to move files from $sourceDir to $targetDir", e)
+ }
+}
+
+fun extractZipFile(zipFilePath: Path, destDir: Path) {
+ if (!zipFilePath.exists()) {
+ throw FileNotFoundException("Zip file not found: $zipFilePath")
+ }
+
+ try {
+ FileSystems.newFileSystem(
+ // jar prefix due to potentially ambiguous resolution to wrong fs impl for zipfs on windows
+ URI("jar:${zipFilePath.toUri()}"),
+ mapOf(ZIP_PROPERTY_POSIX to destDir.hasPosixFilePermissions())
+ ).use { zipfs ->
+ Files.walk(zipfs.getPath("/")).use { paths ->
+ paths
+ .filter { !it.isDirectory() }
+ .forEach { zipEntry ->
+ val destPath = Paths.get(destDir.toString(), zipEntry.toString())
+ destPath.createParentDirectories()
+ Files.copy(zipEntry, destPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES)
+ }
+ }
+ }
+ } catch (e: Exception) {
+ throw LspException("Failed to extract zip file: ${e.message}", LspException.ErrorCode.UNZIP_FAILED, cause = e)
+ }
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ManifestFetcher.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ManifestFetcher.kt
new file mode 100644
index 00000000000..74656d5665b
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ManifestFetcher.kt
@@ -0,0 +1,115 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts
+
+import org.jetbrains.annotations.VisibleForTesting
+import software.aws.toolkits.core.utils.deleteIfExists
+import software.aws.toolkits.core.utils.error
+import software.aws.toolkits.core.utils.exists
+import software.aws.toolkits.core.utils.getLogger
+import software.aws.toolkits.core.utils.info
+import software.aws.toolkits.core.utils.readText
+import software.aws.toolkits.jetbrains.core.getETagFromUrl
+import software.aws.toolkits.jetbrains.core.getTextFromUrl
+import software.aws.toolkits.jetbrains.core.saveFileFromUrl
+import software.aws.toolkits.jetbrains.services.amazonq.project.manifest.ManifestManager
+import java.nio.file.Path
+
+class ManifestFetcher(
+ private val lspManifestUrl: String = DEFAULT_MANIFEST_URL,
+ private val manifestManager: ManifestManager = ManifestManager(),
+ private val manifestPath: Path = DEFAULT_MANIFEST_PATH,
+) {
+ companion object {
+ private val logger = getLogger()
+
+ private const val DEFAULT_MANIFEST_URL =
+ "https://aws-toolkit-language-servers.amazonaws.com/remoteWorkspaceContext/0/manifest.json"
+
+ private val DEFAULT_MANIFEST_PATH: Path = getToolkitsCommonCacheRoot()
+ .resolve("aws")
+ .resolve("toolkits")
+ .resolve("language-servers")
+ .resolve("jetbrains-lsp-manifest.json")
+ }
+
+ @get:VisibleForTesting
+ internal val lspManifestFilePath: Path
+ get() = manifestPath
+
+ /**
+ * Method which will be used to fetch latest manifest.
+ * */
+ fun fetch(): ManifestManager.Manifest? {
+ val localManifest = fetchManifestFromLocal()
+ if (localManifest != null) {
+ return localManifest
+ }
+ return fetchManifestFromRemote()
+ }
+
+ @VisibleForTesting
+ internal fun fetchManifestFromRemote(): ManifestManager.Manifest? {
+ val manifest: ManifestManager.Manifest?
+ try {
+ val manifestString = getTextFromUrl(lspManifestUrl)
+ manifest = manifestManager.readManifestFile(manifestString) ?: return null
+ } catch (e: Exception) {
+ logger.error(e) { "error fetching lsp manifest from remote URL ${e.message}" }
+ return null
+ }
+ if (manifest.isManifestDeprecated == true) {
+ logger.info { "Manifest is deprecated" }
+ return null
+ }
+ updateManifestCache()
+ logger.info { "Using manifest found from remote URL" }
+ return manifest
+ }
+
+ private fun updateManifestCache() {
+ try {
+ saveFileFromUrl(lspManifestUrl, lspManifestFilePath)
+ } catch (e: Exception) {
+ logger.error(e) { "error occurred while saving lsp manifest to local cache ${e.message}" }
+ }
+ }
+
+ @VisibleForTesting
+ internal fun fetchManifestFromLocal(): ManifestManager.Manifest? {
+ val localETag = getManifestETagFromLocal()
+ val remoteETag = getManifestETagFromUrl()
+ // If local and remote have same ETag, we can re-use the manifest file from local to fetch artifacts.
+ // If remote manifest is null or system is offline, re-use localManifest
+ if ((localETag != null && remoteETag != null && localETag == remoteETag) or (localETag != null && remoteETag == null)) {
+ try {
+ val manifestContent = lspManifestFilePath.readText()
+ val manifest = manifestManager.readManifestFile(manifestContent)
+ if (manifest != null) return manifest
+ lspManifestFilePath.deleteIfExists() // delete manifest if it fails to de-serialize
+ } catch (e: Exception) {
+ logger.error(e) { "error reading lsp manifest file from local ${e.message}" }
+ return null
+ }
+ }
+ return null
+ }
+
+ private fun getManifestETagFromLocal(): String? {
+ if (lspManifestFilePath.exists()) {
+ return generateMD5Hash(lspManifestFilePath)
+ }
+ return null
+ }
+
+ private fun getManifestETagFromUrl(): String? {
+ try {
+ val actualETag = getETagFromUrl(lspManifestUrl)
+ return actualETag.trim('"')
+ } catch (e: Exception) {
+ logger.error(e) { "error fetching ETag of lsp manifest from url." }
+ }
+ return null
+ }
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/auth/AuthCredentialsService.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/auth/AuthCredentialsService.kt
new file mode 100644
index 00000000000..a38c8da4bbc
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/auth/AuthCredentialsService.kt
@@ -0,0 +1,12 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.auth
+
+import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage
+import java.util.concurrent.CompletableFuture
+
+interface AuthCredentialsService {
+ fun updateTokenCredentials(accessToken: String, encrypted: Boolean): CompletableFuture
+ fun deleteTokenCredentials(): CompletableFuture
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/auth/DefaultAuthCredentialsService.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/auth/DefaultAuthCredentialsService.kt
new file mode 100644
index 00000000000..d3a99a1f4fe
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/auth/DefaultAuthCredentialsService.kt
@@ -0,0 +1,137 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.auth
+
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.project.Project
+import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage
+import software.aws.toolkits.core.TokenConnectionSettings
+import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection
+import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
+import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManagerListener
+import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
+import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProvider
+import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProviderListener
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.encryption.JwtEncryptionManager
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.LspServerConfigurations
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.UpdateConfigurationParams
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.BearerCredentials
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.UpdateCredentialsPayload
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.UpdateCredentialsPayloadData
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
+import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileSelectedListener
+import software.aws.toolkits.jetbrains.utils.isQConnected
+import software.aws.toolkits.jetbrains.utils.isQExpired
+import java.util.concurrent.CompletableFuture
+
+class DefaultAuthCredentialsService(
+ private val project: Project,
+ private val encryptionManager: JwtEncryptionManager,
+ serverInstance: Disposable,
+) : AuthCredentialsService,
+ BearerTokenProviderListener,
+ ToolkitConnectionManagerListener,
+ QRegionProfileSelectedListener {
+
+ init {
+ project.messageBus.connect(serverInstance).apply {
+ subscribe(BearerTokenProviderListener.TOPIC, this@DefaultAuthCredentialsService)
+ subscribe(ToolkitConnectionManagerListener.TOPIC, this@DefaultAuthCredentialsService)
+ subscribe(QRegionProfileSelectedListener.TOPIC, this@DefaultAuthCredentialsService)
+ }
+
+ if (isQConnected(project) && !isQExpired(project)) {
+ updateTokenFromActiveConnection()
+ .thenRun {
+ updateConfiguration()
+ }
+ }
+ }
+
+ override fun updateTokenCredentials(accessToken: String, encrypted: Boolean): CompletableFuture {
+ val payload = createUpdateCredentialsPayload(accessToken, encrypted)
+
+ return AmazonQLspService.executeIfRunning(project) { server ->
+ server.updateTokenCredentials(payload)
+ } ?: (CompletableFuture.failedFuture(IllegalStateException("LSP Server not running")))
+ }
+
+ override fun deleteTokenCredentials(): CompletableFuture =
+ CompletableFuture().also { completableFuture ->
+ AmazonQLspService.executeIfRunning(project) { server ->
+ server.deleteTokenCredentials()
+ completableFuture.complete(null)
+ } ?: completableFuture.completeExceptionally(IllegalStateException("LSP Server not running"))
+ }
+
+ override fun onChange(providerId: String, newScopes: List?) {
+ updateTokenFromActiveConnection()
+ }
+
+ override fun activeConnectionChanged(newConnection: ToolkitConnection?) {
+ val qConnection = ToolkitConnectionManager.getInstance(project)
+ .activeConnectionForFeature(QConnection.getInstance())
+ ?: return
+ if (newConnection?.id != qConnection.id) return
+
+ updateTokenFromConnection(newConnection)
+ }
+
+ private fun updateTokenFromActiveConnection(): CompletableFuture {
+ val connection = ToolkitConnectionManager.getInstance(project)
+ .activeConnectionForFeature(QConnection.getInstance())
+ ?: return CompletableFuture.failedFuture(IllegalStateException("No active Q connection"))
+
+ return updateTokenFromConnection(connection)
+ }
+
+ private fun updateTokenFromConnection(connection: ToolkitConnection): CompletableFuture =
+ (connection.getConnectionSettings() as? TokenConnectionSettings)
+ ?.tokenProvider
+ ?.delegate
+ ?.let { it as? BearerTokenProvider }
+ ?.currentToken()
+ ?.accessToken
+ ?.let { token -> updateTokenCredentials(token, true) }
+ ?: CompletableFuture.failedFuture(IllegalStateException("Unable to get token from connection"))
+
+ override fun invalidate(providerId: String) {
+ deleteTokenCredentials()
+ }
+
+ private fun createUpdateCredentialsPayload(token: String, encrypted: Boolean): UpdateCredentialsPayload =
+ if (encrypted) {
+ UpdateCredentialsPayload(
+ data = encryptionManager.encrypt(
+ UpdateCredentialsPayloadData(
+ BearerCredentials(token)
+ )
+ ),
+ encrypted = true
+ )
+ } else {
+ UpdateCredentialsPayload(
+ data = token,
+ encrypted = false
+ )
+ }
+
+ override fun onProfileSelected(project: Project, profile: QRegionProfile?) {
+ updateConfiguration()
+ }
+
+ private fun updateConfiguration(): CompletableFuture {
+ val payload = UpdateConfigurationParams(
+ section = "aws.q",
+ settings = mapOf(
+ "profileArn" to QRegionProfileManager.getInstance().activeProfile(project)?.arn
+ )
+ )
+ return AmazonQLspService.executeIfRunning(project) { server ->
+ server.updateConfiguration(payload)
+ } ?: (CompletableFuture.failedFuture(IllegalStateException("LSP Server not running")))
+ }
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/dependencies/DefaultModuleDependenciesService.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/dependencies/DefaultModuleDependenciesService.kt
new file mode 100644
index 00000000000..80239696d14
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/dependencies/DefaultModuleDependenciesService.kt
@@ -0,0 +1,52 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.dependencies
+
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.module.ModuleManager
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.roots.ModuleRootEvent
+import com.intellij.openapi.roots.ModuleRootListener
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.dependencies.ModuleDependencyProvider.Companion.EP_NAME
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.dependencies.DidChangeDependencyPathsParams
+import java.util.concurrent.CompletableFuture
+
+class DefaultModuleDependenciesService(
+ private val project: Project,
+ serverInstance: Disposable,
+) : ModuleDependenciesService,
+ ModuleRootListener {
+
+ init {
+ project.messageBus.connect(serverInstance).subscribe(
+ ModuleRootListener.TOPIC,
+ this
+ )
+ // project initiation with initial list of dependencies
+ syncAllModules()
+ }
+
+ override fun rootsChanged(event: ModuleRootEvent) {
+ if (event.isCausedByFileTypesChange) return
+ // call on change with updated dependencies
+ syncAllModules()
+ }
+
+ override fun didChangeDependencyPaths(params: DidChangeDependencyPathsParams): CompletableFuture =
+ AmazonQLspService.executeIfRunning(project) { languageServer ->
+ languageServer.didChangeDependencyPaths(params)
+ }?.toCompletableFuture() ?: CompletableFuture.failedFuture(IllegalStateException("LSP Server not running"))
+
+ private fun syncAllModules() {
+ ModuleManager.getInstance(project).modules.forEach { module ->
+ EP_NAME.forEachExtensionSafe {
+ if (it.isApplicable(module)) {
+ didChangeDependencyPaths(it.createParams(module))
+ return@forEachExtensionSafe
+ }
+ }
+ }
+ }
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/dependencies/ModuleDependenciesService.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/dependencies/ModuleDependenciesService.kt
new file mode 100644
index 00000000000..82370dad895
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/dependencies/ModuleDependenciesService.kt
@@ -0,0 +1,11 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.dependencies
+
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.dependencies.DidChangeDependencyPathsParams
+import java.util.concurrent.CompletableFuture
+
+interface ModuleDependenciesService {
+ fun didChangeDependencyPaths(params: DidChangeDependencyPathsParams): CompletableFuture
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/dependencies/ModuleDependencyProvider.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/dependencies/ModuleDependencyProvider.kt
new file mode 100644
index 00000000000..e8d0087e7d2
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/dependencies/ModuleDependencyProvider.kt
@@ -0,0 +1,25 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.dependencies
+
+import com.intellij.openapi.extensions.ExtensionPointName
+import com.intellij.openapi.module.Module
+import com.intellij.openapi.roots.ModuleRootManager
+import com.intellij.openapi.vfs.VirtualFile
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.dependencies.DidChangeDependencyPathsParams
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.FileUriUtil.toUriString
+
+interface ModuleDependencyProvider {
+ companion object {
+ val EP_NAME = ExtensionPointName("software.aws.toolkits.jetbrains.moduleDependencyProvider")
+ }
+
+ fun isApplicable(module: Module): Boolean
+ fun createParams(module: Module): DidChangeDependencyPathsParams
+
+ fun getWorkspaceFolderPath(module: Module): String {
+ val contentRoots: Array = ModuleRootManager.getInstance(module).contentRoots
+ return contentRoots.firstOrNull()?.let { toUriString(it) }.orEmpty()
+ }
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/dependencies/providers/JavaModuleDependencyProvider.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/dependencies/providers/JavaModuleDependencyProvider.kt
new file mode 100644
index 00000000000..25d1d36c00f
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/dependencies/providers/JavaModuleDependencyProvider.kt
@@ -0,0 +1,35 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.dependencies.providers
+
+import com.intellij.openapi.module.Module
+import com.intellij.openapi.projectRoots.JavaSdkType
+import com.intellij.openapi.roots.ModuleRootManager
+import com.intellij.openapi.roots.OrderRootType
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.dependencies.ModuleDependencyProvider
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.dependencies.DidChangeDependencyPathsParams
+
+internal class JavaModuleDependencyProvider : ModuleDependencyProvider {
+ override fun isApplicable(module: Module): Boolean =
+ ModuleRootManager.getInstance(module).sdk?.sdkType is JavaSdkType
+
+ override fun createParams(module: Module): DidChangeDependencyPathsParams {
+ val dependencies = mutableListOf()
+
+ ModuleRootManager.getInstance(module).orderEntries().forEachLibrary { library ->
+ library.getFiles(OrderRootType.CLASSES).forEach { file ->
+ dependencies.add(file.path.removeSuffix("!/"))
+ }
+ true
+ }
+
+ return DidChangeDependencyPathsParams(
+ moduleName = getWorkspaceFolderPath(module),
+ runtimeLanguage = "java",
+ paths = dependencies,
+ includePatterns = emptyList(),
+ excludePatterns = emptyList()
+ )
+ }
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/dependencies/providers/PythonModuleDependencyProvider.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/dependencies/providers/PythonModuleDependencyProvider.kt
new file mode 100644
index 00000000000..9a7961d4fbe
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/dependencies/providers/PythonModuleDependencyProvider.kt
@@ -0,0 +1,39 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.dependencies.providers
+
+import com.intellij.openapi.module.Module
+import com.jetbrains.python.packaging.management.PythonPackageManager
+import com.jetbrains.python.sdk.PythonSdkUtil
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.dependencies.ModuleDependencyProvider
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.dependencies.DidChangeDependencyPathsParams
+
+internal class PythonModuleDependencyProvider : ModuleDependencyProvider {
+ override fun isApplicable(module: Module): Boolean =
+ PythonSdkUtil.findPythonSdk(module) != null
+
+ override fun createParams(module: Module): DidChangeDependencyPathsParams {
+ val dependencies = mutableListOf()
+
+ PythonSdkUtil.findPythonSdk(module)?.let { sdk ->
+ PythonSdkUtil.getSitePackagesDirectory(sdk)?.let { sitePackagesDir ->
+ val packageManager = PythonPackageManager.forSdk(module.project, sdk)
+ packageManager.installedPackages.forEach { pkg ->
+ val packageDir = sitePackagesDir.findChild(pkg.name)
+ if (packageDir != null) {
+ dependencies.add(packageDir.path)
+ }
+ }
+ }
+ }
+
+ return DidChangeDependencyPathsParams(
+ moduleName = getWorkspaceFolderPath(module),
+ runtimeLanguage = "python",
+ paths = dependencies,
+ includePatterns = emptyList(),
+ excludePatterns = emptyList()
+ )
+ }
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/encryption/JwtEncryptionManager.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/encryption/JwtEncryptionManager.kt
new file mode 100644
index 00000000000..bca385682ea
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/encryption/JwtEncryptionManager.kt
@@ -0,0 +1,65 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.encryption
+
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
+import com.nimbusds.jose.EncryptionMethod
+import com.nimbusds.jose.JWEAlgorithm
+import com.nimbusds.jose.JWEHeader
+import com.nimbusds.jose.JWEObject
+import com.nimbusds.jose.Payload
+import com.nimbusds.jose.crypto.DirectDecrypter
+import com.nimbusds.jose.crypto.DirectEncrypter
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.EncryptionInitializationRequest
+import java.io.OutputStream
+import java.security.SecureRandom
+import java.util.Base64
+import javax.crypto.SecretKey
+import javax.crypto.spec.SecretKeySpec
+
+class JwtEncryptionManager(private val key: SecretKey) {
+ constructor() : this(generateHmacKey())
+
+ private val mapper = jacksonObjectMapper()
+
+ fun writeInitializationPayload(os: OutputStream) {
+ val payload = EncryptionInitializationRequest(
+ EncryptionInitializationRequest.Version.V1_0,
+ EncryptionInitializationRequest.Mode.JWT,
+ Base64.getUrlEncoder().withoutPadding().encodeToString(key.encoded)
+ )
+
+ // write directly to stream because utils are closing the underlying stream
+ os.write("${mapper.writeValueAsString(payload)}\n".toByteArray())
+ }
+
+ fun encrypt(data: Any): String {
+ val header = JWEHeader(JWEAlgorithm.DIR, EncryptionMethod.A256GCM)
+ val payload = if (data is String) {
+ Payload(data)
+ } else {
+ Payload(mapper.writeValueAsBytes(data))
+ }
+
+ val jweObject = JWEObject(header, payload)
+ jweObject.encrypt(DirectEncrypter(key))
+
+ return jweObject.serialize()
+ }
+
+ fun decrypt(jwt: String): String {
+ val jweObject = JWEObject.parse(jwt)
+ jweObject.decrypt(DirectDecrypter(key))
+
+ return jweObject.payload.toString()
+ }
+
+ private companion object {
+ private fun generateHmacKey(): SecretKey {
+ val keyBytes = ByteArray(32)
+ SecureRandom().nextBytes(keyBytes)
+ return SecretKeySpec(keyBytes, "HmacSHA256")
+ }
+ }
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/EncryptionInitializationRequest.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/EncryptionInitializationRequest.kt
new file mode 100644
index 00000000000..53748475195
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/EncryptionInitializationRequest.kt
@@ -0,0 +1,20 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.model
+
+import com.fasterxml.jackson.annotation.JsonValue
+
+data class EncryptionInitializationRequest(
+ val version: Version,
+ val mode: Mode,
+ val key: String,
+) {
+ enum class Version(@JsonValue val value: String) {
+ V1_0("1.0"),
+ }
+
+ enum class Mode(@JsonValue val value: String) {
+ JWT("JWT"),
+ }
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/ExtendedClientMetadata.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/ExtendedClientMetadata.kt
new file mode 100644
index 00000000000..b25150c36bb
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/ExtendedClientMetadata.kt
@@ -0,0 +1,57 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.model
+
+import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata
+
+data class ExtendedClientMetadata(
+ val aws: AwsMetadata,
+)
+
+data class AwsMetadata(
+ val clientInfo: ClientInfoMetadata,
+ val awsClientCapabilities: AwsClientCapabilities,
+)
+
+data class AwsClientCapabilities(
+ val q: DeveloperProfiles,
+)
+
+data class DeveloperProfiles(
+ val developerProfiles: Boolean,
+)
+
+data class ClientInfoMetadata(
+ val extension: ExtensionMetadata,
+ val clientId: String,
+ val version: String,
+ val name: String,
+)
+
+data class ExtensionMetadata(
+ val name: String,
+ val version: String,
+)
+
+fun createExtendedClientMetadata(): ExtendedClientMetadata {
+ val metadata = ClientMetadata.getDefault()
+ return ExtendedClientMetadata(
+ aws = AwsMetadata(
+ clientInfo = ClientInfoMetadata(
+ extension = ExtensionMetadata(
+ name = metadata.awsProduct.toString(),
+ version = metadata.awsVersion
+ ),
+ clientId = metadata.clientId,
+ version = metadata.parentProductVersion,
+ name = metadata.parentProduct
+ ),
+ awsClientCapabilities = AwsClientCapabilities(
+ q = DeveloperProfiles(
+ developerProfiles = true
+ )
+ )
+ )
+ )
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/GetConfigurationFromServerParams.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/GetConfigurationFromServerParams.kt
new file mode 100644
index 00000000000..551ddfa97b0
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/GetConfigurationFromServerParams.kt
@@ -0,0 +1,8 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws
+
+data class GetConfigurationFromServerParams(
+ val section: String,
+)
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/LspServerConfigurations.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/LspServerConfigurations.kt
new file mode 100644
index 00000000000..a0f23875b62
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/LspServerConfigurations.kt
@@ -0,0 +1,17 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws
+
+// This represents each item in the array
+data class WorkspaceInfo(val workspaceRoot: String, val workspaceId: String)
+
+// This represents the entire array
+data class LspServerConfigurations(val workspaces: List)
+
+data class UpdateConfigurationParams(
+ val section: String,
+ val settings: LSPAny,
+)
+
+typealias LSPAny = Any?
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/credentials/ConnectionMetadata.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/credentials/ConnectionMetadata.kt
new file mode 100644
index 00000000000..c6216b97cff
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/credentials/ConnectionMetadata.kt
@@ -0,0 +1,12 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials
+
+data class ConnectionMetadata(
+ val sso: SsoProfileData,
+)
+
+data class SsoProfileData(
+ val startUrl: String,
+)
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/credentials/UpdateCredentialsPayload.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/credentials/UpdateCredentialsPayload.kt
new file mode 100644
index 00000000000..a427330c055
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/credentials/UpdateCredentialsPayload.kt
@@ -0,0 +1,17 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials
+
+data class UpdateCredentialsPayload(
+ val data: String,
+ val encrypted: Boolean,
+)
+
+data class UpdateCredentialsPayloadData(
+ val data: BearerCredentials,
+)
+
+data class BearerCredentials(
+ val token: String,
+)
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/dependencies/DidChangeDependencyPathsParams.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/dependencies/DidChangeDependencyPathsParams.kt
new file mode 100644
index 00000000000..8436b985825
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/dependencies/DidChangeDependencyPathsParams.kt
@@ -0,0 +1,12 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.dependencies
+
+class DidChangeDependencyPathsParams(
+ val moduleName: String,
+ val runtimeLanguage: String,
+ val paths: List,
+ val includePatterns: List,
+ val excludePatterns: List,
+)
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/textdocument/TextDocumentServiceHandler.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/textdocument/TextDocumentServiceHandler.kt
new file mode 100644
index 00000000000..bbf60200810
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/textdocument/TextDocumentServiceHandler.kt
@@ -0,0 +1,144 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.textdocument
+
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.editor.Document
+import com.intellij.openapi.fileEditor.FileDocumentManager
+import com.intellij.openapi.fileEditor.FileDocumentManagerListener
+import com.intellij.openapi.fileEditor.FileEditorManager
+import com.intellij.openapi.fileEditor.FileEditorManagerListener
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.openapi.vfs.VirtualFileManager
+import com.intellij.openapi.vfs.newvfs.BulkFileListener
+import com.intellij.openapi.vfs.newvfs.events.VFileContentChangeEvent
+import com.intellij.openapi.vfs.newvfs.events.VFileEvent
+import org.eclipse.lsp4j.DidChangeTextDocumentParams
+import org.eclipse.lsp4j.DidCloseTextDocumentParams
+import org.eclipse.lsp4j.DidOpenTextDocumentParams
+import org.eclipse.lsp4j.DidSaveTextDocumentParams
+import org.eclipse.lsp4j.TextDocumentContentChangeEvent
+import org.eclipse.lsp4j.TextDocumentIdentifier
+import org.eclipse.lsp4j.TextDocumentItem
+import org.eclipse.lsp4j.VersionedTextDocumentIdentifier
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.FileUriUtil.toUriString
+import software.aws.toolkits.jetbrains.utils.pluginAwareExecuteOnPooledThread
+
+class TextDocumentServiceHandler(
+ private val project: Project,
+ serverInstance: Disposable,
+) : FileDocumentManagerListener,
+ FileEditorManagerListener,
+ BulkFileListener {
+
+ init {
+ // didOpen & didClose events
+ project.messageBus.connect(serverInstance).subscribe(
+ FileEditorManagerListener.FILE_EDITOR_MANAGER,
+ this
+ )
+
+ // didChange events
+ project.messageBus.connect(serverInstance).subscribe(
+ VirtualFileManager.VFS_CHANGES,
+ this
+ )
+
+ // didSave events
+ project.messageBus.connect(serverInstance).subscribe(
+ FileDocumentManagerListener.TOPIC,
+ this
+ )
+
+ // open files on startup
+ val fileEditorManager = FileEditorManager.getInstance(project)
+ fileEditorManager.openFiles.forEach { file ->
+ handleFileOpened(file)
+ }
+ }
+
+ private fun handleFileOpened(file: VirtualFile) {
+ AmazonQLspService.executeIfRunning(project) { languageServer ->
+ toUriString(file)?.let { uri ->
+ languageServer.textDocumentService.didOpen(
+ DidOpenTextDocumentParams().apply {
+ textDocument = TextDocumentItem().apply {
+ this.uri = uri
+ text = file.inputStream.readAllBytes().decodeToString()
+ languageId = file.fileType.name.lowercase()
+ version = file.modificationStamp.toInt()
+ }
+ }
+ )
+ }
+ }
+ }
+
+ override fun beforeDocumentSaving(document: Document) {
+ AmazonQLspService.executeIfRunning(project) { languageServer ->
+ val file = FileDocumentManager.getInstance().getFile(document) ?: return@executeIfRunning
+ toUriString(file)?.let { uri ->
+ languageServer.textDocumentService.didSave(
+ DidSaveTextDocumentParams().apply {
+ textDocument = TextDocumentIdentifier().apply {
+ this.uri = uri
+ }
+ text = document.text
+ }
+ )
+ }
+ }
+ }
+
+ override fun after(events: MutableList) {
+ AmazonQLspService.executeIfRunning(project) { languageServer ->
+ pluginAwareExecuteOnPooledThread {
+ events.filterIsInstance().forEach { event ->
+ val document = FileDocumentManager.getInstance().getCachedDocument(event.file) ?: return@forEach
+ toUriString(event.file)?.let { uri ->
+ languageServer.textDocumentService.didChange(
+ DidChangeTextDocumentParams().apply {
+ textDocument = VersionedTextDocumentIdentifier().apply {
+ this.uri = uri
+ version = document.modificationStamp.toInt()
+ }
+ contentChanges = listOf(
+ TextDocumentContentChangeEvent().apply {
+ text = document.text
+ }
+ )
+ }
+ )
+ }
+ }
+ }
+ }
+ }
+
+ override fun fileOpened(
+ source: FileEditorManager,
+ file: VirtualFile,
+ ) {
+ handleFileOpened(file)
+ }
+
+ override fun fileClosed(
+ source: FileEditorManager,
+ file: VirtualFile,
+ ) {
+ AmazonQLspService.executeIfRunning(project) { languageServer ->
+ toUriString(file)?.let { uri ->
+ languageServer.textDocumentService.didClose(
+ DidCloseTextDocumentParams().apply {
+ textDocument = TextDocumentIdentifier().apply {
+ this.uri = uri
+ }
+ }
+ )
+ }
+ }
+ }
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/FileUriUtil.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/FileUriUtil.kt
new file mode 100644
index 00000000000..b2821257a49
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/FileUriUtil.kt
@@ -0,0 +1,42 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.util
+
+import com.intellij.openapi.vfs.VfsUtilCore
+import com.intellij.openapi.vfs.VirtualFile
+import software.aws.toolkits.core.utils.getLogger
+import software.aws.toolkits.core.utils.warn
+import java.io.File
+import java.net.URI
+import java.net.URISyntaxException
+
+object FileUriUtil {
+
+ fun toUriString(virtualFile: VirtualFile): String? {
+ val protocol = virtualFile.fileSystem.protocol
+ val uri = when (protocol) {
+ "jar" -> VfsUtilCore.convertToURL(virtualFile.url)?.toExternalForm()
+ "jrt" -> virtualFile.url
+ else -> toUri(VfsUtilCore.virtualToIoFile(virtualFile)).toASCIIString()
+ } ?: return null
+
+ return if (virtualFile.isDirectory) {
+ uri.trimEnd('/', '\\')
+ } else {
+ uri
+ }
+ }
+
+ private fun toUri(file: File): URI {
+ try {
+ // URI scheme specified by language server protocol
+ return URI("file", "", file.absoluteFile.toURI().path, null)
+ } catch (e: URISyntaxException) {
+ LOG.warn { "${e.localizedMessage}: $e" }
+ return file.absoluteFile.toURI()
+ }
+ }
+
+ private val LOG = getLogger()
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/WorkspaceFolderUtil.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/WorkspaceFolderUtil.kt
new file mode 100644
index 00000000000..87e570b9f48
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/WorkspaceFolderUtil.kt
@@ -0,0 +1,26 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.util
+
+import com.intellij.openapi.module.ModuleManager
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.roots.ModuleRootManager
+import org.eclipse.lsp4j.WorkspaceFolder
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.FileUriUtil.toUriString
+
+object WorkspaceFolderUtil {
+ fun createWorkspaceFolders(project: Project): List =
+ if (project.isDefault) {
+ emptyList()
+ } else {
+ ModuleManager.getInstance(project).modules.mapNotNull { module ->
+ ModuleRootManager.getInstance(module).contentRoots.firstOrNull()?.let { contentRoot ->
+ WorkspaceFolder().apply {
+ name = module.name
+ uri = toUriString(contentRoot)
+ }
+ }
+ }
+ }
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt
new file mode 100644
index 00000000000..a457a6e0ef5
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt
@@ -0,0 +1,287 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.workspace
+
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.roots.ModuleRootEvent
+import com.intellij.openapi.roots.ModuleRootListener
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.openapi.vfs.VirtualFileManager
+import com.intellij.openapi.vfs.newvfs.BulkFileListener
+import com.intellij.openapi.vfs.newvfs.events.VFileCopyEvent
+import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent
+import com.intellij.openapi.vfs.newvfs.events.VFileDeleteEvent
+import com.intellij.openapi.vfs.newvfs.events.VFileEvent
+import com.intellij.openapi.vfs.newvfs.events.VFileMoveEvent
+import com.intellij.openapi.vfs.newvfs.events.VFilePropertyChangeEvent
+import org.eclipse.lsp4j.CreateFilesParams
+import org.eclipse.lsp4j.DeleteFilesParams
+import org.eclipse.lsp4j.DidChangeWatchedFilesParams
+import org.eclipse.lsp4j.DidChangeWorkspaceFoldersParams
+import org.eclipse.lsp4j.DidCloseTextDocumentParams
+import org.eclipse.lsp4j.DidOpenTextDocumentParams
+import org.eclipse.lsp4j.FileChangeType
+import org.eclipse.lsp4j.FileCreate
+import org.eclipse.lsp4j.FileDelete
+import org.eclipse.lsp4j.FileEvent
+import org.eclipse.lsp4j.FileRename
+import org.eclipse.lsp4j.RenameFilesParams
+import org.eclipse.lsp4j.TextDocumentIdentifier
+import org.eclipse.lsp4j.TextDocumentItem
+import org.eclipse.lsp4j.WorkspaceFolder
+import org.eclipse.lsp4j.WorkspaceFoldersChangeEvent
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.FileUriUtil.toUriString
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.WorkspaceFolderUtil.createWorkspaceFolders
+import software.aws.toolkits.jetbrains.utils.pluginAwareExecuteOnPooledThread
+import java.nio.file.FileSystems
+import java.nio.file.Paths
+
+class WorkspaceServiceHandler(
+ private val project: Project,
+ serverInstance: Disposable,
+) : BulkFileListener,
+ ModuleRootListener {
+
+ private var lastSnapshot: List = emptyList()
+ private val supportedFilePatterns = FileSystems.getDefault().getPathMatcher(
+ "glob:**/*.{ts,js,py,java}"
+ )
+
+ init {
+ project.messageBus.connect(serverInstance).subscribe(
+ VirtualFileManager.VFS_CHANGES,
+ this
+ )
+
+ project.messageBus.connect(serverInstance).subscribe(
+ ModuleRootListener.TOPIC,
+ this
+ )
+ }
+
+ private fun didCreateFiles(events: List) {
+ AmazonQLspService.executeIfRunning(project) { languageServer ->
+ val validFiles = events.mapNotNull { event ->
+ when (event) {
+ is VFileCopyEvent -> {
+ val newFile = event.newParent.findChild(event.newChildName)?.takeIf { shouldHandleFile(it) }
+ ?: return@mapNotNull null
+ toUriString(newFile)?.let { uri ->
+ FileCreate().apply {
+ this.uri = uri
+ }
+ }
+ }
+ else -> {
+ val file = event.file?.takeIf { shouldHandleFile(it) }
+ ?: return@mapNotNull null
+ toUriString(file)?.let { uri ->
+ FileCreate().apply {
+ this.uri = uri
+ }
+ }
+ }
+ }
+ }
+
+ if (validFiles.isNotEmpty()) {
+ languageServer.workspaceService.didCreateFiles(
+ CreateFilesParams().apply {
+ files = validFiles
+ }
+ )
+ }
+ }
+ }
+
+ private fun didDeleteFiles(events: List) {
+ AmazonQLspService.executeIfRunning(project) { languageServer ->
+ val validFiles = events.mapNotNull { event ->
+ when (event) {
+ is VFileDeleteEvent -> {
+ val file = event.file.takeIf { shouldHandleFile(it) } ?: return@mapNotNull null
+ toUriString(file)
+ }
+ is VFileMoveEvent -> {
+ val oldFile = event.oldParent?.takeIf { shouldHandleFile(it) } ?: return@mapNotNull null
+ toUriString(oldFile)
+ }
+ else -> null
+ }?.let { uri ->
+ FileDelete().apply {
+ this.uri = uri
+ }
+ }
+ }
+
+ if (validFiles.isNotEmpty()) {
+ languageServer.workspaceService.didDeleteFiles(
+ DeleteFilesParams().apply {
+ files = validFiles
+ }
+ )
+ }
+ }
+ }
+
+ private fun didRenameFiles(events: List) {
+ AmazonQLspService.executeIfRunning(project) { languageServer ->
+ val validRenames = events
+ .filter { it.propertyName == VirtualFile.PROP_NAME }
+ .mapNotNull { event ->
+ val renamedFile = event.file.takeIf { shouldHandleFile(it) } ?: return@mapNotNull null
+ val oldFileName = event.oldValue as? String ?: return@mapNotNull null
+ val parentFile = renamedFile.parent ?: return@mapNotNull null
+
+ val oldUri = toUriString(parentFile)?.let { parentUri -> "$parentUri/$oldFileName" }
+ val newUri = toUriString(renamedFile)
+
+ if (!renamedFile.isDirectory) {
+ oldUri?.let { uri ->
+ languageServer.textDocumentService.didClose(
+ DidCloseTextDocumentParams().apply {
+ textDocument = TextDocumentIdentifier().apply {
+ this.uri = uri
+ }
+ }
+ )
+ }
+
+ newUri?.let { uri ->
+ languageServer.textDocumentService.didOpen(
+ DidOpenTextDocumentParams().apply {
+ textDocument = TextDocumentItem().apply {
+ this.uri = uri
+ text = renamedFile.inputStream.readAllBytes().decodeToString()
+ languageId = renamedFile.fileType.name.lowercase()
+ version = renamedFile.modificationStamp.toInt()
+ }
+ }
+ )
+ }
+ }
+
+ FileRename().apply {
+ this.oldUri = oldUri
+ this.newUri = newUri
+ }
+ }
+
+ if (validRenames.isNotEmpty()) {
+ languageServer.workspaceService.didRenameFiles(
+ RenameFilesParams().apply {
+ files = validRenames
+ }
+ )
+ }
+ }
+ }
+
+ private fun didChangeWatchedFiles(events: List) {
+ AmazonQLspService.executeIfRunning(project) { languageServer ->
+ val validChanges = events.flatMap { event ->
+ when (event) {
+ is VFileCopyEvent -> {
+ event.newParent.findChild(event.newChildName)?.let { newFile ->
+ toUriString(newFile)?.let { uri ->
+ listOf(
+ FileEvent().apply {
+ this.uri = uri
+ type = FileChangeType.Created
+ }
+ )
+ }
+ }.orEmpty()
+ }
+ is VFileMoveEvent -> {
+ listOfNotNull(
+ toUriString(event.oldParent)?.let { oldUri ->
+ FileEvent().apply {
+ uri = oldUri
+ type = FileChangeType.Deleted
+ }
+ },
+ toUriString(event.file)?.let { newUri ->
+ FileEvent().apply {
+ uri = newUri
+ type = FileChangeType.Created
+ }
+ }
+ )
+ }
+ else -> {
+ event.file?.let { file ->
+ toUriString(file)?.let { uri ->
+ listOf(
+ FileEvent().apply {
+ this.uri = uri
+ type = when (event) {
+ is VFileCreateEvent -> FileChangeType.Created
+ is VFileDeleteEvent -> FileChangeType.Deleted
+ else -> FileChangeType.Changed
+ }
+ }
+ )
+ }
+ }.orEmpty()
+ }
+ }
+ }
+
+ if (validChanges.isNotEmpty()) {
+ languageServer.workspaceService.didChangeWatchedFiles(
+ DidChangeWatchedFilesParams().apply {
+ changes = validChanges
+ }
+ )
+ }
+ }
+ }
+
+ override fun after(events: List) {
+ // since we are using synchronous FileListener
+ pluginAwareExecuteOnPooledThread {
+ didCreateFiles(events.filter { it is VFileCreateEvent || it is VFileMoveEvent || it is VFileCopyEvent })
+ didDeleteFiles(events.filter { it is VFileMoveEvent || it is VFileDeleteEvent })
+ didRenameFiles(events.filterIsInstance())
+ didChangeWatchedFiles(events)
+ }
+ }
+
+ override fun beforeRootsChange(event: ModuleRootEvent) {
+ lastSnapshot = createWorkspaceFolders(project)
+ }
+
+ override fun rootsChanged(event: ModuleRootEvent) {
+ AmazonQLspService.executeIfRunning(project) { languageServer ->
+ val currentSnapshot = createWorkspaceFolders(project)
+ val addedFolders = currentSnapshot.filter { folder -> lastSnapshot.none { it.uri == folder.uri } }
+ val removedFolders = lastSnapshot.filter { folder -> currentSnapshot.none { it.uri == folder.uri } }
+
+ if (addedFolders.isNotEmpty() || removedFolders.isNotEmpty()) {
+ languageServer.workspaceService.didChangeWorkspaceFolders(
+ DidChangeWorkspaceFoldersParams().apply {
+ this.event = WorkspaceFoldersChangeEvent().apply {
+ added = addedFolders
+ removed = removedFolders
+ }
+ }
+ )
+ }
+
+ lastSnapshot = currentSnapshot
+ }
+ }
+
+ private fun shouldHandleFile(file: VirtualFile): Boolean {
+ if (file.isDirectory) {
+ return true // Matches "**/*" with matches: "folder"
+ }
+ val path = Paths.get(file.path)
+ val result = supportedFilePatterns.matches(path)
+ return result
+ }
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QEndpoints.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QEndpoints.kt
new file mode 100644
index 00000000000..8119def3686
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QEndpoints.kt
@@ -0,0 +1,38 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.profile
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
+import com.fasterxml.jackson.module.kotlin.readValue
+import com.intellij.openapi.util.registry.Registry
+import software.aws.toolkits.core.utils.getLogger
+import software.aws.toolkits.core.utils.warn
+
+object QEndpoints {
+ private val LOG = getLogger()
+ data class QRegionEndpoint(val region: String, val endpoint: String)
+
+ object Q_DEFAULT_SERVICE_CONFIG {
+ const val REGION = "us-east-1"
+ const val ENDPOINT = "https://codewhisperer.us-east-1.amazonaws.com/"
+ }
+
+ private fun parseEndpoints(): Map {
+ val rawJson = Registry.get("amazon.q.endpoints.json").asString().takeIf { it.isNotBlank() } ?: return emptyMap()
+ return try {
+ val regionList: List = jacksonObjectMapper().readValue(rawJson)
+ regionList.associate { it.region to it.endpoint }
+ } catch (e: Exception) {
+ LOG.warn(e) { "Failed to parse amazon.q.endpoints.json: $rawJson" }
+ emptyMap()
+ }
+ }
+
+ fun listRegionEndpoints(): List = parseEndpoints().map { (region, endpoint) -> QRegionEndpoint(region, endpoint) }
+
+ fun getQEndpointWithRegion(regionId: String): String {
+ val all = parseEndpoints()
+ return all[regionId]
+ ?: error("No available endpoint for region=$regionId (check amazon.q.endpoints.json or default fallback)")
+ }
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QProfileResources.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QProfileResources.kt
new file mode 100644
index 00000000000..9f954de1c4e
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QProfileResources.kt
@@ -0,0 +1,59 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.profile
+
+import software.amazon.awssdk.awscore.exception.AwsServiceException
+import software.amazon.awssdk.services.codewhispererruntime.CodeWhispererRuntimeClient
+import software.aws.toolkits.core.ClientConnectionSettings
+import software.aws.toolkits.core.utils.debug
+import software.aws.toolkits.core.utils.getLogger
+import software.aws.toolkits.core.utils.warn
+import software.aws.toolkits.jetbrains.core.AwsClientManager
+import software.aws.toolkits.jetbrains.core.Resource
+import software.aws.toolkits.jetbrains.core.region.AwsRegionProvider
+import java.time.Duration
+
+/**
+ * Save Amazon Q Profile Resource Cache
+ */
+object QProfileResources {
+ /**
+ * save available Q Profile list as cache with default duration 60 s。
+ */
+ val LIST_REGION_PROFILES = object : Resource.Cached>() {
+ override val id: String = "amazonq.allProfiles"
+
+ override fun fetch(connectionSettings: ClientConnectionSettings<*>): List {
+ val mappedProfiles = QEndpoints.listRegionEndpoints().flatMap { (regionKey, _) ->
+ val awsRegion = AwsRegionProvider.getInstance()[regionKey] ?: return@flatMap emptyList()
+ val client = AwsClientManager
+ .getInstance()
+ .getClient(connectionSettings.withRegion(awsRegion))
+
+ try {
+ val profiles = client.listAvailableProfilesPaginator {}
+ .profiles()
+ .map { p -> QRegionProfile(arn = p.arn(), profileName = p.profileName() ?: "") }
+ LOG.debug { "Found profiles for region $regionKey : $profiles" }
+
+ profiles
+ } catch (e: Exception) {
+ LOG.warn(e) { "Failed to list Q profiles for region $regionKey" }
+
+ // service has low TPS so only suppress if not a service error
+ if (e is AwsServiceException) {
+ throw e
+ }
+
+ emptyList()
+ }
+ }
+ return mappedProfiles
+ }
+
+ override fun expiry(): Duration = Duration.ofSeconds(60)
+ }
+
+ private val LOG = getLogger()
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QProfileSwitchIntent.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QProfileSwitchIntent.kt
new file mode 100644
index 00000000000..c1a7b4627ab
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QProfileSwitchIntent.kt
@@ -0,0 +1,19 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.profile
+
+/**
+ * 'user' -> users change the profile through Q menu
+ * 'auth' -> users change the profile through webview profile selector page
+ * 'update' -> plugin auto select the profile on users' behalf as there is only 1 profile
+ * 'reload' -> on plugin restart, plugin will try to reload previous selected profile
+ */
+enum class QProfileSwitchIntent(val value: String) {
+ User("user"),
+ Auth("auth"),
+ Update("update"),
+ Reload("reload"), ;
+
+ override fun toString() = value
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QRegionProfile.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QRegionProfile.kt
new file mode 100644
index 00000000000..e8181d75228
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QRegionProfile.kt
@@ -0,0 +1,24 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.profile
+import software.amazon.awssdk.arns.Arn
+import software.aws.toolkits.core.utils.tryOrNull
+
+data class QRegionProfile(
+ var profileName: String = "",
+ var arn: String = "",
+) {
+ private val parsedArn: Arn? by lazy {
+ tryOrNull {
+ Arn.fromString(arn)
+ }
+ }
+ val accountId: String by lazy {
+ parsedArn?.accountId()?.get().orEmpty()
+ }
+
+ val region: String by lazy {
+ parsedArn?.region()?.get().orEmpty()
+ }
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QRegionProfileDialog.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QRegionProfileDialog.kt
new file mode 100644
index 00000000000..c0921f3362f
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QRegionProfileDialog.kt
@@ -0,0 +1,143 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.profile
+
+import com.intellij.icons.AllIcons
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.ui.DialogPanel
+import com.intellij.openapi.ui.DialogWrapper
+import com.intellij.ui.ColoredListCellRenderer
+import com.intellij.ui.SimpleTextAttributes
+import com.intellij.ui.dsl.builder.AlignX
+import com.intellij.ui.dsl.builder.BottomGap
+import com.intellij.ui.dsl.builder.bindItem
+import com.intellij.ui.dsl.builder.panel
+import com.intellij.ui.dsl.builder.toNullableProperty
+import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection
+import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
+import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
+import software.aws.toolkits.jetbrains.core.help.HelpIds
+import software.aws.toolkits.jetbrains.ui.AsyncComboBox
+import software.aws.toolkits.jetbrains.utils.ui.selected
+import software.aws.toolkits.resources.AmazonQBundle.message
+import software.aws.toolkits.resources.AwsCoreBundle
+import software.aws.toolkits.telemetry.MetricResult
+import software.aws.toolkits.telemetry.Telemetry
+import javax.swing.JComponent
+import javax.swing.JList
+
+data class QRegionProfileDialogState(
+ var selectedProfile: QRegionProfile? = null,
+)
+
+class QRegionProfileDialog(
+ private var project: Project,
+ val state: QRegionProfileDialogState = QRegionProfileDialogState(),
+ private var selectedProfile: QRegionProfile?,
+) : DialogWrapper(project) {
+
+ private val renderer = object : ColoredListCellRenderer() {
+ override fun customizeCellRenderer(
+ list: JList,
+ value: QRegionProfile?,
+ index: Int,
+ selected: Boolean,
+ hasFocus: Boolean,
+ ) {
+ value?.let {
+ append(
+ if (it == selectedProfile) {
+ "${it.profileName} - ${it.region} (connected)"
+ } else {
+ "${it.profileName} - ${it.region}"
+ },
+ SimpleTextAttributes.REGULAR_ATTRIBUTES
+ )
+
+ append(" " + message("action.q.switchProfiles.dialog.account.label", it.accountId), SimpleTextAttributes.GRAY_SMALL_ATTRIBUTES)
+ }
+ }
+ }
+
+ private val combo = AsyncComboBox(customRenderer = renderer)
+
+ private val panel: DialogPanel by lazy {
+ panel {
+ row { label(message("action.q.switchProfiles.dialog.panel.text")).bold() }
+ .bottomGap(BottomGap.MEDIUM)
+ row { text(message("action.q.switchProfiles.dialog.panel.description")) }
+ row {
+ icon(AllIcons.General.Warning)
+ text(message("action.q.switchProfiles.dialog.panel.warning"))
+ }
+ separator().bottomGap(BottomGap.MEDIUM)
+
+ combo.proposeModelUpdate { model ->
+ try {
+ QRegionProfileManager.getInstance().listRegionProfiles(project)?.forEach {
+ model.addElement(it)
+ } ?: error("Attempted to fetch profiles while there does not exist")
+
+ model.selectedItem = selectedProfile
+ } catch (e: Exception) {
+ val conn = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance()) as? AwsBearerTokenConnection
+ Telemetry.amazonq.didSelectProfile.use { span ->
+ span.source(QProfileSwitchIntent.User.value)
+ .amazonQProfileRegion(QRegionProfileManager.getInstance().activeProfile(project)?.region ?: "not-set")
+ .ssoRegion(conn?.region)
+ .credentialStartUrl(conn?.startUrl)
+ .result(MetricResult.Failed)
+ .reason(e.message)
+ }
+ throw e
+ }
+ }
+
+ row {
+ cell(combo)
+ .align(AlignX.FILL)
+ .errorOnApply(AwsCoreBundle.message("gettingstarted.setup.error.not_selected")) { it.selected() == null }
+ .bindItem(state::selectedProfile.toNullableProperty())
+ }
+
+ separator().bottomGap(BottomGap.MEDIUM)
+ }
+ }
+
+ private val selectedOption
+ get() = state.selectedProfile // user selected
+
+ init {
+ title = message("action.q.switchProfiles.dialog.text")
+ setOKButtonText(message("general.ok"))
+ setCancelButtonText(message("general.cancel"))
+ init()
+ }
+
+ override fun getHelpId(): String = HelpIds.Q_SWITCH_PROFILES_DIALOG.id
+ override fun createCenterPanel(): JComponent = panel
+ override fun doOKAction() {
+ panel.apply()
+ if (selectedOption != selectedProfile) {
+ QRegionProfileManager.getInstance().switchProfile(project, selectedOption, intent = QProfileSwitchIntent.User)
+ }
+ close(OK_EXIT_CODE)
+ }
+
+ override fun doCancelAction() {
+ super.doCancelAction()
+ val profileManager = QRegionProfileManager.getInstance()
+ val conn = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance()) as? AwsBearerTokenConnection
+ Telemetry.amazonq.didSelectProfile.use { span ->
+ span.source(QProfileSwitchIntent.User.value)
+ .amazonQProfileRegion(profileManager.activeProfile(project)?.region ?: "not-set")
+ .profileCount(combo.model.size)
+ .ssoRegion(conn?.region)
+ .credentialStartUrl(conn?.startUrl)
+ .result(MetricResult.Cancelled)
+ }
+
+ close(CANCEL_EXIT_CODE)
+ }
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QRegionProfileManager.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QRegionProfileManager.kt
new file mode 100644
index 00000000000..b3b242c363d
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QRegionProfileManager.kt
@@ -0,0 +1,237 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.profile
+
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.components.BaseState
+import com.intellij.openapi.components.PersistentStateComponent
+import com.intellij.openapi.components.Service
+import com.intellij.openapi.components.State
+import com.intellij.openapi.components.Storage
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.Project
+import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
+import com.intellij.util.xmlb.annotations.MapAnnotation
+import com.intellij.util.xmlb.annotations.Property
+import software.amazon.awssdk.core.SdkClient
+import software.aws.toolkits.core.TokenConnectionSettings
+import software.aws.toolkits.core.utils.debug
+import software.aws.toolkits.core.utils.getLogger
+import software.aws.toolkits.core.utils.tryOrNull
+import software.aws.toolkits.core.utils.warn
+import software.aws.toolkits.jetbrains.core.AwsClientManager
+import software.aws.toolkits.jetbrains.core.AwsResourceCache
+import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection
+import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
+import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
+import software.aws.toolkits.jetbrains.core.credentials.sono.isSono
+import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenAuthState
+import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProviderListener
+import software.aws.toolkits.jetbrains.core.region.AwsRegionProvider
+import software.aws.toolkits.jetbrains.utils.notifyInfo
+import software.aws.toolkits.resources.AmazonQBundle.message
+import software.aws.toolkits.telemetry.MetricResult
+import software.aws.toolkits.telemetry.Telemetry
+import java.time.Duration
+import java.util.Collections
+import kotlin.reflect.KClass
+
+@Service(Service.Level.APP)
+@State(name = "qProfileStates", storages = [Storage("aws.xml")])
+class QRegionProfileManager : PersistentStateComponent, Disposable {
+
+ // Map to store connectionId to its active profile
+ private val connectionIdToActiveProfile = Collections.synchronizedMap(mutableMapOf())
+ private val connectionIdToProfileCount = mutableMapOf()
+
+ init {
+ ApplicationManager.getApplication().messageBus.connect(this)
+ .subscribe(
+ BearerTokenProviderListener.TOPIC,
+ object : BearerTokenProviderListener {
+ override fun invalidate(providerId: String) {
+ connectionIdToActiveProfile.remove(providerId)
+ connectionIdToProfileCount.remove(providerId)
+ }
+ }
+ )
+ }
+
+ // should be call on project startup to validate if profile is still active
+ @RequiresBackgroundThread
+ fun validateProfile(project: Project) {
+ val conn = getIdcConnectionOrNull(project)
+ val selected = activeProfile(project) ?: return
+ val profiles = tryOrNull {
+ listRegionProfiles(project)
+ }
+
+ if (profiles == null || profiles.none { it.arn == selected.arn }) {
+ invalidateProfile(selected.arn)
+ switchProfile(project, null, intent = QProfileSwitchIntent.Reload)
+ Telemetry.amazonq.profileState.use { span ->
+ span.source(QProfileSwitchIntent.Reload.value)
+ .amazonQProfileRegion(selected.region)
+ .ssoRegion(conn?.region)
+ .credentialStartUrl(conn?.startUrl)
+ .result(MetricResult.Failed)
+ }
+ }
+ }
+
+ fun listRegionProfiles(project: Project): List? {
+ val connection = getIdcConnectionOrNull(project) ?: return null
+ return try {
+ val connectionSettings = connection.getConnectionSettings()
+ val mappedProfiles = AwsResourceCache.getInstance().getResourceNow(
+ resource = QProfileResources.LIST_REGION_PROFILES,
+ connectionSettings = connectionSettings,
+ timeout = Duration.ofSeconds(30),
+ useStale = true,
+ forceFetch = false
+ )
+ if (mappedProfiles.size == 1) {
+ switchProfile(project, mappedProfiles.first(), intent = QProfileSwitchIntent.Update)
+ }
+ mappedProfiles.takeIf { it.isNotEmpty() }?.also {
+ connectionIdToProfileCount[connection.id] = it.size
+ } ?: error("You don't have access to the resource")
+ } catch (e: Exception) {
+ LOG.warn(e) { "Failed to list region profiles: ${e.message}" }
+ throw e
+ }
+ }
+
+ fun activeProfile(project: Project): QRegionProfile? = getIdcConnectionOrNull(project)?.let { connectionIdToActiveProfile[it.id] }
+
+ fun hasValidConnectionButNoActiveProfile(project: Project): Boolean = getIdcConnectionOrNull(project) != null && activeProfile(project) == null
+
+ fun switchProfile(project: Project, newProfile: QRegionProfile?, intent: QProfileSwitchIntent) {
+ val conn = getIdcConnectionOrNull(project) ?: return
+
+ val oldProfile = connectionIdToActiveProfile[conn.id]
+ if (oldProfile == newProfile) return
+
+ connectionIdToActiveProfile[conn.id] = newProfile
+ LOG.debug { "Switch from profile $oldProfile to $newProfile for project ${project.name}" }
+
+ if (newProfile != null) {
+ if (intent == QProfileSwitchIntent.User || intent == QProfileSwitchIntent.Auth) {
+ notifyInfo(
+ title = message("action.q.profile.usage.text"),
+ content = message("action.q.profile.usage", newProfile.profileName),
+ project = project
+ )
+
+ Telemetry.amazonq.didSelectProfile.use { span ->
+ span.source(intent.value)
+ .amazonQProfileRegion(newProfile.region)
+ .profileCount(connectionIdToProfileCount[conn.id])
+ .ssoRegion(conn.region)
+ .credentialStartUrl(conn.startUrl)
+ .result(MetricResult.Succeeded)
+ }
+ } else {
+ Telemetry.amazonq.profileState.use { span ->
+ span.source(intent.value)
+ .amazonQProfileRegion(newProfile.region)
+ .ssoRegion(conn.region)
+ .credentialStartUrl(conn.startUrl)
+ .result(MetricResult.Succeeded)
+ }
+ }
+ }
+
+ ApplicationManager.getApplication().messageBus
+ .syncPublisher(QRegionProfileSelectedListener.TOPIC)
+ .onProfileSelected(project, newProfile)
+ }
+
+ private fun invalidateProfile(arn: String) {
+ val updated = connectionIdToActiveProfile.filterValues { it.arn != arn }
+ connectionIdToActiveProfile.clear()
+ connectionIdToActiveProfile.putAll(updated)
+ }
+
+ // for each idc connection, user should have a profile, otherwise should show the profile selection error page
+ fun isPendingProfileSelection(project: Project): Boolean = getIdcConnectionOrNull(project)?.let { conn ->
+ val profileCounts = connectionIdToProfileCount[conn.id] ?: 0
+ val activeProfile = connectionIdToActiveProfile[conn.id]
+ profileCounts == 0 || (profileCounts > 1 && activeProfile?.arn.isNullOrEmpty())
+ } ?: false
+
+ fun shouldDisplayProfileInfo(project: Project): Boolean = getIdcConnectionOrNull(project)?.let { conn ->
+ (connectionIdToProfileCount[conn.id] ?: 0) > 1
+ } ?: false
+
+ fun getQClientSettings(project: Project): TokenConnectionSettings {
+ val conn = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance())
+ if (conn !is AwsBearerTokenConnection) {
+ error("not a bearer connection")
+ }
+
+ val settings = conn.getConnectionSettings()
+ val awsRegion = AwsRegionProvider.getInstance()[QEndpoints.Q_DEFAULT_SERVICE_CONFIG.REGION] ?: error("unknown region from Q default service config")
+
+ // TODO: different window should be able to select different profile
+ return activeProfile(project)?.let { profile ->
+ AwsRegionProvider.getInstance()[profile.region]?.let { region ->
+ settings.withRegion(region)
+ }
+ } ?: settings.withRegion(awsRegion)
+ }
+
+ inline fun getQClient(project: Project): T = getQClient(project, T::class)
+
+ fun getQClient(project: Project, sdkClass: KClass): T {
+ val settings = getQClientSettings(project)
+ val client = AwsClientManager.getInstance().getClient(sdkClass, settings)
+ return client
+ }
+
+ fun getIdcConnectionOrNull(project: Project): AwsBearerTokenConnection? {
+ val manager = ToolkitConnectionManager.getInstance(project)
+ val connection = manager.activeConnectionForFeature(QConnection.getInstance()) as? AwsBearerTokenConnection
+ val state = manager.connectionStateForFeature(QConnection.getInstance())
+
+ return if (connection != null && !connection.isSono() && state == BearerTokenAuthState.AUTHORIZED) {
+ connection
+ } else {
+ null
+ }
+ }
+
+ companion object {
+ private val LOG = getLogger()
+ fun getInstance(): QRegionProfileManager = service()
+ }
+
+ override fun dispose() {}
+
+ override fun getState(): QProfileState {
+ val state = QProfileState()
+ state.connectionIdToActiveProfile.putAll(this.connectionIdToActiveProfile)
+ state.connectionIdToProfileList.putAll(this.connectionIdToProfileCount)
+ return state
+ }
+
+ override fun loadState(state: QProfileState) {
+ connectionIdToActiveProfile.clear()
+ connectionIdToActiveProfile.putAll(state.connectionIdToActiveProfile)
+
+ connectionIdToProfileCount.clear()
+ connectionIdToProfileCount.putAll(state.connectionIdToProfileList)
+ }
+}
+
+class QProfileState : BaseState() {
+ @get:Property
+ @get:MapAnnotation
+ val connectionIdToActiveProfile by map()
+
+ @get:Property
+ @get:MapAnnotation
+ val connectionIdToProfileList by map()
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QRegionProfileSelectedListener.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QRegionProfileSelectedListener.kt
new file mode 100644
index 00000000000..e507cd04a13
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/profile/QRegionProfileSelectedListener.kt
@@ -0,0 +1,16 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.profile
+
+import com.intellij.openapi.project.Project
+import com.intellij.util.messages.Topic
+
+interface QRegionProfileSelectedListener {
+ companion object {
+ @Topic.AppLevel
+ val TOPIC = Topic.create("QRegionProfileSelected", QRegionProfileSelectedListener::class.java)
+ }
+
+ fun onProfileSelected(project: Project, profile: QRegionProfile?)
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/project/EncoderServer.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/project/EncoderServer.kt
index 5b61aedf94d..2da14c2dd50 100644
--- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/project/EncoderServer.kt
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/project/EncoderServer.kt
@@ -21,15 +21,13 @@ import com.nimbusds.jwt.JWTClaimsSet
import com.nimbusds.jwt.SignedJWT
import org.apache.commons.codec.digest.DigestUtils
import software.amazon.awssdk.utils.UserHomeDirectoryUtils
-import software.aws.toolkits.core.utils.createParentDirectories
-import software.aws.toolkits.core.utils.exists
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.info
import software.aws.toolkits.core.utils.tryDirOp
import software.aws.toolkits.core.utils.warn
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.extractZipFile
import software.aws.toolkits.jetbrains.services.amazonq.project.manifest.ManifestManager
import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings
-import java.io.FileOutputStream
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
@@ -39,7 +37,6 @@ import java.security.Key
import java.security.SecureRandom
import java.util.Base64
import java.util.concurrent.atomic.AtomicInteger
-import java.util.zip.ZipFile
import javax.crypto.spec.SecretKeySpec
class EncoderServer(val project: Project) : Disposable {
@@ -183,7 +180,7 @@ class EncoderServer(val project: Project) : Disposable {
if (serverContent?.url != null) {
if (validateHash(serverContent.hashes?.first(), HttpRequests.request(serverContent.url).readBytes(null))) {
downloadFromRemote(serverContent.url, zipFilePath)
- unzipFile(zipFilePath, cachePath)
+ extractZipFile(zipFilePath, cachePath)
}
}
} catch (e: Exception) {
@@ -231,26 +228,6 @@ class EncoderServer(val project: Project) : Disposable {
Files.setPosixFilePermissions(filePath, permissions)
}
- private fun unzipFile(zipFilePath: Path, destDir: Path) {
- if (!zipFilePath.exists()) return
- try {
- val zipFile = ZipFile(zipFilePath.toFile())
- zipFile.use { file ->
- file.entries().asSequence()
- .filterNot { it.isDirectory }
- .map { zipEntry ->
- val destPath = destDir.resolve(zipEntry.name)
- destPath.createParentDirectories()
- FileOutputStream(destPath.toFile()).use { targetFile ->
- zipFile.getInputStream(zipEntry).copyTo(targetFile)
- }
- }.toList()
- }
- } catch (e: Exception) {
- logger.warn { "error while unzipping project context artifact: ${e.message}" }
- }
- }
-
private fun downloadFromRemote(url: String, path: Path) {
try {
HttpRequests.request(url).saveToFile(path, null)
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/project/FeatureDevSessionContext.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/project/FeatureDevSessionContext.kt
index e02e1d5bd7a..974ac171b6a 100644
--- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/project/FeatureDevSessionContext.kt
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/project/FeatureDevSessionContext.kt
@@ -18,6 +18,7 @@ import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext
import software.aws.toolkits.jetbrains.services.amazonq.QConstants.MAX_FILE_SIZE_BYTES
+import software.aws.toolkits.jetbrains.utils.getWorkspaceDevFile
import software.aws.toolkits.jetbrains.utils.isWorkspaceDevFile
import software.aws.toolkits.resources.AwsCoreBundle
import software.aws.toolkits.telemetry.AmazonqTelemetry
@@ -57,11 +58,8 @@ open class FeatureDevSessionContext(val project: Project, val maxProjectSizeByte
private var _selectionRoot = workspaceRoot
- // This function checks for existence of `devfile.yaml` in customer's repository, currently only `devfile.yaml` is supported for this feature.
- fun checkForDevFile(): Boolean {
- val devFile = File(addressableRoot.toString(), "devfile.yaml")
- return devFile.exists()
- }
+ fun hasDevFile(): Boolean =
+ getWorkspaceDevFile(addressableRoot) != null
fun getProjectZip(isAutoBuildFeatureEnabled: Boolean?): ZipCreationResult {
val zippedProject = runBlocking {
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/project/ProjectContextProvider.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/project/ProjectContextProvider.kt
index cb9d8b9362a..854124a180f 100644
--- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/project/ProjectContextProvider.kt
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/project/ProjectContextProvider.kt
@@ -70,12 +70,12 @@ class ProjectContextProvider(val project: Project, private val encoderServer: En
data class FileCollectionResult(
val files: List,
- val fileSize: Int,
+ val fileSize: Int, // in MB
)
// TODO: move to LspMessage.kt
+ @JsonIgnoreProperties(ignoreUnknown = true)
data class Usage(
- @JsonIgnoreProperties(ignoreUnknown = true)
@JsonProperty("memoryUsage")
val memoryUsage: Int? = null,
@JsonProperty("cpuUsage")
@@ -83,8 +83,8 @@ class ProjectContextProvider(val project: Project, private val encoderServer: En
)
// TODO: move to LspMessage.kt
+ @JsonIgnoreProperties(ignoreUnknown = true)
data class Chunk(
- @JsonIgnoreProperties(ignoreUnknown = true)
@JsonProperty("filePath")
val filePath: String? = null,
@JsonProperty("content")
@@ -116,10 +116,14 @@ class ProjectContextProvider(val project: Project, private val encoderServer: En
logger.info { "project context index starting" }
delay(300)
val isIndexSuccess = index()
- if (isIndexSuccess) isIndexComplete.set(true)
+ if (isIndexSuccess) {
+ isIndexComplete.set(true)
+ }
return
}
+ retryCount.incrementAndGet()
} catch (e: Exception) {
+ logger.warn(e) { "failed to init project context" }
if (e.stackTraceToString().contains("Connection refused")) {
retryCount.incrementAndGet()
delay(10000)
@@ -133,6 +137,7 @@ class ProjectContextProvider(val project: Project, private val encoderServer: En
private suspend fun initEncryption(): Boolean {
val request = encoderServer.getEncryptionRequest()
val response = sendMsgToLsp(LspMessage.Initialize, request)
+ logger.info { "received response to init encryption: $response" }
return response?.responseCode == 200
}
@@ -241,59 +246,7 @@ class ProjectContextProvider(val project: Project, private val encoderServer: En
}
}
- private fun willExceedPayloadLimit(currentTotalFileSize: Long, currentFileSize: Long): Boolean {
- val maxSize = CodeWhispererSettings.getInstance().getProjectContextIndexMaxSize()
- return currentTotalFileSize.let { totalSize -> totalSize > (maxSize * 1024 * 1024 - currentFileSize) }
- }
-
- private fun isBuildOrBin(fileName: String): Boolean {
- val regex = Regex("""bin|build|node_modules|venv|\.venv|env|\.idea|\.conda""", RegexOption.IGNORE_CASE)
- return regex.find(fileName) != null
- }
-
- fun collectFiles(): FileCollectionResult {
- val collectedFiles = mutableListOf()
- var currentTotalFileSize = 0L
- val allFiles = mutableListOf()
-
- val projectBaseDirectories = project.getBaseDirectories()
- val changeListManager = ChangeListManager.getInstance(project)
-
- projectBaseDirectories.forEach {
- VfsUtilCore.visitChildrenRecursively(
- it,
- object : VirtualFileVisitor(NO_FOLLOW_SYMLINKS) {
- // TODO: refactor this along with /dev & codescan file traversing logic
- override fun visitFile(file: VirtualFile): Boolean {
- if ((file.isDirectory && isBuildOrBin(file.name)) ||
- !isWorkspaceSourceContent(file, projectBaseDirectories, changeListManager, additionalGlobalIgnoreRulesForStrictSources) ||
- (file.isFile && file.length > 10 * 1024 * 1024)
- ) {
- return false
- }
- if (file.isFile) {
- allFiles.add(file)
- return false
- }
- return true
- }
- }
- )
- }
-
- for (file in allFiles) {
- if (willExceedPayloadLimit(currentTotalFileSize, file.length)) {
- break
- }
- collectedFiles.add(file.path)
- currentTotalFileSize += file.length
- }
-
- return FileCollectionResult(
- files = collectedFiles.toList(),
- fileSize = (currentTotalFileSize / 1024 / 1024).toInt()
- )
- }
+ fun collectFiles(): FileCollectionResult = collectFiles(project.getBaseDirectories(), ChangeListManager.getInstance(project))
private fun queryResultToRelevantDocuments(queryResult: List): List {
val documents: MutableList = mutableListOf()
@@ -315,12 +268,12 @@ class ProjectContextProvider(val project: Project, private val encoderServer: En
}
private suspend fun sendMsgToLsp(msgType: LspMessage, request: String?): LspResponse? {
- logger.info { "sending message: ${msgType.endpoint} to lsp on port ${encoderServer.port}" }
- val url = URI("http://127.0.0.1:${encoderServer.port}/${msgType.endpoint}").toURL()
if (!encoderServer.isNodeProcessRunning()) {
logger.warn { "language server for ${project.name} is not running" }
return null
}
+ logger.info { "sending message: ${msgType.endpoint} to lsp on port ${encoderServer.port}" }
+ val url = URI("http://127.0.0.1:${encoderServer.port}/${msgType.endpoint}").toURL()
// use 1h as timeout for index, 5 seconds for other APIs
val timeoutMs = if (msgType is LspMessage.Index) 60.minutes.inWholeMilliseconds.toInt() else 5000
// dedicate single thread to index operation because it can be long running
@@ -353,5 +306,57 @@ class ProjectContextProvider(val project: Project, private val encoderServer: En
companion object {
private val logger = getLogger()
+ private val regex = Regex("""bin|build|node_modules|venv|\.venv|env|\.idea|\.conda""", RegexOption.IGNORE_CASE)
+ private val mega = (1024 * 1024).toULong()
+ private val tenMb = 10 * mega.toInt()
+
+ private fun willExceedPayloadLimit(maxSize: ULong, currentTotalFileSize: ULong, currentFileSize: Long) =
+ currentTotalFileSize.let { totalSize -> totalSize > (maxSize - currentFileSize.toUInt()) }
+
+ private fun isBuildOrBin(fileName: String): Boolean =
+ regex.find(fileName) != null
+
+ fun collectFiles(projectBaseDirectories: Set, changeListManager: ChangeListManager): FileCollectionResult {
+ val maxSize = CodeWhispererSettings.getInstance()
+ .getProjectContextIndexMaxSize().toULong() * mega
+ val collectedFiles = mutableListOf()
+ var currentTotalFileSize = 0UL
+ val allFiles = mutableListOf()
+
+ projectBaseDirectories.forEach {
+ VfsUtilCore.visitChildrenRecursively(
+ it,
+ object : VirtualFileVisitor(NO_FOLLOW_SYMLINKS) {
+ // TODO: refactor this along with /dev & codescan file traversing logic
+ override fun visitFile(file: VirtualFile): Boolean {
+ if ((file.isDirectory && isBuildOrBin(file.name)) ||
+ !isWorkspaceSourceContent(file, projectBaseDirectories, changeListManager, additionalGlobalIgnoreRulesForStrictSources) ||
+ (file.isFile && file.length > tenMb)
+ ) {
+ return false
+ }
+ if (file.isFile) {
+ allFiles.add(file)
+ return false
+ }
+ return true
+ }
+ }
+ )
+ }
+
+ for (file in allFiles) {
+ if (willExceedPayloadLimit(maxSize, currentTotalFileSize, file.length)) {
+ break
+ }
+ collectedFiles.add(file.path)
+ currentTotalFileSize += file.length.toUInt()
+ }
+
+ return FileCollectionResult(
+ files = collectedFiles.toList(),
+ fileSize = (currentTotalFileSize / 1024u / 1024u).toInt()
+ )
+ }
}
}
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/project/manifest/ManifestManager.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/project/manifest/ManifestManager.kt
index 52b95acf976..484c319db52 100644
--- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/project/manifest/ManifestManager.kt
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/project/manifest/ManifestManager.kt
@@ -3,8 +3,8 @@
package software.aws.toolkits.jetbrains.services.amazonq.project.manifest
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.intellij.openapi.util.SystemInfo
@@ -15,13 +15,12 @@ import software.aws.toolkits.jetbrains.core.getTextFromUrl
class ManifestManager {
private val cloudFrontUrl = "https://aws-toolkit-language-servers.amazonaws.com/q-context/manifest.json"
- val currentVersion = "0.1.46"
+ val currentVersion = "0.1.49"
val currentOs = getOs()
private val arch = CpuArch.CURRENT
- private val mapper = jacksonObjectMapper()
+ private val mapper = jacksonObjectMapper().apply { configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) }
data class TargetContent(
- @JsonIgnoreProperties(ignoreUnknown = true)
@JsonProperty("filename")
val filename: String? = null,
@JsonProperty("url")
@@ -33,7 +32,6 @@ class ManifestManager {
)
data class VersionTarget(
- @JsonIgnoreProperties(ignoreUnknown = true)
@JsonProperty("platform")
val platform: String? = null,
@JsonProperty("arch")
@@ -41,8 +39,8 @@ class ManifestManager {
@JsonProperty("contents")
val contents: List? = emptyList(),
)
+
data class Version(
- @JsonIgnoreProperties(ignoreUnknown = true)
@JsonProperty("serverVersion")
val serverVersion: String? = null,
@JsonProperty("isDelisted")
@@ -52,7 +50,6 @@ class ManifestManager {
)
data class Manifest(
- @JsonIgnoreProperties(ignoreUnknown = true)
@JsonProperty("manifestSchemaVersion")
val manifestSchemaVersion: String? = null,
@JsonProperty("artifactId")
@@ -67,7 +64,7 @@ class ManifestManager {
fun getManifest(): Manifest? = fetchFromRemoteAndSave()
- private fun readManifestFile(content: String): Manifest? {
+ fun readManifestFile(content: String): Manifest? {
try {
return mapper.readValue(content)
} catch (e: Exception) {
diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererColorUtil.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererColorUtil.kt
similarity index 91%
rename from plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererColorUtil.kt
rename to plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererColorUtil.kt
index a4847345d99..4385555c01d 100644
--- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererColorUtil.kt
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererColorUtil.kt
@@ -21,6 +21,6 @@ object CodeWhispererColorUtil {
val EDITOR_CODE_REFERENCE_HOVER = JBColor(0x4B4D4D, 0x4B4D4D)
val INACTIVE_TEXT_COLOR = UIUtil.getInactiveTextColor().getHexString()
val TRY_EXAMPLE_EVEN_ROW_COLOR = JBColor(0xCACACA, 0x252525)
-
- fun Color.getHexString() = String.format("#%02x%02x%02x", this.red, this.green, this.blue)
}
+
+fun Color.getHexString() = String.format(null, "#%02x%02x%02x", this.red, this.green, this.blue)
diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/CodeWhispererSettings.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/CodeWhispererSettings.kt
index 45b96113dcc..2c5cac62e57 100644
--- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/CodeWhispererSettings.kt
+++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/CodeWhispererSettings.kt
@@ -10,7 +10,9 @@ import com.intellij.openapi.components.Service
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.openapi.components.service
+import com.intellij.openapi.project.ProjectManager
import com.intellij.util.xmlb.annotations.Property
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService
import software.aws.toolkits.jetbrains.utils.notifyInfo
import software.aws.toolkits.resources.AmazonQBundle
@@ -21,6 +23,13 @@ class CodeWhispererSettings : PersistentStateComponent {
+ private var state = LspConfiguration()
+
+ override fun getState(): LspConfiguration = state
+
+ override fun loadState(state: LspConfiguration) {
+ this.state = state
+ }
+
+ fun getArtifactPath() = state.artifactPath
+
+ fun setArtifactPath(artifactPath: String?) {
+ state.artifactPath = artifactPath.nullize(nullizeSpaces = true)
+ }
+
+ companion object {
+ fun getInstance(): LspSettings = service()
+ }
+}
+
+class LspConfiguration : BaseState() {
+ var artifactPath by string()
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImplTest.kt b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImplTest.kt
new file mode 100644
index 00000000000..83131a45cc9
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImplTest.kt
@@ -0,0 +1,135 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+@file:Suppress("BannedImports")
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp
+
+import com.google.gson.Gson
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.Project
+import com.intellij.testFramework.ApplicationExtension
+import io.mockk.every
+import io.mockk.mockk
+import org.assertj.core.api.Assertions.assertThat
+import org.eclipse.lsp4j.ConfigurationItem
+import org.eclipse.lsp4j.ConfigurationParams
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection
+import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
+import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.ConnectionMetadata
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.SsoProfileData
+import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings
+
+@ExtendWith(ApplicationExtension::class)
+class AmazonQLanguageClientImplTest {
+ private val project: Project = mockk(relaxed = true)
+ private val sut = AmazonQLanguageClientImpl(project)
+
+ @Test
+ fun `getConnectionMetadata returns connection metadata with start URL for bearer token connection`() {
+ val mockConnectionManager = mockk()
+ every { project.service() } returns mockConnectionManager
+
+ val expectedStartUrl = "https://test.aws.com"
+ val mockConnection = mockk {
+ every { startUrl } returns expectedStartUrl
+ }
+
+ every { mockConnectionManager.activeConnectionForFeature(QConnection.getInstance()) } returns mockConnection
+
+ assertThat(sut.getConnectionMetadata().get())
+ .isEqualTo(ConnectionMetadata(SsoProfileData(expectedStartUrl)))
+ }
+
+ @Test
+ fun `getConnectionMetadata returns empty start URL when no active connection`() {
+ val mockConnectionManager = mockk()
+ every { project.service() } returns mockConnectionManager
+
+ every { mockConnectionManager.activeConnectionForFeature(QConnection.getInstance()) } returns null
+
+ assertThat(sut.getConnectionMetadata().get())
+ .isEqualTo(ConnectionMetadata(SsoProfileData(AmazonQLspConstants.AWS_BUILDER_ID_URL)))
+ }
+
+ @Test
+ fun `configuration null if no attributes requested`() {
+ assertThat(sut.configuration(configurationParams()).get()).isNull()
+ }
+
+ @Test
+ fun `configuration for codeWhisperer respects opt-out`() {
+ CodeWhispererSettings.getInstance().toggleMetricOptIn(false)
+ CodeWhispererSettings.getInstance().toggleWorkspaceContextEnabled(true)
+ assertThat(sut.configuration(configurationParams("aws.codeWhisperer")).get())
+ .singleElement()
+ .isEqualTo(
+ CodeWhispererLspConfiguration(
+ shouldShareData = false,
+ shouldShareCodeReferences = false,
+ shouldEnableWorkspaceContext = true
+ )
+ )
+ }
+
+ @Test
+ fun `configuration for codeWhisperer respects opt-in`() {
+ CodeWhispererSettings.getInstance().toggleMetricOptIn(true)
+ CodeWhispererSettings.getInstance().toggleWorkspaceContextEnabled(true)
+ assertThat(sut.configuration(configurationParams("aws.codeWhisperer")).get())
+ .singleElement()
+ .isEqualTo(
+ CodeWhispererLspConfiguration(
+ shouldShareData = true,
+ shouldShareCodeReferences = false,
+ shouldEnableWorkspaceContext = true
+ )
+ )
+ }
+
+ @Test
+ fun `configuration for workspace context respects opt-in`() {
+ CodeWhispererSettings.getInstance().toggleWorkspaceContextEnabled(false)
+ assertThat(sut.configuration(configurationParams("aws.codeWhisperer")).get())
+ .singleElement()
+ .isEqualTo(
+ CodeWhispererLspConfiguration(
+ shouldShareData = true,
+ shouldShareCodeReferences = false,
+ shouldEnableWorkspaceContext = false
+ )
+ )
+ }
+
+ @Test
+ fun `configuration empty if attributes unknown`() {
+ CodeWhispererSettings.getInstance().toggleMetricOptIn(true)
+ assertThat(sut.configuration(configurationParams("something random")).get()).isEmpty()
+ }
+
+ @Test
+ fun `Gson serializes CodeWhispererLspConfiguration serializes correctly`() {
+ val sut = CodeWhispererLspConfiguration(
+ shouldShareData = true,
+ shouldShareCodeReferences = true
+ )
+ assertThat(Gson().toJson(sut)).isEqualToIgnoringWhitespace(
+ """
+ {
+ "shareCodeWhispererContentWithAWS": true,
+ "includeSuggestionsWithCodeReferences": true
+ }
+ """.trimIndent()
+ )
+ }
+
+ private fun configurationParams(vararg attributes: String) = ConfigurationParams(
+ attributes.map {
+ ConfigurationItem().apply {
+ section = it
+ }
+ }
+ )
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelperTest.kt b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelperTest.kt
new file mode 100644
index 00000000000..5fede85b27e
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelperTest.kt
@@ -0,0 +1,237 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts
+
+import com.intellij.openapi.project.Project
+import com.intellij.testFramework.ApplicationExtension
+import com.intellij.util.io.createDirectories
+import com.intellij.util.text.SemVer
+import io.mockk.Runs
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.spyk
+import kotlinx.coroutines.runBlocking
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.junit.jupiter.api.io.TempDir
+import org.mockito.kotlin.mock
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.ArtifactManager.SupportedManifestVersionRange
+import software.aws.toolkits.jetbrains.services.amazonq.project.manifest.ManifestManager
+import java.nio.file.Path
+
+@ExtendWith(ApplicationExtension::class)
+class ArtifactHelperTest {
+ @TempDir
+ lateinit var tempDir: Path
+
+ private lateinit var artifactHelper: ArtifactHelper
+ private lateinit var manifestVersionRanges: SupportedManifestVersionRange
+ private lateinit var mockManifestManager: ManifestManager
+ private lateinit var contents: List
+ private lateinit var mockProject: Project
+
+ @BeforeEach
+ fun setUp() {
+ artifactHelper = ArtifactHelper(tempDir, 3)
+ mockManifestManager = mock()
+ contents = listOf(
+ ManifestManager.TargetContent(
+ filename = "server.zip",
+ hashes = listOf("sha384:1234")
+ )
+ )
+ mockProject = mockk(relaxed = true) {
+ every { basePath } returns tempDir.toString()
+ every { name } returns "TestProject"
+ }
+ }
+
+ @Test
+ fun `removeDelistedVersions removes specified versions`() {
+ val version1Dir = tempDir.resolve("1.0.0").apply { toFile().mkdirs() }
+ val version2Dir = tempDir.resolve("2.0.0").apply { toFile().mkdirs() }
+
+ val delistedVersions = listOf(
+ ManifestManager.Version(serverVersion = "1.0.0")
+ )
+
+ artifactHelper.removeDelistedVersions(delistedVersions)
+
+ assertThat(version1Dir.toFile().exists()).isFalse()
+ assertThat(version2Dir.toFile().exists()).isTrue()
+ }
+
+ @Test
+ fun `deleteOlderLspArtifacts should not delete if there are only two version`() {
+ val version1Dir = tempDir.resolve("1.0.0").apply { toFile().mkdirs() }
+ val version2Dir = tempDir.resolve("1.0.1").apply { toFile().mkdirs() }
+
+ manifestVersionRanges = SupportedManifestVersionRange(
+ startVersion = SemVer("1.0.0", 1, 0, 0),
+ endVersion = SemVer("2.0.0", 2, 0, 0)
+ )
+
+ artifactHelper.deleteOlderLspArtifacts(manifestVersionRanges)
+
+ assertThat(version1Dir.toFile().exists()).isTrue()
+ assertThat(version2Dir.toFile().exists()).isTrue()
+ }
+
+ @Test
+ fun `deleteOlderLspArtifacts should delete if there are more than two versions`() {
+ val version1Dir = tempDir.resolve("1.0.0").apply { toFile().mkdirs() }
+ val version2Dir = tempDir.resolve("1.0.1").apply { toFile().mkdirs() }
+ val version3Dir = tempDir.resolve("1.0.2").apply { toFile().mkdirs() }
+
+ manifestVersionRanges = SupportedManifestVersionRange(
+ startVersion = SemVer("1.0.0", 1, 0, 0),
+ endVersion = SemVer("2.0.0", 2, 0, 0)
+ )
+
+ artifactHelper.deleteOlderLspArtifacts(manifestVersionRanges)
+
+ assertThat(version1Dir.toFile().exists()).isFalse()
+ assertThat(version2Dir.toFile().exists()).isTrue()
+ assertThat(version3Dir.toFile().exists()).isTrue()
+ }
+
+ @Test
+ fun `getAllLocalLspArtifactsWithinManifestRange should return matching folder path`() {
+ tempDir.resolve("1.0.0").createDirectories()
+ tempDir.resolve("1.0.1").createDirectories()
+ tempDir.resolve("1.0.2").createDirectories()
+ manifestVersionRanges = SupportedManifestVersionRange(
+ startVersion = SemVer("1.0.0", 1, 0, 0),
+ endVersion = SemVer("2.0.0", 2, 0, 0)
+ )
+
+ val actualResult = artifactHelper.getAllLocalLspArtifactsWithinManifestRange(manifestVersionRanges)
+ assertThat(actualResult).isNotNull()
+ assertThat(actualResult.size).isEqualTo(3)
+ assertThat(actualResult.first().first.fileName.toString()).isEqualTo("1.0.2")
+ }
+
+ @Test
+ fun `getExistingLspArtifacts should find all the artifacts`() {
+ val version1Dir = tempDir.resolve("1.0.0").apply { toFile().mkdirs() }
+
+ val serverZipPath = version1Dir.resolve("server.zip")
+ serverZipPath.parent.toFile().mkdirs()
+ serverZipPath.toFile().createNewFile()
+
+ val versions = listOf(
+ ManifestManager.Version(serverVersion = "1.0.0")
+ )
+
+ val target = ManifestManager.VersionTarget(contents = contents)
+
+ mockkStatic("software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.LspUtilsKt")
+ every { generateSHA384Hash(any()) } returns "1234"
+
+ val result = artifactHelper.getExistingLspArtifacts(versions, target)
+
+ assertThat(result).isTrue()
+ assertThat(serverZipPath.toFile().exists()).isTrue()
+ version1Dir.toFile().deleteRecursively()
+ }
+
+ @Test
+ fun `getExistingLspArtifacts should return false due to hash mismatch`() {
+ val version1Dir = tempDir.resolve("1.0.0").apply { toFile().mkdirs() }
+
+ val serverZipPath = version1Dir.resolve("server.zip")
+ serverZipPath.parent.toFile().mkdirs()
+ serverZipPath.toFile().createNewFile()
+
+ val versions = listOf(
+ ManifestManager.Version(serverVersion = "1.0.0")
+ )
+
+ val target = ManifestManager.VersionTarget(contents = contents)
+
+ mockkStatic("software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.LspUtilsKt")
+ every { generateSHA384Hash(any()) } returns "1235"
+
+ val result = artifactHelper.getExistingLspArtifacts(versions, target)
+
+ assertThat(result).isFalse()
+ assertThat(serverZipPath.toFile().exists()).isFalse()
+ }
+
+ @Test
+ fun `getExistingLspArtifacts should return false if versions are empty`() {
+ val versions = emptyList()
+ assertThat(artifactHelper.getExistingLspArtifacts(versions, null)).isFalse()
+ }
+
+ @Test
+ fun `getExistingLspArtifacts should return false if target does not have contents`() {
+ val versions = listOf(
+ ManifestManager.Version(serverVersion = "1.0.0")
+ )
+ assertThat(artifactHelper.getExistingLspArtifacts(versions, null)).isFalse()
+ }
+
+ @Test
+ fun `getExistingLspArtifacts should return false if Lsp path does not exist`() {
+ val versions = listOf(
+ ManifestManager.Version(serverVersion = "1.0.0")
+ )
+ assertThat(artifactHelper.getExistingLspArtifacts(versions, null)).isFalse()
+ }
+
+ @Test
+ fun `tryDownloadLspArtifacts should not download artifacts if target does not have contents`() {
+ val versions = listOf(ManifestManager.Version(serverVersion = "2.0.0"))
+ assertThat(runBlocking { artifactHelper.tryDownloadLspArtifacts(mockProject, versions, null) }).isEqualTo(null)
+ assertThat(tempDir.resolve("2.0.0").toFile().exists()).isFalse()
+ }
+
+ @Test
+ fun `tryDownloadLspArtifacts should throw error if failed to download`() {
+ val versions = listOf(ManifestManager.Version(serverVersion = "1.0.0"))
+
+ val spyArtifactHelper = spyk(artifactHelper)
+ every { spyArtifactHelper.downloadLspArtifacts(any(), any()) } returns false
+
+ assertThat(runBlocking { artifactHelper.tryDownloadLspArtifacts(mockProject, versions, null) }).isEqualTo(null)
+ }
+
+ @Test
+ fun `tryDownloadLspArtifacts should throw error after attempts are exhausted`() {
+ val versions = listOf(ManifestManager.Version(serverVersion = "1.0.0"))
+ val target = ManifestManager.VersionTarget(contents = contents)
+ val spyArtifactHelper = spyk(artifactHelper)
+
+ every { spyArtifactHelper.downloadLspArtifacts(any(), any()) } returns true
+ mockkStatic("software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.LspUtilsKt")
+ every { moveFilesFromSourceToDestination(any(), any()) } just Runs
+ every { extractZipFile(any(), any()) } just Runs
+
+ assertThat(runBlocking { artifactHelper.tryDownloadLspArtifacts(mockProject, versions, target) }).isEqualTo(null)
+ }
+
+ @Test
+ fun `validateFileHash should return false if expected hash is null`() {
+ assertThat(artifactHelper.validateFileHash(tempDir, null)).isFalse()
+ }
+
+ @Test
+ fun `validateFileHash should return false if hash did not match`() {
+ mockkStatic("software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.LspUtilsKt")
+ every { generateSHA384Hash(any()) } returns "1234"
+ assertThat(artifactHelper.validateFileHash(tempDir, "1234")).isFalse()
+ }
+
+ @Test
+ fun `validateFileHash should return true if hash matched`() {
+ mockkStatic("software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.LspUtilsKt")
+ every { generateSHA384Hash(any()) } returns "1234"
+ assertThat(artifactHelper.validateFileHash(tempDir, "sha384:1234")).isTrue()
+ }
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactManagerTest.kt b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactManagerTest.kt
new file mode 100644
index 00000000000..4e163900bff
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactManagerTest.kt
@@ -0,0 +1,145 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts
+
+import com.intellij.openapi.project.Project
+import com.intellij.util.text.SemVer
+import io.mockk.Runs
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.spyk
+import io.mockk.verify
+import kotlinx.coroutines.runBlocking
+import org.assertj.core.api.Assertions.assertThatThrownBy
+import org.jetbrains.annotations.TestOnly
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.io.TempDir
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.ArtifactManager.SupportedManifestVersionRange
+import software.aws.toolkits.jetbrains.services.amazonq.project.manifest.ManifestManager
+import java.nio.file.Path
+
+@TestOnly
+class ArtifactManagerTest {
+
+ @TempDir
+ lateinit var tempDir: Path
+
+ private lateinit var artifactHelper: ArtifactHelper
+ private lateinit var artifactManager: ArtifactManager
+ private lateinit var manifestFetcher: ManifestFetcher
+ private lateinit var manifestVersionRanges: SupportedManifestVersionRange
+ private lateinit var mockProject: Project
+
+ @BeforeEach
+ fun setUp() {
+ artifactHelper = spyk(ArtifactHelper(tempDir, 3))
+ manifestFetcher = spyk(ManifestFetcher())
+ manifestVersionRanges = SupportedManifestVersionRange(
+ startVersion = SemVer("1.0.0", 1, 0, 0),
+ endVersion = SemVer("2.0.0", 2, 0, 0)
+ )
+ mockProject = mockk(relaxed = true) {
+ every { basePath } returns tempDir.toString()
+ every { name } returns "TestProject"
+ }
+ artifactManager = ArtifactManager(mockProject, manifestFetcher, artifactHelper, manifestVersionRanges)
+ }
+
+ @Test
+ fun `fetch artifact fetcher throws exception if manifest is null`() {
+ every { manifestFetcher.fetch() }.returns(null)
+
+ assertThatThrownBy {
+ runBlocking { artifactManager.fetchArtifact() }
+ }
+ .isInstanceOf(LspException::class.java)
+ .hasFieldOrPropertyWithValue("errorCode", LspException.ErrorCode.MANIFEST_FETCH_FAILED)
+ }
+
+ @Test
+ fun `fetch artifact does not have any valid lsp versions`() {
+ every { manifestFetcher.fetch() }.returns(ManifestManager.Manifest())
+ artifactManager = spyk(ArtifactManager(mockProject, manifestFetcher, artifactHelper, manifestVersionRanges))
+
+ every { artifactManager.getLSPVersionsFromManifestWithSpecifiedRange(any()) }.returns(
+ ArtifactManager.LSPVersions(deListedVersions = emptyList(), inRangeVersions = emptyList())
+ )
+
+ assertThatThrownBy {
+ runBlocking { artifactManager.fetchArtifact() }
+ }
+ .isInstanceOf(LspException::class.java)
+ .hasFieldOrPropertyWithValue("errorCode", LspException.ErrorCode.NO_COMPATIBLE_LSP_VERSION)
+ }
+
+ @Test
+ fun `fetch artifact if inRangeVersions are not available should fallback to local lsp`() {
+ val expectedResult = listOf(Pair(tempDir, SemVer("1.0.0", 1, 0, 0)))
+
+ every { manifestFetcher.fetch() }.returns(ManifestManager.Manifest())
+ every { artifactHelper.getAllLocalLspArtifactsWithinManifestRange(any()) }.returns(expectedResult)
+
+ runBlocking { artifactManager.fetchArtifact() }
+
+ verify(exactly = 1) { manifestFetcher.fetch() }
+ verify(exactly = 1) { artifactHelper.getAllLocalLspArtifactsWithinManifestRange(any()) }
+ }
+
+ @Test
+ fun `fetch artifact have valid version in local system`() {
+ val target = ManifestManager.VersionTarget(platform = "temp", arch = "temp")
+ val versions = listOf(ManifestManager.Version("1.0.0", targets = listOf(target)))
+
+ artifactManager = spyk(ArtifactManager(mockProject, manifestFetcher, artifactHelper, manifestVersionRanges))
+
+ every { artifactManager.getLSPVersionsFromManifestWithSpecifiedRange(any()) }.returns(
+ ArtifactManager.LSPVersions(deListedVersions = emptyList(), inRangeVersions = versions)
+ )
+ every { manifestFetcher.fetch() }.returns(ManifestManager.Manifest())
+
+ mockkStatic("software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.LspUtilsKt")
+ every { getCurrentOS() }.returns("temp")
+ every { getCurrentArchitecture() }.returns("temp")
+
+ every { artifactHelper.getExistingLspArtifacts(any(), any()) }.returns(false)
+ coEvery { artifactHelper.tryDownloadLspArtifacts(any(), any(), any()) } returns tempDir
+ every { artifactHelper.deleteOlderLspArtifacts(any()) } just Runs
+
+ runBlocking { artifactManager.fetchArtifact() }
+
+ verify(exactly = 1) { runBlocking { artifactHelper.tryDownloadLspArtifacts(any(), any(), any()) } }
+ verify(exactly = 1) { artifactHelper.deleteOlderLspArtifacts(any()) }
+ }
+
+ @Test
+ fun `fetch artifact does not have valid version in local system`() {
+ val target = ManifestManager.VersionTarget(platform = "temp", arch = "temp")
+ val versions = listOf(ManifestManager.Version("1.0.0", targets = listOf(target)))
+ val expectedResult = listOf(Pair(tempDir, SemVer("1.0.0", 1, 0, 0)))
+
+ artifactManager = spyk(ArtifactManager(mockProject, manifestFetcher, artifactHelper, manifestVersionRanges))
+
+ every { artifactManager.getLSPVersionsFromManifestWithSpecifiedRange(any()) }.returns(
+ ArtifactManager.LSPVersions(deListedVersions = emptyList(), inRangeVersions = versions)
+ )
+ every { manifestFetcher.fetch() }.returns(ManifestManager.Manifest())
+
+ mockkStatic("software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.LspUtilsKt")
+ every { getCurrentOS() }.returns("temp")
+ every { getCurrentArchitecture() }.returns("temp")
+
+ every { artifactHelper.getExistingLspArtifacts(any(), any()) }.returns(true)
+ every { artifactHelper.deleteOlderLspArtifacts(any()) } just Runs
+ every { artifactHelper.getAllLocalLspArtifactsWithinManifestRange(any()) }.returns(expectedResult)
+
+ runBlocking { artifactManager.fetchArtifact() }
+
+ verify(exactly = 0) { runBlocking { artifactHelper.tryDownloadLspArtifacts(any(), any(), any()) } }
+ verify(exactly = 1) { artifactHelper.deleteOlderLspArtifacts(any()) }
+ }
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/LspUtilsTest.kt b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/LspUtilsTest.kt
new file mode 100644
index 00000000000..607b94f34bf
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/LspUtilsTest.kt
@@ -0,0 +1,103 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts
+
+import com.intellij.testFramework.utils.io.createDirectory
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Assumptions.assumeTrue
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.io.TempDir
+import software.aws.toolkits.core.utils.ZIP_PROPERTY_POSIX
+import software.aws.toolkits.core.utils.hasPosixFilePermissions
+import software.aws.toolkits.core.utils.putNextEntry
+import software.aws.toolkits.core.utils.test.assertPosixPermissions
+import software.aws.toolkits.core.utils.writeText
+import software.aws.toolkits.jetbrains.utils.satisfiesKt
+import java.nio.file.FileSystems
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.StandardCopyOption
+import java.nio.file.attribute.PosixFilePermissions
+import java.util.zip.ZipOutputStream
+import kotlin.io.path.isRegularFile
+import kotlin.io.path.setPosixFilePermissions
+
+class LspUtilsTest {
+ @Test
+ fun `extractZipFile works`(@TempDir tempDir: Path) {
+ val source = tempDir.resolve("source").also { it.createDirectory() }
+ val target = tempDir.resolve("target").also { it.createDirectory() }
+
+ source.resolve("file1").writeText("contents1")
+ source.resolve("file2").writeText("contents2")
+ source.resolve("file3").writeText("contents3")
+
+ val sourceZip = tempDir.resolve("source.zip")
+ ZipOutputStream(Files.newOutputStream(sourceZip)).use { zip ->
+ Files.walk(source).use { paths ->
+ paths
+ .filter { it.isRegularFile() }
+ .forEach {
+ zip.putNextEntry(source.relativize(it).toString(), it)
+ }
+ val precedingSlashFile = source.resolve("file4").also { it.writeText("contents4") }
+ zip.putNextEntry("/${source.relativize(precedingSlashFile)}", precedingSlashFile)
+ }
+ }
+
+ extractZipFile(sourceZip, target)
+
+ assertThat(target).satisfiesKt {
+ val files = Files.list(it).use { stream -> stream.toList() }
+ assertThat(files.size).isEqualTo(4)
+ assertThat(target.resolve("file1")).hasContent("contents1")
+ assertThat(target.resolve("file2")).hasContent("contents2")
+ assertThat(target.resolve("file3")).hasContent("contents3")
+ assertThat(target.resolve("file4")).hasContent("contents4")
+ }
+ }
+
+ @Test
+ fun `extractZipFile respects posix`(@TempDir tempDir: Path) {
+ assumeTrue(tempDir.hasPosixFilePermissions())
+
+ val source = tempDir.resolve("source").also { it.createDirectory() }
+ val target = tempDir.resolve("target").also { it.createDirectory() }
+
+ source.resolve("regularFile").also {
+ it.writeText("contents1")
+ it.setPosixFilePermissions(PosixFilePermissions.fromString("rw-r--r--"))
+ }
+ source.resolve("executableFile").also {
+ it.writeText("contents2")
+ it.setPosixFilePermissions(PosixFilePermissions.fromString("rwxr-xr-x"))
+ }
+
+ val sourceZip = tempDir.resolve("source.zip")
+ FileSystems.newFileSystem(
+ sourceZip,
+ mapOf(
+ "create" to true,
+ ZIP_PROPERTY_POSIX to true,
+ )
+ ).use { zipfs ->
+ Files.walk(source).use { paths ->
+ paths
+ .filter { it.isRegularFile() }
+ .forEach { file ->
+ Files.copy(file, zipfs.getPath("/").resolve(source.relativize(file).toString()), StandardCopyOption.COPY_ATTRIBUTES)
+ }
+ }
+ }
+
+ extractZipFile(sourceZip, target)
+
+ assertThat(target).satisfiesKt {
+ val files = Files.list(it).use { stream -> stream.toList() }
+ assertThat(files.size).isEqualTo(2)
+ assertPosixPermissions(target.resolve("regularFile"), "rw-r--r--")
+ assertPosixPermissions(target.resolve("executableFile"), "rwxr-xr-x")
+ }
+ }
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ManifestFetcherTest.kt b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ManifestFetcherTest.kt
new file mode 100644
index 00000000000..b5a1bd32fac
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ManifestFetcherTest.kt
@@ -0,0 +1,122 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts
+
+import com.intellij.testFramework.ApplicationExtension
+import com.intellij.testFramework.utils.io.createFile
+import io.mockk.every
+import io.mockk.junit5.MockKExtension
+import io.mockk.mockkStatic
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.junit.jupiter.api.io.TempDir
+import org.mockito.junit.jupiter.MockitoExtension
+import org.mockito.kotlin.atLeastOnce
+import org.mockito.kotlin.never
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import software.aws.toolkits.jetbrains.core.getTextFromUrl
+import software.aws.toolkits.jetbrains.services.amazonq.project.manifest.ManifestManager
+import java.nio.file.Path
+import java.nio.file.Paths
+
+@ExtendWith(ApplicationExtension::class, MockitoExtension::class, MockKExtension::class)
+class ManifestFetcherTest {
+
+ private lateinit var manifestFetcher: ManifestFetcher
+ private lateinit var manifest: ManifestManager.Manifest
+ private lateinit var manifestManager: ManifestManager
+
+ @BeforeEach
+ fun setup() {
+ manifestFetcher = spy(ManifestFetcher())
+ manifestManager = spy(ManifestManager())
+ manifest = ManifestManager.Manifest()
+ }
+
+ @Test
+ fun `should return null when both local and remote manifests are null`() {
+ whenever(manifestFetcher.fetchManifestFromLocal()).thenReturn(null)
+ whenever(manifestFetcher.fetchManifestFromRemote()).thenReturn(null)
+
+ assertThat(manifestFetcher.fetch()).isNull()
+ }
+
+ @Test
+ fun `should return valid result from local should not execute remote method`() {
+ whenever(manifestFetcher.fetchManifestFromLocal()).thenReturn(manifest)
+
+ assertThat(manifestFetcher.fetch()).isNotNull().isEqualTo(manifest)
+ verify(manifestFetcher, atLeastOnce()).fetchManifestFromLocal()
+ verify(manifestFetcher, never()).fetchManifestFromRemote()
+ }
+
+ @Test
+ fun `should return valid result from remote`() {
+ whenever(manifestFetcher.fetchManifestFromLocal()).thenReturn(null)
+ whenever(manifestFetcher.fetchManifestFromRemote()).thenReturn(manifest)
+
+ assertThat(manifestFetcher.fetch()).isNotNull().isEqualTo(manifest)
+ verify(manifestFetcher, atLeastOnce()).fetchManifestFromLocal()
+ verify(manifestFetcher, atLeastOnce()).fetchManifestFromRemote()
+ }
+
+ @Test
+ fun `fetchManifestFromRemote should return null due to invalid manifestString`() {
+ mockkStatic("software.aws.toolkits.jetbrains.core.HttpUtilsKt")
+ every { getTextFromUrl(any()) } returns "ManifestContent"
+
+ assertThat(manifestFetcher.fetchManifestFromRemote()).isNull()
+ }
+
+ @Test
+ fun `fetchManifestFromRemote should return manifest and update manifest`() {
+ val validManifest = ManifestManager.Manifest(manifestSchemaVersion = "1.0")
+ mockkStatic("software.aws.toolkits.jetbrains.core.HttpUtilsKt")
+
+ every { getTextFromUrl(any()) } returns "{ \"manifestSchemaVersion\": \"1.0\" }"
+
+ val result = manifestFetcher.fetchManifestFromRemote()
+ assertThat(result).isNotNull().isEqualTo(validManifest)
+ }
+
+ @Test
+ fun `fetchManifestFromRemote should return null if manifest is deprecated`() {
+ mockkStatic("software.aws.toolkits.jetbrains.core.HttpUtilsKt")
+ every { getTextFromUrl(any()) } returns
+ // language=JSON
+ """
+ {
+ "manifestSchemaVersion": "1.0",
+ "isManifestDeprecated": true
+ }
+ """.trimIndent()
+
+ assertThat(manifestFetcher.fetchManifestFromRemote()).isNull()
+ }
+
+ @Test
+ fun `fetchManifestFromLocal should return null if path does not exist locally`() {
+ whenever(manifestFetcher.lspManifestFilePath).thenReturn(Paths.get("does", "not", "exist"))
+ assertThat(manifestFetcher.fetchManifestFromLocal()).isNull()
+ }
+
+ @Test
+ fun `fetchManifestFromLocal should return local path if exists locally`(@TempDir tempDir: Path) {
+ val manifestFile = tempDir.createFile("manifest.json")
+ manifestFile.toFile().writeText(
+ // language=JSON
+ """
+ {
+ "manifestSchemaVersion": "1.0"
+ }
+ """.trimIndent()
+ )
+ whenever(manifestFetcher.lspManifestFilePath).thenReturn(manifestFile)
+ assertThat(manifestFetcher.fetchManifestFromLocal()).isNull()
+ }
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/auth/DefaultAuthCredentialsServiceTest.kt b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/auth/DefaultAuthCredentialsServiceTest.kt
new file mode 100644
index 00000000000..d141268d2c3
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/auth/DefaultAuthCredentialsServiceTest.kt
@@ -0,0 +1,253 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.auth
+
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.components.service
+import com.intellij.openapi.components.serviceIfCreated
+import com.intellij.openapi.project.Project
+import com.intellij.testFramework.ProjectExtension
+import com.intellij.util.messages.MessageBus
+import com.intellij.util.messages.MessageBusConnection
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.runs
+import io.mockk.spyk
+import io.mockk.verify
+import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.RegisterExtension
+import software.aws.toolkits.core.TokenConnectionSettings
+import software.aws.toolkits.core.credentials.ToolkitBearerTokenProvider
+import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection
+import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
+import software.aws.toolkits.jetbrains.core.credentials.sso.PKCEAuthorizationGrantToken
+import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.InteractiveBearerTokenProvider
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLanguageServer
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.encryption.JwtEncryptionManager
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.UpdateCredentialsPayload
+import software.aws.toolkits.jetbrains.utils.isQConnected
+import software.aws.toolkits.jetbrains.utils.isQExpired
+import java.time.Instant
+import java.util.concurrent.CompletableFuture
+
+class DefaultAuthCredentialsServiceTest {
+ companion object {
+ @JvmField
+ @RegisterExtension
+ val projectExtension = ProjectExtension()
+
+ private const val TEST_ACCESS_TOKEN = "test-access-token"
+ }
+
+ private lateinit var project: Project
+ private lateinit var mockLanguageServer: AmazonQLanguageServer
+ private lateinit var mockEncryptionManager: JwtEncryptionManager
+ private lateinit var mockConnectionManager: ToolkitConnectionManager
+ private lateinit var mockConnection: AwsBearerTokenConnection
+ private lateinit var sut: DefaultAuthCredentialsService
+
+ @BeforeEach
+ fun setUp() {
+ project = spyk(projectExtension.project)
+ setupMockLspService()
+ setupMockMessageBus()
+ setupMockConnectionManager()
+ }
+
+ private fun setupMockLspService() {
+ mockLanguageServer = mockk()
+ mockEncryptionManager = mockk {
+ every { encrypt(any()) } returns "mock-encrypted-data"
+ }
+
+ val mockLspService = mockk()
+ every {
+ mockLspService.executeSync>(any())
+ } coAnswers {
+ val func = firstArg CompletableFuture>()
+ func.invoke(mockLspService, mockLanguageServer)
+ }
+
+ every {
+ mockLanguageServer.updateTokenCredentials(any())
+ } returns CompletableFuture()
+
+ every {
+ mockLanguageServer.deleteTokenCredentials()
+ } returns CompletableFuture.completedFuture(Unit)
+
+ every { project.getService(AmazonQLspService::class.java) } returns mockLspService
+ every { project.serviceIfCreated() } returns mockLspService
+ }
+
+ private fun setupMockMessageBus() {
+ val messageBus = mockk()
+ val mockConnection = mockk {
+ every { subscribe(any(), any()) } just runs
+ }
+ every { project.messageBus } returns messageBus
+ every { messageBus.connect(any()) } returns mockConnection
+ }
+
+ private fun setupMockConnectionManager(accessToken: String = TEST_ACCESS_TOKEN) {
+ mockConnection = createMockConnection(accessToken)
+ mockConnectionManager = mockk {
+ every { activeConnectionForFeature(any()) } returns mockConnection
+ }
+ every { project.service() } returns mockConnectionManager
+ mockkStatic("software.aws.toolkits.jetbrains.utils.FunctionUtilsKt")
+ // these set so init doesn't always emit
+ every { isQConnected(any()) } returns false
+ every { isQExpired(any()) } returns true
+ }
+
+ private fun createMockConnection(
+ accessToken: String,
+ connectionId: String = "test-connection-id",
+ ): AwsBearerTokenConnection = mockk {
+ every { id } returns connectionId
+ every { getConnectionSettings() } returns createMockTokenSettings(accessToken)
+ }
+
+ private fun createMockTokenSettings(accessToken: String): TokenConnectionSettings {
+ val token = PKCEAuthorizationGrantToken(
+ issuerUrl = "https://example.com",
+ refreshToken = "refreshToken",
+ accessToken = accessToken,
+ expiresAt = Instant.MAX,
+ createdAt = Instant.now(),
+ region = "us-fake-1",
+ )
+
+ val tokenDelegate = mockk {
+ every { currentToken() } returns token
+ }
+
+ val provider = mockk {
+ every { delegate } returns tokenDelegate
+ }
+
+ return mockk {
+ every { tokenProvider } returns provider
+ }
+ }
+
+ @Test
+ fun `activeConnectionChanged updates token when connection ID matches Q connection`() {
+ sut = DefaultAuthCredentialsService(project, mockEncryptionManager, mockk())
+ val newConnection = createMockConnection("new-token", "connection-id")
+ every { mockConnection.id } returns "connection-id"
+
+ sut.activeConnectionChanged(newConnection)
+
+ verify(exactly = 1) { mockLanguageServer.updateTokenCredentials(any()) }
+ }
+
+ @Test
+ fun `activeConnectionChanged does not update token when connection ID differs`() {
+ sut = DefaultAuthCredentialsService(project, mockEncryptionManager, mockk())
+ val newConnection = createMockConnection("new-token", "different-id")
+ every { mockConnection.id } returns "q-connection-id"
+
+ sut.activeConnectionChanged(newConnection)
+
+ verify(exactly = 0) { mockLanguageServer.updateTokenCredentials(any()) }
+ }
+
+ @Test
+ fun `onChange updates token with new connection`() {
+ sut = DefaultAuthCredentialsService(project, mockEncryptionManager, mockk())
+ setupMockConnectionManager("updated-token")
+
+ sut.onChange("providerId", listOf("new-scope"))
+
+ verify(exactly = 1) { mockLanguageServer.updateTokenCredentials(any()) }
+ }
+
+ @Test
+ fun `init does not update token when Q is not connected`() {
+ every { isQConnected(project) } returns false
+ every { isQExpired(project) } returns false
+
+ sut = DefaultAuthCredentialsService(project, mockEncryptionManager, mockk())
+
+ verify(exactly = 0) { mockLanguageServer.updateTokenCredentials(any()) }
+ }
+
+ @Test
+ fun `init does not update token when Q is expired`() {
+ every { isQConnected(project) } returns true
+ every { isQExpired(project) } returns true
+
+ sut = DefaultAuthCredentialsService(project, mockEncryptionManager, mockk())
+
+ verify(exactly = 0) { mockLanguageServer.updateTokenCredentials(any()) }
+ }
+
+ @Test
+ fun `test updateTokenCredentials unencrypted success`() {
+ sut = DefaultAuthCredentialsService(project, mockEncryptionManager, mockk())
+
+ val token = "unencryptedToken"
+ val isEncrypted = false
+
+ sut.updateTokenCredentials(token, isEncrypted)
+
+ verify(exactly = 1) {
+ mockLanguageServer.updateTokenCredentials(
+ UpdateCredentialsPayload(
+ token,
+ isEncrypted
+ )
+ )
+ }
+ }
+
+ @Test
+ fun `test updateTokenCredentials encrypted success`() {
+ sut = DefaultAuthCredentialsService(project, mockEncryptionManager, mockk())
+
+ val encryptedToken = "encryptedToken"
+ val decryptedToken = "decryptedToken"
+ val isEncrypted = true
+
+ every { mockEncryptionManager.encrypt(any()) } returns encryptedToken
+
+ sut.updateTokenCredentials(decryptedToken, isEncrypted)
+
+ verify(atLeast = 1) {
+ mockLanguageServer.updateTokenCredentials(
+ UpdateCredentialsPayload(
+ encryptedToken,
+ isEncrypted
+ )
+ )
+ }
+ }
+
+ @Test
+ fun `test deleteTokenCredentials success`() {
+ sut = DefaultAuthCredentialsService(project, mockEncryptionManager, mockk())
+
+ every { mockLanguageServer.deleteTokenCredentials() } returns CompletableFuture.completedFuture(Unit)
+
+ sut.deleteTokenCredentials()
+
+ verify(exactly = 1) { mockLanguageServer.deleteTokenCredentials() }
+ }
+
+ @Test
+ fun `init results in token update`() {
+ every { isQConnected(any()) } returns true
+ every { isQExpired(any()) } returns false
+ sut = DefaultAuthCredentialsService(project, mockEncryptionManager, mockk())
+
+ verify(exactly = 1) { mockLanguageServer.updateTokenCredentials(any()) }
+ }
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/dependencies/DefaultModuleDependenciesServiceTest.kt b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/dependencies/DefaultModuleDependenciesServiceTest.kt
new file mode 100644
index 00000000000..618b768a69a
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/dependencies/DefaultModuleDependenciesServiceTest.kt
@@ -0,0 +1,202 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.dependencies
+
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.application.Application
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.components.serviceIfCreated
+import com.intellij.openapi.extensions.ExtensionPointName
+import com.intellij.openapi.module.Module
+import com.intellij.openapi.module.ModuleManager
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.roots.ModuleRootEvent
+import com.intellij.util.messages.MessageBus
+import com.intellij.util.messages.MessageBusConnection
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.mockkObject
+import io.mockk.mockkStatic
+import io.mockk.runs
+import io.mockk.verify
+import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLanguageServer
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.dependencies.ModuleDependencyProvider.Companion.EP_NAME
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.dependencies.DidChangeDependencyPathsParams
+import java.util.concurrent.CompletableFuture
+import java.util.function.Consumer
+
+class DefaultModuleDependenciesServiceTest {
+ private lateinit var project: Project
+ private lateinit var mockLanguageServer: AmazonQLanguageServer
+ private lateinit var mockModuleManager: ModuleManager
+ private lateinit var sut: DefaultModuleDependenciesService
+ private lateinit var mockApplication: Application
+ private lateinit var mockDependencyProvider: ModuleDependencyProvider
+
+ @BeforeEach
+ fun setUp() {
+ project = mockk()
+ mockModuleManager = mockk()
+ mockDependencyProvider = mockk()
+ mockLanguageServer = mockk()
+
+ every { mockLanguageServer.didChangeDependencyPaths(any()) } returns CompletableFuture()
+
+ // Mock Application
+ mockApplication = mockk()
+ mockkStatic(ApplicationManager::class)
+ every { ApplicationManager.getApplication() } returns mockApplication
+
+ // Mock message bus
+ val messageBus = mockk()
+ every { project.messageBus } returns messageBus
+ val mockConnection = mockk()
+ every { messageBus.connect(any()) } returns mockConnection
+ every { mockConnection.subscribe(any(), any()) } just runs
+
+ // Mock ModuleManager
+ mockkStatic(ModuleManager::class)
+ every { ModuleManager.getInstance(project) } returns mockModuleManager
+ every { mockModuleManager.modules } returns Array(0) { mockk() }
+
+ // Mock LSP service
+ val mockLspService = mockk()
+ every { project.getService(AmazonQLspService::class.java) } returns mockLspService
+ every { project.serviceIfCreated() } returns mockLspService
+ every {
+ mockLspService.executeSync>(any())
+ } coAnswers {
+ val func = firstArg CompletableFuture>()
+ func.invoke(mockLspService, mockLanguageServer)
+ }
+
+ // Mock extension point
+ mockkObject(ModuleDependencyProvider.Companion)
+ val epName = mockk>()
+ every { EP_NAME } returns epName
+ every { epName.forEachExtensionSafe(any()) } answers {
+ val callback = firstArg<(ModuleDependencyProvider) -> Unit>()
+ callback(mockDependencyProvider)
+ }
+ }
+
+ @Test
+ fun `test initial sync on construction`() {
+ // Arrange
+ val module = mockk()
+ val params = DidChangeDependencyPathsParams(
+ moduleName = "testModule",
+ runtimeLanguage = "java",
+ paths = listOf("/path/to/dependency.jar"),
+ includePatterns = emptyList(),
+ excludePatterns = emptyList()
+ )
+
+ every { mockModuleManager.modules } returns arrayOf(module)
+ prepDependencyProvider(listOf(Pair(module, params)))
+
+ sut = DefaultModuleDependenciesService(project, mockk())
+
+ verify { mockLanguageServer.didChangeDependencyPaths(params) }
+ }
+
+ @Test
+ fun `test rootsChanged with multiple modules`() {
+ // Arrange
+ val module1 = mockk()
+ val module2 = mockk()
+ val params1 = DidChangeDependencyPathsParams(
+ moduleName = "module1",
+ runtimeLanguage = "java",
+ paths = listOf("/path/to/dependency1.jar"),
+ includePatterns = emptyList(),
+ excludePatterns = emptyList()
+ )
+ val params2 = DidChangeDependencyPathsParams(
+ moduleName = "module2",
+ runtimeLanguage = "python",
+ paths = listOf("/path/to/site-packages/package1"),
+ includePatterns = emptyList(),
+ excludePatterns = emptyList()
+ )
+
+ prepDependencyProvider(
+ listOf(
+ Pair(module1, params1),
+ Pair(module2, params2)
+ )
+ )
+
+ sut = DefaultModuleDependenciesService(project, mockk())
+
+ verify { mockLanguageServer.didChangeDependencyPaths(params1) }
+ verify { mockLanguageServer.didChangeDependencyPaths(params2) }
+ }
+
+ @Test
+ fun `test rootsChanged withFileTypesChange`() {
+ // Arrange
+ val module = mockk()
+ val params = DidChangeDependencyPathsParams(
+ moduleName = "testModule",
+ runtimeLanguage = "java",
+ paths = listOf("/path/to/dependency.jar"),
+ includePatterns = emptyList(),
+ excludePatterns = emptyList()
+ )
+ prepDependencyProvider(listOf(Pair(module, params)))
+ val event = mockk()
+ every { event.isCausedByFileTypesChange } returns true
+
+ sut = DefaultModuleDependenciesService(project, mockk())
+
+ sut.rootsChanged(event)
+
+ verify(exactly = 1) { mockLanguageServer.didChangeDependencyPaths(params) }
+ }
+
+ @Test
+ fun `test rootsChanged after module changes`() {
+ // Arrange
+ val module = mockk()
+ val params = DidChangeDependencyPathsParams(
+ moduleName = "testModule",
+ runtimeLanguage = "java",
+ paths = listOf("/path/to/dependency.jar"),
+ includePatterns = emptyList(),
+ excludePatterns = emptyList()
+ )
+ val event = mockk()
+
+ every { mockModuleManager.modules } returns arrayOf(module)
+ every { event.isCausedByFileTypesChange } returns false
+
+ prepDependencyProvider(listOf(Pair(module, params)))
+
+ sut = DefaultModuleDependenciesService(project, mockk())
+
+ sut.rootsChanged(event)
+
+ verify(exactly = 2) { mockLanguageServer.didChangeDependencyPaths(params) }
+ }
+
+ private fun prepDependencyProvider(moduleParamPairs: List>) {
+ every { mockModuleManager.modules } returns moduleParamPairs.map { it.first }.toTypedArray()
+
+ every {
+ EP_NAME.forEachExtensionSafe(any>())
+ } answers {
+ val consumer = firstArg>()
+ moduleParamPairs.forEach { (module, params) ->
+ every { mockDependencyProvider.isApplicable(module) } returns true
+ every { mockDependencyProvider.createParams(module) } returns params
+ }
+ consumer.accept(mockDependencyProvider)
+ }
+ }
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/encryption/JwtEncryptionManagerTest.kt b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/encryption/JwtEncryptionManagerTest.kt
new file mode 100644
index 00000000000..365fd2759b0
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/encryption/JwtEncryptionManagerTest.kt
@@ -0,0 +1,77 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.encryption
+
+import com.nimbusds.jose.JOSEException
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import java.io.ByteArrayOutputStream
+import java.util.concurrent.atomic.AtomicBoolean
+import javax.crypto.spec.SecretKeySpec
+import kotlin.random.Random
+
+class JwtEncryptionManagerTest {
+ @Test
+ fun `uses a different encryption key for each instance`() {
+ val blob = Random.Default.nextBytes(256)
+ val sut1 = JwtEncryptionManager()
+ val encrypted = sut1.encrypt(blob)
+
+ assertThrows {
+ assertThat(sut1.decrypt(encrypted))
+ .isNotEqualTo(JwtEncryptionManager().decrypt(encrypted))
+ }
+ }
+
+ @Test
+ @OptIn(ExperimentalStdlibApi::class)
+ fun `encryption is stable with static key`() {
+ val blob = Random.Default.nextBytes(256)
+ val bytes = "DEADBEEF".repeat(8).hexToByteArray() // 32 bytes
+ val key = SecretKeySpec(bytes, "HmacSHA256")
+ val sut1 = JwtEncryptionManager(key)
+ val encrypted = sut1.encrypt(blob)
+
+ // each encrypt() call will use a different IV so we can't just directly compare
+ assertThat(sut1.decrypt(encrypted))
+ .isEqualTo(JwtEncryptionManager(key).decrypt(encrypted))
+ }
+
+ @Test
+ fun `encryption can be round-tripped`() {
+ val sut = JwtEncryptionManager()
+ val blob = "DEADBEEF".repeat(8)
+ assertThat(sut.decrypt(sut.encrypt(blob))).isEqualTo(blob)
+ }
+
+ @Test
+ @OptIn(ExperimentalStdlibApi::class)
+ fun writeInitializationPayload() {
+ val bytes = "DEADBEEF".repeat(8).hexToByteArray() // 32 bytes
+ val key = SecretKeySpec(bytes, "HmacSHA256")
+
+ val closed = AtomicBoolean(false)
+ val os = object : ByteArrayOutputStream() {
+ override fun close() {
+ closed.set(true)
+ }
+ }
+ JwtEncryptionManager(key).writeInitializationPayload(os)
+ assertThat(os.toString())
+ // Flare requires encryption ends with new line
+ // https://github.com/aws/language-server-runtimes/blob/4d7f81295dc12b59ed2e1c0ebaedb85ccb86cf76/runtimes/README.md#encryption
+ .endsWith("\n")
+ .isEqualTo(
+ // language=JSON
+ """
+ |{"version":"1.0","mode":"JWT","key":"3q2-796tvu_erb7v3q2-796tvu_erb7v3q2-796tvu8"}
+ |
+ """.trimMargin()
+ )
+
+ // writeInitializationPayload should not close the stream
+ assertThat(closed.get()).isFalse
+ }
+}
diff --git a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/textdocument/TextDocumentServiceHandlerTest.kt b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/textdocument/TextDocumentServiceHandlerTest.kt
new file mode 100644
index 00000000000..96da1fc4318
--- /dev/null
+++ b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/textdocument/TextDocumentServiceHandlerTest.kt
@@ -0,0 +1,341 @@
+// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package software.aws.toolkits.jetbrains.services.amazonq.lsp.textdocument
+
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.application.Application
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.components.serviceIfCreated
+import com.intellij.openapi.editor.Document
+import com.intellij.openapi.fileEditor.FileDocumentManager
+import com.intellij.openapi.fileEditor.FileEditorManager
+import com.intellij.openapi.fileTypes.FileType
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.openapi.vfs.newvfs.events.VFileContentChangeEvent
+import com.intellij.openapi.vfs.newvfs.events.VFileEvent
+import com.intellij.util.messages.MessageBus
+import com.intellij.util.messages.MessageBusConnection
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.mockkObject
+import io.mockk.mockkStatic
+import io.mockk.runs
+import io.mockk.slot
+import io.mockk.verify
+import kotlinx.coroutines.test.runTest
+import org.assertj.core.api.Assertions.assertThat
+import org.eclipse.lsp4j.DidChangeTextDocumentParams
+import org.eclipse.lsp4j.DidCloseTextDocumentParams
+import org.eclipse.lsp4j.DidOpenTextDocumentParams
+import org.eclipse.lsp4j.DidSaveTextDocumentParams
+import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage
+import org.eclipse.lsp4j.services.TextDocumentService
+import org.junit.Before
+import org.junit.Test
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLanguageServer
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService
+import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.FileUriUtil
+import java.net.URI
+import java.nio.file.Path
+import java.util.concurrent.Callable
+import java.util.concurrent.CompletableFuture
+
+class TextDocumentServiceHandlerTest {
+ private lateinit var project: Project
+ private lateinit var mockFileEditorManager: FileEditorManager
+ private lateinit var mockLanguageServer: AmazonQLanguageServer
+ private lateinit var mockTextDocumentService: TextDocumentService
+ private lateinit var sut: TextDocumentServiceHandler
+ private lateinit var mockApplication: Application
+
+ @Before
+ fun setup() {
+ project = mockk()
+ mockTextDocumentService = mockk()
+ mockLanguageServer = mockk()
+
+ mockApplication = mockk()
+ mockkStatic(ApplicationManager::class)
+ every { ApplicationManager.getApplication() } returns mockApplication
+ every { mockApplication.executeOnPooledThread(any>()) } answers {
+ CompletableFuture.completedFuture(firstArg>().call())
+ }
+
+ // Mock the LSP service
+ val mockLspService = mockk()
+
+ // Mock the service methods on Project
+ every { project.getService(AmazonQLspService::class.java) } returns mockLspService
+ every { project.serviceIfCreated() } returns mockLspService
+
+ // Mock the LSP service's executeSync method as a suspend function
+ every {
+ mockLspService.executeSync>(any())
+ } coAnswers {
+ val func = firstArg CompletableFuture>()
+ func.invoke(mockLspService, mockLanguageServer)
+ }
+
+ // Mock workspace service
+ every { mockLanguageServer.textDocumentService } returns mockTextDocumentService
+ every { mockTextDocumentService.didChange(any()) } returns Unit
+ every { mockTextDocumentService.didSave(any()) } returns Unit
+ every { mockTextDocumentService.didOpen(any()) } returns Unit
+ every { mockTextDocumentService.didClose(any()) } returns Unit
+
+ // Mock message bus
+ val messageBus = mockk()
+ every { project.messageBus } returns messageBus
+ val mockConnection = mockk()
+ every { messageBus.connect(any()) } returns mockConnection
+ every { mockConnection.subscribe(any(), any()) } just runs
+
+ // Mock FileEditorManager
+ mockFileEditorManager = mockk()
+ every { mockFileEditorManager.openFiles } returns emptyArray()
+ every { project.getService(FileEditorManager::class.java) } returns mockFileEditorManager
+
+ sut = TextDocumentServiceHandler(project, mockk())
+ }
+
+ @Test
+ fun `didSave runs on beforeDocumentSaving`() = runTest {
+ // Create test document and file
+ val uri = URI.create("file:///test/path/file.txt")
+ val document = mockk {
+ every { text } returns "test content"
+ }
+
+ val file = createMockVirtualFile(uri)
+
+ // Mock FileDocumentManager
+ val fileDocumentManager = mockk {
+ every { getFile(document) } returns file
+ }
+
+ // Replace the FileDocumentManager instance
+ mockkStatic(FileDocumentManager::class) {
+ every { FileDocumentManager.getInstance() } returns fileDocumentManager
+
+ // Call the handler method
+ sut.beforeDocumentSaving(document)
+
+ // Verify the correct LSP method was called with matching parameters
+ val paramsSlot = slot()
+ verify { mockTextDocumentService.didSave(capture(paramsSlot)) }
+
+ with(paramsSlot.captured) {
+ assertThat(textDocument.uri).isEqualTo(normalizeFileUri(uri.toString()))
+ assertThat(text).isEqualTo("test content")
+ }
+ }
+ }
+
+ @Test
+ fun `didOpen runs on service init`() = runTest {
+ val uri = URI.create("file:///test/path/file.txt")
+ val content = "test content"
+ val file = createMockVirtualFile(uri, content)
+
+ every { mockFileEditorManager.openFiles } returns arrayOf(file)
+
+ sut = TextDocumentServiceHandler(project, mockk())
+
+ val paramsSlot = slot()
+ verify { mockTextDocumentService.didOpen(capture(paramsSlot)) }
+
+ with(paramsSlot.captured.textDocument) {
+ assertThat(this.uri).isEqualTo(normalizeFileUri(uri.toString()))
+ assertThat(text).isEqualTo(content)
+ assertThat(languageId).isEqualTo("java")
+ assertThat(version).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun `didOpen runs on fileOpened`() = runTest {
+ val uri = URI.create("file:///test/path/file.txt")
+ val content = "test content"
+ val file = createMockVirtualFile(uri, content)
+
+ sut.fileOpened(mockk(), file)
+
+ val paramsSlot = slot()
+ verify { mockTextDocumentService.didOpen(capture(paramsSlot)) }
+
+ with(paramsSlot.captured.textDocument) {
+ assertThat(this.uri).isEqualTo(normalizeFileUri(uri.toString()))
+ assertThat(text).isEqualTo(content)
+ assertThat(languageId).isEqualTo("java")
+ assertThat(version).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun `didClose runs on fileClosed`() = runTest {
+ val uri = URI.create("file:///test/path/file.txt")
+ val file = createMockVirtualFile(uri)
+
+ sut.fileClosed(mockk(), file)
+
+ val paramsSlot = slot()
+ verify { mockTextDocumentService.didClose(capture(paramsSlot)) }
+
+ assertThat(paramsSlot.captured.textDocument.uri).isEqualTo(normalizeFileUri(uri.toString()))
+ }
+
+ @Test
+ fun `didChange runs on content change events`() = runTest {
+ val uri = URI.create("file:///test/path/file.txt")
+ val document = mockk {
+ every { text } returns "changed content"
+ every { modificationStamp } returns 123L
+ }
+
+ val file = createMockVirtualFile(uri)
+
+ val changeEvent = mockk {
+ every { this@mockk.file } returns file
+ }
+
+ // Mock FileDocumentManager
+ val fileDocumentManager = mockk {
+ every { getCachedDocument(file) } returns document
+ }
+
+ mockkStatic(FileDocumentManager::class) {
+ every { FileDocumentManager.getInstance() } returns fileDocumentManager
+
+ // Call the handler method
+ sut.after(mutableListOf(changeEvent))
+ }
+
+ // Verify the correct LSP method was called with matching parameters
+ val paramsSlot = slot()
+ verify { mockTextDocumentService.didChange(capture(paramsSlot)) }
+
+ with(paramsSlot.captured) {
+ assertThat(textDocument.uri).isEqualTo(normalizeFileUri(uri.toString()))
+ assertThat(textDocument.version).isEqualTo(123)
+ assertThat(contentChanges[0].text).isEqualTo("changed content")
+ }
+ }
+
+ @Test
+ fun `didSave does not run when URI is empty`() = runTest {
+ val document = mockk()
+ val file = createMockVirtualFile(URI.create(""))
+
+ mockkObject(FileUriUtil) {
+ every { FileUriUtil.toUriString(file) } returns null
+
+ val fileDocumentManager = mockk {
+ every { getFile(document) } returns file
+ }
+
+ mockkStatic(FileDocumentManager::class) {
+ every { FileDocumentManager.getInstance() } returns fileDocumentManager
+
+ sut.beforeDocumentSaving(document)
+
+ verify(exactly = 0) { mockTextDocumentService.didSave(any()) }
+ }
+ }
+ }
+
+ @Test
+ fun `didSave does not run when file is null`() = runTest {
+ val document = mockk()
+
+ val fileDocumentManager = mockk {
+ every { getFile(document) } returns null
+ }
+
+ mockkStatic(FileDocumentManager::class) {
+ every { FileDocumentManager.getInstance() } returns fileDocumentManager
+
+ sut.beforeDocumentSaving(document)
+
+ verify(exactly = 0) { mockTextDocumentService.didSave(any()) }
+ }
+ }
+
+ @Test
+ fun `didChange ignores non-content change events`() = runTest {
+ val nonContentEvent = mockk() // Some other type of VFileEvent
+
+ sut.after(mutableListOf(nonContentEvent))
+
+ verify(exactly = 0) { mockTextDocumentService.didChange(any()) }
+ }
+
+ @Test
+ fun `didChange skips files without cached documents`() = runTest {
+ val uri = URI.create("file:///test/path/file.txt")
+ val path = mockk {
+ every { toUri() } returns uri
+ }
+ val file = mockk {
+ every { toNioPath() } returns path
+ }
+ val changeEvent = mockk {
+ every { this@mockk.file } returns file
+ }
+
+ val fileDocumentManager = mockk {
+ every { getCachedDocument(file) } returns null
+ }
+
+ mockkStatic(FileDocumentManager::class) {
+ every { FileDocumentManager.getInstance() } returns fileDocumentManager
+
+ sut.after(mutableListOf(changeEvent))
+
+ verify(exactly = 0) { mockTextDocumentService.didChange(any()) }
+ }
+ }
+
+ private fun createMockVirtualFile(
+ uri: URI,
+ content: String = "",
+ fileTypeName: String = "JAVA",
+ modificationStamp: Long = 1L,
+ ): VirtualFile {
+ val path = mockk {
+ every { toUri() } returns uri
+ }
+ val inputStream = content.byteInputStream()
+
+ val mockFileType = mockk